@windstream/react-shared-components 0.2.57 → 0.2.58
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/dist/contentful/index.d.ts +15 -1
- package/dist/contentful/index.esm.js +4 -4
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +4 -4
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/next/index.esm.js +1 -17
- package/dist/next/index.esm.js.map +1 -1
- package/dist/next/index.js +1 -17
- package/dist/next/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/brand-button/index.tsx +2 -2
- package/src/components/next-image/index.tsx +1 -1
- package/src/components/video-link/contentful-video-embed.tsx +1 -1
- package/src/components/video-link/index.tsx +1 -1
- package/src/components/video-link/vimeo-embed.tsx +1 -1
- package/src/components/video-link/youtube-embed.tsx +1 -1
- package/src/contentful/blocks/button/index.tsx +1 -0
- package/src/contentful/blocks/cart-retention-banner/CartRetentionBanner.stories.tsx +89 -1
- package/src/contentful/blocks/cart-retention-banner/index.test.tsx +85 -0
- package/src/contentful/blocks/cart-retention-banner/index.tsx +222 -39
- package/src/contentful/blocks/cart-retention-banner/types.ts +34 -0
- package/src/contentful/blocks/modal/index.tsx +14 -3
- package/src/contentful/blocks/shape-background-wrapper/index.test.tsx +17 -1
- package/src/contentful/blocks/shape-background-wrapper/index.tsx +30 -0
- package/src/contentful/index.ts +5 -0
|
@@ -45,6 +45,13 @@ jest.mock("@shared/utils/contentful/to-document", () => ({
|
|
|
45
45
|
toDocument: (json: any) => json,
|
|
46
46
|
}));
|
|
47
47
|
|
|
48
|
+
jest.mock("@shared/components/next-image", () => ({
|
|
49
|
+
NextImage: ({ src, alt, ...props }: any) => (
|
|
50
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
51
|
+
<img src={src} alt={alt} data-testid="banner-image" {...props} />
|
|
52
|
+
),
|
|
53
|
+
}));
|
|
54
|
+
|
|
48
55
|
describe("CartRetentionBanner", () => {
|
|
49
56
|
const defaultFields: TypeCartRetentionBannerFields = {
|
|
50
57
|
entryTitle: "Cart Banner",
|
|
@@ -406,4 +413,82 @@ describe("CartRetentionBanner", () => {
|
|
|
406
413
|
expect(screen.queryByTestId("rich-text")).not.toBeInTheDocument();
|
|
407
414
|
});
|
|
408
415
|
});
|
|
416
|
+
|
|
417
|
+
describe("image and layout options", () => {
|
|
418
|
+
it("does not render an image when none is provided", () => {
|
|
419
|
+
render(<CartRetentionBanner fields={defaultFields} isInPopupContainer={true} />);
|
|
420
|
+
expect(screen.queryByTestId("banner-image")).not.toBeInTheDocument();
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("renders the image when provided", () => {
|
|
424
|
+
const fields = {
|
|
425
|
+
...defaultFields,
|
|
426
|
+
image: "https://example.com/image.jpg",
|
|
427
|
+
imageAltText: "Promo image",
|
|
428
|
+
};
|
|
429
|
+
render(<CartRetentionBanner fields={fields} isInPopupContainer={true} />);
|
|
430
|
+
const image = screen.getByTestId("banner-image");
|
|
431
|
+
expect(image).toHaveAttribute("src", "https://example.com/image.jpg");
|
|
432
|
+
expect(image).toHaveAttribute("alt", "Promo image");
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("lays the image out beside the text for left/right imagePosition", () => {
|
|
436
|
+
const fields = {
|
|
437
|
+
...defaultFields,
|
|
438
|
+
image: "https://example.com/image.jpg",
|
|
439
|
+
imagePosition: "left" as const,
|
|
440
|
+
};
|
|
441
|
+
render(<CartRetentionBanner fields={fields} isInPopupContainer={true} />);
|
|
442
|
+
expect(
|
|
443
|
+
document.querySelector(".cart-retention-banner-image")
|
|
444
|
+
).toBeInTheDocument();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("applies the backgroundColor class to the popup container", () => {
|
|
448
|
+
const fields = { ...defaultFields, backgroundColor: "navy" as const };
|
|
449
|
+
render(<CartRetentionBanner fields={fields} isInPopupContainer={true} />);
|
|
450
|
+
const popupBody = document.querySelector(".pop-up-body");
|
|
451
|
+
expect(popupBody?.className).toContain("bg-[#00002D]");
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("passes the backgroundColor class through to the Modal", () => {
|
|
455
|
+
const fields = { ...defaultFields, backgroundColor: "cream500" as const };
|
|
456
|
+
render(<CartRetentionBanner fields={fields} />);
|
|
457
|
+
expect(screen.getByTestId("modal")).toBeInTheDocument();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("does not render an accent shape when imageAccentColor is not provided", () => {
|
|
461
|
+
const fields = {
|
|
462
|
+
...defaultFields,
|
|
463
|
+
image: "https://example.com/image.jpg",
|
|
464
|
+
imagePosition: "left" as const,
|
|
465
|
+
};
|
|
466
|
+
render(<CartRetentionBanner fields={fields} isInPopupContainer={true} />);
|
|
467
|
+
expect(document.querySelector(".shape-bg-svg")).not.toBeInTheDocument();
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("renders the accent shape behind the side-image layout when imageAccentColor is supported", () => {
|
|
471
|
+
const fields = {
|
|
472
|
+
...defaultFields,
|
|
473
|
+
image: "https://example.com/image.jpg",
|
|
474
|
+
imagePosition: "left" as const,
|
|
475
|
+
imageAccentColor: "yellow" as const,
|
|
476
|
+
};
|
|
477
|
+
render(<CartRetentionBanner fields={fields} isInPopupContainer={true} />);
|
|
478
|
+
const shape = document.querySelector(".shape-bg-svg");
|
|
479
|
+
expect(shape).toBeInTheDocument();
|
|
480
|
+
expect(shape?.getAttribute("class")).toContain("text-[#F5FF1E]");
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("does not render an accent shape for a color ShapeBackgroundWrapper doesn't support", () => {
|
|
484
|
+
const fields = {
|
|
485
|
+
...defaultFields,
|
|
486
|
+
image: "https://example.com/image.jpg",
|
|
487
|
+
imagePosition: "right" as const,
|
|
488
|
+
imageAccentColor: "cream500" as const,
|
|
489
|
+
};
|
|
490
|
+
render(<CartRetentionBanner fields={fields} isInPopupContainer={true} />);
|
|
491
|
+
expect(document.querySelector(".shape-bg-svg")).not.toBeInTheDocument();
|
|
492
|
+
});
|
|
493
|
+
});
|
|
409
494
|
});
|
|
@@ -3,12 +3,46 @@
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import type { TypeCartRetentionBannerFields } from "./types";
|
|
5
5
|
|
|
6
|
+
import { NextImage } from "@shared/components/next-image";
|
|
6
7
|
import { Text } from "@shared/components/text";
|
|
7
8
|
import { Button } from "@shared/contentful/blocks/button";
|
|
8
9
|
import { Modal } from "@shared/contentful/blocks/modal";
|
|
10
|
+
import { ShapeBackgroundWrapper } from "@shared/contentful/blocks/shape-background-wrapper";
|
|
9
11
|
import { renderContentfulRichText } from "@shared/hooks/contentful/use-contentful-rich-text";
|
|
12
|
+
import { cx } from "@shared/utils";
|
|
10
13
|
import { toDocument } from "@shared/utils/contentful/to-document";
|
|
11
14
|
|
|
15
|
+
// Literal Tailwind classes (JIT only picks up literal tokens found in source).
|
|
16
|
+
const backgroundColorClassMap: Record<
|
|
17
|
+
NonNullable<TypeCartRetentionBannerFields["backgroundColor"]>,
|
|
18
|
+
string
|
|
19
|
+
> = {
|
|
20
|
+
white: "bg-white",
|
|
21
|
+
navy: "bg-[#00002D] text-white",
|
|
22
|
+
green: "bg-[#26B170] text-white",
|
|
23
|
+
blue: "bg-[#07B2E2] text-white",
|
|
24
|
+
purple: "bg-[#931D69] text-white",
|
|
25
|
+
yellow: "bg-[#F5FF1E]",
|
|
26
|
+
cream500: "bg-[#FFFEEF]",
|
|
27
|
+
gray100: "bg-bg-fill-secondary",
|
|
28
|
+
transparent: "",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ShapeBackgroundWrapper only supports this subset of the accent color palette.
|
|
32
|
+
const shapeBackgroundThemeKeys = [
|
|
33
|
+
"blue",
|
|
34
|
+
"green",
|
|
35
|
+
"yellow",
|
|
36
|
+
"purple",
|
|
37
|
+
"white",
|
|
38
|
+
"navy",
|
|
39
|
+
] as const;
|
|
40
|
+
type ShapeBackgroundThemeKey = (typeof shapeBackgroundThemeKeys)[number];
|
|
41
|
+
const isShapeBackgroundThemeKey = (
|
|
42
|
+
color?: TypeCartRetentionBannerFields["imageAccentColor"]
|
|
43
|
+
): color is ShapeBackgroundThemeKey =>
|
|
44
|
+
!!color && (shapeBackgroundThemeKeys as readonly string[]).includes(color);
|
|
45
|
+
|
|
12
46
|
const CartRetentionBanner: React.FC<{
|
|
13
47
|
fields: TypeCartRetentionBannerFields;
|
|
14
48
|
sys?: any;
|
|
@@ -25,7 +59,27 @@ const CartRetentionBanner: React.FC<{
|
|
|
25
59
|
checkShouldShowBanner,
|
|
26
60
|
onBannerShown,
|
|
27
61
|
}) => {
|
|
28
|
-
const {
|
|
62
|
+
const {
|
|
63
|
+
anchorId,
|
|
64
|
+
mainHeading,
|
|
65
|
+
description,
|
|
66
|
+
cta,
|
|
67
|
+
image,
|
|
68
|
+
imageAltText,
|
|
69
|
+
imagePosition = "center",
|
|
70
|
+
backgroundColor,
|
|
71
|
+
imageAccentColor,
|
|
72
|
+
showImageAsIcon = false,
|
|
73
|
+
Logo,
|
|
74
|
+
} = fields;
|
|
75
|
+
|
|
76
|
+
const hasSideImage = Boolean(image) && imagePosition !== "center";
|
|
77
|
+
const isRightSide = imagePosition === "right";
|
|
78
|
+
const backgroundColorClassName = backgroundColor
|
|
79
|
+
? (backgroundColorClassMap[backgroundColor] ?? "")
|
|
80
|
+
: "";
|
|
81
|
+
// Decorative shape sits behind the whole card, both layouts.
|
|
82
|
+
const showAccentShape = isShapeBackgroundThemeKey(imageAccentColor);
|
|
29
83
|
|
|
30
84
|
const [modalOpen, setModalOpen] = useState<boolean>(false);
|
|
31
85
|
|
|
@@ -50,30 +104,167 @@ const CartRetentionBanner: React.FC<{
|
|
|
50
104
|
onContinueCheckout?.();
|
|
51
105
|
};
|
|
52
106
|
|
|
107
|
+
const renderLogo = () => {
|
|
108
|
+
if (!Logo) return null;
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className="cart-retention-banner-logo relative mx-auto mb-4 h-12 w-40">
|
|
112
|
+
<NextImage
|
|
113
|
+
src={Logo}
|
|
114
|
+
alt={mainHeading ?? ""}
|
|
115
|
+
fill={true}
|
|
116
|
+
sizes="160px"
|
|
117
|
+
className="object-contain"
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const renderImage = () => {
|
|
124
|
+
if (!image) return null;
|
|
125
|
+
|
|
126
|
+
if (showImageAsIcon) {
|
|
127
|
+
return (
|
|
128
|
+
<div className="cart-retention-banner-image relative mx-auto mb-4 h-[87px] w-[93px] overflow-hidden md:h-[193px] md:w-[207px]">
|
|
129
|
+
<NextImage
|
|
130
|
+
src={image}
|
|
131
|
+
alt={imageAltText ?? ""}
|
|
132
|
+
fill={true}
|
|
133
|
+
sizes="(min-width: 768px) 207px, 93px"
|
|
134
|
+
className="object-contain"
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (hasSideImage) {
|
|
141
|
+
// Mobile: full-width photo on top. md+: fixed 358px column filling the modal's height.
|
|
142
|
+
return (
|
|
143
|
+
<div className="cart-retention-banner-image relative md:h-full">
|
|
144
|
+
<div className="relative h-[258px] w-full overflow-hidden rounded-[20px] md:h-[400px]">
|
|
145
|
+
<NextImage
|
|
146
|
+
src={image}
|
|
147
|
+
alt={imageAltText ?? ""}
|
|
148
|
+
fill={true}
|
|
149
|
+
sizes="(min-width: 768px) 314px, 100vw"
|
|
150
|
+
className="object-cover object-top"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div className="cart-retention-banner-image relative mb-4 aspect-[2/1] w-full overflow-hidden rounded-[20px]">
|
|
159
|
+
<NextImage
|
|
160
|
+
src={image}
|
|
161
|
+
alt={imageAltText ?? ""}
|
|
162
|
+
fill={true}
|
|
163
|
+
sizes="(min-width: 640px) 400px, 90vw"
|
|
164
|
+
className="object-cover"
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const renderTextContent = () => (
|
|
171
|
+
<>
|
|
172
|
+
<Text
|
|
173
|
+
className={cx(
|
|
174
|
+
"heading2",
|
|
175
|
+
hasSideImage ? "text-center md:text-left" : "text-center"
|
|
176
|
+
)}
|
|
177
|
+
>
|
|
178
|
+
{mainHeading || "Welcome back. Let's finish your order"}
|
|
179
|
+
</Text>
|
|
180
|
+
{description &&
|
|
181
|
+
renderContentfulRichText(
|
|
182
|
+
toDocument(description?.json),
|
|
183
|
+
false,
|
|
184
|
+
cx("body2", hasSideImage ? "text-center md:text-left" : "text-center")
|
|
185
|
+
)}
|
|
186
|
+
<div
|
|
187
|
+
className={cx(
|
|
188
|
+
"flex w-full pt-5",
|
|
189
|
+
hasSideImage ? "justify-center md:justify-start" : "justify-center"
|
|
190
|
+
)}
|
|
191
|
+
>
|
|
192
|
+
<Button
|
|
193
|
+
onClick={continueCheckout}
|
|
194
|
+
buttonLabel={cta?.buttonLabel ?? "Continue checkout"}
|
|
195
|
+
disableAnimation={cta?.disableAnimation ?? true}
|
|
196
|
+
animationType={cta?.animationType}
|
|
197
|
+
fullWidth={true}
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
</>
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const bodyContent = hasSideImage ? (
|
|
204
|
+
<>
|
|
205
|
+
{renderLogo()}
|
|
206
|
+
<div
|
|
207
|
+
className={cx(
|
|
208
|
+
"flex flex-col md:grid md:gap-6",
|
|
209
|
+
isRightSide ? "md:grid-cols-[1fr_314px]" : "md:grid-cols-[314px_1fr]"
|
|
210
|
+
)}
|
|
211
|
+
>
|
|
212
|
+
<div
|
|
213
|
+
className={cx(
|
|
214
|
+
"order-1",
|
|
215
|
+
isRightSide && "md:order-2",
|
|
216
|
+
"md:max-w-[314px]",
|
|
217
|
+
showImageAsIcon && "flex items-center justify-center"
|
|
218
|
+
)}
|
|
219
|
+
>
|
|
220
|
+
{renderImage()}
|
|
221
|
+
</div>
|
|
222
|
+
<div
|
|
223
|
+
className={cx(
|
|
224
|
+
"order-2 flex flex-col justify-center gap-8 pt-5 md:gap-10 md:pt-0",
|
|
225
|
+
isRightSide ? "md:order-1" : ""
|
|
226
|
+
)}
|
|
227
|
+
>
|
|
228
|
+
{renderTextContent()}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</>
|
|
232
|
+
) : (
|
|
233
|
+
<>
|
|
234
|
+
{renderLogo()}
|
|
235
|
+
{renderImage()}
|
|
236
|
+
{renderTextContent()}
|
|
237
|
+
</>
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const wrappedBodyContent = (
|
|
241
|
+
<ShapeBackgroundWrapper
|
|
242
|
+
path="path5"
|
|
243
|
+
fill={imageAccentColor as ShapeBackgroundThemeKey}
|
|
244
|
+
background="transparent"
|
|
245
|
+
className={cx(
|
|
246
|
+
"rounded-3xl px-6 pb-8 md:p-6",
|
|
247
|
+
showImageAsIcon && "md:!py-[95px]",
|
|
248
|
+
Logo ? "pt-6" : "pt-13"
|
|
249
|
+
)}
|
|
250
|
+
show={showAccentShape}
|
|
251
|
+
>
|
|
252
|
+
{bodyContent}
|
|
253
|
+
</ShapeBackgroundWrapper>
|
|
254
|
+
);
|
|
255
|
+
|
|
53
256
|
if (isInPopupContainer) {
|
|
54
257
|
return (
|
|
55
258
|
<div
|
|
56
|
-
className=
|
|
259
|
+
className={cx(
|
|
260
|
+
"pop-up-body w-full",
|
|
261
|
+
hasSideImage ? "overflow-hidden" : "mb-5 mt-10",
|
|
262
|
+
backgroundColorClassName
|
|
263
|
+
)}
|
|
57
264
|
data-section-type={"cart-retention-banner"}
|
|
58
265
|
data-section-index={"0"}
|
|
59
266
|
>
|
|
60
|
-
|
|
61
|
-
{mainHeading || "Welcome back. Let’s finish your order"}
|
|
62
|
-
</Text>
|
|
63
|
-
{description &&
|
|
64
|
-
renderContentfulRichText(
|
|
65
|
-
toDocument(description?.json),
|
|
66
|
-
false,
|
|
67
|
-
"body1 my-4 text-center"
|
|
68
|
-
)}
|
|
69
|
-
<div className="flex w-full justify-center">
|
|
70
|
-
<Button
|
|
71
|
-
onClick={continueCheckout}
|
|
72
|
-
buttonLabel={cta?.buttonLabel ?? "Continue checkout"}
|
|
73
|
-
disableAnimation={cta?.disableAnimation ?? true}
|
|
74
|
-
animationType={cta?.animationType}
|
|
75
|
-
/>
|
|
76
|
-
</div>
|
|
267
|
+
{wrappedBodyContent}
|
|
77
268
|
</div>
|
|
78
269
|
);
|
|
79
270
|
}
|
|
@@ -83,28 +274,20 @@ const CartRetentionBanner: React.FC<{
|
|
|
83
274
|
size="sm"
|
|
84
275
|
isOpen={modalOpen}
|
|
85
276
|
onRequestClose={handleClose}
|
|
86
|
-
bodyClassName=
|
|
277
|
+
bodyClassName={cx(
|
|
278
|
+
hasSideImage
|
|
279
|
+
? cx("overflow-hidden md:!max-w-[828px]", !showAccentShape && "p-0")
|
|
280
|
+
: "flex items-center justify-center",
|
|
281
|
+
"min-[370px] p-[0px]",
|
|
282
|
+
backgroundColorClassName
|
|
283
|
+
)}
|
|
87
284
|
type="cart-retention-banner"
|
|
88
285
|
>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
</
|
|
93
|
-
|
|
94
|
-
renderContentfulRichText(
|
|
95
|
-
toDocument(description?.json),
|
|
96
|
-
false,
|
|
97
|
-
"body1 my-4 text-center"
|
|
98
|
-
)}
|
|
99
|
-
<div className="flex w-full justify-center">
|
|
100
|
-
<Button
|
|
101
|
-
onClick={continueCheckout}
|
|
102
|
-
buttonLabel={cta?.buttonLabel ?? "Continue checkout"}
|
|
103
|
-
disableAnimation={cta?.disableAnimation ?? true}
|
|
104
|
-
animationType={cta?.animationType}
|
|
105
|
-
/>
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
286
|
+
{hasSideImage ? (
|
|
287
|
+
wrappedBodyContent
|
|
288
|
+
) : (
|
|
289
|
+
<div className="pop-up-body mb-5 w-full">{wrappedBodyContent}</div>
|
|
290
|
+
)}
|
|
108
291
|
</Modal>
|
|
109
292
|
</section>
|
|
110
293
|
);
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import type { AnimationType } from "@shared/components/animation-wrapper";
|
|
2
2
|
|
|
3
|
+
export type CartRetentionBannerImagePosition = "left" | "right" | "center";
|
|
4
|
+
|
|
5
|
+
export type CartRetentionBannerBackgroundColor =
|
|
6
|
+
| "white"
|
|
7
|
+
| "navy"
|
|
8
|
+
| "green"
|
|
9
|
+
| "blue"
|
|
10
|
+
| "purple"
|
|
11
|
+
| "yellow"
|
|
12
|
+
| "cream500"
|
|
13
|
+
| "gray100"
|
|
14
|
+
| "transparent";
|
|
15
|
+
|
|
16
|
+
export type CartRetentionBannerImageAccentColor =
|
|
17
|
+
| "white"
|
|
18
|
+
| "navy"
|
|
19
|
+
| "green"
|
|
20
|
+
| "blue"
|
|
21
|
+
| "purple"
|
|
22
|
+
| "yellow"
|
|
23
|
+
| "cream500"
|
|
24
|
+
| "gray100";
|
|
25
|
+
|
|
3
26
|
export interface TypeCartRetentionBannerFields {
|
|
4
27
|
entryTitle?: string;
|
|
5
28
|
anchorId?: string;
|
|
@@ -13,6 +36,17 @@ export interface TypeCartRetentionBannerFields {
|
|
|
13
36
|
animationType?: AnimationType | AnimationType[];
|
|
14
37
|
[key: string]: any;
|
|
15
38
|
};
|
|
39
|
+
/** Image URL rendered alongside the banner content. */
|
|
40
|
+
image?: string;
|
|
41
|
+
imageAltText?: string;
|
|
42
|
+
/** Position of the image relative to the text content; "center" (default) preserves the original image-above-heading layout. */
|
|
43
|
+
imagePosition?: CartRetentionBannerImagePosition;
|
|
44
|
+
/** Background color token applied to the banner/modal content area. */
|
|
45
|
+
backgroundColor?: CartRetentionBannerBackgroundColor;
|
|
46
|
+
/** Accent color for the decorative shape peeking from behind the image's outer corner; omit for no accent. */
|
|
47
|
+
imageAccentColor?: CartRetentionBannerImageAccentColor;
|
|
48
|
+
Logo?: string;
|
|
49
|
+
showImageAsIcon?: boolean;
|
|
16
50
|
}
|
|
17
51
|
|
|
18
52
|
export type TypeCartRetentionBanner = TypeCartRetentionBannerFields;
|
|
@@ -59,7 +59,13 @@ export const Modal: React.FC<ExtendedModalProps> = props => {
|
|
|
59
59
|
<Dialog.Content
|
|
60
60
|
onOpenAutoFocus={e => e.preventDefault()}
|
|
61
61
|
className={cx(
|
|
62
|
-
|
|
62
|
+
// `rounded-modal` isn't a registered Tailwind token - use an explicit radius.
|
|
63
|
+
"relative mx-auto w-full overflow-hidden rounded-[24px] shadow-drop focus:outline-none",
|
|
64
|
+
// Only fall back to the default surface color / padding when
|
|
65
|
+
// the caller hasn't supplied its own override (avoids two
|
|
66
|
+
// competing `bg-*`/`p-*` classes racing on CSS order).
|
|
67
|
+
!/(^|\s)bg-/.test(bodyClassName ?? "") && "bg-bg",
|
|
68
|
+
!/(^|\s)p-/.test(bodyClassName ?? "") && "p-[25px]",
|
|
63
69
|
bodyClassName
|
|
64
70
|
)}
|
|
65
71
|
style={{ maxWidth: sizeToPixel[size || "lg"] }}
|
|
@@ -68,12 +74,17 @@ export const Modal: React.FC<ExtendedModalProps> = props => {
|
|
|
68
74
|
<Dialog.Close asChild={true}>
|
|
69
75
|
<Button
|
|
70
76
|
showButtonAs="unstyled"
|
|
71
|
-
|
|
77
|
+
// Pin icon color: button always sits on a light bg-bg
|
|
78
|
+
// square, so it must not inherit a light text color (e.g.
|
|
79
|
+
// text-white) from a dark bodyClassName like navy.
|
|
80
|
+
// z-10: keeps the button clickable above positioned body
|
|
81
|
+
// content (e.g. ShapeBackgroundWrapper's decorative layer).
|
|
82
|
+
buttonClassName="absolute right-0 top-0 z-10 mr-2 mt-2"
|
|
72
83
|
>
|
|
73
84
|
<MaterialIcon name="close" />
|
|
74
85
|
</Button>
|
|
75
86
|
</Dialog.Close>
|
|
76
|
-
<div className="custom-modal-body max-h-[85vh] overflow-
|
|
87
|
+
<div className="custom-modal-body max-h-[85vh] overflow-y-auto scroll-smooth [scrollbar-color:rgba(0,0,0,0.2)_transparent] [scrollbar-width:thin] [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-black/20 hover:[&::-webkit-scrollbar-thumb]:bg-black/30 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar]:w-1.5">
|
|
77
88
|
{title ? (
|
|
78
89
|
<Text
|
|
79
90
|
className={cx(
|
|
@@ -130,9 +130,25 @@ describe("ShapeBackgroundWrapper", () => {
|
|
|
130
130
|
expect(svgs[0]).toHaveAttribute("height", "3666");
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
it("renders both mobile and desktop path5 SVGs when path='path5'", () => {
|
|
134
|
+
const { container } = render(
|
|
135
|
+
<ShapeBackgroundWrapper path="path5" background="white">
|
|
136
|
+
<span>Path5</span>
|
|
137
|
+
</ShapeBackgroundWrapper>
|
|
138
|
+
);
|
|
139
|
+
const svgs = container.querySelectorAll("svg");
|
|
140
|
+
expect(svgs).toHaveLength(2);
|
|
141
|
+
expect(svgs[0]).toHaveAttribute("width", "367");
|
|
142
|
+
expect(svgs[0]).toHaveAttribute("height", "644");
|
|
143
|
+
expect(svgs[0]).toHaveClass("md:hidden");
|
|
144
|
+
expect(svgs[1]).toHaveAttribute("width", "367");
|
|
145
|
+
expect(svgs[1]).toHaveAttribute("height", "448");
|
|
146
|
+
expect(svgs[1]).toHaveClass("md:block");
|
|
147
|
+
});
|
|
148
|
+
|
|
133
149
|
it("does not render SVG for unrecognized path value", () => {
|
|
134
150
|
const { container } = render(
|
|
135
|
-
<ShapeBackgroundWrapper path={"
|
|
151
|
+
<ShapeBackgroundWrapper path={"path6" as any} background="white">
|
|
136
152
|
<span>Unknown</span>
|
|
137
153
|
</ShapeBackgroundWrapper>
|
|
138
154
|
);
|
|
@@ -118,6 +118,36 @@ export const ShapeBackgroundWrapper: React.FC<ShapeBackgroundWrapperProps> = ({
|
|
|
118
118
|
<path d="M1662.93 3664.31C2153.52 3637.23 2442.1 3474.65 2872.31 2646.41C3278.25 1865.1 4009.1 647.091 3634.26 264.381C3417.79 27.9951 3025.37 31.7826 2716.76 13.902C2511.72 8.81709 2327.31 8.87474 2126.55 0.512528C2060.73 -2.00052 2040.7 85.9483 2101.3 114.088C2417.76 260.743 2628.86 431.51 2731.94 722.143C2940.79 1225.9 2508.91 2178.82 2150.63 2656.7C1779.64 3169.63 1293.64 3217.84 798.345 3136.04C534.128 3085.8 291.072 2986.98 92.1133 2864.4C33.4593 2828.06 -29.3236 2901.01 15.967 2953.13C371.841 3364.12 1128.29 3656.09 1663.56 3665.34" />
|
|
119
119
|
</svg>
|
|
120
120
|
)}
|
|
121
|
+
{path === "path5" && (
|
|
122
|
+
// Exact Figma export, cropped via viewBox at 0,0 (no translate hack needed);
|
|
123
|
+
// mobile/desktop swap via CSS breakpoint since the crop window differs by height.
|
|
124
|
+
<>
|
|
125
|
+
<svg
|
|
126
|
+
className={`shape-bg-svg pointer-events-none absolute left-0 top-0 z-0 block md:hidden ${variantClass} fill-current ${fillClass}`}
|
|
127
|
+
width="367"
|
|
128
|
+
height="644"
|
|
129
|
+
viewBox="0 0 367 644"
|
|
130
|
+
fill="none"
|
|
131
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
132
|
+
aria-hidden="true"
|
|
133
|
+
focusable="false"
|
|
134
|
+
>
|
|
135
|
+
<path d="M-2083.1 549.586C-1857.78 764.836 -1656.5 828.759 -1081.83 673.557C-539.683 527.203 347.968 344.365 364.89 7.43135C381.862 -193.724 210.008 -373.544 84.6305 -523.879C-1.85227 -620.854 -81.806 -706.094 -164.9 -802.494C-192.25 -834.002 -242.154 -805.661 -229.077 -765.61C-160.659 -556.59 -149.205 -385.975 -240.751 -214.056C-386.348 97.8893 -1020.17 305.621 -1399.44 344.278C-1800.65 392.049 -2033.89 187.954 -2210.22 -76.0251C-2301.18 -219.67 -2360.21 -374.3 -2388.99 -518.696C-2397.38 -561.352 -2458.78 -559.193 -2463.58 -515.969C-2501.97 -175.709 -2310.97 298.878 -2083.31 550.316" />
|
|
136
|
+
</svg>
|
|
137
|
+
<svg
|
|
138
|
+
className={`shape-bg-svg pointer-events-none absolute left-0 top-0 z-0 hidden md:block ${variantClass} fill-current ${fillClass}`}
|
|
139
|
+
width="367"
|
|
140
|
+
height="448"
|
|
141
|
+
viewBox="0 0 367 448"
|
|
142
|
+
fill="none"
|
|
143
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
144
|
+
aria-hidden="true"
|
|
145
|
+
focusable="false"
|
|
146
|
+
>
|
|
147
|
+
<path d="M-2083.1 549.587C-1857.78 764.837 -1656.5 828.759 -1081.83 673.557C-539.682 527.203 347.968 344.365 364.89 7.43156C381.863 -193.724 210.008 -373.543 84.6307 -523.879C-1.85203 -620.854 -81.8058 -706.094 -164.9 -802.494C-192.249 -834.001 -242.154 -805.661 -229.077 -765.609C-160.658 -556.59 -149.205 -385.975 -240.751 -214.056C-386.348 97.8895 -1020.17 305.621 -1399.44 344.278C-1800.65 392.049 -2033.89 187.954 -2210.22 -76.0249C-2301.18 -219.669 -2360.21 -374.3 -2388.99 -518.696C-2397.38 -561.352 -2458.78 -559.193 -2463.58 -515.969C-2501.97 -175.709 -2310.97 298.879 -2083.31 550.317" />
|
|
148
|
+
</svg>
|
|
149
|
+
</>
|
|
150
|
+
)}
|
|
121
151
|
|
|
122
152
|
<div className="shape-bg-content relative z-[1]">{children}</div>
|
|
123
153
|
</div>
|
package/src/contentful/index.ts
CHANGED
|
@@ -98,6 +98,11 @@ export type { EmailInputBlockProps } from "./blocks/email-input-block/types";
|
|
|
98
98
|
|
|
99
99
|
export { default as CartRetentionBanner } from "./blocks/cart-retention-banner";
|
|
100
100
|
export type { TypeCartRetentionBannerFields as CartRetentionBannerProps } from "./blocks/cart-retention-banner/types";
|
|
101
|
+
export type {
|
|
102
|
+
CartRetentionBannerImagePosition,
|
|
103
|
+
CartRetentionBannerBackgroundColor,
|
|
104
|
+
CartRetentionBannerImageAccentColor,
|
|
105
|
+
} from "./blocks/cart-retention-banner/types";
|
|
101
106
|
|
|
102
107
|
export { IframeEmbed } from "./blocks/iframe-embed";
|
|
103
108
|
export type { IframeEmbedProps } from "./blocks/iframe-embed/types";
|