Add CarouselCardList pattern component and showcase

- Introduced the CarouselCardList component for a horizontal scrolling card carousel with navigation buttons.
- Implemented responsive design for mobile, tablet, and desktop views.
- Added SCSS styles for light and dark mode support.
- Created a dedicated showcase page demonstrating the CarouselCardList features and usage examples.
- Included comprehensive documentation for props and variants.
This commit is contained in:
akcodez
2026-01-20 12:41:58 -08:00
committed by Calvin Jhunjhuwala
parent e37dd720cd
commit 16f606a34c
8 changed files with 1835 additions and 619 deletions

View File

@@ -0,0 +1,608 @@
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
import { CarouselCardList } from "shared/patterns/CarouselCardList";
import { Divider } from "shared/components/Divider";
export const frontmatter = {
seo: {
title: 'CarouselCardList Pattern Showcase',
description: "A comprehensive showcase of the CarouselCardList pattern component demonstrating horizontal scrolling, navigation buttons, and color variants in the XRPL.org Design System.",
}
};
// Sample icon components for demonstration
const TokenIcon = () => (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="34" cy="34" r="20" stroke="currentColor" strokeWidth="2" fill="none"/>
<path d="M34 22V46M26 34H42" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
const WalletIcon = () => (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="14" y="20" width="40" height="28" rx="4" stroke="currentColor" strokeWidth="2" fill="none"/>
<circle cx="46" cy="34" r="4" fill="currentColor"/>
<path d="M14 28H54" stroke="currentColor" strokeWidth="2"/>
</svg>
);
const ChartIcon = () => (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 50L26 38L34 46L54 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M46 18H54V26" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const ShieldIcon = () => (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M34 12L52 20V32C52 44 44 52 34 56C24 52 16 44 16 32V20L34 12Z" stroke="currentColor" strokeWidth="2" fill="none"/>
<path d="M26 34L32 40L42 28" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const GlobeIcon = () => (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="34" cy="34" r="20" stroke="currentColor" strokeWidth="2" fill="none"/>
<ellipse cx="34" cy="34" rx="10" ry="20" stroke="currentColor" strokeWidth="2" fill="none"/>
<path d="M14 34H54" stroke="currentColor" strokeWidth="2"/>
</svg>
);
const CodeIcon = () => (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26 24L14 34L26 44" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M42 24L54 34L42 44" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M38 18L30 50" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
// Sample cards data for neutral variant
const neutralCards = [
{
icon: <TokenIcon />,
title: "Native\nTokenization",
description: "Issue and manage digital assets directly on the ledger without smart contracts.",
href: "#tokenization",
},
{
icon: <WalletIcon />,
title: "Low Cost\nTransactions",
description: "Transaction costs are a fraction of a cent, making microtransactions viable.",
href: "#low-cost",
},
{
icon: <ChartIcon />,
title: "Built-in\nDEX",
description: "Trade any token for any other token using the native decentralized exchange.",
href: "#dex",
},
{
icon: <ShieldIcon />,
title: "Enterprise\nSecurity",
description: "Multi-signature support and advanced key management for institutional needs.",
href: "#security",
},
{
icon: <GlobeIcon />,
title: "Global\nReach",
description: "Connect to a worldwide network of validators in seconds, not minutes.",
href: "#global",
},
{
icon: <CodeIcon />,
title: "Developer\nFriendly",
description: "Comprehensive SDKs and APIs for JavaScript, Python, Java, and more.",
href: "#developer",
},
];
// Sample cards data for green variant
const greenCards = [
{
icon: <TokenIcon />,
title: "Stablecoin\nIssuance",
description: "Create and manage compliant stablecoins with built-in freeze and clawback capabilities.",
href: "#stablecoin",
},
{
icon: <WalletIcon />,
title: "Institutional\nCustody",
description: "Multi-signature accounts and escrow features for enterprise-grade custody solutions.",
href: "#custody",
},
{
icon: <ChartIcon />,
title: "Real-Time\nSettlement",
description: "Transactions settle in 3-5 seconds with finality, enabling real-time payments.",
href: "#settlement",
},
{
icon: <ShieldIcon />,
title: "Regulatory\nCompliance",
description: "Built-in features for AML/KYC compliance and regulatory reporting requirements.",
href: "#compliance",
},
{
icon: <GlobeIcon />,
title: "Cross-Border\nPayments",
description: "Seamless international transfers without correspondent banking delays.",
href: "#cross-border",
},
];
export default function CarouselCardListShowcase() {
return (
<div className="landing">
<div className="overflow-hidden">
{/* Hero Section */}
<section className="py-26 text-center">
<div className="col-lg-8 mx-auto">
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
<h1 className="mb-4">CarouselCardList Pattern</h1>
<p className="longform">
A horizontal scrolling carousel that displays CardOffgrid components with navigation buttons.
Supports neutral and green color variants, responsive sizing, and dark/light mode theming.
</p>
</div>
</section>
{/* Feature Overview */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Features</h2>
<div className="d-flex flex-row gap-6" style={{ flexWrap: 'wrap' }}>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Layout</h6>
<ul className="mb-0">
<li>Horizontal scrolling cards</li>
<li>Navigation buttons (prev/next)</li>
<li>Title constrained to grid</li>
<li>Hidden scrollbar</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Responsive Behavior</h6>
<ul className="mb-0">
<li><strong>Mobile:</strong> 343×400px cards</li>
<li><strong>Tablet:</strong> 356×440px cards</li>
<li><strong>Desktop:</strong> 400×480px cards</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Button States</h6>
<ul className="mb-0">
<li>Enabled / Disabled</li>
<li>Hover / Active states</li>
<li>Focus ring for keyboard nav</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Theming</h6>
<ul className="mb-0">
<li>Dark mode (default)</li>
<li>Light mode (<code>html.light</code>)</li>
<li>Neutral &amp; Green card variants</li>
<li>Independent button colors (neutral, green, black)</li>
</ul>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Neutral Cards + Neutral Buttons (Default) */}
<section className="py-10">
<PageGrid className="mb-6">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-3">Neutral Cards + Neutral Buttons (Default)</h2>
<p className="mb-0">
<code>variant="neutral"</code> - Gray cards with matching gray navigation buttons.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CarouselCardList
variant="neutral"
buttonVariant="neutral"
heading="Why Build on the XRP Ledger"
description="Discover the unique features that make XRPL the ideal blockchain for building tokenization, payments, and DeFi applications."
cards={neutralCards}
/>
</section>
{/* Neutral Cards + Green Buttons */}
<section className="py-10">
<PageGrid className="mb-6">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-3">Neutral Cards + Green Buttons</h2>
<p className="mb-0">
<code>variant="neutral" buttonVariant="green"</code> - Gray cards with green navigation buttons.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CarouselCardList
variant="neutral"
buttonVariant="green"
heading="Platform Features"
description="Gray cards paired with vibrant green buttons for emphasis."
cards={neutralCards}
/>
</section>
{/* Neutral Cards + Black Buttons */}
<section className="py-10">
<PageGrid className="mb-6">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-3">Neutral Cards + Black Buttons</h2>
<p className="mb-0">
<code>variant="neutral" buttonVariant="black"</code> - Gray cards with black navigation buttons.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CarouselCardList
variant="neutral"
buttonVariant="black"
heading="Developer Tools"
description="Gray cards paired with black buttons for high contrast."
cards={neutralCards}
/>
</section>
{/* Green Cards + Green Buttons */}
<section className="py-10">
<PageGrid className="mb-6">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-3">Green Cards + Green Buttons</h2>
<p className="mb-0">
<code>variant="green" buttonVariant="green"</code> - Green cards with matching green navigation buttons.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CarouselCardList
variant="green"
buttonVariant="green"
heading="Enterprise Solutions"
description="Purpose-built features for institutional adoption with cohesive green theming."
cards={greenCards}
/>
</section>
{/* Green Cards + Black Buttons */}
<section className="py-10">
<PageGrid className="mb-6">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-3">Green Cards + Black Buttons</h2>
<p className="mb-0">
<code>variant="green" buttonVariant="black"</code> - Green cards with black navigation buttons.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CarouselCardList
variant="green"
buttonVariant="black"
heading="Cross-Border Payments"
description="Green cards with contrasting black buttons for visual interest."
cards={greenCards}
/>
</section>
{/* Navigation Buttons */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Navigation Button Specifications</h2>
<div className="d-flex flex-row gap-6 mb-6" style={{ flexWrap: 'wrap' }}>
<div style={{ flex: '1 1 220px' }}>
<h6 className="mb-3">Dimensions</h6>
<ul className="mb-0">
<li><strong>Desktop:</strong> 40px × 40px</li>
<li><strong>Tablet/Mobile:</strong> 37px × 37px</li>
<li><strong>Gap:</strong> 8px between buttons</li>
</ul>
</div>
<div style={{ flex: '1 1 220px' }}>
<h6 className="mb-3">Neutral Colors (Dark Mode)</h6>
<ul className="mb-0">
<li><strong>Enabled:</strong> $gray-500 (#72777E)</li>
<li><strong>Hover:</strong> $gray-400 (#8A919A)</li>
<li><strong>Disabled:</strong> $gray-500 @ 50%</li>
</ul>
</div>
<div style={{ flex: '1 1 220px' }}>
<h6 className="mb-3">Green Colors (Dark Mode)</h6>
<ul className="mb-0">
<li><strong>Enabled:</strong> $green-300 (#21E46B)</li>
<li><strong>Hover:</strong> $green-200 (#70EE97)</li>
<li><strong>Disabled:</strong> $green-500 @ 50%</li>
</ul>
</div>
<div style={{ flex: '1 1 220px' }}>
<h6 className="mb-3">Black Colors (Dark Mode)</h6>
<ul className="mb-0">
<li><strong>Enabled:</strong> $black (#141414)</li>
<li><strong>Hover:</strong> $gray-500 (#72777E)</li>
<li><strong>Disabled:</strong> $black @ 50%</li>
</ul>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Spacing Tokens */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Spacing Tokens</h2>
<div className="mb-6">
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
<div style={{ width: '200px', flexShrink: 0 }}><strong>Token</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Mobile</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Tablet</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Desktop</strong></div>
</div>
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '200px', flexShrink: 0 }}>Header Gap</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>8px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>8px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>16px</code></div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '200px', flexShrink: 0 }}>Section Gap</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>24px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>32px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>40px</code></div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '200px', flexShrink: 0 }}>Cards Gap</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>8px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>8px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>8px</code></div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '200px', flexShrink: 0 }}>Card Dimensions</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>343×400px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>356×440px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>400×480px</code></div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '200px', flexShrink: 0 }}>Card Padding</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>16px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>20px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>24px</code></div>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* API Reference */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Component API</h2>
<h5 className="mb-4">CarouselCardListProps</h5>
<div className="mb-8">
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
<div style={{ width: '140px', flexShrink: 0 }}><strong>Prop</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Type</strong></div>
<div style={{ width: '100px', flexShrink: 0 }}><strong>Default</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Description</strong></div>
</div>
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>variant</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>'neutral' | 'green'</code></div>
<div style={{ width: '100px', flexShrink: 0 }}><code>'neutral'</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Color variant for cards</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>buttonVariant</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>'neutral' | 'green' | 'black'</code></div>
<div style={{ width: '100px', flexShrink: 0 }}><code>'neutral'</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Color variant for navigation buttons (independent of cards)</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>heading</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>ReactNode</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Section heading text</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>description</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>ReactNode</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Section description text</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>cards</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>CarouselCardConfig[]</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Array of card configurations</div>
</div>
</div>
<h5 className="mb-4">CarouselCardConfig</h5>
<p className="mb-4">Each card in the <code>cards</code> array accepts the following properties (same as CardOffgrid, without variant):</p>
<div className="mb-6">
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
<div style={{ width: '120px', flexShrink: 0 }}><strong>Prop</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Type</strong></div>
<div style={{ width: '100px', flexShrink: 0 }}><strong>Required</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Description</strong></div>
</div>
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>icon</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>ReactNode | string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Icon component or image URL</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>title</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Card title (use \n for line breaks)</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>description</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Card description text</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>href</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>No</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Link destination URL</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>onClick</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>() =&gt; void</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>No</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Click handler function</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>disabled</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>boolean</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>No</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Disabled state</div>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Usage Example */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Usage Example</h2>
<div className="card p-4">
<pre className="mb-0" style={{ backgroundColor: 'var(--bs-gray-800)', padding: '1rem', borderRadius: '4px', overflow: 'auto' }}>
{`import { CarouselCardList } from 'shared/patterns/CarouselCardList';
// Basic usage - button color matches card color by default
<CarouselCardList
variant="neutral"
heading="Why Build on the XRP Ledger"
description="Discover the unique features that make XRPL ideal for your project."
cards={[
{
icon: <TokenIcon />,
title: "Native\\nTokenization",
description: "Issue and manage digital assets directly on the ledger.",
href: "/docs/tokenization",
},
// ... more cards
]}
/>
// With independent button color
<CarouselCardList
variant="neutral"
buttonVariant="black" // Button color independent of card color
heading="Developer Tools"
description="Gray cards with black navigation buttons."
cards={cards}
/>`}
</pre>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Design References */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Design References</h2>
<div className="d-flex flex-column gap-3">
<div>
<strong>Main Carousel Design:</strong>{' '}
<a href="https://www.figma.com/design/w0CVv1c40nWDRD27mLiMWS/Section-Carousel---Card-List?node-id=15055-3730&m=dev" target="_blank" rel="noopener noreferrer">
Section Carousel - Card List (Figma)
</a>
</div>
<div>
<strong>Button States:</strong>{' '}
<a href="https://www.figma.com/design/w0CVv1c40nWDRD27mLiMWS/Section-Carousel---Card-List?node-id=15055-1033&m=dev" target="_blank" rel="noopener noreferrer">
Carousel Button States (Figma)
</a>
</div>
<div>
<strong>Component Location:</strong>{' '}
<code>shared/patterns/CarouselCardList/</code>
</div>
<div>
<strong>Documentation:</strong>{' '}
<code>shared/patterns/CarouselCardList/CarouselCardList.md</code>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
</div>
</div>
);
}

View File

@@ -0,0 +1,175 @@
# CarouselCardList Pattern
A horizontal scrolling carousel pattern that displays `CardOffgrid` components with navigation buttons. Features responsive sizing, smooth scrolling, and full dark/light mode theming support.
## Features
- Horizontal scrolling card carousel with navigation buttons
- Automatic button enable/disable based on scroll position
- Two color variants: `neutral` and `green` (inherited by cards and buttons)
- Responsive card sizing across mobile, tablet, and desktop breakpoints
- Full dark mode and light mode support
- Heading and description constrained to page grid
- Hidden scrollbar with smooth scroll behavior
- Keyboard navigation and accessibility support
## Usage
```tsx
import { CarouselCardList } from 'shared/patterns/CarouselCardList';
<CarouselCardList
variant="neutral"
heading="Why Build on the XRP Ledger"
description="Discover the unique features that make XRPL ideal for your project."
cards={[
{
icon: <TokenIcon />,
title: "Native\nTokenization",
description: "Issue and manage digital assets directly on the ledger.",
href: "/docs/tokenization",
},
{
icon: <WalletIcon />,
title: "Low Cost\nTransactions",
description: "Transaction costs are a fraction of a cent.",
href: "/docs/fees",
},
// ... more cards
]}
/>
```
## Props
### CarouselCardListProps
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `variant` | `'neutral' \| 'green'` | No | `'neutral'` | Color variant for cards and navigation buttons |
| `heading` | `ReactNode` | Yes | - | Section heading text |
| `description` | `ReactNode` | Yes | - | Section description text |
| `cards` | `CarouselCardConfig[]` | Yes | - | Array of card configurations |
| `className` | `string` | No | - | Additional CSS classes for the section |
### CarouselCardConfig
Each card in the `cards` array accepts the following properties (same as `CardOffgridProps`, excluding `variant`):
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `icon` | `ReactNode \| string` | Yes | Icon component or image URL |
| `title` | `string` | Yes | Card title (use `\n` for line breaks) |
| `description` | `string` | Yes | Card description text |
| `href` | `string` | No | Link destination URL |
| `onClick` | `() => void` | No | Click handler function |
| `disabled` | `boolean` | No | Disabled state |
| `className` | `string` | No | Additional CSS classes |
## Variants
### Neutral Variant
The default variant using gray color palette. Best for general purpose content sections.
```tsx
<CarouselCardList
variant="neutral"
heading="Platform Features"
description="Explore what makes our platform unique."
cards={cards}
/>
```
### Green Variant
Uses the brand green color palette. Best for featured or highlighted sections.
```tsx
<CarouselCardList
variant="green"
heading="Enterprise Solutions"
description="Purpose-built for institutional adoption."
cards={cards}
/>
```
## Responsive Behavior
| Breakpoint | Card Width | Card Height | Card Padding | Button Size |
|------------|------------|-------------|--------------|-------------|
| Mobile (< 576px) | 343px | 400px | 16px | 37px |
| Tablet (576px - 991px) | 356px | 440px | 20px | 37px |
| Desktop (≥ 992px) | 400px | 480px | 24px | 40px |
### Spacing Tokens
| Token | Mobile | Tablet | Desktop |
|-------|--------|--------|---------|
| Header gap | 8px | 8px | 16px |
| Section gap | 24px | 32px | 40px |
| Cards gap | 8px | 8px | 8px |
## Accessibility
- Navigation buttons have descriptive `aria-label` attributes ("Previous cards", "Next cards")
- Carousel track has `role="region"` with `aria-label="Card carousel"`
- Keyboard navigation supported via Tab and arrow keys
- Focus ring visible on keyboard navigation (uses `focus-visible`)
- Disabled buttons properly convey state via `disabled` attribute and visual styling
- Cards support keyboard interaction (Tab, Enter, Space)
## Design Tokens
### Colors (Dark Mode - Default)
**Neutral Variant:**
- Button Enabled: `$gray-300` (#CAD4DF)
- Button Hover: `$gray-400` (#8A919A)
- Button Disabled: `$gray-500` @ 50% opacity
**Green Variant:**
- Button Enabled: `$green-300` (#21E46B)
- Button Hover: `$green-200` (#70EE97)
- Button Disabled: `$green-100` (#ACFFC5)
### Colors (Light Mode - `html.light`)
**Neutral Variant:**
- Button Enabled: `$gray-300` (#CAD4DF)
- Button Hover: `$gray-400` (#8A919A)
- Button Disabled: `$gray-100` (#F0F3F7)
**Green Variant:**
- Button Enabled: `$green-300` (#21E46B)
- Button Hover: `$green-200` (#70EE97)
- Button Disabled: `$green-100` (#ACFFC5)
## CSS Classes
| Class | Description |
|-------|-------------|
| `.bds-carousel-card-list` | Base section container |
| `.bds-carousel-card-list--neutral` | Neutral color variant |
| `.bds-carousel-card-list--green` | Green color variant |
| `.bds-carousel-card-list__header` | Header wrapper (title, subtitle, nav) |
| `.bds-carousel-card-list__header-content` | Title and subtitle wrapper |
| `.bds-carousel-card-list__heading` | Section heading (uses `.h-md`) |
| `.bds-carousel-card-list__description` | Section description (uses `.body-l`) |
| `.bds-carousel-card-list__nav` | Navigation buttons wrapper |
| `.bds-carousel-card-list__button` | Navigation button |
| `.bds-carousel-card-list__button--prev` | Previous button modifier |
| `.bds-carousel-card-list__button--disabled` | Disabled button modifier |
| `.bds-carousel-card-list__track-wrapper` | Scroll container wrapper |
| `.bds-carousel-card-list__track` | Horizontal scroll track |
| `.bds-carousel-card-list__card` | Individual card wrapper |
## Showcase
View the pattern showcase at: `/about/carousel-card-list-showcase`
## Design References
- **Main Design:** [Section Carousel - Card List (Figma)](https://www.figma.com/design/w0CVv1c40nWDRD27mLiMWS/Section-Carousel---Card-List?node-id=15055-3730&m=dev)
- **Button States:** [Carousel Button States (Figma)](https://www.figma.com/design/w0CVv1c40nWDRD27mLiMWS/Section-Carousel---Card-List?node-id=15055-1033&m=dev)

View File

@@ -0,0 +1,514 @@
// BDS CarouselCardList Pattern Styles
// Brand Design System - Horizontal scrolling carousel with CardOffgrid components
//
// Naming Convention: BEM with 'bds' namespace
// .bds-carousel-card-list - Base section container
// .bds-carousel-card-list--neutral - Neutral color variant
// .bds-carousel-card-list--green - Green color variant
// .bds-carousel-card-list__header - Header wrapper (title, subtitle, nav)
// .bds-carousel-card-list__header-content - Title and subtitle wrapper
// .bds-carousel-card-list__heading - Section heading (uses .h-md)
// .bds-carousel-card-list__description - Section description (uses .body-l)
// .bds-carousel-card-list__nav - Navigation buttons wrapper
// .bds-carousel-card-list__button - Navigation button
// .bds-carousel-card-list__track-wrapper - Scroll container wrapper
// .bds-carousel-card-list__track - Horizontal scroll track
// .bds-carousel-card-list__card - Individual card wrapper
// =============================================================================
// Design Tokens (from Figma)
// =============================================================================
$bds-grid-gutter: 8px;
// Grid padding (matches PageGrid container padding)
$bds-carousel-grid-padding-sm: 18px; // Mobile
$bds-carousel-grid-padding-md: 24px; // Tablet
$bds-carousel-grid-padding-lg: 32px; // Desktop (lg+)
$bds-carousel-grid-max-width: 1280px; // Max container width (per _breakpoints.scss $xl)
// Spacing - Header gap (between heading and description)
$bds-carousel-header-gap-sm: 8px; // Mobile
$bds-carousel-header-gap-md: 8px; // Tablet
$bds-carousel-header-gap-lg: 16px; // Desktop
// Spacing - Section gap (between header content and buttons row on mobile)
$bds-carousel-section-gap-sm: 24px; // Mobile
$bds-carousel-section-gap-md: 32px; // Tablet
$bds-carousel-section-gap-lg: 40px; // Desktop
// Spacing - Gap between header and cards
$bds-carousel-cards-gap-sm: 24px; // Mobile
$bds-carousel-cards-gap-md: 32px; // Tablet
$bds-carousel-cards-gap-lg: 40px; // Desktop
// Button dimensions
$bds-carousel-button-size-sm: 37px; // Mobile/Tablet
$bds-carousel-button-size-lg: 40px; // Desktop
$bds-carousel-button-gap: 8px;
// Card dimensions per breakpoint
$bds-carousel-card-width-sm: 343px; // Mobile
$bds-carousel-card-height-sm: 400px;
$bds-carousel-card-width-md: 356px; // Tablet
$bds-carousel-card-height-md: 440px;
$bds-carousel-card-width-lg: 400px; // Desktop
$bds-carousel-card-height-lg: 480px;
// Card padding per breakpoint
$bds-carousel-card-padding-sm: 16px;
$bds-carousel-card-padding-md: 20px;
$bds-carousel-card-padding-lg: 24px;
// Transition
$bds-carousel-transition: 200ms cubic-bezier(0.98, 0.12, 0.12, 0.98);
// =============================================================================
// Section Container
// =============================================================================
.bds-carousel-card-list {
width: 100%;
// Constrain to max-width at xl breakpoint (per _breakpoints.scss)
@include media-breakpoint-up(xl) {
max-width: $bds-carousel-grid-max-width;
margin-left: auto;
margin-right: auto;
}
// Allow focus rings to be visible (no overflow:hidden)
}
// =============================================================================
// Header Section
// =============================================================================
.bds-carousel-card-list__header {
display: flex;
flex-direction: column;
gap: $bds-carousel-section-gap-sm;
// Apply same padding as track to align header with cards
padding-left: $bds-carousel-grid-padding-sm;
padding-right: $bds-carousel-grid-padding-sm;
@include media-breakpoint-up(md) {
gap: $bds-carousel-section-gap-md;
padding-left: $bds-carousel-grid-padding-md;
padding-right: $bds-carousel-grid-padding-md;
}
// Row layout only at desktop (lg and up)
@include media-breakpoint-up(lg) {
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
gap: $bds-carousel-section-gap-lg;
padding-left: $bds-carousel-grid-padding-lg;
padding-right: $bds-carousel-grid-padding-lg;
}
}
.bds-carousel-card-list__header-content {
display: flex;
flex-direction: column;
gap: $bds-carousel-header-gap-sm;
// Full width on mobile and tablet
max-width: 100%;
@include media-breakpoint-up(md) {
gap: $bds-carousel-header-gap-md;
}
// Constrain heading/description to grid (8 columns at desktop)
@include media-breakpoint-up(lg) {
gap: $bds-carousel-header-gap-lg;
max-width: 808px; // Desktop: 8 columns
}
}
.bds-carousel-card-list__heading {
margin: 0;
// Typography handled by .h-md class from _font.scss
}
.bds-carousel-card-list__description {
margin: 0;
// Typography handled by .body-l class from _font.scss
}
// =============================================================================
// Navigation Buttons Container
// =============================================================================
.bds-carousel-card-list__nav {
display: flex;
gap: $bds-carousel-button-gap;
justify-content: flex-end;
flex-shrink: 0;
// Add padding to allow focus ring to be visible without clipping
padding: 4px;
margin: -4px;
}
// =============================================================================
// Navigation Buttons
// =============================================================================
.bds-carousel-card-list__button {
// Reset button styles
appearance: none;
border: none;
background: none;
padding: 0;
margin: 0;
cursor: pointer;
// Layout
display: flex;
align-items: center;
justify-content: center;
width: $bds-carousel-button-size-sm;
height: $bds-carousel-button-size-sm;
// Transition
transition: background-color $bds-carousel-transition,
opacity $bds-carousel-transition;
@include media-breakpoint-up(lg) {
width: $bds-carousel-button-size-lg;
height: $bds-carousel-button-size-lg;
}
// Focus styles
&:focus {
outline: 2px solid $white;
outline-offset: 2px;
}
&:focus:not(:focus-visible) {
outline: none;
}
&:focus-visible {
outline: 2px solid $white;
outline-offset: 2px;
}
}
// Arrow icon
.bds-carousel-card-list__arrow-icon {
width: 18px;
height: 16px;
@include media-breakpoint-down(lg) {
width: 18px;
height: 15px;
}
}
// =============================================================================
// Scroll Track
// =============================================================================
.bds-carousel-card-list__track-wrapper {
margin-top: $bds-carousel-cards-gap-sm;
overflow: visible;
// Add left padding here so it's OUTSIDE the scrollable area
padding-left: $bds-carousel-grid-padding-sm;
@include media-breakpoint-up(md) {
margin-top: $bds-carousel-cards-gap-md;
padding-left: $bds-carousel-grid-padding-md;
}
@include media-breakpoint-up(lg) {
margin-top: $bds-carousel-cards-gap-lg;
padding-left: $bds-carousel-grid-padding-lg;
}
}
.bds-carousel-card-list__track {
display: flex;
gap: $bds-grid-gutter;
overflow-x: auto;
overflow-y: visible;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
// Hide scrollbar but keep functionality
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
// Vertical padding to prevent focus ring clipping
padding-top: 4px;
padding-bottom: 4px;
margin-top: -4px;
margin-bottom: -4px;
// Focus outline for keyboard navigation
&:focus {
outline: none;
}
&:focus-visible {
outline: 2px solid $white;
outline-offset: 4px;
}
}
// =============================================================================
// Card Wrapper
// =============================================================================
.bds-carousel-card-list__card {
flex-shrink: 0;
scroll-snap-align: start;
// Override CardOffgrid dimensions for carousel
.bds-card-offgrid {
width: $bds-carousel-card-width-sm;
height: $bds-carousel-card-height-sm;
padding: $bds-carousel-card-padding-sm;
@include media-breakpoint-up(md) {
width: $bds-carousel-card-width-md;
height: $bds-carousel-card-height-md;
padding: $bds-carousel-card-padding-md;
}
@include media-breakpoint-up(lg) {
width: $bds-carousel-card-width-lg;
height: $bds-carousel-card-height-lg;
padding: $bds-carousel-card-padding-lg;
}
// Fix: Prevent unwanted hover styles from parent styles
// No text underline on hover
&:hover {
text-decoration: none;
}
// Ensure title and description never have underline
.bds-card-offgrid__title,
.bds-card-offgrid__description {
text-decoration: none;
&:hover {
text-decoration: none;
}
}
// Ensure icon does not change color on hover
.bds-card-offgrid__icon-container {
// Icon color is inherited from card text color, which CardOffgrid manages
// No additional color changes should happen on hover
> * {
transition: none;
}
}
}
}
// =============================================================================
// DARK MODE (Default) - Section Text Colors
// =============================================================================
// Section text colors - Dark Mode (applies to both neutral and green card variants)
.bds-carousel-card-list--neutral,
.bds-carousel-card-list--green {
.bds-carousel-card-list__heading,
.bds-carousel-card-list__description {
color: $white;
}
}
// =============================================================================
// DARK MODE (Default) - Button Color Variants
// Button colors are independent of card colors - use buttonVariant prop
// =============================================================================
// Neutral button variant - Dark Mode
// Enabled: #CAD4DF (gray-300), Hover: #E6EAF0 (gray-200), Active: #CAD4DF (gray-300)
// Disabled: opacity 0.5
.bds-carousel-card-list__button--neutral {
background-color: $gray-300; // #CAD4DF
color: $black; // #141414
&:hover:not(:disabled) {
background-color: $gray-200; // #E6EAF0
}
&:active:not(:disabled) {
background-color: $gray-300; // #CAD4DF (same as enabled)
}
&.bds-carousel-card-list__button--disabled,
&:disabled {
background-color: $gray-300; // #CAD4DF
color: $gray-400; // #8A919A
opacity: 0.5;
cursor: not-allowed;
}
}
// Green button variant - Dark Mode
// Enabled: #21E46B (green-300), Hover: #70EE97 (green-200), Active: #21E46B (green-300)
// Disabled: opacity 0.5
.bds-carousel-card-list__button--green {
background-color: $green-300; // #21E46B
color: $black; // #141414
&:hover:not(:disabled) {
background-color: $green-200; // #70EE97
}
&:active:not(:disabled) {
background-color: $green-300; // #21E46B (same as enabled)
}
&.bds-carousel-card-list__button--disabled,
&:disabled {
background-color: $green-300; // #21E46B
color: $gray-400; // #8A919A
opacity: 0.5;
cursor: not-allowed;
}
}
// Black variant becomes White in Dark Mode (inverts for contrast)
// Enabled: #FFFFFF (white), Hover: #E6EAF0 (gray-200), Active: #FFFFFF (white)
// Disabled: opacity 0.5
.bds-carousel-card-list__button--black {
background-color: $white; // #FFFFFF
color: $black; // #141414
&:hover:not(:disabled) {
background-color: $gray-200; // #E6EAF0
}
&:active:not(:disabled) {
background-color: $white; // #FFFFFF (same as enabled)
}
&.bds-carousel-card-list__button--disabled,
&:disabled {
background-color: $white; // #FFFFFF
color: $gray-400; // #8A919A
opacity: 0.5;
cursor: not-allowed;
}
}
// =============================================================================
// LIGHT MODE (html.light) - Color Variants
// =============================================================================
html.light {
// Focus styles - Light Mode
.bds-carousel-card-list__button {
&:focus {
outline-color: $gray-900;
}
&:focus-visible {
outline-color: $gray-900;
}
}
.bds-carousel-card-list__track {
&:focus-visible {
outline-color: $gray-900;
}
}
// Section text colors - Light Mode
.bds-carousel-card-list--neutral,
.bds-carousel-card-list--green {
.bds-carousel-card-list__heading,
.bds-carousel-card-list__description {
color: $black;
}
}
// ---------------------------------------------------------------------------
// Button Color Variants - Light Mode
// Button colors are independent of card colors - use buttonVariant prop
// ---------------------------------------------------------------------------
// Neutral button variant - Light Mode
// Enabled: #CAD4DF (gray-300), Hover: #E6EAF0 (gray-200), Active: #CAD4DF (gray-300)
// Disabled: opacity 0.5
.bds-carousel-card-list__button--neutral {
background-color: $gray-300; // #CAD4DF
color: $black; // #141414
&:hover:not(:disabled) {
background-color: $gray-200; // #E6EAF0
}
&:active:not(:disabled) {
background-color: $gray-300; // #CAD4DF (same as enabled)
}
&.bds-carousel-card-list__button--disabled,
&:disabled {
background-color: $gray-300; // #CAD4DF
color: $gray-400; // #8A919A
opacity: 0.5;
cursor: not-allowed;
}
}
// Green button variant - Light Mode
// Enabled: #21E46B (green-300), Hover: #70EE97 (green-200), Active: #21E46B (green-300)
// Disabled: opacity 0.5
.bds-carousel-card-list__button--green {
background-color: $green-300; // #21E46B
color: $black; // #141414
&:hover:not(:disabled) {
background-color: $green-200; // #70EE97
}
&:active:not(:disabled) {
background-color: $green-300; // #21E46B (same as enabled)
}
&.bds-carousel-card-list__button--disabled,
&:disabled {
background-color: $green-300; // #21E46B
color: $gray-400; // #8A919A
opacity: 0.5;
cursor: not-allowed;
}
}
// Black variant in Light Mode (stays black for contrast)
// Enabled: #141414 (black), Hover: #72777E (gray-500), Active: #141414 (black)
// Disabled: opacity 0.5
.bds-carousel-card-list__button--black {
background-color: $black; // #141414
color: $white; // #FFFFFF
&:hover:not(:disabled) {
background-color: $gray-500; // #72777E
}
&:active:not(:disabled) {
background-color: $black; // #141414 (same as enabled)
}
&.bds-carousel-card-list__button--disabled,
&:disabled {
background-color: $black; // #141414
color: $gray-400; // #8A919A
opacity: 0.5;
cursor: not-allowed;
}
}
}

View File

@@ -0,0 +1,252 @@
import React, { useRef, useState, useCallback, useEffect } from 'react';
import clsx from 'clsx';
import { CardOffgrid, CardOffgridProps } from '../../components/CardOffgrid';
/**
* Configuration for a single card in the CarouselCardList pattern
* Extends CardOffgridProps but removes variant (controlled by carousel)
*/
export type CarouselCardConfig = Omit<CardOffgridProps, 'variant'>;
/**
* Props for the CarouselCardList pattern component
*/
export interface CarouselCardListProps extends React.ComponentPropsWithoutRef<'section'> {
/** Color variant of the cards */
variant?: 'neutral' | 'green';
/** Color variant of the navigation buttons (independent of card color). Defaults to 'neutral' */
buttonVariant?: 'neutral' | 'green' | 'black';
/** Section heading text */
heading: React.ReactNode;
/** Section description text */
description: React.ReactNode;
/** Array of card configurations */
cards: readonly CarouselCardConfig[];
}
/**
* Generates a stable key for a card based on its properties.
*/
const getCardKey = (card: CarouselCardConfig, index: number): string | number => {
if (card.href) return card.href;
if (card.title) return `${card.title}-${index}`;
return index;
};
/**
* CarouselCardList Pattern Component
*
* A horizontal scrolling carousel that displays CardOffgrid components.
* Features navigation buttons that scroll cards in/out of view.
* The navigation button colors can be set independently of the card colors
* using the `buttonVariant` prop.
*
* @example
* ```tsx
* <CarouselCardList
* variant="neutral"
* buttonVariant="green"
* heading="Why Choose Our Platform"
* description="Discover the benefits of our solution."
* cards={[
* { icon: <Icon />, title: "Feature 1", description: "..." },
* { icon: <Icon />, title: "Feature 2", description: "..." },
* ]}
* />
* ```
*/
export const CarouselCardList = React.forwardRef<HTMLElement, CarouselCardListProps>(
(props, ref) => {
const { variant = 'neutral', buttonVariant = 'neutral', heading, description, cards, className, ...rest } = props;
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [canScrollPrev, setCanScrollPrev] = useState(false);
const [canScrollNext, setCanScrollNext] = useState(true);
// Check scroll position and update button states
const updateScrollButtons = useCallback(() => {
const container = scrollContainerRef.current;
if (!container) return;
const { scrollLeft, scrollWidth, clientWidth } = container;
setCanScrollPrev(scrollLeft > 0);
setCanScrollNext(scrollLeft + clientWidth < scrollWidth - 1);
}, []);
// Initialize and listen for scroll events
useEffect(() => {
const container = scrollContainerRef.current;
if (!container) return;
updateScrollButtons();
container.addEventListener('scroll', updateScrollButtons, { passive: true });
window.addEventListener('resize', updateScrollButtons);
return () => {
container.removeEventListener('scroll', updateScrollButtons);
window.removeEventListener('resize', updateScrollButtons);
};
}, [updateScrollButtons, cards.length]);
// Scroll by one card width
const scroll = useCallback((direction: 'prev' | 'next') => {
const container = scrollContainerRef.current;
if (!container) return;
// Get the first card to determine scroll amount
const card = container.querySelector('.bds-carousel-card-list__card') as HTMLElement;
if (!card) return;
const cardWidth = card.offsetWidth;
const gap = 8; // 8px gap between cards
const scrollAmount = cardWidth + gap;
container.scrollBy({
left: direction === 'next' ? scrollAmount : -scrollAmount,
behavior: 'smooth',
});
}, []);
// Early return for empty cards
if (cards.length === 0) {
console.warn('CarouselCardList: No cards provided');
return null;
}
return (
<section
ref={ref}
className={clsx('bds-carousel-card-list', `bds-carousel-card-list--${variant}`, className)}
{...rest}
>
{/* Header with title, description, and navigation buttons */}
<div className="bds-carousel-card-list__header">
<div className="bds-carousel-card-list__header-content">
<h2 className="bds-carousel-card-list__heading h-md">{heading}</h2>
<p className="bds-carousel-card-list__description body-l">{description}</p>
</div>
<div className="bds-carousel-card-list__nav">
<CarouselButton
direction="prev"
variant={buttonVariant}
disabled={!canScrollPrev}
onClick={() => scroll('prev')}
aria-label="Previous cards"
/>
<CarouselButton
direction="next"
variant={buttonVariant}
disabled={!canScrollNext}
onClick={() => scroll('next')}
aria-label="Next cards"
/>
</div>
</div>
{/* Cards scroll container - full bleed */}
<div className="bds-carousel-card-list__track-wrapper">
<div
ref={scrollContainerRef}
className="bds-carousel-card-list__track"
role="region"
aria-label="Card carousel"
tabIndex={0}
>
{cards.map((card, index) => (
<div key={getCardKey(card, index)} className="bds-carousel-card-list__card">
<CardOffgrid {...card} variant={variant} />
</div>
))}
</div>
</div>
</section>
);
}
);
CarouselCardList.displayName = 'CarouselCardList';
/**
* Props for the CarouselButton component
*/
interface CarouselButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Arrow direction */
direction: 'prev' | 'next';
/** Color variant (independent of card color) */
variant: 'neutral' | 'green' | 'black';
}
/**
* Internal CarouselButton component for navigation
*/
const CarouselButton: React.FC<CarouselButtonProps> = ({
direction,
variant,
disabled,
className,
...buttonProps
}) => {
return (
<button
type="button"
className={clsx(
'bds-carousel-card-list__button',
`bds-carousel-card-list__button--${direction}`,
`bds-carousel-card-list__button--${variant}`,
{ 'bds-carousel-card-list__button--disabled': disabled },
className
)}
disabled={disabled}
{...buttonProps}
>
{direction === 'prev' ? <CarouselArrowIconLeft /> : <CarouselArrowIconRight />}
</button>
);
};
/**
* SVG Arrow Icon for carousel navigation - Right arrow
*/
const CarouselArrowIconRight: React.FC = () => (
<svg
className="bds-carousel-card-list__arrow-icon"
width="18"
height="16"
viewBox="0 0 18 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M9.33387 1.33461L15.9999 8.00058L9.33387 14.6666M15.9982 7.99893L-0.000149269 7.99893"
stroke="currentColor"
strokeWidth="1.5"
strokeMiterlimit="10"
/>
</svg>
);
/**
* SVG Arrow Icon for carousel navigation - Left arrow
*/
const CarouselArrowIconLeft: React.FC = () => (
<svg
className="bds-carousel-card-list__arrow-icon"
width="18"
height="15"
viewBox="0 0 18 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M7.72667 0.530285L1.0607 7.19626L7.72667 13.8622M1.06235 7.19461L17.0607 7.19461"
stroke="currentColor"
strokeWidth="1.5"
strokeMiterlimit="10"
/>
</svg>
);
export default CarouselCardList;

View File

@@ -0,0 +1,3 @@
export { CarouselCardList, type CarouselCardListProps, type CarouselCardConfig } from './CarouselCardList';
export { default } from './CarouselCardList';

File diff suppressed because it is too large Load Diff

View File

@@ -123,9 +123,9 @@ h6,
// Navigation ------------------------------------------------------------------
a:not(.bds-link),
nav a:not(.bds-link),
a:not([role="button"]):not(.bds-link) {
a:not(.bds-link):not(.bds-card-offgrid),
nav a:not(.bds-link):not(.bds-card-offgrid),
a:not([role="button"]):not(.bds-link):not(.bds-card-offgrid) {
color: $light-fg;
&.btn-primary {

View File

@@ -103,6 +103,7 @@ $line-height-base: 1.5;
@import "../shared/patterns/CalloutMediaBanner/CalloutMediaBanner.scss";
@import "../shared/patterns/CardStats/CardStats.scss";
@import "../shared/patterns/FeatureTwoColumn/FeatureTwoColumn.scss";
@import "../shared/patterns/CarouselCardList/CarouselCardList.scss";
@import "_code-tabs.scss";
@import "_code-walkthrough.scss";
@import "_diagrams.scss";