@windstream/react-shared-components 0.2.73 → 0.2.75

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windstream/react-shared-components",
3
- "version": "0.2.73",
3
+ "version": "0.2.75",
4
4
  "type": "module",
5
5
  "description": "Shared React components for Kinetic applications",
6
6
  "main": "dist/index.js",
@@ -87,7 +87,8 @@ export const RichTextBasic: FC<RichTextBasicProps> = props => {
87
87
  }
88
88
  className={cx(
89
89
  "last:[&_p]:mb-0",
90
- "text-body1 leading-normal",
90
+ // mobile-first: body3/leading-6 below md, body1/leading-normal from md up
91
+ "text-body3 leading-6 md:text-body1 md:leading-normal",
91
92
  "[&_b]:font-bold [&_strong]:font-bold [&_i]:italic [&_em]:italic",
92
93
  className,
93
94
  !enableMaxWidth && "max-w-none"
@@ -281,6 +281,37 @@ describe("ProductCard", () => {
281
281
  expect(screen.queryByText("Free router")).not.toBeInTheDocument();
282
282
  });
283
283
 
284
+ it("omits the empty gift slot when no visible card has a gift", () => {
285
+ const { container } = render(
286
+ <ProductCard {...defaultProps} hasAnyGift={false} />
287
+ );
288
+ expect(
289
+ container.querySelector("[data-pc-gift-empty]")
290
+ ).not.toBeInTheDocument();
291
+ });
292
+
293
+ it("reserves the empty gift slot when a visible card has a gift", () => {
294
+ const { container } = render(
295
+ <ProductCard {...defaultProps} hasAnyGift={true} />
296
+ );
297
+ expect(
298
+ container.querySelector("[data-pc-gift-empty]")
299
+ ).toBeInTheDocument();
300
+ });
301
+
302
+ it("renders the real gift slot (not the empty placeholder) when the card has its own gift", () => {
303
+ const giftBadge = [
304
+ { title: "Free router", icon: "/router.png" },
305
+ ] as any;
306
+ const { container } = render(
307
+ <ProductCard {...defaultProps} giftBadge={giftBadge} hasAnyGift={true} />
308
+ );
309
+ expect(
310
+ container.querySelector("[data-pc-gift-empty]")
311
+ ).not.toBeInTheDocument();
312
+ expect(container.querySelector("[data-pc-gift]")).toBeInTheDocument();
313
+ });
314
+
284
315
  it("renders the piped dollar value with the heavier label3 weight", () => {
285
316
  const giftBadge = [
286
317
  { title: "|$100| prepaid Mastercard®", icon: "/card.png" },
@@ -53,6 +53,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
53
53
  ismaxSpeed,
54
54
  priceSuffixBottom,
55
55
  hasAnyBadge = false,
56
+ hasAnyGift = false,
56
57
  twoColumn = false,
57
58
  enableBusinessSettings = false,
58
59
  }) => {
@@ -109,7 +110,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
109
110
  >
110
111
  {/* Plan Name & Price */}
111
112
  <header
112
- data-pc-header
113
+ data-pc-header={true}
113
114
  className="overflow-none flex flex-wrap items-start justify-between gap-2 md:gap-4"
114
115
  >
115
116
  <div className="flex w-full flex-col md:w-auto">
@@ -178,7 +179,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
178
179
  </header>
179
180
 
180
181
  {/* Description */}
181
- <section data-pc-description>
182
+ <section data-pc-description={true}>
182
183
  <Text
183
184
  className={cx(
184
185
  twoColumn ? "footnote md:body2" : "body2",
@@ -192,7 +193,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
192
193
 
193
194
  {/* Gift Badge */}
194
195
  {giftBadge?.length ? (
195
- <div data-pc-gift className="min-h-[76px]">
196
+ <div data-pc-gift={true}>
196
197
  <div
197
198
  className={cx(
198
199
  "flex w-full flex-col gap-3 rounded-surface-sm p-2",
@@ -225,11 +226,11 @@ export const ProductCard: React.FC<ProductCardProps> = ({
225
226
  })}
226
227
  </div>
227
228
  </div>
228
- ) : (
229
+ ) : hasAnyGift ? (
229
230
  <div
230
- data-pc-gift
231
- data-pc-gift-empty
232
- className="invisible min-h-[76px] p-2"
231
+ data-pc-gift={true}
232
+ data-pc-gift-empty={true}
233
+ className="invisible p-2"
233
234
  aria-hidden="true"
234
235
  >
235
236
  <Text
@@ -242,7 +243,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
242
243
  &nbsp;
243
244
  </Text>
244
245
  </div>
245
- )}
246
+ ) : null}
246
247
 
247
248
  {/* CTA + features pinned to the bottom so they align across cards of
248
249
  differing content height (e.g. with/without a gift badge). */}
@@ -31,6 +31,7 @@ export interface ProductCardProps {
31
31
  /** True when any card in the set has a best-value badge, so non-badge cards
32
32
  * reserve the banner height and stay vertically aligned. */
33
33
  hasAnyBadge?: boolean;
34
+ hasAnyGift?: boolean;
34
35
  /** True when the card renders in the two-column mobile layout, which uses the
35
36
  * compact mobile typography below `md`. One-column stacks keep the standard
36
37
  * sizes at every breakpoint. */
@@ -1,3 +1,4 @@
1
+ import { useState } from "react";
1
2
  import { Carousel } from "./index";
2
3
  import type { carouselFieldsInterface } from "./types";
3
4
 
@@ -31,7 +32,8 @@ const sampleProduct = (
31
32
  productCardDescription = "Reliable internet for your home.",
32
33
  giftRewards: { list: { items: Array<{ title: string; icon: string }> } } = {
33
34
  list: { items: [] },
34
- }
35
+ },
36
+ productCategory?: string
35
37
  ) => ({
36
38
  __typename: "ComponentProductCard" as const,
37
39
  sys: { id },
@@ -41,6 +43,7 @@ const sampleProduct = (
41
43
  techType: "Fiber",
42
44
  highlighted,
43
45
  productCardDescription,
46
+ productCategory,
44
47
  benefitsTitle: "Plan benefits",
45
48
  benefitsExpanded: false,
46
49
  cta: { buttonLabel: "Select plan" },
@@ -83,8 +86,20 @@ const twoColumnFields: carouselFieldsInterface = {
83
86
  },
84
87
  }
85
88
  ),
86
- sampleProduct("plan-3", "1 Gig", "69", false, "Good for most day-to-day internet uses including streaming video."),
87
- sampleProduct("plan-4", "2 Gig", "89", false, "Worry-free internet for the whole home."),
89
+ sampleProduct(
90
+ "plan-3",
91
+ "1 Gig",
92
+ "69",
93
+ false,
94
+ "Good for most day-to-day internet uses including streaming video."
95
+ ),
96
+ sampleProduct(
97
+ "plan-4",
98
+ "2 Gig",
99
+ "89",
100
+ false,
101
+ "Worry-free internet for the whole home."
102
+ ),
88
103
  ],
89
104
  },
90
105
  };
@@ -106,3 +121,288 @@ export const TwoColumnProductCards: Story = {
106
121
  },
107
122
  };
108
123
 
124
+ const mastercardGift = (amount: string) => ({
125
+ list: {
126
+ items: [
127
+ {
128
+ title: `|$${amount}| prepaid Mastercard®`,
129
+ icon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Crect width='24' height='24' rx='4' fill='%23ffd200'/%3E%3C/svg%3E",
130
+ },
131
+ ],
132
+ },
133
+ });
134
+
135
+ const tabbedFields: carouselFieldsInterface = {
136
+ __typename: "ComponentCarousel",
137
+ sys: { id: "carousel-tabbed-gift" },
138
+ title: "fiber internet plans that fit your way of working",
139
+ twoColumn: false,
140
+ backgroundColor: "white",
141
+ items: {
142
+ items: [
143
+ // "Business Ready" tab — cards carry a gift badge, so the gift slot is
144
+ // reserved and the CTAs stay aligned.
145
+ sampleProduct(
146
+ "br-1",
147
+ "fiber 1 gig",
148
+ "99",
149
+ true,
150
+ "Fast, secure connectivity solution for growing teams.",
151
+ mastercardGift("100"),
152
+ "Business Ready"
153
+ ),
154
+ sampleProduct(
155
+ "br-2",
156
+ "fiber 2 gig",
157
+ "129",
158
+ false,
159
+ "Ultra-fast connectivity solution for high-traffic businesses.",
160
+ mastercardGift("200"),
161
+ "Business Ready"
162
+ ),
163
+ // "Internet Only" tab — no gift badges, so the gift slot must collapse and
164
+ // the CTA should sit flush with the description (the fix under test).
165
+ sampleProduct(
166
+ "io-1",
167
+ "fiber 300 mbps",
168
+ "49",
169
+ true,
170
+ "Ideal for guest Wi-Fi, multiple POS devices, video uploads and streaming.",
171
+ { list: { items: [] } },
172
+ "Internet Only"
173
+ ),
174
+ ],
175
+ },
176
+ };
177
+
178
+ const TabbedTemplate = () => {
179
+ const [activeTab, setActiveTab] = useState("Business Ready");
180
+ return (
181
+ <div style={{ width: "100%", maxWidth: 960 }}>
182
+ <Carousel
183
+ fields={tabbedFields}
184
+ hasProductCards={true}
185
+ hasTestimonialCards={false}
186
+ disclaimerText={
187
+ "Price includes a $5.00/mo credit with AutoPay, option to disable at the next step."
188
+ }
189
+ backgroundColor="bg-white"
190
+ showSwitch={true}
191
+ activeTab={activeTab}
192
+ setActiveTab={setActiveTab}
193
+ tabs={["Business Ready", "Internet Only"]}
194
+ />
195
+ </div>
196
+ );
197
+ };
198
+
199
+ /**
200
+ * Reproduces the reported gap: switching to the "Internet Only" tab (whose cards
201
+ * have no gift badge) must collapse the gift slot so the Select plan CTA sits
202
+ * flush with the description, while the "Business Ready" tab (gift badges
203
+ * present) keeps the slot reserved so CTAs stay aligned.
204
+ */
205
+ export const TabbedGiftAlignment: Story = {
206
+ parameters: { layout: "fullscreen" },
207
+ render: () => <TabbedTemplate />,
208
+ };
209
+
210
+ const mixedGiftFields: carouselFieldsInterface = {
211
+ __typename: "ComponentCarousel",
212
+ sys: { id: "carousel-mixed-gift" },
213
+ title: "Mixed gift / no-gift cards",
214
+ subTitle: "The no-gift card reserves the gift height so both CTAs align.",
215
+ twoColumn: false,
216
+ backgroundColor: "white",
217
+ items: {
218
+ items: [
219
+ sampleProduct(
220
+ "mixed-1",
221
+ "fiber 1 gig",
222
+ "99",
223
+ true,
224
+ "Fast, secure connectivity solution for growing teams.",
225
+ mastercardGift("100")
226
+ ),
227
+ sampleProduct(
228
+ "mixed-2",
229
+ "fiber 2 gig",
230
+ "129",
231
+ false,
232
+ "Ultra-fast connectivity solution for high-traffic businesses."
233
+ ),
234
+ ],
235
+ },
236
+ };
237
+
238
+ /**
239
+ * One card has a single gift badge, the other has none. The no-gift card must
240
+ * reserve the gift-slot height so both Select plan CTAs sit at the same height,
241
+ * while the gift card's CTA sits directly below its badge (no over-reservation).
242
+ */
243
+ export const MixedGiftAlignment: Story = {
244
+ parameters: { layout: "fullscreen" },
245
+ args: {
246
+ fields: mixedGiftFields,
247
+ hasProductCards: true,
248
+ hasTestimonialCards: false,
249
+ disclaimerText: null,
250
+ backgroundColor: "bg-white",
251
+ activeTab: "",
252
+ setActiveTab: () => {},
253
+ tabs: [],
254
+ },
255
+ };
256
+
257
+ const tabbedMixedFields: carouselFieldsInterface = {
258
+ __typename: "ComponentCarousel",
259
+ sys: { id: "carousel-tabbed-mixed-gift" },
260
+ title: "fiber internet plans that fit your way of working",
261
+ twoColumn: false,
262
+ backgroundColor: "white",
263
+ items: {
264
+ items: [
265
+ // "Business Ready" tab — one card has a gift badge, one does not. The
266
+ // no-gift card must reserve the gift height so both CTAs align.
267
+ sampleProduct(
268
+ "tm-br-1",
269
+ "fiber 1 gig",
270
+ "99",
271
+ true,
272
+ "Fast, secure connectivity solution for growing teams.",
273
+ mastercardGift("100"),
274
+ "Business Ready"
275
+ ),
276
+ sampleProduct(
277
+ "tm-br-2",
278
+ "fiber 2 gig",
279
+ "129",
280
+ false,
281
+ "Ultra-fast connectivity solution for high-traffic businesses.",
282
+ { list: { items: [] } },
283
+ "Business Ready"
284
+ ),
285
+ // "Internet Only" tab — neither card has a gift badge, so both gift slots
286
+ // collapse and both CTAs sit flush with the description (recalculated for
287
+ // this tab on switch).
288
+ sampleProduct(
289
+ "tm-io-1",
290
+ "fiber 300 mbps",
291
+ "49",
292
+ false,
293
+ "Ideal for guest Wi-Fi, multiple POS devices, video uploads and streaming.",
294
+ { list: { items: [] } },
295
+ "Internet Only"
296
+ ),
297
+ sampleProduct(
298
+ "tm-io-2",
299
+ "fiber 500 mbps",
300
+ "59",
301
+ true,
302
+ "Great for busy storefronts with several connected devices.",
303
+ { list: { items: [] } },
304
+ "Internet Only"
305
+ ),
306
+ ],
307
+ },
308
+ };
309
+
310
+ const TabbedMixedTemplate = () => {
311
+ const [activeTab, setActiveTab] = useState("Business Ready");
312
+ return (
313
+ <div style={{ width: "100%", maxWidth: 960 }}>
314
+ <Carousel
315
+ fields={tabbedMixedFields}
316
+ hasProductCards={true}
317
+ hasTestimonialCards={false}
318
+ disclaimerText={
319
+ "Price includes a $5.00/mo credit with AutoPay, option to disable at the next step."
320
+ }
321
+ backgroundColor="bg-white"
322
+ showSwitch={true}
323
+ activeTab={activeTab}
324
+ setActiveTab={setActiveTab}
325
+ tabs={["Business Ready", "Internet Only"]}
326
+ />
327
+ </div>
328
+ );
329
+ };
330
+
331
+ /**
332
+ * Tab-switch coverage: the "Business Ready" tab mixes one gift card with one
333
+ * no-gift card (the no-gift card reserves the gift height so both CTAs align),
334
+ * while the "Internet Only" tab has two no-gift cards (both gift slots collapse
335
+ * and the CTAs sit flush with the description). Switching tabs recalculates the
336
+ * gift-slot height for only the visible cards.
337
+ */
338
+ export const TabbedMixedGiftAlignment: Story = {
339
+ parameters: { layout: "fullscreen" },
340
+ render: () => <TabbedMixedTemplate />,
341
+ };
342
+
343
+ const twoColumnRowGiftFields: carouselFieldsInterface = {
344
+ __typename: "ComponentCarousel",
345
+ sys: { id: "carousel-two-column-row-gift" },
346
+ title: "Two-column rows: per-row gift height",
347
+ subTitle:
348
+ "On mobile (resize below 768px) the giftless top row collapses while the gift row below reserves its own height.",
349
+ twoColumn: true,
350
+ backgroundColor: "white",
351
+ items: {
352
+ items: [
353
+ // Row 1 — both giftless: the gift slot must collapse so these CTAs sit
354
+ // flush with the description (they must NOT borrow row 2's gift height).
355
+ sampleProduct(
356
+ "row-1a",
357
+ "fiber 100 mbps",
358
+ "19",
359
+ false,
360
+ "Reliable support for essentials like email, browsing, and video chat."
361
+ ),
362
+ sampleProduct(
363
+ "row-1b",
364
+ "fiber 300 mbps",
365
+ "34",
366
+ false,
367
+ "Good for most day-to-day internet uses including streaming video."
368
+ ),
369
+ // Row 2 — both have a gift badge: this row reserves its own gift height.
370
+ sampleProduct(
371
+ "row-2a",
372
+ "fiber 1 gig",
373
+ "39",
374
+ false,
375
+ "Boosted speed and capacity for working from a home office.",
376
+ mastercardGift("100")
377
+ ),
378
+ sampleProduct(
379
+ "row-2b",
380
+ "fiber 2 gig",
381
+ "59",
382
+ false,
383
+ "Ultra-fast speeds for large smart homes.",
384
+ mastercardGift("200")
385
+ ),
386
+ ],
387
+ },
388
+ };
389
+
390
+ /**
391
+ * Two-column mobile regression guard: gift-slot height is equalized per row
392
+ * (the cards next to each other), never across the whole tab. Resize below
393
+ * 768px — row 1 (both giftless) collapses so its CTAs sit flush with the
394
+ * description, while row 2 (both with a gift) reserves only its own height.
395
+ */
396
+ export const TwoColumnRowGiftAlignment: Story = {
397
+ parameters: { layout: "fullscreen" },
398
+ args: {
399
+ fields: twoColumnRowGiftFields,
400
+ hasProductCards: true,
401
+ hasTestimonialCards: false,
402
+ disclaimerText: null,
403
+ backgroundColor: "bg-white",
404
+ activeTab: "",
405
+ setActiveTab: () => {},
406
+ tabs: [],
407
+ },
408
+ };