Initial commit

This commit is contained in:
dk
2025-12-28 22:40:34 +02:00
commit d113b2f821
308 changed files with 62520 additions and 0 deletions

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,181 @@
"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);
// Override heightClasses for staggered variants to use smaller heights
const heightClassesOverride =
gridVariant === "four-items-2x2-staggered-grid" || gridVariant === "four-items-2x2-staggered-grid-inverted"
? "h-110 2xl:h-120"
: uniformGridCustomHeightClasses;
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={heightClassesOverride}
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 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])
}