Enhance FeatureSingleTopic component with button variant support and responsive behavior updates

- Updated the FeatureSingleTopic component to allow configurable button variants (primary or secondary) based on the `singleButtonVariant` prop.
- Improved mobile and tablet responsiveness by ensuring content always appears above the image, regardless of orientation.
- Refined button rendering logic for better clarity in the README and component documentation.
- Adjusted SCSS styles for improved spacing and alignment across different screen sizes.
This commit is contained in:
akcodez
2026-02-04 11:29:56 -08:00
committed by Calvin Jhunjhuwala
parent 7b075f8690
commit 4ba8a7058b
6 changed files with 165 additions and 135 deletions

View File

@@ -110,8 +110,9 @@ export default function FeatureSingleTopicShowcase() {
<div className="mb-4 p-3" style={{ backgroundColor: '#f0f3f7', borderRadius: '8px' }}>
<strong>📱 Responsive Behavior:</strong>
<ul className="mb-0 mt-2">
<li><code>orientation="left"</code>: Image left, content right on desktop; image above content on mobile</li>
<li><code>orientation="right"</code>: Image right, content left on desktop; content above image on mobile</li>
<li><code>orientation="left"</code>: Image left, content right on desktop</li>
<li><code>orientation="right"</code>: Image right, content left on desktop</li>
<li><strong>Mobile/Tablet:</strong> Content always appears above image regardless of orientation</li>
</ul>
</div>
</PageGridCol>
@@ -127,7 +128,7 @@ export default function FeatureSingleTopicShowcase() {
<strong>Orientation Left (Default)</strong> - <code>orientation="left"</code>
<br />
<small className="text-muted">
Desktop: Image left, content right | Mobile: Image above content
Desktop: Image left, content right | Mobile/Tablet: Content above image
</small>
</div>
</PageGridCol>
@@ -155,7 +156,7 @@ export default function FeatureSingleTopicShowcase() {
<strong>Orientation Right</strong> - <code>orientation="right"</code>
<br />
<small className="text-muted">
Desktop: Image right, content left | Mobile: Content above image
Desktop: Image right, content left | Mobile/Tablet: Content above image
</small>
</div>
</PageGridCol>
@@ -180,24 +181,24 @@ export default function FeatureSingleTopicShowcase() {
<PageGridCol span={12}>
<h2 className="h4 mb-6">Button Behavior</h2>
<p className="mb-4">
The component uses the ButtonGroup pattern which automatically adjusts button rendering based on the number of links provided:
The component automatically adjusts button rendering based on the number of links provided:
</p>
<ul className="mb-6">
<li><strong>1 link:</strong> Primary button</li>
<li><strong>2 links:</strong> Primary + Tertiary buttons (responsive layout)</li>
<li><strong>3+ links:</strong> All Tertiary buttons in block layout</li>
<li><strong>1 link:</strong> Primary or Secondary button (configurable via <code>singleButtonVariant</code> prop)</li>
<li><strong>2 links:</strong> Primary + Tertiary buttons side by side</li>
<li><strong>3+ links:</strong> All Tertiary buttons stacked</li>
</ul>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* 1 Link */}
{/* 1 Link - Primary */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>1 Link</strong> - Primary Button
<strong>ex: 1 button</strong> - Primary Button (default)
<br />
<small className="text-muted">Single action rendered as a primary (filled) button.</small>
</div>
@@ -207,11 +208,37 @@ export default function FeatureSingleTopicShowcase() {
<FeatureSingleTopic
variant="default"
orientation="left"
title="Single Action"
description="This example shows a single call-to-action button."
title="Developer Spotlight"
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
media={{ src: placeholderImage, alt: "Single button" }}
links={[
{ label: "Get Started", href: "#start" }
{ label: "Primary Link", href: "#start" }
]}
/>
</div>
{/* 1 Link - Secondary */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>ex: 1 button</strong> - Secondary Button
<br />
<small className="text-muted">Single action rendered as a secondary (outlined) button using <code>singleButtonVariant="secondary"</code>.</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureSingleTopic
variant="default"
orientation="left"
title="Developer Spotlight"
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
media={{ src: placeholderImage, alt: "Single button secondary" }}
singleButtonVariant="secondary"
links={[
{ label: "Secondary Link", href: "#start" }
]}
/>
</div>
@@ -222,9 +249,9 @@ export default function FeatureSingleTopicShowcase() {
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>2 Links</strong> - Primary + Tertiary Side by Side
<strong>ex: 2 button</strong> - Primary + Tertiary Side by Side
<br />
<small className="text-muted">Primary and tertiary buttons displayed side by side on desktop.</small>
<small className="text-muted">Primary and tertiary buttons displayed side by side on all breakpoints.</small>
</div>
</PageGridCol>
</PageGridRow>
@@ -232,8 +259,8 @@ export default function FeatureSingleTopicShowcase() {
<FeatureSingleTopic
variant="default"
orientation="left"
title="Two Actions"
description="This example shows primary and tertiary buttons side by side."
title="Developer Spotlight"
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
media={{ src: placeholderImage, alt: "Two buttons" }}
links={[
{ label: "Primary Link", href: "#primary" },
@@ -242,42 +269,15 @@ export default function FeatureSingleTopicShowcase() {
/>
</div>
{/* 3 Links */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>3 Links</strong> - Primary + Tertiary + Secondary
<br />
<small className="text-muted">Primary + tertiary side by side on first row, secondary button below.</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureSingleTopic
variant="default"
orientation="left"
title="Three Actions"
description="This example shows the 3-link layout with secondary button."
media={{ src: placeholderImage, alt: "Three buttons" }}
links={[
{ label: "Primary Link", href: "#primary" },
{ label: "Tertiary Link", href: "#tertiary" },
{ label: "Secondary Link", href: "#secondary" }
]}
/>
</div>
{/* 5 Links */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>5 Links</strong> - Primary + Tertiary + Stacked Tertiary
<strong>ex: 5 button</strong> - All Tertiary Stacked
<br />
<small className="text-muted">Primary + tertiary side by side on first row, remaining tertiary buttons stacked below.</small>
<small className="text-muted">3+ links render as all tertiary buttons stacked vertically.</small>
</div>
</PageGridCol>
</PageGridRow>
@@ -285,12 +285,12 @@ export default function FeatureSingleTopicShowcase() {
<FeatureSingleTopic
variant="accentSurface"
orientation="left"
title="Multiple Actions"
description="This example shows the 5-link layout with stacked tertiary buttons."
title="Developer Spotlight"
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
media={{ src: placeholderImage, alt: "Multiple buttons" }}
links={[
{ label: "Primary Link", href: "#primary" },
{ label: "Tertiary Link", href: "#tertiary" },
{ label: "Tertiary Link", href: "#link1" },
{ label: "Tertiary Link", href: "#link2" },
{ label: "Tertiary Link", href: "#link3" },
{ label: "Tertiary Link", href: "#link4" },
{ label: "Tertiary Link", href: "#link5" }
@@ -298,6 +298,33 @@ export default function FeatureSingleTopicShowcase() {
/>
</div>
{/* 3 Links */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>ex: 3 button</strong> - All Tertiary Stacked
<br />
<small className="text-muted">3+ links render as all tertiary buttons stacked vertically.</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureSingleTopic
variant="default"
orientation="left"
title="Developer Spotlight"
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
media={{ src: placeholderImage, alt: "Three buttons" }}
links={[
{ label: "Tertiary Link", href: "#link1" },
{ label: "Tertiary Link", href: "#link2" },
{ label: "Tertiary Link", href: "#link3" }
]}
/>
</div>
{/* Alternating Pattern Example */}

View File

@@ -1,4 +1,3 @@
// FeatureSingleTopic Pattern Styles
// =============================================================================
// A feature section pattern with single topic layout for title and media.
// Supports variants (default, accentSurface) and orientation (left, right).
@@ -29,11 +28,13 @@ $bds-single-topic-desktop-height: 565px; // Fixed height from Figma design
$bds-single-topic-tablet-py: 32px;
$bds-single-topic-tablet-content-gap: 32px; // Gap between image and content on tablet
$bds-single-topic-tablet-content-min-height: 320px; // Min height for content on tablet
$bds-single-topic-tablet-title-description-gap: 80px; // Gap between accent/title and description on tablet
// Spacing - Mobile (<576px)
$bds-single-topic-mobile-py: 24px;
$bds-single-topic-mobile-content-gap: 24px; // Gap between image and content on mobile
$bds-single-topic-mobile-content-min-height: 280px; // Min height for content on mobile
$bds-single-topic-mobile-title-description-gap: 40px; // Gap between accent/title and description on mobile
// =============================================================================
// Base Styles
@@ -127,13 +128,14 @@ $bds-single-topic-mobile-content-min-height: 280px; // Min height for content on
display: flex;
flex-direction: column;
height: 100%;
gap: $bds-single-topic-mobile-content-gap;
// Gap between accent/title section and description section
gap: $bds-single-topic-mobile-title-description-gap; // 40px on mobile
// Min height on mobile to prevent squished content
min-height: $bds-single-topic-mobile-content-min-height;
justify-content: space-between;
@include media-breakpoint-up(md) {
gap: $bds-single-topic-tablet-content-gap;
gap: $bds-single-topic-tablet-title-description-gap; // 80px on tablet
min-height: $bds-single-topic-tablet-content-min-height;
}
@@ -183,17 +185,15 @@ $bds-single-topic-mobile-content-min-height: 280px; // Min height for content on
gap: $bds-single-topic-desktop-cta-gap;
}
// CTA row - primary + tertiary side by side
// CTA row - primary + tertiary side by side on all breakpoints
&__cta-row {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
flex-direction: row;
align-items: center;
gap: 16px; // Gap between primary and tertiary on mobile
@include media-breakpoint-up(md) {
flex-direction: row;
align-items: center;
gap: 24px; // 24px gap between primary and tertiary per Figma
gap: 24px; // 24px gap between primary and tertiary on tablet/desktop per Figma
}
}
}
@@ -215,9 +215,17 @@ $bds-single-topic-mobile-content-min-height: 280px; // Min height for content on
.bds-feature-single-topic__title-section {
background-color: $bds-single-topic-title-bg;
padding: $bds-single-topic-desktop-title-padding;
// Mobile min-height
min-height: 160px;
@include media-breakpoint-up(md) {
// Tablet min-height
min-height: 200px;
}
@include media-breakpoint-up(lg) {
height: 200px;
// Desktop min-height
min-height: 200px;
}
}
}
@@ -249,8 +257,14 @@ $bds-single-topic-mobile-content-min-height: 280px; // Min height for content on
}
// Right orientation - image on right, content on left on desktop
// On mobile/tablet: image appears ABOVE content (default column order)
// On mobile/tablet: content appears ABOVE image (column-reverse) - same as left
.bds-feature-single-topic--right {
.bds-feature-single-topic__row {
@include media-breakpoint-down(lg) {
flex-direction: column-reverse;
}
}
.bds-feature-single-topic__media-col {
@include media-breakpoint-up(lg) {
order: 2;

View File

@@ -27,12 +27,16 @@ export interface FeatureSingleTopicProps {
/** Feature description text (label-l typography) */
description?: string;
/** Array of links (1-5 links supported)
* - 1 link: renders as primary button
* - 1 link: renders as primary or secondary button (based on singleButtonVariant)
* - 2 links: renders as primary + tertiary buttons side by side
* - 3 links: primary + tertiary side by side, secondary button below
* - 4-5 links: primary + tertiary side by side, remaining as tertiary stacked
* - 3+ links: all tertiary buttons stacked
*/
links?: FeatureSingleTopicLink[];
/** Button variant for single button configuration
* - 'primary': Primary button (default)
* - 'secondary': Secondary button
*/
singleButtonVariant?: 'primary' | 'secondary';
/** Feature media (image) configuration */
media: {
src: string;
@@ -59,6 +63,7 @@ export const FeatureSingleTopic: React.FC<FeatureSingleTopicProps> = ({
title,
description,
links = [],
singleButtonVariant = 'primary',
media,
className,
}) => {
@@ -97,19 +102,18 @@ export const FeatureSingleTopic: React.FC<FeatureSingleTopicProps> = ({
);
// Render CTA buttons based on count
// - 1 link: primary button
// - 1 link: primary or secondary button (based on singleButtonVariant prop)
// - 2 links: primary + tertiary side by side
// - 3 links: primary + tertiary side by side, secondary button below
// - 4-5 links: primary + tertiary side by side, remaining as tertiary stacked
// - 3+ links: all tertiary buttons stacked
const renderCTA = () => {
const buttons = buttonValidation.buttons;
if (!buttonValidation.isValid || buttons.length === 0) return null;
// 1 button: just primary
// 1 button: primary or secondary based on singleButtonVariant prop
if (buttons.length === 1) {
return (
<div className="bds-feature-single-topic__cta">
<Button variant="primary" color={buttonColor} href={buttons[0].href}>
<Button variant={singleButtonVariant} color={buttonColor} href={buttons[0].href}>
{buttons[0].label}
</Button>
</div>
@@ -132,38 +136,10 @@ export const FeatureSingleTopic: React.FC<FeatureSingleTopicProps> = ({
);
}
// 3 buttons: primary + tertiary side by side, secondary button below
if (buttons.length === 3) {
return (
<div className="bds-feature-single-topic__cta">
<div className="bds-feature-single-topic__cta-row">
<Button variant="primary" color={buttonColor} href={buttons[0].href}>
{buttons[0].label}
</Button>
<Button variant="tertiary" color={buttonColor} href={buttons[1].href} forceNoPadding>
{buttons[1].label}
</Button>
</div>
<Button variant="secondary" color={buttonColor} href={buttons[2].href}>
{buttons[2].label}
</Button>
</div>
);
}
// 4-5 buttons: primary + tertiary side by side, remaining as tertiary stacked
const remainingButtons = buttons.slice(2);
// 3+ buttons: all tertiary buttons stacked
return (
<div className="bds-feature-single-topic__cta">
<div className="bds-feature-single-topic__cta-row">
<Button variant="primary" color={buttonColor} href={buttons[0].href}>
{buttons[0].label}
</Button>
<Button variant="tertiary" color={buttonColor} href={buttons[1].href} forceNoPadding>
{buttons[1].label}
</Button>
</div>
{remainingButtons.map((button, index) => (
{buttons.map((button, index) => (
<Button
key={index}
variant="tertiary"

View File

@@ -42,6 +42,7 @@ import { FeatureSingleTopic } from 'shared/patterns/FeatureSingleTopic';
| `title` | `string` | *required* | Feature title (heading-md typography) |
| `description` | `string` | - | Feature description (label-l typography) |
| `links` | `FeatureSingleTopicLink[]` | `[]` | Array of CTA links (1-5 supported) |
| `singleButtonVariant` | `'primary' \| 'secondary'` | `'primary'` | Button variant for single button configuration |
| `media` | `{ src: string; alt: string }` | *required* | Image configuration |
| `className` | `string` | - | Additional CSS classes |
@@ -60,10 +61,11 @@ The component automatically determines button variants based on count:
| Count | Layout |
|-------|--------|
| 1 link | Primary button |
| 1 link | Primary or Secondary button (configurable via `singleButtonVariant` prop) |
| 2 links | Primary + Tertiary side by side |
| 3 links | Primary + Tertiary side by side, Secondary button below |
| 4-5 links | Primary + Tertiary side by side, remaining Tertiary stacked |
| 3+ links | All Tertiary buttons stacked |
**Note:** On mobile, the first two buttons (Primary + Tertiary) remain side by side.
## Variants
@@ -84,10 +86,12 @@ Gray background (#E6EAF0 light / #CAD4DF dark) on the title section.
## Orientation
### Left (default)
Image on left, content on right on desktop. On mobile/tablet, content appears above image.
Image on left, content on right on desktop.
### Right
Image on right, content on left on desktop. On mobile/tablet, image appears above content.
Image on right, content on left on desktop.
**Note:** On mobile/tablet, content always appears above image regardless of orientation.
## Responsive Behavior

View File

@@ -21536,13 +21536,13 @@ html.dark .bds-feature-two-column--green {
display: flex;
flex-direction: column;
height: 100%;
gap: 24px;
gap: 40px;
min-height: 280px;
justify-content: space-between;
}
@media (min-width: 576px) {
.bds-feature-single-topic__content {
gap: 32px;
gap: 80px;
min-height: 320px;
}
}
@@ -21629,14 +21629,12 @@ html.dark .bds-feature-two-column--green {
}
.bds-feature-single-topic__cta-row {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
flex-direction: row;
align-items: center;
gap: 16px;
}
@media (min-width: 576px) {
.bds-feature-single-topic__cta-row {
flex-direction: row;
align-items: center;
gap: 24px;
}
}
@@ -21649,10 +21647,16 @@ html.dark .bds-feature-two-column--green {
.bds-feature-single-topic--accentSurface .bds-feature-single-topic__title-section {
background-color: #E6EAF0;
padding: 16px;
min-height: 160px;
}
@media (min-width: 576px) {
.bds-feature-single-topic--accentSurface .bds-feature-single-topic__title-section {
min-height: 200px;
}
}
@media (min-width: 992px) {
.bds-feature-single-topic--accentSurface .bds-feature-single-topic__title-section {
height: 200px;
min-height: 200px;
}
}
@@ -21672,6 +21676,11 @@ html.dark .bds-feature-two-column--green {
}
}
@media (max-width: 991.98px) {
.bds-feature-single-topic--right .bds-feature-single-topic__row {
flex-direction: column-reverse;
}
}
@media (min-width: 992px) {
.bds-feature-single-topic--right .bds-feature-single-topic__media-col {
order: 2;
@@ -28259,33 +28268,33 @@ html.light .progress {
html.light [data-component-name="Search/SearchIcon"] > path {
fill: black;
}
html.light a:not(.bds-link):not(.bds-card-offgrid),
html.light nav a:not(.bds-link):not(.bds-card-offgrid),
html.light a:not([role=button]):not(.bds-link):not(.bds-card-offgrid) {
html.light a:not(.bds-link):not(.bds-card-offgrid):not(.bds-btn),
html.light nav a:not(.bds-link):not(.bds-card-offgrid):not(.bds-btn),
html.light a:not([role=button]):not(.bds-link):not(.bds-card-offgrid):not(.bds-btn) {
color: #141414;
}
html.light a:not(.bds-link):not(.bds-card-offgrid).btn-primary,
html.light nav a:not(.bds-link):not(.bds-card-offgrid).btn-primary,
html.light a:not([role=button]):not(.bds-link):not(.bds-card-offgrid).btn-primary {
html.light a:not(.bds-link):not(.bds-card-offgrid):not(.bds-btn).btn-primary,
html.light nav a:not(.bds-link):not(.bds-card-offgrid):not(.bds-btn).btn-primary,
html.light a:not([role=button]):not(.bds-link):not(.bds-card-offgrid):not(.bds-btn).btn-primary {
color: #FFFFFF;
}
html.light a:not(.bds-link):not(.bds-card-offgrid).btn-primary:hover,
html.light nav a:not(.bds-link):not(.bds-card-offgrid).btn-primary:hover,
html.light a:not([role=button]):not(.bds-link):not(.bds-card-offgrid).btn-primary:hover {
html.light a:not(.bds-link):not(.bds-card-offgrid):not(.bds-btn).btn-primary:hover,
html.light nav a:not(.bds-link):not(.bds-card-offgrid):not(.bds-btn).btn-primary:hover,
html.light a:not([role=button]):not(.bds-link):not(.bds-card-offgrid):not(.bds-btn).btn-primary:hover {
color: #FFFFFF;
}
html.light a:not(.bds-link):not(.bds-card-offgrid):hover, html.light a:not(.bds-link):not(.bds-card-offgrid):active, html.light a:not(.bds-link):not(.bds-card-offgrid).active,
html.light nav a:not(.bds-link):not(.bds-card-offgrid):hover,
html.light nav a:not(.bds-link):not(.bds-card-offgrid):active,
html.light nav a:not(.bds-link):not(.bds-card-offgrid).active,
html.light a:not([role=button]):not(.bds-link):not(.bds-card-offgrid):hover,
html.light a:not([role=button]):not(.bds-link):not(.bds-card-offgrid):active,
html.light a:not([role=button]):not(.bds-link):not(.bds-card-offgrid).active {
html.light a:not(.bds-link):not(.bds-card-offgrid):not(.bds-btn):hover, html.light a:not(.bds-link):not(.bds-card-offgrid):not(.bds-btn):active, html.light a:not(.bds-link):not(.bds-card-offgrid):not(.bds-btn).active,
html.light nav a:not(.bds-link):not(.bds-card-offgrid):not(.bds-btn):hover,
html.light nav a:not(.bds-link):not(.bds-card-offgrid):not(.bds-btn):active,
html.light nav a:not(.bds-link):not(.bds-card-offgrid):not(.bds-btn).active,
html.light a:not([role=button]):not(.bds-link):not(.bds-card-offgrid):not(.bds-btn):hover,
html.light a:not([role=button]):not(.bds-link):not(.bds-card-offgrid):not(.bds-btn):active,
html.light a:not([role=button]):not(.bds-link):not(.bds-card-offgrid):not(.bds-btn).active {
color: #5429A1;
}
html.light a:not(.bds-link):not(.bds-card-offgrid):not(.btn):not(.bds-text-card):focus,
html.light nav a:not(.bds-link):not(.bds-card-offgrid):not(.btn):not(.bds-text-card):focus,
html.light a:not([role=button]):not(.bds-link):not(.bds-card-offgrid):not(.btn):not(.bds-text-card):focus {
html.light a:not(.bds-link):not(.bds-card-offgrid):not(.bds-btn):not(.btn):not(.bds-text-card):focus,
html.light nav a:not(.bds-link):not(.bds-card-offgrid):not(.bds-btn):not(.btn):not(.bds-text-card):focus,
html.light a:not([role=button]):not(.bds-link):not(.bds-card-offgrid):not(.bds-btn):not(.btn):not(.bds-text-card):focus {
background-color: transparent;
}
html.light a.card:hover, html.light:active, html.light.active {

View File

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