Initial commit

This commit is contained in:
DK
2026-01-19 19:37:33 +00:00
commit 129a8a27d0
259 changed files with 49969 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
"use client";
import TimelineHorizontalCardStack from "@/components/cardStack/layouts/timelines/TimelineHorizontalCardStack";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: number;
title: string;
description: string;
imageAlt?: string;
videoAriaLabel?: string;
} & (
| { imageSrc: string; videoSrc?: never }
| { videoSrc: string; imageSrc?: never }
);
interface FeatureCardEightProps {
features: FeatureCard[];
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardClassName?: string;
progressBarClassName?: string;
cardContentClassName?: string;
stepNumberClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
mediaContainerClassName?: string;
mediaClassName?: string;
}
const FeatureCardEight = ({
features,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardClassName = "",
progressBarClassName = "",
cardContentClassName = "",
stepNumberClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
mediaContainerClassName = "",
mediaClassName = "",
}: FeatureCardEightProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const mediaItems = features.map((feature) => ({
imageSrc: feature.imageSrc,
videoSrc: feature.videoSrc,
imageAlt: feature.imageAlt || feature.title,
videoAriaLabel: feature.videoAriaLabel || feature.title,
}));
return (
<TimelineHorizontalCardStack
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
mediaItems={mediaItems}
className={className}
containerClassName={containerClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
cardClassName={cardClassName}
progressBarClassName={progressBarClassName}
mediaContainerClassName={mediaContainerClassName}
mediaClassName={mediaClassName}
ariaLabel={ariaLabel}
>
{features.map((feature) => (
<div
key={feature.id}
className={cls("relative z-1 w-full min-h-0 h-fit flex flex-col gap-3", cardContentClassName)}
>
<div
className={cls(
"h-8 w-[var(--height-8)] primary-button text-background rounded-theme flex items-center justify-center",
stepNumberClassName
)}
>
<p className="text-sm truncate">
{feature.id}
</p>
</div>
<h2 className={cls("mt-1 text-3xl font-medium leading-[1.15] text-balance", shouldUseLightText && "text-background", cardTitleClassName)}>
{feature.title}
</h2>
<p className={cls("text-base leading-[1.15] text-balance", shouldUseLightText ? "text-background" : "text-foreground", cardDescriptionClassName)}>
{feature.description}
</p>
</div>
))}
</TimelineHorizontalCardStack>
);
};
FeatureCardEight.displayName = "FeatureCardEight";
export default FeatureCardEight;

View File

@@ -0,0 +1,255 @@
"use client";
import { memo } from "react";
import CardStack from "@/components/cardStack/CardStack";
import MediaContent from "@/components/shared/MediaContent";
import Tag from "@/components/shared/Tag";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: string;
title: string;
description: string;
tag: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
buttons?: ButtonConfig[];
onCardClick?: () => void;
};
interface FeatureCardMediaProps {
features: FeatureCard[];
carouselMode?: "auto" | "buttons";
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
itemClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
tagClassName?: string;
contentClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardButtonContainerClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
interface FeatureCardItemProps {
feature: FeatureCard;
shouldUseLightText: boolean;
useInvertedBackground: InvertedBackground;
itemClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
tagClassName?: string;
contentClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardButtonContainerClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
}
const FeatureCardItem = memo(({
feature,
shouldUseLightText,
useInvertedBackground,
itemClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
tagClassName = "",
contentClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardButtonContainerClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
}: FeatureCardItemProps) => {
const theme = useTheme();
return (
<article
className={cls("relative h-full flex flex-col gap-6 cursor-pointer group", itemClassName)}
onClick={feature.onCardClick}
role="article"
aria-label={feature.title}
>
<div className={cls("relative w-full aspect-square overflow-hidden rounded-theme-capped", mediaWrapperClassName)}>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || feature.title}
videoAriaLabel={feature.videoAriaLabel || feature.title}
imageClassName={cls("w-full h-full object-cover transition-transform duration-500 ease-in-out group-hover:scale-105", mediaClassName)}
/>
<div className="absolute top-4 right-4">
<Tag
text={feature.tag}
useInvertedBackground={useInvertedBackground}
className={tagClassName}
/>
</div>
</div>
<div className={cls("relative z-1 card rounded-theme-capped p-6 flex flex-col gap-2", contentClassName)}>
<h3 className={cls(
"text-xl md:text-2xl font-medium leading-tight",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{feature.title}
</h3>
<p className={cls(
"text-base leading-tight",
shouldUseLightText ? "text-background/75" : "text-foreground/75",
cardDescriptionClassName
)}>
{feature.description}
</p>
{feature.buttons && feature.buttons.length > 0 && (
<div className={cls("flex flex-wrap gap-4 mt-2", cardButtonContainerClassName)}>
{feature.buttons.slice(0, 2).map((button, index) => (
<Button
key={`${button.text}-${index}`}
{...getButtonProps(button, index, theme.defaultButtonVariant, cardButtonClassName, cardButtonTextClassName)}
/>
))}
</div>
)}
</div>
</article>
);
});
FeatureCardItem.displayName = "FeatureCardItem";
const FeatureCardMedia = ({
features,
carouselMode = "buttons",
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Features section",
className = "",
containerClassName = "",
itemClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
tagClassName = "",
contentClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardButtonContainerClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureCardMediaProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant="uniform-all-items-equal"
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
ariaLabel={ariaLabel}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
>
{features.map((feature) => (
<FeatureCardItem
key={feature.id}
feature={feature}
shouldUseLightText={shouldUseLightText}
useInvertedBackground={useInvertedBackground}
itemClassName={itemClassName}
mediaWrapperClassName={mediaWrapperClassName}
mediaClassName={mediaClassName}
tagClassName={tagClassName}
contentClassName={contentClassName}
cardTitleClassName={cardTitleClassName}
cardDescriptionClassName={cardDescriptionClassName}
cardButtonContainerClassName={cardButtonContainerClassName}
cardButtonClassName={cardButtonClassName}
cardButtonTextClassName={cardButtonTextClassName}
/>
))}
</CardStack>
);
};
FeatureCardMedia.displayName = "FeatureCardMedia";
export default FeatureCardMedia;

View File

@@ -0,0 +1,199 @@
"use client";
import TimelinePhoneView from "@/components/cardStack/layouts/timelines/TimelinePhoneView";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { TimelinePhoneViewItem } from "@/components/cardStack/hooks/usePhoneAnimations";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeaturePhone = {
imageAlt?: string;
videoAriaLabel?: string;
} & (
| { imageSrc: string; videoSrc?: never }
| { videoSrc: string; imageSrc?: never }
);
type FeatureCard = {
id: number;
title: string;
description: string;
phoneOne: FeaturePhone;
phoneTwo: FeaturePhone;
};
interface FeatureCardNineProps {
features: FeatureCard[];
showStepNumbers: boolean;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
desktopContainerClassName?: string;
mobileContainerClassName?: string;
desktopContentClassName?: string;
desktopWrapperClassName?: string;
mobileWrapperClassName?: string;
phoneFrameClassName?: string;
mobilePhoneFrameClassName?: string;
featureContentClassName?: string;
stepNumberClassName?: string;
featureTitleClassName?: string;
featureDescriptionClassName?: string;
}
interface FeatureContentProps {
feature: FeatureCard;
showStepNumbers: boolean;
useInvertedBackground: InvertedBackground;
featureContentClassName: string;
stepNumberClassName: string;
featureTitleClassName: string;
featureDescriptionClassName: string;
}
const FeatureContent = ({
feature,
showStepNumbers,
useInvertedBackground,
featureContentClassName,
stepNumberClassName,
featureTitleClassName,
featureDescriptionClassName,
}: FeatureContentProps) => (
<div className={cls("relative z-1 h-full w-content-width mx-auto md:w-full flex flex-col items-center text-center gap-3 md:px-5", featureContentClassName)}>
{showStepNumbers && (
<div
className={cls(
"h-8 w-[var(--height-8)] primary-button text-background rounded-theme flex items-center justify-center",
stepNumberClassName
)}
>
<p className="text-sm truncate">
{feature.id}
</p>
</div>
)}
<h2 className={cls("text-5xl font-medium leading-[1.15] text-balance", useInvertedBackground === "invertDefault" && "text-background", featureTitleClassName)}>
{feature.title}
</h2>
<p className={cls("text-base leading-[1.2] text-balance", useInvertedBackground === "invertDefault" ? "text-background/75" : "text-foreground/75", featureDescriptionClassName)}>
{feature.description}
</p>
</div>
);
const FeatureCardNine = ({
features,
showStepNumbers,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
desktopContainerClassName = "",
mobileContainerClassName = "",
desktopContentClassName = "",
desktopWrapperClassName = "",
mobileWrapperClassName = "",
phoneFrameClassName = "",
mobilePhoneFrameClassName = "",
featureContentClassName = "",
stepNumberClassName = "",
featureTitleClassName = "",
featureDescriptionClassName = "",
}: FeatureCardNineProps) => {
const items: TimelinePhoneViewItem[] = features.map((feature, index) => ({
trigger: `trigger-${index}`,
content: (
<FeatureContent
feature={feature}
showStepNumbers={showStepNumbers}
useInvertedBackground={useInvertedBackground}
featureContentClassName={featureContentClassName}
stepNumberClassName={stepNumberClassName}
featureTitleClassName={featureTitleClassName}
featureDescriptionClassName={featureDescriptionClassName}
/>
),
imageOne: feature.phoneOne.imageSrc,
videoOne: feature.phoneOne.videoSrc,
imageAltOne: feature.phoneOne.imageAlt || `${feature.title} - Phone 1`,
videoAriaLabelOne: feature.phoneOne.videoAriaLabel || `${feature.title} - Phone 1 video`,
imageTwo: feature.phoneTwo.imageSrc,
videoTwo: feature.phoneTwo.videoSrc,
imageAltTwo: feature.phoneTwo.imageAlt || `${feature.title} - Phone 2`,
videoAriaLabelTwo: feature.phoneTwo.videoAriaLabel || `${feature.title} - Phone 2 video`,
}));
return (
<TimelinePhoneView
items={items}
showTextBox={true}
showDivider={true}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
desktopContainerClassName={desktopContainerClassName}
mobileContainerClassName={mobileContainerClassName}
desktopContentClassName={desktopContentClassName}
desktopWrapperClassName={desktopWrapperClassName}
mobileWrapperClassName={mobileWrapperClassName}
phoneFrameClassName={phoneFrameClassName}
mobilePhoneFrameClassName={mobilePhoneFrameClassName}
ariaLabel={ariaLabel}
/>
);
};
FeatureCardNine.displayName = "FeatureCardNine";
export default FeatureCardNine;

View File

@@ -0,0 +1,193 @@
"use client";
import TimelineCardStack from "@/components/cardStack/layouts/timelines/TimelineCardStack";
import MediaContent from "@/components/shared/MediaContent";
import Tag from "@/components/shared/Tag";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: number;
tag: string;
title: string;
subtitle: string;
description: string;
buttons?: ButtonConfig[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface FeatureCardNineteenProps {
features: FeatureCard[];
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardContentClassName?: string;
cardTagClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
imageContainerClassName?: string;
imageClassName?: string;
}
const FeatureCardNineteen = ({
features,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
cardTagClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
imageContainerClassName = "",
imageClassName = "",
}: FeatureCardNineteenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<TimelineCardStack
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{features.map((feature) => {
const stepNumber = String(feature.id).padStart(2, "0");
return (
<div
key={feature.id}
className={cls("relative z-1 w-full min-h-0 h-full flex flex-col md:flex-row justify-between overflow-hidden", cardContentClassName)}
>
<div className="relative w-full md:w-1/2 md:h-full flex flex-col justify-between p-8 md:p-12">
<div className="flex flex-col gap-4 md:gap-6">
<Tag
text={feature.tag}
useInvertedBackground={useInvertedBackground}
className={cardTagClassName}
/>
<h2 className={cls(
"text-5xl md:text-7xl font-medium leading-none",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{feature.title}
</h2>
</div>
<div className="block md:hidden w-full h-px my-6 bg-accent/20" />
<div className="flex flex-col gap-2 md:gap-4">
<h3 className={cls(
"text-xl md:text-2xl font-medium",
shouldUseLightText ? "text-background" : "text-foreground"
)}>
{feature.subtitle}
</h3>
<p className={cls(
"text-base md:text-lg leading-tight",
shouldUseLightText ? "text-background/80" : "text-foreground/80",
cardDescriptionClassName
)}>
{feature.description}
</p>
{feature.buttons && feature.buttons.length > 0 && (
<div className="flex gap-4 mt-2">
{feature.buttons.slice(0, 2).map((button, buttonIndex) => (
<Button
key={`${button.text}-${buttonIndex}`}
{...getButtonProps(button, buttonIndex, theme.defaultButtonVariant, cardButtonClassName, cardButtonTextClassName)}
/>
))}
</div>
)}
</div>
</div>
<div className="relative w-full md:w-4/10 min-h-0 h-full flex flex-col gap-10 p-8 md:p-12">
<h3 className="hidden md:block text-8xl font-medium text-accent/20 self-end">
{stepNumber}
</h3>
<div
className={cls(
"relative max-h-full min-h-0 w-full h-full min-w-0 max-w-full md:aspect-[4/5] rounded-theme-capped overflow-hidden rotate-3",
imageContainerClassName
)}
>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || feature.title}
videoAriaLabel={feature.videoAriaLabel || feature.title}
imageClassName={cls("w-full h-full object-cover", imageClassName)}
/>
</div>
</div>
</div>
);
})}
</TimelineCardStack>
);
};
FeatureCardNineteen.displayName = "FeatureCardNineteen";
export default FeatureCardNineteen;

View File

@@ -0,0 +1,171 @@
"use client";
import CardStack from "@/components/cardStack/CardStack";
import MediaContent from "@/components/shared/MediaContent";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
title: string;
description: string;
button?: ButtonConfig;
} & (
| {
imageSrc: string;
imageAlt?: string;
videoSrc?: never;
videoAriaLabel?: never;
}
| {
videoSrc: string;
videoAriaLabel?: string;
imageSrc?: never;
imageAlt?: never;
}
);
interface FeatureCardOneProps {
features: FeatureCard[];
carouselMode?: "auto" | "buttons";
gridVariant: GridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
mediaClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
}
const FeatureCardOne = ({
features,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
mediaClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
}: FeatureCardOneProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{features.map((feature, index) => (
<div
key={`${feature.title}-${index}`}
className={cls("card flex flex-col gap-4 p-4 rounded-theme-capped min-h-0 h-full", cardClassName)}
>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || "Feature image"}
videoAriaLabel={feature.videoAriaLabel || "Feature video"}
imageClassName={cls("relative z-1 min-h-0 h-full", mediaClassName)}
/>
<div className="relative z-1 flex flex-col gap-1">
<h3 className={cls("text-2xl font-medium", shouldUseLightText && "text-background", cardTitleClassName)}>
{feature.title}
</h3>
<p className={cls("text-sm leading-[1.1]", shouldUseLightText ? "text-background" : "text-foreground", cardDescriptionClassName)}>
{feature.description}
</p>
</div>
{feature.button && (
<Button {...getButtonProps(feature.button, 0, theme.defaultButtonVariant, cls("w-full", cardButtonClassName), cardButtonTextClassName)} />
)}
</div>
))}
</CardStack>
);
};
FeatureCardOne.displayName = "FeatureCardOne";
export default FeatureCardOne;

View File

@@ -0,0 +1,159 @@
"use client";
import CardList from "@/components/cardStack/CardList";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: number;
title: string;
description: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface FeatureCardSevenProps {
features: FeatureCard[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardContentClassName?: string;
stepNumberClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
imageContainerClassName?: string;
imageClassName?: string;
}
const FeatureCardSeven = ({
features,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
stepNumberClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
imageContainerClassName = "",
imageClassName = "",
}: FeatureCardSevenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardList
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
cardClassName={cardClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{features.map((feature, index) => (
<div
key={feature.id}
className={cls("relative z-1 w-full min-h-0 h-full flex flex-col justify-between items-center p-6 gap-6 md:p-15 md:gap-15", index % 2 === 0 ? "md:flex-row" : "md:flex-row-reverse", cardContentClassName)}
>
<div className="w-full md:w-1/2 min-w-0 h-fit md:h-full flex flex-col justify-center">
<div className="w-full min-w-0 flex flex-col gap-3 md:gap-5">
<div
className={cls(
"h-8 w-[var(--height-8)] primary-button text-background rounded-theme flex items-center justify-center",
stepNumberClassName
)}
>
<p className="text-sm truncate">
{feature.id}
</p>
</div>
<h2 className={cls("mt-1 text-4xl md:text-5xl font-medium leading-[1.15] text-balance", shouldUseLightText && "text-background", cardTitleClassName)}>
{feature.title}
</h2>
<p className={cls("text-base leading-[1.15] text-balance", shouldUseLightText ? "text-background" : "text-foreground", cardDescriptionClassName)}>
{feature.description}
</p>
</div>
</div>
<div
className={cls(
"relative w-full md:w-1/2 aspect-square overflow-hidden rounded-theme-capped",
imageContainerClassName
)}
>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || feature.title}
videoAriaLabel={feature.videoAriaLabel || feature.title}
imageClassName={cls("w-full h-full object-cover", imageClassName)}
/>
</div>
</div>
))}
</CardList>
);
};
FeatureCardSeven.displayName = "FeatureCardSeven";
export default FeatureCardSeven;

View File

@@ -0,0 +1,153 @@
"use client";
import TimelineCardStack from "@/components/cardStack/layouts/timelines/TimelineCardStack";
import MediaContent from "@/components/shared/MediaContent";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: number;
title: string;
description: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
};
interface FeatureCardSixProps {
features: FeatureCard[];
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardContentClassName?: string;
stepNumberClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
imageContainerClassName?: string;
imageClassName?: string;
}
const FeatureCardSix = ({
features,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
stepNumberClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
imageContainerClassName = "",
imageClassName = "",
}: FeatureCardSixProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<TimelineCardStack
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{features.map((feature) => (
<div
key={feature.id}
className={cls("relative z-1 w-full min-h-0 h-full flex flex-col md:flex-row justify-between items-center p-10 gap-10 md:p-15 md:gap-15", cardContentClassName)}
>
<div className="w-full md:w-1/2 min-w-0 h-fit md:h-full flex flex-col justify-center">
<div className="w-full min-w-0 flex flex-col gap-3 md:gap-5">
<div
className={cls(
"h-8 w-[var(--height-8)] primary-button text-background rounded-theme flex items-center justify-center",
stepNumberClassName
)}
>
<p className="text-sm truncate">
{feature.id}
</p>
</div>
<h2 className={cls("mt-1 text-4xl md:text-5xl font-medium leading-[1.15] text-balance truncate", shouldUseLightText && "text-background", cardTitleClassName)}>
{feature.title}
</h2>
<p className={cls("text-base leading-[1.15] text-balance truncate", shouldUseLightText ? "text-background" : "text-foreground", cardDescriptionClassName)}>
{feature.description}
</p>
</div>
</div>
<div
className={cls(
"relative z-1 w-full md:w-1/2 min-h-0 h-full overflow-hidden rounded-theme-capped",
imageContainerClassName
)}
>
<MediaContent
imageSrc={feature.imageSrc}
videoSrc={feature.videoSrc}
imageAlt={feature.imageAlt || feature.title}
videoAriaLabel={feature.videoAriaLabel || feature.title}
imageClassName={cls("w-full min-h-0 h-full object-cover", imageClassName)}
/>
</div>
</div>
))}
</TimelineCardStack>
);
};
FeatureCardSix.displayName = "FeatureCardSix";
export default FeatureCardSix;

View File

@@ -0,0 +1,151 @@
"use client";
import CardStackTextBox from "@/components/cardStack/CardStackTextBox";
import PricingFeatureList from "@/components/shared/PricingFeatureList";
import { useCardAnimation } from "@/components/cardStack/hooks/useCardAnimation";
import { Check, X } from "lucide-react";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type ComparisonItem = {
items: string[];
};
interface FeatureCardSixteenProps {
negativeCard: ComparisonItem;
positiveCard: ComparisonItem;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxTitleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
gridClassName?: string;
cardClassName?: string;
itemsListClassName?: string;
itemClassName?: string;
itemIconClassName?: string;
itemTextClassName?: string;
}
const FeatureCardSixteen = ({
negativeCard,
positiveCard,
animationType,
title,
titleSegments,
description,
textboxLayout,
useInvertedBackground,
tag,
tagIcon,
buttons,
ariaLabel = "Feature comparison section",
className = "",
containerClassName = "",
textBoxTitleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
gridClassName = "",
cardClassName = "",
itemsListClassName = "",
itemClassName = "",
itemIconClassName = "",
itemTextClassName = "",
}: FeatureCardSixteenProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
const { itemRefs } = useCardAnimation({ animationType, itemCount: 2 });
const cards = [
{ ...negativeCard, variant: "negative" as const },
{ ...positiveCard, variant: "positive" as const },
];
return (
<section
aria-label={ariaLabel}
className={cls("relative py-20 w-full", useInvertedBackground === "invertDefault" && "bg-foreground", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
<CardStackTextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
/>
<div className={cls(
"relative mx-auto w-full md:w-60 grid grid-cols-1 gap-6",
cards.length >= 2 ? "md:grid-cols-2" : "md:grid-cols-1",
gridClassName
)}>
{cards.map((card, index) => (
<div
key={card.variant}
ref={(el) => { itemRefs.current[index] = el; }}
className={cls(
"relative h-full card rounded-theme-capped p-6",
cardClassName
)}
>
<div className={cls("flex flex-col gap-6", card.variant === "negative" && "opacity-50")}>
<PricingFeatureList
features={card.items}
icon={card.variant === "positive" ? Check : X}
shouldUseLightText={shouldUseLightText}
className={itemsListClassName}
featureItemClassName={itemClassName}
featureIconWrapperClassName=""
featureIconClassName={itemIconClassName}
featureTextClassName={cls("truncate", itemTextClassName)}
/>
</div>
</div>
))}
</div>
</div>
</section>
);
};
FeatureCardSixteen.displayName = "FeatureCardSixteen";
export default FeatureCardSixteen;

View File

@@ -0,0 +1,177 @@
"use client";
import { Fragment } from "react";
import CardList from "@/components/cardStack/CardList";
import Button from "@/components/button/Button";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
interface FeatureCard {
id: string;
label: string;
title: string;
items: string[];
buttons?: ButtonConfig[];
}
interface FeatureCardTwelveProps {
features: FeatureCard[];
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxDescriptionClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
cardContentClassName?: string;
labelClassName?: string;
cardTitleClassName?: string;
itemsContainerClassName?: string;
itemTextClassName?: string;
cardButtonClassName?: string;
cardButtonTextClassName?: string;
}
const FeatureCardTwelve = ({
features,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxDescriptionClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
cardContentClassName = "",
labelClassName = "",
cardTitleClassName = "",
itemsContainerClassName = "",
itemTextClassName = "",
cardButtonClassName = "",
cardButtonTextClassName = "",
}: FeatureCardTwelveProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<CardList
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
animationType={animationType}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
cardClassName={cardClassName}
titleClassName={textBoxTitleClassName}
descriptionClassName={textBoxDescriptionClassName}
textBoxClassName={textBoxClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
ariaLabel={ariaLabel}
>
{features.map((feature) => (
<div
key={feature.id}
className={cls(
"relative z-1 w-full min-h-0 h-full flex flex-col md:flex-row gap-6 p-6 md:p-15",
cardContentClassName
)}
>
<div className="relative z-1 w-full md:w-1/2 flex md:justify-start">
<h2 className={cls(
"text-5xl md:text-6xl font-medium leading-[1.1]",
shouldUseLightText && "text-background",
labelClassName
)}>
{feature.label}
</h2>
</div>
<div className="relative z-1 w-full h-px bg-foreground/20 md:hidden" />
<div className="relative z-1 w-full md:w-1/2 flex flex-col gap-4">
<h3 className={cls(
"text-xl md:text-3xl font-medium leading-tight",
shouldUseLightText ? "text-background" : "text-foreground",
cardTitleClassName
)}>
{feature.title}
</h3>
<div className={cls("flex flex-wrap items-center gap-2", itemsContainerClassName)}>
{feature.items.map((item, index) => (
<Fragment key={index}>
<span className={cls(
"text-base",
shouldUseLightText ? "text-background" : "text-foreground",
itemTextClassName
)}>
{item}
</span>
{index < feature.items.length - 1 && (
<span className="text-base text-accent"></span>
)}
</Fragment>
))}
</div>
{feature.buttons && feature.buttons.length > 0 && (
<div className="mt-3 flex gap-4">
{feature.buttons.slice(0, 2).map((button, index) => (
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, theme.defaultButtonVariant, cardButtonClassName, cardButtonTextClassName)} />
))}
</div>
)}
</div>
</div>
))}
</CardList>
);
};
FeatureCardTwelve.displayName = "FeatureCardTwelve";
export default FeatureCardTwelve;

View File

@@ -0,0 +1,220 @@
"use client";
import { useState } from "react";
import TextBox from "@/components/Textbox";
import Accordion from "@/components/Accordion";
import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, TitleSegment } from "@/components/cardStack/types";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
type MediaProps =
| {
imageSrc: string;
imageAlt?: string;
videoSrc?: never;
videoAriaLabel?: never;
}
| {
videoSrc: string;
videoAriaLabel?: string;
imageSrc?: never;
imageAlt?: never;
};
type AccordionItem = {
id: string;
title: string;
content: string;
};
type FeatureCardTwentyOneProps = MediaProps & {
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
accordionItems: AccordionItem[];
useInvertedBackground: InvertedBackground;
mediaPosition?: "left" | "right";
ariaLabel?: string;
className?: string;
containerClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
contentClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
accordionContainerClassName?: string;
accordionClassName?: string;
accordionTitleClassName?: string;
accordionContentClassName?: string;
accordionIconContainerClassName?: string;
accordionIconClassName?: string;
};
const FeatureCardTwentyOne = ({
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
accordionItems,
imageSrc,
imageAlt,
videoSrc,
videoAriaLabel,
useInvertedBackground,
mediaPosition = "left",
ariaLabel = "Feature section",
className = "",
containerClassName = "",
mediaWrapperClassName = "",
mediaClassName = "",
contentClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
accordionContainerClassName = "",
accordionClassName = "",
accordionTitleClassName = "",
accordionContentClassName = "",
accordionIconContainerClassName = "",
accordionIconClassName = "",
}: FeatureCardTwentyOneProps) => {
const [activeAccordion, setActiveAccordion] = useState<number>(0);
const handleAccordionToggle = (index: number) => {
setActiveAccordion(activeAccordion === index ? -1 : index);
};
const mediaElement = (
<div className={cls(
"w-full md:w-1/2 h-[50svh] md:h-auto rounded-theme-capped overflow-hidden",
mediaWrapperClassName
)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", mediaClassName)}
/>
</div>
);
const contentElement = (
<div className={cls(
"w-full md:w-1/2 flex flex-col",
contentClassName
)}>
{/* Mobile */}
<TextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
useInvertedBackground={useInvertedBackground}
className={cls("flex flex-col gap-1 md:hidden", textBoxClassName)}
titleClassName={cls("text-4xl md:text-5xl font-medium text-center md:text-left text-balance", titleClassName)}
descriptionClassName={cls("text-base md:text-lg leading-[1.2] text-center md:text-left", descriptionClassName)}
tagClassName={cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-1 md:mb-2", tagClassName)}
buttonContainerClassName={cls("flex gap-4 mt-4", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={cls("text-base", buttonTextClassName)}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
center={true}
/>
{/* Desktop */}
<TextBox
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
useInvertedBackground={useInvertedBackground}
className={cls("hidden md:flex flex-col gap-1", textBoxClassName)}
titleClassName={cls("text-4xl md:text-5xl font-medium text-center md:text-left text-balance", titleClassName)}
descriptionClassName={cls("text-base md:text-lg leading-[1.2] text-center md:text-left", descriptionClassName)}
tagClassName={cls("w-fit px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-1 md:mb-2", tagClassName)}
buttonContainerClassName={cls("flex gap-4 mt-4", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={cls("text-base", buttonTextClassName)}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
center={false}
/>
<div className={cls(
"flex flex-col mt-8 divide-y divide-accent/20 border-y border-accent/20",
accordionContainerClassName
)}>
{accordionItems.map((item, index) => (
<Accordion
key={item.id}
index={index}
isActive={activeAccordion === index}
onToggle={handleAccordionToggle}
title={item.title}
content={item.content}
showCard={false}
useInvertedBackground={useInvertedBackground === "noInvert" ? undefined : useInvertedBackground}
className={cls("py-4 md:py-6", accordionClassName)}
titleClassName={cls("text-xl md:text-2xl", accordionTitleClassName)}
contentClassName={accordionContentClassName}
iconContainerClassName={accordionIconContainerClassName}
iconClassName={accordionIconClassName}
/>
))}
</div>
</div>
);
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full py-20", useInvertedBackground === "invertDefault" && "bg-foreground", className)}
>
<div className={cls(
"w-content-width mx-auto flex flex-col md:flex-row gap-8 md:gap-15",
containerClassName
)}>
{mediaPosition === "left" ? (
<>
{mediaElement}
{contentElement}
</>
) : (
<>
{contentElement}
{mediaElement}
</>
)}
</div>
</section>
);
};
FeatureCardTwentyOne.displayName = "FeatureCardTwentyOne";
export default FeatureCardTwentyOne;

View File

@@ -0,0 +1,189 @@
"use client";
import { memo } from "react";
import TextBox from "@/components/Textbox";
import Tag from "@/components/shared/Tag";
import { cls, shouldUseInvertedText } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { LucideIcon } from "lucide-react";
import type { InvertedBackground } from "@/providers/themeProvider/config/constants";
import type { ButtonConfig } from "@/types/button";
interface ProcessStep {
number: string;
title: string;
description: string;
tag?: string;
}
interface FeatureProcessStepsProps {
title: string;
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
steps: ProcessStep[];
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
gridClassName?: string;
leftColumnClassName?: string;
rightColumnClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
stepsContainerClassName?: string;
stepClassName?: string;
stepNumberClassName?: string;
stepContentClassName?: string;
stepTitleClassName?: string;
stepTagClassName?: string;
stepDescriptionClassName?: string;
}
const FeatureProcessSteps = ({
title,
description,
tag,
tagIcon,
buttons,
steps,
useInvertedBackground,
ariaLabel = "Process steps section",
className = "",
containerClassName = "",
gridClassName = "",
leftColumnClassName = "",
rightColumnClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
stepsContainerClassName = "",
stepClassName = "",
stepNumberClassName = "",
stepContentClassName = "",
stepTitleClassName = "",
stepTagClassName = "",
stepDescriptionClassName = "",
}: FeatureProcessStepsProps) => {
const theme = useTheme();
const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle);
return (
<section
aria-label={ariaLabel}
className={cls(
"relative py-20 w-full",
useInvertedBackground === "invertDefault" && "bg-foreground",
className
)}
>
<div className={cls("w-content-width mx-auto", containerClassName)}>
<div className={cls("grid grid-cols-1 md:grid-cols-2 gap-10 md:gap-10 items-center", gridClassName)}>
<div className={cls("flex flex-col", leftColumnClassName)}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
useInvertedBackground={useInvertedBackground}
className={cls("gap-3 md:gap-3", textBoxClassName)}
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
descriptionClassName={cls("text-lg leading-tight text-balance", descriptionClassName)}
tagClassName={cls("mb-1", tagClassName)}
buttonContainerClassName={cls("mt-2", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
/>
</div>
<div className={cls("flex flex-col gap-6", rightColumnClassName)}>
{steps && steps.length > 0 && (
<div className={cls("flex flex-col", stepsContainerClassName)}>
{steps.map((step, index) => (
<div
key={index}
className={cls(
"flex gap-6",
stepClassName
)}
>
{/* Number box with line below */}
<div className="flex flex-col items-center">
<div
className={cls(
"card h-12 w-fit aspect-square rounded-theme flex items-center justify-center shrink-0",
stepNumberClassName
)}
>
<p
className={cls(
"text-lg font-medium",
shouldUseLightText ? "text-background" : "text-foreground"
)}
>
{step.number}
</p>
</div>
{/* Line segment */}
{index < steps.length - 1 && (
<div
className={cls(
"h-full w-px",
useInvertedBackground === "invertDefault" ? "bg-background/20" : "bg-foreground/20"
)}
/>
)}
</div>
<div className={cls("w-full min-w-0 flex flex-col gap-2", stepContentClassName)}>
<div className="w-full min-w-0 flex items-center gap-3">
<h3
className={cls(
"text-2xl font-medium truncate",
useInvertedBackground === "invertDefault" ? "text-background" : "text-foreground",
stepTitleClassName
)}
>
{step.title}
</h3>
{step.tag && (
<Tag
text={step.tag}
useInvertedBackground={useInvertedBackground}
className={cls("text-xs text-nowrap", stepTagClassName)}
/>
)}
</div>
<p
className={cls(
"text-base leading-tight mb-12",
useInvertedBackground === "invertDefault" ? "text-background/75" : "text-foreground/75",
stepDescriptionClassName
)}
>
{step.description}
</p>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</section>
);
};
FeatureProcessSteps.displayName = "FeatureProcessSteps";
export default memo(FeatureProcessSteps);

View File

@@ -0,0 +1,32 @@
.feature-card-three-title {
font-size: var(--text-2xl);
}
.feature-card-three-description {
font-size: var(--text-sm);
}
/* Mobile touch support - duplicate hover states for is-active class */
.feature-card-three-item.is-active .feature-card-three-item-box > div {
transform: rotateY(180deg);
}
.feature-card-three-item.is-active .feature-card-three-content-wrapper {
transform: translateY(var(--hover-translate-y));
}
.feature-card-three-item.is-active .feature-card-three-title {
color: var(--color-foreground);
}
.feature-card-three-item.is-active .feature-card-three-description-wrapper {
opacity: 1;
}
.feature-card-three-item.is-active .feature-card-three-reveal-bg {
--tw-translate-y: 0px !important;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important;
left: calc(var(--vw-1_5) * 0.75);
bottom: calc(var(--vw-1_5) * 0.75);
right: calc(var(--vw-1_5) * 0.75);
}

View File

@@ -0,0 +1,176 @@
"use client";
import "./FeatureCardThree.css";
import { useRef, useCallback, useState } from "react";
import CardStack from "@/components/cardStack/CardStack";
import FeatureCardThreeItem from "./FeatureCardThreeItem";
import { useDynamicDimensions } from "./useDynamicDimensions";
import { useClickOutside } from "@/hooks/useClickOutside";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment } from "@/components/cardStack/types";
import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants";
type FeatureCard = {
id: string;
title: string;
description: string;
imageSrc: string;
imageAlt?: string;
};
interface FeatureCardThreeProps {
features: FeatureCard[];
carouselMode?: "auto" | "buttons";
gridVariant: GridVariant;
uniformGridCustomHeightClasses?: string;
animationType: CardAnimationType;
title: string;
titleSegments?: TitleSegment[];
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
textboxLayout: TextboxLayout;
useInvertedBackground: InvertedBackground;
ariaLabel?: string;
className?: string;
containerClassName?: string;
cardClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardTitleClassName?: string;
cardDescriptionClassName?: string;
gridClassName?: string;
carouselClassName?: string;
controlsClassName?: string;
textBoxClassName?: string;
textBoxTagClassName?: string;
textBoxButtonContainerClassName?: string;
textBoxButtonClassName?: string;
textBoxButtonTextClassName?: string;
itemContentClassName?: string;
}
const FeatureCardThree = ({
features,
carouselMode = "buttons",
gridVariant,
uniformGridCustomHeightClasses,
animationType,
title,
titleSegments,
description,
tag,
tagIcon,
buttons,
textboxLayout,
useInvertedBackground,
ariaLabel = "Feature section",
className = "",
containerClassName = "",
cardClassName = "",
textBoxTitleClassName = "",
textBoxTitleImageWrapperClassName = "",
textBoxTitleImageClassName = "",
textBoxDescriptionClassName = "",
cardTitleClassName = "",
cardDescriptionClassName = "",
gridClassName = "",
carouselClassName = "",
controlsClassName = "",
textBoxClassName = "",
textBoxTagClassName = "",
textBoxButtonContainerClassName = "",
textBoxButtonClassName = "",
textBoxButtonTextClassName = "",
itemContentClassName = "",
}: FeatureCardThreeProps) => {
const featureCardThreeRefs = useRef<(HTMLDivElement | null)[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const setRef = useCallback(
(index: number) => (el: HTMLDivElement | null) => {
if (featureCardThreeRefs.current) {
featureCardThreeRefs.current[index] = el;
}
},
[]
);
// Check if device supports hover (desktop) or not (mobile/touch)
const isTouchDevice = typeof window !== "undefined" && window.matchMedia("(hover: none)").matches;
// Handle click outside to deactivate on mobile
useClickOutside(
containerRef,
() => setActiveIndex(null),
activeIndex !== null && isTouchDevice
);
const handleItemClick = useCallback((index: number) => {
if (typeof window !== "undefined" && !window.matchMedia("(hover: none)").matches) return;
setActiveIndex((prev) => (prev === index ? null : index));
}, []);
useDynamicDimensions([featureCardThreeRefs], {
titleSelector: ".feature-card-three-title-row .feature-card-three-title",
descriptionSelector: ".feature-card-three-description-wrapper .feature-card-three-description",
});
return (
<div ref={containerRef}>
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout={textboxLayout}
useInvertedBackground={useInvertedBackground}
className={className}
containerClassName={containerClassName}
gridClassName={gridClassName}
carouselClassName={carouselClassName}
controlsClassName={controlsClassName}
textBoxClassName={textBoxClassName}
titleClassName={textBoxTitleClassName}
titleImageWrapperClassName={textBoxTitleImageWrapperClassName}
titleImageClassName={textBoxTitleImageClassName}
descriptionClassName={textBoxDescriptionClassName}
tagClassName={textBoxTagClassName}
buttonContainerClassName={textBoxButtonContainerClassName}
buttonClassName={textBoxButtonClassName}
buttonTextClassName={textBoxButtonTextClassName}
ariaLabel={ariaLabel}
>
{features.map((feature, index) => (
<FeatureCardThreeItem
key={`${feature.id}-${index}`}
ref={setRef(index)}
item={feature}
isActive={activeIndex === index}
onItemClick={() => handleItemClick(index)}
className={cardClassName}
itemContentClassName={itemContentClassName}
itemTitleClassName={cardTitleClassName}
itemDescriptionClassName={cardDescriptionClassName}
/>
))}
</CardStack>
</div>
);
};
FeatureCardThree.displayName = "FeatureCardThree";
export default FeatureCardThree;

View File

@@ -0,0 +1,144 @@
"use client";
import { forwardRef, memo } from "react";
import Image from "next/image";
import { Info } from "lucide-react";
import { cls } from "@/lib/utils";
interface FeatureCardThreeItemData {
id: string;
title: string;
description: string;
imageSrc: string;
imageAlt?: string;
}
interface FeatureCardThreeItemProps {
item: FeatureCardThreeItemData;
isActive?: boolean;
onItemClick?: () => void;
className?: string;
itemContentClassName?: string;
itemTitleClassName?: string;
itemDescriptionClassName?: string;
}
const MASK_GRADIENT = "linear-gradient(to bottom, transparent, black 60%)";
const FeatureCardThreeItem = memo(
forwardRef<HTMLDivElement, FeatureCardThreeItemProps>(
(
{
item,
isActive = false,
onItemClick,
className = "",
itemContentClassName = "",
itemTitleClassName = "",
itemDescriptionClassName = "",
},
ref
) => {
return (
<div
ref={ref}
className={cls(
"feature-card-three-item relative overflow-hidden h-full rounded-theme-capped group",
isActive && "is-active",
className
)}
role="article"
aria-label={`${item.title} - Feature ${item.id}`}
tabIndex={0}
onClick={onItemClick}
>
<div
className="feature-card-three-item-box absolute top-6 left-6 z-10 flex items-center justify-center [perspective:1000px] [transform-style:preserve-3d]"
>
<div
className="relative h-8 aspect-square rounded-theme bg-background transition-transform duration-400 ease-[cubic-bezier(0.4,0,0.2,1)] [transform-style:preserve-3d] group-hover:[transform:rotateY(180deg)]"
>
<div
className="feature-card-three-item-box-front absolute w-full h-full rounded-theme bg-background flex items-center justify-center [backface-visibility:hidden]"
>
<p
className="feature-card-three-description text-foreground truncate"
>
{item.id}
</p>
</div>
<div
className="feature-card-three-item-box-back absolute w-full h-full rounded-theme bg-background flex items-center justify-center [backface-visibility:hidden] [transform:rotateY(180deg)]"
>
<Info
className="w-1/2 h-1/2 text-foreground"
strokeWidth={1.5}
aria-hidden="true"
/>
</div>
</div>
</div>
<Image
src={item.imageSrc}
alt={item.imageAlt || item.title}
width={1920}
height={1080}
className="relative z-1 object-cover rounded-theme-capped h-full w-full "
unoptimized={item.imageSrc.startsWith('http') || item.imageSrc.startsWith('//')}
aria-hidden={item.imageAlt === ""}
/>
<div
className="absolute z-10 bottom-0 left-0 right-0 h-30 backdrop-blur-xl opacity-100"
style={{ maskImage: MASK_GRADIENT }}
aria-hidden="true"
/>
<div
className="feature-card-three-content-wrapper absolute z-20 transition-all duration-400 ease-out flex flex-col gap-2 group-hover:[transform:translateY(var(--hover-translate-y))]"
style={{
top: "var(--content-top-position)",
left: "calc((var(--vw-1_5) * 1.5))",
width: "calc(100% - (var(--vw-1_5) * 3))",
}}
>
<div className="feature-card-three-title-row">
<h2
className={cls(
"feature-card-three-title font-semibold leading-[110%] transition-colors text-background group-hover:text-foreground",
itemTitleClassName
)}
>
{item.title}
</h2>
</div>
<div
className="feature-card-three-description-wrapper transition-all duration-400 ease-out opacity-0 group-hover:opacity-100"
>
<p
className={cls("feature-card-three-description leading-[120%] w-full text-foreground", itemDescriptionClassName)}
>
{item.description}
</p>
</div>
</div>
<div
className={cls(
"feature-card-three-reveal-bg !absolute left-0 bottom-0 card backdrop-blur-xs z-10 rounded-theme-capped transition-all duration-400 ease-out translate-y-full right-0 group-hover:translate-y-0 group-hover:left-[calc(var(--vw-1_5)*0.75)] group-hover:bottom-[calc(var(--vw-1_5)*0.75)] group-hover:right-[calc(var(--vw-1_5)*0.75)]",
itemContentClassName
)}
style={{
height: "var(--reveal-bg-height)",
}}
></div>
</div>
);
}
)
);
FeatureCardThreeItem.displayName = "FeatureCardThreeItem";
export default FeatureCardThreeItem;

View File

@@ -0,0 +1,122 @@
import { useEffect, useCallback, useMemo } from 'react'
let cachedVw15: number | null = null
let lastWindowWidth = 0
const getVw15InPixels = (): number => {
const currentWidth = window.innerWidth
if (cachedVw15 !== null && lastWindowWidth === currentWidth) {
return cachedVw15
}
const temp = document.createElement('div')
temp.style.position = 'absolute'
temp.style.width = 'var(--vw-1_5)'
document.body.appendChild(temp)
const width = temp.getBoundingClientRect().width
document.body.removeChild(temp)
cachedVw15 = width || 0
lastWindowWidth = currentWidth
return cachedVw15
}
const debounce = <T extends (...args: unknown[]) => void>(func: T, wait: number): ((...args: Parameters<T>) => void) & { cancel: () => void } => {
let timeout: NodeJS.Timeout | null = null
const debouncedFunction = function executedFunction(...args: Parameters<T>) {
const later = () => {
timeout = null
func(...args)
}
if (timeout !== null) {
clearTimeout(timeout)
}
timeout = setTimeout(later, wait)
}
debouncedFunction.cancel = () => {
if (timeout !== null) {
clearTimeout(timeout)
timeout = null
}
}
return debouncedFunction
}
interface DynamicDimensionsOptions {
titleSelector?: string
descriptionSelector?: string
containerSelector?: string | null
}
type RefArray = React.RefObject<(HTMLDivElement | null)[]> | React.RefObject<(HTMLDivElement | null)[]>[]
export const useDynamicDimensions = (refs: RefArray, options: DynamicDimensionsOptions = {}) => {
const {
titleSelector = '.feature-card-three-title',
descriptionSelector = '.feature-card-three-description',
containerSelector = null
} = options
const calculateDimensions = useCallback(() => {
const processRef = (ref: HTMLElement | null) => {
if (!ref) return
const container = containerSelector ? ref.querySelector(containerSelector) as HTMLElement : ref
if (!container) return
const titleElement = container.querySelector(titleSelector) as HTMLElement
const descriptionElement = container.querySelector(descriptionSelector) as HTMLElement
if (titleElement && descriptionElement) {
const titleHeight = titleElement.offsetHeight
const descriptionHeight = descriptionElement.offsetHeight
const contentTop = `calc(100% - ${titleHeight}px - calc(var(--vw-1_5) * 1.5))`
const vw15 = getVw15InPixels()
const contentWrapperHeight = titleHeight + descriptionHeight
const revealBgHeight = contentWrapperHeight + (vw15 * 2)
const moveUp = descriptionHeight + (vw15 * 0.55)
ref.style.setProperty('--reveal-bg-height', `${revealBgHeight}px`)
ref.style.setProperty('--content-top-position', contentTop)
ref.style.setProperty('--hover-translate-y', `-${moveUp}px`)
}
}
if (Array.isArray(refs)) {
refs.forEach((refArray) => {
if (refArray?.current && Array.isArray(refArray.current)) {
refArray.current.forEach(processRef)
}
})
} else if ('current' in refs && refs.current) {
if (Array.isArray(refs.current)) {
refs.current.forEach(processRef)
} else {
processRef(refs.current)
}
}
}, [titleSelector, descriptionSelector, containerSelector, refs])
const debouncedCalculate = useMemo(
() => debounce(calculateDimensions, 250),
[calculateDimensions]
)
useEffect(() => {
calculateDimensions()
window.addEventListener('resize', debouncedCalculate)
return () => {
window.removeEventListener('resize', debouncedCalculate)
debouncedCalculate.cancel()
}
}, [calculateDimensions, debouncedCalculate])
}