@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/dist/contentful/index.d.ts +6 -0
- package/dist/contentful/index.esm.js +3 -3
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +3 -3
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +16 -3
- package/dist/index.d.ts +21 -8
- package/dist/index.esm.js +6 -6
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/animation-wrapper/index.tsx +129 -0
- package/src/components/animation-wrapper/types.ts +11 -0
- package/src/contentful/blocks/callout/index.tsx +16 -2
- package/src/contentful/blocks/callout/types.ts +6 -0
- package/src/contentful/blocks/carousel/helper.tsx +74 -20
- package/src/contentful/blocks/carousel/index.tsx +2 -0
- package/src/contentful/blocks/carousel/types.ts +1 -0
- package/src/index.ts +6 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
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
|
|