mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2026-10-06 11:57:58 +00:00
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:
committed by
Calvin Jhunjhuwala
parent
e37dd720cd
commit
16f606a34c
608
about/carousel-card-list-showcase.page.tsx
Normal file
608
about/carousel-card-list-showcase.page.tsx
Normal 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 & 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>() => 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>
|
||||
);
|
||||
}
|
||||
|
||||
175
shared/patterns/CarouselCardList/CarouselCardList.md
Normal file
175
shared/patterns/CarouselCardList/CarouselCardList.md
Normal 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)
|
||||
|
||||
514
shared/patterns/CarouselCardList/CarouselCardList.scss
Normal file
514
shared/patterns/CarouselCardList/CarouselCardList.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
252
shared/patterns/CarouselCardList/CarouselCardList.tsx
Normal file
252
shared/patterns/CarouselCardList/CarouselCardList.tsx
Normal 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;
|
||||
|
||||
3
shared/patterns/CarouselCardList/index.ts
Normal file
3
shared/patterns/CarouselCardList/index.ts
Normal 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
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user