mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2026-09-27 15:37:57 +00:00
277 lines
9.6 KiB
TypeScript
277 lines
9.6 KiB
TypeScript
import React, { useState, useCallback } from "react";
|
|
import clsx from "clsx";
|
|
import { CarouselButton } from "../../components/CarouselButton";
|
|
import { Divider } from "../../components/Divider";
|
|
import { PageGrid, PageGridRow, PageGridCol } from "../../components/PageGrid";
|
|
import { ButtonGroup, ButtonConfig } from "../ButtonGroup/ButtonGroup";
|
|
|
|
/**
|
|
* Props for a single slide in the CarouselFeatured component
|
|
*/
|
|
export interface CarouselSlide {
|
|
/** Unique identifier for the slide */
|
|
id: string | number;
|
|
/** Heading text displayed at the top of the content area */
|
|
heading: string;
|
|
/** Array of feature items to display in the list */
|
|
features: readonly CarouselFeatureItem[];
|
|
/** Button configurations (1-2 buttons supported) */
|
|
buttons?: ButtonConfig[];
|
|
/** Image source URL */
|
|
imageSrc: string;
|
|
/** Alt text for the image */
|
|
imageAlt: string;
|
|
}
|
|
|
|
/**
|
|
* Props for a feature list item
|
|
*/
|
|
export interface CarouselFeatureItem {
|
|
/** Feature title */
|
|
title: React.ReactNode;
|
|
/** Feature description */
|
|
description: React.ReactNode;
|
|
}
|
|
|
|
/**
|
|
* Background color options for CarouselFeatured
|
|
* Each variant adapts to light/dark mode:
|
|
* - 'grey': Light mode: gray-200 (#E6EAF0), Dark mode: gray-300 (#CAD4DF)
|
|
* - 'neutral': Light mode: white (#FFF), Dark mode: black (#141414)
|
|
* - 'yellow': Light mode: yellow-100 (#F3F1EB), Dark mode: yellow-100 (#F3F1EB)
|
|
*/
|
|
export type CarouselFeaturedBackground = "grey" | "neutral" | "yellow";
|
|
|
|
/**
|
|
* Slide transition style for CarouselFeatured
|
|
* - 'fade': slides crossfade in place, with the image/heading leading and the
|
|
* feature list and buttons following on a short stagger (default)
|
|
* - 'slide': slides wipe horizontally left/right
|
|
*/
|
|
export type CarouselFeaturedTransition = "slide" | "fade";
|
|
|
|
/**
|
|
* Props for the CarouselFeatured pattern component
|
|
*/
|
|
export interface CarouselFeaturedProps extends React.ComponentPropsWithoutRef<"section"> {
|
|
/** Array of slides to display */
|
|
slides: readonly CarouselSlide[];
|
|
/** Background color variant. Defaults to 'grey'. */
|
|
background?: CarouselFeaturedBackground;
|
|
/** Slide transition style. Defaults to 'fade'. */
|
|
transition?: CarouselFeaturedTransition;
|
|
}
|
|
|
|
/**
|
|
* CarouselFeatured Pattern Component
|
|
*
|
|
* A featured image carousel with two-column layout on desktop (image left, content right)
|
|
* and single-column layout on tablet/mobile (content top, image bottom).
|
|
* Features a heading, feature list with dividers, and optional buttons.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <CarouselFeatured
|
|
* slides={[
|
|
* {
|
|
* id: 1,
|
|
* heading: "Powered by Developers",
|
|
* features: [
|
|
* { title: "Easy-to-Integrate APIs", description: "Build with common languages..." },
|
|
* { title: "Full Lifecycle Support", description: "From dev tools to deployment..." },
|
|
* ],
|
|
* buttons: [
|
|
* { label: "Get Started", href: "/docs" },
|
|
* { label: "Learn More", href: "/about" }
|
|
* ],
|
|
* imageSrc: '/image1.jpg',
|
|
* imageAlt: 'Slide 1'
|
|
* },
|
|
* ]}
|
|
* />
|
|
* ```
|
|
*/
|
|
export const CarouselFeatured = React.forwardRef<
|
|
HTMLElement,
|
|
CarouselFeaturedProps
|
|
>((props, ref) => {
|
|
const {
|
|
slides,
|
|
background = "grey",
|
|
transition = "fade",
|
|
className,
|
|
children,
|
|
...rest
|
|
} = props;
|
|
|
|
const isFade = transition === "fade";
|
|
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
|
|
const canGoPrev = currentIndex > 0;
|
|
const canGoNext = currentIndex < slides.length - 1;
|
|
|
|
const goToPrev = useCallback(() => {
|
|
if (canGoPrev) {
|
|
setCurrentIndex((prev) => prev - 1);
|
|
}
|
|
}, [canGoPrev]);
|
|
|
|
const goToNext = useCallback(() => {
|
|
if (canGoNext) {
|
|
setCurrentIndex((prev) => prev + 1);
|
|
}
|
|
}, [canGoNext]);
|
|
|
|
// Determine carousel nav button variant based on background
|
|
// grey/yellow → black (always), neutral → green (always)
|
|
const buttonVariant = background === "neutral" ? "green" : "black";
|
|
const currentSlide = slides[currentIndex];
|
|
|
|
// Early return for empty slides
|
|
if (slides.length === 0) {
|
|
console.warn("CarouselFeatured: No slides provided");
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<PageGrid
|
|
ref={ref as React.Ref<HTMLDivElement>}
|
|
className={clsx(
|
|
"bds-carousel-featured",
|
|
`bds-carousel-featured--bg-${background}`,
|
|
`bds-carousel-featured--${transition}`,
|
|
className,
|
|
)}
|
|
aria-roledescription="carousel"
|
|
aria-label={currentSlide.heading}
|
|
{...rest}
|
|
>
|
|
<div
|
|
className="bds-carousel-featured__slides"
|
|
role="group"
|
|
aria-roledescription="slide"
|
|
aria-label={`Slide ${currentIndex + 1} of ${slides.length}`}
|
|
>
|
|
<div
|
|
className="bds-carousel-featured__slide-track"
|
|
// The fade variant stacks all slides in one grid cell, so there is no
|
|
// track to move — the crossfade is driven entirely by the --active class.
|
|
style={
|
|
isFade
|
|
? undefined
|
|
: { transform: `translateX(-${currentIndex * 100}%)` }
|
|
}
|
|
>
|
|
{slides.map((slide, index) => (
|
|
<div
|
|
key={slide.id}
|
|
className={clsx("bds-carousel-featured__slide", {
|
|
"bds-carousel-featured__slide--active":
|
|
index === currentIndex,
|
|
})}
|
|
aria-hidden={index !== currentIndex}
|
|
// Keeps focus and pointer events out of inactive slides, which are
|
|
// still rendered (off-track for 'slide', stacked underneath for 'fade')
|
|
inert={index !== currentIndex}
|
|
>
|
|
<PageGridRow>
|
|
{/* Content Column - Right on desktop, top on mobile */}
|
|
<PageGridCol
|
|
span={{ base: 4, md: 8, lg: 6 }}
|
|
className="bds-carousel-featured__content-col order-1 order-lg-2"
|
|
>
|
|
<div className="bds-carousel-featured__content">
|
|
{/* Header row with heading */}
|
|
<div className="bds-carousel-featured__header">
|
|
<h2 className="bds-carousel-featured__heading h-md">
|
|
{slide.heading}
|
|
</h2>
|
|
</div>
|
|
|
|
{/* Bottom section: features + CTA grouped together */}
|
|
<div className="bds-carousel-featured__bottom">
|
|
{/* Feature list with dividers */}
|
|
<ul className="bds-carousel-featured__features">
|
|
{slide.features.map((feature, featureIndex) => (
|
|
<li
|
|
key={featureIndex}
|
|
className="bds-carousel-featured__feature"
|
|
>
|
|
<Divider color="base" weight="regular" />
|
|
<div className="bds-carousel-featured__feature-title body-r">
|
|
{feature.title}
|
|
</div>
|
|
<div className="bds-carousel-featured__feature-description label-l">
|
|
{feature.description}
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
|
|
{/* CTA section with buttons */}
|
|
<div className="bds-carousel-featured__cta">
|
|
{/* Buttons wrapper - groups primary and tertiary together */}
|
|
{slide.buttons && slide.buttons.length > 0 && (
|
|
<ButtonGroup
|
|
buttons={slide.buttons}
|
|
maxButtons={2}
|
|
surface={
|
|
background === "neutral"
|
|
? { intention: "neutral" }
|
|
: { context: "on-saturated" }
|
|
}
|
|
className="bds-carousel-featured__buttons"
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</PageGridCol>
|
|
|
|
{/* Image/Media Column - Left on desktop, bottom on mobile */}
|
|
<PageGridCol
|
|
span={{ base: 4, md: 8, lg: 6 }}
|
|
className="bds-carousel-featured__media-col order-2 order-lg-1"
|
|
>
|
|
<div className="bds-carousel-featured__media">
|
|
<img
|
|
src={slide.imageSrc}
|
|
alt={slide.imageAlt}
|
|
className="bds-carousel-featured__image"
|
|
loading={index === 0 ? "eager" : "lazy"}
|
|
/>
|
|
</div>
|
|
</PageGridCol>
|
|
</PageGridRow>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{slides.length > 1 && (
|
|
<div className={clsx("bds-carousel-featured__nav", "clickable")}>
|
|
{(["prev", "next"] as const).map((direction) => (
|
|
<CarouselButton
|
|
key={direction}
|
|
direction={direction}
|
|
variant={buttonVariant}
|
|
disabled={direction === "prev" ? !canGoPrev : !canGoNext}
|
|
onClick={direction === "prev" ? goToPrev : goToNext}
|
|
aria-label={
|
|
direction === "prev" ? "Previous slide" : "Next slide"
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
{/* Render any additional children */}
|
|
{children}
|
|
</PageGrid>
|
|
);
|
|
});
|
|
|
|
CarouselFeatured.displayName = "CarouselFeatured";
|
|
|
|
export default CarouselFeatured;
|