Component Library Refactor & New Components (#3510)

* adding showcase page

* adding CardStatsList

* clean up, tighter code

* code review and code clean up

* update import, clean up env for error message

* tweak some css code

* less css, rebuilt

* re-adding bem, modifier for bds variants
This commit is contained in:
Calvin
2026-02-23 15:00:18 -08:00
committed by Calvin Jhunjhuwala
parent fe1aa0ecb1
commit 176fb3cbc7
120 changed files with 2394 additions and 793 deletions

View File

@@ -2,6 +2,7 @@ navbar.about: Acerca de
navbar.docs: Docs
navbar.resources: Recursos
navbar.community: Comunidad
navbar.showcase: Escaparate
footer.about: Acerca de
footer.docs: Docs
footer.resources: Recursos

View File

@@ -16,6 +16,7 @@ navbar.about: 概要
navbar.docs: ドキュメント
navbar.resources: リソース
navbar.community: コミュニティ
navbar.showcase: ショーケース
footer.about: 概要
footer.docs: ドキュメント
footer.resources: リソース

View File

@@ -14,6 +14,7 @@ export const navItems: NavItem[] = [
{ label: "Use Cases", labelTranslationKey: "navbar.usecases", href: "/use-cases", hasSubmenu: true },
{ label: "Community", labelTranslationKey: "navbar.community", href: "/community", hasSubmenu: true },
{ label: "Network", labelTranslationKey: "navbar.network", href: "/resources", hasSubmenu: true },
{ label: "Showcase (Temporary)", labelTranslationKey: "navbar.showcase", href: "/showcase", hasSubmenu: false },
];
// Develop submenu data structure

View File

@@ -19,13 +19,14 @@ module.exports = {
...(isProduction || process.env.PURGECSS === 'true'
? [
purgecss({
// Scan all content files for class names
// Scan all content files for class names (TSX/TS/HTML = usage; SCSS = BEM modifier definitions)
content: [
'./**/*.tsx',
'./**/*.ts',
'./**/*.md',
'./**/*.yaml',
'./**/*.html',
'./**/*.scss',
'./static/js/**/*.js',
'./static/vendor/**/*.js',
// Ignore node_modules except for specific libraries that inject classes
@@ -42,7 +43,11 @@ module.exports = {
const m = match.match(/["']([^"']*)["']/);
return m ? m[1].split(/\s+/) : [];
});
return [...broadMatches, ...classes];
// Extract BEM modifier classes from SCSS (e.g. .bds-callout-media-banner--green)
// These are dynamically applied in TSX via template literals so PurgeCSS won't find them as literals
const scssBemMatches = content.match(/\.(bds-[a-z0-9-]+--[a-z0-9-]+)/g) || [];
const bemModifiers = [...new Set(scssBemMatches.map(m => m.replace(/^\./, '')))];
return [...new Set([...broadMatches, ...classes, ...bemModifiers])];
},
// Safelist - classes that should never be removed
@@ -68,6 +73,9 @@ module.exports = {
/^col-/, // Column classes
/^bds-grid__col/, // PageGrid column classes (dynamic span values)
/^bds-grid__offset/, // PageGrid offset classes
// BDS BEM modifier classes - applied dynamically via template literals (e.g. bds-callout-media-banner--green)
// Required: PurgeCSS cannot find these in TSX/SCSS due to interpolation
/^bds-[a-z0-9-]+--/,
/^g-/, // Gap utilities
/^p-/, // Padding utilities
/^m-/, // Margin utilities

View File

@@ -44,7 +44,8 @@ $bds-card-stat-transition-timing: $bds-transition-timing;
.bds-card-stat {
// Layout
display: flex;
// !important needed to override PageGridCol's default display (e.g. grid/flex from parent)
display: flex !important;
flex-direction: column;
width: 100%;
min-height: 200px;

View File

@@ -87,43 +87,41 @@ export const CardStat: React.FC<CardStatProps> = ({
const isNumericSuperscript = superscript && /^[0-9]+$/.test(superscript);
return (
<PageGridCol span={span}>
<div className={classNames}>
{/* Text section */}
<div className="bds-card-stat__text">
<div className="bds-card-stat__statistic">
{statistic}{superscript && <sup className={isNumericSuperscript ? 'bds-card-stat__superscript--numeric' : ''}>{superscript}</sup>}</div>
<div className="body-r">{label}</div>
</div>
{/* Buttons section */}
{hasButtons && (
<div className="bds-card-stat__buttons">
{primaryButton && (
<Button
// forceColor - prop to be added once feature two column is merged
variant="primary"
color="black"
href={primaryButton.href}
onClick={primaryButton.onClick}
>
{primaryButton.label}
</Button>
)}
{secondaryButton && (
<Button
// forceColor - prop to be added once feature two column is merged
variant="secondary"
color="black"
href={secondaryButton.href}
onClick={secondaryButton.onClick}
>
{secondaryButton.label}
</Button>
)}
</div>
)}
<PageGridCol span={span} as="li" className={classNames}>
{/* Text section */}
<div className="bds-card-stat__text">
<div className="bds-card-stat__statistic">
{statistic}{superscript && <sup className={isNumericSuperscript ? 'bds-card-stat__superscript--numeric' : ''}>{superscript}</sup>}</div>
<div className="body-r">{label}</div>
</div>
{/* Buttons section */}
{hasButtons && (
<div className="bds-card-stat__buttons">
{primaryButton && (
<Button
forceColor
variant="primary"
color="black"
href={primaryButton.href}
onClick={primaryButton.onClick}
>
{primaryButton.label}
</Button>
)}
{secondaryButton && (
<Button
forceColor
variant="secondary"
color="black"
href={secondaryButton.href}
onClick={secondaryButton.onClick}
>
{secondaryButton.label}
</Button>
)}
</div>
)}
</PageGridCol>
);
};

View File

@@ -92,9 +92,9 @@ CardTextIconCard displays an icon at the top, followed by a heading and descript
| Breakpoint | Icon Size | Padding | Gap |
|------------|-----------|---------|-----|
| Base (< 576px) | 56px | 16px | 16px |
| MD (576px - 991px) | 60px | 20px | 20px |
| LG (≥ 992px) | 64px | 24px | 24px |
| Base (< 576px) | 32px | 16px | 16px |
| MD (576px - 991px) | 36px | 20px | 20px |
| LG (≥ 992px) | 40px | 32px | 24px |
## Files

View File

@@ -0,0 +1,188 @@
// BDS Video Component Styles
// Brand Design System - Flexible video component supporting native video,
// YouTube/Vimeo/Wistia embeds, and cover image + modal playback.
//
// .bds-video - Container
// .bds-video__element - Native video element
// .bds-video__iframe - Embed iframe
// .bds-video__cover-button - Clickable cover (opens modal)
// .bds-video__cover-image - Cover image
// .bds-video__play-icon - Play overlay icon
// .bds-video-modal - Full-screen modal overlay
// .bds-video-modal__backdrop - Backdrop (click to close)
// .bds-video-modal__content - Modal content wrapper
// .bds-video-modal__close - Close button
// .bds-video-modal__video - Video/iframe container in modal
.bds-video {
width: 100%;
height: auto;
overflow: hidden;
position: relative;
&--aspect-16-9 {
aspect-ratio: 16 / 9;
}
&--aspect-4-3 {
aspect-ratio: 4 / 3;
}
&--aspect-1-1 {
aspect-ratio: 1 / 1;
}
&__inner {
width: 100%;
height: 100%;
position: relative;
}
&__element {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
display: block;
}
&__iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
&__cover-button {
position: relative;
display: block;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
border: none;
background: none;
cursor: pointer;
overflow: hidden;
}
&__cover-image {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
display: block;
transition: transform 0.3s ease;
}
&__cover-button:hover &__cover-image {
transform: scale(1.02);
}
&__play-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
svg {
width: 64px;
height: 64px;
transition: transform 0.2s ease;
}
}
&__cover-button:hover &__play-icon svg {
transform: scale(1.1);
}
}
// =============================================================================
// Modal
// =============================================================================
.bds-video-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1200;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.bds-video-modal__backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
cursor: pointer;
}
.bds-video-modal__content {
position: relative;
top: 0;
left: 0;
width: 100%;
max-width: 90vw;
max-height: 90vh;
aspect-ratio: 16 / 9;
z-index: 2;
}
.bds-video-modal__close {
position: absolute;
top: -2.5rem;
right: 0;
width: 2rem;
height: 2rem;
padding: 0;
border: none;
background: transparent;
color: white;
font-size: 2rem;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.2s;
&:hover {
opacity: 0.8;
}
span {
line-height: 1;
}
}
.bds-video-modal__video {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
border-radius: 4px;
.bds-video__element,
.bds-video__iframe,
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
}

View File

@@ -0,0 +1,278 @@
import React, { useCallback, useEffect, useRef } from 'react';
import clsx from 'clsx';
import type { DesignConstrainedVideoProps } from 'shared/utils/types';
/** Native HTML video source */
export type VideoSourceNative = {
type: 'native';
props: DesignConstrainedVideoProps;
};
/**
* Embed source - raw HTML iframe code from YouTube/Vimeo/Wistia
*
* ⚠️ **SECURITY WARNING**: embedCode uses regex-based parsing which can be bypassed
* with malformed HTML (e.g., newlines in attributes, missing quotes, unusual spacing).
* **Always prefer `embedUrl` over `embedCode` when possible.**
*
* Only use embedCode when:
* - The HTML comes from a trusted source you control
* - You cannot extract the URL beforehand
*
* The component validates against TRUSTED_EMBED_ORIGINS, but regex parsing is not
* foolproof against all HTML variations.
*/
export type VideoSourceEmbedCode = {
type: 'embed';
embedCode: string;
embedUrl?: never;
};
/**
* Embed source - direct embed URL (safer, preferred)
*
* ✅ **RECOMMENDED**: This is the preferred and safer method for embedding videos.
* Directly validates the URL against TRUSTED_EMBED_ORIGINS without HTML parsing.
*/
export type VideoSourceEmbedUrl = {
type: 'embed';
embedUrl: string;
embedCode?: never;
};
export type VideoSource =
| VideoSourceNative
| VideoSourceEmbedCode
| VideoSourceEmbedUrl;
export interface VideoProps {
/**
* Video source: native HTML video or embed (YouTube/Vimeo/Wistia)
*
* **For embeds, prefer `embedUrl` over `embedCode`:**
* - ✅ `embedUrl`: Direct URL validation (safer, recommended)
* - ⚠️ `embedCode`: Regex-based HTML parsing (can be bypassed, use with caution)
*/
source: VideoSource;
/** Optional cover image - when provided, video shows in modal on click */
coverImage?: {
src: string;
alt: string;
};
/** Aspect ratio for container (default 16/9) */
aspectRatio?: '16/9' | '4/3' | '1/1';
/** Additional className for container */
className?: string;
}
/** Trusted embed origins for embedCode sanitization */
const TRUSTED_EMBED_ORIGINS = [
'youtube.com',
'www.youtube.com',
'youtube-nocookie.com',
'www.youtube-nocookie.com',
'vimeo.com',
'player.vimeo.com',
'fast.wistia.net',
'fast.wistia.com',
];
function isTrustedEmbedUrl(url: string): boolean {
try {
const parsed = new URL(url);
const host = parsed.hostname.toLowerCase();
return TRUSTED_EMBED_ORIGINS.some(
(origin) => host === origin || host.endsWith('.' + origin)
);
} catch {
return false;
}
}
/**
* Extract iframe src from embed code if from trusted origin
*
* ⚠️ **SECURITY WARNING**: This regex-based parser is NOT foolproof and can be bypassed
* with malformed HTML. Known bypass vectors include:
* - Newlines or unusual whitespace in attributes
* - Missing or unusual quote characters
* - HTML comments or CDATA sections
* - Encoded characters or HTML entities
*
* This function should only be used as a fallback when embedUrl is not available.
* Always prefer using embedUrl directly when possible.
*
* @param embedCode - Raw HTML iframe embed code (from trusted sources only)
* @returns Extracted URL if valid and from trusted origin, null otherwise
*/
function extractEmbedUrlFromCode(embedCode: string): string | null {
const iframeMatch = embedCode.match(/<iframe[^>]+src=["']([^"']+)["']/i);
if (!iframeMatch) return null;
const url = iframeMatch[1];
return isTrustedEmbedUrl(url) ? url : null;
}
export const Video = React.forwardRef<HTMLDivElement, VideoProps>(
(props, ref) => {
const {
source,
coverImage,
aspectRatio = '16/9',
className,
} = props;
const [isModalOpen, setIsModalOpen] = React.useState(false);
const closeButtonRef = useRef<HTMLButtonElement>(null);
const previousActiveElementRef = useRef<HTMLElement | null>(null);
const closeModal = useCallback(() => {
setIsModalOpen(false);
}, []);
useEffect(() => {
if (!isModalOpen) return;
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') closeModal();
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [isModalOpen, closeModal]);
// Focus trap: capture previous focus, focus close button, restore on close
useEffect(() => {
if (isModalOpen) {
previousActiveElementRef.current = document.activeElement as HTMLElement | null;
closeButtonRef.current?.focus();
} else {
previousActiveElementRef.current?.focus();
}
}, [isModalOpen]);
const renderVideoContent = () => {
if (source.type === 'native') {
return (
<video
{...source.props}
className="bds-video__element"
/>
);
}
// Embed: prefer embedUrl, else extract from embedCode (trusted origins only)
let embedUrl: string | null = null;
if ('embedUrl' in source && source.embedUrl) {
embedUrl = isTrustedEmbedUrl(source.embedUrl)
? source.embedUrl
: null;
} else if ('embedCode' in source && source.embedCode) {
embedUrl = extractEmbedUrlFromCode(source.embedCode);
}
if (embedUrl) {
return (
<iframe
src={embedUrl}
title="Video"
className="bds-video__iframe"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
);
}
return null;
};
const videoContent = renderVideoContent();
if (!videoContent) return null;
const containerClass = clsx(
'bds-video',
`bds-video--aspect-${aspectRatio.replace('/', '-')}`,
className
);
if (coverImage) {
return (
<>
<div ref={ref} className={containerClass}>
<button
type="button"
className="bds-video__cover-button"
onClick={() => setIsModalOpen(true)}
aria-label="Play video"
>
<img
src={coverImage.src}
alt={coverImage.alt}
className="bds-video__cover-image"
/>
<span className="bds-video__play-icon" aria-hidden>
<svg
width="64"
height="64"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="32"
cy="32"
r="32"
fill="rgba(0,0,0,0.5)"
/>
<path
d="M26 20v24l18-12-18-12z"
fill="white"
/>
</svg>
</span>
</button>
</div>
{isModalOpen && (
<div
className="bds-video-modal"
role="dialog"
aria-modal="true"
aria-label="Video"
>
<div
className="bds-video-modal__backdrop"
onClick={closeModal}
aria-hidden
/>
<div className="bds-video-modal__content">
<button
ref={closeButtonRef}
type="button"
className="bds-video-modal__close"
onClick={closeModal}
aria-label="Close video"
>
<span aria-hidden>&times;</span>
</button>
<div className="bds-video-modal__video">
{renderVideoContent()}
</div>
</div>
</div>
)}
</>
);
}
return (
<div ref={ref} className={containerClass}>
<div className="bds-video__inner">{videoContent}</div>
</div>
);
}
);
Video.displayName = 'Video';
export default Video;

View File

@@ -0,0 +1 @@
export { Video, type VideoProps, type VideoSource } from './Video';

View File

@@ -1,104 +0,0 @@
// BDS CardStats Pattern Styles
// Brand Design System - Section with heading, description, and grid of CardStat components
//
// Naming Convention: BEM with 'bds' namespace
// .bds-card-stats - Base section container
// .bds-card-stats__header - Header wrapper for heading and description
// .bds-card-stats__heading - Section heading (uses .h-md)
// .bds-card-stats__description - Section description (uses .body-l)
// .bds-card-stats__cards - Cards grid container
// .bds-card-stats__card-wrapper - Individual card wrapper
//
// Design tokens from Figma:
// Light Mode:
// - Background: White (#FFFFFF)
// - Heading: Neutral Black (#141414) → $black
// - Description: Neutral Black (#141414) → $black
//
// Dark Mode:
// - Background: transparent (inherits page background)
// - Heading: Neutral White (#FFFFFF) → $white
// - Description: Neutral White (#FFFFFF) → $white
//
// - Header content max-width: 808px (approximately 8 columns at desktop)
// - Gap between heading and description: 16px
// - Gap between cards: 8px (matches $bds-grid-gutter)
// Color tokens - Light Mode (from Figma: node 32051-2839)
$bds-card-stats-heading-light: $black; // --neutral/black (#141414)
$bds-card-stats-description-light: $black; // --neutral/black (#141414)
// Color tokens - Dark Mode (from Figma: node 32051-2524)
$bds-card-stats-bg-dark: transparent; // Inherits page background
$bds-card-stats-heading-dark: $white; // --neutral/white (#FFFFFF)
$bds-card-stats-description-dark: $white; // --neutral/white (#FFFFFF)
// Spacing - Header gap (between heading and description)
// Note: Uses centralized spacing tokens from _spacing.scss.
$bds-card-stats-header-gap-base: $bds-space-sm; // 8px - spacing('sm')
$bds-card-stats-header-gap-lg: $bds-space-lg; // 16px - spacing('lg')
// Spacing - Section gap (between header and cards)
$bds-card-stats-section-gap-sm: $bds-space-2xl; // 24px - spacing('2xl')
$bds-card-stats-section-gap-md: $bds-space-3xl; // 32px - spacing('3xl')
$bds-card-stats-section-gap-lg: $bds-space-4xl; // 40px - spacing('4xl')
// Spacing - Section padding
$bds-card-stats-padding-y-sm: $bds-space-2xl; // 24px - spacing('2xl')
$bds-card-stats-padding-y-md: $bds-space-3xl; // 32px - spacing('3xl')
$bds-card-stats-padding-y-lg: $bds-space-4xl; // 40px - spacing('4xl')
// =============================================================================
// Base Section Styles
// =============================================================================
.bds-card-stats {
// Vertical padding
padding-top: $bds-card-stats-padding-y-sm;
padding-bottom: $bds-card-stats-padding-y-sm;
@include media-breakpoint-up(md) {
padding-top: $bds-card-stats-padding-y-md;
padding-bottom: $bds-card-stats-padding-y-md;
}
@include media-breakpoint-up(lg) {
padding-top: $bds-card-stats-padding-y-lg;
padding-bottom: $bds-card-stats-padding-y-lg;
}
}
// =============================================================================
// Header Styles
// =============================================================================
.bds-card-stats__header {
display: flex;
flex-direction: column;
gap: $bds-card-stats-header-gap-base;
margin-bottom: $bds-card-stats-section-gap-sm;
@include media-breakpoint-up(md) {
margin-bottom: $bds-card-stats-section-gap-md;
}
@include media-breakpoint-up(lg) {
gap: $bds-card-stats-header-gap-lg;
margin-bottom: $bds-card-stats-section-gap-lg;
}
}
// =============================================================================
// Dark Mode Styles
// =============================================================================
html.dark {
.bds-card-stats__heading,
.bds-card-stats__description {
color: $white;
}
}

View File

@@ -1,3 +0,0 @@
export { CardStats, type CardStatsProps, type CardStatsCardConfig } from './CardStats';
export { default } from './CardStats';

View File

@@ -5,34 +5,36 @@
// .bds-link-text-card - Base card container with border-top divider
// .bds-link-text-card__header - Header section (number + heading)
// .bds-link-text-card__content - Content section (description + buttons)
//
// Note: Uses centralized spacing tokens from _spacing.scss.
// =============================================================================
// Design Tokens
// Design Tokens (from _spacing.scss)
// =============================================================================
// Header gap (between number and heading)
$bds-link-text-card-header-gap-base: 8px;
$bds-link-text-card-header-gap-md: 12px;
$bds-link-text-card-header-gap-lg: 16px;
$bds-link-text-card-header-gap-base: $bds-space-sm; // 8px - spacing('sm')
$bds-link-text-card-header-gap-md: $bds-space-md; // 12px - spacing('md')
$bds-link-text-card-header-gap-lg: $bds-space-lg; // 16px - spacing('lg')
// Content gap (between description and buttons)
$bds-link-text-card-content-gap-base: 16px;
$bds-link-text-card-content-gap-md: 24px;
$bds-link-text-card-content-gap-lg: 32px;
$bds-link-text-card-content-gap-base: $bds-space-lg; // 16px - spacing('lg')
$bds-link-text-card-content-gap-md: $bds-space-2xl; // 24px - spacing('2xl')
$bds-link-text-card-content-gap-lg: $bds-space-3xl; // 32px - spacing('3xl')
// Card gap (between header and content sections)
$bds-link-text-card-gap-base: 24px;
$bds-link-text-card-gap-md: 32px;
$bds-link-text-card-gap-lg: 40px;
$bds-link-text-card-gap-base: $bds-gap-section-sm; // 24px - spacing('2xl')
$bds-link-text-card-gap-md: $bds-gap-section-md; // 32px - spacing('3xl')
$bds-link-text-card-gap-lg: $bds-gap-section-lg; // 40px - spacing('4xl')
// Padding
$bds-link-text-card-padding-top-base: 8px;
$bds-link-text-card-padding-top-md: 12px;
$bds-link-text-card-padding-top-lg: 16px;
$bds-link-text-card-padding-top-base: $bds-space-sm; // 8px - spacing('sm')
$bds-link-text-card-padding-top-md: $bds-space-md; // 12px - spacing('md')
$bds-link-text-card-padding-top-lg: $bds-space-lg; // 16px - spacing('lg')
$bds-link-text-card-padding-bottom-base: 24px;
$bds-link-text-card-padding-bottom-md: 32px;
$bds-link-text-card-padding-bottom-lg: 40px;
$bds-link-text-card-padding-bottom-base: $bds-space-2xl; // 24px - spacing('2xl')
$bds-link-text-card-padding-bottom-md: $bds-space-3xl; // 32px - spacing('3xl')
$bds-link-text-card-padding-bottom-lg: $bds-space-4xl; // 40px - spacing('4xl')
// Border
$bds-link-text-card-border-width: 1px;
@@ -98,6 +100,9 @@ $bds-link-text-card-content-width-md: calc(75% - 2px);
p {
color: $bds-link-text-card-number-color;
}
@include bds-theme-mode(dark) {
color: $white;
}
}
// =============================================================================
@@ -121,4 +126,7 @@ $bds-link-text-card-content-width-md: calc(75% - 2px);
p {
color: $bds-link-text-card-number-color;
}
@include bds-theme-mode(dark) {
color: $white;
}
}

View File

@@ -0,0 +1,50 @@
// BDS SectionHeader Pattern Styles
// Brand Design System - Consolidated section header (heading + description)
//
// Naming Convention: BEM with 'bds' namespace
// .bds-section-header - Header wrapper for heading and description
// .bds-section-header__heading - Section heading (uses .h-md)
// .bds-section-header__description - Section description (uses .body-l)
//
// Design tokens from _spacing.scss:
// - Gap between heading and description: $bds-gap-header-*
// - Light/Dark mode colors: $black / $white
// =============================================================================
// Header Section
// =============================================================================
.bds-section-header {
display: flex;
flex-direction: column;
gap: $bds-gap-header-sm;
margin-bottom: $bds-gap-section-sm;
@include media-breakpoint-up(md) {
gap: $bds-gap-header-md;
margin-bottom: $bds-gap-section-md;
}
@include media-breakpoint-up(lg) {
gap: $bds-gap-header-lg;
margin-bottom: $bds-gap-section-lg;
}
@include bds-theme-mode(light) {
color: $black;
}
@include bds-theme-mode(dark) {
color: $white;
}
}
.bds-section-header__heading {
margin: 0;
// Typography handled by .h-md class from _font.scss
}
.bds-section-header__description {
margin: 0;
// Typography handled by .body-l class from _font.scss
}

View File

@@ -0,0 +1,111 @@
import React from 'react';
import clsx from 'clsx';
import { PageGrid } from '../../components/PageGrid/page-grid';
import type { ResponsiveValue, PageGridSpanValue } from '../../components/PageGrid/page-grid';
import { isEnvironment } from '../../utils/helpers';
const DEFAULT_SPAN = {
base: 'fill' as const,
md: 6,
lg: 8,
};
export interface SectionHeaderProps {
/** Section heading text */
heading?: React.ReactNode;
/** Section description text */
description?: React.ReactNode;
/** Polymorphic heading element - h1 through h6 */
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
/** PageGrid.Col span - defaults to { base: 'fill', md: 6, lg: 8 } */
span?: ResponsiveValue<PageGridSpanValue>;
/** Optional slot for trailing content (e.g. ButtonGroup) */
children?: React.ReactNode;
/** Additional CSS classes for the header wrapper */
className?: string;
}
/**
* SectionHeader - Consolidated section header pattern
*
* Renders a PageGrid.Row + Col with heading (polymorphic h1-h6) and optional description.
* Used across CardsFeatured, StandardCardGroupSection, CardsIconGrid, and other sections.
*
* **Behavior:**
* - Returns `null` if no content is provided (no heading, description, or children)
* - Logs a development warning when returning null to help catch missing props
* - At least one of `heading`, `description`, or `children` should be provided
*
* @example
* // Typical usage with heading and description
* <SectionHeader
* heading="Our Features"
* description="Explore what we offer"
* />
*
* @example
* // With custom heading level
* <SectionHeader
* heading="Main Title"
* as="h1"
* description="Subtitle text"
* />
*
* @example
* // With children (e.g., ButtonGroup)
* <SectionHeader heading="Products">
* <ButtonGroup buttons={[...]} />
* </SectionHeader>
*/
export const SectionHeader = React.forwardRef<HTMLDivElement, SectionHeaderProps>(
(props, ref) => {
const {
heading,
description,
as = 'h2',
span = DEFAULT_SPAN,
children,
className,
} = props;
const hasContent = heading || description || children;
if (!hasContent) {
if (isEnvironment(["development", "test"])) {
console.warn(
'SectionHeader: No content provided. Component requires at least one of: heading, description, or children. ' +
'Returning null - this may indicate a missing prop or data issue.'
);
}
return null;
}
const HeadingTag = as;
return (
<PageGrid.Row>
<PageGrid.Col span={span}>
<div
ref={ref}
className={clsx('bds-section-header', className)}
>
{heading != null && heading !== '' && (
<HeadingTag className="bds-section-header__heading h-md">
{heading}
</HeadingTag>
)}
{description != null && description !== '' && (
<p className="bds-section-header__description body-l">
{description}
</p>
)}
{children}
</div>
</PageGrid.Col>
</PageGrid.Row>
);
}
);
SectionHeader.displayName = 'SectionHeader';
export default SectionHeader;

View File

@@ -0,0 +1 @@
export { SectionHeader, type SectionHeaderProps } from './SectionHeader';

View File

@@ -1,7 +1,7 @@
import React from 'react';
import clsx from 'clsx';
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
import { ButtonGroup, ButtonConfig, validateButtonGroup } from 'shared/patterns/ButtonGroup/ButtonGroup';
export interface CalloutMediaBannerProps {
/** Color variant - determines background color (ignored if backgroundImage is provided) */
@@ -12,6 +12,8 @@ export interface CalloutMediaBannerProps {
textColor?: 'white' | 'black';
/** Main heading text */
heading?: string;
/** Heading element type - h1 through h6 (defaults to h6) */
headingAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
/** Subheading/description text */
subheading: string;
/** Button configurations (1-2 buttons supported) */
@@ -22,12 +24,12 @@ export interface CalloutMediaBannerProps {
/**
* CalloutMediaBanner Component
*
*
* A full-width banner component featuring a heading, subheading, and optional action buttons.
* Supports 5 color variants or a custom background image. Responsive across mobile, tablet, and desktop.
*
*
* @example
* // Color variant
* // Color variant with default h6 heading
* <CalloutMediaBanner
* variant="green"
* heading="The Compliant Ledger Protocol"
@@ -39,6 +41,16 @@ export interface CalloutMediaBannerProps {
* />
*
* @example
* // With custom heading level (h1)
* <CalloutMediaBanner
* variant="green"
* heading="The Compliant Ledger Protocol"
* headingAs="h1"
* subheading="A decentralized public Layer 1 blockchain..."
* buttons={[{ label: "Get Started", href: "/docs" }]}
* />
*
* @example
* // With background image (white text - default)
* <CalloutMediaBanner
* backgroundImage="/images/hero-bg.jpg"
@@ -53,6 +65,7 @@ export interface CalloutMediaBannerProps {
* backgroundImage="/images/light-hero-bg.jpg"
* textColor="black"
* heading="Build on XRPL"
* headingAs="h2"
* subheading="Start building your next project"
* buttons={[{ label: "Start Building", onClick: handleClick }]}
* />
@@ -62,6 +75,7 @@ export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
backgroundImage,
textColor = 'white',
heading,
headingAs = 'h6',
subheading,
buttons,
className = '',
@@ -96,6 +110,9 @@ export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
? { backgroundImage: `url(${backgroundImage})` }
: {};
// Create the heading element dynamically
const HeadingElement = headingAs;
return (
<PageGrid containerType="wide">
<PageGridRow className={classNames} style={inlineStyle}>
@@ -103,7 +120,7 @@ export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
<div className="bds-callout-media-banner__content">
{/* Text Content */}
<div className="bds-callout-media-banner__text">
{heading && <h2 className="bds-callout-media-banner__heading">{heading}</h2>}
{heading && <HeadingElement className="bds-callout-media-banner__heading">{heading}</HeadingElement>}
<p className="bds-callout-media-banner__subheading">{subheading}</p>
</div>

View File

@@ -94,7 +94,7 @@ interface CalloutMediaBannerProps {
### Basic Usage with Color Variant
```tsx
import { CalloutMediaBanner } from 'shared/patterns/CalloutMediaBanner';
import { CalloutMediaBanner } from 'shared/sections/CalloutMediaBanner';
<CalloutMediaBanner
variant="green"
@@ -313,7 +313,7 @@ $gray-600 // Gray (dark)
- **Figma Design**: [Callout - Media Banner](https://www.figma.com/design/i4OuOX6QSBauMaJE4iY4kV/Callout---Media-Banner?node-id=1-2&m=dev)
- **Showcase Page**: `/about/callout-media-banner-showcase.page.tsx`
- **Component Location**: `shared/patterns/CalloutMediaBanner/`
- **Component Location**: `shared/sections/CalloutMediaBanner/`
## Version History

View File

@@ -14,7 +14,7 @@ A section pattern that displays a heading, optional description, and a responsiv
## Usage
```tsx
import { CardStats } from 'shared/patterns/CardStats';
import { CardStats } from 'shared/sections/CardStatsList';
<CardStats
heading="Blockchain Trusted at Scale"
@@ -136,7 +136,7 @@ Uses breakpoints from `styles/_breakpoints.scss`:
- **Figma Design**: [Section Cards - Stats](https://www.figma.com/design/drnQQXnK9Q67MTPPKQsY9l/Section-Cards---Stats?node-id=32051-2839&m=dev)
- **Showcase Page**: `/about/card-stats-showcase`
- **Pattern Location**: `shared/patterns/CardStats/`
- **Pattern Location**: `shared/sections/CardStatsList/`
- **Component Used**: `shared/components/CardStat/`
## Accessibility

View File

@@ -0,0 +1,48 @@
// BDS CardStats Pattern Styles
// Brand Design System - Section with heading, description, and grid of CardStat components
//
// Naming Convention: BEM with 'bds' namespace
// .bds-card-stats - Base section container
//
// Design tokens from Figma:
// Light Mode:
// - Background: White (#FFFFFF)
// - Heading: Neutral Black (#141414) → $black
// - Description: Neutral Black (#141414) → $black
//
// Dark Mode:
// - Background: transparent (inherits page background)
// - Heading: Neutral White (#FFFFFF) → $white
// - Description: Neutral White (#FFFFFF) → $white
//
// - Header content max-width: 808px (approximately 8 columns at desktop)
// - Gap between heading and description: 16px
// - Gap between cards: 8px (matches $bds-grid-gutter)
// Spacing - Section padding
$bds-card-stats-padding-y-sm: $bds-space-2xl; // 24px - spacing('2xl')
$bds-card-stats-padding-y-md: $bds-space-3xl; // 32px - spacing('3xl')
$bds-card-stats-padding-y-lg: $bds-space-4xl; // 40px - spacing('4xl')
// =============================================================================
// Base Section Styles
// =============================================================================
.bds-card-stats {
// Vertical padding
padding-top: $bds-card-stats-padding-y-sm;
padding-bottom: $bds-card-stats-padding-y-sm;
@include media-breakpoint-up(md) {
padding-top: $bds-card-stats-padding-y-md;
padding-bottom: $bds-card-stats-padding-y-md;
}
@include media-breakpoint-up(lg) {
padding-top: $bds-card-stats-padding-y-lg;
padding-bottom: $bds-card-stats-padding-y-lg;
}
}
// Header section uses SectionHeader component

View File

@@ -2,6 +2,7 @@ import React from 'react';
import clsx from 'clsx';
import { CardStat, CardStatProps } from '../../components/CardStat';
import { PageGrid } from '../../components/PageGrid/page-grid';
import { SectionHeader } from 'shared/patterns/SectionHeader';
/**
* Configuration for a single stat card in the CardStats pattern
@@ -72,20 +73,8 @@ export const CardStats = React.forwardRef<HTMLElement, CardStatsProps>(
className={clsx('bds-card-stats', className)}
{...rest}
>
<PageGrid.Row>
<PageGrid.Col span={{ base: 4, md: 6, lg: 8 }}>
{/* Header section */}
<div className="bds-card-stats__header">
<h2 className="mb-0 h-md">{heading}</h2>
{description && (
<p className="bmb-0 body-l">
{description}
</p>
)}
</div>
</PageGrid.Col>
</PageGrid.Row>
<PageGrid.Row>
<SectionHeader heading={heading} description={description} span={{ base: 4, md: 6, lg: 8 }} />
<PageGrid.Row as="ul">
{cards.map((cardConfig, index) => (
<CardStat
key={index}

View File

@@ -0,0 +1,2 @@
export { CardStats, type CardStatsProps, type CardStatsCardConfig } from './CardStatsList';
export { default } from './CardStatsList';

View File

@@ -13,7 +13,7 @@ A section pattern that displays a heading, description, and a responsive grid of
## Usage
```tsx
import { CardsFeatured } from 'shared/patterns/CardsFeatured';
import { CardsFeatured } from 'shared/sections/CardsFeatured';
<CardsFeatured
heading="Trusted by Leaders in Real-World Asset Tokenization"

View File

@@ -3,9 +3,6 @@
//
// Naming Convention: BEM with 'bds' namespace
// .bds-cards-featured - Base section container
// .bds-cards-featured__header - Header wrapper for heading and description
// .bds-cards-featured__heading - Section heading (uses .h-md)
// .bds-cards-featured__description - Section description (uses .body-l)
// .bds-cards-featured__cards - Cards grid container
// .bds-cards-featured__card-wrapper - Individual card wrapper
//
@@ -18,8 +15,7 @@
// - Heading: Neutral White (#FFFFFF) $white
// - Description: Neutral White (#FFFFFF) $white
//
// - Header content max-width: 808px (approximately 8 columns at desktop)
// - Gap between heading and description: 16px
// - Header: uses SectionHeader component
// - Gap between cards: 8px (matches $bds-grid-gutter)
// =============================================================================
@@ -28,15 +24,7 @@
// Note: Uses centralized spacing tokens from _spacing.scss.
// $bds-grid-gutter is defined in _spacing.scss (8px).
// Spacing - Header gap (between heading and description)
$bds-cards-featured-header-gap-sm: $bds-space-sm; // 8px - spacing('sm')
$bds-cards-featured-header-gap-md: $bds-space-sm; // 8px - spacing('sm')
$bds-cards-featured-header-gap-lg: $bds-space-lg; // 16px - spacing('lg')
// Spacing - Section gap (between header and cards)
$bds-cards-featured-section-gap-sm: $bds-space-2xl; // 24px - spacing('2xl')
$bds-cards-featured-section-gap-md: $bds-space-3xl; // 32px - spacing('3xl')
$bds-cards-featured-section-gap-lg: $bds-space-4xl; // 40px - spacing('4xl')
// Spacing - Section gap handled by SectionHeader margin-bottom
// Spacing - Cards gap
$bds-cards-featured-cards-gap-sm: $bds-space-5xl; // 48px - spacing('5xl')
@@ -50,14 +38,6 @@ $bds-cards-featured-padding-y-lg: $bds-space-8xl; // 80px - spacing('8xl')
// Spacing - Section padding (horizontal) - handled by PageGrid
// Colors - Light Mode (default)
$bds-cards-featured-heading-color: $black; // #141414 - Neutral black
$bds-cards-featured-description-color: $black; // #141414 - Neutral black
// Colors - Dark Mode (from Figma node 27020-3045)
$bds-cards-featured-heading-color-dark: $white; // #FFFFFF - Neutral white
$bds-cards-featured-description-color-dark: $white; // #FFFFFF - Neutral white
// =============================================================================
// Section Container
// =============================================================================
@@ -78,34 +58,6 @@ $bds-cards-featured-description-color-dark: $white; // #FFFFFF - Neutral white
}
}
// =============================================================================
// Header Section
// =============================================================================
.bds-cards-featured__header {
display: flex;
flex-direction: column;
gap: $bds-cards-featured-header-gap-sm;
@include media-breakpoint-up(md) {
gap: $bds-cards-featured-header-gap-md;
}
@include media-breakpoint-up(lg) {
gap: $bds-cards-featured-header-gap-lg;
}
}
.bds-cards-featured__heading {
margin: 0;
// Typography handled by .h-md class from _font.scss
}
.bds-cards-featured__description {
margin: 0;
// Typography handled by .body-l class from _font.scss
}
// =============================================================================
// Cards Grid
// =============================================================================
@@ -116,20 +68,17 @@ $bds-cards-featured-description-color-dark: $white; // #FFFFFF - Neutral white
column-gap: $bds-cards-featured-cards-gap-sm;
row-gap: $bds-space-5xl; // 48px - spacing('5xl')
width: 100%;
margin-top: $bds-cards-featured-section-gap-sm;
@include media-breakpoint-up(md) {
grid-template-columns: repeat(2, 1fr);
column-gap: $bds-cards-featured-cards-gap-md;
row-gap: 52px; // Non-standard value, kept as-is
margin-top: $bds-cards-featured-section-gap-md;
}
@include media-breakpoint-up(lg) {
grid-template-columns: repeat(3, 1fr);
column-gap: $bds-cards-featured-cards-gap-lg;
row-gap: 56px; // Non-standard value, kept as-is
margin-top: $bds-cards-featured-section-gap-lg;
}
}
@@ -152,27 +101,10 @@ html.light {
.bds-cards-featured {
background-color: $white;
}
.bds-cards-featured__heading {
color: $bds-cards-featured-heading-color;
}
.bds-cards-featured__description {
color: $bds-cards-featured-description-color;
}
}
// =============================================================================
// Dark Mode Styles (from Figma node 27020-3045)
// =============================================================================
html.dark {
.bds-cards-featured__heading {
color: $bds-cards-featured-heading-color-dark;
}
.bds-cards-featured__description {
color: $bds-cards-featured-description-color-dark;
}
}
// Header colors handled by SectionHeader component

View File

@@ -2,6 +2,7 @@ import React from 'react';
import clsx from 'clsx';
import { CardImage, CardImageProps } from '../../components/CardImage';
import { PageGrid } from '../../components/PageGrid/page-grid';
import { SectionHeader } from 'shared/patterns/SectionHeader';
import { getCardKey, isEnvironment } from '../../utils';
/**
@@ -72,21 +73,7 @@ export const CardsFeatured = React.forwardRef<HTMLElement, CardsFeaturedProps>(
{...rest}
>
<PageGrid>
{/* Header content row */}
<PageGrid.Row>
<PageGrid.Col
span={{
base: 'fill',
md: 6,
lg: 8,
}}
>
<div className="bds-cards-featured__header">
<h2 className="bds-cards-featured__heading h-md">{heading}</h2>
<p className="bds-cards-featured__description body-l">{description}</p>
</div>
</PageGrid.Col>
</PageGrid.Row>
<SectionHeader heading={heading} description={description} />
{/* Cards grid row */}
<PageGrid.Row>

View File

@@ -3,7 +3,6 @@
//
// Naming Convention: BEM with 'bds' namespace
// .bds-cards-icon-grid - Base section container
// .bds-cards-icon-grid__header - Header section (heading + description)
// .bds-cards-icon-grid__list - Grid list (ul as PageGrid.Row)
// =============================================================================
@@ -14,14 +13,6 @@ $bds-cards-icon-grid-padding-base: $bds-space-2xl;
$bds-cards-icon-grid-padding-md: $bds-space-3xl;
$bds-cards-icon-grid-padding-lg: $bds-space-4xl;
$bds-cards-icon-grid-header-gap-base: $bds-gap-header-sm;
$bds-cards-icon-grid-header-gap-md: $bds-gap-header-md;
$bds-cards-icon-grid-header-gap-lg: $bds-gap-header-lg;
$bds-cards-icon-grid-header-margin-base: $bds-gap-section-sm;
$bds-cards-icon-grid-header-margin-md: $bds-gap-section-md;
$bds-cards-icon-grid-header-margin-lg: $bds-gap-section-lg;
// =============================================================================
// Section Container
// =============================================================================
@@ -47,31 +38,6 @@ $bds-cards-icon-grid-header-margin-lg: $bds-gap-section-lg;
}
}
// =============================================================================
// Header Section
// =============================================================================
.bds-cards-icon-grid__header {
display: flex;
flex-direction: column;
gap: $bds-cards-icon-grid-header-gap-base;
margin-bottom: $bds-cards-icon-grid-header-margin-base;
@include media-breakpoint-up(md) {
gap: $bds-cards-icon-grid-header-gap-md;
margin-bottom: $bds-cards-icon-grid-header-margin-md;
}
@include media-breakpoint-up(lg) {
gap: $bds-cards-icon-grid-header-gap-lg;
margin-bottom: $bds-cards-icon-grid-header-margin-lg;
}
h2, p {
margin-bottom: 0;
}
}
// =============================================================================
// List Grid - Aspect Ratios
// =============================================================================

View File

@@ -1,6 +1,7 @@
import React from 'react';
import clsx from 'clsx';
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
import { PageGrid, PageGridRow } from 'shared/components/PageGrid/page-grid';
import { SectionHeader } from 'shared/patterns/SectionHeader';
import { CardTextIconCard, CardTextIconCardProps } from 'shared/components/CardTextIcon';
export interface CardsIconGridProps {
@@ -38,12 +39,7 @@ export const CardsIconGrid: React.FC<CardsIconGridProps> = ({
}) => {
return (
<PageGrid className={clsx('bds-cards-icon-grid', className)}>
<PageGridRow>
<PageGridCol className="bds-cards-icon-grid__header" span={{ base: 12, md: 6, lg: 8 }}>
<h2 className="h-md">{heading}</h2>
{description && <p className="body-l">{description}</p>}
</PageGridCol>
</PageGridRow>
<SectionHeader heading={heading} description={description} span={{ base: 12, md: 6, lg: 8 }} />
<PageGridRow as="ul" className="bds-cards-icon-grid__list list-none pl-0">
{cards.map((card, index) => (

View File

@@ -16,7 +16,7 @@ CardsIconGrid mirrors the LinkTextDirectory header structure and renders cards i
## Usage
```tsx
import { CardsIconGrid } from 'shared/patterns/CardsIconGrid';
import { CardsIconGrid } from 'shared/sections/CardsIconGrid';
<CardsIconGrid
heading="Explore Tools"

View File

@@ -3,7 +3,6 @@
//
// Naming Convention: BEM with 'bds' namespace
// .bds-cards-text-grid - Base section container
// .bds-cards-text-grid__header - Header section (heading + description)
// .bds-cards-text-grid__list - Grid list (ul as PageGrid.Row)
// =============================================================================
@@ -14,14 +13,6 @@ $bds-cards-text-grid-padding-base: $bds-space-2xl;
$bds-cards-text-grid-padding-md: $bds-space-3xl;
$bds-cards-text-grid-padding-lg: $bds-space-4xl;
$bds-cards-text-grid-header-gap-base: $bds-gap-header-sm;
$bds-cards-text-grid-header-gap-md: $bds-gap-header-md;
$bds-cards-text-grid-header-gap-lg: $bds-gap-header-lg;
$bds-cards-text-grid-header-margin-base: $bds-gap-section-sm;
$bds-cards-text-grid-header-margin-md: $bds-gap-section-md;
$bds-cards-text-grid-header-margin-lg: $bds-gap-section-lg;
// =============================================================================
// Section Container
// =============================================================================
@@ -47,31 +38,6 @@ $bds-cards-text-grid-header-margin-lg: $bds-gap-section-lg;
}
}
// =============================================================================
// Header Section
// =============================================================================
.bds-cards-text-grid__header {
display: flex;
flex-direction: column;
gap: $bds-cards-text-grid-header-gap-base;
margin-bottom: $bds-cards-text-grid-header-margin-base;
@include media-breakpoint-up(md) {
gap: $bds-cards-text-grid-header-gap-md;
margin-bottom: $bds-cards-text-grid-header-margin-md;
}
@include media-breakpoint-up(lg) {
gap: $bds-cards-text-grid-header-gap-lg;
margin-bottom: $bds-cards-text-grid-header-margin-lg;
}
h2, p {
margin-bottom: 0;
}
}
// =============================================================================
// List Grid - Aspect Ratios
// =============================================================================

View File

@@ -1,6 +1,7 @@
import React from 'react';
import clsx from 'clsx';
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
import { PageGrid, PageGridRow } from 'shared/components/PageGrid/page-grid';
import { SectionHeader } from 'shared/patterns/SectionHeader';
import { CardTextIconCard, CardTextIconCardProps } from 'shared/components/CardTextIcon';
export interface CardsTextGridProps {
@@ -38,12 +39,7 @@ export const CardsTextGrid: React.FC<CardsTextGridProps> = ({
}) => {
return (
<PageGrid className={clsx('bds-cards-text-grid', className)}>
<PageGridRow>
<PageGridCol className="bds-cards-text-grid__header" span={{ base: 12, md: 6, lg: 8 }}>
<h2 className="h-md">{heading}</h2>
{description && <p className="body-l">{description}</p>}
</PageGridCol>
</PageGridRow>
<SectionHeader heading={heading} description={description} span={{ base: 12, md: 6, lg: 8 }} />
<PageGridRow as="ul" className="bds-cards-text-grid__list list-none pl-0">
{cards.map((card, index) => (

View File

@@ -16,7 +16,7 @@ CardsTextGrid mirrors the LinkTextDirectory header structure and renders cards i
## Usage
```tsx
import { CardsTextGrid } from 'shared/patterns/CardsTextGrid';
import { CardsTextGrid } from 'shared/sections/CardsTextGrid';
<CardsTextGrid
heading="Explore Tools"

View File

@@ -15,7 +15,7 @@ A section pattern featuring a header with title and description, plus a 2×2 gri
## Usage
```tsx
import { CardsTwoColumn } from 'shared/patterns/CardsTwoColumn';
import { CardsTwoColumn } from 'shared/sections/CardsTwoColumn';
<CardsTwoColumn
title="The Future of Finance is Already Onchain"
@@ -153,7 +153,7 @@ Each color variant has four interactive states with a "window shade" hover anima
- **Figma Design**: [Section Cards - Two Column](https://www.figma.com/design/MP5gjNp7yPBnKBKleb8LRL/Section-Cards---Two-Column)
- **Showcase Page**: `/about/cards-two-column-showcase`
- **Component Location**: `shared/patterns/CardsTwoColumn/`
- **Component Location**: `shared/sections/CardsTwoColumn/`
## Version History

View File

@@ -16,7 +16,7 @@ A horizontal scrolling carousel pattern that displays `CardOffgrid` components w
## Usage
```tsx
import { CarouselCardList } from 'shared/patterns/CarouselCardList';
import { CarouselCardList } from 'shared/sections/CarouselCardList';
<CarouselCardList
variant="neutral"

View File

@@ -1,7 +1,7 @@
import React from 'react';
import clsx from 'clsx';
import { PageGrid } from '../../components/PageGrid/page-grid';
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
import { ButtonGroup, ButtonConfig, validateButtonGroup } from 'shared/patterns/ButtonGroup/ButtonGroup';
export interface FeatureSingleTopicProps {
/** Background variant for the title section

View File

@@ -15,7 +15,7 @@ A feature section pattern that pairs a title/description with a media element in
## Basic Usage
```tsx
import { FeatureSingleTopic } from 'shared/patterns/FeatureSingleTopic';
import { FeatureSingleTopic } from 'shared/sections/FeatureSingleTopic';
<FeatureSingleTopic
variant="default"
@@ -167,7 +167,7 @@ Full dark mode support with `html.dark` selector:
- **Figma Design**: [Section Feature - Single Topic](https://www.figma.com/design/sg6T5EptbN0V2olfCSHzcx/Section-Feature---Single-Topic?node-id=18030-2250&m=dev)
- **Showcase Page**: `/about/feature-single-topic-showcase`
- **Component Location**: `shared/patterns/FeatureSingleTopic/`
- **Component Location**: `shared/sections/FeatureSingleTopic/`
## Related Components

View File

@@ -210,7 +210,7 @@ FeatureTwoColumn
## Design References
- **Figma Design**: [Pattern - Feature - Two Column](https://www.figma.com/design/3tmqxMrEvOVvpYhgOCxv2D/Pattern-Feature---Two-Column?node-id=20017-3501&m=dev)
- **Component Location**: `shared/patterns/FeatureTwoColumn/`
- **Component Location**: `shared/sections/FeatureTwoColumn/`
- **Color Tokens**: `styles/_colors.scss`
- **Typography**: `styles/_font.scss`

View File

@@ -1,7 +1,7 @@
import React from 'react';
import clsx from 'clsx';
import { PageGrid } from '../../components/PageGrid/page-grid';
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
import { ButtonGroup, ButtonConfig, validateButtonGroup } from 'shared/patterns/ButtonGroup/ButtonGroup';
export interface FeatureTwoColumnLink {
/** Link label text */

View File

@@ -14,7 +14,7 @@ The FeaturedVideoHero component provides a structured hero section with:
## Basic Usage
```tsx
import { FeaturedVideoHero } from "shared/patterns/FeaturedVideoHero";
import { FeaturedVideoHero } from "shared/sections/FeaturedVideoHero";
function MyPage() {
return (
@@ -26,7 +26,7 @@ function MyPage() {
smart contracts.
</p>
}
callsToAction={[{ children: "Get Started", href: "/docs" }]}
links={[{ label: "Get Started", href: "/docs" }]}
videoElement={{
src: "/video/intro.mp4",
autoPlay: true,
@@ -45,19 +45,18 @@ function MyPage() {
| --------------- | --------------------------------- | -------- | ---------------------------------------------------------------------------- |
| `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. |
| `links` | `DesignConstrainedLink[]` | No | Array of `{ label, href }` for ButtonGroup. Omit to hide button 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
### Links (ButtonGroup)
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:
The `links` prop is optional. When provided, at least one non-empty link (`label` and `href`) is required to show the button section. Uses `{ label, href }` format for consistent ButtonGroup rendering; `variant` and `color` are set by the component:
- **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.
- **First link**: `variant="primary"`, `color="green"`, `forceColor={true}`
- **Second link**: `variant="tertiary"`, `color="green"`, `forceColor={true}`
- Max 2 links supported (ButtonGroup validation)
### Video Element
@@ -77,9 +76,9 @@ The video is rendered with `object-fit: cover` and a 16:9 aspect ratio container
<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" },
links={[
{ label: "Get Started", href: "/docs" },
{ label: "Learn More", href: "/about" },
]}
videoElement={{
src: "/video/tokenization.mp4",
@@ -96,7 +95,7 @@ The video is rendered with `object-fit: cover` and a 16:9 aspect ratio container
```tsx
<FeaturedVideoHero
headline="Headline Only"
callsToAction={[{ children: "Get Started", href: "/docs" }]}
links={[{ label: "Get Started", href: "/docs" }]}
videoElement={{
src: "/video/intro.mp4",
autoPlay: true,
@@ -113,7 +112,7 @@ The video is rendered with `object-fit: cover` and a 16:9 aspect ratio container
<FeaturedVideoHero
headline="Watch and Learn"
subtitle="Explore our video tutorials and guides."
callsToAction={[{ children: "Watch Tutorials", href: "/tutorials" }]}
links={[{ label: "Watch Tutorials", href: "/tutorials" }]}
videoElement={{
src: "/video/intro.mp4",
autoPlay: false,
@@ -129,7 +128,7 @@ The video is rendered with `object-fit: cover` and a 16:9 aspect ratio container
## 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.
- **Optional props**: `subtitle`, `links`. Omit `links` or pass an empty array to hide the button section.
## Responsive Behavior

View File

@@ -1,15 +1,12 @@
import React, { forwardRef, memo, useEffect } from "react";
import clsx from "clsx";
import { PageGrid } from "shared/components/PageGrid/page-grid";
import { Button } from "shared/components/Button/Button";
import {
isEmpty,
DesignConstrainedButtonProps,
isEnvironment,
} from "shared/utils";
import { ButtonGroup, validateButtonGroup } from "shared/patterns/ButtonGroup/ButtonGroup";
import { isEmpty, isEnvironment } from "shared/utils";
import {
DesignConstrainedImageProps,
DesignConstrainedVideoProps,
DesignConstrainedLinksProps,
} from "shared/utils/types";
/**
@@ -45,12 +42,11 @@ export type HeaderHeroMedia =
| VideoMediaProps
| CustomMediaProps;
export interface HeaderHeroPrimaryMediaProps extends React.ComponentPropsWithoutRef<"header"> {
export interface HeaderHeroPrimaryMediaProps extends React.ComponentPropsWithoutRef<"header">, DesignConstrainedLinksProps {
/** Hero title text (display-md typography) */
headline: React.ReactNode;
/** Hero subtitle text (subhead-sm-l typography) */
subtitle: React.ReactNode;
callsToAction: [DesignConstrainedButtonProps, DesignConstrainedButtonProps?];
/** Media element - supports image, video, or custom React element */
media: HeaderHeroMedia;
}
@@ -110,10 +106,14 @@ const HeaderHeroPrimaryMedia = forwardRef<
HTMLElement,
HeaderHeroPrimaryMediaProps
>((props, ref) => {
const { headline, subtitle, callsToAction, media, className, ...restProps } =
const { headline, subtitle, links, media, className, ...restProps } =
props;
const [primaryCta, secondaryCta] = callsToAction;
const buttonValidation = validateButtonGroup(
(links ?? []).map((l) => ({ label: l.label, href: l.href, forceColor: true })),
2,
isEnvironment(["development", "test"])
);
// Headline is critical - exit early if missing
if (!headline) {
@@ -132,7 +132,7 @@ const HeaderHeroPrimaryMedia = forwardRef<
const propsToValidate = {
subtitle,
callsToAction,
links,
media,
};
@@ -141,7 +141,7 @@ const HeaderHeroPrimaryMedia = forwardRef<
console.warn(`${key} is required for HeaderHeroPrimaryMedia`);
}
});
}, [subtitle, callsToAction, media]);
}, [subtitle, links, media]);
return (
<header
@@ -166,28 +166,14 @@ const HeaderHeroPrimaryMedia = forwardRef<
{subtitle}
</div>
)}
{(!isEmpty(primaryCta) || !isEmpty(secondaryCta)) && (
{buttonValidation.hasButtons && (
<div className="bds-header-hero-primary-media__cta-buttons">
{!isEmpty(primaryCta) && (
<Button
{...primaryCta!}
variant="primary"
color="green"
showIcon={true}
/>
)}
{!isEmpty(secondaryCta) && (
<Button
{...secondaryCta!}
className={clsx(
"bds-header-hero-primary-media__cta-button-tertiary",
secondaryCta?.className,
)}
variant="tertiary"
color="green"
showIcon={true}
/>
)}
<ButtonGroup
buttons={buttonValidation.buttons}
color="green"
forceColor
gap="small"
/>
</div>
)}
</div>

View File

@@ -14,14 +14,14 @@ The HeaderHeroPrimaryMedia component provides a structured hero section with:
## Basic Usage
```tsx
import HeaderHeroPrimaryMedia from "shared/patterns/HeaderHeroPrimaryMedia/HeaderHeroPrimaryMedia";
import HeaderHeroPrimaryMedia from "shared/sections/HeaderHeroPrimaryMedia/HeaderHeroPrimaryMedia";
function MyPage() {
return (
<HeaderHeroPrimaryMedia
headline="Build on XRPL"
subtitle="Start developing today with our comprehensive developer tools."
callsToAction={[{ children: "Get Started", href: "/docs" }]}
links={[{ label: "Get Started", href: "/docs" }]}
media={{
type: "image",
src: "/img/hero.png",
@@ -38,19 +38,18 @@ function MyPage() {
| --------------- | ------------------------------ | -------- | ------------------------------------------------------------ |
| `headline` | `React.ReactNode` | Yes | Hero headline text (display-md typography) |
| `subtitle` | `React.ReactNode` | Yes | Hero subtitle text (label-l typography) |
| `callsToAction` | `[ButtonProps, ButtonProps?]` | Yes | Array with primary CTA (required) and optional secondary CTA |
| `links` | `DesignConstrainedLink[]` | No | Array of `{ label, href }` for ButtonGroup |
| `media` | `HeaderHeroMedia` | Yes | Media element (image, video, or custom) |
| `className` | `string` | No | Additional CSS classes for the header element |
| `...rest` | `HTMLHeaderElement attributes` | No | Any other HTML header attributes |
### Calls to Action
### Links (ButtonGroup)
The `callsToAction` prop accepts Button component props, but `variant` and `color` are automatically set:
The `links` prop accepts an array of `{ label, href }` objects for consistent ButtonGroup rendering; `variant` and `color` are set by the component:
- **Primary CTA**: `variant="primary"`, `color="green"`
- **Secondary CTA**: `variant="tertiary"`, `color="green"`
All other Button props are supported (e.g., `children`, `href`, `onClick`, etc.).
- **First link**: `variant="primary"`, `color="green"`
- **Second link**: `variant="tertiary"`, `color="green"`
- Max 2 links supported (ButtonGroup validation)
## Media Types
@@ -130,9 +129,9 @@ media={{
<HeaderHeroPrimaryMedia
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" },
links={[
{ label: "Get Started", href: "/docs" },
{ label: "Learn More", href: "/about" },
]}
media={{
type: "image",
@@ -148,7 +147,7 @@ media={{
<HeaderHeroPrimaryMedia
headline="Watch and Learn"
subtitle="Explore our video tutorials."
callsToAction={[{ children: "Watch Tutorials", href: "/tutorials" }]}
links={[{ label: "Watch Tutorials", href: "/tutorials" }]}
media={{
type: "video",
src: "/video/intro.mp4",
@@ -166,7 +165,7 @@ media={{
<HeaderHeroPrimaryMedia
headline="Interactive Experience"
subtitle="Engage with custom media."
callsToAction={[{ children: "Explore", href: "/interactive" }]}
links={[{ label: "Explore", href: "/interactive" }]}
media={{
type: "custom",
element: <MyAnimationComponent />,
@@ -190,7 +189,7 @@ The component enforces specific design requirements:
The component includes development-time validation that logs warnings to the console when required props are missing:
- Missing `headline`: Component returns `null` (error logged)
- Missing `subtitle`, `callsToAction`, or `media`: Warning logged, component still renders
- Missing `subtitle`, `links`, or `media`: Warning logged, component still renders
## CSS Classes

View File

@@ -3,7 +3,6 @@
//
// Naming Convention: BEM with 'bds' namespace
// .bds-link-small-grid - Base section container
// .bds-link-small-grid__header - Header container (heading + description)
// =============================================================================
// Design Tokens
@@ -42,23 +41,6 @@ $bds-link-small-grid-spacing-lg: $bds-space-4xl; // 40px - spacing('4xl')
background-color: transparent;
}
// =============================================================================
// Header Section
// =============================================================================
.bds-link-small-grid__header {
gap: $bds-space-sm; // 8px - spacing('sm')
margin-bottom: $bds-link-small-grid-spacing-base;
@include media-breakpoint-up(md) {
gap: $bds-space-lg; // 16px - spacing('lg')
margin-bottom: $bds-link-small-grid-spacing-md;
}
@include media-breakpoint-up(lg) {
gap: $bds-space-lg; // 16px - spacing('lg')
margin-bottom: $bds-link-small-grid-spacing-lg;
}
}
// Header section uses SectionHeader component

View File

@@ -1,7 +1,8 @@
import React, { useMemo } from 'react';
import clsx from 'clsx';
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
import { TileLink, TileLinkProps } from '../TileLinks/TileLink';
import { SectionHeader } from 'shared/patterns/SectionHeader';
import { TileLink, TileLinkProps } from 'shared/patterns/TileLinks';
import { calculateTileOffset } from 'shared/utils/helpers';
export interface LinkItem extends Omit<TileLinkProps, 'variant'> {}
@@ -90,17 +91,7 @@ export const LinkSmallGrid: React.FC<LinkSmallGridProps> = ({
return (
<section className={classNames}>
<PageGrid>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 6, lg: 8 }}>
{/* Header Section */}
<div className="bds-link-small-grid__header">
<h2 className="bds-link-small-grid__heading h-md">{heading}</h2>
{description && (
<p className="body-l mb-0">{description}</p>
)}
</div>
</PageGridCol>
</PageGridRow>
<SectionHeader heading={heading} description={description} span={{ base: 4, md: 6, lg: 8 }} />
<PageGridRow>
{links.map((link, index) => {
const offset = linkOffsets[index];

View File

@@ -3,7 +3,6 @@
//
// Naming Convention: BEM with 'bds' namespace
// .bds-link-text-directory - Base section container
// .bds-link-text-directory__header - Header section (heading + description)
// =============================================================================
// Design Tokens
@@ -14,16 +13,6 @@ $bds-link-text-directory-padding-base: 24px;
$bds-link-text-directory-padding-md: 32px;
$bds-link-text-directory-padding-lg: 40px;
// Header gap (between heading and description)
$bds-link-text-directory-header-gap-base: 8px;
$bds-link-text-directory-header-gap-md: 8px;
$bds-link-text-directory-header-gap-lg: 16px;
// Header margin-bottom (space before cards list)
$bds-link-text-directory-header-margin-base: 24px;
$bds-link-text-directory-header-margin-md: 32px;
$bds-link-text-directory-header-margin-lg: 40px;
// =============================================================================
// Section Container
// =============================================================================
@@ -51,26 +40,4 @@ $bds-link-text-directory-header-margin-lg: 40px;
}
}
// =============================================================================
// Header Section
// =============================================================================
.bds-link-text-directory__header {
display: flex;
flex-direction: column;
gap: $bds-link-text-directory-header-gap-base;
margin-bottom: $bds-link-text-directory-header-margin-base;
@include media-breakpoint-up(md) {
gap: $bds-link-text-directory-header-gap-md;
margin-bottom: $bds-link-text-directory-header-margin-md;
}
@include media-breakpoint-up(lg) {
gap: $bds-link-text-directory-header-gap-lg;
margin-bottom: $bds-link-text-directory-header-margin-lg;
}
h2, p {
margin-bottom: 0;
}
}
// Header section uses SectionHeader component

View File

@@ -1,8 +1,9 @@
import React from 'react';
import clsx from 'clsx';
import { LinkTextCard } from '../LinkTextCard';
import { ButtonConfig } from '../ButtonGroup';
import { LinkTextCard } from 'shared/patterns/LinkTextCard';
import { ButtonConfig } from 'shared/patterns/ButtonGroup';
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
import { SectionHeader } from 'shared/patterns/SectionHeader';
export interface LinkTextCardData {
/** Heading text for the card */
@@ -75,13 +76,7 @@ export const LinkTextDirectory: React.FC<LinkTextDirectoryProps> = ({
}) => {
return (
<PageGrid className={clsx('bds-link-text-directory', className)}>
{/* Header Section */}
<PageGridRow>
<PageGridCol className="bds-link-text-directory__header" span={{ base: 12, md: 6, lg: 8}}>
<h2 className="h-md">{heading}</h2>
{description && <p className="body-l">{description}</p>}
</PageGridCol>
</PageGridRow>
<SectionHeader heading={heading} description={description} span={{ base: 12, md: 6, lg: 8 }} />
{/* Cards List */}
<PageGridRow className="bds-link-text-directory__list">

View File

@@ -214,13 +214,13 @@ cards[2] → LinkTextCard(index: 2) → displays "03"
## Import
```tsx
import { LinkTextDirectory } from 'shared/patterns/LinkTextDirectory';
import { LinkTextDirectory } from 'shared/sections/LinkTextDirectory';
// or
import {
LinkTextDirectory,
type LinkTextDirectoryProps,
type LinkTextCardData
} from 'shared/patterns/LinkTextDirectory';
} from 'shared/sections/LinkTextDirectory';
```
## Design System

View File

@@ -1,71 +1,82 @@
// BDS LogoSquareGrid Component Styles
// Brand Design System - Logo grid pattern with optional header section
// BDS LogoRectangleGrid Component Styles
// Brand Design System - Logo grid pattern with rectangle tiles
//
// Naming Convention: BEM with 'bds' namespace
// .bds-logo-square-grid - Base component
// .bds-logo-square-grid--gray - Gray variant (maps to TileLogo 'neutral')
// .bds-logo-square-grid--green - Green variant (maps to TileLogo 'green')
// .bds-logo-square-grid__header - Header section container
// .bds-logo-square-grid__text - Text content container
// .bds-logo-square-grid__heading - Heading element
// .bds-logo-square-grid__description - Description element
// .bds-logo-rectangle-grid - Base component
// .bds-logo-rectangle-grid--gray - Gray variant (maps to TileLogo 'neutral')
// .bds-logo-rectangle-grid--green - Green variant (maps to TileLogo 'green')
// .bds-logo-rectangle-grid__header - Header section container
// .bds-logo-rectangle-grid__text - Text content container
//
// Note: Individual logo tiles are rendered using the TileLogo component
// Note: Button layout is handled by the ButtonGroup component
// Note: Individual logo tiles are rendered using the TileLogo component with shape="rectangle"
// Note: Alignment is handled dynamically by PageGridCol offset prop
// =============================================================================
// Design Tokens
// =============================================================================
// Note: Color variants are now handled by the TileLogo component
// LogoSquareGrid 'gray' maps to TileLogo 'neutral'
// LogoSquareGrid 'green' maps to TileLogo 'green'
// LogoRectangleGrid 'gray' maps to TileLogo 'neutral'
// LogoRectangleGrid 'green' maps to TileLogo 'green'
// Spacing tokens - responsive
// Note: Uses centralized spacing tokens from _spacing.scss.
// Mobile (<768px)
$bds-lsg-header-gap-mobile: $bds-space-2xl; // 24px - spacing('2xl')
$bds-lsg-text-gap-mobile: $bds-space-sm; // 8px - spacing('sm')
$bds-lrg-header-gap-mobile: $bds-space-2xl; // 24px - spacing('2xl')
$bds-lrg-text-gap-mobile: $bds-space-sm; // 8px - spacing('sm')
// Tablet (768px-1023px)
$bds-lsg-header-gap-tablet: $bds-space-3xl; // 32px - spacing('3xl')
$bds-lrg-header-gap-tablet: $bds-space-3xl; // 32px - spacing('3xl')
// Desktop (1024px)
$bds-lsg-header-gap-desktop: $bds-space-4xl; // 40px - spacing('4xl')
$bds-lsg-text-gap-desktop: $bds-space-lg; // 16px - spacing('lg')
$bds-lrg-header-gap-desktop: $bds-space-4xl; // 40px - spacing('4xl')
$bds-lrg-text-gap-desktop: $bds-space-lg; // 16px - spacing('lg')
// =============================================================================
// Base Component Styles
// =============================================================================
.bds-logo-square-grid {
.bds-logo-rectangle-grid {
@extend .d-flex;
@extend .flex-column;
@extend .w-100;
// Mobile-first gap
gap: $bds-lrg-header-gap-mobile;
// Tablet breakpoint
@include media-breakpoint-up(md) {
gap: $bds-lrg-header-gap-tablet;
}
// Desktop breakpoint
@include media-breakpoint-up(lg) {
gap: $bds-lrg-header-gap-desktop;
}
}
// =============================================================================
// Header Section
// =============================================================================
.bds-logo-square-grid__header {
.bds-logo-rectangle-grid__header {
@extend .d-flex;
@extend .flex-column;
margin-top: $bds-space-2xl; // 24px - spacing('2xl')
margin-bottom: $bds-space-2xl; // 24px - spacing('2xl')
// Mobile-first gap
gap: $bds-lsg-header-gap-mobile;
gap: $bds-lrg-header-gap-mobile;
// Tablet breakpoint
@include media-breakpoint-up(md) {
gap: $bds-lsg-header-gap-tablet;
gap: $bds-lrg-header-gap-tablet;
margin-top: $bds-space-3xl; // 32px - spacing('3xl')
margin-bottom: $bds-space-3xl; // 32px - spacing('3xl')
}
// Desktop breakpoint
@include media-breakpoint-up(lg) {
gap: $bds-lsg-header-gap-desktop;
gap: $bds-lrg-header-gap-desktop;
margin-top: $bds-space-4xl; // 40px - spacing('4xl')
margin-bottom: $bds-space-4xl; // 40px - spacing('4xl')
}
@@ -75,29 +86,25 @@ $bds-lsg-text-gap-desktop: $bds-space-lg; // 16px - spacing('lg')
// Text Content
// =============================================================================
.bds-logo-square-grid__text {
.bds-logo-rectangle-grid__text {
@extend .d-flex;
@extend .flex-column;
// Mobile-first gap
gap: $bds-lsg-text-gap-mobile;
gap: $bds-lrg-text-gap-mobile;
// Desktop breakpoint
@include media-breakpoint-up(lg) {
gap: $bds-lsg-text-gap-desktop;
gap: $bds-lrg-text-gap-desktop;
}
}
// =============================================================================
// Action Buttons
// =============================================================================
// Note: Button layout is now handled by the ButtonGroup component
// =============================================================================
// Logo Grid Row
// =============================================================================
// Note: Grid layout is now handled by PageGridRow/PageGridCol
// Each tile uses PageGridCol with span={{ base: 2, lg: 3 }}
// This gives us 2 columns on mobile (2/4) and 4 columns on desktop (3/12)
// Each tile uses PageGridCol with span={{ base: 2, md: 2, lg: 3 }}
// This gives us 2 columns on mobile (2/4), 3 columns on tablet (2/6),
// and 4 columns on desktop (3/12)
// Alignment is handled by offset prop based on logo count
// Tile rendering and styling is handled by the TileLogo component

View File

@@ -0,0 +1,119 @@
import React, { useMemo } from 'react';
import clsx from 'clsx';
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
import { TileLogo, TileLogoProps } from '../../components/TileLogo/TileLogo';
import { calculateTileOffset } from 'shared/utils/helpers';
export interface LogoItem extends TileLogoProps {}
export interface LogoRectangleGridProps {
/** Color variant - determines background color */
variant?: 'gray' | 'green';
/** Heading text (required) */
heading: string;
/** Optional description text */
description?: string;
/** Array of logo items to display in the grid */
logos: LogoItem[];
/** Additional CSS classes */
className?: string;
}
/**
* LogoRectangleGrid Component
*
* A responsive grid pattern for displaying company/partner logos with rectangle tiles
* and dynamic offset based on tile count. Features 9:5 aspect ratio rectangle tiles
* with 2 color variants and dark mode support.
*
* Offset Logic (lg breakpoint only, applied to first tile):
* - 1 tile: offset 9
* - 2 tiles: offset 6
* - 3 tiles: offset 3
* - 4 tiles: offset 9
* - 5 tiles: offset 6
* - 6 tiles: offset 3
* - 7 tiles: offset 9
* - 8 tiles: offset 6
* - 9 tiles: offset 3
* - 10+ tiles: no offset
*
* @example
* // Basic usage with gray variant
* <LogoRectangleGrid
* variant="gray"
* heading="Developer tools & APIs"
* description="Streamline development with comprehensive tools."
* logos={[
* { src: "/logos/company1.svg", alt: "Company 1" },
* { src: "/logos/company2.svg", alt: "Company 2" }
* ]}
* />
*
* @example
* // With clickable logos
* <LogoRectangleGrid
* variant="green"
* heading="Our Partners"
* description="Leading companies building on XRPL."
* logos={[
* { src: "/logos/partner1.svg", alt: "Partner 1", href: "https://partner1.com" }
* ]}
* />
*/
export const LogoRectangleGrid: React.FC<LogoRectangleGridProps> = ({
variant = 'gray',
heading,
description,
logos,
className,
}) => {
// Build class names using BEM with bds namespace
const classNames = clsx(
'bds-logo-rectangle-grid',
`bds-logo-rectangle-grid--${variant}`,
);
// Memoize offset calculations - only recalculate when logos array changes
const logoOffsets = useMemo(() => {
const total = logos.length;
return logos.map((_, index) => calculateTileOffset(index, total));
}, [logos]);
return (
<PageGrid className={classNames}>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 6, lg: 8 }}>
{/* Header Section */}
<div className="bds-logo-rectangle-grid__header">
<div className="bds-logo-rectangle-grid__text">
<h4 className="h-md mb-0">{heading}</h4>
{description && <p className="body-l mb-0">{description}</p>}
</div>
</div>
</PageGridCol>
</PageGridRow>
<PageGridRow>
{logos.map((logo, index) => {
const offset = logoOffsets[index];
const hasOffset = offset.md > 0 || offset.lg > 0;
return (
<PageGridCol
key={index}
span={{ base: 2, md: 2, lg: 3 }}
offset={hasOffset ? { md: offset.md, lg: offset.lg } : undefined}
>
<TileLogo
shape="rectangle"
variant={variant === 'gray' ? 'neutral' : 'green'}
{...logo}
/>
</PageGridCol>
);
})}
</PageGridRow>
</PageGrid>
);
};
export default LogoRectangleGrid;

View File

@@ -0,0 +1,424 @@
# LogoRectangleGrid Component
A responsive grid pattern for displaying company/partner logos with rectangle tiles and dynamic alignment based on tile count. Built on top of the TileLogo component, featuring 9:5 aspect ratio rectangle tiles with 2 color variants and full dark mode support.
## Features
- **2 Color Variants**: Gray and Green backgrounds
- **Dynamic Alignment**: Grid alignment changes based on logo count (1-3: right, 4: left, 5-9: right, 9+: left)
- **Responsive Grid**: Automatically adapts from 2 columns (mobile) to 3 columns (tablet) to 4 columns (desktop)
- **Required Header**: Heading is required, description is optional
- **Clickable Logos**: Support for optional links on individual logos
- **Dark Mode Support**: Full light and dark mode compatibility
- **Rectangle Tiles**: Maintains 9:5 aspect ratio at all breakpoints
- **Grid Integration**: Built-in PageGrid wrapper with standard container support
## Responsive Behavior
The component automatically adapts its grid layout based on viewport width:
| Breakpoint | Columns | Gap | Tile Aspect Ratio |
|------------|---------|-----|-------------------|
| Mobile (< 768px) | 2 | 8px | 9:5 |
| Tablet (768px - 1023px) | 3 | 8px | 9:5 |
| Desktop (≥ 1024px) | 4 | 8px | 9:5 |
## Dynamic Alignment
The grid alignment changes based on the number of logos:
| Logo Count | Alignment | Offset |
|------------|-----------|--------|
| 1-3 | Right | 4 columns |
| 4 | Left | 0 columns |
| 5-9 | Right | 4 columns |
| 9+ | Left | 0 columns |
This creates a visually balanced layout that adapts to different content volumes.
## Color Variants
The LogoRectangleGrid pattern uses two color variants that map directly to TileLogo component variants:
| LogoRectangleGrid Variant | TileLogo Variant | Description |
|---------------------------|------------------|-------------|
| `gray` | `neutral` | Subtle, professional appearance for general partner showcases |
| `green` | `green` | Highlights featured or primary partners |
Colors are managed by the TileLogo component and automatically adapt between light and dark modes with proper hover states and animations.
## Props API
```typescript
interface LogoItem {
/** Logo image source URL */
logo: string;
/** Alt text for the logo image */
alt: string;
/** Optional link URL - makes the logo clickable */
href?: string;
/** Optional click handler - makes the logo a button */
onClick?: () => void;
/** Disabled state */
disabled?: boolean;
}
interface LogoRectangleGridProps {
/** Color variant - determines background color */
variant?: 'gray' | 'green';
/** Heading text (required) */
heading: string;
/** Optional description text */
description?: string;
/** Array of logo items to display in the grid */
logos: LogoItem[];
/** Additional CSS classes */
className?: string;
}
```
**Note**: `LogoItem` extends `TileLogoProps` from the TileLogo component. The `variant` property is NOT included in individual logo items - it's controlled at the component level via the `LogoRectangleGridProps.variant` prop, which applies to all tiles in the grid.
### Default Values
- `variant`: `'gray'`
- `description`: `undefined`
- `className`: `''`
### Required Props
- `heading`: Heading text (required)
- `logos`: Array of logo items (required)
## Usage Examples
### Basic Usage with Gray Variant
```tsx
import { LogoRectangleGrid } from 'shared/sections/LogoRectangleGrid';
<LogoRectangleGrid
variant="gray"
heading="Developer tools & APIs"
logos={[
{ logo: "/img/logos/company1.svg", alt: "Company 1" },
{ logo: "/img/logos/company2.svg", alt: "Company 2" },
{ logo: "/img/logos/company3.svg", alt: "Company 3" },
{ logo: "/img/logos/company4.svg", alt: "Company 4" }
]}
/>
```
### With Description
```tsx
<LogoRectangleGrid
variant="green"
heading="Developer tools & APIs"
description="Streamline development and build powerful RWA tokenization solutions with XRP Ledger's comprehensive developer toolset."
logos={[
{ logo: "/img/logos/tool1.svg", alt: "Tool 1" },
{ logo: "/img/logos/tool2.svg", alt: "Tool 2" },
{ logo: "/img/logos/tool3.svg", alt: "Tool 3" },
{ logo: "/img/logos/tool4.svg", alt: "Tool 4" },
{ logo: "/img/logos/tool5.svg", alt: "Tool 5" },
{ logo: "/img/logos/tool6.svg", alt: "Tool 6" },
{ logo: "/img/logos/tool7.svg", alt: "Tool 7" },
{ logo: "/img/logos/tool8.svg", alt: "Tool 8" }
]}
/>
```
### With Clickable Logos
```tsx
<LogoRectangleGrid
variant="gray"
heading="Our Partners"
description="Leading companies building on XRPL."
logos={[
{
logo: "/img/logos/partner1.svg",
alt: "Partner 1",
href: "https://partner1.com"
},
{
logo: "/img/logos/partner2.svg",
alt: "Partner 2",
href: "https://partner2.com"
}
]}
/>
```
### With Button Handlers
```tsx
<LogoRectangleGrid
variant="green"
heading="Interactive Partners"
description="Click any logo to learn more."
logos={[
{
logo: "/img/logos/partner1.svg",
alt: "Partner 1",
onClick: () => openModal('partner1')
},
{
logo: "/img/logos/partner2.svg",
alt: "Partner 2",
onClick: () => openModal('partner2')
}
]}
/>
```
### With Disabled State
```tsx
<LogoRectangleGrid
variant="gray"
heading="Coming Soon"
description="New partners joining the ecosystem."
logos={[
{
logo: "/img/logos/partner1.svg",
alt: "Partner 1",
href: "/partners/partner1"
},
{
logo: "/img/logos/coming-soon.svg",
alt: "Coming Soon",
disabled: true
}
]}
/>
```
### Heading Only (No Description)
```tsx
<LogoRectangleGrid
variant="gray"
heading="Ecosystem Members"
logos={[
{ logo: "/img/logos/sponsor1.svg", alt: "Sponsor 1" },
{ logo: "/img/logos/sponsor2.svg", alt: "Sponsor 2" },
{ logo: "/img/logos/sponsor3.svg", alt: "Sponsor 3" },
{ logo: "/img/logos/sponsor4.svg", alt: "Sponsor 4" }
]}
/>
```
### Demonstrating Alignment Logic
```tsx
// 1-3 logos: Right-aligned
<LogoRectangleGrid
variant="green"
heading="Featured Partners"
logos={[
{ logo: "/img/logos/partner1.svg", alt: "Partner 1" },
{ logo: "/img/logos/partner2.svg", alt: "Partner 2" }
]}
/>
// 4 logos: Left-aligned
<LogoRectangleGrid
variant="gray"
heading="Core Technologies"
logos={[
{ logo: "/img/logos/tech1.svg", alt: "Tech 1" },
{ logo: "/img/logos/tech2.svg", alt: "Tech 2" },
{ logo: "/img/logos/tech3.svg", alt: "Tech 3" },
{ logo: "/img/logos/tech4.svg", alt: "Tech 4" }
]}
/>
// 5-9 logos: Right-aligned
<LogoRectangleGrid
variant="green"
heading="Developer Tools"
logos={[
{ logo: "/img/logos/tool1.svg", alt: "Tool 1" },
{ logo: "/img/logos/tool2.svg", alt: "Tool 2" },
{ logo: "/img/logos/tool3.svg", alt: "Tool 3" },
{ logo: "/img/logos/tool4.svg", alt: "Tool 4" },
{ logo: "/img/logos/tool5.svg", alt: "Tool 5" },
{ logo: "/img/logos/tool6.svg", alt: "Tool 6" }
]}
/>
// 9+ logos: Left-aligned
<LogoRectangleGrid
variant="gray"
heading="Partner Ecosystem"
logos={[
// ... 12 logos
]}
/>
```
## Important Implementation Details
### Logo Image Requirements
For best results, logo images should:
- Be SVG format for crisp scaling
- Have transparent backgrounds
- Be reasonably sized (width: 150-250px recommended for 9:5 aspect ratio)
- Use monochrome or simple color schemes
- Have consistent visual weight across all logos
### Grid Behavior
- The grid uses PageGridCol components for responsive layout
- Each tile uses `span={{ base: 2, md: 2, lg: 3 }}` (2 cols on mobile out of 4, 2 cols on tablet out of 6, 3 cols on desktop out of 12)
- This creates 2 columns on mobile, 3 columns on tablet, and 4 columns on desktop
- Tiles maintain a 9:5 aspect ratio using the TileLogo rectangle shape
- Gaps between tiles are handled by PageGrid's built-in gutter system
- Grid automatically wraps to new rows as needed
- Grid alignment changes dynamically based on logo count
### Alignment Logic Implementation
The alignment is controlled by the `alignRight` variable:
```typescript
const logoCount = logos.length;
const alignRight =
(logoCount >= 1 && logoCount <= 3) ||
(logoCount >= 5 && logoCount <= 9);
```
When `alignRight` is true:
- Grid container spans 8 columns on desktop (lg breakpoint)
- Grid container has 4-column offset on desktop (pushes content right)
When `alignRight` is false:
- Grid container spans full width
- Grid container has 0 offset (content aligns left)
### Clickable Logo Behavior
Logo tiles leverage the TileLogo component's interactive capabilities:
- **With `href` property**: Renders as a link (`<a>` tag) with window shade hover animation
- **With `onClick` property**: Renders as a button with the same interactive states
- **With `disabled` property**: Prevents interaction and applies disabled styling
- **Interactive states**: Default, Hover, Focused, Pressed, and Disabled
- **Animation**: Window shade effect that wipes from bottom to top on hover
- All tiles automatically maintain focus states for keyboard accessibility
## Styling
### BEM Class Structure
```scss
.bds-logo-rectangle-grid // Base component
.bds-logo-rectangle-grid--gray // Gray variant (maps to TileLogo 'neutral')
.bds-logo-rectangle-grid--green // Green variant (maps to TileLogo 'green')
.bds-logo-rectangle-grid__header // Header section container
.bds-logo-rectangle-grid__text // Text content container
```
**Note**: Individual logo tiles are rendered using the TileLogo component with its own BEM structure (`bds-tile-logo`) and `shape="rectangle"`. Grid layout is handled by PageGridRow and PageGridCol components.
### Typography Tokens
- **Heading**: Uses `heading-md` type token (Tobias Light font)
- Desktop: 40px / 46px line-height / -1px letter-spacing
- Tablet: 36px / 45px line-height / -0.5px letter-spacing
- Mobile: 32px / 40px line-height / 0px letter-spacing
- **Description**: Uses `body-l` type token (Booton Light font)
- Desktop: 18px / 26.1px line-height / -0.5px letter-spacing
- Tablet: 18px / 26.1px line-height / -0.5px letter-spacing
- Mobile: 18px / 26.1px line-height / -0.5px letter-spacing
### Color Tokens
All colors are sourced from `styles/_colors.scss`:
```scss
// Tile backgrounds
$gray-200 // Gray variant (light mode)
$gray-700 // Gray variant (dark mode)
$green-200 // Green variant (light mode)
$green-300 // Green variant (dark mode)
```
## Accessibility
- Semantic HTML structure with proper heading hierarchy
- All logos include descriptive alt text
- Clickable logos have proper link semantics
- Keyboard navigation support with visible focus states
- Color contrast meets WCAG AA standards in all variants
## Best Practices
### When to Use Each Variant
- **Gray**: General-purpose logo grids, subtle integration
- **Green**: Featured partnerships, brand-focused sections
### Content Guidelines
- **Heading**: Required, keep concise (1-2 lines preferred), use sentence case
- **Description**: Optional, provide context (2-3 lines max), complete sentences
- **Logo Count**: Consider the alignment logic when choosing how many logos to display
- **Alt Text**: Use company/product names, not generic "logo"
### Logo Preparation
1. **Consistent Sizing**: Ensure all logos have similar visual weight
2. **Format**: Use SVG for scalability and crisp rendering
3. **Background**: Transparent backgrounds work best
4. **Aspect Ratio**: Rectangle tiles work well with horizontal logos
5. **Padding**: Include minimal internal padding in the SVG itself
### Performance
- Use optimized SVG files (run through SVGO or similar)
- Consider lazy loading for grids with many logos
- Provide appropriate alt text for all images
- Use `width` and `height` attributes on img tags when possible
### Technical Implementation
- **Grid System**: Uses PageGridCol with `span={{ base: 2, md: 2, lg: 3 }}` for responsive layout
- **Tile Rendering**: Leverages TileLogo component with `shape="rectangle"` for all logo tiles
- **Variant Mapping**: LogoRectangleGrid 'gray' TileLogo 'neutral', LogoRectangleGrid 'green' TileLogo 'green'
- **Interactive States**: TileLogo handles href (links), onClick (buttons), and disabled states
- **Aspect Ratio**: Rectangle tiles maintained by TileLogo with CSS `aspect-ratio: 9/5`
- **Animations**: Window shade hover effect managed by TileLogo component
- **Dynamic Alignment**: Grid alignment controlled by conditional offset based on logo count
## Files
- `LogoRectangleGrid.tsx` - Component implementation
- `LogoRectangleGrid.scss` - Styles with color variants and responsive breakpoints
- `index.ts` - Barrel exports
- `README.md` - This documentation
## Related Components
- **TileLogo**: Core component used to render individual logo tiles with interactive states and rectangle shape
- **LogoSquareGrid**: Similar pattern but with square tiles instead of rectangle tiles
- **PageGrid**: Used internally for responsive grid structure and standard container support
## Design References
- **Figma Design**: [Section Logo - Rectangle Grid](https://www.figma.com/design/gaTsImoTRsiRXAGzbGKcCd/Section-Logo---Rectangle-Grid?node-id=1-2)
- **Showcase Page**: `/about/logo-rectangle-grid-showcase.page.tsx`
- **Component Location**: `shared/sections/LogoRectangleGrid/`
## Version History
- **January 2026**: Initial implementation
- Figma design alignment with 2 color variants
- Responsive grid with 2/3/4 column layout
- Dynamic alignment based on logo count
- Required header section with optional description
- Clickable logo support
- Rectangle tiles with 9:5 aspect ratio

View File

@@ -0,0 +1,2 @@
export { LogoRectangleGrid } from './LogoRectangleGrid';
export type { LogoRectangleGridProps, LogoItem } from './LogoRectangleGrid';

View File

@@ -0,0 +1,52 @@
// BDS LogoSquareGrid Component Styles
// Brand Design System - Logo grid pattern with optional header section
//
// Naming Convention: BEM with 'bds' namespace
// .bds-logo-square-grid - Base component
// .bds-logo-square-grid--gray - Gray variant (maps to TileLogo 'neutral')
// .bds-logo-square-grid--green - Green variant (maps to TileLogo 'green')
// .bds-logo-square-grid__section-header - SectionHeader with optional top margin
//
// Note: Individual logo tiles are rendered using the TileLogo component
// Note: Button layout is handled by the ButtonGroup component
// =============================================================================
// Design Tokens
// =============================================================================
// Note: Color variants are now handled by the TileLogo component
// LogoSquareGrid 'gray' maps to TileLogo 'neutral'
// LogoSquareGrid 'green' maps to TileLogo 'green'
// =============================================================================
// Base Component Styles
// =============================================================================
.bds-logo-square-grid {
@extend .d-flex;
@extend .flex-column;
@extend .w-100;
}
// =============================================================================
// Header Section (uses SectionHeader component)
// =============================================================================
// LogoSquareGrid needs additional top margin for the header row
.bds-logo-square-grid__section-header {
margin-top: $bds-space-2xl;
@include media-breakpoint-up(md) {
margin-top: $bds-space-3xl;
}
@include media-breakpoint-up(lg) {
margin-top: $bds-space-4xl;
}
}
// =============================================================================
// Logo Grid Row
// =============================================================================
// Note: Grid layout is now handled by PageGridRow/PageGridCol
// Each tile uses PageGridCol with span={{ base: 2, lg: 3 }}
// This gives us 2 columns on mobile (2/4) and 4 columns on desktop (3/12)
// Tile rendering and styling is handled by the TileLogo component

View File

@@ -1,8 +1,9 @@
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, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
import { SectionHeader } from 'shared/patterns/SectionHeader';
import { TileLogo, TileLogoProps } from 'shared/components/TileLogo/TileLogo';
import { ButtonGroup, ButtonConfig, validateButtonGroup } from 'shared/patterns/ButtonGroup/ButtonGroup';
export interface LogoItem extends TileLogoProps {}
@@ -73,36 +74,23 @@ export const LogoSquareGrid: React.FC<LogoSquareGridProps> = ({
className
);
// Determine if we should show the header section
const hasHeader = !!(heading || description || hasButtons);
return (
<PageGrid className={classNames}>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 6, lg: 8 }}>
{/* Header Section */}
{hasHeader && (
<div className="bds-logo-square-grid__header">
{/* Text Content */}
{(heading || description) && (
<div className="bds-logo-square-grid__text">
{heading && <h4 className="h-md mb-0">{heading}</h4>}
{description && <p className="body-l mb-0">{description}</p>}
</div>
)}
{/* Buttons */}
{hasButtons && (
<ButtonGroup
buttons={buttonValidation.buttons}
color="green"
gap="small"
/>
)}
</div>
)}
</PageGridCol>
</PageGridRow>
<SectionHeader
heading={heading}
description={description}
as="h4"
span={{ base: 4, md: 6, lg: 8 }}
className="bds-logo-square-grid__section-header"
>
{hasButtons && (
<ButtonGroup
buttons={buttonValidation.buttons}
color="green"
gap="small"
/>
)}
</SectionHeader>
<PageGridRow>
{logos.map((logo, index) => (
<PageGridCol key={index} span={{ base: 2, lg: 3 }}>

View File

@@ -95,7 +95,7 @@ interface LogoSquareGridProps {
### Basic Usage with Gray Variant
```tsx
import { LogoSquareGrid } from 'shared/patterns/LogoSquareGrid';
import { LogoSquareGrid } from 'shared/sections/LogoSquareGrid';
<LogoSquareGrid
variant="gray"
@@ -395,7 +395,7 @@ $green-300 // Green variant (dark mode)
- **Figma Design**: [Pattern Logo - Square Grid](https://www.figma.com/design/ThBcoYLNKsBGw3r9g1L6Z8/Pattern-Logo---Square-Grid?node-id=1-2)
- **Showcase Page**: `/about/logo-square-grid-showcase.page.tsx`
- **Component Location**: `shared/patterns/LogoSquareGrid/`
- **Component Location**: `shared/sections/LogoSquareGrid/`
## Version History

View File

@@ -15,7 +15,7 @@ The StandardCardGroupSection component provides a structured section with:
## Basic Usage
```tsx
import StandardCardGroupSection from "shared/patterns/StandardCardGroupSection/StandardCardGroupSection";
import StandardCardGroupSection from "shared/sections/StandardCardGroupSection/StandardCardGroupSection";
function MyPage() {
return (

View File

@@ -1,6 +1,7 @@
import React from "react";
import clsx from "clsx";
import { PageGrid } from "../../components/PageGrid/page-grid";
import { SectionHeader } from 'shared/patterns/SectionHeader';
import StandardCard, {
StandardCardPropsWithoutVariant,
StandardCardVariant,
@@ -79,29 +80,7 @@ export const StandardCardGroupSection = React.forwardRef<
{...rest}
>
<PageGrid>
{/* Header content row */}
<PageGrid.Row>
<PageGrid.Col
span={{
base: "fill",
md: 6,
lg: 8,
}}
>
<div className="bds-standard-card-group-section__header">
{headline && (
<h2 className="bds-standard-card-group-section__headline h-md">
{headline}
</h2>
)}
{description && (
<div className="bds-standard-card-group-section__description body-l">
{description}
</div>
)}
</div>
</PageGrid.Col>
</PageGrid.Row>
<SectionHeader heading={headline} description={description} />
{/* Cards grid row */}
<PageGrid.Row

View File

@@ -3,9 +3,6 @@
//
// Naming Convention: BEM with 'bds' namespace
// .bds-standard-card-group-section - Base section container
// .bds-standard-card-group-section__header - Header wrapper for headline and description
// .bds-standard-card-group-section__headline - Section headline (uses .h-md)
// .bds-standard-card-group-section__description - Section description (uses .body-l)
//
// Design tokens from Figma:
// Light Mode:
@@ -25,16 +22,6 @@
// =============================================================================
// Note: Uses centralized spacing tokens from _spacing.scss.
// Spacing - Header gap (between headline and description)
$bds-standard-card-group-section-header-gap-sm: $bds-space-sm; // 8px - spacing('sm')
$bds-standard-card-group-section-header-gap-md: $bds-space-sm; // 8px - spacing('sm')
$bds-standard-card-group-section-header-gap-lg: $bds-space-lg; // 16px - spacing('lg')
// Spacing - Section gap (between header and cards)
$bds-standard-card-group-section-section-gap-sm: $bds-space-2xl; // 24px - spacing('2xl')
$bds-standard-card-group-section-section-gap-md: $bds-space-3xl; // 32px - spacing('3xl')
$bds-standard-card-group-section-section-gap-lg: $bds-space-4xl; // 40px - spacing('4xl')
// Spacing - Section padding (vertical)
$bds-standard-card-group-section-padding-y-sm: $bds-space-5xl; // 48px - spacing('5xl')
$bds-standard-card-group-section-padding-y-md: $bds-space-6xl; // 64px - spacing('6xl')
@@ -69,43 +56,8 @@ $bds-standard-card-group-section-padding-y-lg: $bds-space-8xl; // 80px - spaci
}
}
// =============================================================================
// Header Section
// =============================================================================
.bds-standard-card-group-section__header {
display: flex;
flex-direction: column;
gap: $bds-standard-card-group-section-header-gap-sm;
@include media-breakpoint-up(md) {
gap: $bds-standard-card-group-section-header-gap-md;
}
@include media-breakpoint-up(lg) {
gap: $bds-standard-card-group-section-header-gap-lg;
}
}
.bds-standard-card-group-section__headline {
margin: 0;
// Typography handled by .h-md class from _font.scss
}
.bds-standard-card-group-section__description {
margin: 0;
// Typography handled by .body-l class from _font.scss
}
// Header section uses SectionHeader component
.bds-standard-card-group-section__cards {
margin-top: $bds-standard-card-group-section-section-gap-sm;
@include media-breakpoint-up(md) {
margin-top: $bds-standard-card-group-section-section-gap-md;
}
@include media-breakpoint-up(lg) {
margin-top: $bds-standard-card-group-section-section-gap-lg;
}
// Section gap handled by SectionHeader margin-bottom
}

View File

@@ -26,6 +26,16 @@ export type DesignConstrainedCallToActionsProps = {
callsToAction?: DesignConstrainedCallsToActions;
};
/** Link config for ButtonGroup - consistent with ButtonConfig (label + href) */
export interface DesignConstrainedLink {
label: string;
href: string;
}
export type DesignConstrainedLinksProps = {
links?: DesignConstrainedLink[];
};
/**
* Base props that all media elements must have to ensure proper styling.
* These props are automatically applied to maintain the 9:16 aspect ratio

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
import { CalloutMediaBanner } from "shared/patterns/CalloutMediaBanner";
import { CalloutMediaBanner } from "shared/sections/CalloutMediaBanner";
export const frontmatter = {
seo: {
@@ -39,6 +39,7 @@ export default function CalloutMediaBannerShowcase() {
<CalloutMediaBanner
variant="green"
heading="The Compliant Ledger Protocol"
headingAs="h1"
subheading="A decentralized public Layer 1 blockchain for creating, transferring, and exchanging digital assets with a focus on compliance."
buttons={[
{ label: "Get Started", onClick: () => handleClick('responsive-demo-primary') },
@@ -290,6 +291,7 @@ export default function CalloutMediaBannerShowcase() {
backgroundImage={sampleLightBackgroundImage}
textColor="black"
heading="Build the Future of Finance"
headingAs="h2"
subheading="Create powerful decentralized applications with XRPL's fast, efficient, and sustainable blockchain technology."
buttons={[
{ label: "Start Building", onClick: () => handleClick('image-black-primary') },
@@ -574,10 +576,18 @@ export default function CalloutMediaBannerShowcase() {
<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>heading</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '120px', flexShrink: 0 }}><em>required</em></div>
<div style={{ width: '120px', flexShrink: 0 }}><code>undefined</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Main heading text</div>
</div>
{/* headingAs */}
<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>headingAs</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'</code></div>
<div style={{ width: '120px', flexShrink: 0 }}><code>'h6'</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Heading element type - allows semantic HTML customization while maintaining visual styling</div>
</div>
{/* subheading */}
<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>subheading</code></div>
@@ -620,7 +630,7 @@ export default function CalloutMediaBannerShowcase() {
</div>
<div>
<strong>Component Location:</strong>{' '}
<code>shared/patterns/CalloutMediaBanner/</code>
<code>shared/sections/CalloutMediaBanner/</code>
</div>
<div>
<strong>Color Tokens:</strong>{' '}

View File

@@ -1,5 +1,5 @@
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
import { CardStats, CardStatsCardConfig } from "shared/patterns/CardStats";
import { CardStats, CardStatsCardConfig } from "shared/sections/CardStatsList";
import { Divider } from "shared/components/Divider";
export const frontmatter = {
@@ -208,7 +208,7 @@ export default function CardStatsShowcase() {
<h5 className="mb-4">Basic Usage</h5>
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`import { CardStats } from 'shared/patterns/CardStats';
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`import { CardStats } from 'shared/sections/CardStatsList';
<CardStats
heading="Blockchain Trusted at Scale"
@@ -258,7 +258,7 @@ export default function CardStatsShowcase() {
</div>
<div>
<strong>Pattern Location:</strong>{' '}
<code>shared/patterns/CardStats/</code>
<code>shared/sections/CardStatsList/</code>
</div>
<div>
<strong>Component Used:</strong>{' '}

View File

@@ -1,5 +1,5 @@
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
import { CardsFeatured } from "shared/patterns/CardsFeatured";
import { CardsFeatured } from "shared/sections/CardsFeatured";
import { Divider } from "shared/components/Divider";
export const frontmatter = {

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import { CardsIconGrid } from "shared/patterns/CardsIconGrid";
import { CardsIconGrid } from "shared/sections/CardsIconGrid";
import { Divider } from "shared/components/Divider";
export const frontmatter = {

View File

@@ -1,4 +1,5 @@
import { CardsTextGrid } from "shared/patterns/CardsTextGrid";
import * as React from "react";
import { CardsTextGrid } from "shared/sections/CardsTextGrid";
import { Divider } from "shared/components/Divider";
export const frontmatter = {

View File

@@ -1,6 +1,5 @@
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
import { CardsTwoColumn } from "shared/patterns/CardsTwoColumn";
import { TextCard } from "shared/patterns/CardsTwoColumn";
import { CardsTwoColumn, TextCard } from "shared/sections/CardsTwoColumn";
import { Divider } from "shared/components/Divider";
export const frontmatter = {

View File

@@ -1,5 +1,5 @@
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
import { CarouselCardList } from "shared/patterns/CarouselCardList";
import { CarouselCardList } from "shared/sections/CarouselCardList";
import { CarouselButton } from "shared/components/CarouselButton";
import { Divider } from "shared/components/Divider";
@@ -615,7 +615,7 @@ export default function CarouselCardListShowcase() {
<h2 className="h4 mb-6">Usage Example</h2>
<div className="card p-4">
<pre className="mb-0" style={{ backgroundColor: 'var(--bs-gray-800)', padding: '1rem', borderRadius: '4px', overflow: 'auto' }}>
{`import { CarouselCardList } from 'shared/patterns/CarouselCardList';
{`import { CarouselCardList } from 'shared/sections/CarouselCardList';
// Basic usage - button color matches card color by default
<CarouselCardList
@@ -669,11 +669,11 @@ export default function CarouselCardListShowcase() {
</div>
<div>
<strong>Component Location:</strong>{' '}
<code>shared/patterns/CarouselCardList/</code>
<code>shared/sections/CarouselCardList/</code>
</div>
<div>
<strong>Documentation:</strong>{' '}
<code>shared/patterns/CarouselCardList/CarouselCardList.md</code>
<code>shared/sections/CarouselCardList/CarouselCardList.md</code>
</div>
</div>
</PageGridCol>

Some files were not shown because too many files have changed in this diff Show More