@windstream/react-shared-components 0.2.77 → 0.2.78

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.
@@ -42,7 +42,11 @@ function ProductCardPanel({
42
42
  // When unset, fall back to the count heuristic: two-column once there are
43
43
  // more than 3 cards; 3 or fewer stack vertically.
44
44
  const twoColumn = fields?.twoColumn ?? items.length > 3;
45
- const isCarousel = items.length > 2;
45
+ // Product cards render as a horizontal carousel whenever there is more than
46
+ // one card so they stay side by side (horizontal) at tablet/desktop widths,
47
+ // matching the legacy residential layout, instead of wrapping to a vertical
48
+ // stack. A lone card renders centered via the non-carousel branch.
49
+ const isCarousel = items.length > 1;
46
50
  // When any card is flagged best-value, every card reserves the banner height
47
51
  // so plan name/price stay aligned across a two-column row.
48
52
  const hasAnyBadge = items.some((item: any) => item?.highlighted);
@@ -67,6 +71,14 @@ function ProductCardPanel({
67
71
  const containerWidth = windowRef.current?.offsetWidth || 0;
68
72
  // Skip while hidden (offsetWidth is 0) to avoid overestimating maxIndex
69
73
  if (containerWidth === 0) return;
74
+ // If the whole row already fits, center it (maxIndex 0) rather than
75
+ // subtracting the ml-8 scroll indent — doing so on an exact fit would
76
+ // otherwise clip the last card and enable needless scrolling.
77
+ const fullRowWidth = items.length * cardWidth - gap; // no trailing gap
78
+ if (fullRowWidth <= containerWidth) {
79
+ setMaxIndex(0);
80
+ return;
81
+ }
70
82
  const usableWidth = containerWidth - trackMargin;
71
83
  const visibleCards = Math.max(
72
84
  1,
@@ -143,8 +155,12 @@ function ProductCardPanel({
143
155
  useEffect(() => {
144
156
  if (!isCarousel || !isVisible) return;
145
157
 
158
+ // Capture the ref array once so the resize handler and cleanup operate on a
159
+ // stable reference (satisfies react-hooks/exhaustive-deps for the cleanup).
160
+ const cardsSnapshot = cardsRef.current;
161
+
146
162
  const equalizeHeights = () => {
147
- const cards = cardsRef.current.filter(Boolean) as HTMLDivElement[];
163
+ const cards = cardsSnapshot.filter(Boolean) as HTMLDivElement[];
148
164
  if (cards.length === 0) return;
149
165
 
150
166
  // Reset heights first so each card reports its natural height
@@ -152,6 +168,30 @@ function ProductCardPanel({
152
168
  card.style.height = "auto";
153
169
  });
154
170
 
171
+ // Equalize the plan-name/price header AND the description across cards so
172
+ // the "Select plan" CTA below them starts at the same Y. A longer plan
173
+ // name / wider price (e.g. "$34" vs "$19") can wrap to an extra header
174
+ // line, and below md the fixed description height (md:h-[56px]) does not
175
+ // apply, so differing description lengths would otherwise leave the CTAs
176
+ // at different heights. Reserve the tallest height for each section.
177
+ const equalizeSection = (selector: string) => {
178
+ const els = cards
179
+ .map(card => card.querySelector<HTMLElement>(selector))
180
+ .filter(Boolean) as HTMLElement[];
181
+ els.forEach(el => el.style.removeProperty("min-height"));
182
+ const max = Math.max(
183
+ 0,
184
+ ...els.map(el => el.getBoundingClientRect().height)
185
+ );
186
+ if (max > 0) {
187
+ els.forEach(el =>
188
+ el.style.setProperty("min-height", `${max}px`, "important")
189
+ );
190
+ }
191
+ };
192
+ equalizeSection("[data-pc-header]");
193
+ equalizeSection("[data-pc-description]");
194
+
155
195
  // Find the tallest card
156
196
  const maxHeight = Math.max(...cards.map(card => card.offsetHeight));
157
197
 
@@ -176,6 +216,14 @@ function ProductCardPanel({
176
216
  cancelAnimationFrame(rafId);
177
217
  clearTimeout(timeoutId);
178
218
  window.removeEventListener("resize", equalizeHeights);
219
+ (cardsSnapshot.filter(Boolean) as HTMLDivElement[]).forEach(card => {
220
+ card
221
+ .querySelector<HTMLElement>("[data-pc-header]")
222
+ ?.style.removeProperty("min-height");
223
+ card
224
+ .querySelector<HTMLElement>("[data-pc-description]")
225
+ ?.style.removeProperty("min-height");
226
+ });
179
227
  };
180
228
  }, [isCarousel, isVisible, desktopExpanded, items.length]);
181
229
 
@@ -420,7 +468,7 @@ function ProductCardPanel({
420
468
  : "flex flex-wrap justify-center"
421
469
  )}
422
470
  >
423
- {items.map((item, index) => renderCard(item, index, twoColumn))}
471
+ {items.map((item, index) => renderCard(item, index, true))}
424
472
  </div>
425
473
  ) : (
426
474
  <>
@@ -428,7 +476,12 @@ function ProductCardPanel({
428
476
  <div
429
477
  ref={mobileGridRef}
430
478
  className={cx(
431
- "md:hidden",
479
+ // Two-column layouts keep their stacked/grid view until md (768px)
480
+ // so the tablet 2-up grid and its CTA alignment are preserved;
481
+ // single-row layouts switch to the horizontal carousel from sm
482
+ // (640px) so they stay horizontal on tablets (matching the legacy
483
+ // residential layout) instead of wrapping to a vertical stack.
484
+ twoColumn ? "md:hidden" : "sm:hidden",
432
485
  twoColumn ? "grid grid-cols-2 gap-4" : "flex flex-col gap-6"
433
486
  )}
434
487
  >
@@ -436,10 +489,19 @@ function ProductCardPanel({
436
489
  </div>
437
490
 
438
491
  {/* Desktop View: Horizontal Carousel */}
439
- <div className="relative hidden w-full md:block">
440
- {/* Navigation Arrows */}
492
+ <div
493
+ className={cx(
494
+ "relative hidden w-full",
495
+ twoColumn ? "md:block" : "sm:block"
496
+ )}
497
+ >
498
+ {/* Navigation Arrows. Below md the carousel can appear as early as
499
+ sm (single-row layouts), where the enclosing block has little
500
+ horizontal room and overflow-hidden; keep the arrows at the
501
+ window edges (inside) there, and restore the outset desktop
502
+ positioning from md up. */}
441
503
  {showArrows && (
442
- <div className="pointer-events-none absolute -left-16 -right-16 top-[50%] z-30 flex -translate-y-1/2 justify-between px-4 md:px-10">
504
+ <div className="pointer-events-none absolute left-0 right-0 top-[50%] z-30 flex -translate-y-1/2 justify-between px-4 md:-left-16 md:-right-16 md:px-10">
443
505
  {currentIndex > 0 ? (
444
506
  <Button
445
507
  onClick={prevSlide}
@@ -477,7 +539,12 @@ function ProductCardPanel({
477
539
  >
478
540
  <div
479
541
  className={cx(
480
- "ml-8 flex items-stretch gap-4 pb-4",
542
+ "flex items-stretch gap-4 py-4",
543
+ // Center the row when every card already fits (no scrolling
544
+ // needed) so a short row (e.g. two cards) sits under the
545
+ // centered title; only indent for the scrollable state so the
546
+ // first card clears the left arrow.
547
+ maxIndex === 0 ? "justify-center" : "ml-8",
481
548
  isSwiping
482
549
  ? "transition-none"
483
550
  : "transition-transform duration-500 ease-in-out"