@windstream/react-shared-components 0.2.22 → 0.2.24
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 +6 -3
- package/dist/contentful/index.esm.js +3 -3
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +2 -2
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +13 -1
- package/dist/index.d.ts +16 -4
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/next/index.esm.js +1 -1
- package/dist/next/index.esm.js.map +1 -1
- package/dist/next/index.js +1 -1
- package/dist/next/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/utils/index.d.ts +3 -1
- package/dist/utils/index.esm.js +1 -1
- package/dist/utils/index.esm.js.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/link/index.test.tsx +103 -4
- package/src/components/link/index.tsx +9 -1
- package/src/components/link/types.ts +12 -0
- package/src/contentful/blocks/anchored-bottom-banner/AnchoredBottomBanner.stories.tsx +155 -0
- package/src/contentful/blocks/breadcrumbs/index.tsx +3 -2
- package/src/contentful/blocks/callout/Callout.stories.mocks.ts +327 -0
- package/src/contentful/blocks/callout/Callout.stories.tsx +315 -2
- package/src/contentful/blocks/callout/types.ts +1 -2
- package/src/contentful/blocks/email-input-block/index.tsx +41 -17
- package/src/contentful/blocks/email-input-block/types.ts +2 -1
- package/src/contentful/blocks/find-kinetic/index.test.tsx +35 -0
- package/src/contentful/blocks/find-kinetic/index.tsx +33 -9
- package/src/contentful/blocks/find-kinetic/types.ts +1 -0
- package/src/contentful/blocks/primary-hero/index.test.tsx +63 -0
- package/src/contentful/blocks/primary-hero/index.tsx +66 -9
- package/src/hooks/contentful/use-contentful-rich-text.test.tsx +29 -0
- package/src/hooks/contentful/use-contentful-rich-text.tsx +20 -9
- package/src/types/global.d.ts +5 -0
|
@@ -1,23 +1,336 @@
|
|
|
1
1
|
import { Callout } from "./index";
|
|
2
|
+
import type { CalloutProps } from "./types";
|
|
3
|
+
import {
|
|
4
|
+
blogItems,
|
|
5
|
+
defaultCalloutArgs,
|
|
6
|
+
defaultSimpleItems,
|
|
7
|
+
floatingImageItems,
|
|
8
|
+
fourColumnItems,
|
|
9
|
+
fullImageItems,
|
|
10
|
+
withFloatingImageArgs,
|
|
11
|
+
} from "./Callout.stories.mocks";
|
|
2
12
|
|
|
3
13
|
import { DocsPage } from "@shared/stories/DocsTemplate";
|
|
4
14
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
15
|
+
import React from "react";
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* Tailwind safelist — classes referenced here are included in the CSS bundle
|
|
19
|
+
* so they work when entered dynamically via Storybook controls.
|
|
20
|
+
* border-2 border-red-500 border-blue-500 border-green-500
|
|
21
|
+
* rounded-3xl p-8 bg-gray-100
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/* Controls */
|
|
25
|
+
|
|
26
|
+
const controls: Record<string, any> = {
|
|
27
|
+
title: { control: "text" },
|
|
28
|
+
subtitle: { control: "text" },
|
|
29
|
+
description: { control: "text" },
|
|
30
|
+
enableHeading: { control: "boolean" },
|
|
31
|
+
applyBoxShadow: { control: "boolean" },
|
|
32
|
+
cardStackingMobile: { control: "boolean" },
|
|
33
|
+
items: { control: "object" },
|
|
34
|
+
cta: { control: "object" },
|
|
35
|
+
finePrint: { control: "text" },
|
|
36
|
+
color: {
|
|
37
|
+
control: "select",
|
|
38
|
+
options: ["dark", "light"],
|
|
39
|
+
},
|
|
40
|
+
cardsWidth: { control: "boolean" },
|
|
41
|
+
maxCardsPerRow: { control: { type: "number", min: 1, max: 6 } },
|
|
42
|
+
noGutter: { control: "boolean" },
|
|
43
|
+
maxWidth: { control: "boolean" },
|
|
44
|
+
cardType: {
|
|
45
|
+
control: "select",
|
|
46
|
+
options: ["simple", "blog", "fullImage", "floatingImage"],
|
|
47
|
+
description:
|
|
48
|
+
"Default card type. Each type expects a different item data shape — use the dedicated card type stories (SimpleCards, BlogCards, etc.) to preview each variant with correct mock data.",
|
|
49
|
+
},
|
|
50
|
+
backgroundColor: {
|
|
51
|
+
control: "select",
|
|
52
|
+
options: [
|
|
53
|
+
"cream500",
|
|
54
|
+
"gray100",
|
|
55
|
+
"white",
|
|
56
|
+
"transparent",
|
|
57
|
+
"blue",
|
|
58
|
+
"green",
|
|
59
|
+
"navy",
|
|
60
|
+
"purple",
|
|
61
|
+
"yellow",
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
background: { control: "text" },
|
|
65
|
+
textColor: { control: "text" },
|
|
66
|
+
disableAnimation: { control: "boolean" },
|
|
67
|
+
animationType: {
|
|
68
|
+
control: "select",
|
|
69
|
+
options: ["scale", "shadow", "lift", "opacity", "grow", "pop"],
|
|
70
|
+
},
|
|
71
|
+
anchorId: { table: { disable: true } },
|
|
72
|
+
containerClassName: { control: "text" },
|
|
73
|
+
innerClassName: { control: "text" },
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/* --------
|
|
77
|
+
Decorator: maps backgroundColor prop values to correct
|
|
78
|
+
CSS variable colors via inline style. Applied in Storybook
|
|
79
|
+
only — index.tsx remains untouched.
|
|
80
|
+
----------- */
|
|
81
|
+
|
|
82
|
+
const bgColorVarMap: Record<string, string> = {
|
|
83
|
+
blue: "var(--color-bg-fill-brand-supporting)",
|
|
84
|
+
green: "var(--color-bg-fill-brand)",
|
|
85
|
+
navy: "var(--color-bg-fill-inverse)",
|
|
86
|
+
purple: "var(--color-bg-fill-brand-tertiary)",
|
|
87
|
+
gray100: "var(--color-bg-fill-info)",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const navyTextOverride = `
|
|
91
|
+
.navy-callout-override .text-text,
|
|
92
|
+
.navy-callout-override .decoration-text,
|
|
93
|
+
.navy-callout-override .text-link,
|
|
94
|
+
.navy-callout-override .heading2,
|
|
95
|
+
.navy-callout-override .heading1,
|
|
96
|
+
.navy-callout-override .subheading1,
|
|
97
|
+
.navy-callout-override .subheading3,
|
|
98
|
+
.navy-callout-override .body1,
|
|
99
|
+
.navy-callout-override .heading6,
|
|
100
|
+
.navy-callout-override .label1,
|
|
101
|
+
.navy-callout-override .footnote {
|
|
102
|
+
color: white !important;
|
|
103
|
+
text-decoration-color: white !important;
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Transforms item image fields based on cardType, mimicking the martech
|
|
109
|
+
* `toLocalImageAsset` conversion. Converts string images to the
|
|
110
|
+
* { href, title, width, height } format that fullImage/floatingImage expect.
|
|
111
|
+
*/
|
|
112
|
+
const normalizeItemsForCardType = (
|
|
113
|
+
items: CalloutProps["items"],
|
|
114
|
+
cardType: string
|
|
115
|
+
): CalloutProps["items"] => {
|
|
116
|
+
if (cardType !== "fullImage" && cardType !== "floatingImage") return items;
|
|
117
|
+
return items.map((item) => {
|
|
118
|
+
const img = (item as any).image;
|
|
119
|
+
if (typeof img === "string") {
|
|
120
|
+
return {
|
|
121
|
+
...item,
|
|
122
|
+
image: {
|
|
123
|
+
href: img,
|
|
124
|
+
title: (item as any).imageAlt ?? (item as any).title ?? "",
|
|
125
|
+
width: (item as any).imageWidth ?? 400,
|
|
126
|
+
height: (item as any).imageHeight ?? 300,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return item;
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/* --------
|
|
135
|
+
Meta
|
|
136
|
+
----------- */
|
|
5
137
|
|
|
6
138
|
const meta: Meta<typeof Callout> = {
|
|
7
139
|
title: "Contentful Blocks/Callout",
|
|
8
140
|
component: Callout,
|
|
9
141
|
tags: ["autodocs"],
|
|
142
|
+
argTypes: controls,
|
|
10
143
|
parameters: {
|
|
11
144
|
layout: "centered",
|
|
12
145
|
docs: {
|
|
13
146
|
page: DocsPage,
|
|
14
147
|
description: {
|
|
15
|
-
component:
|
|
148
|
+
component:
|
|
149
|
+
"Flexible card grid/stack section with multiple card types (simple, blog, full-image, floating-image), configurable backgrounds, column layouts, and animation support.",
|
|
16
150
|
},
|
|
17
151
|
},
|
|
18
152
|
},
|
|
19
|
-
|
|
153
|
+
decorators: [
|
|
154
|
+
(Story, context) => {
|
|
155
|
+
const bg = context.args.backgroundColor as string;
|
|
156
|
+
const cardType = context.args.cardType as string;
|
|
157
|
+
const isNavy = bg === "navy" || context.args.background === bgColorVarMap.navy;
|
|
158
|
+
const cssVar = bgColorVarMap[bg];
|
|
159
|
+
|
|
160
|
+
const transformedItems = normalizeItemsForCardType(
|
|
161
|
+
context.args.items as CalloutProps["items"],
|
|
162
|
+
cardType
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const newArgs: Record<string, unknown> = { ...context.args };
|
|
166
|
+
const userBackground = context.args.background as string;
|
|
167
|
+
if (cssVar && !userBackground) {
|
|
168
|
+
newArgs.backgroundColor = undefined;
|
|
169
|
+
newArgs.background = cssVar;
|
|
170
|
+
}
|
|
171
|
+
if (transformedItems !== context.args.items) {
|
|
172
|
+
newArgs.items = transformedItems;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const hasOverrides = (cssVar && !userBackground) || transformedItems !== context.args.items;
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<>
|
|
179
|
+
<style>{navyTextOverride}</style>
|
|
180
|
+
<div className={isNavy ? "navy-callout-override" : undefined}>
|
|
181
|
+
{hasOverrides ? <Story args={newArgs} /> : <Story />}
|
|
182
|
+
</div>
|
|
183
|
+
</>
|
|
184
|
+
);
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
args: {
|
|
188
|
+
...defaultCalloutArgs,
|
|
189
|
+
items: defaultSimpleItems,
|
|
190
|
+
} as CalloutProps,
|
|
20
191
|
};
|
|
192
|
+
|
|
21
193
|
export default meta;
|
|
22
194
|
type Story = StoryObj<typeof meta>;
|
|
195
|
+
|
|
196
|
+
/* Stories */
|
|
197
|
+
|
|
23
198
|
export const Default: Story = {};
|
|
199
|
+
|
|
200
|
+
export const SimpleCards: Story = {
|
|
201
|
+
args: {
|
|
202
|
+
title: "Simple Card Variant",
|
|
203
|
+
subtitle: "Basic cards with title and description",
|
|
204
|
+
items: defaultSimpleItems,
|
|
205
|
+
cardType: "simple",
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export const BlogCards: Story = {
|
|
210
|
+
args: {
|
|
211
|
+
title: "Blog Card Variant",
|
|
212
|
+
subtitle: "Stay informed with our blog",
|
|
213
|
+
items: blogItems,
|
|
214
|
+
cardType: "blog",
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export const FullImageCards: Story = {
|
|
219
|
+
args: {
|
|
220
|
+
title: "Full Image Card Variant",
|
|
221
|
+
subtitle: "Cards with full-bleed imagery",
|
|
222
|
+
items: fullImageItems,
|
|
223
|
+
cardType: "fullImage",
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export const FloatingImageCards: Story = {
|
|
228
|
+
args: {
|
|
229
|
+
...withFloatingImageArgs,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
export const BackgroundWhite: Story = {
|
|
235
|
+
args: {
|
|
236
|
+
title: "White Background",
|
|
237
|
+
items: defaultSimpleItems,
|
|
238
|
+
backgroundColor: "white",
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export const BackgroundNavy: Story = {
|
|
243
|
+
args: {
|
|
244
|
+
title: "Navy Background",
|
|
245
|
+
subtitle: "Dark theme with light text",
|
|
246
|
+
items: defaultSimpleItems,
|
|
247
|
+
background: bgColorVarMap.navy,
|
|
248
|
+
color: "light",
|
|
249
|
+
textColor: "#ffffff",
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export const BackgroundGreen: Story = {
|
|
254
|
+
args: {
|
|
255
|
+
title: "Green Background",
|
|
256
|
+
items: defaultSimpleItems,
|
|
257
|
+
background: bgColorVarMap.green,
|
|
258
|
+
color: "light",
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export const BackgroundBlue: Story = {
|
|
263
|
+
args: {
|
|
264
|
+
title: "Blue Background",
|
|
265
|
+
items: defaultSimpleItems,
|
|
266
|
+
background: bgColorVarMap.blue,
|
|
267
|
+
color: "light",
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export const BackgroundPurple: Story = {
|
|
272
|
+
args: {
|
|
273
|
+
title: "Purple Background",
|
|
274
|
+
items: defaultSimpleItems,
|
|
275
|
+
background: bgColorVarMap.purple,
|
|
276
|
+
color: "light",
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export const BackgroundYellow: Story = {
|
|
281
|
+
args: {
|
|
282
|
+
title: "Yellow Background",
|
|
283
|
+
items: defaultSimpleItems,
|
|
284
|
+
backgroundColor: "yellow",
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export const BackgroundTransparent: Story = {
|
|
289
|
+
args: {
|
|
290
|
+
title: "Transparent Background",
|
|
291
|
+
items: defaultSimpleItems,
|
|
292
|
+
backgroundColor: "transparent",
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
export const StackLayout: Story = {
|
|
297
|
+
args: {
|
|
298
|
+
title: "Stacked Layout",
|
|
299
|
+
items: defaultSimpleItems,
|
|
300
|
+
cardsWidth: false,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export const WithCta: Story = {
|
|
305
|
+
args: {
|
|
306
|
+
title: "Ready to Get Started?",
|
|
307
|
+
subtitle: "Pick the plan that works for you",
|
|
308
|
+
items: defaultSimpleItems,
|
|
309
|
+
cta: {
|
|
310
|
+
buttonLabel: "View All Plans",
|
|
311
|
+
href: "/plans",
|
|
312
|
+
showButtonAs: "solid",
|
|
313
|
+
buttonVariant: "primary_brand",
|
|
314
|
+
},
|
|
315
|
+
finePrint: "* Terms and conditions apply. Prices subject to change.",
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
export const WithAnimation: Story = {
|
|
320
|
+
args: {
|
|
321
|
+
title: "Animated Cards",
|
|
322
|
+
subtitle: "Hover over the cards to see the grow animation",
|
|
323
|
+
items: defaultSimpleItems,
|
|
324
|
+
disableAnimation: false,
|
|
325
|
+
animationType: "grow",
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
export const CustomColumns: Story = {
|
|
330
|
+
args: {
|
|
331
|
+
title: "Custom 4-Column Layout",
|
|
332
|
+
items: fourColumnItems,
|
|
333
|
+
maxCardsPerRow: 4,
|
|
334
|
+
applyBoxShadow: true,
|
|
335
|
+
},
|
|
336
|
+
};
|
|
@@ -30,8 +30,7 @@ export type CalloutProps = {
|
|
|
30
30
|
finePrint?: ReactNode;
|
|
31
31
|
cta?: CalloutCtaProps;
|
|
32
32
|
applyBoxShadow?: boolean;
|
|
33
|
-
cardStackingMobile?: boolean;
|
|
34
|
-
bottomText?: string;
|
|
33
|
+
cardStackingMobile?: boolean;
|
|
35
34
|
color?: "dark" | "light";
|
|
36
35
|
/**
|
|
37
36
|
* When `true` (default) cards are laid out in a responsive Tailwind
|
|
@@ -4,6 +4,25 @@ import React, { useState } from "react";
|
|
|
4
4
|
import { Button } from "../button";
|
|
5
5
|
import { EmailInputBlockProps } from "./types";
|
|
6
6
|
|
|
7
|
+
const LogoCream = ({ fill = "#FFFEEF" }: { fill?: string }) => {
|
|
8
|
+
const clipPathId = `logo-cream-clip-${React.useId()}`;
|
|
9
|
+
return (
|
|
10
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" fill="none" viewBox="0 0 100 99" preserveAspectRatio="xMidYMid meet">
|
|
11
|
+
<g fill={fill} clipPath={`url(#${clipPathId})`}>
|
|
12
|
+
<path d="M23.877 73.439c2.829 9.044 7.288 13.706 25.03 18.07 16.735 4.111 43.31 12.356 48.798 3.65 3.454-5.095 1.534-12.395.415-18.244-.875-3.853-1.756-7.3-2.534-11.074-.255-1.237-2.034-1.22-2.278.022-1.284 6.552-3.528 11.26-8.55 14.482-8.572 6.125-28.7 2.295-39.453-2.272-11.487-4.656-14.696-13.509-15.486-23.126-.296-5.157.437-10.123 1.818-14.392.409-1.254-1.273-2.097-2.045-1.023-6.113 8.464-8.073 23.88-5.71 33.913" opacity=".45" />
|
|
13
|
+
<path d="M74.18 75.362c9.135-2.8 13.845-7.215 18.253-24.779 4.153-16.569 12.481-42.878 3.687-48.31-5.147-3.42-12.52-1.52-18.429-.411-3.89.866-7.373 1.738-11.185 2.508-1.25.253-1.233 2.014.023 2.255 6.618 1.271 11.373 3.493 14.628 8.465 6.186 8.486 2.318 28.412-2.295 39.059-4.704 11.371-13.646 14.549-23.36 15.33-5.21.293-10.226-.432-14.537-1.799-1.267-.405-2.12 1.26-1.034 2.025 8.55 6.051 24.12 7.992 34.255 5.652" opacity=".45" />
|
|
14
|
+
<path d="M76.124 25.562c-2.83-9.044-7.289-13.706-25.03-18.07C34.358 3.38 7.783-4.865 2.3 3.842-1.153 8.937.767 16.237 1.886 22.086c.875 3.852 1.756 7.3 2.534 11.074.256 1.237 2.034 1.22 2.278-.023 1.284-6.552 3.528-11.26 8.55-14.482 8.572-6.124 28.7-2.294 39.453 2.272 11.487 4.657 14.696 13.51 15.486 23.126.296 5.158-.437 10.124-1.818 14.392-.409 1.255 1.273 2.098 2.045 1.024 6.113-8.464 8.073-23.88 5.71-33.913" opacity=".45" />
|
|
15
|
+
<path d="M25.82 23.638c-9.135 2.8-13.845 7.216-18.253 24.78-4.153 16.568-12.48 42.877-3.687 48.31 5.147 3.42 12.52 1.519 18.429.41 3.891-.865 7.374-1.737 11.186-2.508 1.25-.253 1.232-2.013-.023-2.255-6.619-1.27-11.373-3.492-14.629-8.464-6.186-8.487-2.317-28.413 2.296-39.059 4.703-11.372 13.645-14.55 23.36-15.331 5.209-.293 10.225.433 14.537 1.8 1.266.404 2.118-1.26 1.033-2.025-8.55-6.052-24.12-7.992-34.255-5.652" opacity=".45" />
|
|
16
|
+
</g>
|
|
17
|
+
<defs>
|
|
18
|
+
<clipPath id={clipPathId}>
|
|
19
|
+
<path fill="#fff" d="M0 0h100v99H0z" />
|
|
20
|
+
</clipPath>
|
|
21
|
+
</defs>
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
7
26
|
export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
8
27
|
const {
|
|
9
28
|
anchorId = "email-input",
|
|
@@ -11,7 +30,8 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
11
30
|
subTitle,
|
|
12
31
|
emailInputDescription,
|
|
13
32
|
inputPlaceholder = "Enter your email here",
|
|
14
|
-
|
|
33
|
+
ctaButtonText,
|
|
34
|
+
ctaText,
|
|
15
35
|
caption,
|
|
16
36
|
backgroundColor = "green",
|
|
17
37
|
successFeedback,
|
|
@@ -23,7 +43,9 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
23
43
|
validationErrorMessage = "Invalid email address",
|
|
24
44
|
} = props || {};
|
|
25
45
|
|
|
26
|
-
const
|
|
46
|
+
const buttonLabel = ctaButtonText || ctaText || "Sign me up";
|
|
47
|
+
|
|
48
|
+
const backgroundColorClasses: Record<string, string> = {
|
|
27
49
|
green: "bg-bg-fill-brand",
|
|
28
50
|
white: "bg-bg",
|
|
29
51
|
gray90: "bg-[#464646]",
|
|
@@ -34,16 +56,20 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
34
56
|
blue: "bg-bg-fill-brand-supporting",
|
|
35
57
|
orange: "bg-orange-500",
|
|
36
58
|
purple: "bg-bg-fill-brand-tertiary",
|
|
59
|
+
white: "bg-bg",
|
|
37
60
|
};
|
|
38
61
|
|
|
39
|
-
const normalizedBg = (backgroundColor as string)?.toLowerCase?.() || "
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
const normalizedBg = (backgroundColor as string)?.toLowerCase?.() || "white";
|
|
63
|
+
const outerBgClass = backgroundColorClasses[normalizedBg as keyof typeof backgroundColorClasses] || backgroundColorClasses.white;
|
|
64
|
+
|
|
65
|
+
const normalizedTheme = (themeColor as string)?.toLowerCase?.() || "green";
|
|
66
|
+
const containerBgClass = themeColorClasses[normalizedTheme as keyof typeof themeColorClasses] || themeColorClasses.green;
|
|
43
67
|
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const textColor =
|
|
68
|
+
const darkThemeColors = ["green", "blue", "purple"];
|
|
69
|
+
const isDarkTheme = darkThemeColors.includes(normalizedTheme);
|
|
70
|
+
const textColor = isDarkTheme ? "text-white" : "text-text";
|
|
71
|
+
const errorTextColor = isDarkTheme ? "text-text-inverse" : "text-text";
|
|
72
|
+
const pinwheelFill = normalizedTheme === "white" ? "#E0E0E0" : "#FFFEEF";
|
|
47
73
|
|
|
48
74
|
const paddingClass = padding === "none" ? "!py-0 md:!py-0" : "!py-8 md:!py-20";
|
|
49
75
|
|
|
@@ -70,15 +96,13 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
70
96
|
};
|
|
71
97
|
|
|
72
98
|
return (
|
|
73
|
-
<section id={anchorId} className={`w-full px-4 ${paddingClass} md:px-0`}>
|
|
99
|
+
<section id={anchorId} className={`w-full px-4 ${paddingClass} md:px-0 ${outerBgClass}`}>
|
|
74
100
|
<div
|
|
75
|
-
className={`mx-auto max-w-[1200px] overflow-hidden !rounded-[20px] md:rounded-[20px] flex flex-col items-start !px-5 !pt-6 !pb-10 md:!p-10 md:!pt-12 !h-[344px] md:!w-auto md:!h-[367px] relative ${
|
|
101
|
+
className={`mx-auto max-w-[1200px] overflow-hidden !rounded-[20px] md:rounded-[20px] flex flex-col items-start !px-5 !pt-6 !pb-10 md:!p-10 md:!pt-12 !h-[344px] md:!w-auto md:!h-[367px] relative ${containerBgClass}`}
|
|
76
102
|
>
|
|
77
103
|
{showPinWheel && (
|
|
78
|
-
<div className=
|
|
79
|
-
<
|
|
80
|
-
<circle cx="242" cy="248" r="200" fill="currentColor" />
|
|
81
|
-
</svg>
|
|
104
|
+
<div className="absolute right-[-50px] top-[-30px] hidden h-[420px] w-[420px] pointer-events-none lg:block" aria-hidden="true">
|
|
105
|
+
<LogoCream fill={pinwheelFill} />
|
|
82
106
|
</div>
|
|
83
107
|
)}
|
|
84
108
|
<div className="w-full">
|
|
@@ -121,14 +145,14 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
121
145
|
/>
|
|
122
146
|
</div>
|
|
123
147
|
<Button
|
|
124
|
-
buttonLabel={
|
|
148
|
+
buttonLabel={buttonLabel}
|
|
125
149
|
buttonVariant="primary_inverse"
|
|
126
150
|
buttonClassName="capitalize-first whitespace-nowrap rounded-2xl px-8 !text-lg mt-3 w-full sm:mt-0 sm:w-auto sm:absolute sm:right-[8px] sm:top-[calc(50%-28px)]"
|
|
127
151
|
onClick={handleOnSubmit}
|
|
128
152
|
/>
|
|
129
153
|
</div>
|
|
130
154
|
{emailError && (
|
|
131
|
-
<p className=
|
|
155
|
+
<p className={`mt-2 text-label4 font-semibold ${errorTextColor}`}>
|
|
132
156
|
{emailError}
|
|
133
157
|
</p>
|
|
134
158
|
)}
|
|
@@ -7,10 +7,11 @@ export interface EmailInputBlockProps {
|
|
|
7
7
|
emailInputDescription?: string;
|
|
8
8
|
inputPlaceholder?: string;
|
|
9
9
|
successFeedback?: string;
|
|
10
|
+
ctaButtonText?: string;
|
|
10
11
|
ctaText?: string;
|
|
11
12
|
caption?: React.ReactNode;
|
|
12
13
|
backgroundColor?: "green" | "white" | "yellow" | "blue" | "navy" | "gray90";
|
|
13
|
-
themeColor?: "blue" | "green" | "orange" | "purple";
|
|
14
|
+
themeColor?: "blue" | "green" | "orange" | "purple" | "white";
|
|
14
15
|
showPinWheel?: boolean;
|
|
15
16
|
padding?: "none" | "large";
|
|
16
17
|
validationErrorMessage?: string;
|
|
@@ -202,6 +202,41 @@ describe("FindKinetic", () => {
|
|
|
202
202
|
});
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
+
it("does not append a trailing slash by default", () => {
|
|
206
|
+
render(<FindKinetic list={[{ name: "Alabama", code: "AL" }]} />);
|
|
207
|
+
const links = screen.getAllByText("Alabama");
|
|
208
|
+
links.forEach(link => {
|
|
209
|
+
expect(link.closest("a")).toHaveAttribute("href", "/local/al");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("appends a trailing slash when addTrailingSlash is true", () => {
|
|
214
|
+
render(
|
|
215
|
+
<FindKinetic
|
|
216
|
+
list={[{ name: "Alabama", code: "AL" }]}
|
|
217
|
+
addTrailingSlash
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
const links = screen.getAllByText("Alabama");
|
|
221
|
+
links.forEach(link => {
|
|
222
|
+
expect(link.closest("a")).toHaveAttribute("href", "/local/al/");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("does not produce a protocol-relative href when localPathPrefix is '/'", () => {
|
|
227
|
+
render(
|
|
228
|
+
<FindKinetic
|
|
229
|
+
list={[{ name: "Alabama", code: "AL" }]}
|
|
230
|
+
localPathPrefix="/"
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
233
|
+
const links = screen.getAllByText("Alabama");
|
|
234
|
+
links.forEach(link => {
|
|
235
|
+
expect(link.closest("a")).toHaveAttribute("href", "/al");
|
|
236
|
+
expect(link.closest("a")?.getAttribute("href")).not.toMatch(/^\/\//);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
205
240
|
it("sorts list items alphabetically by name", () => {
|
|
206
241
|
const unsortedList = [
|
|
207
242
|
{ name: "Texas", code: "TX" },
|
|
@@ -16,6 +16,7 @@ export const FindKinetic: React.FC<FindKineticProps> = ({
|
|
|
16
16
|
title,
|
|
17
17
|
color = "dark",
|
|
18
18
|
maxWidth = true,
|
|
19
|
+
addTrailingSlash = false,
|
|
19
20
|
}) => {
|
|
20
21
|
const bgColorClasses: Record<ThemeKey, string> = {
|
|
21
22
|
blue: "bg-[#07B2E2]",
|
|
@@ -25,12 +26,22 @@ export const FindKinetic: React.FC<FindKineticProps> = ({
|
|
|
25
26
|
white: "bg-white",
|
|
26
27
|
navy: "bg-[#00002D]",
|
|
27
28
|
};
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
// Trim leading/trailing slashes without a regex to avoid ReDoS on long
|
|
30
|
+
// runs of "/" (CodeQL: polynomial regular expression on uncontrolled data).
|
|
31
|
+
const trimSlashes = (value: string) => {
|
|
32
|
+
let start = 0;
|
|
33
|
+
let end = value.length;
|
|
34
|
+
while (start < end && value.charCodeAt(start) === 47 /* "/" */) start += 1;
|
|
35
|
+
while (end > start && value.charCodeAt(end - 1) === 47 /* "/" */) end -= 1;
|
|
36
|
+
return value.slice(start, end);
|
|
37
|
+
};
|
|
38
|
+
const cleanedLocalPathPrefix = trimSlashes(localPathPrefix ?? "");
|
|
39
|
+
const normalizedLocalPathPrefix = cleanedLocalPathPrefix
|
|
40
|
+
? `/${cleanedLocalPathPrefix}`
|
|
30
41
|
: "";
|
|
31
42
|
|
|
32
43
|
const getLocationHref = (code: string) =>
|
|
33
|
-
`${normalizedLocalPathPrefix}/${code.toLowerCase()}`;
|
|
44
|
+
`${normalizedLocalPathPrefix}/${code.toLowerCase()}${addTrailingSlash ? "/" : ""}`;
|
|
34
45
|
|
|
35
46
|
return (
|
|
36
47
|
<div
|
|
@@ -120,16 +131,21 @@ export const FindKinetic: React.FC<FindKineticProps> = ({
|
|
|
120
131
|
key={`item-list-3-${index}`}
|
|
121
132
|
className={`${image ? "" : "w-[172px]"}`}
|
|
122
133
|
>
|
|
123
|
-
<Link
|
|
134
|
+
<Link
|
|
135
|
+
href={getLocationHref(item.code)}
|
|
136
|
+
className="label1"
|
|
137
|
+
>
|
|
124
138
|
{item.name}
|
|
125
139
|
</Link>
|
|
126
140
|
</li>
|
|
127
|
-
) :
|
|
141
|
+
) : (
|
|
142
|
+
<li key={`empty-3-${index}`} aria-hidden="true" />
|
|
143
|
+
)
|
|
128
144
|
);
|
|
129
145
|
})()}
|
|
130
146
|
</ul>
|
|
131
147
|
{!image && (
|
|
132
|
-
<ul className="hidden xl:grid xl:grid-cols-4
|
|
148
|
+
<ul className="hidden gap-x-20 gap-y-5 xl:grid xl:grid-cols-4">
|
|
133
149
|
{(() => {
|
|
134
150
|
const sortedList = [...list].sort((a, b) =>
|
|
135
151
|
a.name.localeCompare(b.name)
|
|
@@ -145,12 +161,20 @@ export const FindKinetic: React.FC<FindKineticProps> = ({
|
|
|
145
161
|
}
|
|
146
162
|
return rearranged.map((item, index: number) =>
|
|
147
163
|
item ? (
|
|
148
|
-
<li
|
|
149
|
-
|
|
164
|
+
<li
|
|
165
|
+
key={`item-list-4-${index}`}
|
|
166
|
+
className="w-[172px]"
|
|
167
|
+
>
|
|
168
|
+
<Link
|
|
169
|
+
href={getLocationHref(item.code)}
|
|
170
|
+
className="label1"
|
|
171
|
+
>
|
|
150
172
|
{item.name}
|
|
151
173
|
</Link>
|
|
152
174
|
</li>
|
|
153
|
-
) :
|
|
175
|
+
) : (
|
|
176
|
+
<li key={`empty-4-${index}`} aria-hidden="true" />
|
|
177
|
+
)
|
|
154
178
|
);
|
|
155
179
|
})()}
|
|
156
180
|
</ul>
|
|
@@ -155,6 +155,69 @@ describe("PrimaryHero", () => {
|
|
|
155
155
|
render(<PrimaryHero {...baseProps} />);
|
|
156
156
|
expect(screen.queryByText("Get Started")).not.toBeInTheDocument();
|
|
157
157
|
});
|
|
158
|
+
|
|
159
|
+
it("renders both CTAs side by side when both are button type", () => {
|
|
160
|
+
const { container } = render(
|
|
161
|
+
<PrimaryHero
|
|
162
|
+
{...baseProps}
|
|
163
|
+
primaryCta1={{ buttonLabel: "Check plans", showButtonAs: "solid" }}
|
|
164
|
+
primaryCta2={{
|
|
165
|
+
buttonLabel: "Shop AT&T deals",
|
|
166
|
+
showButtonAs: "solid",
|
|
167
|
+
}}
|
|
168
|
+
/>
|
|
169
|
+
);
|
|
170
|
+
expect(screen.getByText("Check plans")).toBeInTheDocument();
|
|
171
|
+
expect(screen.getByText("Shop AT&T deals")).toBeInTheDocument();
|
|
172
|
+
const row = container.querySelector(".sm\\:flex-row");
|
|
173
|
+
expect(row).toBeInTheDocument();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("renders CTAs stacked when only primaryCta1 is present", () => {
|
|
177
|
+
const { container } = render(
|
|
178
|
+
<PrimaryHero
|
|
179
|
+
{...baseProps}
|
|
180
|
+
primaryCta1={{ buttonLabel: "Check plans", showButtonAs: "solid" }}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
expect(screen.getByText("Check plans")).toBeInTheDocument();
|
|
184
|
+
expect(container.querySelector(".sm\\:flex-row")).not.toBeInTheDocument();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("renders CTAs stacked when one CTA is not a button type", () => {
|
|
188
|
+
const { container } = render(
|
|
189
|
+
<PrimaryHero
|
|
190
|
+
{...baseProps}
|
|
191
|
+
primaryCta1={{ buttonLabel: "Check plans", showButtonAs: "solid" }}
|
|
192
|
+
primaryCta2={{ buttonLabel: "Learn more", showButtonAs: "text" }}
|
|
193
|
+
/>
|
|
194
|
+
);
|
|
195
|
+
expect(screen.getByText("Check plans")).toBeInTheDocument();
|
|
196
|
+
expect(screen.getByText("Learn more")).toBeInTheDocument();
|
|
197
|
+
expect(container.querySelector(".sm\\:flex-row")).not.toBeInTheDocument();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("hides primaryCta2 on mobile when hideOnMobileCta2 is true", () => {
|
|
201
|
+
const { container } = render(
|
|
202
|
+
<PrimaryHero
|
|
203
|
+
{...baseProps}
|
|
204
|
+
primaryCta2={{ buttonLabel: "Shop deals", showButtonAs: "solid" }}
|
|
205
|
+
hideOnMobileCta2={true}
|
|
206
|
+
/>
|
|
207
|
+
);
|
|
208
|
+
const ctaWrapper = container.querySelector(".hidden.md\\:block");
|
|
209
|
+
expect(ctaWrapper).toBeInTheDocument();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("renders only primaryCta2 when primaryCta1 is not provided", () => {
|
|
213
|
+
render(
|
|
214
|
+
<PrimaryHero
|
|
215
|
+
{...baseProps}
|
|
216
|
+
primaryCta2={{ buttonLabel: "Shop deals", showButtonAs: "solid" }}
|
|
217
|
+
/>
|
|
218
|
+
);
|
|
219
|
+
expect(screen.getByText("Shop deals")).toBeInTheDocument();
|
|
220
|
+
});
|
|
158
221
|
});
|
|
159
222
|
|
|
160
223
|
describe("Secondary CTA", () => {
|