mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2026-02-03 21:45:19 +00:00
Compare commits
12 Commits
qa-carouse
...
section/ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33c6315510 | ||
|
|
af0b8cd40a | ||
|
|
95c4ffaa1b | ||
|
|
08c5572f16 | ||
|
|
b49bc02dd2 | ||
|
|
65a61c5e47 | ||
|
|
237ddc3c74 | ||
|
|
dd6cfd34fe | ||
|
|
7b601da3a0 | ||
|
|
6021b458e6 | ||
|
|
e5f3bf75e3 | ||
|
|
7dd32d63da |
@@ -40,8 +40,10 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="green"
|
||||
heading="The Compliant Ledger Protocol"
|
||||
subheading="A decentralized public Layer 1 blockchain for creating, transferring, and exchanging digital assets with a focus on compliance."
|
||||
primaryButton={{ label: "Get Started", onClick: () => handleClick('responsive-demo-primary') }}
|
||||
tertiaryButton={{ label: "Learn More", onClick: () => handleClick('responsive-demo-tertiary') }}
|
||||
buttons={[
|
||||
{ label: "Get Started", onClick: () => handleClick('responsive-demo-primary') },
|
||||
{ label: "Learn More", onClick: () => handleClick('responsive-demo-tertiary') }
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Responsive Behavior */}
|
||||
@@ -122,7 +124,9 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="default"
|
||||
heading="Build on XRPL"
|
||||
subheading="Start building your next decentralized application on the XRP Ledger."
|
||||
primaryButton={{ label: "Start Building", href: "#start" }}
|
||||
buttons={[
|
||||
{ label: "Start Building", href: "#start" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -143,8 +147,10 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="light-gray"
|
||||
heading="Developer Resources"
|
||||
subheading="Access comprehensive documentation, tutorials, and code samples."
|
||||
primaryButton={{ label: "View Docs", href: "#docs" }}
|
||||
tertiaryButton={{ label: "Browse Tutorials", href: "#tutorials" }}
|
||||
buttons={[
|
||||
{ label: "View Docs", href: "#docs" },
|
||||
{ label: "Browse Tutorials", href: "#tutorials" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -165,7 +171,9 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="lilac"
|
||||
heading="New Feature Release"
|
||||
subheading="Discover the latest enhancements and capabilities added to the XRP Ledger."
|
||||
primaryButton={{ label: "Learn More", href: "#features" }}
|
||||
buttons={[
|
||||
{ label: "Learn More", href: "#features" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -186,8 +194,10 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="green"
|
||||
heading="The Compliant Ledger Protocol"
|
||||
subheading="A decentralized public Layer 1 blockchain for creating, transferring, and exchanging digital assets with a focus on compliance."
|
||||
primaryButton={{ label: "Get Started", href: "#get-started" }}
|
||||
tertiaryButton={{ label: "Learn More", href: "#learn" }}
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "#get-started" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -208,8 +218,10 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="gray"
|
||||
heading="Join the Community"
|
||||
subheading="Connect with developers building on XRPL."
|
||||
primaryButton={{ label: "Join Discord", href: "#discord" }}
|
||||
tertiaryButton={{ label: "View Events", href: "#events" }}
|
||||
buttons={[
|
||||
{ label: "Join Discord", href: "#discord" },
|
||||
{ label: "View Events", href: "#events" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -255,7 +267,9 @@ export default function CalloutMediaBannerShowcase() {
|
||||
<CalloutMediaBanner
|
||||
backgroundImage={sampleBackgroundImage}
|
||||
subheading="A decentralized public Layer 1 blockchain for creating, transferring, and exchanging digital assets with a focus on compliance."
|
||||
primaryButton={{ label: "Start Building", onClick: () => handleClick('image-white-primary') }}
|
||||
buttons={[
|
||||
{ label: "Start Building", onClick: () => handleClick('image-white-primary') }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -277,8 +291,10 @@ export default function CalloutMediaBannerShowcase() {
|
||||
textColor="black"
|
||||
heading="Build the Future of Finance"
|
||||
subheading="Create powerful decentralized applications with XRPL's fast, efficient, and sustainable blockchain technology."
|
||||
primaryButton={{ label: "Start Building", onClick: () => handleClick('image-black-primary') }}
|
||||
tertiaryButton={{ label: "Explore Features", onClick: () => handleClick('image-black-tertiary') }}
|
||||
buttons={[
|
||||
{ label: "Start Building", onClick: () => handleClick('image-black-primary') },
|
||||
{ label: "Explore Features", onClick: () => handleClick('image-black-tertiary') }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -321,8 +337,10 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="default"
|
||||
heading="Complete Feature Set"
|
||||
subheading="Access all the tools you need to build on XRPL."
|
||||
primaryButton={{ label: "Get Started", href: "#start" }}
|
||||
tertiaryButton={{ label: "Learn More", href: "#learn" }}
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "#start" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -343,7 +361,9 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="light-gray"
|
||||
heading="Simple Call-to-Action"
|
||||
subheading="Focus user attention on a single primary action."
|
||||
primaryButton={{ label: "Take Action", href: "#action" }}
|
||||
buttons={[
|
||||
{ label: "Take Action", href: "#action" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -383,8 +403,10 @@ export default function CalloutMediaBannerShowcase() {
|
||||
<CalloutMediaBanner
|
||||
variant="green"
|
||||
subheading="Important information or announcement without requiring user action."
|
||||
primaryButton={{ label: "Take Action", href: "#action" }}
|
||||
tertiaryButton={{ label: "Learn More", href: "#learn" }}
|
||||
buttons={[
|
||||
{ label: "Take Action", href: "#action" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -564,20 +586,12 @@ export default function CalloutMediaBannerShowcase() {
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Subheading/description text</div>
|
||||
</div>
|
||||
|
||||
{/* primaryButton */}
|
||||
{/* buttons */}
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>primaryButton</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>{`{ label, href?, onClick? }`}</code></div>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>buttons</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>{`Array<{ label, href?, onClick?, forceColor? }>`}</code></div>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>undefined</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Primary button configuration</div>
|
||||
</div>
|
||||
|
||||
{/* tertiaryButton */}
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>tertiaryButton</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>{`{ label, href?, onClick? }`}</code></div>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>undefined</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Tertiary button configuration</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Button configurations (1-2 buttons supported)</div>
|
||||
</div>
|
||||
|
||||
{/* className */}
|
||||
|
||||
@@ -34,12 +34,12 @@ export default function FeatureTwoColumnShowcase() {
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Button Behavior</h2>
|
||||
<p className="mb-4">
|
||||
The component automatically adjusts button rendering based on the number of links provided:
|
||||
The component uses the ButtonGroup pattern which automatically adjusts button rendering based on the number of links provided:
|
||||
</p>
|
||||
<ul className="mb-6">
|
||||
<li><strong>1 link:</strong> Secondary button</li>
|
||||
<li><strong>2 links:</strong> Primary + Tertiary buttons</li>
|
||||
<li><strong>3-5 links:</strong> Primary + Tertiary (row), Secondary, then remaining Tertiary links</li>
|
||||
<li><strong>2 links:</strong> Primary + Tertiary buttons (responsive layout)</li>
|
||||
<li><strong>3+ links:</strong> All Tertiary buttons in block layout (vertical on all screen sizes)</li>
|
||||
</ul>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
495
about/featured-video-hero-showcase.page.tsx
Normal file
495
about/featured-video-hero-showcase.page.tsx
Normal file
@@ -0,0 +1,495 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
PageGrid,
|
||||
PageGridRow,
|
||||
PageGridCol,
|
||||
} from "shared/components/PageGrid/page-grid";
|
||||
import FeaturedVideoHero from "shared/patterns/FeaturedVideoHero/FeaturedVideoHero";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: "FeaturedVideoHero Pattern Showcase",
|
||||
description:
|
||||
"Interactive showcase of the FeaturedVideoHero pattern with video hero, CTAs, and responsive behavior.",
|
||||
},
|
||||
};
|
||||
|
||||
const DemoCaption = ({
|
||||
title,
|
||||
description,
|
||||
code,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
code?: string;
|
||||
}) => (
|
||||
<div className="mb-6">
|
||||
<h3 className="h4 mb-4">{title}</h3>
|
||||
{description && <p className="mb-6">{description}</p>}
|
||||
{code && (
|
||||
<div
|
||||
className="mb-6 p-4 bg-light br-4 text-black"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0, whiteSpace: "pre-wrap", color: "#000" }}>
|
||||
{code}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const placeholderVideo =
|
||||
"https://cdn.sanity.io/files/ior4a5y3/production/6e2fcba46e3f045a5570c86fd5d20d5ba93d6aad.mp4";
|
||||
|
||||
export default function FeaturedVideoHeroShowcase() {
|
||||
const videoRef = React.useRef<HTMLVideoElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (videoRef.current) {
|
||||
console.log("FeaturedVideoHero video element:", videoRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="landing">
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="text-center mb-26">
|
||||
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
|
||||
<h1 className="h2 mb-4">FeaturedVideoHero Pattern</h1>
|
||||
<p className="longform">
|
||||
A page-level hero pattern featuring a headline, optional
|
||||
subtitle, call-to-action buttons, and a featured video. The
|
||||
video uses native HTML video element props and is displayed in a
|
||||
responsive two-column layout with content on the left and video
|
||||
on the right.
|
||||
</p>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Basic Usage */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<DemoCaption
|
||||
title="Basic Usage with Video"
|
||||
description="The simplest implementation with a headline, subtitle, primary CTA, and video. This example assigns a ref to the video element and logs it to the console on mount."
|
||||
code={`const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (videoRef.current) {
|
||||
console.log("FeaturedVideoHero video element:", videoRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
<FeaturedVideoHero
|
||||
headline="Build on XRPL"
|
||||
subtitle={
|
||||
<>
|
||||
<p>Issue, manage, and trade real-world assets without needing to build smart contracts.</p>
|
||||
<p>XRP Ledger's built-in functionality and compliance-enabling features allow asset tokenization without additional layers of complexity.</p>
|
||||
</>
|
||||
}
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" }
|
||||
]}
|
||||
videoElement={{
|
||||
ref: videoRef,
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true
|
||||
}}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeaturedVideoHero
|
||||
headline="Build on XRPL"
|
||||
subtitle={
|
||||
<>
|
||||
<p>
|
||||
Issue, manage, and trade real-world assets without needing to
|
||||
build smart contracts.
|
||||
</p>
|
||||
<p>
|
||||
XRP Ledger's built-in functionality and compliance-enabling
|
||||
features allow asset tokenization without additional layers of
|
||||
complexity.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
videoElement={{
|
||||
ref: videoRef,
|
||||
src: placeholderVideo,
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
<PageGrid className="py-26">
|
||||
{/* Primary + Secondary CTA */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<DemoCaption
|
||||
title="Primary and Secondary CTAs"
|
||||
description="Include both primary and secondary call-to-action buttons."
|
||||
code={`<FeaturedVideoHero
|
||||
headline="Real-world asset tokenization"
|
||||
subtitle="Learn how to issue crypto tokens and build tokenization solutions."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" },
|
||||
{ children: "Learn More", href: "/about" }
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/tokenization.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true
|
||||
}}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeaturedVideoHero
|
||||
headline="Real-world asset tokenization"
|
||||
subtitle="Learn how to issue crypto tokens and build tokenization solutions."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" },
|
||||
{ children: "Learn More", href: "/about" },
|
||||
]}
|
||||
videoElement={{
|
||||
src: placeholderVideo,
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
<PageGrid className="py-26">
|
||||
{/* Video with extended props */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<DemoCaption
|
||||
title="Extended Video Props"
|
||||
description="Use native video element props for controls, preload, poster, etc."
|
||||
code={`<FeaturedVideoHero
|
||||
headline="Watch and Learn"
|
||||
subtitle="Explore our video tutorials and guides."
|
||||
callsToAction={[
|
||||
{ children: "Watch Tutorials", href: "/tutorials" }
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
controls: true,
|
||||
preload: "metadata"
|
||||
}}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeaturedVideoHero
|
||||
headline="Watch and Learn"
|
||||
subtitle="Explore our video tutorials and guides."
|
||||
callsToAction={[{ children: "Watch Tutorials", href: "/tutorials" }]}
|
||||
videoElement={{
|
||||
src: placeholderVideo,
|
||||
autoPlay: false,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
controls: true,
|
||||
preload: "metadata",
|
||||
}}
|
||||
/>
|
||||
<PageGrid className="py-26">
|
||||
{/* Validation Examples */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Validation Examples</h2>
|
||||
<p className="mb-6">
|
||||
The component includes development-time validation that logs
|
||||
warnings to the console when required props (headline,
|
||||
videoElement) are missing. The component will return null and
|
||||
not render when validation fails. The callsToAction prop is
|
||||
optional; when provided, at least one non-empty CTA is needed to
|
||||
show the CTA section.
|
||||
</p>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Primary CTA Only */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<DemoCaption
|
||||
title="Primary CTA Only (No Secondary)"
|
||||
description="The secondary CTA is optional. When omitted, only the primary CTA button is displayed."
|
||||
code={`<FeaturedVideoHero
|
||||
headline="Single Call to Action"
|
||||
subtitle="Focus on one primary action for better conversion."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" }
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true
|
||||
}}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeaturedVideoHero
|
||||
headline="Single Call to Action"
|
||||
subtitle="Focus on one primary action for better conversion."
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
videoElement={{
|
||||
src: placeholderVideo,
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
<PageGrid className="py-26">
|
||||
{/* Without subtitle */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<DemoCaption
|
||||
title="Without Subtitle"
|
||||
description="Subtitle is optional. The component renders without a subtitle section when omitted."
|
||||
code={`<FeaturedVideoHero
|
||||
headline="Headline Only"
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" }
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true
|
||||
}}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeaturedVideoHero
|
||||
headline="Headline Only"
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
videoElement={{
|
||||
src: placeholderVideo,
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
<PageGrid className="py-26">
|
||||
{/* Props Documentation */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Props Documentation</h2>
|
||||
|
||||
<h4 className="h5 mb-4">FeaturedVideoHeroProps</h4>
|
||||
<div className="mb-6">
|
||||
<ul>
|
||||
<li>
|
||||
<code>headline</code> (required) -{" "}
|
||||
<code>React.ReactNode</code> - Hero headline text
|
||||
</li>
|
||||
<li>
|
||||
<code>subtitle</code> (optional) -{" "}
|
||||
<code>React.ReactNode</code> - Hero subtitle text
|
||||
</li>
|
||||
<li>
|
||||
<code>callsToAction</code> (optional) -{" "}
|
||||
<code>DesignConstrainedCallsToActions</code> - Array with
|
||||
primary CTA and optional secondary CTA. Omit or pass
|
||||
empty/non-rendering CTAs to hide the CTA section.
|
||||
</li>
|
||||
<li>
|
||||
<code>videoElement</code> (required) - Native{" "}
|
||||
<code><video></code> element props (e.g. src,
|
||||
autoPlay, loop, muted, playsInline, controls, preload,
|
||||
poster)
|
||||
</li>
|
||||
<li>
|
||||
<code>className</code> (optional) - <code>string</code> -
|
||||
Additional CSS classes for the header element
|
||||
</li>
|
||||
<li>
|
||||
All standard HTML <code><header></code> attributes are
|
||||
supported
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h4 className="h5 mb-4">Button Props (callsToAction)</h4>
|
||||
<p className="mb-4">
|
||||
The <code>callsToAction</code> prop accepts design-constrained
|
||||
Button props; <code>variant</code> and <code>color</code> are
|
||||
set by the component:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Primary CTA: <code>variant="primary"</code>,{" "}
|
||||
<code>color="green"</code>
|
||||
</li>
|
||||
<li>
|
||||
Secondary CTA: <code>variant="tertiary"</code>,{" "}
|
||||
<code>color="green"</code>
|
||||
</li>
|
||||
<li>
|
||||
All other Button props are supported (e.g.,{" "}
|
||||
<code>children</code>, <code>href</code>, <code>onClick</code>
|
||||
, etc.)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Code Examples */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Code Examples</h2>
|
||||
|
||||
<h4 className="h5 mb-4">Import</h4>
|
||||
<div
|
||||
className="p-4 bg-light br-4 mb-6"
|
||||
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
||||
>
|
||||
<pre style={{ margin: 0, color: "#000" }}>
|
||||
{`import { FeaturedVideoHero } from "shared/patterns/FeaturedVideoHero";`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h4 className="h5 mb-4">Basic Example</h4>
|
||||
<div
|
||||
className="p-4 bg-light br-4 mb-6"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
overflow: "auto",
|
||||
backgroundColor: "#1e1e1e",
|
||||
color: "#d4d4d4",
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
|
||||
{`<FeaturedVideoHero
|
||||
headline="Build on XRPL"
|
||||
subtitle={
|
||||
<>
|
||||
<p>Issue, manage, and trade real-world assets without needing to build smart contracts.</p>
|
||||
<p>XRP Ledger's built-in functionality and compliance-enabling features allow asset tokenization without additional layers of complexity.</p>
|
||||
</>
|
||||
}
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" }
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true
|
||||
}}
|
||||
/>`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h4 className="h5 mb-4">With Secondary CTA</h4>
|
||||
<div
|
||||
className="p-4 bg-light br-4 mb-6"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
overflow: "auto",
|
||||
backgroundColor: "#1e1e1e",
|
||||
color: "#d4d4d4",
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
|
||||
{`<FeaturedVideoHero
|
||||
headline="Real-world asset tokenization"
|
||||
subtitle="Learn how to issue crypto tokens and build solutions."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" },
|
||||
{ children: "Learn More", href: "/about" }
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/tokenization.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true
|
||||
}}
|
||||
/>`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Best Practices */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Best Practices</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Video format:</strong> Use MP4 with H.264 for broad
|
||||
compatibility. Keep file sizes reasonable for fast loading.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Autoplay:</strong> Use <code>muted</code> and{" "}
|
||||
<code>playsInline</code> with <code>autoPlay</code> for
|
||||
reliable autoplay on mobile.
|
||||
</li>
|
||||
<li>
|
||||
<strong>CTAs:</strong> Keep CTA text concise and
|
||||
action-oriented. Primary CTA should be the main action.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Headlines:</strong> Keep headlines concise and
|
||||
impactful. Use the subtitle for additional context.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Responsive design:</strong> The layout stacks on
|
||||
smaller screens; test across breakpoints to ensure video and
|
||||
content display correctly.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -64,14 +64,10 @@ export default function LogoSquareGridShowcase() {
|
||||
variant="green"
|
||||
heading="Developer tools & APIs"
|
||||
description="Streamline development and build powerful RWA tokenization solutions with XRP Ledger's comprehensive developer toolset."
|
||||
primaryButton={{
|
||||
label: "View Documentation",
|
||||
onClick: () => handleClick('green-primary')
|
||||
}}
|
||||
tertiaryButton={{
|
||||
label: "Explore Tools",
|
||||
onClick: () => handleClick('green-tertiary')
|
||||
}}
|
||||
buttons={[
|
||||
{ label: "View Documentation", onClick: () => handleClick('green-primary') },
|
||||
{ label: "Explore Tools", onClick: () => handleClick('green-tertiary') }
|
||||
]}
|
||||
logos={sampleLogos}
|
||||
/>
|
||||
|
||||
@@ -150,8 +146,10 @@ export default function LogoSquareGridShowcase() {
|
||||
variant="gray"
|
||||
heading="Our Partners"
|
||||
description="Leading companies building innovative solutions on the XRP Ledger."
|
||||
primaryButton={{ label: "View All Partners", href: "#partners" }}
|
||||
tertiaryButton={{ label: "Become a Partner", href: "#partner-program" }}
|
||||
buttons={[
|
||||
{ label: "View All Partners", href: "#partners" },
|
||||
{ label: "Become a Partner", href: "#partner-program" }
|
||||
]}
|
||||
logos={sampleLogos}
|
||||
/>
|
||||
</div>
|
||||
@@ -173,7 +171,9 @@ export default function LogoSquareGridShowcase() {
|
||||
variant="green"
|
||||
heading="Featured Integrations"
|
||||
description="Connect with leading platforms and services built on XRPL."
|
||||
primaryButton={{ label: "See All Integrations", href: "#integrations" }}
|
||||
buttons={[
|
||||
{ label: "See All Integrations", href: "#integrations" }
|
||||
]}
|
||||
logos={sampleLogos}
|
||||
/>
|
||||
</div>
|
||||
@@ -247,7 +247,9 @@ export default function LogoSquareGridShowcase() {
|
||||
variant="green"
|
||||
heading="Developer Resources"
|
||||
description="Access comprehensive tools and libraries for building on XRPL."
|
||||
primaryButton={{ label: "Get Started", onClick: () => handleClick('single-button') }}
|
||||
buttons={[
|
||||
{ label: "Get Started", onClick: () => handleClick('single-button') }
|
||||
]}
|
||||
logos={smallLogoSet}
|
||||
/>
|
||||
</div>
|
||||
@@ -431,20 +433,12 @@ export default function LogoSquareGridShowcase() {
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Optional description text</div>
|
||||
</div>
|
||||
|
||||
{/* primaryButton */}
|
||||
{/* buttons */}
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>primaryButton</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>{`{ label, href?, onClick? }`}</code></div>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>buttons</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>{`Array<{ label, href?, onClick?, forceColor? }>`}</code></div>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>undefined</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Primary button configuration</div>
|
||||
</div>
|
||||
|
||||
{/* tertiaryButton */}
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>tertiaryButton</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>{`{ label, href?, onClick? }`}</code></div>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>undefined</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Tertiary button configuration</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Button configurations (1-2 buttons supported)</div>
|
||||
</div>
|
||||
|
||||
{/* logos */}
|
||||
|
||||
@@ -1900,4 +1900,34 @@ html.dark {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// No Padding Modifier
|
||||
// =============================================================================
|
||||
// When .bds-btn--no-padding is applied, removes all padding and left-aligns content.
|
||||
// Useful for tertiary buttons in block layouts where left alignment is needed.
|
||||
// =============================================================================
|
||||
|
||||
.bds-btn--no-padding {
|
||||
padding: 0 !important;
|
||||
justify-content: flex-start !important;
|
||||
|
||||
// Override all state paddings
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
// Responsive overrides
|
||||
@include media-breakpoint-down(xl) {
|
||||
padding: 0 !important;
|
||||
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,12 @@ export interface ButtonProps {
|
||||
href?: string;
|
||||
/** Link target - only applies when href is provided */
|
||||
target?: '_self' | '_blank';
|
||||
/**
|
||||
* Force no padding and left-align text.
|
||||
* When true, removes all padding and aligns content to the left.
|
||||
* Useful for tertiary buttons in block layouts where left alignment is needed.
|
||||
*/
|
||||
forceNoPadding?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,6 +121,7 @@ export const Button: React.FC<ButtonProps> = ({
|
||||
ariaLabel,
|
||||
href,
|
||||
target = '_self',
|
||||
forceNoPadding = false,
|
||||
}) => {
|
||||
// Hide icon when disabled (per design spec)
|
||||
const shouldShowIcon = showIcon && !disabled;
|
||||
@@ -131,6 +138,7 @@ export const Button: React.FC<ButtonProps> = ({
|
||||
'bds-btn--disabled': disabled,
|
||||
'bds-btn--no-icon': !shouldShowIcon,
|
||||
'bds-btn--force-color': forceColor,
|
||||
'bds-btn--no-padding': forceNoPadding,
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
isEnvironment,
|
||||
isEmpty,
|
||||
} from "../../utils";
|
||||
import { DesignConstrainedCallToActionsProps } from "shared/utils/types";
|
||||
|
||||
/**
|
||||
* Available background color variants for StandardCard:
|
||||
@@ -16,11 +17,13 @@ import {
|
||||
*/
|
||||
export type StandardCardVariant = "neutral" | "green" | "yellow" | "blue";
|
||||
|
||||
export interface StandardCardProps extends React.ComponentPropsWithoutRef<"article"> {
|
||||
export interface StandardCardProps
|
||||
extends
|
||||
React.ComponentPropsWithoutRef<"article">,
|
||||
DesignConstrainedCallToActionsProps {
|
||||
headline: React.ReactNode;
|
||||
/** Background color variant */
|
||||
variant: StandardCardVariant;
|
||||
callsToAction: [DesignConstrainedButtonProps, DesignConstrainedButtonProps?];
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -46,7 +49,7 @@ const StandardCard = forwardRef<HTMLElement, StandardCardProps>(
|
||||
|
||||
const [primaryButton, secondaryButton] = callsToAction;
|
||||
|
||||
const hasButtons = callsToAction.some((button) => button !== undefined);
|
||||
const hasButtons = callsToAction.some((button) => !isEmpty(button));
|
||||
|
||||
if (!headline) {
|
||||
if (isEnvironment("development")) {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// .bds-button-group - Base component
|
||||
// .bds-button-group--gap-none - No gap between buttons on tablet+ (0px)
|
||||
// .bds-button-group--gap-small - Small gap between buttons on tablet+ (8px)
|
||||
// .bds-button-group--block - Block layout for 3+ buttons (all tertiary)
|
||||
|
||||
// =============================================================================
|
||||
// Base Component Styles
|
||||
@@ -13,8 +14,8 @@
|
||||
.bds-button-group {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
@extend .align-items-start;
|
||||
@extend .flex-wrap;
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
|
||||
// Tablet breakpoint - horizontal layout
|
||||
@@ -41,3 +42,18 @@
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Block Layout Modifier (3+ buttons)
|
||||
// =============================================================================
|
||||
|
||||
.bds-button-group--block {
|
||||
// Override default flex layout - force column layout on all screen sizes
|
||||
flex-direction: column !important;
|
||||
gap: 16px !important;
|
||||
|
||||
// All buttons should be full width in block layout
|
||||
.bds-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,84 +13,212 @@ export interface ButtonConfig {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export interface ButtonGroupValidationResult {
|
||||
/** The validated and potentially trimmed list of buttons */
|
||||
buttons: ButtonConfig[];
|
||||
/** Whether the button list is valid and should render */
|
||||
isValid: boolean;
|
||||
/** Any warnings generated during validation */
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and processes a ButtonConfig array for ButtonGroup.
|
||||
*
|
||||
* Performs the following validations:
|
||||
* - Applies maxButtons limit if specified
|
||||
* - Checks for empty button arrays
|
||||
* - Validates individual button configs (label required, href or onClick recommended)
|
||||
*
|
||||
* @param buttons - Array of button configurations
|
||||
* @param maxButtons - Optional maximum number of buttons to render
|
||||
* @returns Validation result with processed buttons, validity flag, and warnings
|
||||
*
|
||||
* @example
|
||||
* const result = validateButtonGroup(buttons, 2);
|
||||
* if (!result.isValid) return null;
|
||||
* // Use result.buttons for rendering
|
||||
*/
|
||||
export function validateButtonGroup(
|
||||
buttons: ButtonConfig[],
|
||||
maxButtons?: number
|
||||
): ButtonGroupValidationResult {
|
||||
const warnings: string[] = [];
|
||||
let buttonList = [...buttons];
|
||||
|
||||
// Validate individual button configs
|
||||
buttonList.forEach((button, index) => {
|
||||
if (!button.label || button.label.trim() === '') {
|
||||
warnings.push(
|
||||
`[ButtonGroup] Button at index ${index} is missing a label. This button may not render correctly.`
|
||||
);
|
||||
}
|
||||
if (!button.href && !button.onClick) {
|
||||
warnings.push(
|
||||
`[ButtonGroup] Button "${button.label || `at index ${index}`}" has no href or onClick. Consider adding an action.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Apply maxButtons limit if specified
|
||||
if (maxButtons !== undefined && maxButtons > 0 && buttons.length > maxButtons) {
|
||||
warnings.push(
|
||||
`[ButtonGroup] ${buttons.length} buttons were passed but maxButtons is set to ${maxButtons}. ` +
|
||||
`Only the first ${maxButtons} button(s) will be rendered.`
|
||||
);
|
||||
buttonList = buttonList.slice(0, maxButtons);
|
||||
}
|
||||
|
||||
// Check for empty array
|
||||
if (buttonList.length === 0) {
|
||||
warnings.push(
|
||||
`[ButtonGroup] No buttons to render. ` +
|
||||
`Either an empty buttons array was passed or all buttons were removed by maxButtons limit.`
|
||||
);
|
||||
return { buttons: [], isValid: false, warnings };
|
||||
}
|
||||
|
||||
return { buttons: buttonList, isValid: true, warnings };
|
||||
}
|
||||
|
||||
export interface ButtonGroupProps {
|
||||
/** Primary button configuration */
|
||||
primaryButton?: ButtonConfig;
|
||||
/** Tertiary button configuration */
|
||||
tertiaryButton?: ButtonConfig;
|
||||
/** Array of button configurations
|
||||
* - 1 button: renders with singleButtonVariant (default: primary)
|
||||
* - 2 buttons: first as primary, second as tertiary
|
||||
* - 3+ buttons: all tertiary in block layout
|
||||
*/
|
||||
buttons: ButtonConfig[];
|
||||
/** Button color theme */
|
||||
color?: 'green' | 'black';
|
||||
/** Whether to force the color to remain constant regardless of theme mode */
|
||||
forceColor?: boolean;
|
||||
forceColor?: boolean;
|
||||
/** Gap between buttons on tablet+ (0px or 4px) */
|
||||
gap?: 'none' | 'small';
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
/** Override variant for single button (default: 'primary', can be 'secondary') */
|
||||
singleButtonVariant?: 'primary' | 'secondary';
|
||||
/** Maximum number of buttons to render. If more buttons are passed, only the first N will be rendered. */
|
||||
maxButtons?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ButtonGroup Component
|
||||
*
|
||||
* A responsive button group container that displays primary and/or tertiary buttons.
|
||||
* Stacks vertically on mobile and horizontally on tablet+.
|
||||
*
|
||||
*
|
||||
* A responsive button group container that displays buttons with adaptive layout:
|
||||
* - 1 button: Renders with singleButtonVariant (default: primary, can be secondary)
|
||||
* - 2 buttons: First as primary, second as tertiary (responsive layout)
|
||||
* - 3+ buttons: All tertiary in block layout
|
||||
*
|
||||
* @example
|
||||
* // Basic usage with both buttons
|
||||
* // Single button
|
||||
* <ButtonGroup
|
||||
* primaryButton={{ label: "Get Started", href: "/start" }}
|
||||
* tertiaryButton={{ label: "Learn More", href: "/learn" }}
|
||||
* buttons={[{ label: "Get Started", href: "/start" }]}
|
||||
* color="green"
|
||||
* />
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // With custom gap
|
||||
* // Two buttons (primary + tertiary)
|
||||
* <ButtonGroup
|
||||
* primaryButton={{ label: "Action", onClick: handleClick }}
|
||||
* color="black"
|
||||
* gap="small"
|
||||
* buttons={[
|
||||
* { label: "Get Started", href: "/start" },
|
||||
* { label: "Learn More", href: "/learn" }
|
||||
* ]}
|
||||
* color="green"
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Three or more buttons (all tertiary, block layout)
|
||||
* <ButtonGroup
|
||||
* buttons={[
|
||||
* { label: "Option 1", href: "/option1" },
|
||||
* { label: "Option 2", href: "/option2" },
|
||||
* { label: "Option 3", href: "/option3" }
|
||||
* ]}
|
||||
* color="green"
|
||||
* />
|
||||
*/
|
||||
export const ButtonGroup: React.FC<ButtonGroupProps> = ({
|
||||
primaryButton,
|
||||
tertiaryButton,
|
||||
buttons,
|
||||
color = 'green',
|
||||
forceColor = false,
|
||||
gap = 'small',
|
||||
className = '',
|
||||
singleButtonVariant = 'primary',
|
||||
maxButtons,
|
||||
}) => {
|
||||
// Don't render if no buttons are provided
|
||||
if (!primaryButton && !tertiaryButton) {
|
||||
// Validate and process buttons
|
||||
const validation = validateButtonGroup(buttons, maxButtons);
|
||||
|
||||
// Log warnings in development mode
|
||||
if (process.env.NODE_ENV === 'development' && validation.warnings.length > 0) {
|
||||
validation.warnings.forEach(warning => console.warn(warning));
|
||||
}
|
||||
|
||||
// Don't render if validation failed
|
||||
if (!validation.isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const buttonList = validation.buttons;
|
||||
|
||||
const isMultiButton = buttonList.length >= 3;
|
||||
|
||||
const classNames = clsx(
|
||||
'bds-button-group',
|
||||
`bds-button-group--gap-${gap}`,
|
||||
{
|
||||
'bds-button-group--block': isMultiButton,
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
// Render 3+ buttons: all tertiary in block layout
|
||||
if (isMultiButton) {
|
||||
return (
|
||||
<div className={classNames}>
|
||||
{buttonList.map((button, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="tertiary"
|
||||
color={color}
|
||||
forceColor={forceColor}
|
||||
href={button.href}
|
||||
onClick={button.onClick}
|
||||
forceNoPadding
|
||||
>
|
||||
{button.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render 1-2 buttons
|
||||
// Single button: use singleButtonVariant (default: primary, can be secondary)
|
||||
// Two buttons: first as primary, second as tertiary
|
||||
const firstButtonVariant = buttonList.length === 1 ? singleButtonVariant : 'primary';
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
{primaryButton && (
|
||||
{buttonList[0] && (
|
||||
<Button
|
||||
variant="primary"
|
||||
variant={firstButtonVariant}
|
||||
color={color}
|
||||
forceColor={forceColor}
|
||||
href={primaryButton.href}
|
||||
onClick={primaryButton.onClick}
|
||||
{...buttonList[0]}
|
||||
>
|
||||
{primaryButton.label}
|
||||
{buttonList[0].label}
|
||||
</Button>
|
||||
)}
|
||||
{tertiaryButton && (
|
||||
{buttonList[1] && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
color={color}
|
||||
forceColor={forceColor}
|
||||
href={tertiaryButton.href}
|
||||
onClick={tertiaryButton.onClick}
|
||||
{...buttonList[1]}
|
||||
>
|
||||
{tertiaryButton.label}
|
||||
{buttonList[1].label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,76 @@
|
||||
# ButtonGroup Component
|
||||
|
||||
A responsive button group container that displays primary and/or tertiary buttons. Stacks vertically on mobile and horizontally on tablet+.
|
||||
A responsive button group container that automatically assigns button variants based on the number of buttons passed. Stacks vertically on mobile and horizontally on tablet+.
|
||||
|
||||
## Features
|
||||
|
||||
- **Responsive Layout**: Vertical stack on mobile, horizontal row on tablet+
|
||||
- **Flexible Configuration**: Support for primary, tertiary, or both buttons
|
||||
- **Auto-Variant Assignment**: Automatically assigns Primary/Tertiary/Secondary variants based on button count
|
||||
- **Responsive Layout**: Vertical stack on mobile, horizontal row on tablet+ (for 1-2 buttons)
|
||||
- **Block Layout**: 3+ buttons render as all tertiary in a vertical block layout
|
||||
- **Customizable Spacing**: Control gap between buttons on tablet+ (none or small)
|
||||
- **Theme Support**: Green or black color themes
|
||||
- **Max Buttons Limit**: Optionally limit the number of buttons rendered
|
||||
|
||||
## Button Behavior
|
||||
|
||||
The component automatically determines button variants based on count:
|
||||
|
||||
| Count | Behavior |
|
||||
|-------|----------|
|
||||
| 1 button | Renders as Primary (or Secondary with `singleButtonVariant="secondary"`) |
|
||||
| 2 buttons | First as Primary, second as Tertiary (responsive layout) |
|
||||
| 3+ buttons | All as Tertiary in block layout (vertical on all screen sizes) |
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { ButtonGroup } from 'shared/patterns/ButtonGroup';
|
||||
|
||||
// Basic usage with both buttons
|
||||
// Single button (Primary by default)
|
||||
<ButtonGroup
|
||||
primaryButton={{ label: "Get Started", href: "/start" }}
|
||||
tertiaryButton={{ label: "Learn More", href: "/learn" }}
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "/start" }
|
||||
]}
|
||||
color="green"
|
||||
/>
|
||||
|
||||
// With no gap on tablet+
|
||||
// Single button as Secondary
|
||||
<ButtonGroup
|
||||
primaryButton={{ label: "Action", onClick: handleClick }}
|
||||
color="black"
|
||||
gap="none"
|
||||
buttons={[
|
||||
{ label: "Learn More", href: "/learn" }
|
||||
]}
|
||||
singleButtonVariant="secondary"
|
||||
color="green"
|
||||
/>
|
||||
|
||||
// With small gap on tablet+ (4px - default)
|
||||
// Two buttons (auto: Primary + Tertiary)
|
||||
<ButtonGroup
|
||||
primaryButton={{ label: "Primary Action", href: "/action" }}
|
||||
tertiaryButton={{ label: "Secondary", href: "/secondary" }}
|
||||
gap="small"
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "/start" },
|
||||
{ label: "Learn More", href: "/learn" }
|
||||
]}
|
||||
color="green"
|
||||
/>
|
||||
|
||||
// Three or more buttons (auto: all Tertiary, block layout)
|
||||
<ButtonGroup
|
||||
buttons={[
|
||||
{ label: "Documentation", href: "/docs" },
|
||||
{ label: "API Reference", href: "/api" },
|
||||
{ label: "Tutorials", href: "/tutorials" }
|
||||
]}
|
||||
color="black"
|
||||
/>
|
||||
|
||||
// Limit to 2 buttons even if more are passed
|
||||
<ButtonGroup
|
||||
buttons={[
|
||||
{ label: "First", href: "/first" },
|
||||
{ label: "Second", href: "/second" },
|
||||
{ label: "Third (not rendered)", href: "/third" }
|
||||
]}
|
||||
maxButtons={2}
|
||||
color="green"
|
||||
/>
|
||||
```
|
||||
|
||||
@@ -40,10 +78,12 @@ import { ButtonGroup } from 'shared/patterns/ButtonGroup';
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `primaryButton` | `ButtonConfig` | - | Primary button configuration |
|
||||
| `tertiaryButton` | `ButtonConfig` | - | Tertiary button configuration |
|
||||
| `buttons` | `ButtonConfig[]` | *required* | Array of button configurations |
|
||||
| `color` | `'green' \| 'black'` | `'green'` | Button color theme |
|
||||
| `forceColor` | `boolean` | `false` | Force color to remain constant across light/dark modes |
|
||||
| `gap` | `'none' \| 'small'` | `'small'` | Gap between buttons on tablet+ (0px or 4px) |
|
||||
| `singleButtonVariant` | `'primary' \| 'secondary'` | `'primary'` | Variant for single button |
|
||||
| `maxButtons` | `number` | - | Maximum number of buttons to render |
|
||||
| `className` | `string` | `''` | Additional CSS classes |
|
||||
|
||||
### ButtonConfig
|
||||
@@ -53,6 +93,7 @@ interface ButtonConfig {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
forceColor?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
|
||||
import { ButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
|
||||
export interface CalloutMediaBannerProps {
|
||||
/** Color variant - determines background color (ignored if backgroundImage is provided) */
|
||||
@@ -14,18 +14,8 @@ export interface CalloutMediaBannerProps {
|
||||
heading?: string;
|
||||
/** Subheading/description text */
|
||||
subheading: string;
|
||||
/** Primary button configuration */
|
||||
primaryButton?: {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
/** Tertiary button configuration */
|
||||
tertiaryButton?: {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
/** Button configurations (1-2 buttons supported) */
|
||||
buttons?: ButtonConfig[];
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
@@ -42,19 +32,21 @@ export interface CalloutMediaBannerProps {
|
||||
* variant="green"
|
||||
* heading="The Compliant Ledger Protocol"
|
||||
* subheading="A decentralized public Layer 1 blockchain..."
|
||||
* primaryButton={{ label: "Get Started", href: "/docs" }}
|
||||
* tertiaryButton={{ label: "Learn More", href: "/about" }}
|
||||
* buttons={[
|
||||
* { label: "Get Started", href: "/docs" },
|
||||
* { label: "Learn More", href: "/about" }
|
||||
* ]}
|
||||
* />
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // With background image (white text - default)
|
||||
* <CalloutMediaBanner
|
||||
* backgroundImage="/images/hero-bg.jpg"
|
||||
* heading="Build on XRPL"
|
||||
* subheading="Start building your next project"
|
||||
* primaryButton={{ label: "Start Building", onClick: handleClick }}
|
||||
* buttons={[{ label: "Start Building", onClick: handleClick }]}
|
||||
* />
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // With background image and black text (fixed across light/dark modes)
|
||||
* <CalloutMediaBanner
|
||||
@@ -62,7 +54,7 @@ export interface CalloutMediaBannerProps {
|
||||
* textColor="black"
|
||||
* heading="Build on XRPL"
|
||||
* subheading="Start building your next project"
|
||||
* primaryButton={{ label: "Start Building", onClick: handleClick }}
|
||||
* buttons={[{ label: "Start Building", onClick: handleClick }]}
|
||||
* />
|
||||
*/
|
||||
export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
|
||||
@@ -71,13 +63,20 @@ export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
|
||||
textColor = 'white',
|
||||
heading,
|
||||
subheading,
|
||||
primaryButton,
|
||||
tertiaryButton,
|
||||
buttons,
|
||||
className = '',
|
||||
}) => {
|
||||
// Check if there are any buttons
|
||||
const hasButtons = !!(primaryButton || tertiaryButton);
|
||||
|
||||
// Validate buttons if provided (max 2 buttons supported)
|
||||
const buttonValidation = buttons ? validateButtonGroup(buttons, 2) : null;
|
||||
|
||||
// Log warnings in development mode
|
||||
if (process.env.NODE_ENV === 'development' && buttonValidation?.warnings.length) {
|
||||
buttonValidation.warnings.forEach(warning => console.warn(warning));
|
||||
}
|
||||
|
||||
// Check if there are any valid buttons
|
||||
const hasButtons = buttonValidation?.isValid && buttonValidation.buttons.length > 0;
|
||||
|
||||
// Check if we should center content: no buttons OR (no heading but has buttons)
|
||||
const shouldCenter = !hasButtons || (!heading && hasButtons);
|
||||
|
||||
@@ -116,12 +115,13 @@ export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<ButtonGroup
|
||||
primaryButton={primaryButton}
|
||||
tertiaryButton={tertiaryButton}
|
||||
color={buttonColor}
|
||||
gap="none"
|
||||
/>
|
||||
{hasButtons && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color={buttonColor}
|
||||
gap="none"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
@@ -29,7 +29,8 @@ $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-base: 8px; // Base
|
||||
$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)
|
||||
@@ -108,16 +109,31 @@ $bds-carousel-transition: 200ms cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
.bds-carousel-card-list__header-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-carousel-header-gap-base;
|
||||
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
|
||||
// =============================================================================
|
||||
@@ -127,16 +143,9 @@ $bds-carousel-transition: 200ms cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
gap: $bds-carousel-button-gap;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
margin-top: 24px;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
// Constrain heading/description to grid (8 columns at desktop)
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-top: 40px;
|
||||
}
|
||||
// Add padding to allow focus ring to be visible without clipping
|
||||
padding: 4px;
|
||||
margin: -4px;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -147,16 +156,16 @@ $bds-carousel-transition: 200ms cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
padding-left: $bds-carousel-grid-padding-lg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,19 +269,6 @@ $bds-carousel-transition: 200ms cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bds-carousel-card-list__grid {
|
||||
gap: $bds-carousel-section-gap-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-carousel-section-gap-md;
|
||||
}
|
||||
|
||||
// Row layout only at desktop (lg and up)
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-carousel-section-gap-lg;
|
||||
}
|
||||
}
|
||||
// =============================================================================
|
||||
// LIGHT MODE (html.light) - Color Variants
|
||||
// =============================================================================
|
||||
|
||||
@@ -3,7 +3,6 @@ import clsx from 'clsx';
|
||||
import { CardOffgrid, CardOffgridProps } from '../../components/CardOffgrid';
|
||||
import { CarouselButton } from '../../components/CarouselButton';
|
||||
import type { ButtonProps } from '../../components/Button';
|
||||
import { PageGrid } from 'shared/components/PageGrid/page-grid';
|
||||
|
||||
/**
|
||||
* Configuration for a single card in the CarouselCardList pattern
|
||||
@@ -122,55 +121,45 @@ export const CarouselCardList = React.forwardRef<HTMLElement, CarouselCardListPr
|
||||
return (
|
||||
<section
|
||||
ref={ref}
|
||||
className={clsx(`bds-carousel-card-list--${variant}`, className)}
|
||||
className={clsx('bds-carousel-card-list', `bds-carousel-card-list--${variant}`, className)}
|
||||
{...rest}
|
||||
>
|
||||
<PageGrid className="bds-carousel-card-list__grid">
|
||||
{/* Header with title, description, and navigation buttons */}
|
||||
<PageGrid.Row>
|
||||
<PageGrid.Col span={{ base: "fill", md: 6, lg: 8 }}>
|
||||
<div className="bds-carousel-card-list__header-content">
|
||||
<h2 className="mb-0 h-md">{heading}</h2>
|
||||
<p className="mb-0 body-l">{description}</p>
|
||||
</div>
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
<PageGrid.Row>
|
||||
<PageGrid.Col 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"
|
||||
/>
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
|
||||
{/* 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={CARD_CLASS_NAME}>
|
||||
<CardOffgrid {...card} variant={variant} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* 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>
|
||||
</PageGrid>
|
||||
<div className="bds-carousel-card-list__nav">
|
||||
{(['prev', 'next'] as const).map((direction) => (
|
||||
<CarouselButton
|
||||
key={direction}
|
||||
direction={direction}
|
||||
variant={buttonVariant}
|
||||
disabled={direction === 'prev' ? !canScrollPrev : !canScrollNext}
|
||||
onClick={() => scroll(direction)}
|
||||
aria-label={direction === 'prev' ? 'Previous cards' : '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={CARD_CLASS_NAME}>
|
||||
<CardOffgrid {...card} variant={variant} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '../../components/Button';
|
||||
import { CarouselButton } from '../../components/CarouselButton';
|
||||
import { Divider } from '../../components/Divider';
|
||||
import { PageGrid, PageGridRow, PageGridCol } from '../../components/PageGrid';
|
||||
import { ButtonConfig } from '../ButtonGroup';
|
||||
import { ButtonGroup, ButtonConfig } from '../ButtonGroup';
|
||||
|
||||
/**
|
||||
* Props for a single slide in the CarouselFeatured component
|
||||
@@ -182,20 +181,16 @@ export const CarouselFeatured = React.forwardRef<HTMLElement, CarouselFeaturedPr
|
||||
<div className="bds-carousel-featured__header">
|
||||
<h2 className="bds-carousel-featured__heading h-md">{heading}</h2>
|
||||
<div className="bds-carousel-featured__nav bds-carousel-featured__nav--desktop">
|
||||
<CarouselButton
|
||||
direction="prev"
|
||||
variant={buttonVariant}
|
||||
disabled={!canGoPrev}
|
||||
onClick={goToPrev}
|
||||
aria-label="Previous slide"
|
||||
/>
|
||||
<CarouselButton
|
||||
direction="next"
|
||||
variant={buttonVariant}
|
||||
disabled={!canGoNext}
|
||||
onClick={goToNext}
|
||||
aria-label="Next slide"
|
||||
/>
|
||||
{(['prev', 'next'] as const).map((direction) => (
|
||||
<CarouselButton
|
||||
key={direction}
|
||||
direction={direction}
|
||||
variant={buttonVariant}
|
||||
disabled={direction === 'prev' ? !canGoPrev : !canGoNext}
|
||||
onClick={direction === 'prev' ? goToPrev : goToNext}
|
||||
aria-label={direction === 'prev' ? 'Previous slide' : 'Next slide'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -221,52 +216,27 @@ export const CarouselFeatured = React.forwardRef<HTMLElement, CarouselFeaturedPr
|
||||
primaryButton && tertiaryButton && 'bds-carousel-featured__cta--two-buttons'
|
||||
)}>
|
||||
{/* Buttons wrapper - groups primary and tertiary together */}
|
||||
<div className="bds-carousel-featured__buttons">
|
||||
{/* Primary button */}
|
||||
{primaryButton && (
|
||||
<Button
|
||||
variant="primary"
|
||||
color="black"
|
||||
forceColor={background !== 'neutral'}
|
||||
href={primaryButton.href}
|
||||
onClick={primaryButton.onClick}
|
||||
className="bds-carousel-featured__primary-btn"
|
||||
>
|
||||
{primaryButton.label}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Tertiary button */}
|
||||
{tertiaryButton && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
color="black"
|
||||
forceColor={background !== 'neutral'}
|
||||
href={tertiaryButton.href}
|
||||
onClick={tertiaryButton.onClick}
|
||||
className="bds-carousel-featured__tertiary-btn"
|
||||
>
|
||||
{tertiaryButton.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{(primaryButton || tertiaryButton) && (
|
||||
<ButtonGroup
|
||||
buttons={[primaryButton, tertiaryButton].filter((btn): btn is ButtonConfig => !!btn)}
|
||||
color="black"
|
||||
forceColor={background !== 'neutral'}
|
||||
className="bds-carousel-featured__buttons"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mobile/Tablet nav buttons */}
|
||||
<div className="bds-carousel-featured__nav bds-carousel-featured__nav--mobile">
|
||||
<CarouselButton
|
||||
direction="prev"
|
||||
variant={buttonVariant}
|
||||
disabled={!canGoPrev}
|
||||
onClick={goToPrev}
|
||||
aria-label="Previous slide"
|
||||
/>
|
||||
<CarouselButton
|
||||
direction="next"
|
||||
variant={buttonVariant}
|
||||
disabled={!canGoNext}
|
||||
onClick={goToNext}
|
||||
aria-label="Next slide"
|
||||
/>
|
||||
{(['prev', 'next'] as const).map((direction) => (
|
||||
<CarouselButton
|
||||
key={direction}
|
||||
direction={direction}
|
||||
variant={buttonVariant}
|
||||
disabled={direction === 'prev' ? !canGoPrev : !canGoNext}
|
||||
onClick={direction === 'prev' ? goToPrev : goToNext}
|
||||
aria-label={direction === 'prev' ? 'Previous slide' : 'Next slide'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div> {/* Close bottom */}
|
||||
|
||||
@@ -40,7 +40,6 @@ $bds-feature-desktop-py: 96px;
|
||||
$bds-feature-desktop-text-gap: 16px;
|
||||
$bds-feature-desktop-cta-gap-col: 0; // Gap between button rows
|
||||
$bds-feature-desktop-content-gap: 0; // Gap between text-group and cta (space-between handles this)
|
||||
$bds-feature-desktop-button-gap: 16px; // Gap between buttons in ButtonGroup (consistent across all sizes)
|
||||
|
||||
// Spacing - Tablet (576px - 991px) - based on Figma 768px design
|
||||
$bds-feature-tablet-py: 80px;
|
||||
@@ -50,7 +49,6 @@ $bds-feature-tablet-text-gap: 8px;
|
||||
$bds-feature-tablet-cta-gap-row: 16px; // Gap between buttons in row from Figma
|
||||
$bds-feature-tablet-cta-gap-col: 0;
|
||||
$bds-feature-tablet-content-gap: 32px;
|
||||
$bds-feature-tablet-button-gap: 16px; // Gap between buttons in ButtonGroup (consistent across all sizes)
|
||||
|
||||
// Spacing - Mobile (<576px) - based on Figma 375px design
|
||||
$bds-feature-mobile-py: 64px;
|
||||
@@ -58,7 +56,6 @@ $bds-feature-mobile-px: 16px;
|
||||
$bds-feature-mobile-text-gap: 8px;
|
||||
$bds-feature-mobile-cta-gap: 16px; // Gap between stacked buttons from Figma
|
||||
$bds-feature-mobile-content-gap: 24px;
|
||||
$bds-feature-mobile-button-gap: 16px; // Gap between buttons in ButtonGroup (consistent across all sizes)
|
||||
|
||||
// Grid gutter - consistent with PageGrid
|
||||
$bds-grid-gutter: 8px;
|
||||
@@ -66,18 +63,6 @@ $bds-grid-gutter: 8px;
|
||||
// =============================================================================
|
||||
// Base Styles
|
||||
// =============================================================================
|
||||
.bds-feature-two-column__button-group {
|
||||
.bds-btn--tertiary {
|
||||
padding-top: 0px !important;
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-feature-two-column__cta-row{
|
||||
.bds-btn--tertiary {
|
||||
padding-top: 16px !important;
|
||||
}
|
||||
}
|
||||
.bds-feature-two-column {
|
||||
width: 100%;
|
||||
|
||||
@@ -230,11 +215,14 @@ $bds-grid-gutter: 8px;
|
||||
&__content--multiple {
|
||||
// Mobile: no gap since we only have 2 items (text-group and button-group)
|
||||
// The 24px gap is handled via button-group margin or flex gap
|
||||
gap: 0;
|
||||
gap: 24px;
|
||||
justify-content: flex-start;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: 0; // Desktop uses space-between for auto distribution
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@@ -244,39 +232,6 @@ $bds-grid-gutter: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// Button group - contains all buttons in the multiple links layout
|
||||
// 16px gap between buttons on ALL screen sizes
|
||||
&__button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $bds-feature-mobile-button-gap; // 16px on mobile
|
||||
margin-top: $bds-feature-mobile-content-gap; // 24px spacing from text-group on mobile
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-feature-tablet-button-gap; // 16px on tablet
|
||||
margin-top: $bds-feature-tablet-content-gap; // 32px spacing from text-group on tablet
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-feature-desktop-button-gap; // 16px on desktop
|
||||
margin-top: 0; // Desktop uses space-between, no explicit margin needed
|
||||
}
|
||||
|
||||
// Tertiary links need left padding removed to align text with title
|
||||
>.bds-btn--tertiary {
|
||||
padding-left: 0 !important;
|
||||
margin-left: 0;
|
||||
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Text group - title + description
|
||||
&__text-group {
|
||||
display: flex;
|
||||
@@ -308,69 +263,7 @@ $bds-grid-gutter: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// CTA container - base styles
|
||||
&__cta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
// CTA variant: Single secondary button - maintains vertical layout
|
||||
&__cta--single {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// CTA variant: Primary + tertiary - horizontal on tablet+
|
||||
&__cta--double {
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
// CTA row - for first two buttons in multiple links layout (Primary + Tertiary)
|
||||
// Horizontal layout on tablet+ (md and lg share same styles)
|
||||
&__cta-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
@include media-breakpoint-up(lg) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Tertiary group - groups remaining tertiary links together as a single unit
|
||||
// No gap between tertiary links - they stack tightly as a group
|
||||
&__tertiary-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0; // No spacing between tertiary links in the group
|
||||
|
||||
// Tertiary links need left padding removed to align text with title
|
||||
.bds-btn--tertiary {
|
||||
padding-left: 0 !important;
|
||||
margin-left: 0;
|
||||
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Media container
|
||||
&__media {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '../../components/Button/Button';
|
||||
import { PageGrid } from '../../components/PageGrid/page-grid';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
|
||||
export interface FeatureTwoColumnLink {
|
||||
/** Link label text */
|
||||
@@ -59,6 +59,26 @@ export const FeatureTwoColumn: React.FC<FeatureTwoColumnProps> = ({
|
||||
media,
|
||||
className,
|
||||
}) => {
|
||||
// Determine button color based on background
|
||||
// Rule: Black buttons must be used for all backgrounds (including neutral)
|
||||
const buttonColor = 'black';
|
||||
const forceColor = true;
|
||||
|
||||
// Convert links to ButtonConfig format
|
||||
const buttonConfigs: ButtonConfig[] = links.map(link => ({
|
||||
label: link.label,
|
||||
href: link.href,
|
||||
forceColor: forceColor,
|
||||
}));
|
||||
|
||||
// Validate buttons (FeatureTwoColumn supports 1-5 links per design spec)
|
||||
const buttonValidation = validateButtonGroup(buttonConfigs, 5);
|
||||
|
||||
// Log warnings in development mode
|
||||
if (process.env.NODE_ENV === 'development' && buttonValidation.warnings.length > 0) {
|
||||
buttonValidation.warnings.forEach(warning => console.warn(warning));
|
||||
}
|
||||
|
||||
// Build root class names
|
||||
const rootClasses = clsx(
|
||||
'bds-feature-two-column',
|
||||
@@ -67,94 +87,30 @@ export const FeatureTwoColumn: React.FC<FeatureTwoColumnProps> = ({
|
||||
className
|
||||
);
|
||||
|
||||
// Determine button color based on background
|
||||
// Rule: Black buttons must be used for all backgrounds (including neutral)
|
||||
const buttonColor = 'black';
|
||||
const forceColor = true;
|
||||
|
||||
// Render content section with appropriate CTA layout based on link count
|
||||
// For 3-5 links, items are direct children for space-between distribution
|
||||
// Render content section with ButtonGroup
|
||||
const renderContent = () => {
|
||||
const linkCount = links.length;
|
||||
// Determine content class based on validated button count
|
||||
const contentClass = clsx(
|
||||
'bds-feature-two-column__content',
|
||||
{
|
||||
'bds-feature-two-column__content--multiple': buttonValidation.buttons.length >= 3,
|
||||
}
|
||||
);
|
||||
|
||||
// 1 link: Secondary button
|
||||
if (linkCount === 1) {
|
||||
return (
|
||||
<div className="bds-feature-two-column__content">
|
||||
<div className="bds-feature-two-column__text-group">
|
||||
<h2 className="bds-feature-two-column__title">{title}</h2>
|
||||
<p className="bds-feature-two-column__description">{description}</p>
|
||||
</div>
|
||||
<div className="bds-feature-two-column__cta bds-feature-two-column__cta--single">
|
||||
<Button variant="secondary" color={buttonColor} forceColor={forceColor} href={links[0].href}>
|
||||
{links[0].label}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 2 links: Primary + Tertiary in a row
|
||||
if (linkCount === 2) {
|
||||
return (
|
||||
<div className="bds-feature-two-column__content">
|
||||
<div className="bds-feature-two-column__text-group">
|
||||
<h2 className="bds-feature-two-column__title">{title}</h2>
|
||||
<p className="bds-feature-two-column__description">{description}</p>
|
||||
</div>
|
||||
<div className="bds-feature-two-column__cta bds-feature-two-column__cta--double">
|
||||
<Button variant="primary" color={buttonColor} forceColor={forceColor} href={links[0].href}>
|
||||
{links[0].label}
|
||||
</Button>
|
||||
<Button variant="tertiary" color={buttonColor} forceColor={forceColor} href={links[1].href}>
|
||||
{links[1].label}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 3-5 links: Text group + Button group (contains all buttons with consistent 16px spacing)
|
||||
// Desktop: space-between distribution between text-group and button-group
|
||||
// Tablet: 32px gap, Mobile: 24px gap
|
||||
return (
|
||||
<div className="bds-feature-two-column__content bds-feature-two-column__content--multiple">
|
||||
<div className={contentClass}>
|
||||
<div className="bds-feature-two-column__text-group">
|
||||
<h2 className="bds-feature-two-column__title">{title}</h2>
|
||||
<p className="bds-feature-two-column__description">{description}</p>
|
||||
</div>
|
||||
{/* Button group - all buttons grouped with 16px spacing between them */}
|
||||
<div className="bds-feature-two-column__button-group">
|
||||
{/* First two links in a row: Primary + Tertiary */}
|
||||
<div className="bds-feature-two-column__cta-row">
|
||||
<Button variant="primary" color={buttonColor} forceColor={forceColor} href={links[0].href}>
|
||||
{links[0].label}
|
||||
</Button>
|
||||
{links[1] && (
|
||||
<Button variant="tertiary" color={buttonColor} forceColor={forceColor} href={links[1].href}>
|
||||
{links[1].label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{/* Secondary button */}
|
||||
{links[2] && (
|
||||
<Button variant="secondary" color={buttonColor} forceColor={forceColor} href={links[2].href}>
|
||||
{links[2].label}
|
||||
</Button>
|
||||
)}
|
||||
{/* Remaining tertiary links */}
|
||||
{links.slice(3).map((link) => (
|
||||
<Button
|
||||
key={`${link.href}-${link.label}`}
|
||||
variant="tertiary"
|
||||
color={buttonColor}
|
||||
forceColor={forceColor}
|
||||
href={link.href}
|
||||
>
|
||||
{link.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{buttonValidation.isValid && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color={buttonColor}
|
||||
forceColor={forceColor}
|
||||
singleButtonVariant="secondary"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
124
shared/patterns/FeaturedVideoHero/FeaturedVideoHero.tsx
Normal file
124
shared/patterns/FeaturedVideoHero/FeaturedVideoHero.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, { forwardRef, useCallback } from "react";
|
||||
import clsx from "clsx";
|
||||
import { PageGrid } from "shared/components/PageGrid/page-grid";
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from "shared/patterns/ButtonGroup/ButtonGroup";
|
||||
import { isEmpty, isEnvironment } from "shared/utils";
|
||||
import {
|
||||
DesignConstrainedCallToActionsProps,
|
||||
DesignConstrainedVideoProps,
|
||||
} from "shared/utils/types";
|
||||
|
||||
export interface FeaturedVideoHeroProps
|
||||
extends
|
||||
React.ComponentPropsWithoutRef<"header">,
|
||||
DesignConstrainedCallToActionsProps {
|
||||
headline: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
videoElement: DesignConstrainedVideoProps;
|
||||
}
|
||||
const FeaturedVideoHero = forwardRef<HTMLElement, FeaturedVideoHeroProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
headline,
|
||||
subtitle,
|
||||
videoElement,
|
||||
callsToAction,
|
||||
className,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const validateProps = useCallback<() => boolean>(() => {
|
||||
const requiredProps = { headline, videoElement } as const;
|
||||
let isValid = true;
|
||||
|
||||
for (const [key, value] of Object.entries(requiredProps)) {
|
||||
if (isEmpty(value)) {
|
||||
if (isEnvironment(["development", "test"])) {
|
||||
console.warn(`${key} is required for FeaturedVideoHero`);
|
||||
}
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
return isValid;
|
||||
}, [headline, videoElement]);
|
||||
|
||||
if (!validateProps()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert callsToAction to ButtonConfig format for ButtonGroup
|
||||
const buttonConfigs: ButtonConfig[] = (callsToAction ?? [])
|
||||
.filter((cta) => !isEmpty(cta))
|
||||
.map((cta) => ({
|
||||
label: typeof cta?.children === 'string' ? cta.children : '',
|
||||
href: cta?.href,
|
||||
onClick: cta?.onClick,
|
||||
forceColor: true,
|
||||
}));
|
||||
|
||||
// Validate buttons (max 2 CTAs supported)
|
||||
const buttonValidation = validateButtonGroup(buttonConfigs, 2);
|
||||
|
||||
// Log warnings in development mode
|
||||
if (isEnvironment(["development", "test"]) && buttonValidation.warnings.length > 0) {
|
||||
buttonValidation.warnings.forEach(warning => console.warn(warning));
|
||||
}
|
||||
|
||||
const hasCallsToAction = buttonValidation.isValid && buttonValidation.buttons.length > 0;
|
||||
|
||||
return (
|
||||
<header
|
||||
ref={ref}
|
||||
className={clsx("bds-featured-video-hero", className)}
|
||||
{...rest}
|
||||
>
|
||||
<PageGrid>
|
||||
<PageGrid.Row>
|
||||
<PageGrid.Col span={{ base: 4, md: 8, lg: 6 }}>
|
||||
<div className="bds-featured-video-hero__content">
|
||||
<h1 className="mb-0 h-md">
|
||||
{headline}
|
||||
</h1>
|
||||
|
||||
<div className="bds-featured-video-hero__bottom-group">
|
||||
{subtitle && (
|
||||
<PageGrid.Row className="bds-featured-video-hero__subtitle body-l">
|
||||
<PageGrid.Col
|
||||
span={{ base: "fill", md: 6, lg: 10 }}
|
||||
className="bds-featured-video-hero__subtitle-col"
|
||||
>
|
||||
{subtitle}
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
)}
|
||||
{hasCallsToAction && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color="green"
|
||||
forceColor
|
||||
gap="small"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageGrid.Col>
|
||||
<PageGrid.Col
|
||||
span={{ base: 4, md: 8, lg: 6 }}
|
||||
>
|
||||
<div className="bds-featured-video-hero__video-container">
|
||||
<video
|
||||
{...videoElement}
|
||||
className="bds-featured-video-hero__video"
|
||||
/>
|
||||
</div>
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
</PageGrid>
|
||||
</header>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FeaturedVideoHero.displayName = "FeaturedVideoHero";
|
||||
|
||||
export default FeaturedVideoHero;
|
||||
162
shared/patterns/FeaturedVideoHero/README.md
Normal file
162
shared/patterns/FeaturedVideoHero/README.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# FeaturedVideoHero Pattern
|
||||
|
||||
A page-level hero pattern featuring a headline, optional subtitle, call-to-action buttons, and a featured video. The video uses native HTML `<video>` props and is displayed in a responsive two-column layout with content on the left and video on the right.
|
||||
|
||||
## Overview
|
||||
|
||||
The FeaturedVideoHero component provides a structured hero section with:
|
||||
|
||||
- Responsive two-column layout (content left, video right) that stacks on smaller screens
|
||||
- Required headline and video; optional subtitle and call-to-action buttons
|
||||
- Design-constrained CTAs: primary and optional secondary, with variant and color set by the component
|
||||
- Development-time validation: returns `null` when required props are missing and logs warnings in development/test
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```tsx
|
||||
import { FeaturedVideoHero } from "shared/patterns/FeaturedVideoHero";
|
||||
|
||||
function MyPage() {
|
||||
return (
|
||||
<FeaturedVideoHero
|
||||
headline="Build on XRPL"
|
||||
subtitle={
|
||||
<p>
|
||||
Issue, manage, and trade real-world assets without needing to build
|
||||
smart contracts.
|
||||
</p>
|
||||
}
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
| --------------- | --------------------------------- | -------- | ---------------------------------------------------------------------------- |
|
||||
| `headline` | `React.ReactNode` | Yes | Hero headline text (h-md typography) |
|
||||
| `subtitle` | `React.ReactNode` | No | Hero subtitle content |
|
||||
| `callsToAction` | `DesignConstrainedCallsToActions` | No | Array with primary CTA and optional secondary CTA. Omit to hide CTA section. |
|
||||
| `videoElement` | `DesignConstrainedVideoProps` | Yes | Native `<video>` element props (e.g. `src`, `autoPlay`, `loop`, `muted`) |
|
||||
| `className` | `string` | No | Additional CSS classes for the header element |
|
||||
| `...rest` | `HTMLHeaderElement` attributes | No | Any other HTML header attributes |
|
||||
|
||||
### Calls to Action
|
||||
|
||||
The `callsToAction` prop is optional. When provided, at least one non-empty CTA is required to show the CTA section. The component uses design-constrained Button props; `variant` and `color` are set automatically:
|
||||
|
||||
- **Primary CTA**: `variant="primary"`, `color="green"`, `forceColor={true}`
|
||||
- **Secondary CTA**: `variant="tertiary"`, `color="green"`, `forceColor={true}`
|
||||
|
||||
All other Button props are supported (e.g., `children`, `href`, `onClick`). Do not pass `variant` or `color` in the CTA objects.
|
||||
|
||||
### Video Element
|
||||
|
||||
`videoElement` accepts native HTML video element props. Required and commonly used props:
|
||||
|
||||
- `src` (required) – Video URL
|
||||
- `autoPlay`, `loop`, `muted`, `playsInline` – Typical for background/hero autoplay
|
||||
- `controls`, `preload`, `poster` – Optional; use for user-controlled playback
|
||||
|
||||
The video is rendered with `object-fit: cover` and a 16:9 aspect ratio container.
|
||||
|
||||
## Examples
|
||||
|
||||
### With primary and secondary CTAs
|
||||
|
||||
```tsx
|
||||
<FeaturedVideoHero
|
||||
headline="Real-world asset tokenization"
|
||||
subtitle="Learn how to issue crypto tokens and build tokenization solutions."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" },
|
||||
{ children: "Learn More", href: "/about" },
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/tokenization.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Without subtitle
|
||||
|
||||
```tsx
|
||||
<FeaturedVideoHero
|
||||
headline="Headline Only"
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### With video controls
|
||||
|
||||
```tsx
|
||||
<FeaturedVideoHero
|
||||
headline="Watch and Learn"
|
||||
subtitle="Explore our video tutorials and guides."
|
||||
callsToAction={[{ children: "Watch Tutorials", href: "/tutorials" }]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: false,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
controls: true,
|
||||
preload: "metadata",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
- **Required props**: `headline`, `videoElement`. If either is missing or empty, the component returns `null` and (in development/test) logs a console warning.
|
||||
- **Optional props**: `subtitle`, `callsToAction`. Omit `callsToAction` or pass an array with no renderable CTAs to hide the CTA section.
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
- **Mobile / small screens**: Content and video stack vertically; video appears below the content block with top margin.
|
||||
- **Large (lg+)**: Two-column layout: content (5 cols) on the left, video (6 cols, offset 1) on the right. Video container uses 16:9 aspect ratio and `object-fit: cover`.
|
||||
|
||||
## CSS Classes
|
||||
|
||||
- `bds-featured-video-hero` – Root header element
|
||||
- `bds-featured-video-hero__content` – Content column (headline, subtitle, CTAs)
|
||||
- `bds-featured-video-hero__title` – Headline (`h1`)
|
||||
- `bds-featured-video-hero__subtitle` – Subtitle row
|
||||
- `bds-featured-video-hero__subtitle-col` – Subtitle column
|
||||
- `bds-featured-video-hero__cta-buttons` – CTA buttons wrapper
|
||||
- `bds-featured-video-hero__video-container` – Video wrapper (16:9)
|
||||
- `bds-featured-video-hero__video` – Video element
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Video format**: Use MP4 with H.264 for broad compatibility; keep file sizes reasonable for fast loading.
|
||||
2. **Autoplay**: Use `muted` and `playsInline` with `autoPlay` for reliable autoplay on mobile.
|
||||
3. **CTAs**: Keep CTA text concise and action-oriented; primary CTA should be the main action.
|
||||
4. **Headlines**: Keep headlines concise; use the subtitle for additional context.
|
||||
5. **Accessibility**: Provide an `aria-label` (or other accessible name) on the video when it conveys meaningful content.
|
||||
|
||||
## Showcase
|
||||
|
||||
An interactive showcase with more examples and prop documentation is available at:
|
||||
|
||||
- **Showcase page**: `/about/featured-video-hero-showcase.page.tsx`
|
||||
88
shared/patterns/FeaturedVideoHero/_featured-video-hero.scss
Normal file
88
shared/patterns/FeaturedVideoHero/_featured-video-hero.scss
Normal file
@@ -0,0 +1,88 @@
|
||||
.bds-featured-video-hero{
|
||||
padding: 24px 0;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
@include bds-theme-mode(light) {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
@include bds-theme-mode(dark) {
|
||||
background-color: $black;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
height: 100%;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__video-container {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
margin-top: 16px;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&__video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__bottom-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,30 @@
|
||||
import React, { forwardRef, memo, useEffect } from "react";
|
||||
import clsx from "clsx";
|
||||
import { PageGrid } from "shared/components/PageGrid/page-grid";
|
||||
import { Button, ButtonProps } from "shared/components/Button/Button";
|
||||
import { Button } from "shared/components/Button/Button";
|
||||
import {
|
||||
isEmpty,
|
||||
DesignConstrainedButtonProps,
|
||||
isEnvironment,
|
||||
} from "shared/utils";
|
||||
|
||||
/**
|
||||
* Base props that all media elements must have to ensure proper styling.
|
||||
* These props are automatically applied to maintain the 9:16 aspect ratio
|
||||
* and object-fit: cover behavior.
|
||||
*/
|
||||
type MediaStyleProps = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
import {
|
||||
DesignConstrainedImageProps,
|
||||
DesignConstrainedVideoProps,
|
||||
} from "shared/utils/types";
|
||||
|
||||
/**
|
||||
* Image media type - extends native img element props
|
||||
*/
|
||||
type ImageMediaProps = {
|
||||
type: "image";
|
||||
} & Omit<
|
||||
React.ComponentPropsWithoutRef<"img">,
|
||||
keyof MediaStyleProps | "src" | "alt"
|
||||
> & {
|
||||
src: string; // Required for image media
|
||||
alt: string; // Required for image media
|
||||
};
|
||||
} & DesignConstrainedImageProps;
|
||||
|
||||
/**
|
||||
* Video media type - extends native video element props
|
||||
*/
|
||||
type VideoMediaProps = {
|
||||
type: "video";
|
||||
} & Omit<
|
||||
React.ComponentPropsWithoutRef<"video">,
|
||||
keyof MediaStyleProps | "src"
|
||||
> & {
|
||||
src: string; // Required for video media
|
||||
alt?: string; // Optional for video, but recommended for accessibility
|
||||
};
|
||||
} & DesignConstrainedVideoProps;
|
||||
|
||||
/**
|
||||
* Custom element media type - allows passing any React element
|
||||
@@ -95,6 +77,7 @@ const MediaRenderer: React.FC<{ media: HeaderHeroMedia }> = memo(
|
||||
}
|
||||
|
||||
case "video": {
|
||||
// alt here is being used as a aria label value
|
||||
const { type, alt, ...videoProps } = media;
|
||||
return (
|
||||
<div className={mediaContainerClassName}>
|
||||
|
||||
@@ -108,7 +108,6 @@ export const LogoRectangleGrid: React.FC<LogoRectangleGridProps> = ({
|
||||
heading,
|
||||
description,
|
||||
logos,
|
||||
className = '',
|
||||
}) => {
|
||||
// Build class names using BEM with bds namespace
|
||||
const classNames = clsx(
|
||||
|
||||
@@ -40,19 +40,6 @@ $bds-lsg-text-gap-desktop: 16px;
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
@extend .w-100;
|
||||
|
||||
// Mobile-first gap
|
||||
gap: $bds-lsg-header-gap-mobile;
|
||||
|
||||
// Tablet breakpoint
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-lsg-header-gap-tablet;
|
||||
}
|
||||
|
||||
// Desktop breakpoint
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-lsg-header-gap-desktop;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
|
||||
import { TileLogo, TileLogoProps } from '../../components/TileLogo/TileLogo';
|
||||
import { ButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
|
||||
export interface LogoItem extends TileLogoProps {}
|
||||
|
||||
@@ -13,18 +13,8 @@ export interface LogoSquareGridProps {
|
||||
heading?: string;
|
||||
/** Optional description text */
|
||||
description?: string;
|
||||
/** Primary button configuration */
|
||||
primaryButton?: {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
/** Tertiary button configuration */
|
||||
tertiaryButton?: {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
/** Button configurations (1-2 buttons supported) */
|
||||
buttons?: ButtonConfig[];
|
||||
/** Array of logo items to display in the grid */
|
||||
logos: LogoItem[];
|
||||
/** Additional CSS classes */
|
||||
@@ -55,8 +45,10 @@ export interface LogoSquareGridProps {
|
||||
* variant="green"
|
||||
* heading="Our Partners"
|
||||
* description="Leading companies building on XRPL."
|
||||
* primaryButton={{ label: "View All Partners", href: "/partners" }}
|
||||
* tertiaryButton={{ label: "Become a Partner", href: "/partner-program" }}
|
||||
* buttons={[
|
||||
* { label: "View All Partners", href: "/partners" },
|
||||
* { label: "Become a Partner", href: "/partner-program" }
|
||||
* ]}
|
||||
* logos={[
|
||||
* { src: "/logos/partner1.svg", alt: "Partner 1", href: "https://partner1.com" }
|
||||
* ]}
|
||||
@@ -66,11 +58,18 @@ export const LogoSquareGrid: React.FC<LogoSquareGridProps> = ({
|
||||
variant = 'gray',
|
||||
heading,
|
||||
description,
|
||||
primaryButton,
|
||||
tertiaryButton,
|
||||
buttons,
|
||||
logos,
|
||||
className = '',
|
||||
}) => {
|
||||
// Validate buttons if provided (max 2 buttons supported)
|
||||
const buttonValidation = buttons ? validateButtonGroup(buttons, 2) : null;
|
||||
|
||||
// Log warnings in development mode
|
||||
if (process.env.NODE_ENV === 'development' && buttonValidation?.warnings.length) {
|
||||
buttonValidation.warnings.forEach(warning => console.warn(warning));
|
||||
}
|
||||
|
||||
// Build class names using BEM with bds namespace
|
||||
const classNames = clsx(
|
||||
'bds-logo-square-grid',
|
||||
@@ -79,7 +78,8 @@ export const LogoSquareGrid: React.FC<LogoSquareGridProps> = ({
|
||||
);
|
||||
|
||||
// Determine if we should show the header section
|
||||
const hasHeader = !!(heading || description || primaryButton || tertiaryButton);
|
||||
const hasButtons = buttonValidation?.isValid && buttonValidation.buttons.length > 0;
|
||||
const hasHeader = !!(heading || description || hasButtons);
|
||||
|
||||
return (
|
||||
<PageGrid className={classNames}>
|
||||
@@ -97,12 +97,13 @@ export const LogoSquareGrid: React.FC<LogoSquareGridProps> = ({
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
<ButtonGroup
|
||||
primaryButton={primaryButton}
|
||||
tertiaryButton={tertiaryButton}
|
||||
color="green"
|
||||
gap="small"
|
||||
/>
|
||||
{hasButtons && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color="green"
|
||||
gap="small"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PageGridCol>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ButtonProps } from '../components/Button/Button';
|
||||
import { ButtonProps } from "../components/Button/Button";
|
||||
|
||||
/**
|
||||
* Button props with design constraints applied.
|
||||
@@ -12,4 +12,42 @@ import { ButtonProps } from '../components/Button/Button';
|
||||
* onClick: () => console.log('clicked')
|
||||
* };
|
||||
*/
|
||||
export type DesignConstrainedButtonProps = Omit<ButtonProps, 'variant' | 'color'>;
|
||||
export type DesignConstrainedButtonProps = Omit<
|
||||
ButtonProps,
|
||||
"variant" | "color"
|
||||
>;
|
||||
|
||||
export type DesignConstrainedCallsToActions = [
|
||||
DesignConstrainedButtonProps,
|
||||
DesignConstrainedButtonProps?,
|
||||
];
|
||||
|
||||
export type DesignConstrainedCallToActionsProps = {
|
||||
callsToAction?: DesignConstrainedCallsToActions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Base props that all media elements must have to ensure proper styling.
|
||||
* These props are automatically applied to maintain the 9:16 aspect ratio
|
||||
* and object-fit: cover behavior.
|
||||
*/
|
||||
export type MediaStyleProps = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
type ModifiedMediaProps = {
|
||||
alt?: string;
|
||||
};
|
||||
|
||||
export type DesignConstrainedVideoProps = Omit<
|
||||
React.ComponentPropsWithRef<"video">,
|
||||
keyof MediaStyleProps
|
||||
> &
|
||||
// the "alt" value here will be used with an aria label
|
||||
ModifiedMediaProps;
|
||||
|
||||
export type DesignConstrainedImageProps = Omit<
|
||||
React.ComponentPropsWithRef<"img">,
|
||||
keyof MediaStyleProps
|
||||
>;
|
||||
|
||||
@@ -5770,7 +5770,7 @@ textarea.form-control-lg {
|
||||
justify-content: space-evenly !important;
|
||||
}
|
||||
|
||||
.align-items-start, .bds-button-group {
|
||||
.align-items-start {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
@@ -13242,6 +13242,22 @@ html.dark .bds-btn--tertiary:not(.bds-btn--black):disabled, html.dark .bds-btn--
|
||||
}
|
||||
}
|
||||
|
||||
.bds-btn--no-padding {
|
||||
padding: 0 !important;
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
.bds-btn--no-padding:hover:not(:disabled):not(.bds-btn--disabled), .bds-btn--no-padding:focus-visible:not(:disabled):not(.bds-btn--disabled), .bds-btn--no-padding:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
@media (max-width: 1279.98px) {
|
||||
.bds-btn--no-padding {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.bds-btn--no-padding:hover:not(:disabled):not(.bds-btn--disabled), .bds-btn--no-padding:focus-visible:not(:disabled):not(.bds-btn--disabled), .bds-btn--no-padding:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-carousel-button {
|
||||
appearance: none;
|
||||
border: none;
|
||||
@@ -20030,6 +20046,95 @@ html.dark .bds-text-card--disabled .bds-text-card__overlay {
|
||||
}
|
||||
}
|
||||
|
||||
.bds-featured-video-hero {
|
||||
padding: 24px 0;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-featured-video-hero {
|
||||
padding: 32px 0;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-featured-video-hero {
|
||||
padding: 40px 0;
|
||||
}
|
||||
}
|
||||
html.light .bds-featured-video-hero {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
html.dark .bds-featured-video-hero {
|
||||
background-color: #141414;
|
||||
}
|
||||
.bds-featured-video-hero__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
height: 100%;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-featured-video-hero__content {
|
||||
gap: 32px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-featured-video-hero__content {
|
||||
gap: 40px;
|
||||
}
|
||||
}
|
||||
.bds-featured-video-hero__subtitle {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.bds-featured-video-hero__video-container {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16/9;
|
||||
overflow: hidden;
|
||||
margin-top: 16px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-featured-video-hero__video-container {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-featured-video-hero__video-container {
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
.bds-featured-video-hero__video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
display: inline-block;
|
||||
}
|
||||
.bds-featured-video-hero__bottom-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-featured-video-hero__bottom-group {
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-featured-video-hero__bottom-group {
|
||||
gap: 40px;
|
||||
}
|
||||
}
|
||||
.bds-featured-video-hero__bottom-group p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bds-header-hero-primary-media {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
@@ -20438,6 +20543,7 @@ html.light .bds-hero-split-media--accent .bds-hero-split-media__subtitle {
|
||||
}
|
||||
|
||||
.bds-button-group {
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
@@ -20459,6 +20565,14 @@ html.light .bds-hero-split-media--accent .bds-hero-split-media__subtitle {
|
||||
}
|
||||
}
|
||||
|
||||
.bds-button-group--block {
|
||||
flex-direction: column !important;
|
||||
gap: 16px !important;
|
||||
}
|
||||
.bds-button-group--block .bds-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bds-callout-media-banner {
|
||||
box-sizing: border-box;
|
||||
min-height: 280px;
|
||||
@@ -20771,42 +20885,50 @@ html.dark .bds-card-stats__description {
|
||||
gap: 8px;
|
||||
max-width: 100%;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-carousel-card-list__header-content {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-carousel-card-list__header-content {
|
||||
gap: 16px;
|
||||
max-width: 808px;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-carousel-card-list__heading {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bds-carousel-card-list__description {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bds-carousel-card-list__nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
margin-top: 24px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-carousel-card-list__nav {
|
||||
margin-top: 32px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-carousel-card-list__nav {
|
||||
margin-top: 40px;
|
||||
}
|
||||
padding: 4px;
|
||||
margin: -4px;
|
||||
}
|
||||
|
||||
.bds-carousel-card-list__track-wrapper {
|
||||
margin-top: 24px;
|
||||
overflow: visible;
|
||||
padding-left: 18px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-carousel-card-list__track-wrapper {
|
||||
margin-top: 32px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-carousel-card-list__track-wrapper {
|
||||
margin-top: 40px;
|
||||
padding-left: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20883,20 +21005,6 @@ html.dark .bds-card-stats__description {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.bds-carousel-card-list__grid {
|
||||
gap: 24px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-carousel-card-list__grid {
|
||||
gap: 32px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-carousel-card-list__grid {
|
||||
gap: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
html.light .bds-carousel-card-list__track:focus-visible {
|
||||
outline-color: #111112;
|
||||
}
|
||||
@@ -21262,20 +21370,6 @@ html.light .bds-carousel-featured--bg-yellow .bds-divider {
|
||||
background-color: #141414;
|
||||
}
|
||||
|
||||
.bds-logo-square-grid {
|
||||
gap: 24px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-logo-square-grid {
|
||||
gap: 32px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-logo-square-grid {
|
||||
gap: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-logo-square-grid__header {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 24px;
|
||||
@@ -21441,15 +21535,6 @@ html.dark .bds-cards-featured__description {
|
||||
}
|
||||
}
|
||||
|
||||
.bds-feature-two-column__button-group .bds-btn--tertiary {
|
||||
padding-top: 0px !important;
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
|
||||
.bds-feature-two-column__cta-row .bds-btn--tertiary {
|
||||
padding-top: 16px !important;
|
||||
}
|
||||
|
||||
.bds-feature-two-column {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -21569,44 +21654,22 @@ html.dark .bds-cards-featured__description {
|
||||
}
|
||||
}
|
||||
.bds-feature-two-column__content--multiple {
|
||||
gap: 0;
|
||||
gap: 24px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-feature-two-column__content--multiple {
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-feature-two-column__content--multiple {
|
||||
gap: 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
.bds-feature-two-column__content--multiple > .bds-btn--secondary {
|
||||
align-self: flex-start;
|
||||
}
|
||||
.bds-feature-two-column__button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-feature-two-column__button-group {
|
||||
gap: 16px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-feature-two-column__button-group {
|
||||
gap: 16px;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.bds-feature-two-column__button-group > .bds-btn--tertiary {
|
||||
padding-left: 0 !important;
|
||||
margin-left: 0;
|
||||
}
|
||||
.bds-feature-two-column__button-group > .bds-btn--tertiary:hover:not(:disabled):not(.bds-btn--disabled), .bds-feature-two-column__button-group > .bds-btn--tertiary:focus:not(:disabled):not(.bds-btn--disabled), .bds-feature-two-column__button-group > .bds-btn--tertiary:focus-visible:not(:disabled):not(.bds-btn--disabled), .bds-feature-two-column__button-group > .bds-btn--tertiary:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
.bds-feature-two-column__text-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -21678,56 +21741,6 @@ html.dark .bds-cards-featured__description {
|
||||
color: #141414;
|
||||
margin: 0;
|
||||
}
|
||||
.bds-feature-two-column__cta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-feature-two-column__cta {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
.bds-feature-two-column__cta--single {
|
||||
flex-direction: column;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-feature-two-column__cta--double {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.bds-feature-two-column__cta-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-feature-two-column__cta-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-feature-two-column__cta-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.bds-feature-two-column__tertiary-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0;
|
||||
}
|
||||
.bds-feature-two-column__tertiary-group .bds-btn--tertiary {
|
||||
padding-left: 0 !important;
|
||||
margin-left: 0;
|
||||
}
|
||||
.bds-feature-two-column__tertiary-group .bds-btn--tertiary:hover:not(:disabled):not(.bds-btn--disabled), .bds-feature-two-column__tertiary-group .bds-btn--tertiary:focus:not(:disabled):not(.bds-btn--disabled), .bds-feature-two-column__tertiary-group .bds-btn--tertiary:focus-visible:not(:disabled):not(.bds-btn--disabled), .bds-feature-two-column__tertiary-group .bds-btn--tertiary:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
.bds-feature-two-column__media {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -102,6 +102,7 @@ $line-height-base: 1.5;
|
||||
@import "../shared/components/TextCard/TextCard.scss";
|
||||
@import "../shared/components/SmallTilesSection/_small-tiles-section.scss";
|
||||
@import "../shared/components/StandardCard/_standard-card.scss";
|
||||
@import "../shared/patterns/FeaturedVideoHero/_featured-video-hero.scss";
|
||||
@import "../shared/patterns/HeaderHeroPrimaryMedia/_header-hero-primary-media.scss";
|
||||
@import "../shared/patterns/HeaderHeroSplitMedia/HeaderHeroSplitMedia.scss";
|
||||
@import "../shared/patterns/ButtonGroup/ButtonGroup.scss";
|
||||
|
||||
Reference in New Issue
Block a user