Initial commit

This commit is contained in:
2026-02-04 17:02:27 +02:00
commit c09374dcbb
640 changed files with 76651 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
"use client";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import LogoMarquee, { type MarqueeItem } from "@/components/shared/LogoMarquee";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
import type { Avatar } from "@/components/shared/AvatarGroup";
type HeroBillboardBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface HeroBillboardProps {
title: string;
description: string;
background: HeroBillboardBackgroundProps;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
avatars?: Avatar[];
avatarText?: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
marqueeItems?: MarqueeItem[];
marqueeSpeed?: number;
showMarqueeCard?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
avatarGroupClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
marqueeClassName?: string;
marqueeItemClassName?: string;
marqueeCardClassName?: string;
marqueeImageClassName?: string;
marqueeTextClassName?: string;
marqueeIconClassName?: string;
}
const HeroBillboard = ({
title,
description,
background,
tag,
tagIcon,
buttons,
avatars,
avatarText,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
marqueeItems,
marqueeSpeed = 30,
showMarqueeCard = true,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
avatarGroupClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
marqueeClassName = "",
marqueeItemClassName = "",
marqueeCardClassName = "",
marqueeImageClassName = "",
marqueeTextClassName = "",
marqueeIconClassName = "",
}: HeroBillboardProps) => {
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full py-hero-page-padding", className)}
>
<HeroBackgrounds {...background} />
<div className={cls("w-content-width mx-auto flex flex-col gap-14 md:gap-15 relative z-10", containerClassName)}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
avatars={avatars}
avatarText={avatarText}
avatarsAbove={true}
className={cls("flex flex-col gap-3 md:gap-1", textBoxClassName)}
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
avatarGroupClassName={avatarGroupClassName}
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-3", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
center={true}
/>
<div className="flex flex-col gap-6" >
<div className={cls("w-full overflow-hidden rounded-theme-capped card p-4", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("z-1", imageClassName)}
/>
</div>
{marqueeItems && marqueeItems.length > 0 && (
<LogoMarquee
items={marqueeItems}
speed={marqueeSpeed}
showCard={showMarqueeCard}
className={cls("w-content-width mx-auto z-10", marqueeClassName)}
itemClassName={marqueeItemClassName}
cardClassName={marqueeCardClassName}
imageClassName={marqueeImageClassName}
textClassName={marqueeTextClassName}
iconClassName={marqueeIconClassName}
/>
)}
</div>
</div>
</section>
);
};
HeroBillboard.displayName = "HeroBillboard";
export default HeroBillboard;

View File

@@ -0,0 +1,149 @@
"use client";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
export interface MediaItem {
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
}
type HeroBillboardCarouselBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface HeroBillboardCarouselProps {
title: string;
description: string;
background: HeroBillboardCarouselBackgroundProps;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
mediaItems: MediaItem[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
}
const HeroBillboardCarousel = ({
title,
description,
background,
tag,
tagIcon,
buttons,
mediaItems,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
}: HeroBillboardCarouselProps) => {
const renderCarouselItem = (item: MediaItem, index: number) => (
<div
key={index}
className="w-full aspect-[4/5] overflow-hidden rounded-theme-capped card p-2 shadow-lg"
>
<MediaContent
imageSrc={item.imageSrc}
videoSrc={item.videoSrc}
imageAlt={item.imageAlt || ""}
videoAriaLabel={item.videoAriaLabel || "Carousel media"}
imageClassName="z-1 h-full object-cover"
/>
</div>
);
return (
<section
aria-label={ariaLabel}
className={cls(
"relative w-full py-hero-page-padding md:h-svh md:py-0",
className
)}
>
<HeroBackgrounds {...background} />
<div className={cls(
"mx-auto flex flex-col gap-14 md:gap-10 relative z-10",
"w-full md:w-content-width md:h-full md:items-center md:justify-center",
containerClassName
)}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls(
"flex flex-col gap-3 md:gap-1 w-content-width mx-auto",
textBoxClassName
)}
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-3", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
center={true}
/>
<div className={cls("w-full -mx-[var(--content-padding)]", mediaWrapperClassName)}>
<AutoCarousel
title=""
description=""
textboxLayout="default"
animationType="none"
className="py-0"
carouselClassName="py-0"
containerClassName="!w-full"
itemClassName="!w-55 md:!w-carousel-item-4"
ariaLabel="Hero carousel"
showTextBox={false}
>
{mediaItems?.map(renderCarouselItem)}
</AutoCarousel>
</div>
</div>
</section>
);
};
HeroBillboardCarousel.displayName = "HeroBillboardCarousel";
export default HeroBillboardCarousel;

View File

@@ -0,0 +1,126 @@
"use client";
import TextBox from "@/components/Textbox";
import Dashboard from "@/components/shared/Dashboard";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
import type { DashboardSidebarItem, DashboardStat, DashboardListItem } from "@/components/shared/Dashboard";
import type { ChartDataItem } from "@/components/bento/BentoLineChart/utils";
type HeroBillboardDashboardBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface HeroBillboardDashboardProps {
title: string;
description: string;
background: HeroBillboardDashboardBackgroundProps;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
ariaLabel?: string;
dashboard: {
title: string;
stats: [DashboardStat, DashboardStat, DashboardStat];
logoIcon: LucideIcon;
sidebarItems: DashboardSidebarItem[];
searchPlaceholder?: string;
buttons: ButtonConfig[];
chartTitle?: string;
chartData?: ChartDataItem[];
listItems: DashboardListItem[];
listTitle?: string;
imageSrc: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
className?: string;
containerClassName?: string;
sidebarClassName?: string;
statClassName?: string;
chartClassName?: string;
listClassName?: string;
};
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
dashboardClassName?: string;
}
const HeroBillboardDashboard = ({
title,
description,
background,
tag,
tagIcon,
buttons,
ariaLabel = "Hero section",
dashboard,
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
dashboardClassName = "",
}: HeroBillboardDashboardProps) => {
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full py-hero-page-padding", className)}
>
<HeroBackgrounds {...background} />
<div className={cls("w-content-width mx-auto flex flex-col gap-14 md:gap-15 relative z-10", containerClassName)}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls("flex flex-col gap-3 md:gap-1", textBoxClassName)}
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-3", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
center={true}
/>
<Dashboard
{...dashboard}
className={cls(dashboard.className, dashboardClassName)}
/>
</div>
</section>
);
};
HeroBillboardDashboard.displayName = "HeroBillboardDashboard";
export default HeroBillboardDashboard;

View File

@@ -0,0 +1,189 @@
"use client";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
export interface MediaItem {
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
}
type HeroBillboardGalleryBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface HeroBillboardGalleryProps {
title: string;
description: string;
background: HeroBillboardGalleryBackgroundProps;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
mediaItems: MediaItem[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
}
const HeroBillboardGallery = ({
title,
description,
background,
tag,
tagIcon,
buttons,
mediaItems,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
}: HeroBillboardGalleryProps) => {
const renderCarouselItem = (item: MediaItem, index: number) => (
<div
key={index}
className="w-full aspect-[4/5] overflow-hidden rounded-theme-capped card p-2 shadow-lg"
>
<MediaContent
imageSrc={item.imageSrc}
videoSrc={item.videoSrc}
imageAlt={item.imageAlt || ""}
videoAriaLabel={item.videoAriaLabel || "Gallery media"}
imageClassName="h-full object-cover"
/>
</div>
);
const itemCount = mediaItems?.length || 0;
const desktopWidthClass = itemCount === 3 ? "md:w-[24%]" : itemCount === 4 ? "md:w-[24%]" : "md:w-[23%]";
return (
<section
aria-label={ariaLabel}
className={cls(
"relative w-full py-hero-page-padding md:h-svh md:py-0",
className
)}
>
<HeroBackgrounds {...background} />
<div className={cls(
"mx-auto flex flex-col gap-14 relative z-10",
"w-full md:w-content-width md:h-full md:items-center md:justify-center",
containerClassName
)}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls(
"flex flex-col gap-3 md:gap-1 w-content-width mx-auto",
textBoxClassName
)}
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-3", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
center={true}
/>
<div className={cls("w-full", mediaWrapperClassName)}>
<div className="block md:hidden -mx-[var(--content-padding)]">
<AutoCarousel
title=""
description=""
textboxLayout="default"
animationType="none"
className="py-0"
carouselClassName="py-0"
containerClassName="!w-full"
itemClassName="!w-55"
ariaLabel="Hero gallery carousel"
showTextBox={false}
>
{mediaItems?.slice(0, 5).map(renderCarouselItem)}
</AutoCarousel>
</div>
<div className="hidden md:flex justify-center items-center pt-2">
<div className="relative flex items-center justify-center w-full">
{mediaItems?.slice(0, 5).map((item, index) => {
const rotations = ["-rotate-6", "rotate-6", "-rotate-6", "rotate-6", "-rotate-6"];
const zIndexes = ["z-10", "z-20", "z-30", "z-40", "z-50"];
const translates = ["-translate-y-5", "translate-y-5", "-translate-y-5", "translate-y-5", "-translate-y-5"];
const marginClass = index > 0 ? "-ml-12 md:-ml-15" : "";
return (
<div
key={index}
className={cls(
"relative aspect-[4/5] overflow-hidden rounded-theme-capped card p-2 shadow-lg transition-transform duration-500 ease-out hover:scale-110",
desktopWidthClass,
rotations[index],
zIndexes[index],
translates[index],
marginClass
)}
>
<MediaContent
imageSrc={item.imageSrc}
videoSrc={item.videoSrc}
imageAlt={item.imageAlt || ""}
videoAriaLabel={item.videoAriaLabel || "Gallery media"}
imageClassName={cls("z-1 h-full object-cover", imageClassName)}
/>
</div>
);
})}
</div>
</div>
</div>
</div>
</section>
);
};
HeroBillboardGallery.displayName = "HeroBillboardGallery";
export default HeroBillboardGallery;

View File

@@ -0,0 +1,121 @@
"use client";
import TextBox from "@/components/Textbox";
import AngledCarousel from "@/components/cardStack/layouts/carousels/AngledCarousel";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
type HeroBillboardRotatedCarouselBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface CarouselItem {
id: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
}
interface HeroBillboardRotatedCarouselProps {
title: string;
description: string;
background: HeroBillboardRotatedCarouselBackgroundProps;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
carouselItems: CarouselItem[];
autoPlay?: boolean;
autoPlayInterval?: number;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
carouselClassName?: string;
}
const HeroBillboardRotatedCarousel = ({
title,
description,
background,
tag,
tagIcon,
buttons,
carouselItems,
autoPlay = true,
autoPlayInterval = 4000,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
carouselClassName = "",
}: HeroBillboardRotatedCarouselProps) => {
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full h-fit md:min-h-svh flex items-center justify-center py-hero-page-padding", className)}
>
<HeroBackgrounds {...background} />
<div className={cls("w-full flex flex-col gap-14 md:gap-15 relative z-10", containerClassName)}>
<div className="w-content-width mx-auto" >
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls("flex flex-col gap-3 md:gap-1", textBoxClassName)}
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-3", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
center={true}
/>
</div>
<AngledCarousel
items={carouselItems}
autoPlay={autoPlay}
autoPlayInterval={autoPlayInterval}
className={carouselClassName}
/>
</div>
</section>
);
};
HeroBillboardRotatedCarousel.displayName = "HeroBillboardRotatedCarousel";
export default HeroBillboardRotatedCarousel;

View File

@@ -0,0 +1,162 @@
"use client";
import { useRef } from "react";
import { useScroll, useTransform, motion } from "motion/react";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
type HeroBillboardScrollBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface HeroBillboardScrollProps {
title: string;
description: string;
background: HeroBillboardScrollBackgroundProps;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
cardWrapperClassName?: string;
cardInnerClassName?: string;
imageClassName?: string;
}
const HeroBillboardScroll = ({
title,
description,
background,
tag,
tagIcon,
buttons,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
cardWrapperClassName = "",
cardInnerClassName = "",
imageClassName = "",
}: HeroBillboardScrollProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
});
const rotate = useTransform(scrollYProgress, [0, 1], [20, 0]);
const scale = useTransform(scrollYProgress, [0, 1], [1.05, 1]);
return (
<section
aria-label={ariaLabel}
ref={containerRef}
className={cls("relative h-fit flex items-center justify-center", className)}
>
<HeroBackgrounds {...background} />
<div
className={cls("py-hero-page-padding w-full relative z-10", containerClassName)}
style={{
perspective: "1000px",
}}
>
<div className="w-content-width mx-auto">
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls("flex flex-col gap-3 md:gap-1", textBoxClassName)}
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
descriptionClassName={cls("text-base md:text-lg leading-[1.2]", descriptionClassName)}
tagClassName={cls("px-3 py-1 text-sm rounded-theme card text-foreground inline-flex items-center gap-2 mb-3", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-3", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
center={true}
/>
</div>
<div
className={cls("relative w-content-width h-[50svh] mt-8 mx-auto md:hidden", cardWrapperClassName)}
style={{
transform: "rotateX(20deg)",
}}
>
<div className={cls("h-full w-full overflow-hidden rounded-theme-capped card p-4", cardInnerClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("z-1 h-full w-full object-cover object-left-top", imageClassName)}
/>
</div>
</div>
<motion.div
style={{
rotateX: rotate,
scale,
}}
className={cls("hidden md:block relative w-content-width mt-8 h-[75svh] mx-auto", cardWrapperClassName)}
>
<div className={cls("h-full w-full overflow-hidden rounded-theme-capped card p-4", cardInnerClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("z-1 h-full w-full object-cover object-left-top", imageClassName)}
/>
</div>
</motion.div>
</div>
</section>
);
};
HeroBillboardScroll.displayName = "HeroBillboardScroll";
export default HeroBillboardScroll;

View File

@@ -0,0 +1,128 @@
"use client";
import TextBox from "@/components/Textbox";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import LogoMarquee, { type MarqueeItem } from "@/components/shared/LogoMarquee";
import { cls } from "@/lib/utils";
import type { ButtonConfig } from "@/types/button";
import type { Avatar } from "@/components/shared/AvatarGroup";
type HeroCenteredBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface HeroCenteredProps {
title: string;
description: string;
background: HeroCenteredBackgroundProps;
avatars: Avatar[];
avatarText?: string;
buttons?: ButtonConfig[];
marqueeItems?: MarqueeItem[];
marqueeSpeed?: number;
showMarqueeCard?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
avatarGroupClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
marqueeClassName?: string;
marqueeItemClassName?: string;
marqueeCardClassName?: string;
marqueeImageClassName?: string;
marqueeTextClassName?: string;
marqueeIconClassName?: string;
}
const HeroCentered = ({
title,
description,
background,
avatars,
avatarText,
buttons,
marqueeItems,
marqueeSpeed = 30,
showMarqueeCard = true,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
avatarGroupClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
marqueeClassName = "",
marqueeItemClassName = "",
marqueeCardClassName = "",
marqueeImageClassName = "",
marqueeTextClassName = "",
marqueeIconClassName = "",
}: HeroCenteredProps) => {
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full h-svh md:h-screen flex flex-col items-center justify-center", className)}
>
<HeroBackgrounds {...background} />
<div className={cls("w-content-width mx-auto relative z-10", containerClassName)}>
<TextBox
title={title}
description={description}
avatars={avatars}
avatarText={avatarText}
avatarsAbove={true}
buttons={buttons}
className={cls("md:max-w-7/10 mx-auto flex flex-col gap-3 md:gap-3", textBoxClassName)}
titleClassName={cls("text-7xl font-medium text-balance", titleClassName)}
descriptionClassName={cls("text-lg md:text-xl leading-tight", descriptionClassName)}
avatarGroupClassName={avatarGroupClassName}
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-3", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
center={true}
/>
</div>
{marqueeItems && marqueeItems.length > 0 && (
<LogoMarquee
items={marqueeItems}
speed={marqueeSpeed}
showCard={showMarqueeCard}
className={cls("absolute bottom-6 left-1/2 -translate-x-1/2 w-content-width z-10", marqueeClassName)}
itemClassName={marqueeItemClassName}
cardClassName={marqueeCardClassName}
imageClassName={marqueeImageClassName}
textClassName={marqueeTextClassName}
iconClassName={marqueeIconClassName}
/>
)}
</section>
);
};
HeroCentered.displayName = "HeroCentered";
export default HeroCentered;

View File

@@ -0,0 +1,125 @@
"use client";
import MediaContent from "@/components/shared/MediaContent";
import FillWidthText, { hasDescenders } from "@/components/shared/FillWidthText/FillWidthText";
import TextAnimation from "@/components/text/TextAnimation";
import Button from "@/components/button/Button";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { ButtonConfig } from "@/types/button";
const MASK_GRADIENT = "linear-gradient(to bottom, transparent, black 60%)";
interface HeroLogoProps {
logoText: string;
description: string;
buttons: ButtonConfig[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
showDimOverlay?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentContainerClassName?: string;
descriptionClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
logoContainerClassName?: string;
logoClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
blurClassName?: string;
dimOverlayClassName?: string;
}
const HeroLogo = ({
logoText,
description,
buttons,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
showDimOverlay = false,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
contentContainerClassName = "",
descriptionClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
logoContainerClassName = "",
logoClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
blurClassName = "",
dimOverlayClassName = "",
}: HeroLogoProps) => {
const theme = useTheme();
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full h-svh overflow-hidden flex flex-col justify-end", className)}
>
<div className={cls("absolute inset-0 w-full h-full", mediaWrapperClassName)}>
{showDimOverlay && (
<div className={cls("absolute top-0 left-0 w-full h-full bg-background/20 pointer-events-none select-none", dimOverlayClassName)} />
)}
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("w-full h-full object-cover !rounded-none", imageClassName)}
/>
</div>
<div
className={cls(
"absolute z-10 backdrop-blur-xl opacity-100 w-full h-[50svh] md:h-[75svh] left-0 bottom-0 pointer-events-none select-none",
blurClassName
)}
style={{ maskImage: MASK_GRADIENT }}
aria-hidden="true"
/>
<div className={cls("relative z-20 w-content-width mx-auto h-fit flex items-end", containerClassName)}>
<div className={cls("w-full flex flex-col", logoContainerClassName)}>
<div className={cls("w-full flex flex-col md:flex-row md:justify-between items-start md:items-end gap-3 md:gap-6", contentContainerClassName)}>
<div className="w-full md:w-1/2" >
<TextAnimation
type={theme.defaultTextAnimation}
text={description}
variant="words-trigger"
start="top 100%"
className={cls("text-lg md:text-2xl text-background text-balance font-medium leading-[1.2] md:max-w-1/2", descriptionClassName)}
/>
</div>
<div className="w-full md:w-1/2 flex justify-start md:justify-end" >
<div className={cls("flex flex-wrap gap-4", buttonContainerClassName)}>
{buttons.slice(0, 2).map((button, index) => (
<Button key={index} {...getButtonProps(button, index, theme.defaultButtonVariant, cls("", buttonClassName), cls("text-base", buttonTextClassName))} />
))}
</div>
</div>
</div>
<div className="w-full flex">
<FillWidthText className={cls("font-bold text-background", !hasDescenders(logoText) && "my-10", logoClassName)}>
{logoText}
</FillWidthText>
</div>
</div>
</div>
</section>
);
};
HeroLogo.displayName = "HeroLogo";
export default HeroLogo;

View File

@@ -0,0 +1,160 @@
"use client";
import MediaContent from "@/components/shared/MediaContent";
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
import TextAnimation from "@/components/text/TextAnimation";
import Button from "@/components/button/Button";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { Plus } from "lucide-react";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { ButtonConfig } from "@/components/cardStack/types";
type HeroLogoBillboardBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "glowing-orb" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface HeroLogoBillboardProps {
logoText: string;
description: string;
buttons: ButtonConfig[];
background: HeroLogoBillboardBackgroundProps;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
frameStyle?: "card" | "browser";
ariaLabel?: string;
className?: string;
containerClassName?: string;
logoContainerClassName?: string;
logoClassName?: string;
descriptionClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
browserBarClassName?: string;
addressBarClassName?: string;
}
const HeroLogoBillboard = ({
logoText,
description,
buttons,
background,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
frameStyle = "card",
ariaLabel = "Hero section",
className = "",
containerClassName = "",
logoContainerClassName = "",
logoClassName = "",
descriptionClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
browserBarClassName = "",
addressBarClassName = "",
}: HeroLogoBillboardProps) => {
const theme = useTheme();
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full py-hero-page-padding", className)}
>
<HeroBackgrounds {...background} />
<div className={cls("w-content-width mx-auto flex flex-col gap-14 md:gap-15 relative z-10", containerClassName)}>
<div className={cls("w-full flex flex-col items-end gap-6 md:gap-8", logoContainerClassName)}>
<div className="relative w-full flex">
<FillWidthText className={cls("text-foreground", logoClassName)}>
{logoText}
</FillWidthText>
</div>
<div className="relative w-full md:w-1/2" >
<TextAnimation
type={theme.defaultTextAnimation}
text={description}
variant="words-trigger"
start="top 100%"
className={cls("text-lg md:text-3xl text-foreground/75 text-balance text-end leading-[1.2]", descriptionClassName)}
/>
</div>
<div className={cls("flex flex-wrap justify-end gap-3", buttonContainerClassName)}>
{buttons.slice(0, 2).map((button, index) => (
<Button
key={`${button.text}-${index}`}
{...getButtonProps(button, index, theme.defaultButtonVariant, buttonClassName, buttonTextClassName)}
/>
))}
</div>
</div>
{frameStyle === "browser" ? (
<div className={cls("w-full overflow-hidden rounded-theme-capped card", mediaWrapperClassName)}>
<div className={cls("relative z-1 bg-background border-b border-foreground/10 px-4 py-3 flex items-center gap-4", browserBarClassName)}>
<div className="flex items-center gap-2">
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
</div>
<div className="flex items-center gap-2 flex-1">
<div className={cls("w-15 md:w-10 h-8 rounded-theme bg-accent/10", addressBarClassName)} />
<div className="w-15 md:w-10 h-8 rounded-theme bg-accent/10" />
<div className="hidden md:block w-10 h-8 rounded-theme bg-accent/10" />
</div>
<Plus className="h-[var(--text-sm)] w-auto text-foreground" />
</div>
<div className="relative z-1 p-0">
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("z-1 rounded-none! aspect-square md:aspect-video", imageClassName)}
/>
</div>
</div>
) : (
<div className={cls("w-full overflow-hidden rounded-theme-capped card p-4", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("z-1 aspect-square md:aspect-video", imageClassName)}
/>
</div>
)}
</div>
</section>
);
};
HeroLogoBillboard.displayName = "HeroLogoBillboard";
export default HeroLogoBillboard;

View File

@@ -0,0 +1,165 @@
"use client";
import MediaContent from "@/components/shared/MediaContent";
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
import TextAnimation from "@/components/text/TextAnimation";
import Button from "@/components/button/Button";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { Plus } from "lucide-react";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import type { ButtonConfig } from "@/types/button";
type HeroLogoBillboardSplitBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "glowing-orb" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface HeroLogoBillboardSplitProps {
logoText: string;
description: string;
background: HeroLogoBillboardSplitBackgroundProps;
buttons: ButtonConfig[];
layoutOrder: "default" | "reverse";
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
frameStyle?: "card" | "browser";
ariaLabel?: string;
className?: string;
containerClassName?: string;
logoContainerClassName?: string;
descriptionClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
logoClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
browserBarClassName?: string;
addressBarClassName?: string;
}
const HeroLogoBillboardSplit = ({
logoText,
description,
background,
buttons,
layoutOrder,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
frameStyle = "card",
ariaLabel = "Hero section",
className = "",
containerClassName = "",
logoContainerClassName = "",
descriptionClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
logoClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
browserBarClassName = "",
addressBarClassName = "",
}: HeroLogoBillboardSplitProps) => {
const theme = useTheme();
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full py-hero-page-padding", className)}
>
<HeroBackgrounds {...background} />
<div className={cls("w-content-width mx-auto flex flex-col gap-6 md:gap-15 relative z-10", containerClassName)}>
<div className={cls(
"w-full flex gap-6 md:gap-8",
layoutOrder === "default" ? "flex-col" : "flex-col-reverse",
logoContainerClassName
)}>
<div className="relative flex flex-col gap-3 md:flex-row justify-between md:items-end w-full" >
<div className="relative flex flex-col gap-4 w-full md:w-1/2" >
<TextAnimation
type={theme.defaultTextAnimation}
text={description}
variant="words-trigger"
start="top 100%"
className={cls("text-lg md:text-3xl text-foreground/75 text-balance text-start leading-[1.2]", descriptionClassName)}
/>
</div>
<div className={cls("flex flex-wrap gap-4", buttonContainerClassName)}>
{buttons.slice(0, 2).map((button, index) => (
<Button key={`${button.text}-${index}`} {...getButtonProps(button, index, theme.defaultButtonVariant, buttonClassName, buttonTextClassName)} />
))}
</div>
</div>
<div className="relative w-full flex">
<FillWidthText className={cls("text-foreground", logoClassName)}>
{logoText}
</FillWidthText>
</div>
</div>
{frameStyle === "browser" ? (
<div className={cls("w-full overflow-hidden rounded-theme-capped card", mediaWrapperClassName)}>
<div className={cls("relative z-1 bg-background border-b border-foreground/10 px-4 py-3 flex items-center gap-4", browserBarClassName)}>
<div className="flex items-center gap-2">
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
<div className="h-3 w-auto aspect-square rounded-theme bg-accent" />
</div>
<div className="flex items-center gap-2 flex-1">
<div className={cls("w-15 md:w-10 h-8 rounded-theme bg-accent/10", addressBarClassName)} />
<div className="w-15 md:w-10 h-8 rounded-theme bg-accent/10" />
<div className="hidden md:block w-10 h-8 rounded-theme bg-accent/10" />
</div>
<Plus className="h-[var(--text-sm)] w-auto text-foreground" />
</div>
<div className="relative z-1 p-0">
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("z-1 rounded-none! aspect-square md:aspect-video!", imageClassName)}
/>
</div>
</div>
) : (
<div className={cls("w-full overflow-hidden rounded-theme-capped card p-4", mediaWrapperClassName)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("z-1 aspect-square md:aspect-video", imageClassName)}
/>
</div>
)}
</div>
</section>
);
};
HeroLogoBillboardSplit.displayName = "HeroLogoBillboardSplit";
export default HeroLogoBillboardSplit;

View File

@@ -0,0 +1,142 @@
"use client";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
const RADIAL_MASK_GRADIENT = "radial-gradient(circle, black 20%, transparent 70%)";
interface HeroOverlayProps {
title: string;
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
textPosition?: "center" | "bottom-left";
showDimOverlay?: boolean;
showBlur?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
blurClassName?: string;
dimOverlayClassName?: string;
}
const HeroOverlay = ({
title,
description,
tag,
tagIcon,
buttons,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
textPosition = "bottom-left",
showDimOverlay = false,
showBlur = true,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
blurClassName = "",
dimOverlayClassName = "",
}: HeroOverlayProps) => {
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full h-svh overflow-hidden flex flex-col justify-end", className)}
>
<div className={cls("absolute inset-0 w-full h-full", mediaWrapperClassName)}>
{showDimOverlay && (
<div className={cls("absolute top-0 left-0 w-full h-full bg-background/20 pointer-events-none select-none", dimOverlayClassName)} />
)}
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("w-full h-full object-cover !rounded-none", imageClassName)}
/>
</div>
{showBlur && (
<div
className={cls(
"absolute z-10 backdrop-blur-sm opacity-100 pointer-events-auto select-none",
textPosition === "center"
? "w-[100vw] h-[80vw] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
: "w-[150vw] h-[150vw] left-0 bottom-0 -translate-x-1/2 translate-y-1/2",
blurClassName
)}
style={{ maskImage: RADIAL_MASK_GRADIENT }}
aria-hidden="true"
/>
)}
<div className={cls(
"relative z-10 w-content-width mx-auto h-fit flex",
textPosition === "center" ? "items-center justify-center" : "items-end pb-[var(--width-10)] md:pb-hero-page-padding",
containerClassName
)}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls(
"flex flex-col gap-3 md:gap-3 text-background",
textPosition === "center" ? "w-full" : "w-full md:w-1/2",
textBoxClassName
)}
titleClassName={cls(
"text-7xl 2xl:text-8xl font-medium text-balance",
textPosition === "center" ? "text-center" : "text-left",
titleClassName
)}
descriptionClassName={cls(
"text-lg md:text-xl leading-[1.2]",
textPosition === "center" ? "text-center" : "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-3",
tagClassName
)}
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-4", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={textPosition === "center"}
/>
</div>
</section>
);
};
HeroOverlay.displayName = "HeroOverlay";
export default HeroOverlay;

View File

@@ -0,0 +1,198 @@
"use client";
import { memo } from "react";
import Textbox from "@/components/Textbox";
import Button from "@/components/button/Button";
import MediaContent from "@/components/shared/MediaContent";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useButtonClick } from "@/components/button/useButtonClick";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
type TitleSegment =
| { type: "text"; content: string }
| { type: "image"; src: string; alt?: string };
type HeroPersonalLinksBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface SocialLink {
icon: LucideIcon;
label: string;
href: string;
}
interface LinkCard {
icon?: LucideIcon;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
imageClassName?: string;
title: string;
description: string;
button: ButtonConfig;
}
interface HeroPersonalLinksProps {
background: HeroPersonalLinksBackgroundProps;
title: string;
titleSegments?: TitleSegment[];
socialLinks?: SocialLink[];
linkCards: LinkCard[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
textboxClassName?: string;
titleClassName?: string;
titleImageWrapperClassName?: string;
titleImageClassName?: string;
socialLinksClassName?: string;
socialLinkClassName?: string;
linkCardsClassName?: string;
linkCardClassName?: string;
linkCardIconClassName?: string;
linkCardTitleClassName?: string;
linkCardDescriptionClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
}
const SocialLinkButton = ({ social, className }: { social: SocialLink; className?: string }) => {
const handleClick = useButtonClick(social.href);
const Icon = social.icon;
return (
<button
type="button"
onClick={handleClick}
className={cls(
"flex items-center gap-2 px-4 py-2 rounded-theme card text-sm text-foreground hover:opacity-80 transition-opacity duration-300 ease-out cursor-pointer",
className
)}
>
<Icon className="h-[1em] w-auto aspect-square" />
<span>{social.label}</span>
</button>
);
};
const HeroPersonalLinks = ({
background,
title,
titleSegments,
socialLinks,
linkCards,
ariaLabel = "Personal links section",
className = "",
containerClassName = "",
textboxClassName = "",
titleClassName = "",
titleImageWrapperClassName = "",
titleImageClassName = "",
socialLinksClassName = "",
socialLinkClassName = "",
linkCardsClassName = "",
linkCardClassName = "",
linkCardIconClassName = "",
linkCardTitleClassName = "",
linkCardDescriptionClassName = "",
buttonClassName = "",
buttonTextClassName = "",
}: HeroPersonalLinksProps) => {
const theme = useTheme();
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full min-h-screen flex items-center justify-center py-20", className)}
>
<HeroBackgrounds {...background} />
<div className={cls("w-content-width md:w-35 mx-auto flex flex-col items-center gap-8 relative z-10", containerClassName)}>
<Textbox
title={title}
titleSegments={titleSegments}
description=""
textboxLayout="inline-image"
center
className={textboxClassName}
titleClassName={titleClassName}
titleImageWrapperClassName={titleImageWrapperClassName}
titleImageClassName={titleImageClassName}
/>
{socialLinks && socialLinks.length > 0 && (
<div className={cls("flex flex-wrap justify-center gap-3", socialLinksClassName)}>
{socialLinks.map((social, index) => (
<SocialLinkButton
key={index}
social={social}
className={socialLinkClassName}
/>
))}
</div>
)}
<div className={cls("w-full flex flex-col gap-4 mt-4", linkCardsClassName)}>
{linkCards.map((card, index) => (
<div
key={index}
className={cls("w-full card rounded-theme-capped p-5 flex items-center gap-5", linkCardClassName)}
>
<div className={cls("relative h-10 w-auto aspect-square card shadow rounded-theme flex items-center justify-center flex-shrink-0 overflow-hidden", linkCardIconClassName)}>
{card.videoSrc ? (
<MediaContent
videoSrc={card.videoSrc}
videoAriaLabel={card.videoAriaLabel}
imageClassName={cls("w-full h-full object-cover", card.imageClassName)}
/>
) : card.imageSrc ? (
<MediaContent
imageSrc={card.imageSrc}
imageAlt={card.imageAlt}
imageClassName={cls("w-full h-full object-cover", card.imageClassName)}
/>
) : card.icon ? (
<card.icon className="h-4/10 w-4/10 text-foreground" strokeWidth={1.5} />
) : null}
</div>
<div className="flex-1 min-w-0">
<h3 className={cls("font-medium text-foreground", linkCardTitleClassName)}>{card.title}</h3>
<p className={cls("text-sm text-foreground/60 truncate", linkCardDescriptionClassName)}>{card.description}</p>
</div>
<Button
{...getButtonProps(card.button, 0, theme.defaultButtonVariant, buttonClassName, buttonTextClassName)}
className={cls("flex-shrink-0", buttonClassName)}
/>
</div>
))}
</div>
</div>
</section>
);
};
HeroPersonalLinks.displayName = "HeroPersonalLinks";
export default memo(HeroPersonalLinks);

View File

@@ -0,0 +1,111 @@
"use client";
import TextBox from "@/components/Textbox";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import EmailSignupForm from "@/components/form/EmailSignupForm";
import { cls } from "@/lib/utils";
import { LucideIcon } from "lucide-react";
type HeroSignupBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "glowing-orb" }
| { variant: "glowing-orb-sparkles" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface HeroSignupProps {
title: string;
description: string;
background: HeroSignupBackgroundProps;
tag: string;
tagIcon?: LucideIcon;
inputPlaceholder?: string;
buttonText?: string;
onSubmit?: (email: string) => void;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
formWrapperClassName?: string;
formClassName?: string;
inputClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
}
const HeroSignup = ({
title,
description,
background,
tag,
tagIcon,
inputPlaceholder = "Enter your email",
buttonText = "Get Started",
onSubmit,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
formWrapperClassName = "",
formClassName = "",
inputClassName = "",
buttonClassName = "",
buttonTextClassName = "",
}: HeroSignupProps) => {
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full h-svh md:h-screen flex items-center justify-center", className)}
>
<HeroBackgrounds {...background} />
<div className={cls("w-content-width mx-auto relative z-10", containerClassName)}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
className={cls("md:max-w-6/10 xl:max-w-45/100 mx-auto flex flex-col gap-3 md:gap-6", textBoxClassName)}
titleClassName={cls("text-7xl font-medium text-balance", titleClassName)}
descriptionClassName={cls("text-lg md:text-xl leading-tight text-balance", descriptionClassName)}
tagClassName={cls("mb-0", tagClassName)}
center={true}
/>
<div className={cls("md:max-w-6/10 xl:max-w-45/100 mx-auto mt-6", formWrapperClassName)}>
<EmailSignupForm
inputPlaceholder={inputPlaceholder}
buttonText={buttonText}
onSubmit={onSubmit}
className={cls("w-full mx-auto", formClassName)}
inputClassName={inputClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
/>
</div>
</div>
</section>
);
};
HeroSignup.displayName = "HeroSignup";
export default HeroSignup;

View File

@@ -0,0 +1,197 @@
"use client";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import LogoMarquee, { type MarqueeItem } from "@/components/shared/LogoMarquee";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
import type { Avatar } from "@/components/shared/AvatarGroup";
type HeroSplitBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "glowing-orb" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface HeroSplitProps {
title: string;
description: string;
background: HeroSplitBackgroundProps;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
avatars?: Avatar[];
avatarText?: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
ariaLabel?: string;
imagePosition?: "left" | "right";
fixedMediaHeight?: boolean;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
avatarGroupClassName?: string;
marqueeItems?: MarqueeItem[];
marqueeSpeed?: number;
showMarqueeCard?: boolean;
marqueeClassName?: string;
marqueeItemClassName?: string;
marqueeCardClassName?: string;
marqueeImageClassName?: string;
marqueeTextClassName?: string;
marqueeIconClassName?: string;
}
const HeroSplit = ({
title,
description,
background,
tag,
tagIcon,
buttons,
avatars,
avatarText,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
ariaLabel = "Hero section",
imagePosition = "right",
fixedMediaHeight = true,
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
avatarGroupClassName = "",
marqueeItems,
marqueeSpeed = 30,
showMarqueeCard = true,
marqueeClassName = "",
marqueeItemClassName = "",
marqueeCardClassName = "",
marqueeImageClassName = "",
marqueeTextClassName = "",
marqueeIconClassName = "",
}: HeroSplitProps) => {
const mediaContent = (
<div className={cls(
"w-full h-fit md:w-1/2 overflow-hidden rounded-theme-capped card p-4 md:max-h-[75svh]",
fixedMediaHeight && "h-100 md:h-[65vh]",
mediaWrapperClassName
)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("h-full min-h-0", imageClassName)}
/>
</div>
);
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
>
<HeroBackgrounds {...background} />
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center relative z-10", containerClassName)}>
{imagePosition === "left" && mediaContent}
<div className={cls("w-full md:w-1/2")}>
{/* Mobile */}
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
avatars={avatars}
avatarText={avatarText}
avatarGroupClassName={cls("!mt-5", avatarGroupClassName)}
className={cls("flex flex-col gap-3 md:hidden", textBoxClassName)}
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl 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-3", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-2", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={true}
/>
{/* Desktop */}
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
avatars={avatars}
avatarText={avatarText}
avatarGroupClassName={cls("", avatarGroupClassName)}
className={cls("hidden md:flex flex-col gap-3 md:gap-4", textBoxClassName)}
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl 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-3", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-2", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={false}
/>
</div>
{imagePosition === "right" && mediaContent}
</div>
{marqueeItems && marqueeItems.length > 0 && (
<LogoMarquee
items={marqueeItems}
speed={marqueeSpeed}
showCard={showMarqueeCard}
className={cls("absolute bottom-6 left-1/2 -translate-x-1/2 w-content-width z-10", marqueeClassName)}
itemClassName={marqueeItemClassName}
cardClassName={marqueeCardClassName}
imageClassName={marqueeImageClassName}
textClassName={marqueeTextClassName}
iconClassName={marqueeIconClassName}
/>
)}
</section>
);
};
HeroSplit.displayName = "HeroSplit";
export default HeroSplit;

View File

@@ -0,0 +1,180 @@
"use client";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import { cls } from "@/lib/utils";
import { Star } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
type HeroSplitDualMediaBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "glowing-orb" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface MediaItem {
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
}
interface HeroSplitDualMediaProps {
title: string;
description: string;
background: HeroSplitDualMediaBackgroundProps;
tag: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
mediaItems: [MediaItem, MediaItem];
rating: number;
ratingText: string;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
mediaItemClassName?: string;
imageClassName?: string;
ratingClassName?: string;
ratingTextClassName?: string;
}
const HeroSplitDualMedia = ({
title,
description,
background,
tag,
tagIcon,
buttons,
mediaItems,
rating,
ratingText,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
mediaItemClassName = "",
imageClassName = "",
ratingClassName = "",
ratingTextClassName = "",
}: HeroSplitDualMediaProps) => {
const mediaContent = (
<div className={cls("w-full md:w-1/2 grid grid-cols-2 gap-4", mediaWrapperClassName)}>
{mediaItems.map((item, index) => (
<div
key={index}
className={cls(
"w-full h-100 md:h-[55vh] overflow-hidden rounded-theme-capped card p-3",
mediaItemClassName
)}
>
<MediaContent
imageSrc={item.imageSrc}
videoSrc={item.videoSrc}
imageAlt={item.imageAlt}
videoAriaLabel={item.videoAriaLabel}
imageClassName={cls("h-full w-full object-cover", imageClassName)}
/>
</div>
))}
</div>
);
const ratingElement = (
<div className={cls("w-full min-w-0 md:w-75/100 flex flex-col gap-6 mt-8", ratingClassName)}>
<div className="w-full h-px bg-background-accent" />
<div className="w-full min-w-0 flex items-center justify-center md:justify-start gap-3 text-base md:text-xl">
<div className="flex items-center gap-1">
{Array.from({ length: rating }).map((_, index) => (
<Star
key={index}
className="h-[1em] w-auto aspect-square text-primary-cta fill-primary-cta"
/>
))}
</div>
<div className="h-[1em] w-px bg-background-accent" />
<p className={cls("text-foreground truncate", ratingTextClassName)}>{ratingText}</p>
</div>
</div>
);
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
>
<HeroBackgrounds {...background} />
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center relative z-10", containerClassName)}>
<div className="w-full md:w-1/2">
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls("flex flex-col gap-3 md:gap-4 md:hidden", textBoxClassName)}
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl 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-3", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-2", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={true}
/>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls("hidden md:flex flex-col gap-3 md:gap-4", textBoxClassName)}
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl 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-3", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-2", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={false}
/>
{ratingElement}
</div>
{mediaContent}
</div>
</section>
);
};
HeroSplitDualMedia.displayName = "HeroSplitDualMedia";
export default HeroSplitDualMedia;

View File

@@ -0,0 +1,310 @@
"use client";
import { useEffect, useRef } from "react";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import HeroBackgrounds, { type HeroBackgroundVariantProps } from "@/components/background/HeroBackgrounds";
import LogoMarquee, { type MarqueeItem } from "@/components/shared/LogoMarquee";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
import type { Avatar } from "@/components/shared/AvatarGroup";
const MOBILE_BREAKPOINT = 768;
const useKpiAnimation = (enableAnimation: boolean = true) => {
const sectionRef = useRef<HTMLElement>(null);
const boxRef1 = useRef<HTMLDivElement>(null);
const boxRef2 = useRef<HTMLDivElement>(null);
const boxRef3 = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!enableAnimation) return;
const isMobile = window.innerWidth <= MOBILE_BREAKPOINT;
if (isMobile) return;
let mouseX = 0;
let mouseY = 0;
let box1X = 0, box1Y = 0;
let box2X = 0, box2Y = 0;
let box3X = 0, box3Y = 0;
const speed = 0.025;
const handleMouseMove = (event: MouseEvent): void => {
mouseX = (event.clientX / window.innerWidth) * 100 - 50;
mouseY = (event.clientY / window.innerHeight) * 100 - 50;
};
const animate = (): void => {
// Box 1 movement
const distX1 = (mouseX * -0.25) - box1X;
const distY1 = (mouseY * -0.25) - box1Y;
box1X += distX1 * speed;
box1Y += distY1 * speed;
// Box 2 movement
const distX2 = (mouseX * -0.5) - box2X;
const distY2 = (mouseY * -0.5) - box2Y;
box2X += distX2 * speed;
box2Y += distY2 * speed;
// Box 3 movement
const distX3 = (mouseX * 0.25) - box3X;
const distY3 = (mouseY * 0.25) - box3Y;
box3X += distX3 * speed;
box3Y += distY3 * speed;
// Apply transforms
if (boxRef1.current) {
boxRef1.current.style.transform = `translate(${box1X}px, ${box1Y}px)`;
}
if (boxRef2.current) {
boxRef2.current.style.transform = `translate(${box2X}px, ${box2Y}px)`;
}
if (boxRef3.current) {
boxRef3.current.style.transform = `translate(${box3X}px, ${box3Y}px)`;
}
requestAnimationFrame(animate);
};
animate();
window.addEventListener('mousemove', handleMouseMove);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
};
}, [enableAnimation]);
return { sectionRef, boxRef1, boxRef2, boxRef3 };
};
type HeroSplitKpiBackgroundProps = Extract<
HeroBackgroundVariantProps,
| { variant: "plain" }
| { variant: "animated-grid" }
| { variant: "canvas-reveal" }
| { variant: "cell-wave" }
| { variant: "downward-rays-animated" }
| { variant: "downward-rays-animated-grid" }
| { variant: "downward-rays-static" }
| { variant: "downward-rays-static-grid" }
| { variant: "glowing-orb" }
| { variant: "gradient-bars" }
| { variant: "radial-gradient" }
| { variant: "rotated-rays-animated" }
| { variant: "rotated-rays-animated-grid" }
| { variant: "rotated-rays-static" }
| { variant: "rotated-rays-static-grid" }
| { variant: "sparkles-gradient" }
>;
interface KpiItem {
value: string;
label: string;
}
interface HeroSplitKpiProps {
title: string;
description: string;
background: HeroSplitKpiBackgroundProps;
kpis: [KpiItem, KpiItem, KpiItem];
enableKpiAnimation: boolean;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
avatars?: Avatar[];
avatarText?: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
ariaLabel?: string;
imagePosition?: "left" | "right";
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
avatarGroupClassName?: string;
kpiClassName?: string;
kpiValueClassName?: string;
kpiLabelClassName?: string;
marqueeItems?: MarqueeItem[];
marqueeSpeed?: number;
showMarqueeCard?: boolean;
marqueeClassName?: string;
marqueeItemClassName?: string;
marqueeCardClassName?: string;
marqueeImageClassName?: string;
marqueeTextClassName?: string;
marqueeIconClassName?: string;
}
const HeroSplitKpi = ({
title,
description,
background,
kpis,
enableKpiAnimation,
tag,
tagIcon,
buttons,
avatars,
avatarText,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
ariaLabel = "Hero section",
imagePosition = "right",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
avatarGroupClassName = "",
kpiClassName = "",
kpiValueClassName = "",
kpiLabelClassName = "",
marqueeItems,
marqueeSpeed = 30,
showMarqueeCard = true,
marqueeClassName = "",
marqueeItemClassName = "",
marqueeCardClassName = "",
marqueeImageClassName = "",
marqueeTextClassName = "",
marqueeIconClassName = "",
}: HeroSplitKpiProps) => {
const { sectionRef, boxRef1, boxRef2, boxRef3 } = useKpiAnimation(enableKpiAnimation);
const boxRefs = [boxRef1, boxRef2, boxRef3];
const mediaContent = (
<div
className={cls(
"relative w-full h-fit md:w-1/2 aspect-square md:aspect-auto md:h-[65vh]",
mediaWrapperClassName
)}
>
<div className="relative h-full scale-75 w-full overflow-hidden rounded-theme-capped card p-4">
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("h-full min-h-0", imageClassName)}
/>
</div>
{kpis.map((kpi, index) => (
<div
key={index}
ref={boxRefs[index]}
className={cls(
"absolute! card backdrop-blur-xs rounded-theme-capped px-4 py-3 md:px-6 md:py-4 flex flex-col items-center",
index === 0 && "top-[5%] left-[5%] md:top-[0%] md:left-[0%]",
index === 1 && "top-[35%] right-[2.5%] md:top-[35%]",
index === 2 && "bottom-[7.5%] left-[10%] md:left-[7.5%] md:bottom-[0%]",
kpiClassName
)}
>
<p className={cls("text-2xl md:text-4xl font-medium text-foreground", kpiValueClassName)}>
{kpi.value}
</p>
<p className={cls("text-sm md:text-base text-foreground/70", kpiLabelClassName)}>
{kpi.label}
</p>
</div>
))}
</div>
);
return (
<section
ref={sectionRef}
aria-label={ariaLabel}
className={cls("relative w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
>
<HeroBackgrounds {...background} />
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center relative z-10", containerClassName)}>
{imagePosition === "left" && mediaContent}
<div className={cls("w-full md:w-1/2")}>
{/* Mobile */}
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
avatars={avatars}
avatarText={avatarText}
avatarGroupClassName={cls("!mt-5", avatarGroupClassName)}
className={cls("flex flex-col gap-3 md:hidden", textBoxClassName)}
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl 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-3", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-2", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={true}
/>
{/* Desktop */}
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
avatars={avatars}
avatarText={avatarText}
avatarGroupClassName={cls("", avatarGroupClassName)}
className={cls("hidden md:flex flex-col gap-3 md:gap-4", textBoxClassName)}
titleClassName={cls("text-7xl 2xl:text-8xl font-medium text-center md:text-left text-balance", titleClassName)}
descriptionClassName={cls("max-w-8/10 text-lg md:text-xl 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-3", tagClassName)}
buttonContainerClassName={cls("flex flex-wrap gap-4 mt-2", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={false}
/>
</div>
{imagePosition === "right" && mediaContent}
</div>
{marqueeItems && marqueeItems.length > 0 && (
<LogoMarquee
items={marqueeItems}
speed={marqueeSpeed}
showCard={showMarqueeCard}
className={cls("absolute bottom-6 left-1/2 -translate-x-1/2 w-content-width z-10", marqueeClassName)}
itemClassName={marqueeItemClassName}
cardClassName={marqueeCardClassName}
imageClassName={marqueeImageClassName}
textClassName={marqueeTextClassName}
iconClassName={marqueeIconClassName}
/>
)}
</section>
);
};
HeroSplitKpi.displayName = "HeroSplitKpi";
export default HeroSplitKpi;

View File

@@ -0,0 +1,171 @@
"use client";
import { useCallback } from "react";
import MediaContent from "@/components/shared/MediaContent";
import FillWidthText, { hasDescenders } from "@/components/shared/FillWidthText/FillWidthText";
import TextAnimation from "@/components/text/TextAnimation";
import Button from "@/components/button/Button";
import { cls } from "@/lib/utils";
import { getButtonProps } from "@/lib/buttonUtils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
import { useCarouselFullscreen } from "./useCarouselFullscreen";
import type { ButtonConfig } from "@/types/button";
const MASK_GRADIENT = "linear-gradient(to bottom, transparent, black 60%)";
interface CarouselSlide {
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
}
interface HeroCarouselLogoProps {
logoText: string;
description: string;
buttons: ButtonConfig[];
slides: CarouselSlide[];
autoplayDelay?: number;
showDimOverlay?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
contentContainerClassName?: string;
descriptionClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
logoContainerClassName?: string;
logoClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
blurClassName?: string;
dimOverlayClassName?: string;
progressBarClassName?: string;
}
const HeroCarouselLogo = ({
logoText,
description,
buttons,
slides,
autoplayDelay = 3000,
showDimOverlay = false,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
contentContainerClassName = "",
descriptionClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
logoContainerClassName = "",
logoClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
blurClassName = "",
dimOverlayClassName = "",
progressBarClassName = "",
}: HeroCarouselLogoProps) => {
const theme = useTheme();
const { currentSlide, progressRefs, goToSlide } = useCarouselFullscreen({
totalSlides: slides.length,
autoplayDelay,
});
const setProgressRef = useCallback(
(el: HTMLDivElement | null, index: number) => {
progressRefs.current[index] = el;
},
[progressRefs]
);
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full h-svh overflow-hidden flex flex-col justify-end", className)}
>
<div className={cls("absolute inset-0 w-full h-full", mediaWrapperClassName)}>
{showDimOverlay && (
<div className={cls("absolute top-0 left-0 w-full h-full bg-background/20 z-10 pointer-events-none select-none", dimOverlayClassName)} />
)}
{slides.map((slide, index) => (
<div
key={index}
className={cls(
"absolute inset-0 transition-opacity duration-600",
currentSlide === index ? "opacity-100" : "opacity-0"
)}
aria-hidden={currentSlide !== index}
>
<MediaContent
imageSrc={slide.imageSrc}
videoSrc={slide.videoSrc}
imageAlt={slide.imageAlt || ""}
videoAriaLabel={slide.videoAriaLabel || "Hero video"}
imageClassName={cls("w-full h-full object-cover !rounded-none", imageClassName)}
/>
</div>
))}
</div>
<div
className={cls(
"absolute z-10 backdrop-blur-xl opacity-100 w-full h-[50svh] md:h-[75svh] left-0 bottom-0 pointer-events-none select-none",
blurClassName
)}
style={{ maskImage: MASK_GRADIENT }}
aria-hidden="true"
/>
<div className={cls("relative z-20 w-content-width mx-auto h-fit flex items-end", containerClassName)}>
<div className={cls("w-full flex flex-col", logoContainerClassName)}>
<div className={cls("w-full flex flex-col md:flex-row md:justify-between items-start md:items-end gap-3 md:gap-6", contentContainerClassName)}>
<div className="w-full md:w-1/2">
<TextAnimation
type={theme.defaultTextAnimation}
text={description}
variant="words-trigger"
className={cls("text-lg md:text-2xl text-background text-balance font-medium leading-[1.2] md:max-w-1/2", descriptionClassName)}
/>
</div>
<div className="w-full md:w-1/2 flex justify-start md:justify-end">
<div className={cls("flex flex-wrap gap-4", buttonContainerClassName)}>
{buttons.slice(0, 2).map((button, index) => (
<Button key={index} {...getButtonProps(button, index, theme.defaultButtonVariant, cls("", buttonClassName), cls("text-base", buttonTextClassName))} />
))}
</div>
</div>
</div>
<div className="w-full flex">
<FillWidthText className={cls("text-background", !hasDescenders(logoText) && "my-10", logoClassName)}>
{logoText}
</FillWidthText>
</div>
<div className="w-full flex gap-3 pb-12 pt-6">
{Array.from({ length: slides.length }, (_, index) => (
<button
key={index}
className={cls("relative cursor-pointer h-1 w-full rounded-theme overflow-hidden bg-white/10 backdrop-blur-sm", progressBarClassName)}
onClick={() => goToSlide(index)}
aria-label={`Go to slide ${index + 1}`}
aria-current={currentSlide === index ? "true" : "false"}
>
<div
ref={(el) => setProgressRef(el, index)}
className="absolute inset-0 bg-white rounded-theme"
style={{ transform: "translateX(-100%)" }}
/>
</button>
))}
</div>
</div>
</div>
</section>
);
};
HeroCarouselLogo.displayName = "HeroCarouselLogo";
export default HeroCarouselLogo;

View File

@@ -0,0 +1,103 @@
import { useEffect, useRef, useState, useCallback } from "react";
interface UseCarouselFullscreenProps {
totalSlides: number;
autoplayDelay?: number;
}
interface UseCarouselFullscreenReturn {
currentSlide: number;
progressRefs: React.MutableRefObject<(HTMLDivElement | null)[]>;
goToSlide: (index: number) => void;
}
export const useCarouselFullscreen = ({
totalSlides,
autoplayDelay = 3000,
}: UseCarouselFullscreenProps): UseCarouselFullscreenReturn => {
const [currentSlide, setCurrentSlide] = useState(0);
const progressRefs = useRef<(HTMLDivElement | null)[]>([]);
const animationFrameRef = useRef<number | null>(null);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const previousSlideRef = useRef<number>(0);
const resetProgressBars = useCallback(
(fromIndex: number, isLooping: boolean = false) => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
progressRefs.current.forEach((bar, index) => {
if (bar) {
if (isLooping || index >= fromIndex) {
bar.style.transform = "translateX(-100%)";
} else {
bar.style.transform = "translateX(0%)";
}
}
});
},
[]
);
const animateProgress = useCallback(
(index: number, prevIndex: number) => {
const isLooping = prevIndex === totalSlides - 1 && index === 0;
resetProgressBars(index, isLooping);
if (!progressRefs.current[index]) return;
let startTime: number | null = null;
const animate = (timestamp: number) => {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / autoplayDelay, 1);
const translateValue = -100 + progress * 100;
if (progressRefs.current[index]) {
progressRefs.current[index]!.style.transform = `translateX(${translateValue}%)`;
}
if (progress < 1) {
animationFrameRef.current = requestAnimationFrame(animate);
}
};
animationFrameRef.current = requestAnimationFrame(animate);
},
[autoplayDelay, resetProgressBars, totalSlides]
);
const goToSlide = useCallback((index: number) => {
setCurrentSlide(index);
}, []);
useEffect(() => {
animateProgress(currentSlide, previousSlideRef.current);
previousSlideRef.current = currentSlide;
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
intervalRef.current = setInterval(() => {
setCurrentSlide((prev) => (prev + 1) % totalSlides);
}, autoplayDelay);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [currentSlide, totalSlides, autoplayDelay, animateProgress]);
return {
currentSlide,
progressRefs,
goToSlide,
};
};