@windstream/react-shared-components 0.1.80 → 0.1.86

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.1.80",
3
+ "version": "0.1.86",
4
4
  "type": "module",
5
5
  "description": "Shared React components for Kinetic applications",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,129 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { AnimationType, AnimationWrapperProps } from "./types";
5
+ import { motion, useReducedMotion } from "framer-motion";
6
+
7
+ interface AnimationPreset {
8
+ whileHover: Record<string, unknown>;
9
+ whileTap?: Record<string, unknown>;
10
+ transition: Record<string, unknown>;
11
+ }
12
+
13
+ const ANIMATION_PRESETS: Record<AnimationType, AnimationPreset> = {
14
+ scale: {
15
+ whileHover: { scale: 1.02 },
16
+ whileTap: { scale: 0.98 },
17
+ transition: { duration: 0.3 },
18
+ },
19
+ shadow: {
20
+ whileHover: { boxShadow: "0 10px 25px rgba(0,0,0,0.15)" },
21
+ transition: { duration: 0.3 },
22
+ },
23
+ lift: {
24
+ whileHover: { y: -5 },
25
+ whileTap: { y: 0 },
26
+ transition: { type: "spring", stiffness: 300, damping: 20 },
27
+ },
28
+ opacity: {
29
+ whileHover: { opacity: 0.8 },
30
+ transition: { duration: 0.2 },
31
+ },
32
+ grow: {
33
+ whileHover: { scale: 1.1 },
34
+ whileTap: { scale: 0.95 },
35
+ transition: { type: "spring", stiffness: 300, damping: 20 },
36
+ },
37
+ };
38
+
39
+ function mergePresets(animationType?: AnimationType | AnimationType[]) {
40
+ if (!animationType) return undefined;
41
+
42
+ const types = Array.isArray(animationType) ? animationType : [animationType];
43
+ if (types.length === 0) return undefined;
44
+
45
+ return types.reduce<AnimationPreset>(
46
+ (merged, type) => {
47
+ const preset = ANIMATION_PRESETS[type];
48
+ return {
49
+ whileHover: { ...merged.whileHover, ...preset.whileHover },
50
+ ...(merged.whileTap || preset.whileTap
51
+ ? {
52
+ whileTap: { ...merged.whileTap, ...preset.whileTap },
53
+ }
54
+ : {}),
55
+ transition: { ...merged.transition, ...preset.transition },
56
+ };
57
+ },
58
+ { whileHover: {}, transition: {} }
59
+ );
60
+ }
61
+
62
+ export function AnimationWrapper({
63
+ children,
64
+ animationType,
65
+ // default to true for backward compatibility
66
+ disableAnimation = true,
67
+ whileHover,
68
+ whileTap,
69
+ transition,
70
+ ...motionProps
71
+ }: AnimationWrapperProps) {
72
+ const prefersReducedMotion = useReducedMotion();
73
+
74
+ const child = React.Children.only(children) as React.ReactElement<
75
+ Record<string, unknown>
76
+ >;
77
+
78
+ if (disableAnimation || prefersReducedMotion) {
79
+ return child;
80
+ }
81
+
82
+ const preset = mergePresets(animationType);
83
+
84
+ const mergedWhileHover = preset
85
+ ? { ...preset.whileHover, ...(whileHover as Record<string, unknown>) }
86
+ : whileHover;
87
+
88
+ const mergedWhileTap =
89
+ preset?.whileTap || whileTap
90
+ ? { ...preset?.whileTap, ...(whileTap as Record<string, unknown>) }
91
+ : undefined;
92
+
93
+ const mergedTransition = preset
94
+ ? { ...preset.transition, ...(transition as Record<string, unknown>) }
95
+ : transition;
96
+
97
+ const isIntrinsicElement = typeof child.type === "string";
98
+
99
+ if (isIntrinsicElement) {
100
+ const MotionComponent = motion[
101
+ child.type as keyof typeof motion
102
+ ] as React.ComponentType<Record<string, unknown>>;
103
+
104
+ return (
105
+ <MotionComponent
106
+ {...child.props}
107
+ whileHover={mergedWhileHover}
108
+ whileTap={mergedWhileTap}
109
+ transition={mergedTransition}
110
+ {...motionProps}
111
+ />
112
+ );
113
+ }
114
+
115
+ return (
116
+ <motion.div
117
+ whileHover={mergedWhileHover}
118
+ whileTap={mergedWhileTap}
119
+ transition={mergedTransition}
120
+ {...motionProps}
121
+ >
122
+ {child}
123
+ </motion.div>
124
+ );
125
+ }
126
+
127
+ AnimationWrapper.displayName = "AnimationWrapper";
128
+
129
+ export type { AnimationWrapperProps, AnimationType };
@@ -0,0 +1,11 @@
1
+ import { ReactElement } from "react";
2
+ import { HTMLMotionProps } from "framer-motion";
3
+
4
+ export type AnimationType = "scale" | "shadow" | "lift" | "opacity" | "grow";
5
+
6
+ export interface AnimationWrapperProps
7
+ extends Omit<HTMLMotionProps<"div">, "children"> {
8
+ children: ReactElement;
9
+ animationType?: AnimationType | AnimationType[];
10
+ disableAnimation?: boolean;
11
+ }
@@ -6,6 +6,7 @@ import FullImageCard from "../cards/full-image-card";
6
6
  import SimpleCard from "../cards/simple-card";
7
7
  import { CalloutCardType, CalloutItem, CalloutProps } from "./types";
8
8
 
9
+ import { AnimationWrapper } from "@shared/components/animation-wrapper";
9
10
  import { Text } from "@shared/components/text";
10
11
  import { cx } from "@shared/utils";
11
12
 
@@ -79,6 +80,8 @@ export const Callout: React.FC<CalloutProps> = ({
79
80
  cardStackingMobile = true,
80
81
  cardsWidth = true,
81
82
  noGutter = false,
83
+ disableAnimation = false,
84
+ animationType = "scale",
82
85
  }) => {
83
86
  const itemCount = items?.length ?? 0;
84
87
  const desktopCols = clampCol(
@@ -227,13 +230,24 @@ export const Callout: React.FC<CalloutProps> = ({
227
230
  <div className={cx("card-holder", gridClass)}>
228
231
  {items.map((item, index: number) =>
229
232
  isStackMode ? (
230
- renderCard(item, index)
233
+ <AnimationWrapper
234
+ key={`callout-card-${index}`}
235
+ animationType={animationType}
236
+ disableAnimation={disableAnimation}
237
+ >
238
+ {renderCard(item, index)}
239
+ </AnimationWrapper>
231
240
  ) : (
232
241
  <div
233
242
  key={`callout-card-${index}`}
234
243
  className={cx("callout-card", cardWidthClass)}
235
244
  >
236
- {renderCard(item, index)}
245
+ <AnimationWrapper
246
+ animationType={animationType}
247
+ disableAnimation={disableAnimation}
248
+ >
249
+ {renderCard(item, index)}
250
+ </AnimationWrapper>
237
251
  </div>
238
252
  )
239
253
  )}
@@ -1,6 +1,8 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { ButtonProps } from "../button/types";
3
3
 
4
+ import type { AnimationType } from "@shared/components/animation-wrapper";
5
+
4
6
  export type CalloutCardType = "simple" | "blog" | "fullImage" | "floatingImage";
5
7
 
6
8
  export type CalloutCtaProps = ButtonProps & {
@@ -69,4 +71,8 @@ export type CalloutProps = {
69
71
  containerClassName?: string;
70
72
  /** Extra class names for the inner content wrapper. */
71
73
  innerClassName?: string;
74
+ /** Disable card hover/tap animations. */
75
+ disableAnimation?: boolean;
76
+ /** Animation type(s) applied to each card. */
77
+ animationType?: AnimationType | AnimationType[];
72
78
  };
@@ -13,13 +13,15 @@ import { useCarouselSwipe } from "@shared/hooks/use-carousel-swipe";
13
13
  import { CheckPlansProps } from "@shared/types/micro-components";
14
14
  import { cx } from "@shared/utils";
15
15
 
16
- export function ProductCardCarousel({
16
+ function ProductCardPanel({
17
17
  fields,
18
18
  renderCheckPlans,
19
+ isVisible = true,
19
20
  }: {
20
21
  fields: CarouselWithProductCards;
21
22
  onModalButtonClick?: (id?: string) => void;
22
23
  renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
24
+ isVisible?: boolean;
23
25
  }) {
24
26
  const itemsExpanded = fields?.items?.items?.[0]?.benefitsExpanded || false;
25
27
  const [desktopExpanded, setDesktopExpanded] = useState(itemsExpanded);
@@ -29,6 +31,7 @@ export function ProductCardCarousel({
29
31
  const [currentIndex, setCurrentIndex] = useState(0);
30
32
  const items = fields?.items?.items || [];
31
33
  const isCarousel = items.length > 2;
34
+ const showArrows = fields?.showArrows !== false && isCarousel;
32
35
  const cardsRef = useRef<(HTMLDivElement | null)[]>([]);
33
36
 
34
37
  const prevSlide = useCallback(() => {
@@ -43,7 +46,7 @@ export function ProductCardCarousel({
43
46
 
44
47
  // Equalize card heights
45
48
  useEffect(() => {
46
- if (!isCarousel) return;
49
+ if (!isCarousel || !isVisible) return;
47
50
 
48
51
  const equalizeHeights = () => {
49
52
  const cards = cardsRef.current.filter(Boolean) as HTMLDivElement[];
@@ -70,7 +73,7 @@ export function ProductCardCarousel({
70
73
  const timeoutId = setTimeout(equalizeHeights, 100);
71
74
 
72
75
  return () => clearTimeout(timeoutId);
73
- }, [isCarousel, desktopExpanded, items.length]);
76
+ }, [isCarousel, isVisible, desktopExpanded, items.length]);
74
77
 
75
78
  if (!items.length && !fields?.title) {
76
79
  return null;
@@ -177,23 +180,25 @@ export function ProductCardCarousel({
177
180
  {/* Desktop View: Horizontal Carousel */}
178
181
  <div className="relative hidden w-full md:block">
179
182
  {/* Navigation Arrows */}
180
- <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">
181
- <Button
182
- onClick={prevSlide}
183
- className="pointer-events-auto flex h-12 w-12 items-center justify-center rounded-full border border-gray-100 bg-white p-2 text-text shadow-cardDrop transition-all hover:bg-gray-50"
184
- aria-label="Previous"
185
- >
186
- <MaterialIcon name="arrow_back" size={24} />
187
- </Button>
188
-
189
- <Button
190
- onClick={nextSlide}
191
- className="pointer-events-auto flex h-12 w-12 items-center justify-center rounded-full border border-gray-100 bg-white p-2 text-text shadow-cardDrop transition-all hover:bg-gray-50"
192
- aria-label="Next"
193
- >
194
- <MaterialIcon name="arrow_forward" size={24} />
195
- </Button>
196
- </div>
183
+ {showArrows && (
184
+ <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">
185
+ <Button
186
+ onClick={prevSlide}
187
+ className="pointer-events-auto flex h-12 w-12 items-center justify-center rounded-full border border-gray-100 bg-white p-2 text-text shadow-cardDrop transition-all hover:bg-gray-50"
188
+ aria-label="Previous"
189
+ >
190
+ <MaterialIcon name="arrow_back" size={24} />
191
+ </Button>
192
+
193
+ <Button
194
+ onClick={nextSlide}
195
+ className="pointer-events-auto flex h-12 w-12 items-center justify-center rounded-full border border-gray-100 bg-white p-2 text-text shadow-cardDrop transition-all hover:bg-gray-50"
196
+ aria-label="Next"
197
+ >
198
+ <MaterialIcon name="arrow_forward" size={24} />
199
+ </Button>
200
+ </div>
201
+ )}
197
202
 
198
203
  {/* Carousel Window */}
199
204
  <div className="mx-auto max-w-[1280px] overflow-hidden">
@@ -211,6 +216,55 @@ export function ProductCardCarousel({
211
216
  );
212
217
  }
213
218
 
219
+ export function ProductCardCarousel({
220
+ fields,
221
+ renderCheckPlans,
222
+ activeTab,
223
+ tabs,
224
+ }: {
225
+ fields: CarouselWithProductCards;
226
+ onModalButtonClick?: (id?: string) => void;
227
+ renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
228
+ activeTab?: string;
229
+ tabs?: string[];
230
+ }) {
231
+ const allItems = fields?.items?.items || [];
232
+
233
+ if (tabs && tabs.length > 1 && activeTab) {
234
+ return (
235
+ <>
236
+ {tabs.map(tab => {
237
+ const tabItems = allItems.filter(item => {
238
+ const category = item.productCategory || tabs[0];
239
+ return category === tab;
240
+ });
241
+ const tabFields = {
242
+ ...fields,
243
+ items: { ...fields.items, items: tabItems },
244
+ };
245
+ return (
246
+ <div
247
+ key={tab}
248
+ style={{ display: tab === activeTab ? "block" : "none" }}
249
+ aria-hidden={tab !== activeTab}
250
+ >
251
+ <ProductCardPanel
252
+ fields={tabFields as CarouselWithProductCards}
253
+ renderCheckPlans={renderCheckPlans}
254
+ isVisible={tab === activeTab}
255
+ />
256
+ </div>
257
+ );
258
+ })}
259
+ </>
260
+ );
261
+ }
262
+
263
+ return (
264
+ <ProductCardPanel fields={fields} renderCheckPlans={renderCheckPlans} />
265
+ );
266
+ }
267
+
214
268
  /**
215
269
  * Individual slide component for the testimonial carousel
216
270
  * Memoized to prevent unnecessary re-renders of inactive slides
@@ -58,6 +58,8 @@ export const Carousel: React.FC<CarouselProps> = ({
58
58
  onModalButtonClick={onModalButtonClick}
59
59
  renderCheckPlans={renderCheckPlans}
60
60
  fields={fields as CarouselWithProductCards}
61
+ activeTab={activeTab}
62
+ tabs={tabs}
61
63
  />
62
64
  )}
63
65
  </div>
@@ -64,6 +64,7 @@ export interface ProductCardFields {
64
64
  benefitsTitle?: string;
65
65
  benefitsExpanded?: boolean;
66
66
  innerBadge?: string;
67
+ productCategory?: string;
67
68
  // GraphQL patterns for nested references
68
69
  benefits?: {
69
70
  items: Array<any>;
package/src/index.ts CHANGED
@@ -83,6 +83,12 @@ export type { SkeletonProps } from "./components/skeleton";
83
83
  export { Tooltip } from "./components/tooltip";
84
84
  export type { ToolTipProps } from "./components/tooltip";
85
85
 
86
+ export { AnimationWrapper } from "./components/animation-wrapper";
87
+ export type {
88
+ AnimationWrapperProps,
89
+ AnimationType,
90
+ } from "./components/animation-wrapper";
91
+
86
92
  export { ViewCartButton } from "./components/view-cart-button";
87
93
  export type { ViewCartButtonProps } from "./components/view-cart-button";
88
94