@asantemedia-org/atlas-copco-vt-storybook 1.0.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 (59) hide show
  1. package/.eslintrc.json +3 -0
  2. package/.nvmrc +1 -0
  3. package/.prettierignore +2 -0
  4. package/.prettierrc +8 -0
  5. package/.storybook/AtlasCopcoTheme.ts +14 -0
  6. package/.storybook/global.scss +15 -0
  7. package/.storybook/main.ts +125 -0
  8. package/.storybook/manager.ts +6 -0
  9. package/.storybook/preview-head.html +4 -0
  10. package/.storybook/preview.tsx +73 -0
  11. package/.storybook/types.d.ts +5 -0
  12. package/.vscode/settings.json +4 -0
  13. package/README.md +59 -0
  14. package/next.config.js +8 -0
  15. package/package.json +76 -0
  16. package/postcss.config.js +6 -0
  17. package/public/.gitkeep +0 -0
  18. package/public/assets/.gitkeep +0 -0
  19. package/public/fonts/.gitkeep +0 -0
  20. package/src/app/globals.css +13 -0
  21. package/src/app/layout.tsx +18 -0
  22. package/src/app/page.tsx +11 -0
  23. package/src/components/Button/Button.module.scss +77 -0
  24. package/src/components/Button/Button.stories.tsx +97 -0
  25. package/src/components/Button/Button.tsx +47 -0
  26. package/src/components/Button/index.ts +2 -0
  27. package/src/components/Image/Image.module.scss +75 -0
  28. package/src/components/Image/Image.tsx +114 -0
  29. package/src/components/Image/Image.types.ts +34 -0
  30. package/src/components/Image/index.ts +2 -0
  31. package/src/components/ProductCardDetails/ProductCardDetails.module.scss +129 -0
  32. package/src/components/ProductCardDetails/ProductCardDetails.stories.tsx +138 -0
  33. package/src/components/ProductCardDetails/ProductCardDetails.tsx +61 -0
  34. package/src/components/ProductCardDetails/index.ts +2 -0
  35. package/src/components/ProductCardHorizontal/ProductCardHorizontal.module.scss +93 -0
  36. package/src/components/ProductCardHorizontal/ProductCardHorizontal.stories.tsx +72 -0
  37. package/src/components/ProductCardHorizontal/ProductCardHorizontal.tsx +50 -0
  38. package/src/components/ProductCardHorizontal/index.ts +2 -0
  39. package/src/experience/algolia-dynamic-search/AlgoliaDynamicSearch.scss +135 -0
  40. package/src/experience/algolia-dynamic-search/AlgoliaDynamicSearch.stories.tsx +109 -0
  41. package/src/experience/algolia-dynamic-search/AlgoliaDynamicSearch.tsx +67 -0
  42. package/src/experience/algolia-dynamic-search/index.ts +2 -0
  43. package/src/experience/qr-form-journey/QrFormJourney.scss +37 -0
  44. package/src/experience/qr-form-journey/QrFormJourney.stories.tsx +134 -0
  45. package/src/experience/qr-form-journey/QrFormJourney.tsx +69 -0
  46. package/src/experience/qr-form-journey/index.ts +2 -0
  47. package/src/index.ts +19 -0
  48. package/src/stories/foundation/_components/StoryLayout.tsx +67 -0
  49. package/src/stories/introduction/Welcome.mdx +36 -0
  50. package/src/types/buttons.ts +4 -0
  51. package/src/types/cards.ts +37 -0
  52. package/src/utils/data/algolia-dynamic-widget-product-data.json +46 -0
  53. package/src/utils/styles/base.scss +100 -0
  54. package/src/utils/styles/global.scss +29 -0
  55. package/src/utils/styles/index.ts +1 -0
  56. package/src/utils/styles/typography.scss +60 -0
  57. package/tailwind.config.js +19 -0
  58. package/tsconfig.json +41 -0
  59. package/types/@asantemedia-org__edwardsvacuum-design-system.d.ts +8 -0
@@ -0,0 +1,47 @@
1
+ import React from "react";
2
+ import styles from "./Button.module.scss";
3
+ import classNames from "classnames";
4
+
5
+ export type ButtonVariant = "primary" | "secondary" | "outlined";
6
+
7
+ export interface ButtonProps {
8
+ children?: React.ReactNode;
9
+ variant?: ButtonVariant;
10
+ size?: "small" | "medium" | "large";
11
+ disabled?: boolean;
12
+ onClick?: (e?: React.MouseEvent<HTMLButtonElement>) => void;
13
+ type?: "button" | "submit" | "reset";
14
+ className?: string;
15
+ style?: React.CSSProperties;
16
+ }
17
+
18
+ export const Button: React.FC<ButtonProps> = ({
19
+ children,
20
+ variant = "primary",
21
+ size = "medium",
22
+ disabled = false,
23
+ onClick,
24
+ type = "button",
25
+ className,
26
+ ...props
27
+ }) => {
28
+ const buttonClasses = classNames(
29
+ styles.button,
30
+ styles[`button--${variant}`],
31
+ styles[`button--${size}`],
32
+ { [styles["button--disabled"]]: disabled },
33
+ className
34
+ );
35
+
36
+ return (
37
+ <button
38
+ type={type}
39
+ className={buttonClasses}
40
+ disabled={disabled}
41
+ onClick={onClick}
42
+ {...props}
43
+ >
44
+ {children}
45
+ </button>
46
+ );
47
+ };
@@ -0,0 +1,2 @@
1
+ export { Button } from "./Button";
2
+ export type { ButtonProps, ButtonVariant } from "./Button";
@@ -0,0 +1,75 @@
1
+ .imageContainer {
2
+ position: relative;
3
+ display: block;
4
+ overflow: hidden;
5
+ border-radius: 0.25rem;
6
+
7
+ &.loading {
8
+ .image {
9
+ opacity: 0;
10
+ }
11
+ }
12
+
13
+ &.error {
14
+ .image {
15
+ display: none;
16
+ }
17
+ }
18
+ }
19
+
20
+ .image {
21
+ display: block;
22
+ width: 100%;
23
+ height: 100%;
24
+ opacity: 1;
25
+ transition: opacity 0.3s ease-in-out;
26
+ }
27
+
28
+ .skeleton {
29
+ position: absolute;
30
+ inset: 0;
31
+ background-color: #e5e7eb;
32
+ overflow: hidden;
33
+ }
34
+
35
+ .shimmer {
36
+ position: absolute;
37
+ inset: 0;
38
+ transform: translateX(-100%);
39
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
40
+ animation: shimmer 1.5s infinite;
41
+ }
42
+
43
+ @keyframes shimmer {
44
+ 100% {
45
+ transform: translateX(100%);
46
+ }
47
+ }
48
+
49
+ .errorState {
50
+ position: absolute;
51
+ inset: 0;
52
+ display: flex;
53
+ flex-direction: column;
54
+ align-items: center;
55
+ justify-content: center;
56
+ gap: 0.5rem;
57
+ background-color: #f9fafb;
58
+ color: #9ca3af;
59
+ }
60
+
61
+ .errorIcon {
62
+ width: 2rem;
63
+ height: 2rem;
64
+ opacity: 0.6;
65
+ }
66
+
67
+ .errorText {
68
+ font-size: 0.75rem;
69
+ font-weight: 500;
70
+ }
71
+
72
+ .imageContainer:focus-visible {
73
+ outline: 2px solid #0062b0;
74
+ outline-offset: 2px;
75
+ }
@@ -0,0 +1,114 @@
1
+ import React, { useState, useCallback } from "react";
2
+ import { ImageProps } from "./Image.types";
3
+ import styles from "./Image.module.scss";
4
+
5
+ export const Image: React.FC<ImageProps> = ({
6
+ src,
7
+ alt,
8
+ width,
9
+ height,
10
+ loading = "lazy",
11
+ decoding = "async",
12
+ objectFit = "cover",
13
+ objectPosition = "center",
14
+ aspectRatio,
15
+ isDecorative = false,
16
+ fallbackSrc,
17
+ onLoad,
18
+ onError,
19
+ className = "",
20
+ srcSet,
21
+ sizes,
22
+ }) => {
23
+ const [isLoading, setIsLoading] = useState(true);
24
+ const [hasError, setHasError] = useState(false);
25
+ const [currentSrc, setCurrentSrc] = useState(src);
26
+
27
+ const handleLoad = useCallback(() => {
28
+ setIsLoading(false);
29
+ setHasError(false);
30
+ onLoad?.();
31
+ }, [onLoad]);
32
+
33
+ const handleError = useCallback(() => {
34
+ setIsLoading(false);
35
+ setHasError(true);
36
+ if (fallbackSrc && currentSrc !== fallbackSrc) {
37
+ setCurrentSrc(fallbackSrc);
38
+ setHasError(false);
39
+ setIsLoading(true);
40
+ } else {
41
+ onError?.();
42
+ }
43
+ }, [fallbackSrc, currentSrc, onError]);
44
+
45
+ const accessibilityProps = isDecorative
46
+ ? { alt: "", "aria-hidden": true as const, role: "presentation" as const }
47
+ : { alt };
48
+
49
+ const containerStyle: React.CSSProperties = {
50
+ ...(aspectRatio && { aspectRatio }),
51
+ ...(width && { width: typeof width === "number" ? `${width}px` : width }),
52
+ ...(height && { height: typeof height === "number" ? `${height}px` : height }),
53
+ };
54
+
55
+ const imageStyle: React.CSSProperties = {
56
+ objectFit,
57
+ objectPosition,
58
+ };
59
+
60
+ return (
61
+ <div
62
+ className={`${styles.imageContainer} ${className} ${isLoading ? styles.loading : ""} ${
63
+ hasError ? styles.error : ""
64
+ }`}
65
+ style={containerStyle}
66
+ >
67
+ {isLoading && (
68
+ <div className={styles.skeleton} aria-hidden="true">
69
+ <div className={styles.shimmer} />
70
+ </div>
71
+ )}
72
+ {hasError && !fallbackSrc ? (
73
+ <div className={styles.errorState} role="img" aria-label={alt || "Image failed to load"}>
74
+ <svg
75
+ xmlns="http://www.w3.org/2000/svg"
76
+ viewBox="0 0 24 24"
77
+ fill="none"
78
+ stroke="currentColor"
79
+ strokeWidth="1.5"
80
+ strokeLinecap="round"
81
+ strokeLinejoin="round"
82
+ className={styles.errorIcon}
83
+ aria-hidden="true"
84
+ >
85
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
86
+ <circle cx="8.5" cy="8.5" r="1.5" />
87
+ <polyline points="21 15 16 10 5 21" />
88
+ </svg>
89
+ <span className={styles.errorText}>Image unavailable</span>
90
+ </div>
91
+ ) : (
92
+ // Design system Image uses <img> for portability; next/image not required here
93
+ // eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text
94
+ <img
95
+ src={currentSrc}
96
+ alt={isDecorative ? "" : alt ?? ""}
97
+ {...(isDecorative && { "aria-hidden": true, role: "presentation" })}
98
+ width={width}
99
+ height={height}
100
+ loading={loading}
101
+ decoding={decoding}
102
+ onLoad={handleLoad}
103
+ onError={handleError}
104
+ className={styles.image}
105
+ style={imageStyle}
106
+ srcSet={srcSet}
107
+ sizes={sizes}
108
+ />
109
+ )}
110
+ </div>
111
+ );
112
+ };
113
+
114
+ export default Image;
@@ -0,0 +1,34 @@
1
+ export interface ImageProps {
2
+ /** Image source URL */
3
+ src: string;
4
+ /** Alt text for accessibility - required for non-decorative images */
5
+ alt: string;
6
+ /** Optional width */
7
+ width?: number | string;
8
+ /** Optional height */
9
+ height?: number | string;
10
+ /** Loading strategy */
11
+ loading?: "lazy" | "eager";
12
+ /** Decoding hint */
13
+ decoding?: "async" | "sync" | "auto";
14
+ /** Object fit behavior */
15
+ objectFit?: "contain" | "cover" | "fill" | "none" | "scale-down";
16
+ /** Object position */
17
+ objectPosition?: string;
18
+ /** Aspect ratio (e.g., "16/9", "4/3", "1/1") */
19
+ aspectRatio?: string;
20
+ /** Whether this is a decorative image (sets alt="" and aria-hidden) */
21
+ isDecorative?: boolean;
22
+ /** Fallback image URL if src fails to load */
23
+ fallbackSrc?: string;
24
+ /** Callback when image loads successfully */
25
+ onLoad?: () => void;
26
+ /** Callback when image fails to load */
27
+ onError?: () => void;
28
+ /** Additional CSS class */
29
+ className?: string;
30
+ /** Responsive image srcset */
31
+ srcSet?: string;
32
+ /** Responsive image sizes */
33
+ sizes?: string;
34
+ }
@@ -0,0 +1,2 @@
1
+ export { Image } from "./Image";
2
+ export type { ImageProps } from "./Image.types";
@@ -0,0 +1,129 @@
1
+ @use "../../utils/styles/base.scss" as *;
2
+
3
+ .productDetails {
4
+ :global(.cmp-card--type-product_details) {
5
+ display: grid;
6
+ grid-template-columns: repeat(1, 1fr);
7
+ grid-template-rows: auto;
8
+ @include respond-to($breakpoint-md) {
9
+ grid-template-columns: repeat(2, 1fr);
10
+ }
11
+ }
12
+
13
+ :global(.cmp-card--type-product_details__product-info) {
14
+ position: relative;
15
+ max-width: 480px;
16
+ }
17
+
18
+ :global(.cmp-card--type-product_details__header) {
19
+ margin-bottom: 1rem;
20
+ }
21
+
22
+ :global(.cmp-card--type-product_details__header__title) {
23
+ font-size: 21px;
24
+ font-weight: 700;
25
+ margin-bottom: 1rem;
26
+ text-transform: uppercase;
27
+ color: $primary-color;
28
+ @media (min-width: 768px) {
29
+ max-width: 325px;
30
+ }
31
+ }
32
+
33
+ :global(.cmp-card--type-product_details__header__product-code) {
34
+ font-size: 0.875rem;
35
+ color: $text-secondary;
36
+ margin-bottom: 1rem;
37
+ }
38
+
39
+ :global(.cmp-card--type-product_details__header__product-image) {
40
+ max-width: 100%;
41
+ height: auto;
42
+ }
43
+
44
+ :global(.cmp-card--type-product_details__spares-list) {
45
+ position: relative;
46
+ }
47
+
48
+ :global(.folding-spares-list) {
49
+ display: grid;
50
+ grid-template-columns: repeat(1, 1fr);
51
+ grid-template-rows: auto;
52
+ gap: 10px;
53
+ margin-left: 0;
54
+ padding-left: 0;
55
+ @include respond-to($breakpoint-md) {
56
+ grid-template-columns: repeat(2, 1fr);
57
+ }
58
+ li {
59
+ list-style: none;
60
+ margin-bottom: 0;
61
+ font-size: 14px;
62
+ }
63
+ }
64
+
65
+ :global(.folding-specification-list) {
66
+ margin-left: 0;
67
+ padding-left: 0;
68
+ display: grid;
69
+ grid-template-columns: repeat(1, 1fr);
70
+ grid-template-rows: auto;
71
+ gap: 10px;
72
+ @include respond-to($breakpoint-md) {
73
+ grid-template-columns: repeat(2, 1fr);
74
+ grid-template-areas: "list-item-1 list-item-2";
75
+ }
76
+ li {
77
+ list-style: none;
78
+ margin-bottom: 0.75rem;
79
+ font-size: 14px;
80
+ strong {
81
+ color: $atlascopco-grey-600;
82
+ }
83
+ & > *:first-child {
84
+ line-height: 2;
85
+ }
86
+ }
87
+ }
88
+
89
+ :global(.folding-specification-holder) {
90
+ margin-bottom: 1rem;
91
+ }
92
+
93
+ :global(.folding-spares-holder) {
94
+ margin-bottom: 1rem;
95
+ }
96
+
97
+ :global(.accordion-section) {
98
+ margin-bottom: 0.5rem;
99
+ &.open {
100
+ border-color: $primary-color;
101
+ }
102
+ }
103
+
104
+ :global(.accordion-section__header) {
105
+ display: flex;
106
+ justify-content: space-between;
107
+ align-items: center;
108
+ padding: 1rem 0;
109
+ cursor: pointer;
110
+ }
111
+
112
+ :global(.accordion-section__title) {
113
+ font-size: 1rem;
114
+ font-weight: 600;
115
+ color: $atlascopco-grey-600;
116
+ margin: 0;
117
+ }
118
+
119
+ :global(.accordion-section__icon) {
120
+ color: $text-primary !important;
121
+ svg {
122
+ color: inherit !important;
123
+ }
124
+ }
125
+
126
+ :global(.accordion-section__content) {
127
+ padding: 0 0 1rem 0;
128
+ }
129
+ }
@@ -0,0 +1,138 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { ProductCardDetails } from "./ProductCardDetails";
3
+
4
+ const meta: Meta<typeof ProductCardDetails> = {
5
+ title: "Components/Cards/ProductCardDetails",
6
+ component: ProductCardDetails,
7
+ tags: ["autodocs"],
8
+ parameters: {
9
+ layout: "padded",
10
+ },
11
+ };
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof ProductCardDetails>;
15
+
16
+ const sampleImage = "https://via.placeholder.com/400x300?text=Atlas+Copco+Product";
17
+
18
+ const sampleHit = {
19
+ objectID: "U20084880",
20
+ code: "U20084880",
21
+ name: "Pump Kit And Checklist",
22
+ title: "Pump Kit And Checklist",
23
+ image: sampleImage,
24
+ description:
25
+ "<h2>Kits</h2><p>Choose genuine Atlas Copco service kits for reliable operation of pumps when properly fitted.</p><ul><li>Genuine parts</li><li>Complete kit contents</li><li>Quality assured</li></ul>",
26
+ price: "£49.99",
27
+ priceValue: "0-49.99",
28
+ itemtype: "Spare Part",
29
+ powerTotalRated: "N/A",
30
+ stockLevelStatus: "In Stock",
31
+ url: "#",
32
+ spares: [
33
+ {
34
+ objectID: "spare-1",
35
+ code: "SP001",
36
+ name: "O-Ring Kit",
37
+ "img-product": sampleImage,
38
+ priceValue: "£12.99",
39
+ url: "#",
40
+ },
41
+ {
42
+ objectID: "spare-2",
43
+ code: "SP002",
44
+ name: "Gasket Set",
45
+ "img-product": sampleImage,
46
+ priceValue: "£24.99",
47
+ url: "#",
48
+ },
49
+ {
50
+ objectID: "spare-3",
51
+ code: "SP003",
52
+ name: "Bearing Assembly",
53
+ "img-product": sampleImage,
54
+ priceValue: "£89.99",
55
+ url: "#",
56
+ },
57
+ ],
58
+ };
59
+
60
+ const sampleFacets = [
61
+ { name: "itemtype", label: "Item Type" },
62
+ { name: "powerTotalRated", label: "Power Total Rated" },
63
+ { name: "stockLevelStatus", label: "Stock Level Status" },
64
+ ];
65
+
66
+ export const Default: Story = {
67
+ args: {
68
+ title: "Pump Kit And Checklist",
69
+ imageUrl: sampleImage,
70
+ code: "U20084880",
71
+ hit: sampleHit,
72
+ facets: sampleFacets,
73
+ },
74
+ };
75
+
76
+ export const WithSpares: Story = {
77
+ args: {
78
+ title: "GA 30+ - Oil-injected rotary screw compressor",
79
+ imageUrl: sampleImage,
80
+ code: "GA30-VT",
81
+ hit: {
82
+ ...sampleHit,
83
+ title: "GA 30+ - Oil-injected rotary screw compressor",
84
+ code: "GA30-VT",
85
+ description:
86
+ "<h2>Industrial Compressor</h2><p>The GA 30+ series offers reliable performance for demanding industrial applications.</p>",
87
+ itemtype: "Compressor",
88
+ powerTotalRated: "30 kW",
89
+ stockLevelStatus: "Available",
90
+ },
91
+ facets: sampleFacets,
92
+ },
93
+ };
94
+
95
+ export const WithoutSpares: Story = {
96
+ args: {
97
+ title: "Replacement Filter Element",
98
+ imageUrl: sampleImage,
99
+ code: "FE-001",
100
+ hit: {
101
+ objectID: "FE-001",
102
+ code: "FE-001",
103
+ name: "Replacement Filter Element",
104
+ title: "Replacement Filter Element",
105
+ image: sampleImage,
106
+ description: "<p>High-quality replacement filter for optimal performance.</p>",
107
+ price: "£35.00",
108
+ itemtype: "Filter",
109
+ stockLevelStatus: "In Stock",
110
+ spares: [],
111
+ },
112
+ facets: [
113
+ { name: "itemtype", label: "Item Type" },
114
+ { name: "stockLevelStatus", label: "Stock Level Status" },
115
+ { name: "description", label: "Description", returnsHTML: true },
116
+ ],
117
+ },
118
+ };
119
+
120
+ export const NoImage: Story = {
121
+ args: {
122
+ title: "Service Manual - Digital",
123
+ code: "SM-DIG-001",
124
+ hit: {
125
+ objectID: "SM-DIG-001",
126
+ code: "SM-DIG-001",
127
+ title: "Service Manual - Digital",
128
+ description: "<p>Comprehensive digital service manual for maintenance.</p>",
129
+ itemtype: "Documentation",
130
+ stockLevelStatus: "Available",
131
+ },
132
+ facets: [
133
+ { name: "itemtype", label: "Item Type" },
134
+ { name: "stockLevelStatus", label: "Availability" },
135
+ { name: "description", label: "Description", returnsHTML: true },
136
+ ],
137
+ },
138
+ };
@@ -0,0 +1,61 @@
1
+ import React from "react";
2
+ import { ProductDetailsCard as ProductDetailsCardBase } from "@asantemedia-org/edwardsvacuum-design-system";
3
+ import { ProductCardHorizontalParts } from "../ProductCardHorizontal/ProductCardHorizontal";
4
+ import { ProductCardDetailsProps } from "../../types/cards";
5
+ import styles from "./ProductCardDetails.module.scss";
6
+
7
+ const SpareCardAdapter = (props: any) => {
8
+ const { spare, ...rest } = props;
9
+ return (
10
+ <ProductCardHorizontalParts
11
+ url={spare?.url || spare?.link || "#"}
12
+ title={spare?.name || spare?.title || ""}
13
+ description={spare?.description || ""}
14
+ price={spare?.priceValue || spare?.price || ""}
15
+ imageUrl={spare?.["img-product"] || spare?.image || ""}
16
+ button={{
17
+ label: rest.cta || "View Product",
18
+ onClick: () => window.open(spare?.url || spare?.link || "#", "_blank"),
19
+ }}
20
+ />
21
+ );
22
+ };
23
+
24
+ export const ProductCardDetails: React.FC<ProductCardDetailsProps> = ({
25
+ id,
26
+ imageUrl,
27
+ title,
28
+ description,
29
+ price,
30
+ code,
31
+ facets,
32
+ hit,
33
+ className = "",
34
+ ProductCardComponent,
35
+ }) => {
36
+ const hitData = hit || {
37
+ id,
38
+ title,
39
+ description,
40
+ price,
41
+ code,
42
+ image: imageUrl,
43
+ };
44
+
45
+ return (
46
+ <div className={styles.productDetails}>
47
+ <ProductDetailsCardBase
48
+ className={`${className}`}
49
+ title={title}
50
+ imageUrl={imageUrl}
51
+ imageAlt={title}
52
+ hit={hitData}
53
+ facets={facets}
54
+ usePlainClasses={true}
55
+ ProductCardComponent={ProductCardComponent || SpareCardAdapter}
56
+ />
57
+ </div>
58
+ );
59
+ };
60
+
61
+ export default ProductCardDetails;
@@ -0,0 +1,2 @@
1
+ export { ProductCardDetails } from "./ProductCardDetails";
2
+ export type { ProductCardDetailsProps } from "../../types/cards";
@@ -0,0 +1,93 @@
1
+ @use "../../utils/styles/base.scss" as *;
2
+
3
+ .productCard {
4
+ display: block;
5
+ border: 1px solid transparent;
6
+ h3,
7
+ p {
8
+ margin: 0;
9
+ }
10
+ &:focus-visible {
11
+ outline: 1px solid $primary-color;
12
+ outline-offset: 1px;
13
+ }
14
+ }
15
+
16
+ .productWrapper {
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: $spacing-sm;
20
+ padding: calc($spacing-md * 0.5) !important;
21
+ background-color: white;
22
+ box-shadow: 2.7px 2.7px 5px 0 rgba(0, 0, 0, 0.1);
23
+
24
+ &.withImage {
25
+ display: grid;
26
+ grid-template-columns: 75px repeat(2, minmax(80px, 1fr));
27
+ grid-template-rows: repeat(3, auto);
28
+ grid-template-areas:
29
+ "image description description"
30
+ "image description description"
31
+ "image cta cta";
32
+ row-gap: calc($spacing-xs * 0.5);
33
+ .productTitle {
34
+ font-size: clamp(12px, 1.25vw, 14px);
35
+ }
36
+ .productDescription {
37
+ font-size: clamp(12px, 1.25vw, 14px);
38
+ }
39
+ .productPrice {
40
+ font-size: clamp(12px, 1.25vw, 14px);
41
+ }
42
+ }
43
+ }
44
+
45
+ .productImage {
46
+ grid-area: image;
47
+ flex-shrink: 0;
48
+ overflow: hidden;
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ }
53
+
54
+ .productTitle,
55
+ h3.productTitle {
56
+ margin: 0 0 0.25rem 0;
57
+ font-size: clamp(12px, 1.25vw, 14px);
58
+ font-weight: 600;
59
+ color: $text-primary;
60
+ line-height: 1.4;
61
+ }
62
+
63
+ .productDescription {
64
+ margin: 0 0 0.5rem 0;
65
+ font-size: clamp(14px, 1.25vw, 1.5rem);
66
+ color: $text-secondary;
67
+ line-height: 1.5;
68
+ display: -webkit-box;
69
+ -webkit-line-clamp: 2;
70
+ -webkit-box-orient: vertical;
71
+ overflow: hidden;
72
+ }
73
+
74
+ .productPrice {
75
+ margin: 0;
76
+ font-size: clamp(14px, 1.25vw, 1.5rem);
77
+ font-weight: 700;
78
+ color: $text-primary;
79
+ }
80
+
81
+ .productTitleDescriptionWrapper {
82
+ grid-area: description;
83
+ display: flex;
84
+ flex-direction: column;
85
+ gap: 0.25rem;
86
+ }
87
+
88
+ .productPriceCtaWrapper {
89
+ grid-area: cta;
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: space-between;
93
+ }