Initial commit

This commit is contained in:
2026-01-13 15:02:33 +02:00
commit a197a72080
298 changed files with 59446 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
"use client";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils";
import { Plus } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
interface HeroBillboardProps {
title: string;
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
frameStyle?: "card" | "browser";
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
browserBarClassName?: string;
addressBarClassName?: string;
}
const HeroBillboard = ({
title,
description,
tag,
tagIcon,
buttons,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
frameStyle = "card",
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
browserBarClassName = "",
addressBarClassName = "",
}: HeroBillboardProps) => {
return (
<section
aria-label={ariaLabel}
className={cls("w-full py-hero-page-padding", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-14 md:gap-15", 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 gap-4 mt-3", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
center={true}
/>
{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("rounded-none!", 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", imageClassName)}
/>
</div>
)}
</div>
</section>
);
};
HeroBillboard.displayName = "HeroBillboard";
export default HeroBillboard;

View File

@@ -0,0 +1,126 @@
"use client";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
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;
}
interface HeroBillboardCarouselProps {
title: string;
description: string;
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,
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(
"w-full py-hero-page-padding md:h-svh md:py-0",
className
)}
>
<div className={cls(
"mx-auto flex flex-col gap-14 md:gap-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 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,112 @@
"use client";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
import { cls } from "@/lib/utils";
import type { ButtonConfig } from "@/types/button";
export interface MediaItem {
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
}
interface HeroBillboardCarouselSplitProps {
title: string;
buttons?: ButtonConfig[];
mediaItems: MediaItem[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
}
const HeroBillboardCarouselSplit = ({
title,
buttons,
mediaItems,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
}: HeroBillboardCarouselSplitProps) => {
const renderCarouselItem = (item: MediaItem, index: number) => (
<div
key={index}
className="w-full aspect-square overflow-hidden rounded-theme-capped card p-2"
>
<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
)}
>
<div className={cls(
"mx-auto flex flex-col gap-14 md:gap-10 pt-hero-page-padding-half",
"w-full md:w-content-width md:h-full md:items-center md:justify-center",
containerClassName
)}>
<TextBox
title={title}
description=""
buttons={buttons}
textboxLayout="split-actions"
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)}
buttonContainerClassName={cls("flex 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>
);
};
HeroBillboardCarouselSplit.displayName = "HeroBillboardCarouselSplit";
export default HeroBillboardCarouselSplit;

View File

@@ -0,0 +1,166 @@
"use client";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
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;
}
interface HeroBillboardGalleryProps {
title: string;
description: string;
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,
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(
"w-full py-hero-page-padding md:h-svh md:py-0",
className
)}
>
<div className={cls(
"mx-auto flex flex-col gap-14",
"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 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,196 @@
"use client";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils";
import { Plus } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
interface Metric {
id: string;
value: string;
label: string;
}
interface HeroBillboardMetricsProps {
title: string;
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
frameStyle?: "card" | "browser";
metricsLabel: string;
metrics: Metric[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
browserBarClassName?: string;
addressBarClassName?: string;
metricsContainerClassName?: string;
metricsLabelClassName?: string;
metricsGridClassName?: string;
metricClassName?: string;
metricValueClassName?: string;
metricLabelClassName?: string;
}
const HeroBillboardMetrics = ({
title,
description,
tag,
tagIcon,
buttons,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
frameStyle = "card",
metricsLabel,
metrics,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
browserBarClassName = "",
addressBarClassName = "",
metricsContainerClassName = "",
metricsLabelClassName = "",
metricsGridClassName = "",
metricClassName = "",
metricValueClassName = "",
metricLabelClassName = "",
}: HeroBillboardMetricsProps) => {
return (
<section
aria-label={ariaLabel}
className={cls("w-full py-hero-page-padding", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-12 md:gap-11", containerClassName)}>
<div className="flex flex-col gap-14 md:gap-15" >
<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 gap-4 mt-3", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
center={true}
/>
{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("rounded-none!", 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", imageClassName)}
/>
</div>
)}
</div>
<div className={cls(
"relative w-full flex flex-col md:flex-row md:items-center justify-between gap-4 md:gap-10 border-t border-accent pt-10",
metricsContainerClassName
)}>
<div className="relative w-full md:w-1/3" >
<p className={cls(
"text-base text-foreground/75 leading-tight text-balance",
metricsLabelClassName
)}>
{metricsLabel}
</p>
</div>
<div className={cls(
"w-full md:w-1/2 grid grid-cols-2 gap-4 md:gap-6",
metrics.length === 2 && "md:grid-cols-2",
metrics.length === 3 && "md:grid-cols-3",
metricsGridClassName
)}>
{metrics.map((metric, index) => (
<div
key={metric.id}
className={cls(
"relative card rounded-theme-capped flex flex-col gap-0 p-4",
metrics.length === 3 && index === 2 && "col-span-2 md:col-span-1",
metricClassName
)}
>
<h3 className={cls(
"relative w-full min-w-0 text-4xl font-medium text-foreground truncate",
metricValueClassName
)}>
{metric.value}
</h3>
<p className={cls(
"relative w-full min-w-0 text-sm text-foreground/70 truncate",
metricLabelClassName
)}>
{metric.label}
</p>
</div>
))}
</div>
</div>
</div>
</section>
);
};
HeroBillboardMetrics.displayName = "HeroBillboardMetrics";
export default HeroBillboardMetrics;

View File

@@ -0,0 +1,98 @@
"use client";
import TextBox from "@/components/Textbox";
import AngledCarousel from "@/components/cardStack/layouts/carousels/AngledCarousel";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
interface CarouselItem {
id: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
}
interface HeroBillboardRotatedCarouselProps {
title: string;
description: string;
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,
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("w-full h-fit flex items-center justify-center py-hero-page-padding", className)}
>
<div className={cls("w-full flex flex-col gap-14 md:gap-15", 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 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,139 @@
"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 { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
interface HeroBillboardScrollProps {
title: string;
description: string;
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,
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)}
>
<div
className={cls("py-hero-page-padding w-full relative", 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 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,136 @@
"use client";
import TextBox from "@/components/Textbox";
import MediaContent from "@/components/shared/MediaContent";
import { cls } from "@/lib/utils";
import { Plus, LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
interface HeroBillboardSplitProps {
title: string;
description: string;
tag: string;
tagIcon?: LucideIcon;
buttons: ButtonConfig[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
frameStyle?: "browser" | "card";
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
browserBarClassName?: string;
addressBarClassName?: string;
imageClassName?: string;
}
const HeroBillboardSplit = ({
title,
description,
tag,
tagIcon,
buttons,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero media",
frameStyle = "browser",
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
browserBarClassName = "",
addressBarClassName = "",
imageClassName = "",
}: HeroBillboardSplitProps) => {
return (
<section
aria-label={ariaLabel}
className={cls(
"w-full h-fit py-hero-page-padding",
className
)}
>
<div className={cls(
"relative w-content-width mx-auto flex flex-col gap-14 md:gap-10",
containerClassName
)}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
textboxLayout="split"
className={cls(
"w-content-width mx-auto",
textBoxClassName
)}
titleClassName={titleClassName}
descriptionClassName={descriptionClassName}
tagClassName={tagClassName}
buttonContainerClassName={buttonContainerClassName}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
/>
{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("rounded-none!", 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", imageClassName)}
/>
</div>
)}
</div>
</section>
);
};
HeroBillboardSplit.displayName = "HeroBillboardSplit";
export default HeroBillboardSplit;

View File

@@ -0,0 +1,123 @@
"use client";
import TextBox from "@/components/Textbox";
import { cls } from "@/lib/utils";
import { ArrowUp } from "lucide-react";
import type { LucideIcon } from "lucide-react";
interface FeatureTag {
id: string;
label: string;
icon: LucideIcon;
}
interface HeroChatPromptProps {
title: string;
description: string;
tag?: string;
tagIcon?: LucideIcon;
promptText?: string;
featureTags: FeatureTag[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
promptContainerClassName?: string;
promptTextClassName?: string;
promptButtonClassName?: string;
featureTagsClassName?: string;
featureTagClassName?: string;
}
const HeroChatPrompt = ({
title,
description,
tag,
tagIcon,
promptText = "Ask me anything...",
featureTags,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
promptContainerClassName = "",
promptTextClassName = "",
promptButtonClassName = "",
featureTagsClassName = "",
featureTagClassName = "",
}: HeroChatPromptProps) => {
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full h-svh flex items-center justify-center", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
className={cls("flex flex-col gap-3 md:gap-1", textBoxClassName)}
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
descriptionClassName={cls("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)}
center={true}
/>
<div className={cls(
"w-full md:w-50 mx-auto card p-6 rounded-theme-capped flex flex-col gap-10",
promptContainerClassName
)}>
<p className={cls(
"relative z-1 flex-1 text-lg text-foreground truncate",
promptTextClassName
)}>
{promptText}
</p>
<div className="relative z-1 w-full flex justify-end" >
<div
className={cls(
"h-8 w-[var(--height-8)] rounded-theme primary-button flex items-center justify-center",
promptButtonClassName
)}
>
<ArrowUp className="w-1/2 h-1/2 text-background" strokeWidth={1.5} />
</div>
</div>
</div>
<div className={cls(
"w-full md:w-50 mx-auto flex flex-wrap items-center justify-center gap-3",
featureTagsClassName
)}>
{featureTags.map((featureTag) => {
const FeatureIcon = featureTag.icon;
return (
<div
key={featureTag.id}
className={cls(
"px-4 py-2 text-sm card rounded-theme flex items-center gap-2",
featureTagClassName
)}
>
<FeatureIcon className="relative z-1 h-[1em] w-auto" />
<span className="relative z-1" >{featureTag.label}</span>
</div>
);
})}
</div>
</div>
</section>
);
};
HeroChatPrompt.displayName = "HeroChatPrompt";
export default HeroChatPrompt;

View File

@@ -0,0 +1,185 @@
"use client";
import TextBox from "@/components/Textbox";
import { cls } from "@/lib/utils";
import { ArrowUp } from "lucide-react";
import type { LucideIcon } from "lucide-react";
interface FeatureTag {
id: string;
label: string;
icon: LucideIcon;
}
interface FeatureHighlight {
id: string;
icon: LucideIcon;
title: string;
subtitle: string;
}
interface HeroChatPromptFeaturesProps {
title: string;
description: string;
tag?: string;
tagIcon?: LucideIcon;
promptText?: string;
featureTags: FeatureTag[];
featureHighlights: FeatureHighlight[];
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
promptContainerClassName?: string;
promptTextClassName?: string;
promptButtonClassName?: string;
featureTagsClassName?: string;
featureTagClassName?: string;
featureHighlightsClassName?: string;
featureHighlightClassName?: string;
featureHighlightIconClassName?: string;
featureHighlightTitleClassName?: string;
featureHighlightSubtitleClassName?: string;
}
const HeroChatPromptFeatures = ({
title,
description,
tag,
tagIcon,
promptText = "Ask me anything...",
featureTags,
featureHighlights,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
promptContainerClassName = "",
promptTextClassName = "",
promptButtonClassName = "",
featureTagsClassName = "",
featureTagClassName = "",
featureHighlightsClassName = "",
featureHighlightClassName = "",
featureHighlightIconClassName = "",
featureHighlightTitleClassName = "",
featureHighlightSubtitleClassName = "",
}: HeroChatPromptFeaturesProps) => {
return (
<section
aria-label={ariaLabel}
className={cls("relative pt-hero-page-padding md:pt-0 w-full h-fit md:h-svh flex items-center justify-center", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-8", containerClassName)}>
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
className={cls("flex flex-col gap-3 md:gap-1", textBoxClassName)}
titleClassName={cls("text-6xl font-medium text-balance", titleClassName)}
descriptionClassName={cls("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)}
center={true}
/>
<div className={cls(
"w-full md:w-50 mx-auto flex flex-wrap items-center justify-center gap-3",
featureTagsClassName
)}>
{featureTags.map((featureTag) => {
const FeatureIcon = featureTag.icon;
return (
<div
key={featureTag.id}
className={cls(
"px-4 py-2 text-sm card rounded-theme flex items-center gap-2",
featureTagClassName
)}
>
<FeatureIcon className="relative z-1 h-[1em] w-auto" />
<span className="relative z-1" >{featureTag.label}</span>
</div>
);
})}
</div>
<div className={cls(
"w-full md:w-50 mx-auto card p-6 rounded-theme-capped flex flex-col gap-10",
promptContainerClassName
)}>
<p className={cls(
"relative z-1 flex-1 text-lg text-foreground truncate",
promptTextClassName
)}>
{promptText}
</p>
<div className="relative z-1 w-full flex justify-end" >
<div
className={cls(
"h-8 w-[var(--height-8)] rounded-theme primary-button flex items-center justify-center",
promptButtonClassName
)}
>
<ArrowUp className="w-1/2 h-1/2 text-background" strokeWidth={1.5} />
</div>
</div>
</div>
<div className="relative h-px w-full md:w-50 mx-auto bg-accent" />
<div className={cls(
"w-full md:w-50 mx-auto grid grid-cols-1 gap-6",
featureHighlights.length === 2 && "md:grid-cols-2",
featureHighlights.length === 3 && "md:grid-cols-3",
featureHighlights.length === 4 && "md:grid-cols-2",
featureHighlightsClassName
)}>
{featureHighlights.map((highlight) => {
const HighlightIcon = highlight.icon;
return (
<div
key={highlight.id}
className={cls(
"relative w-full min-w-0 flex items-center gap-4 p-3 card rounded-theme-capped",
featureHighlightClassName
)}
>
<div className={cls(
"relative shrink-0 h-10 w-auto aspect-square rounded-theme card flex items-center justify-center",
featureHighlightIconClassName
)}>
<HighlightIcon className="relative z-1 h-45/100 w-45/100 text-foreground" strokeWidth={1.5} />
</div>
<div className="relative w-full min-w-0 flex flex-col">
<h3 className={cls(
"relative w-full z-1 text-base font-medium text-foreground leading-tight truncate",
featureHighlightTitleClassName
)}>
{highlight.title}
</h3>
<p className={cls(
"relative w-full z-1 text-sm text-foreground/70 leading-tight truncate",
featureHighlightSubtitleClassName
)}>
{highlight.subtitle}
</p>
</div>
</div>
);
})}
</div>
</div>
</section>
);
};
HeroChatPromptFeatures.displayName = "HeroChatPromptFeatures";
export default HeroChatPromptFeatures;

View File

@@ -0,0 +1,91 @@
"use client";
import TextBox from "@/components/Textbox";
import { Globe } from "@/components/shared/Globe";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
import type { COBEOptions } from "cobe";
interface HeroGlobeOverlayProps {
title: string;
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
globeConfig?: COBEOptions;
ariaLabel?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
globeWrapperClassName?: string;
globeClassName?: string;
}
const HeroGlobeOverlay = ({
title,
description,
tag,
tagIcon,
buttons,
globeConfig,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
globeWrapperClassName = "",
globeClassName = "",
}: HeroGlobeOverlayProps) => {
return (
<section
aria-label={ariaLabel}
className={cls("w-full h-svh relative overflow-hidden flex items-center justify-center", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col items-center", containerClassName)}>
<div className={cls(
"relative w-full aspect-[16/12] md:w-[70%] md:aspect-video mask-fade-bottom-large",
globeWrapperClassName
)}>
<Globe config={globeConfig} className={cls("w-full -mt-[6%] md:-mt-[var(--width-5)]", globeClassName)} />
</div>
<div className="relative -mt-[15%] md:-mt-[10%] z-10 w-full">
<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-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 gap-4 mt-3", buttonContainerClassName)}
buttonClassName={buttonClassName}
buttonTextClassName={buttonTextClassName}
center={true}
/>
</div>
</div>
</section>
);
};
HeroGlobeOverlay.displayName = "HeroGlobeOverlay";
export default HeroGlobeOverlay;

View File

@@ -0,0 +1,127 @@
"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 { 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;
logoLineHeight?: number;
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,
logoLineHeight = 1.1,
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", 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", 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",
blurClassName
)}
style={{ maskImage: MASK_GRADIENT }}
aria-hidden="true"
/>
<div className={cls("relative z-20 w-content-width mx-auto h-full 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 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 lineHeight={logoLineHeight} className={cls("font-bold text-background", logoClassName)}>
{logoText}
</FillWidthText>
</div>
</div>
</div>
</section>
);
};
HeroLogo.displayName = "HeroLogo";
export default HeroLogo;

View File

@@ -0,0 +1,119 @@
"use client";
import MediaContent from "@/components/shared/MediaContent";
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
import TextAnimation from "@/components/text/TextAnimation";
import { cls } from "@/lib/utils";
import { Plus } from "lucide-react";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
interface HeroLogoBillboardProps {
logoText: string;
description: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
frameStyle?: "card" | "browser";
logoLineHeight?: number;
ariaLabel?: string;
className?: string;
containerClassName?: string;
logoContainerClassName?: string;
logoClassName?: string;
descriptionClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
browserBarClassName?: string;
addressBarClassName?: string;
}
const HeroLogoBillboard = ({
logoText,
description,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
frameStyle = "card",
logoLineHeight = 1.1,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
logoContainerClassName = "",
logoClassName = "",
descriptionClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
browserBarClassName = "",
addressBarClassName = "",
}: HeroLogoBillboardProps) => {
const theme = useTheme();
return (
<section
aria-label={ariaLabel}
className={cls("w-full py-hero-page-padding", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-14 md:gap-15", containerClassName)}>
<div className={cls("w-full flex flex-col items-end gap-6 md:gap-8", logoContainerClassName)}>
<div className="relative w-full flex">
<FillWidthText lineHeight={logoLineHeight} 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>
{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,143 @@
"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 { 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";
interface HeroLogoBillboardSplitProps {
logoText: string;
description: string;
buttons: ButtonConfig[];
layoutOrder: "default" | "reverse";
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
frameStyle?: "card" | "browser";
logoLineHeight?: number;
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,
buttons,
layoutOrder,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
frameStyle = "card",
logoLineHeight = 1.1,
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("w-full py-hero-page-padding", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-6 md:gap-15", 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 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 lineHeight={logoLineHeight} 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,107 @@
"use client";
import MediaContent from "@/components/shared/MediaContent";
import FillWidthText from "@/components/shared/FillWidthText/FillWidthText";
import TextAnimation from "@/components/text/TextAnimation";
import AutoCarousel from "@/components/cardStack/layouts/carousels/AutoCarousel";
import { cls } from "@/lib/utils";
import { useTheme } from "@/providers/themeProvider/ThemeProvider";
export interface MediaItem {
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
}
interface HeroLogoCarouselProps {
logoText: string;
description: string;
mediaItems: MediaItem[];
logoLineHeight?: number;
ariaLabel?: string;
className?: string;
containerClassName?: string;
logoContainerClassName?: string;
logoClassName?: string;
descriptionClassName?: string;
carouselWrapperClassName?: string;
}
const HeroLogoCarousel = ({
logoText,
description,
mediaItems,
logoLineHeight = 1.1,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
logoContainerClassName = "",
logoClassName = "",
descriptionClassName = "",
carouselWrapperClassName = "",
}: HeroLogoCarouselProps) => {
const theme = useTheme();
const renderCarouselItem = (item: MediaItem, index: number) => (
<div
key={index}
className="w-full aspect-[3/5] overflow-hidden rounded-theme-capped card p-2"
>
<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("w-full py-hero-page-padding", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col gap-14 md:gap-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 lineHeight={logoLineHeight} 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>
<div className={cls("w-full -mx-[var(--content-padding)]", carouselWrapperClassName)}>
<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>
);
};
HeroLogoCarousel.displayName = "HeroLogoCarousel";
export default HeroLogoCarousel;

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", 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", 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",
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-full 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 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,214 @@
"use client";
import { Fragment } from "react";
import MediaContent from "@/components/shared/MediaContent";
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";
interface HeroShowcaseSplitOverlayProps {
title: string;
description: string;
tags: string[];
buttons: ButtonConfig[];
showcaseImageSrc?: string;
showcaseVideoSrc?: string;
showcaseImageAlt?: string;
showcaseVideoAriaLabel?: string;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
showDimOverlay?: boolean;
ariaLabel?: string;
className?: string;
containerClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagsContainerClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
showcaseWrapperClassName?: string;
showcaseImageClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
dimOverlayClassName?: string;
}
const HeroShowcaseSplitOverlay = ({
title,
description,
tags,
buttons,
showcaseImageSrc,
showcaseVideoSrc,
showcaseImageAlt = "",
showcaseVideoAriaLabel = "Showcase video",
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
showDimOverlay = false,
ariaLabel = "Hero section",
className = "",
containerClassName = "",
titleClassName = "",
descriptionClassName = "",
tagsContainerClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
showcaseWrapperClassName = "",
showcaseImageClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
dimOverlayClassName = "",
}: HeroShowcaseSplitOverlayProps) => {
const theme = useTheme();
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full h-fit md:h-svh overflow-hidden", className)}
>
{/* Background Media with mask fade */}
<div className={cls("absolute inset-0 w-full h-full opacity-50 mask-fade-bottom-long", mediaWrapperClassName)}>
{showDimOverlay && (
<div className={cls("absolute inset-0 z-[1] bg-background/20", dimOverlayClassName)} />
)}
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("w-full h-full object-cover !rounded-none", imageClassName)}
/>
</div>
{/* Mobile Layout */}
<div className={cls(
"relative z-10 w-content-width mx-auto h-full flex md:hidden flex-col gap-10 justify-between pt-hero-page-padding-1_5 pb-hero-page-padding",
containerClassName
)}>
<div className="relative flex flex-col gap-6 w-full">
<TextAnimation
type={theme.defaultTextAnimation}
text={title}
variant="words-trigger"
className={cls("text-5xl font-medium text-foreground text-balance text-start leading-[1]", titleClassName)}
/>
<div className={cls("flex 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">
<TextAnimation
type={theme.defaultTextAnimation}
text={description}
variant="words-trigger"
className={cls("text-base text-foreground/75 text-balance text-start leading-tight", descriptionClassName)}
/>
</div>
<div className="flex flex-col gap-6">
<div className={cls(
"relative w-full aspect-square overflow-hidden rounded-theme-capped card p-4",
showcaseWrapperClassName
)}>
<MediaContent
imageSrc={showcaseImageSrc}
videoSrc={showcaseVideoSrc}
imageAlt={showcaseImageAlt}
videoAriaLabel={showcaseVideoAriaLabel}
imageClassName={cls("h-full w-full object-cover z-1", showcaseImageClassName)}
/>
</div>
<div className={cls("w-full card rounded-theme-capped flex flex-row items-center justify-between gap-2 px-6 py-3", tagsContainerClassName)}>
{tags.slice(0, 4).map((tag, index) => (
<Fragment key={`${tag}-${index}`}>
<span className={cls("text-sm text-foreground truncate", tagClassName)}>
{tag}
</span>
{index < Math.min(tags.length, 4) - 1 && (
<span className="text-accent">·</span>
)}
</Fragment>
))}
</div>
</div>
</div>
{/* Desktop Layout */}
<div className={cls(
"relative z-10 w-content-width mx-auto h-full hidden md:flex flex-col justify-between pt-hero-page-padding-1_5 pb-hero-page-padding",
containerClassName
)}>
<div className="relative w-full flex flex-row gap-[var(--width-10)] 2xl:gap-[var(--width-15)] items-center">
<div className={cls(
"relative w-2/5 aspect-video overflow-hidden rounded-theme-capped card p-4",
showcaseWrapperClassName
)}>
<MediaContent
imageSrc={showcaseImageSrc}
videoSrc={showcaseVideoSrc}
imageAlt={showcaseImageAlt}
videoAriaLabel={showcaseVideoAriaLabel}
imageClassName={cls("h-full w-full object-cover z-1", showcaseImageClassName)}
/>
</div>
<div className="relative flex flex-col gap-6 w-3/5">
<TextAnimation
type={theme.defaultTextAnimation}
text={title}
variant="words-trigger"
className={cls("text-7xl font-medium text-foreground text-balance text-start leading-[1]", titleClassName)}
/>
<div className={cls("flex 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>
<div className="relative w-full flex flex-row gap-10 items-end justify-between">
<div className={cls("flex flex-col gap-2", tagsContainerClassName)}>
{tags.slice(0, 6).map((tag, index) => (
<span
key={`${tag}-${index}`}
className={cls("text-base text-foreground", tagClassName)}
>
{tag}
</span>
))}
</div>
<div className="relative w-1/2">
<TextAnimation
type={theme.defaultTextAnimation}
text={description}
variant="words-trigger"
start="top 100%"
className={cls("text-lg text-foreground/75 text-balance text-start leading-[1.4]", descriptionClassName)}
/>
</div>
</div>
</div>
</section>
);
};
HeroShowcaseSplitOverlay.displayName = "HeroShowcaseSplitOverlay";
export default HeroShowcaseSplitOverlay;

View File

@@ -0,0 +1,127 @@
"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";
interface HeroSplitProps {
title: string;
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
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;
}
const HeroSplit = ({
title,
description,
tag,
tagIcon,
buttons,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
ariaLabel = "Hero section",
imagePosition = "right",
fixedMediaHeight = true,
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
}: 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("w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center", 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}
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 gap-4 mt-4", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={true}
/>
{/* Desktop */}
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls("hidden md:flex flex-col gap-3", 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 gap-4 mt-4", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={false}
/>
</div>
{imagePosition === "right" && mediaContent}
</div>
</section>
);
};
HeroSplit.displayName = "HeroSplit";
export default HeroSplit;

View File

@@ -0,0 +1,140 @@
"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";
import type { Avatar } from "@/components/shared/AvatarGroup";
interface HeroSplitAvatarsProps {
title: string;
description: string;
avatars: Avatar[];
avatarText: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
ariaLabel?: string;
imagePosition?: "left" | "right";
fixedMediaHeight?: boolean;
avatarGroupClassName?: string;
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
mediaWrapperClassName?: string;
imageClassName?: string;
}
const HeroSplitAvatars = ({
title,
description,
avatars,
avatarText,
tag,
tagIcon,
buttons,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
ariaLabel = "Hero section",
imagePosition = "right",
fixedMediaHeight = true,
avatarGroupClassName = "",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
}: HeroSplitAvatarsProps) => {
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("w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center", 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 gap-4 mt-4", 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("!mt-5", avatarGroupClassName)}
className={cls("hidden md:flex flex-col gap-3", 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 gap-4 mt-4", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={false}
/>
</div>
{imagePosition === "right" && mediaContent}
</div>
</section>
);
};
HeroSplitAvatars.displayName = "HeroSplitAvatars";
export default HeroSplitAvatars;

View File

@@ -0,0 +1,115 @@
"use client";
import TextBox from "@/components/Textbox";
import { Globe } from "@/components/shared/Globe";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
import type { COBEOptions } from "cobe";
interface HeroSplitGlobeProps {
title: string;
description: string;
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
globeConfig?: COBEOptions;
ariaLabel?: string;
imagePosition?: "left" | "right";
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
globeWrapperClassName?: string;
globeClassName?: string;
}
const HeroSplitGlobe = ({
title,
description,
tag,
tagIcon,
buttons,
globeConfig,
ariaLabel = "Hero section",
imagePosition = "right",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
globeWrapperClassName = "",
globeClassName = "",
}: HeroSplitGlobeProps) => {
const globeContent = (
<div className={cls(
"relative w-full h-fit aspect-square md:aspect-auto md:w-1/2 overflow-hidden rounded-theme-capped card p-4 md:h-[65vh]",
globeWrapperClassName
)}>
<div className="relative h-full aspect-square max-w-full max-h-full mx-auto flex items-center justify-center">
<Globe config={globeConfig} className={cls("absolute top-1/2 left-1/2 -translate-1/2", globeClassName)} />
</div>
</div>
);
return (
<section
aria-label={ariaLabel}
className={cls("w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center", containerClassName)}>
{imagePosition === "left" && globeContent}
<div className={cls("w-full md:w-1/2")}>
{/* Mobile */}
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
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 gap-4 mt-4", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={true}
/>
{/* Desktop */}
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls("hidden md:flex flex-col gap-3", 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 gap-4 mt-4", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={false}
/>
</div>
{imagePosition === "right" && globeContent}
</div>
</section>
);
};
HeroSplitGlobe.displayName = "HeroSplitGlobe";
export default HeroSplitGlobe;

View File

@@ -0,0 +1,148 @@
"use client";
import TextBox from "@/components/Textbox";
import { Globe } from "@/components/shared/Globe";
import { cls } from "@/lib/utils";
import type { LucideIcon } from "lucide-react";
import type { ButtonConfig } from "@/types/button";
import type { COBEOptions } from "cobe";
interface KpiItem {
value: string;
label: string;
}
interface HeroSplitGlobeKpiProps {
title: string;
description: string;
kpis: [KpiItem, KpiItem, KpiItem];
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
globeConfig?: COBEOptions;
ariaLabel?: string;
globePosition?: "left" | "right";
className?: string;
containerClassName?: string;
textBoxClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
tagClassName?: string;
buttonContainerClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
globeWrapperClassName?: string;
globeClassName?: string;
kpiClassName?: string;
kpiValueClassName?: string;
kpiLabelClassName?: string;
}
const HeroSplitGlobeKpi = ({
title,
description,
kpis,
tag,
tagIcon,
buttons,
globeConfig,
ariaLabel = "Hero section",
globePosition = "right",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
globeWrapperClassName = "",
globeClassName = "",
kpiClassName = "",
kpiValueClassName = "",
kpiLabelClassName = "",
}: HeroSplitGlobeKpiProps) => {
const globeContent = (
<div className={cls(
"relative w-full h-fit aspect-square md:aspect-auto md:w-1/2 md:h-[65vh]",
globeWrapperClassName
)}>
<div className="relative h-full aspect-square max-w-full max-h-full mx-auto flex items-center justify-center" >
<Globe config={globeConfig} className={cls("absolute top-1/2 left-1/2 -translate-1/2", globeClassName)} />
{kpis.map((kpi, index) => (
<div
key={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-[10%] md:left-[5%]",
index === 1 && "top-[35%] right-[2.5%] md:top-[35%]",
index === 2 && "bottom-[7.5%] left-[10%] md:left-[20%] 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>
</div>
);
return (
<section
aria-label={ariaLabel}
className={cls("w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center", containerClassName)}>
{globePosition === "left" && globeContent}
<div className={cls("w-full md:w-1/2")}>
{/* Mobile */}
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
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 gap-4 mt-4", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={true}
/>
{/* Desktop */}
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls("hidden md:flex flex-col gap-3", 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 gap-4 mt-4", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={false}
/>
</div>
{globePosition === "right" && globeContent}
</div>
</section>
);
};
HeroSplitGlobeKpi.displayName = "HeroSplitGlobeKpi";
export default HeroSplitGlobeKpi;

View File

@@ -0,0 +1,159 @@
"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";
interface KpiItem {
value: string;
label: string;
}
interface HeroSplitKpiProps {
title: string;
description: string;
kpis: [KpiItem, KpiItem, KpiItem];
tag?: string;
tagIcon?: LucideIcon;
buttons?: ButtonConfig[];
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;
kpiClassName?: string;
kpiValueClassName?: string;
kpiLabelClassName?: string;
}
const HeroSplitKpi = ({
title,
description,
kpis,
tag,
tagIcon,
buttons,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
ariaLabel = "Hero section",
imagePosition = "right",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
kpiClassName = "",
kpiValueClassName = "",
kpiLabelClassName = "",
}: HeroSplitKpiProps) => {
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}
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
aria-label={ariaLabel}
className={cls("w-full h-fit py-hero-page-padding md:py-0 md:h-svh flex items-center", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-13 md:gap-15 items-center", 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}
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 gap-4 mt-4", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={true}
/>
{/* Desktop */}
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls("hidden md:flex flex-col gap-3", 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 gap-4 mt-4", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={false}
/>
</div>
{imagePosition === "right" && mediaContent}
</div>
</section>
);
};
HeroSplitKpi.displayName = "HeroSplitKpi";
export default HeroSplitKpi;

View File

@@ -0,0 +1,116 @@
"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";
interface HeroSplitLargeProps {
title: string;
description: string;
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;
mediaWrapperClassName?: string;
imageClassName?: string;
}
const HeroSplitLarge = ({
title,
description,
tag,
tagIcon,
buttons,
imageSrc,
videoSrc,
imageAlt = "",
videoAriaLabel = "Hero video",
ariaLabel = "Hero section",
className = "",
containerClassName = "",
textBoxClassName = "",
titleClassName = "",
descriptionClassName = "",
tagClassName = "",
buttonContainerClassName = "",
buttonClassName = "",
buttonTextClassName = "",
mediaWrapperClassName = "",
imageClassName = "",
}: HeroSplitLargeProps) => {
return (
<section
aria-label={ariaLabel}
className={cls("relative w-full h-svh pt-hero-page-padding md:pt-0 flex flex-col gap-15 md:flex-row md:gap-0 items-center", className)}
>
<div className={cls("w-content-width mx-auto flex flex-col md:flex-row gap-0 md:gap-20 items-center", containerClassName)}>
<div className="w-full md:w-1/2">
{/* Mobile */}
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
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 gap-4 mt-4", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={true}
/>
{/* Desktop */}
<TextBox
title={title}
description={description}
tag={tag}
tagIcon={tagIcon}
buttons={buttons}
className={cls("hidden md:flex flex-col gap-3", 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 gap-4 mt-4", buttonContainerClassName)}
buttonClassName={cls("", buttonClassName)}
buttonTextClassName={cls("text-base", buttonTextClassName)}
center={false}
/>
</div>
<div className="relative w-full md:w-1/2" />
</div>
<div className={cls(
"relative! md:absolute! z-0 md:top-0 md:right-0 w-full md:w-[calc(50%-2.5rem)] h-full overflow-hidden",
mediaWrapperClassName
)}>
<MediaContent
imageSrc={imageSrc}
videoSrc={videoSrc}
imageAlt={imageAlt}
videoAriaLabel={videoAriaLabel}
imageClassName={cls("h-full min-h-0 rounded-none!", imageClassName)}
/>
</div>
</section>
);
};
HeroSplitLarge.displayName = "HeroSplitLarge";
export default HeroSplitLarge;

View File

@@ -0,0 +1,173 @@
"use client";
import { useCallback } from "react";
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 { 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;
logoLineHeight?: number;
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,
logoLineHeight = 1.1,
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", 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", 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",
blurClassName
)}
style={{ maskImage: MASK_GRADIENT }}
aria-hidden="true"
/>
<div className={cls("relative z-20 w-content-width mx-auto h-full 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 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 lineHeight={logoLineHeight} className={cls("text-background", 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,
};
};