mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2026-02-23 15:22:34 +00:00
Compare commits
38 Commits
qa-carouse
...
go/code-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdd0c24919 | ||
|
|
14d1d76193 | ||
|
|
cb0c06f404 | ||
|
|
a97a009d93 | ||
|
|
df074e625b | ||
|
|
a4b1925b31 | ||
|
|
26d1cf102c | ||
|
|
a41a9e31cc | ||
|
|
159ac52acc | ||
|
|
c7e01d322a | ||
|
|
17582d543d | ||
|
|
986ca23ff7 | ||
|
|
acb2476d7d | ||
|
|
f5c38ffe77 | ||
|
|
ee6a32d159 | ||
|
|
e1d18bd621 | ||
|
|
a85dc47781 | ||
|
|
eecd14d763 | ||
|
|
42282a2012 | ||
|
|
6442318205 | ||
|
|
1ee76bfbea | ||
|
|
d558b7474d | ||
|
|
da49b0a154 | ||
|
|
01931bd177 | ||
|
|
c2ef761b01 | ||
|
|
d6ce246420 | ||
|
|
33c6315510 | ||
|
|
af0b8cd40a | ||
|
|
95c4ffaa1b | ||
|
|
08c5572f16 | ||
|
|
b49bc02dd2 | ||
|
|
65a61c5e47 | ||
|
|
237ddc3c74 | ||
|
|
dd6cfd34fe | ||
|
|
7b601da3a0 | ||
|
|
6021b458e6 | ||
|
|
e5f3bf75e3 | ||
|
|
7dd32d63da |
@@ -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", labelTranslationKey: "navbar.network", href: "/showcase", hasSubmenu: false },
|
||||
];
|
||||
|
||||
// Develop submenu data structure
|
||||
|
||||
@@ -66,6 +66,9 @@ module.exports = {
|
||||
/^container/, // All container classes
|
||||
/^row$/, // Row class
|
||||
/^col-/, // Column classes
|
||||
/^bds-grid__col/, // PageGrid column classes (dynamic span values)
|
||||
/^bds-grid__offset/, // PageGrid offset classes
|
||||
/^bds-[a-z0-9-]+--/, // BDS BEM modifier classes (e.g. bds-callout-media-banner--green, bds-tile-link--primary)
|
||||
/^g-/, // Gap utilities
|
||||
/^p-/, // Padding utilities
|
||||
/^m-/, // Margin utilities
|
||||
|
||||
@@ -1900,4 +1900,34 @@ html.dark {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// No Padding Modifier
|
||||
// =============================================================================
|
||||
// When .bds-btn--no-padding is applied, removes all padding and left-aligns content.
|
||||
// Useful for tertiary buttons in block layouts where left alignment is needed.
|
||||
// =============================================================================
|
||||
|
||||
.bds-btn--no-padding {
|
||||
padding: 0 !important;
|
||||
justify-content: flex-start !important;
|
||||
|
||||
// Override all state paddings
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
// Responsive overrides
|
||||
@include media-breakpoint-down(xl) {
|
||||
padding: 0 !important;
|
||||
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,12 @@ export interface ButtonProps {
|
||||
href?: string;
|
||||
/** Link target - only applies when href is provided */
|
||||
target?: '_self' | '_blank';
|
||||
/**
|
||||
* Force no padding and left-align text.
|
||||
* When true, removes all padding and aligns content to the left.
|
||||
* Useful for tertiary buttons in block layouts where left alignment is needed.
|
||||
*/
|
||||
forceNoPadding?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,6 +121,7 @@ export const Button: React.FC<ButtonProps> = ({
|
||||
ariaLabel,
|
||||
href,
|
||||
target = '_self',
|
||||
forceNoPadding = false,
|
||||
}) => {
|
||||
// Hide icon when disabled (per design spec)
|
||||
const shouldShowIcon = showIcon && !disabled;
|
||||
@@ -131,6 +138,7 @@ export const Button: React.FC<ButtonProps> = ({
|
||||
'bds-btn--disabled': disabled,
|
||||
'bds-btn--no-icon': !shouldShowIcon,
|
||||
'bds-btn--force-color': forceColor,
|
||||
'bds-btn--no-padding': forceNoPadding,
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
@@ -14,37 +14,40 @@
|
||||
// .bds-card-icon__label - Text label
|
||||
// .bds-card-icon__arrow - Arrow icon wrapper
|
||||
|
||||
@import '../../../styles/breakpoints';
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss where applicable.
|
||||
// Component-specific values (heights, icon sizes) remain local.
|
||||
|
||||
// Focus border
|
||||
// Focus border - uses centralized token
|
||||
$bds-card-icon-focus-border-color: $black;
|
||||
$bds-card-icon-focus-border-width: 2px;
|
||||
$bds-card-icon-focus-border-width: $bds-focus-border-width; // 2px from _spacing.scss
|
||||
|
||||
// Animation (matching TileLogo/CardOffgrid)
|
||||
$bds-card-icon-transition-duration: 200ms;
|
||||
$bds-card-icon-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
// Animation - uses centralized tokens from _animations.scss
|
||||
$bds-card-icon-transition-duration: $bds-transition-duration; // 200ms
|
||||
$bds-card-icon-transition-timing: $bds-transition-timing;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Responsive Size Tokens
|
||||
// -----------------------------------------------------------------------------
|
||||
// Heights and icon sizes are component-specific (not part of spacing scale)
|
||||
// Padding uses centralized spacing tokens
|
||||
|
||||
// SM breakpoint (mobile - default)
|
||||
$bds-card-icon-height-sm: 136px;
|
||||
$bds-card-icon-padding-sm: 8px;
|
||||
$bds-card-icon-padding-sm: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-card-icon-icon-size-sm: 56px;
|
||||
|
||||
// MD breakpoint (tablet)
|
||||
$bds-card-icon-height-md: 140px;
|
||||
$bds-card-icon-padding-md: 12px;
|
||||
$bds-card-icon-padding-md: $bds-space-md; // 12px - spacing('md')
|
||||
$bds-card-icon-icon-size-md: 60px;
|
||||
|
||||
// LG breakpoint (desktop)
|
||||
$bds-card-icon-height-lg: 144px;
|
||||
$bds-card-icon-padding-lg: 16px;
|
||||
$bds-card-icon-padding-lg: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-card-icon-icon-size-lg: 64px;
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
// =============================================================================
|
||||
// Design Tokens (from Figma)
|
||||
// =============================================================================
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss where applicable.
|
||||
// Component-specific values (heights, dimensions) remain local.
|
||||
|
||||
// Card dimensions - LG (Large) variant (default, ≥992px)
|
||||
$bds-card-image-height-lg: 620px;
|
||||
@@ -32,29 +34,29 @@ $bds-card-image-height-sm: 536px;
|
||||
$bds-card-image-image-height-sm: 268px;
|
||||
|
||||
// Spacing - LG (Large) variant (default, ≥992px)
|
||||
$bds-card-image-gap-lg: 24px; // Gap between image and content
|
||||
$bds-card-image-title-gap-lg: 12px; // Gap between title and subtitle
|
||||
$bds-card-image-content-padding: 8px; // Horizontal padding for content (same for all)
|
||||
$bds-card-image-button-margin-lg: 32px; // Margin above button
|
||||
$bds-card-image-gap-lg: $bds-space-2xl; // 24px - Gap between image and content
|
||||
$bds-card-image-title-gap-lg: $bds-space-md; // 12px - Gap between title and subtitle
|
||||
$bds-card-image-content-padding: $bds-space-sm; // 8px - Horizontal padding for content
|
||||
$bds-card-image-button-margin-lg: $bds-space-3xl; // 32px - Margin above button
|
||||
|
||||
// Spacing - MD (Medium) variant (576px - 991px)
|
||||
$bds-card-image-gap-md: 16px; // Gap between image and content
|
||||
$bds-card-image-title-gap-md: 8px; // Gap between title and subtitle
|
||||
$bds-card-image-button-margin-md: 24px; // Margin above button
|
||||
$bds-card-image-gap-md: $bds-space-lg; // 16px - Gap between image and content
|
||||
$bds-card-image-title-gap-md: $bds-space-sm; // 8px - Gap between title and subtitle
|
||||
$bds-card-image-button-margin-md: $bds-space-2xl; // 24px - Margin above button
|
||||
|
||||
// Spacing - SM (Small) variant (<576px)
|
||||
$bds-card-image-gap-sm: 16px; // Gap between image and content
|
||||
$bds-card-image-title-gap-sm: 8px; // Gap between title and subtitle
|
||||
$bds-card-image-button-margin-sm: 24px; // Margin above button
|
||||
$bds-card-image-gap-sm: $bds-space-lg; // 16px - Gap between image and content
|
||||
$bds-card-image-title-gap-sm: $bds-space-sm; // 8px - Gap between title and subtitle
|
||||
$bds-card-image-button-margin-sm: $bds-space-2xl; // 24px - Margin above button
|
||||
|
||||
// Colors
|
||||
$bds-card-image-bg: $white;
|
||||
$bds-card-image-image-bg: $gray-100;
|
||||
$bds-card-image-text-color: #141414; // Neutral black from Figma
|
||||
|
||||
// Animation
|
||||
$bds-card-image-transition-duration: 150ms;
|
||||
$bds-card-image-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
// Animation - uses centralized tokens from _animations.scss
|
||||
$bds-card-image-transition-duration: 150ms; // Component-specific (faster than default)
|
||||
$bds-card-image-transition-timing: $bds-transition-timing;
|
||||
|
||||
// =============================================================================
|
||||
// Base Card Styles
|
||||
|
||||
@@ -21,18 +21,20 @@
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss where applicable.
|
||||
// Component-specific values (dimensions, icon sizes) remain local.
|
||||
|
||||
// Dimensions (from Figma design spec)
|
||||
$bds-card-offgrid-width: 400px;
|
||||
$bds-card-offgrid-height: 480px;
|
||||
$bds-card-offgrid-padding: 24px;
|
||||
$bds-card-offgrid-padding: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-card-offgrid-icon-container: 84px;
|
||||
$bds-card-offgrid-icon-size: 68px;
|
||||
$bds-card-offgrid-content-gap: 40px;
|
||||
$bds-card-offgrid-content-gap: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
|
||||
// Animation (from Figma design spec)
|
||||
$bds-card-offgrid-transition-duration: 200ms;
|
||||
$bds-card-offgrid-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
// Animation - uses centralized tokens from _animations.scss
|
||||
$bds-card-offgrid-transition-duration: $bds-transition-duration; // 200ms
|
||||
$bds-card-offgrid-transition-timing: $bds-transition-timing;
|
||||
|
||||
// Typography - Title (now using .subhead-lg-l from _font.scss)
|
||||
// Typography - Description (now using .body-l from _font.scss)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss where applicable.
|
||||
|
||||
// Color variant map: (variant-name: (light-mode-bg, dark-mode-bg))
|
||||
// null for dark-mode-bg means no change in dark mode
|
||||
@@ -30,12 +31,12 @@ $bds-card-stat-variants: (
|
||||
// Text colors
|
||||
$bds-card-stat-text: $black; // Neutral black
|
||||
|
||||
// Spacing
|
||||
$bds-card-stat-gap: 8px;
|
||||
// Spacing - uses centralized tokens
|
||||
$bds-card-stat-gap: $bds-space-sm; // 8px - spacing('sm')
|
||||
|
||||
// Transitions
|
||||
$bds-card-stat-transition-duration: 150ms;
|
||||
$bds-card-stat-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
$bds-card-stat-transition-duration: 150ms; // Component-specific (faster than default)
|
||||
$bds-card-stat-transition-timing: $bds-transition-timing;
|
||||
|
||||
// =============================================================================
|
||||
// Base Card Styles
|
||||
@@ -43,15 +44,15 @@ $bds-card-stat-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
|
||||
.bds-card-stat {
|
||||
// Layout
|
||||
display: flex;
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
gap: 4px;
|
||||
padding: $bds-space-sm; // 8px - spacing('sm')
|
||||
gap: $bds-space-xs; // 4px - spacing('xs')
|
||||
|
||||
// Visual - default to lilac
|
||||
background-color: nth(map-get($bds-card-stat-variants, 'lilac'), 1);
|
||||
@@ -64,19 +65,19 @@ $bds-card-stat-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
|
||||
// Tablet (md) breakpoint
|
||||
@include media-breakpoint-up(md) {
|
||||
padding: 12px;
|
||||
padding: $bds-space-md; // 12px - spacing('md')
|
||||
}
|
||||
|
||||
// Desktop (lg+) breakpoint
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding: 16px;
|
||||
padding: $bds-space-lg; // 16px - spacing('lg')
|
||||
}
|
||||
|
||||
// Text section
|
||||
&__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: $bds-space-sm; // 8px - spacing('sm')
|
||||
}
|
||||
|
||||
// Statistic (large number)
|
||||
@@ -102,7 +103,7 @@ $bds-card-stat-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $bds-card-stat-gap;
|
||||
align-items: flex-start;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
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 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>
|
||||
);
|
||||
};
|
||||
|
||||
130
shared/components/CardTextIcon/CardTextIconCard.scss
Normal file
130
shared/components/CardTextIcon/CardTextIconCard.scss
Normal file
@@ -0,0 +1,130 @@
|
||||
// BDS CardTextIconCard Pattern Styles
|
||||
// Brand Design System - Card with icon, heading, and description
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-card-text-icon-card - Base card container
|
||||
// .bds-card-text-icon-card__icon - Icon container
|
||||
// .bds-card-text-icon-card__icon-img - Icon image
|
||||
// .bds-card-text-icon-card__heading - Card heading
|
||||
// .bds-card-text-icon-card__description - Card description (supports ReactNode/links)
|
||||
//
|
||||
// Built from Section Cards - Icon and Section Cards - Text Grid Figma designs
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
|
||||
// Icon sizes (responsive)
|
||||
$bds-card-text-icon-icon-size-sm: 32px;
|
||||
$bds-card-text-icon-icon-size-md: 36px;
|
||||
$bds-card-text-icon-icon-size-lg: 40px;
|
||||
|
||||
// Padding (uses BDS card presets from _spacing.scss)
|
||||
$bds-card-text-icon-padding-sm: $bds-padding-card-sm;
|
||||
$bds-card-text-icon-padding-md: $bds-padding-card-md;
|
||||
$bds-card-text-icon-padding-lg: $bds-padding-card-lg;
|
||||
|
||||
// Gaps (between icon, heading, description)
|
||||
$bds-card-text-icon-gap-sm: $bds-padding-card-sm;
|
||||
$bds-card-text-icon-gap-md: $bds-padding-card-md;
|
||||
$bds-card-text-icon-gap-lg: $bds-padding-card-lg;
|
||||
|
||||
// =============================================================================
|
||||
// Card Component
|
||||
// =============================================================================
|
||||
|
||||
.bds-card-text-icon-card {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
text-decoration: none;
|
||||
box-sizing: border-box;
|
||||
border-left: 1px solid $gray-300;
|
||||
margin-bottom: $bds-space-2xl;
|
||||
// When used standalone (not as grid column), take full width
|
||||
&:not(.bds-card-text-icon-card--grid-col) {
|
||||
width: 100%;
|
||||
}
|
||||
padding: $bds-card-text-icon-padding-sm;
|
||||
gap: $bds-card-text-icon-gap-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding: $bds-card-text-icon-padding-md;
|
||||
gap: $bds-card-text-icon-gap-md;
|
||||
margin-bottom: $bds-space-3xl;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding: $bds-card-text-icon-padding-lg;
|
||||
gap: $bds-card-text-icon-gap-lg;
|
||||
margin-bottom: $bds-space-4xl;
|
||||
}
|
||||
|
||||
// Aspect ratio foundation - when CSS variable is set via aspectRatio prop
|
||||
&[style*="--bds-card-text-icon-aspect-ratio"] {
|
||||
aspect-ratio: var(--bds-card-text-icon-aspect-ratio);
|
||||
}
|
||||
|
||||
@include bds-theme-mode(dark) {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Icon
|
||||
// =============================================================================
|
||||
|
||||
.bds-card-text-icon-card__icon {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-space-lg;
|
||||
&-img {
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
width: $bds-card-text-icon-icon-size-sm;
|
||||
height: $bds-card-text-icon-icon-size-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
width: $bds-card-text-icon-icon-size-md;
|
||||
height: $bds-card-text-icon-icon-size-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
width: $bds-card-text-icon-icon-size-lg;
|
||||
height: $bds-card-text-icon-icon-size-lg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Heading
|
||||
// =============================================================================
|
||||
|
||||
.bds-card-text-icon-card__heading {
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Description (supports ReactNode - links, formatted text, etc.)
|
||||
// =============================================================================
|
||||
|
||||
.bds-card-text-icon-card__description {
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
|
||||
&:hover {
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
121
shared/components/CardTextIcon/CardTextIconCard.tsx
Normal file
121
shared/components/CardTextIcon/CardTextIconCard.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid } from '../PageGrid/page-grid';
|
||||
import type { ResponsiveValue, PageGridSpanValue } from '../PageGrid/page-grid';
|
||||
|
||||
export interface CardTextIconCardProps {
|
||||
/** Icon image URL */
|
||||
icon?: string;
|
||||
/** Alt text for the icon image */
|
||||
iconAlt?: string;
|
||||
/** Card heading */
|
||||
heading: string;
|
||||
/** Card description; accepts rich content (e.g., text with inline links) */
|
||||
description: React.ReactNode | string;
|
||||
/** Optional aspect ratio for future use; applied via CSS variable */
|
||||
aspectRatio?: number;
|
||||
/** When provided, renders as PageGrid.Col as="li" with this span—card becomes the grid column */
|
||||
gridColSpan?: ResponsiveValue<PageGridSpanValue>;
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
/** Optional height and width for the icon image */
|
||||
height?: number;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* CardTextIconCard Component
|
||||
*
|
||||
* A card component featuring an icon, heading, and description.
|
||||
* Built from Section Cards - Icon and Section Cards - Text Grid Figma designs.
|
||||
*
|
||||
* The description accepts ReactNode so it can include hyperlinks and other rich content.
|
||||
*
|
||||
* @example
|
||||
* // Basic usage
|
||||
* <CardTextIconCard
|
||||
* icon="/icons/docs.svg"
|
||||
* iconAlt="Documentation"
|
||||
* heading="Documentation"
|
||||
* description="Access everything you need to get started with the XRPL."
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // With inline link in description
|
||||
* <CardTextIconCard
|
||||
* icon="/icons/docs.svg"
|
||||
* heading="Documentation"
|
||||
* description={
|
||||
* <>
|
||||
* Learn more in our{' '}
|
||||
* <a href="/docs">documentation</a>.
|
||||
* </>
|
||||
* }
|
||||
* />
|
||||
*/
|
||||
const cardContent = (
|
||||
heading: string,
|
||||
description: React.ReactNode | string,
|
||||
icon?: string,
|
||||
iconAlt?: string,
|
||||
iconHeight?: number,
|
||||
iconWidth?: number
|
||||
) => (
|
||||
<>
|
||||
<div className="bds-card-text-icon-card__icon">
|
||||
{icon && (
|
||||
<img
|
||||
src={icon}
|
||||
alt={iconAlt}
|
||||
{...(iconHeight != null && { height: iconHeight })}
|
||||
{...(iconWidth != null && { width: iconWidth })}
|
||||
className="bds-card-text-icon-card__icon-img"
|
||||
/>
|
||||
)}
|
||||
<strong className="bds-card-text-icon-card__heading sh-md-r">{heading}</strong>
|
||||
</div>
|
||||
<p className="bds-card-text-icon-card__description body-l">
|
||||
{description}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
export const CardTextIconCard: React.FC<CardTextIconCardProps> = ({
|
||||
icon,
|
||||
iconAlt = '',
|
||||
heading,
|
||||
description,
|
||||
aspectRatio,
|
||||
gridColSpan,
|
||||
className,
|
||||
height,
|
||||
width
|
||||
}) => {
|
||||
const style = aspectRatio
|
||||
? ({ '--bds-card-text-icon-aspect-ratio': aspectRatio } as React.CSSProperties)
|
||||
: undefined;
|
||||
|
||||
if (gridColSpan) {
|
||||
return (
|
||||
<PageGrid.Col
|
||||
as="li"
|
||||
span={gridColSpan}
|
||||
className={clsx('bds-card-text-icon-card', 'bds-card-text-icon-card--grid-col', className)}
|
||||
style={style}
|
||||
>
|
||||
{cardContent(heading, description, icon, iconAlt, height, width)}
|
||||
</PageGrid.Col>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('bds-card-text-icon-card', className)}
|
||||
style={style}
|
||||
>
|
||||
{cardContent(heading, description, icon, iconAlt, height, width)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardTextIconCard;
|
||||
116
shared/components/CardTextIcon/README.md
Normal file
116
shared/components/CardTextIcon/README.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# CardTextIconCard Component
|
||||
|
||||
A card component featuring an icon, heading, and description. Built from Section Cards - Icon and Section Cards - Text Grid Figma designs.
|
||||
|
||||
## Overview
|
||||
|
||||
CardTextIconCard displays an icon at the top, followed by a heading and description. The description accepts `ReactNode`, so it can include hyperlinks and other rich content. No buttons; links are inline within the description.
|
||||
|
||||
## Features
|
||||
|
||||
- **Icon + Text Layout**: Icon container, heading, and description in a vertical stack (optional)
|
||||
- **Rich Description**: `description` accepts `ReactNode` for inline links and formatted content
|
||||
- **Aspect Ratio Foundation**: Optional `aspectRatio` prop for future responsive sizing
|
||||
- **Light/Dark Mode**: Full theming support
|
||||
- **Responsive Design**: Adaptive icon size and spacing across breakpoints
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```tsx
|
||||
<CardTextIconCard
|
||||
icon="/icons/docs.svg"
|
||||
iconAlt="Documentation"
|
||||
heading="Documentation"
|
||||
description="Access everything you need to get started with the XRPL."
|
||||
/>
|
||||
```
|
||||
|
||||
### With Inline Link in Description
|
||||
|
||||
```tsx
|
||||
<CardTextIconCard
|
||||
icon="/icons/docs.svg"
|
||||
heading="Documentation"
|
||||
description={
|
||||
<>
|
||||
Learn more in our{' '}
|
||||
<a href="/docs">documentation</a>.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
```
|
||||
|
||||
### With Aspect Ratio
|
||||
|
||||
```tsx
|
||||
<CardTextIconCard
|
||||
icon="/icons/docs.svg"
|
||||
heading="Documentation"
|
||||
description="Access everything you need."
|
||||
aspectRatio={4 / 3}
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
### CardTextIconCardProps
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `icon` | `string` | Optional | Icon image URL |
|
||||
| `iconAlt` | `string` | `''` | Optional | Alt text for the icon image |
|
||||
| `heading` | `string` | Required | Card heading |
|
||||
| `description` | `React.ReactNode` | Required | Card description; accepts rich content (e.g., text with inline links) |
|
||||
| `aspectRatio` | `number` | - | Optional ratio for future use; applied via CSS variable |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
|
||||
## Component Structure
|
||||
|
||||
```tsx
|
||||
<>
|
||||
<div className="bds-card-text-icon-card__icon">
|
||||
{icon && (
|
||||
<img
|
||||
src={icon}
|
||||
alt={iconAlt}
|
||||
{...(iconHeight != null && { height: iconHeight })}
|
||||
{...(iconWidth != null && { width: iconWidth })}
|
||||
className="bds-card-text-icon-card__icon-img"
|
||||
/>
|
||||
)}
|
||||
<strong className="bds-card-text-icon-card__heading sh-md-r">{heading}</strong>
|
||||
</div>
|
||||
<p className="bds-card-text-icon-card__description body-l">
|
||||
{description}
|
||||
</p>
|
||||
</>
|
||||
```
|
||||
|
||||
## Responsive Sizing
|
||||
|
||||
| Breakpoint | Icon Size | Padding | Gap |
|
||||
|------------|-----------|---------|-----|
|
||||
| Base (< 576px) | 40px | 16px | 16px |
|
||||
| MD (576px - 991px) | 36px | 20px | 20px |
|
||||
| LG (≥ 992px) | 64px | 32px | 24px |
|
||||
|
||||
## Files
|
||||
|
||||
- `CardTextIconCard.tsx` - React component with TypeScript
|
||||
- `CardTextIconCard.scss` - Styles with BEM naming
|
||||
- `index.ts` - Barrel exports
|
||||
- `README.md` - This file
|
||||
|
||||
## Import
|
||||
|
||||
```tsx
|
||||
import { CardTextIconCard } from 'shared/components/CardTextIcon';
|
||||
// or
|
||||
import { CardTextIconCard, type CardTextIconCardProps } from 'shared/components/CardTextIcon';
|
||||
```
|
||||
|
||||
## Design System
|
||||
|
||||
Part of the Brand Design System (BDS) with `bds-` namespace prefix.
|
||||
1
shared/components/CardTextIcon/index.ts
Normal file
1
shared/components/CardTextIcon/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { CardTextIconCard, type CardTextIconCardProps } from './CardTextIconCard';
|
||||
@@ -25,6 +25,101 @@ $bds-carousel-button-size-lg: 40px; // Desktop
|
||||
// Transition
|
||||
$bds-carousel-button-transition: 200ms cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
|
||||
// =============================================================================
|
||||
// Color Variant Configuration Maps
|
||||
// =============================================================================
|
||||
|
||||
// Dark Mode color variants
|
||||
$bds-carousel-button-variants-dark: (
|
||||
'green': (
|
||||
'bg': $green-300,
|
||||
'color': $black,
|
||||
'hover': $green-400,
|
||||
'active': $green-300,
|
||||
'disabled-bg': $green-500,
|
||||
'disabled-color': #F0F3F7,
|
||||
'disabled-opacity': 0.5
|
||||
),
|
||||
'neutral': (
|
||||
'bg': $gray-300,
|
||||
'color': $black,
|
||||
'hover': $gray-400,
|
||||
'active': $gray-300,
|
||||
'disabled-bg': $gray-500,
|
||||
'disabled-color': $gray-300,
|
||||
'disabled-opacity': 0.5
|
||||
),
|
||||
'black': (
|
||||
'bg': $white,
|
||||
'color': $black,
|
||||
'hover': $gray-300,
|
||||
'active': $white,
|
||||
'disabled-bg': $gray-500,
|
||||
'disabled-color': null,
|
||||
'disabled-opacity': 0.5
|
||||
)
|
||||
);
|
||||
|
||||
// Light Mode color variants
|
||||
$bds-carousel-button-variants-light: (
|
||||
'green': (
|
||||
'bg': $green-300,
|
||||
'color': $black,
|
||||
'hover': $green-200,
|
||||
'active': $green-300,
|
||||
'disabled-bg': $green-100,
|
||||
'disabled-color': $gray-300,
|
||||
'disabled-opacity': 1
|
||||
),
|
||||
'neutral': (
|
||||
'bg': $gray-300,
|
||||
'color': $black,
|
||||
'hover': $gray-200,
|
||||
'active': $gray-300,
|
||||
'disabled-bg': $gray-100,
|
||||
'disabled-color': $gray-300,
|
||||
'disabled-opacity': 1
|
||||
),
|
||||
'black': (
|
||||
'bg': $black,
|
||||
'color': $white,
|
||||
'hover': $gray-500,
|
||||
'active': $black,
|
||||
'disabled-bg': #F0F3F7,
|
||||
'disabled-color': $gray-300,
|
||||
'disabled-opacity': 1
|
||||
)
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// Mixin: Apply Color Variant Styles
|
||||
// =============================================================================
|
||||
|
||||
@mixin carousel-button-variant($variant-name, $config) {
|
||||
.bds-carousel-button--#{$variant-name} {
|
||||
background-color: map-get($config, 'bg');
|
||||
color: map-get($config, 'color');
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: map-get($config, 'hover');
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: map-get($config, 'active');
|
||||
}
|
||||
|
||||
&.bds-carousel-button--disabled,
|
||||
&:disabled {
|
||||
background-color: map-get($config, 'disabled-bg') !important;
|
||||
@if map-get($config, 'disabled-color') {
|
||||
color: map-get($config, 'disabled-color') !important;
|
||||
}
|
||||
opacity: map-get($config, 'disabled-opacity') !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Base Button Styles
|
||||
// =============================================================================
|
||||
@@ -88,72 +183,9 @@ $bds-carousel-button-transition: 200ms cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
// DARK MODE (Default) - Color Variants
|
||||
// =============================================================================
|
||||
|
||||
// Green button variant - Dark Mode
|
||||
// Enabled: green-300, Hover: green-400, Active/Pressed: green-300, Disabled: green-500 @ 0.5 opacity
|
||||
.bds-carousel-button--green {
|
||||
background-color: $green-300;
|
||||
color: $black;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $green-400;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $green-300;
|
||||
}
|
||||
|
||||
&.bds-carousel-button--disabled,
|
||||
&:disabled {
|
||||
background-color: $green-500 !important;
|
||||
color: #F0F3F7 !important;
|
||||
opacity: 0.5 !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Neutral/Grey button variant - Dark Mode
|
||||
// Enabled: gray-300, Hover: gray-400, Active/Pressed: gray-300, Disabled: gray-500 @ 0.5 opacity
|
||||
.bds-carousel-button--neutral {
|
||||
background-color: $gray-300;
|
||||
color: $black;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $gray-400;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $gray-300;
|
||||
}
|
||||
|
||||
&.bds-carousel-button--disabled,
|
||||
&:disabled {
|
||||
background-color: $gray-500 !important;
|
||||
color: $gray-300 !important;
|
||||
opacity: 0.5 !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Black variant becomes White in Dark Mode
|
||||
// Enabled: white, Hover: gray-300, Active/Pressed: white, Disabled: gray-500 @ 0.5 opacity
|
||||
.bds-carousel-button--black {
|
||||
background-color: $white;
|
||||
color: $black;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $gray-300;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
&.bds-carousel-button--disabled,
|
||||
&:disabled {
|
||||
background-color: $gray-500 !important;
|
||||
opacity: 0.5 !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
// Generate all dark mode variant styles using the mixin
|
||||
@each $variant-name, $config in $bds-carousel-button-variants-dark {
|
||||
@include carousel-button-variant($variant-name, $config);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -172,73 +204,9 @@ html.light {
|
||||
}
|
||||
}
|
||||
|
||||
// Green button variant - Light Mode
|
||||
// Enabled: green-300, Hover: green-200, Active/Pressed: green-300, Disabled: green-100
|
||||
.bds-carousel-button--green {
|
||||
background-color: $green-300;
|
||||
color: $black;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $green-200;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $green-300;
|
||||
}
|
||||
|
||||
&.bds-carousel-button--disabled,
|
||||
&:disabled {
|
||||
background-color: $green-100 !important;
|
||||
color: $gray-300 !important;
|
||||
opacity: 1 !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Neutral/Grey button variant - Light Mode
|
||||
// Enabled: gray-300, Hover: gray-200, Active/Pressed: gray-300, Disabled: gray-100
|
||||
.bds-carousel-button--neutral {
|
||||
background-color: $gray-300;
|
||||
color: $black;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $gray-300;
|
||||
}
|
||||
|
||||
&.bds-carousel-button--disabled,
|
||||
&:disabled {
|
||||
background-color: $gray-100 !important;
|
||||
color: $gray-300 !important;
|
||||
opacity: 1 !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Black button variant - Light Mode
|
||||
// Enabled: black, Hover: gray-500, Active/Pressed: black, Disabled: #F0F3F7
|
||||
.bds-carousel-button--black {
|
||||
background-color: $black;
|
||||
color: $white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $gray-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $black;
|
||||
}
|
||||
|
||||
&.bds-carousel-button--disabled,
|
||||
&:disabled {
|
||||
background-color: #F0F3F7 !important;
|
||||
color: $gray-300 !important;
|
||||
opacity: 1 !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
// Generate all light mode variant overrides using the mixin
|
||||
@each $variant-name, $config in $bds-carousel-button-variants-light {
|
||||
@include carousel-button-variant($variant-name, $config);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
// Styles for link arrow icons with hover animations that transform arrow to chevron
|
||||
// Animation: 150ms with custom cubic-bezier per Figma specs
|
||||
|
||||
@import "../../../styles/_colors.scss";
|
||||
|
||||
// Animation timing per Figma
|
||||
$bds-link-transition-duration: 150ms;
|
||||
$bds-link-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
// Dark mode colors per Figma: Enabled=green-300, Hover/Focus=green-200+underline,
|
||||
// Active=green-300+underline, Visited=lilac-300, Disabled=gray-400 (#8A919A)
|
||||
|
||||
@import "../../../styles/_colors.scss";
|
||||
|
||||
// Base link styles
|
||||
.bds-link {
|
||||
display: inline-flex;
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// A namespaced grid layer that reuses Bootstrap's grid mixins while providing
|
||||
// XRPL-specific gutters and container padding.
|
||||
|
||||
$bds-grid-gutter: 8px;
|
||||
//
|
||||
// Note: $bds-grid-gutter is now defined in _spacing.scss as the single source
|
||||
// of truth for grid gutter spacing (8px).
|
||||
|
||||
// Custom mixin that accounts for gap spacing in width calculations
|
||||
@mixin bds-make-col($size, $columns) {
|
||||
@@ -17,6 +18,21 @@ $bds-grid-gutter: 8px;
|
||||
width: calc(((100% - (#{$bds-grid-gutter} * (#{$columns} - 1))) / #{$columns}) * #{$size} + (#{$bds-grid-gutter} * (#{$size} - 1)));
|
||||
}
|
||||
|
||||
// Custom mixin that accounts for gap spacing in offset calculations
|
||||
@mixin bds-make-col-offset($size, $columns) {
|
||||
// Calculate margin-left accounting for gap spacing
|
||||
// Formula: (width per column * offset) + (gap * offset)
|
||||
// This accounts for both the column widths AND the gaps between them
|
||||
// Total available width: 100% - (gap * total gaps in grid)
|
||||
// Width per column: available width / columns
|
||||
// For offset of $size: (width per column * size) + (gap * size)
|
||||
@if $size == 0 {
|
||||
margin-left: 0;
|
||||
} @else {
|
||||
margin-left: calc(((100% - (#{$bds-grid-gutter} * (#{$columns} - 1))) / #{$columns}) * #{$size} + (#{$bds-grid-gutter} * #{$size}));
|
||||
}
|
||||
}
|
||||
|
||||
@mixin bds-grid-generate-cols($columns, $suffix: null) {
|
||||
@for $i from 1 through $columns {
|
||||
$selector: if($suffix, ".bds-grid__col-#{$suffix}-#{$i}", ".bds-grid__col-#{$i}");
|
||||
@@ -49,7 +65,7 @@ $bds-grid-gutter: 8px;
|
||||
$selector: if($suffix, ".bds-grid__offset-#{$suffix}-#{$i}", ".bds-grid__offset-#{$i}");
|
||||
|
||||
#{$selector} {
|
||||
@include make-col-offset($i, $columns);
|
||||
@include bds-make-col-offset($i, $columns);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,4 +186,18 @@ $bds-grid-gutter: 8px;
|
||||
@include bds-grid-generate-cols(12, 'xl');
|
||||
@include bds-grid-generate-auto('xl');
|
||||
@include bds-grid-generate-offsets(12, 'xl');
|
||||
}
|
||||
}
|
||||
|
||||
// Polymorphic as="li" — when Col renders as li, override list-item defaults
|
||||
// so flex layout and width calculations apply correctly
|
||||
li.bds-grid__col {
|
||||
list-style: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Polymorphic as="ul" — when Row renders as ul, reset list styling
|
||||
ul.bds-grid__row {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -7,19 +7,24 @@ type PageGridElementProps = React.HTMLAttributes<HTMLDivElement>;
|
||||
export type PageGridBreakpoint = "base" | "sm" | "md" | "lg" | "xl";
|
||||
|
||||
// Define the ResponsiveValue type using Partial<Record> for breakpoints
|
||||
type ResponsiveValue<T> = T | Partial<Record<PageGridBreakpoint, T>>;
|
||||
export type ResponsiveValue<T> = T | Partial<Record<PageGridBreakpoint, T>>;
|
||||
|
||||
export interface PageGridProps extends PageGridElementProps {
|
||||
/** Container layout type - "standard" (default) or "wide" (1504px max-width, 144px padding at xl breakpoint) */
|
||||
containerType?: "standard" | "wide";
|
||||
}
|
||||
|
||||
export interface PageGridRowProps extends PageGridElementProps {}
|
||||
export interface PageGridRowProps extends PageGridElementProps {
|
||||
/** Polymorphic element - e.g. "ul" for semantic list markup */
|
||||
as?: React.ElementType;
|
||||
}
|
||||
|
||||
type PageGridSpanValue = number | "auto" | "fill";
|
||||
export type PageGridSpanValue = number | "auto" | "fill";
|
||||
type PageGridOffsetValue = number;
|
||||
|
||||
export interface PageGridColProps extends PageGridElementProps {
|
||||
/** Polymorphic element - e.g. "li" for semantic list markup */
|
||||
as?: React.ElementType;
|
||||
span?: ResponsiveValue<PageGridSpanValue>;
|
||||
offset?: ResponsiveValue<PageGridOffsetValue>;
|
||||
}
|
||||
@@ -85,17 +90,17 @@ PageGridRoot.displayName = "PageGrid";
|
||||
|
||||
|
||||
// --- PageGrid.Row Component ---
|
||||
const PageGridRow = React.forwardRef<HTMLDivElement, PageGridRowProps>(
|
||||
({ className, ...rest }, ref) => (
|
||||
<div ref={ref} className={clsx("bds-grid__row", className)} {...rest} />
|
||||
const PageGridRow = React.forwardRef<HTMLElement, PageGridRowProps>(
|
||||
({ as: Component = "div", className, ...rest }, ref) => (
|
||||
<Component ref={ref} className={clsx("bds-grid__row", className)} {...rest} />
|
||||
)
|
||||
);
|
||||
|
||||
PageGridRow.displayName = "PageGridRow"; // Renamed display name for clarity
|
||||
PageGridRow.displayName = "PageGridRow";
|
||||
|
||||
// --- PageGrid.Col Component ---
|
||||
const PageGridCol = React.forwardRef<HTMLDivElement, PageGridColProps>((props, ref) => {
|
||||
const { className, span, offset, ...rest } = props;
|
||||
const PageGridCol = React.forwardRef<HTMLElement, PageGridColProps>((props, ref) => {
|
||||
const { as: Component = "div", className, span, offset, ...rest } = props;
|
||||
const spanClasses: string[] = [];
|
||||
const offsetClasses: string[] = [];
|
||||
|
||||
@@ -151,10 +156,9 @@ const PageGridCol = React.forwardRef<HTMLDivElement, PageGridColProps>((props, r
|
||||
|
||||
// Ensure the base class is always applied for styling
|
||||
return (
|
||||
<div
|
||||
<Component
|
||||
ref={ref}
|
||||
// Note: Added "bds-grid__col" base class for consistent column initialization
|
||||
className={clsx("bds-grid__col", className, spanClasses, offsetClasses)}
|
||||
className={clsx("bds-grid__col", className, spanClasses, offsetClasses)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
// =============================================================================
|
||||
// BDS StandardCard Component Styles
|
||||
// =============================================================================
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
|
||||
.bds-standard-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
padding: $bds-space-lg; // 16px - spacing('lg')
|
||||
width: 100%;
|
||||
height: 100%; // Stretch to fill parent column (ensures equal heights in grid rows)
|
||||
// aspect-ratio sets a preferred ratio but allows growth if content requires more space
|
||||
// The card will be at least this tall, but can grow taller if needed
|
||||
aspect-ratio: 4/3;
|
||||
gap: 16px;
|
||||
gap: $bds-space-lg; // 16px - spacing('lg')
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
aspect-ratio: 1;
|
||||
padding: 20px;
|
||||
padding: $bds-space-xl; // 20px - spacing('xl')
|
||||
}
|
||||
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding: 24px;
|
||||
padding: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
}
|
||||
|
||||
&#{&}--neutral {
|
||||
@@ -39,13 +44,13 @@
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: $bds-space-sm; // 8px - spacing('sm')
|
||||
width: 100%;
|
||||
color: $bds-btn-neutral-black;
|
||||
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: 16px;
|
||||
gap: $bds-space-lg; // 16px - spacing('lg')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
isEnvironment,
|
||||
isEmpty,
|
||||
} from "../../utils";
|
||||
import { DesignConstrainedCallToActionsProps } from "shared/utils/types";
|
||||
|
||||
/**
|
||||
* Available background color variants for StandardCard:
|
||||
@@ -16,11 +17,13 @@ import {
|
||||
*/
|
||||
export type StandardCardVariant = "neutral" | "green" | "yellow" | "blue";
|
||||
|
||||
export interface StandardCardProps extends React.ComponentPropsWithoutRef<"article"> {
|
||||
export interface StandardCardProps
|
||||
extends
|
||||
React.ComponentPropsWithoutRef<"article">,
|
||||
DesignConstrainedCallToActionsProps {
|
||||
headline: React.ReactNode;
|
||||
/** Background color variant */
|
||||
variant: StandardCardVariant;
|
||||
callsToAction: [DesignConstrainedButtonProps, DesignConstrainedButtonProps?];
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -46,7 +49,7 @@ const StandardCard = forwardRef<HTMLElement, StandardCardProps>(
|
||||
|
||||
const [primaryButton, secondaryButton] = callsToAction;
|
||||
|
||||
const hasButtons = callsToAction.some((button) => button !== undefined);
|
||||
const hasButtons = callsToAction.some((button) => !isEmpty(button));
|
||||
|
||||
if (!headline) {
|
||||
if (isEnvironment("development")) {
|
||||
|
||||
@@ -24,18 +24,20 @@
|
||||
// =============================================================================
|
||||
// Design Tokens from Figma
|
||||
// =============================================================================
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss where applicable.
|
||||
// Component-specific values (heights, max-widths) remain local.
|
||||
|
||||
// Card internal padding
|
||||
$bds-text-card-padding-mobile: 16px;
|
||||
$bds-text-card-padding-tablet: 20px;
|
||||
$bds-text-card-padding-desktop: 24px;
|
||||
// Card internal padding - uses centralized spacing tokens
|
||||
$bds-text-card-padding-mobile: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-text-card-padding-tablet: $bds-space-xl; // 20px - spacing('xl')
|
||||
$bds-text-card-padding-desktop: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
|
||||
// Card heights (fixed per breakpoint)
|
||||
// Card heights (fixed per breakpoint) - component-specific
|
||||
$bds-text-card-height-mobile: 274px;
|
||||
$bds-text-card-height-tablet: 309px;
|
||||
$bds-text-card-height-desktop: 340px;
|
||||
|
||||
// Card description max-width (from Figma)
|
||||
// Card description max-width (from Figma) - component-specific
|
||||
$bds-text-card-description-max-width: 478px;
|
||||
|
||||
// Colors - Light Mode (from Figma)
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
// .bds-tile-logo--disabled - Disabled state modifier
|
||||
// .bds-tile-logo__overlay - Hover gradient overlay (window shade animation)
|
||||
// .bds-tile-logo__image - Logo image element
|
||||
|
||||
// Grid gutter (matching PageGrid)
|
||||
$bds-grid-gutter: 8px;
|
||||
//
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
// $bds-grid-gutter is now defined in _spacing.scss as the single source of truth.
|
||||
|
||||
// Focus border colors (component-specific, dark mode default)
|
||||
$bds-tile-logo-focus-border-light: $black;
|
||||
|
||||
188
shared/components/Video/Video.scss
Normal file
188
shared/components/Video/Video.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
231
shared/components/Video/Video.tsx
Normal file
231
shared/components/Video/Video.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import React, { useCallback, useEffect } 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 */
|
||||
export type VideoSourceEmbedCode = {
|
||||
type: 'embed';
|
||||
embedCode: string;
|
||||
embedUrl?: never;
|
||||
};
|
||||
|
||||
/** Embed source - direct embed URL (safer, preferred) */
|
||||
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) */
|
||||
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 */
|
||||
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 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]);
|
||||
|
||||
const handleBackdropClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) closeModal();
|
||||
},
|
||||
[closeModal]
|
||||
);
|
||||
|
||||
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={handleBackdropClick}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="bds-video-modal__content">
|
||||
<button
|
||||
type="button"
|
||||
className="bds-video-modal__close"
|
||||
onClick={closeModal}
|
||||
aria-label="Close video"
|
||||
>
|
||||
<span aria-hidden>×</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;
|
||||
1
shared/components/Video/index.ts
Normal file
1
shared/components/Video/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Video, type VideoProps, type VideoSource } from './Video';
|
||||
@@ -5,6 +5,9 @@
|
||||
// .bds-button-group - Base component
|
||||
// .bds-button-group--gap-none - No gap between buttons on tablet+ (0px)
|
||||
// .bds-button-group--gap-small - Small gap between buttons on tablet+ (8px)
|
||||
// .bds-button-group--block - Block layout for 3+ buttons (all tertiary)
|
||||
//
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
|
||||
// =============================================================================
|
||||
// Base Component Styles
|
||||
@@ -13,10 +16,10 @@
|
||||
.bds-button-group {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
@extend .align-items-start;
|
||||
@extend .flex-wrap;
|
||||
gap: 8px;
|
||||
|
||||
align-items: start;
|
||||
gap: $bds-space-sm; // 8px - spacing('sm')
|
||||
|
||||
// Tablet breakpoint - horizontal layout
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row !important;
|
||||
@@ -31,13 +34,28 @@
|
||||
.bds-button-group--gap-none {
|
||||
// Tablet breakpoint - no gap
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: 0px;
|
||||
gap: $bds-space-none; // 0px - spacing('none')
|
||||
}
|
||||
}
|
||||
|
||||
.bds-button-group--gap-small {
|
||||
// Tablet breakpoint - keep 8px gap
|
||||
// Tablet breakpoint - 4px gap
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: 4px;
|
||||
gap: $bds-space-xs; // 4px - spacing('xs')
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Block Layout Modifier (3+ buttons)
|
||||
// =============================================================================
|
||||
|
||||
.bds-button-group--block {
|
||||
// Override default flex layout - force column layout on all screen sizes
|
||||
flex-direction: column !important;
|
||||
gap: $bds-space-lg !important; // 16px - spacing('lg')
|
||||
|
||||
// All buttons should be full width in block layout
|
||||
.bds-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,84 +13,248 @@ export interface ButtonConfig {
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export interface ButtonGroupValidationResult {
|
||||
/** The validated and potentially trimmed list of buttons */
|
||||
buttons: ButtonConfig[];
|
||||
/** Whether the button list is valid and should render */
|
||||
isValid: boolean;
|
||||
/** True if there are valid buttons to render (convenience flag) */
|
||||
hasButtons: boolean;
|
||||
/** Any warnings generated during validation */
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and processes a ButtonConfig array for ButtonGroup.
|
||||
*
|
||||
* Performs the following validations:
|
||||
* - Applies maxButtons limit if specified
|
||||
* - Checks for empty button arrays
|
||||
* - Validates individual button configs (label required, href or onClick recommended)
|
||||
* - Automatically logs warnings in development mode
|
||||
*
|
||||
* @param buttons - Array of button configurations (can be undefined)
|
||||
* @param maxButtons - Optional maximum number of buttons to render
|
||||
* @param autoLogWarnings - Whether to automatically log warnings in development mode (default: true)
|
||||
* @returns Validation result with processed buttons, validity flag, hasButtons flag, and warnings
|
||||
*
|
||||
* @example
|
||||
* // Basic usage with auto-logging
|
||||
* const validation = validateButtonGroup(buttons, 2);
|
||||
* if (validation.hasButtons) {
|
||||
* <ButtonGroup buttons={validation.buttons} />
|
||||
* }
|
||||
*
|
||||
* @example
|
||||
* // Disable auto-logging
|
||||
* const validation = validateButtonGroup(buttons, 2, false);
|
||||
* // Handle warnings manually
|
||||
* validation.warnings.forEach(w => customLogger(w));
|
||||
*/
|
||||
export function validateButtonGroup(
|
||||
buttons: ButtonConfig[] | undefined,
|
||||
maxButtons?: number,
|
||||
autoLogWarnings: boolean = true
|
||||
): ButtonGroupValidationResult {
|
||||
// Handle undefined/null buttons
|
||||
if (!buttons || buttons.length === 0) {
|
||||
return {
|
||||
buttons: [],
|
||||
isValid: false,
|
||||
hasButtons: false,
|
||||
warnings: []
|
||||
};
|
||||
}
|
||||
const warnings: string[] = [];
|
||||
let buttonList = [...buttons];
|
||||
|
||||
// Validate individual button configs
|
||||
buttonList.forEach((button, index) => {
|
||||
if (!button.label || button.label.trim() === '') {
|
||||
warnings.push(
|
||||
`[ButtonGroup] Button at index ${index} is missing a label. This button may not render correctly.`
|
||||
);
|
||||
}
|
||||
if (!button.href && !button.onClick) {
|
||||
warnings.push(
|
||||
`[ButtonGroup] Button "${button.label || `at index ${index}`}" has no href or onClick. Consider adding an action.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Apply maxButtons limit if specified
|
||||
if (maxButtons !== undefined && maxButtons > 0 && buttons.length > maxButtons) {
|
||||
warnings.push(
|
||||
`[ButtonGroup] ${buttons.length} buttons were passed but maxButtons is set to ${maxButtons}. ` +
|
||||
`Only the first ${maxButtons} button(s) will be rendered.`
|
||||
);
|
||||
buttonList = buttonList.slice(0, maxButtons);
|
||||
}
|
||||
|
||||
// Check for empty array
|
||||
if (buttonList.length === 0) {
|
||||
warnings.push(
|
||||
`[ButtonGroup] No buttons to render. ` +
|
||||
`Either an empty buttons array was passed or all buttons were removed by maxButtons limit.`
|
||||
);
|
||||
|
||||
// Auto-log warnings in development mode
|
||||
if (autoLogWarnings && process.env.NODE_ENV === 'development' && warnings.length > 0) {
|
||||
warnings.forEach(warning => console.warn(warning));
|
||||
}
|
||||
|
||||
return { buttons: [], isValid: false, hasButtons: false, warnings };
|
||||
}
|
||||
|
||||
// Auto-log warnings in development mode
|
||||
if (autoLogWarnings && process.env.NODE_ENV === 'development' && warnings.length > 0) {
|
||||
warnings.forEach(warning => console.warn(warning));
|
||||
}
|
||||
|
||||
const hasButtons = buttonList.length > 0;
|
||||
return { buttons: buttonList, isValid: true, hasButtons, warnings };
|
||||
}
|
||||
|
||||
export interface ButtonGroupProps {
|
||||
/** Primary button configuration */
|
||||
primaryButton?: ButtonConfig;
|
||||
/** Tertiary button configuration */
|
||||
tertiaryButton?: ButtonConfig;
|
||||
/** Array of button configurations
|
||||
* - 1 button: renders with singleButtonVariant (default: primary)
|
||||
* - 2 buttons: first as primary, second as tertiary
|
||||
* - 3+ buttons: all tertiary in block layout
|
||||
*/
|
||||
buttons: ButtonConfig[];
|
||||
/** Button color theme */
|
||||
color?: 'green' | 'black';
|
||||
/** Whether to force the color to remain constant regardless of theme mode */
|
||||
forceColor?: boolean;
|
||||
forceColor?: boolean;
|
||||
/** Gap between buttons on tablet+ (0px or 4px) */
|
||||
gap?: 'none' | 'small';
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
/** Override variant for single button (default: 'primary', can be 'secondary') */
|
||||
singleButtonVariant?: 'primary' | 'secondary';
|
||||
/** Maximum number of buttons to render. If more buttons are passed, only the first N will be rendered. */
|
||||
maxButtons?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ButtonGroup Component
|
||||
*
|
||||
* A responsive button group container that displays primary and/or tertiary buttons.
|
||||
* Stacks vertically on mobile and horizontally on tablet+.
|
||||
*
|
||||
*
|
||||
* A responsive button group container that displays buttons with adaptive layout:
|
||||
* - 1 button: Renders with singleButtonVariant (default: primary, can be secondary)
|
||||
* - 2 buttons: First as primary, second as tertiary (responsive layout)
|
||||
* - 3+ buttons: All tertiary in block layout
|
||||
*
|
||||
* @example
|
||||
* // Basic usage with both buttons
|
||||
* // Single button
|
||||
* <ButtonGroup
|
||||
* primaryButton={{ label: "Get Started", href: "/start" }}
|
||||
* tertiaryButton={{ label: "Learn More", href: "/learn" }}
|
||||
* buttons={[{ label: "Get Started", href: "/start" }]}
|
||||
* color="green"
|
||||
* />
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // With custom gap
|
||||
* // Two buttons (primary + tertiary)
|
||||
* <ButtonGroup
|
||||
* primaryButton={{ label: "Action", onClick: handleClick }}
|
||||
* color="black"
|
||||
* gap="small"
|
||||
* buttons={[
|
||||
* { label: "Get Started", href: "/start" },
|
||||
* { label: "Learn More", href: "/learn" }
|
||||
* ]}
|
||||
* color="green"
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Three or more buttons (all tertiary, block layout)
|
||||
* <ButtonGroup
|
||||
* buttons={[
|
||||
* { label: "Option 1", href: "/option1" },
|
||||
* { label: "Option 2", href: "/option2" },
|
||||
* { label: "Option 3", href: "/option3" }
|
||||
* ]}
|
||||
* color="green"
|
||||
* />
|
||||
*/
|
||||
export const ButtonGroup: React.FC<ButtonGroupProps> = ({
|
||||
primaryButton,
|
||||
tertiaryButton,
|
||||
buttons,
|
||||
color = 'green',
|
||||
forceColor = false,
|
||||
gap = 'small',
|
||||
className = '',
|
||||
singleButtonVariant = 'primary',
|
||||
maxButtons,
|
||||
}) => {
|
||||
// Don't render if no buttons are provided
|
||||
if (!primaryButton && !tertiaryButton) {
|
||||
// Validate and process buttons
|
||||
const validation = validateButtonGroup(buttons, maxButtons);
|
||||
|
||||
// Log warnings in development mode
|
||||
if (process.env.NODE_ENV === 'development' && validation.warnings.length > 0) {
|
||||
validation.warnings.forEach(warning => console.warn(warning));
|
||||
}
|
||||
|
||||
// Don't render if validation failed
|
||||
if (!validation.isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const buttonList = validation.buttons;
|
||||
|
||||
const isMultiButton = buttonList.length >= 3;
|
||||
|
||||
const classNames = clsx(
|
||||
'bds-button-group',
|
||||
`bds-button-group--gap-${gap}`,
|
||||
{
|
||||
'bds-button-group--block': isMultiButton,
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
// Render 3+ buttons: all tertiary in block layout
|
||||
if (isMultiButton) {
|
||||
return (
|
||||
<div className={classNames}>
|
||||
{buttonList.map((button, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="tertiary"
|
||||
color={color}
|
||||
forceColor={forceColor}
|
||||
href={button.href}
|
||||
onClick={button.onClick}
|
||||
forceNoPadding
|
||||
>
|
||||
{button.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render 1-2 buttons
|
||||
// Single button: use singleButtonVariant (default: primary, can be secondary)
|
||||
// Two buttons: first as primary, second as tertiary
|
||||
const firstButtonVariant = buttonList.length === 1 ? singleButtonVariant : 'primary';
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
{primaryButton && (
|
||||
{buttonList[0] && (
|
||||
<Button
|
||||
variant="primary"
|
||||
variant={firstButtonVariant}
|
||||
color={color}
|
||||
forceColor={forceColor}
|
||||
href={primaryButton.href}
|
||||
onClick={primaryButton.onClick}
|
||||
href={buttonList[0].href}
|
||||
onClick={buttonList[0].onClick}
|
||||
>
|
||||
{primaryButton.label}
|
||||
{buttonList[0].label}
|
||||
</Button>
|
||||
)}
|
||||
{tertiaryButton && (
|
||||
{buttonList[1] && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
color={color}
|
||||
forceColor={forceColor}
|
||||
href={tertiaryButton.href}
|
||||
onClick={tertiaryButton.onClick}
|
||||
href={buttonList[1].href}
|
||||
onClick={buttonList[1].onClick}
|
||||
>
|
||||
{tertiaryButton.label}
|
||||
{buttonList[1].label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,76 @@
|
||||
# ButtonGroup Component
|
||||
|
||||
A responsive button group container that displays primary and/or tertiary buttons. Stacks vertically on mobile and horizontally on tablet+.
|
||||
A responsive button group container that automatically assigns button variants based on the number of buttons passed. Stacks vertically on mobile and horizontally on tablet+.
|
||||
|
||||
## Features
|
||||
|
||||
- **Responsive Layout**: Vertical stack on mobile, horizontal row on tablet+
|
||||
- **Flexible Configuration**: Support for primary, tertiary, or both buttons
|
||||
- **Auto-Variant Assignment**: Automatically assigns Primary/Tertiary/Secondary variants based on button count
|
||||
- **Responsive Layout**: Vertical stack on mobile, horizontal row on tablet+ (for 1-2 buttons)
|
||||
- **Block Layout**: 3+ buttons render as all tertiary in a vertical block layout
|
||||
- **Customizable Spacing**: Control gap between buttons on tablet+ (none or small)
|
||||
- **Theme Support**: Green or black color themes
|
||||
- **Max Buttons Limit**: Optionally limit the number of buttons rendered
|
||||
|
||||
## Button Behavior
|
||||
|
||||
The component automatically determines button variants based on count:
|
||||
|
||||
| Count | Behavior |
|
||||
|-------|----------|
|
||||
| 1 button | Renders as Primary (or Secondary with `singleButtonVariant="secondary"`) |
|
||||
| 2 buttons | First as Primary, second as Tertiary (responsive layout) |
|
||||
| 3+ buttons | All as Tertiary in block layout (vertical on all screen sizes) |
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { ButtonGroup } from 'shared/patterns/ButtonGroup';
|
||||
|
||||
// Basic usage with both buttons
|
||||
// Single button (Primary by default)
|
||||
<ButtonGroup
|
||||
primaryButton={{ label: "Get Started", href: "/start" }}
|
||||
tertiaryButton={{ label: "Learn More", href: "/learn" }}
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "/start" }
|
||||
]}
|
||||
color="green"
|
||||
/>
|
||||
|
||||
// With no gap on tablet+
|
||||
// Single button as Secondary
|
||||
<ButtonGroup
|
||||
primaryButton={{ label: "Action", onClick: handleClick }}
|
||||
color="black"
|
||||
gap="none"
|
||||
buttons={[
|
||||
{ label: "Learn More", href: "/learn" }
|
||||
]}
|
||||
singleButtonVariant="secondary"
|
||||
color="green"
|
||||
/>
|
||||
|
||||
// With small gap on tablet+ (4px - default)
|
||||
// Two buttons (auto: Primary + Tertiary)
|
||||
<ButtonGroup
|
||||
primaryButton={{ label: "Primary Action", href: "/action" }}
|
||||
tertiaryButton={{ label: "Secondary", href: "/secondary" }}
|
||||
gap="small"
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "/start" },
|
||||
{ label: "Learn More", href: "/learn" }
|
||||
]}
|
||||
color="green"
|
||||
/>
|
||||
|
||||
// Three or more buttons (auto: all Tertiary, block layout)
|
||||
<ButtonGroup
|
||||
buttons={[
|
||||
{ label: "Documentation", href: "/docs" },
|
||||
{ label: "API Reference", href: "/api" },
|
||||
{ label: "Tutorials", href: "/tutorials" }
|
||||
]}
|
||||
color="black"
|
||||
/>
|
||||
|
||||
// Limit to 2 buttons even if more are passed
|
||||
<ButtonGroup
|
||||
buttons={[
|
||||
{ label: "First", href: "/first" },
|
||||
{ label: "Second", href: "/second" },
|
||||
{ label: "Third (not rendered)", href: "/third" }
|
||||
]}
|
||||
maxButtons={2}
|
||||
color="green"
|
||||
/>
|
||||
```
|
||||
|
||||
@@ -40,10 +78,12 @@ import { ButtonGroup } from 'shared/patterns/ButtonGroup';
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `primaryButton` | `ButtonConfig` | - | Primary button configuration |
|
||||
| `tertiaryButton` | `ButtonConfig` | - | Tertiary button configuration |
|
||||
| `buttons` | `ButtonConfig[]` | *required* | Array of button configurations |
|
||||
| `color` | `'green' \| 'black'` | `'green'` | Button color theme |
|
||||
| `forceColor` | `boolean` | `false` | Force color to remain constant across light/dark modes |
|
||||
| `gap` | `'none' \| 'small'` | `'small'` | Gap between buttons on tablet+ (0px or 4px) |
|
||||
| `singleButtonVariant` | `'primary' \| 'secondary'` | `'primary'` | Variant for single button |
|
||||
| `maxButtons` | `number` | - | Maximum number of buttons to render |
|
||||
| `className` | `string` | `''` | Additional CSS classes |
|
||||
|
||||
### ButtonConfig
|
||||
@@ -53,6 +93,7 @@ interface ButtonConfig {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
forceColor?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { ButtonGroup } from './ButtonGroup';
|
||||
export type { ButtonGroupProps, ButtonConfig } from './ButtonGroup';
|
||||
export { ButtonGroup, validateButtonGroup } from './ButtonGroup';
|
||||
export type { ButtonGroupProps, ButtonConfig, ButtonGroupValidationResult } from './ButtonGroup';
|
||||
|
||||
@@ -1,103 +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)
|
||||
$bds-card-stats-header-gap-base: 8px; // Base: 8px
|
||||
$bds-card-stats-header-gap-lg: 16px; // Desktop: 16px
|
||||
|
||||
// Spacing - Section gap (between header and cards)
|
||||
$bds-card-stats-section-gap-sm: 24px; // Mobile: 24px
|
||||
$bds-card-stats-section-gap-md: 32px; // Tablet: 32px
|
||||
$bds-card-stats-section-gap-lg: 40px; // Desktop: 40px
|
||||
|
||||
// Spacing - Section padding
|
||||
$bds-card-stats-padding-y-sm: 24px; // Mobile: 24px
|
||||
$bds-card-stats-padding-y-md: 32px; // Tablet: 32px
|
||||
$bds-card-stats-padding-y-lg: 40px; // Desktop: 40px
|
||||
|
||||
// =============================================================================
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export { CardStats, type CardStatsProps, type CardStatsCardConfig } from './CardStats';
|
||||
export { default } from './CardStats';
|
||||
|
||||
@@ -7,21 +7,23 @@
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-carousel-featured - Base section container
|
||||
// .bds-carousel-featured__container - Two-column grid container
|
||||
// .bds-carousel-featured__media - Image/media column
|
||||
// .bds-carousel-featured__media-col - Image/media column wrapper
|
||||
// .bds-carousel-featured__content-col - Content column wrapper
|
||||
// .bds-carousel-featured__content - Content column
|
||||
// .bds-carousel-featured__header - Header row (heading + nav)
|
||||
// .bds-carousel-featured__heading - Section heading
|
||||
// .bds-carousel-featured__nav - Navigation buttons wrapper
|
||||
// .bds-carousel-featured__bottom - Bottom section (features + CTA)
|
||||
// .bds-carousel-featured__features - Feature list container
|
||||
// .bds-carousel-featured__feature - Individual feature item
|
||||
// .bds-carousel-featured__feature-title - Feature title
|
||||
// .bds-carousel-featured__feature-description - Feature description
|
||||
// .bds-carousel-featured__cta - CTA section (buttons + mobile nav)
|
||||
// .bds-carousel-featured__buttons - Primary + tertiary buttons
|
||||
// .bds-carousel-featured__buttons - Button group wrapper
|
||||
// .bds-carousel-featured__slides - Slides container
|
||||
// .bds-carousel-featured__slide-track - Sliding track
|
||||
// .bds-carousel-featured__slide - Individual slide
|
||||
// .bds-carousel-featured__slide--active - Active slide modifier
|
||||
// .bds-carousel-featured__image - Slide image
|
||||
//
|
||||
// Note: This file is imported within xrpl.scss after Bootstrap and project
|
||||
@@ -30,19 +32,131 @@
|
||||
// =============================================================================
|
||||
// Design Tokens (from Figma)
|
||||
// =============================================================================
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
|
||||
// Spacing
|
||||
$bds-carousel-featured-padding-sm: 16px;
|
||||
$bds-carousel-featured-padding-md: 24px;
|
||||
$bds-carousel-featured-padding-lg: 32px;
|
||||
$bds-carousel-featured-padding-y-lg: 40px;
|
||||
$bds-carousel-featured-padding-sm: $bds-space-2xl $bds-space-lg; // 24px 16px
|
||||
$bds-carousel-featured-padding-md: $bds-space-3xl $bds-space-2xl; // 32px 24px
|
||||
$bds-carousel-featured-padding-lg: $bds-space-4xl $bds-space-3xl; // 40px 32px
|
||||
|
||||
// Content gap between image and content columns
|
||||
$bds-carousel-featured-column-gap: 8px;
|
||||
$bds-carousel-featured-column-gap: $bds-space-sm; // 8px - spacing('sm')
|
||||
|
||||
// Transition
|
||||
$bds-carousel-featured-transition: 400ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
// =============================================================================
|
||||
// Color Variant Configuration Map
|
||||
// =============================================================================
|
||||
|
||||
// Define all background variants with their color properties (Dark Mode)
|
||||
$bds-carousel-featured-variants: (
|
||||
'grey': (
|
||||
'bg-color': $gray-300,
|
||||
'text-color': $black,
|
||||
'divider-color': $black,
|
||||
'button-variant': 'black',
|
||||
'button-bg': $black,
|
||||
'button-color': $white,
|
||||
'button-hover': $gray-500,
|
||||
'button-active': $black
|
||||
),
|
||||
'neutral': (
|
||||
'bg-color': $black,
|
||||
'text-color': $white,
|
||||
'divider-color': $white,
|
||||
'button-variant': 'green',
|
||||
'button-bg': $green-300,
|
||||
'button-color': $black,
|
||||
'button-hover': $green-200,
|
||||
'button-active': $green-300
|
||||
),
|
||||
'yellow': (
|
||||
'bg-color': $yellow-100,
|
||||
'text-color': $black,
|
||||
'divider-color': $black,
|
||||
'button-variant': 'black',
|
||||
'button-bg': $black,
|
||||
'button-color': $white,
|
||||
'button-hover': $gray-500,
|
||||
'button-active': $black
|
||||
)
|
||||
);
|
||||
|
||||
// Define light mode variant overrides
|
||||
$bds-carousel-featured-variants-light: (
|
||||
'grey': (
|
||||
'bg-color': $gray-200,
|
||||
'text-color': $black,
|
||||
'divider-color': $black
|
||||
),
|
||||
'neutral': (
|
||||
'bg-color': $white,
|
||||
'text-color': $black,
|
||||
'divider-color': $black
|
||||
),
|
||||
'yellow': (
|
||||
'bg-color': $yellow-100,
|
||||
'text-color': $black,
|
||||
'divider-color': $black
|
||||
)
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// Mixins: Apply Background Variant Styles
|
||||
// =============================================================================
|
||||
|
||||
// Full variant mixin (for dark mode with button styles)
|
||||
@mixin carousel-featured-variant($variant-name, $config) {
|
||||
&--bg-#{$variant-name} {
|
||||
background-color: map-get($config, 'bg-color');
|
||||
|
||||
// Text colors
|
||||
.bds-carousel-featured__heading,
|
||||
.bds-carousel-featured__feature-title,
|
||||
.bds-carousel-featured__feature-description {
|
||||
color: map-get($config, 'text-color');
|
||||
}
|
||||
|
||||
// Divider color
|
||||
.bds-divider {
|
||||
background-color: map-get($config, 'divider-color');
|
||||
}
|
||||
|
||||
// Carousel nav buttons - enabled states only
|
||||
// Disabled states are handled by CarouselButton component styles
|
||||
.bds-carousel-button--#{map-get($config, 'button-variant')} {
|
||||
background-color: map-get($config, 'button-bg');
|
||||
color: map-get($config, 'button-color');
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: map-get($config, 'button-hover');
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: map-get($config, 'button-active');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Light mode variant mixin (only colors, no button states)
|
||||
@mixin carousel-featured-variant-light($variant-name, $config) {
|
||||
.bds-carousel-featured--bg-#{$variant-name} {
|
||||
background-color: map-get($config, 'bg-color');
|
||||
|
||||
.bds-carousel-featured__heading,
|
||||
.bds-carousel-featured__feature-title,
|
||||
.bds-carousel-featured__feature-description {
|
||||
color: map-get($config, 'text-color');
|
||||
}
|
||||
|
||||
.bds-divider {
|
||||
background-color: map-get($config, 'divider-color');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Base Container Styles
|
||||
// =============================================================================
|
||||
@@ -63,7 +177,7 @@ $bds-carousel-featured-transition: 400ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
// Desktop
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding: $bds-carousel-featured-padding-y-lg $bds-carousel-featured-padding-lg;
|
||||
padding: $bds-carousel-featured-padding-lg;
|
||||
}
|
||||
|
||||
// Max width constraint
|
||||
@@ -76,147 +190,13 @@ $bds-carousel-featured-transition: 400ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
// ---------------------------------------------------------------------------
|
||||
// Background Color Variants
|
||||
// ---------------------------------------------------------------------------
|
||||
// Each variant has light/dark mode colors defined below in html.light/html.dark sections
|
||||
// Default styles are for dark mode (site default)
|
||||
|
||||
// Grey variant - Light: gray-200, Dark: gray-300
|
||||
// Both modes use black text (light backgrounds)
|
||||
&--bg-grey {
|
||||
background-color: $gray-300; // Dark mode default
|
||||
|
||||
// Black text on grey background (both modes)
|
||||
.bds-carousel-featured__heading,
|
||||
.bds-carousel-featured__feature-title,
|
||||
.bds-carousel-featured__feature-description {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
// Divider: dark on light background
|
||||
.bds-divider {
|
||||
background-color: $black;
|
||||
}
|
||||
|
||||
// Carousel nav buttons: always black background (both modes) - enabled states only
|
||||
// Disabled states are handled by CarouselButton component styles
|
||||
.bds-carousel-button--black {
|
||||
background-color: $black;
|
||||
color: $white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $gray-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $black;
|
||||
}
|
||||
// Dark Mode (default) - Generate all variant styles using the mixin
|
||||
@include bds-theme-mode(dark) {
|
||||
@each $variant-name, $config in $bds-carousel-featured-variants {
|
||||
@include carousel-featured-variant($variant-name, $config);
|
||||
}
|
||||
}
|
||||
|
||||
// Neutral variant - Light: white, Dark: black
|
||||
&--bg-neutral {
|
||||
background-color: $black; // Dark mode default
|
||||
|
||||
// Dark mode: light text on dark background
|
||||
.bds-carousel-featured__heading,
|
||||
.bds-carousel-featured__feature-title,
|
||||
.bds-carousel-featured__feature-description {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
// Divider: light on dark background
|
||||
.bds-divider {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
// Carousel nav buttons: always green background (both modes) - enabled states only
|
||||
// Disabled states are handled by CarouselButton component styles
|
||||
.bds-carousel-button--green {
|
||||
background-color: $green-300;
|
||||
color: $black;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $green-200;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $green-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Yellow variant - Light: yellow-100, Dark: yellow-100 (same for both)
|
||||
&--bg-yellow {
|
||||
background-color: $yellow-100;
|
||||
|
||||
// Yellow is a light background, so dark text
|
||||
.bds-carousel-featured__heading,
|
||||
.bds-carousel-featured__feature-title {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.bds-carousel-featured__feature-description {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
// Divider: dark on light background
|
||||
.bds-divider {
|
||||
background-color: $black;
|
||||
}
|
||||
|
||||
// Carousel nav buttons: always black background (both modes) - enabled states only
|
||||
// Disabled states are handled by CarouselButton component styles
|
||||
.bds-carousel-button--black {
|
||||
background-color: $black;
|
||||
color: $white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $gray-500;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PageGrid Container
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-featured__container {
|
||||
// Reset PageGrid container padding - section already has padding
|
||||
&.bds-grid__container {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
// Override row to handle column ordering on mobile/tablet
|
||||
.bds-grid__row {
|
||||
// On mobile/tablet, reverse the order so content is above image
|
||||
flex-direction: column-reverse;
|
||||
gap: 24px; // Gap between content and image on mobile
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: 32px; // Gap on tablet
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
flex-direction: row;
|
||||
align-items: stretch; // Stretch both columns to same height
|
||||
gap: 8px; // Column gap on desktop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Media Column (Image)
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-featured__media {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -231,7 +211,7 @@ $bds-carousel-featured-transition: 400ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
// Stretch to match image height
|
||||
align-self: stretch;
|
||||
// Add 8px padding-left to create 16px total gap (8px row gap + 8px padding)
|
||||
padding-left: 8px;
|
||||
padding-left: $bds-space-sm; // 8px - spacing('sm')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,14 +258,14 @@ $bds-carousel-featured-transition: 400ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
.bds-carousel-featured__bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px; // Mobile
|
||||
gap: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: 32px; // Tablet
|
||||
gap: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: 40px; // Desktop
|
||||
gap: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +275,7 @@ $bds-carousel-featured-transition: 400ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
.bds-carousel-featured__nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: $bds-space-sm; // 8px - spacing('sm')
|
||||
flex-shrink: 0;
|
||||
|
||||
// Desktop nav (in header row)
|
||||
@@ -330,16 +310,16 @@ $bds-carousel-featured-transition: 400ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
.bds-carousel-featured__feature {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: $bds-space-sm; // 8px - spacing('sm')
|
||||
width: 100%;
|
||||
|
||||
// Spacing between description and next divider
|
||||
// Mobile/Tablet: 16px, Desktop: 24px
|
||||
&:not(:first-child) {
|
||||
padding-top: 16px;
|
||||
padding-top: $bds-space-lg; // 16px - spacing('lg')
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: 24px;
|
||||
padding-top: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,7 +347,7 @@ $bds-carousel-featured-transition: 400ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
gap: 16px;
|
||||
gap: $bds-space-lg; // 16px - spacing('lg')
|
||||
|
||||
// Tablet+: no wrap needed
|
||||
@include media-breakpoint-up(md) {
|
||||
@@ -381,26 +361,13 @@ $bds-carousel-featured-transition: 400ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.bds-carousel-featured__buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
|
||||
// Tablet+: row layout
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row;
|
||||
gap: 0;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Slides Container
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-featured__slides {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
@@ -453,10 +420,7 @@ html.light {
|
||||
background-color: $gray-200;
|
||||
|
||||
.bds-carousel-featured__heading,
|
||||
.bds-carousel-featured__feature-title {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.bds-carousel-featured__feature-title,
|
||||
.bds-carousel-featured__feature-description {
|
||||
color: $black;
|
||||
}
|
||||
@@ -466,64 +430,9 @@ html.light {
|
||||
}
|
||||
}
|
||||
|
||||
// Grey variant - Light mode: gray-200 background
|
||||
.bds-carousel-featured--bg-grey {
|
||||
background-color: $gray-200;
|
||||
|
||||
// Light mode: dark text on light background
|
||||
.bds-carousel-featured__heading,
|
||||
.bds-carousel-featured__feature-title {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.bds-carousel-featured__feature-description {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
// Divider: dark on light background
|
||||
.bds-divider {
|
||||
background-color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
// Neutral variant - Light mode: white background
|
||||
.bds-carousel-featured--bg-neutral {
|
||||
background-color: $white;
|
||||
|
||||
// Light mode: dark text on light background
|
||||
.bds-carousel-featured__heading,
|
||||
.bds-carousel-featured__feature-title {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.bds-carousel-featured__feature-description {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
// Divider: dark on light background
|
||||
.bds-divider {
|
||||
background-color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
// Yellow variant - Light mode: yellow-100 (same as dark mode)
|
||||
.bds-carousel-featured--bg-yellow {
|
||||
background-color: $yellow-100;
|
||||
|
||||
// Yellow is a light background, so dark text
|
||||
.bds-carousel-featured__heading,
|
||||
.bds-carousel-featured__feature-title {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.bds-carousel-featured__feature-description {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
// Divider: dark on light background
|
||||
.bds-divider {
|
||||
background-color: $black;
|
||||
}
|
||||
// Generate all light mode variant overrides using the mixin
|
||||
@each $variant-name, $config in $bds-carousel-featured-variants-light {
|
||||
@include carousel-featured-variant-light($variant-name, $config);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '../../components/Button';
|
||||
import { CarouselButton } from '../../components/CarouselButton';
|
||||
import { Divider } from '../../components/Divider';
|
||||
import { PageGrid, PageGridRow, PageGridCol } from '../../components/PageGrid';
|
||||
import { ButtonConfig } from '../ButtonGroup';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
|
||||
/**
|
||||
* Props for a single slide in the CarouselFeatured component
|
||||
@@ -47,10 +46,8 @@ export interface CarouselFeaturedProps extends React.ComponentPropsWithoutRef<'s
|
||||
heading: string;
|
||||
/** Array of feature items to display in the list */
|
||||
features: readonly CarouselFeatureItem[];
|
||||
/** Primary button configuration (optional) */
|
||||
primaryButton?: ButtonConfig;
|
||||
/** Tertiary button configuration (optional) */
|
||||
tertiaryButton?: ButtonConfig;
|
||||
/** Button configurations (1-2 buttons supported) */
|
||||
buttons?: ButtonConfig[];
|
||||
/** Background color variant. Defaults to 'grey'. */
|
||||
background?: CarouselFeaturedBackground;
|
||||
}
|
||||
@@ -70,8 +67,10 @@ export interface CarouselFeaturedProps extends React.ComponentPropsWithoutRef<'s
|
||||
* { title: "Easy-to-Integrate APIs", description: "Build with common languages..." },
|
||||
* { title: "Full Lifecycle Support", description: "From dev tools to deployment..." },
|
||||
* ]}
|
||||
* primaryButton={{ label: "Get Started", href: "/docs" }}
|
||||
* tertiaryButton={{ label: "Learn More", href: "/about" }}
|
||||
* buttons={[
|
||||
* { label: "Get Started", href: "/docs" },
|
||||
* { label: "Learn More", href: "/about" }
|
||||
* ]}
|
||||
* slides={[
|
||||
* { id: 1, imageSrc: '/image1.jpg', imageAlt: 'Slide 1' },
|
||||
* ]}
|
||||
@@ -84,8 +83,7 @@ export const CarouselFeatured = React.forwardRef<HTMLElement, CarouselFeaturedPr
|
||||
slides,
|
||||
heading,
|
||||
features,
|
||||
primaryButton,
|
||||
tertiaryButton,
|
||||
buttons,
|
||||
background = 'grey',
|
||||
className,
|
||||
children,
|
||||
@@ -97,6 +95,10 @@ export const CarouselFeatured = React.forwardRef<HTMLElement, CarouselFeaturedPr
|
||||
const canGoPrev = currentIndex > 0;
|
||||
const canGoNext = currentIndex < slides.length - 1;
|
||||
|
||||
// Validate buttons if provided (max 2 buttons supported)
|
||||
const buttonValidation = validateButtonGroup(buttons, 2);
|
||||
const hasButtons = buttonValidation.hasButtons;
|
||||
|
||||
const goToPrev = useCallback(() => {
|
||||
if (canGoPrev) {
|
||||
setCurrentIndex((prev) => prev - 1);
|
||||
@@ -120,8 +122,8 @@ export const CarouselFeatured = React.forwardRef<HTMLElement, CarouselFeaturedPr
|
||||
const buttonVariant = background === 'neutral' ? 'green' : 'black';
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={ref}
|
||||
<PageGrid
|
||||
ref={ref as React.Ref<HTMLDivElement>}
|
||||
className={clsx(
|
||||
'bds-carousel-featured',
|
||||
`bds-carousel-featured--bg-${background}`,
|
||||
@@ -129,155 +131,123 @@ export const CarouselFeatured = React.forwardRef<HTMLElement, CarouselFeaturedPr
|
||||
)}
|
||||
aria-roledescription="carousel"
|
||||
aria-label={heading}
|
||||
{...rest}
|
||||
>
|
||||
<PageGrid className="bds-carousel-featured__container">
|
||||
{...rest}>
|
||||
<PageGridRow>
|
||||
{/* Image/Media Column - Left on desktop, bottom on mobile */}
|
||||
{/* Content Column - Right on desktop, top on mobile */}
|
||||
<PageGridCol
|
||||
span={{ base: 4, md: 8, lg: 6 }}
|
||||
className="bds-carousel-featured__media-col"
|
||||
className="bds-carousel-featured__content-col order-1 order-lg-2"
|
||||
>
|
||||
<div className="bds-carousel-featured__media">
|
||||
<div
|
||||
className="bds-carousel-featured__slides"
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
aria-label={`Slide ${currentIndex + 1} of ${slides.length}`}
|
||||
>
|
||||
<div
|
||||
className="bds-carousel-featured__slide-track"
|
||||
style={{ transform: `translateX(-${currentIndex * 100}%)` }}
|
||||
>
|
||||
{slides.map((slide, index) => (
|
||||
<div
|
||||
key={slide.id}
|
||||
className={clsx(
|
||||
'bds-carousel-featured__slide',
|
||||
{ 'bds-carousel-featured__slide--active': index === currentIndex }
|
||||
)}
|
||||
aria-hidden={index !== currentIndex}
|
||||
>
|
||||
<img
|
||||
src={slide.imageSrc}
|
||||
alt={slide.imageAlt}
|
||||
className="bds-carousel-featured__image"
|
||||
loading={index === 0 ? 'eager' : 'lazy'}
|
||||
/>
|
||||
</div>
|
||||
<div className="bds-carousel-featured__content">
|
||||
{/* Header row with heading and nav buttons */}
|
||||
<div className="bds-carousel-featured__header">
|
||||
<h2 className="bds-carousel-featured__heading h-md">{heading}</h2>
|
||||
<div className={clsx(
|
||||
'bds-carousel-featured__nav',
|
||||
'bds-carousel-featured__nav--desktop',
|
||||
slides.length === 1 && 'd-none'
|
||||
)}>
|
||||
{(['prev', 'next'] as const).map((direction) => (
|
||||
<CarouselButton
|
||||
key={direction}
|
||||
direction={direction}
|
||||
variant={buttonVariant}
|
||||
disabled={direction === 'prev' ? !canGoPrev : !canGoNext}
|
||||
onClick={direction === 'prev' ? goToPrev : goToNext}
|
||||
aria-label={direction === 'prev' ? 'Previous slide' : 'Next slide'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom section: features + CTA grouped together */}
|
||||
<div className="bds-carousel-featured__bottom">
|
||||
{/* Feature list with dividers */}
|
||||
<ul className="bds-carousel-featured__features">
|
||||
{features.map((feature, index) => (
|
||||
<li key={index} className="bds-carousel-featured__feature">
|
||||
<Divider color="base" weight="regular" />
|
||||
<p className="bds-carousel-featured__feature-title body-r">{feature.title}</p>
|
||||
<p className="bds-carousel-featured__feature-description label-l">{feature.description}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* CTA section with buttons and mobile nav */}
|
||||
<div className="bds-carousel-featured__cta">
|
||||
{/* Buttons wrapper - groups primary and tertiary together */}
|
||||
{hasButtons && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color="black"
|
||||
forceColor={background !== 'neutral'}
|
||||
className="bds-carousel-featured__buttons"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mobile/Tablet nav buttons */}
|
||||
<div className={clsx(
|
||||
'bds-carousel-featured__nav',
|
||||
'bds-carousel-featured__nav--mobile',
|
||||
slides.length === 1 && 'd-none'
|
||||
)}>
|
||||
{(['prev', 'next'] as const).map((direction) => (
|
||||
<CarouselButton
|
||||
key={direction}
|
||||
direction={direction}
|
||||
variant={buttonVariant}
|
||||
disabled={direction === 'prev' ? !canGoPrev : !canGoNext}
|
||||
onClick={direction === 'prev' ? goToPrev : goToNext}
|
||||
aria-label={direction === 'prev' ? 'Previous slide' : 'Next slide'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
|
||||
{/* Content Column - Right on desktop, top on mobile */}
|
||||
{/* Image/Media Column - Left on desktop, bottom on mobile */}
|
||||
<PageGridCol
|
||||
span={{ base: 4, md: 8, lg: 6 }}
|
||||
className="bds-carousel-featured__content-col"
|
||||
className="bds-carousel-featured__media-col order-2 order-lg-1"
|
||||
>
|
||||
{/* Content Column - Right on desktop, top on mobile */}
|
||||
<div className="bds-carousel-featured__content">
|
||||
{/* Header row with heading and nav buttons */}
|
||||
<div className="bds-carousel-featured__header">
|
||||
<h2 className="bds-carousel-featured__heading h-md">{heading}</h2>
|
||||
<div className="bds-carousel-featured__nav bds-carousel-featured__nav--desktop">
|
||||
<CarouselButton
|
||||
direction="prev"
|
||||
variant={buttonVariant}
|
||||
disabled={!canGoPrev}
|
||||
onClick={goToPrev}
|
||||
aria-label="Previous slide"
|
||||
/>
|
||||
<CarouselButton
|
||||
direction="next"
|
||||
variant={buttonVariant}
|
||||
disabled={!canGoNext}
|
||||
onClick={goToNext}
|
||||
aria-label="Next slide"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom section: features + CTA grouped together */}
|
||||
<div className="bds-carousel-featured__bottom">
|
||||
{/* Feature list with dividers */}
|
||||
<div className="bds-carousel-featured__features">
|
||||
{features.map((feature, index) => (
|
||||
<div key={index} className="bds-carousel-featured__feature">
|
||||
<Divider color="base" weight="regular" />
|
||||
<p className="bds-carousel-featured__feature-title body-r">{feature.title}</p>
|
||||
<p className="bds-carousel-featured__feature-description label-l">{feature.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA section with buttons and mobile nav */}
|
||||
<div className={clsx(
|
||||
'bds-carousel-featured__cta',
|
||||
// Only one button provided (not both)
|
||||
((primaryButton && !tertiaryButton) || (!primaryButton && tertiaryButton)) && 'bds-carousel-featured__cta--single-button',
|
||||
// Both buttons provided
|
||||
primaryButton && tertiaryButton && 'bds-carousel-featured__cta--two-buttons'
|
||||
)}>
|
||||
{/* Buttons wrapper - groups primary and tertiary together */}
|
||||
<div className="bds-carousel-featured__buttons">
|
||||
{/* Primary button */}
|
||||
{primaryButton && (
|
||||
<Button
|
||||
variant="primary"
|
||||
color="black"
|
||||
forceColor={background !== 'neutral'}
|
||||
href={primaryButton.href}
|
||||
onClick={primaryButton.onClick}
|
||||
className="bds-carousel-featured__primary-btn"
|
||||
<div
|
||||
className="bds-carousel-featured__slides"
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
aria-label={`Slide ${currentIndex + 1} of ${slides.length}`}
|
||||
>
|
||||
<div
|
||||
className="bds-carousel-featured__slide-track"
|
||||
style={{ transform: `translateX(-${currentIndex * 100}%)` }}
|
||||
>
|
||||
{slides.map((slide, index) => (
|
||||
<div
|
||||
key={slide.id}
|
||||
className={clsx(
|
||||
'bds-carousel-featured__slide',
|
||||
{ 'bds-carousel-featured__slide--active': index === currentIndex }
|
||||
)}
|
||||
aria-hidden={index !== currentIndex}
|
||||
>
|
||||
{primaryButton.label}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Tertiary button */}
|
||||
{tertiaryButton && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
color="black"
|
||||
forceColor={background !== 'neutral'}
|
||||
href={tertiaryButton.href}
|
||||
onClick={tertiaryButton.onClick}
|
||||
className="bds-carousel-featured__tertiary-btn"
|
||||
>
|
||||
{tertiaryButton.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile/Tablet nav buttons */}
|
||||
<div className="bds-carousel-featured__nav bds-carousel-featured__nav--mobile">
|
||||
<CarouselButton
|
||||
direction="prev"
|
||||
variant={buttonVariant}
|
||||
disabled={!canGoPrev}
|
||||
onClick={goToPrev}
|
||||
aria-label="Previous slide"
|
||||
/>
|
||||
<CarouselButton
|
||||
direction="next"
|
||||
variant={buttonVariant}
|
||||
disabled={!canGoNext}
|
||||
onClick={goToNext}
|
||||
aria-label="Next slide"
|
||||
/>
|
||||
<img
|
||||
src={slide.imageSrc}
|
||||
alt={slide.imageAlt}
|
||||
className="bds-carousel-featured__image"
|
||||
loading={index === 0 ? 'eager' : 'lazy'}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div> {/* Close bottom */}
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
|
||||
{/* Render any additional children */}
|
||||
{children}
|
||||
</section>
|
||||
</PageGrid>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
117
shared/patterns/CarouselFeatured/README.md
Normal file
117
shared/patterns/CarouselFeatured/README.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# CarouselFeatured
|
||||
|
||||
A featured image carousel pattern with a two-column layout on desktop (image left, content right) and single-column layout on tablet/mobile (content top, image bottom). Features a heading, feature list with dividers, optional buttons, and navigation controls.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { CarouselFeatured } from '@/shared/patterns/CarouselFeatured';
|
||||
|
||||
<CarouselFeatured
|
||||
heading="Powered by Developers"
|
||||
features={[
|
||||
{ title: "Easy-to-Integrate APIs", description: "Build with common languages..." },
|
||||
{ title: "Full Lifecycle Support", description: "From dev tools to deployment..." },
|
||||
]}
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "/docs" },
|
||||
{ label: "Learn More", href: "/about" }
|
||||
]}
|
||||
slides={[
|
||||
{ id: 1, imageSrc: '/image1.jpg', imageAlt: 'Slide 1' },
|
||||
{ id: 2, imageSrc: '/image2.jpg', imageAlt: 'Slide 2' },
|
||||
]}
|
||||
background="grey"
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
### Required Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `heading` | `string` | Heading text displayed at the top of the content area |
|
||||
| `features` | `CarouselFeatureItem[]` | Array of feature items with title and description |
|
||||
| `slides` | `CarouselSlide[]` | Array of slides to display in the carousel |
|
||||
|
||||
### Optional Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `buttons` | `ButtonConfig[]` | `undefined` | Array of button configurations (1-2 buttons supported, uses ButtonGroup) |
|
||||
| `background` | `'grey' \| 'neutral' \| 'yellow'` | `'grey'` | Background color variant |
|
||||
|
||||
## Type Definitions
|
||||
|
||||
### CarouselSlide
|
||||
|
||||
```tsx
|
||||
interface CarouselSlide {
|
||||
id: string | number; // Unique identifier for the slide
|
||||
imageSrc: string; // Image source URL
|
||||
imageAlt: string; // Alt text for the image
|
||||
}
|
||||
```
|
||||
|
||||
### CarouselFeatureItem
|
||||
|
||||
```tsx
|
||||
interface CarouselFeatureItem {
|
||||
title: string; // Feature title
|
||||
description: string; // Feature description
|
||||
}
|
||||
```
|
||||
|
||||
### ButtonConfig
|
||||
|
||||
```tsx
|
||||
interface ButtonConfig {
|
||||
label: string; // Button text
|
||||
href?: string; // Optional link URL
|
||||
onClick?: () => void; // Optional click handler
|
||||
forceColor?: boolean; // Force button color override
|
||||
}
|
||||
```
|
||||
|
||||
## Background Variants
|
||||
|
||||
The component supports three background variants that adapt to light/dark mode:
|
||||
|
||||
- **`grey`** (default): Light mode: gray-200 (#E6EAF0), Dark mode: gray-300 (#CAD4DF)
|
||||
- **`neutral`**: Light mode: white (#FFF), Dark mode: black (#141414)
|
||||
- **`yellow`**: Light mode: yellow-100 (#F3F1EB), Dark mode: yellow-100 (#F3F1EB)
|
||||
|
||||
## Features
|
||||
|
||||
- **Responsive Layout**: Two-column on desktop (lg+), single-column on mobile/tablet
|
||||
- **Image Carousel**: Navigate through multiple slides with prev/next buttons
|
||||
- **Auto-hide Navigation**: Navigation buttons automatically hide when only one slide is present
|
||||
- **Feature List**: Display multiple features with dividers
|
||||
- **Button Group**: Supports 1-2 buttons with validation
|
||||
- **Background Variants**: Three color options with light/dark mode support
|
||||
- **Accessibility**: Proper ARIA labels for navigation buttons
|
||||
|
||||
## Layout Behavior
|
||||
|
||||
### Desktop (lg+)
|
||||
- Image column on the left (6 columns)
|
||||
- Content column on the right (6 columns)
|
||||
- Navigation buttons in header (desktop variant)
|
||||
|
||||
### Tablet/Mobile
|
||||
- Content section at the top
|
||||
- Image section at the bottom
|
||||
- Navigation buttons in CTA section (mobile variant)
|
||||
|
||||
## Examples
|
||||
|
||||
See the [showcase page](../../../about/carousel-featured-showcase.page.tsx) for live examples with different configurations.
|
||||
|
||||
## Notes
|
||||
|
||||
- Navigation buttons are automatically hidden when `slides.length === 1`
|
||||
- Buttons are validated using `validateButtonGroup` with a maximum of 2 buttons
|
||||
- Button colors are automatically adjusted based on the background variant
|
||||
- The component uses `ButtonGroup` pattern for consistent button styling
|
||||
|
||||
132
shared/patterns/LinkTextCard/LinkTextCard.scss
Normal file
132
shared/patterns/LinkTextCard/LinkTextCard.scss
Normal file
@@ -0,0 +1,132 @@
|
||||
// BDS LinkTextCard Pattern Styles
|
||||
// Brand Design System - Numbered card with heading, description, and CTA buttons
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .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 (from _spacing.scss)
|
||||
// =============================================================================
|
||||
|
||||
// Header gap (between number and heading)
|
||||
$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: $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: $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: $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: $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;
|
||||
$bds-link-text-card-border-color: $gray-300;
|
||||
|
||||
// Text color
|
||||
$bds-link-text-card-number-color: $gray-500;
|
||||
|
||||
// Content width at MD+
|
||||
$bds-link-text-card-content-width-md: calc(75% - 2px);
|
||||
|
||||
// =============================================================================
|
||||
// Card Component
|
||||
// =============================================================================
|
||||
|
||||
.bds-link-text-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: $bds-link-text-card-border-width solid $bds-link-text-card-border-color;
|
||||
padding-top: $bds-link-text-card-padding-top-base;
|
||||
padding-bottom: $bds-link-text-card-padding-bottom-base;
|
||||
gap: $bds-link-text-card-gap-base;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: $bds-link-text-card-padding-top-md;
|
||||
padding-bottom: $bds-link-text-card-padding-bottom-md;
|
||||
gap: $bds-link-text-card-gap-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: $bds-link-text-card-padding-top-lg;
|
||||
padding-bottom: $bds-link-text-card-padding-bottom-lg;
|
||||
gap: $bds-link-text-card-gap-lg;
|
||||
}
|
||||
|
||||
// Remove padding-bottom for the last card in the list
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
@include bds-theme-mode(dark) {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Section (Number + Heading)
|
||||
// =============================================================================
|
||||
|
||||
.bds-link-text-card__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-link-text-card-header-gap-base;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-link-text-card-header-gap-md;
|
||||
width: $bds-link-text-card-content-width-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-link-text-card-header-gap-lg;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $bds-link-text-card-number-color;
|
||||
}
|
||||
@include bds-theme-mode(dark) {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Content Section (Description + Buttons)
|
||||
// =============================================================================
|
||||
|
||||
.bds-link-text-card__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-link-text-card-content-gap-base;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-link-text-card-content-gap-md;
|
||||
width: $bds-link-text-card-content-width-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-link-text-card-content-gap-lg;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $bds-link-text-card-number-color;
|
||||
}
|
||||
@include bds-theme-mode(dark) {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
80
shared/patterns/LinkTextCard/LinkTextCard.tsx
Normal file
80
shared/patterns/LinkTextCard/LinkTextCard.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { ButtonGroup, ButtonConfig } from '../ButtonGroup';
|
||||
|
||||
export interface LinkTextCardProps {
|
||||
/** Card index for numbering (displays as index + 1) */
|
||||
index: number;
|
||||
/** Heading text (required) */
|
||||
heading: string;
|
||||
/** Description text (required) */
|
||||
description: string;
|
||||
/** Array of button configurations (max 2) */
|
||||
buttons: ButtonConfig[];
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* LinkTextCard Component
|
||||
*
|
||||
* A card component that displays a numbered item with heading, description, and action buttons.
|
||||
* Features a top divider, sequential numbering (01, 02, 03...), and up to 2 call-to-action buttons.
|
||||
*
|
||||
* Design:
|
||||
* - Flat HTML structure for minimal DOM depth
|
||||
* - Fixed green button color for brand consistency
|
||||
* - Responsive typography using existing design tokens
|
||||
* - Full light/dark mode support
|
||||
*
|
||||
* @example
|
||||
* // Basic usage
|
||||
* <LinkTextCard
|
||||
* index={0}
|
||||
* heading="Fast Settlement and Low Fees"
|
||||
* description="Settle transactions in 3-5 seconds for a fraction of a cent"
|
||||
* buttons={[
|
||||
* { label: "Get Started", href: "/start" },
|
||||
* { label: "Learn More", href: "/learn" }
|
||||
* ]}
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Single button
|
||||
* <LinkTextCard
|
||||
* index={1}
|
||||
* heading="Secure and Reliable"
|
||||
* description="Built on proven blockchain technology"
|
||||
* buttons={[{ label: "Read Docs", href: "/docs" }]}
|
||||
* />
|
||||
*/
|
||||
export const LinkTextCard: React.FC<LinkTextCardProps> = ({
|
||||
index,
|
||||
heading,
|
||||
description,
|
||||
buttons,
|
||||
className,
|
||||
}) => {
|
||||
// Format number with zero padding (01, 02, 03...)
|
||||
const formattedNumber = String(index + 1).padStart(2, '0');
|
||||
|
||||
// Ensure max 2 buttons for proper layout
|
||||
const maxButtons = buttons.slice(0, 2);
|
||||
|
||||
return (
|
||||
<li className={clsx('bds-link-text-card', className)}>
|
||||
<div className="bds-link-text-card__header">
|
||||
<p className="mb-0 body-l">{formattedNumber}</p>
|
||||
<h5 className="mb-0 sh-lg-l">{heading}</h5>
|
||||
</div>
|
||||
<div className="bds-link-text-card__content">
|
||||
<p className="mb-0 body-l">{description}</p>
|
||||
{maxButtons.length > 0 && (
|
||||
<ButtonGroup buttons={maxButtons} color="green" />
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkTextCard;
|
||||
209
shared/patterns/LinkTextCard/README.md
Normal file
209
shared/patterns/LinkTextCard/README.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# LinkTextCard Component
|
||||
|
||||
A numbered card component displaying a heading, description, and call-to-action buttons.
|
||||
|
||||
## Overview
|
||||
|
||||
LinkTextCard is a pattern component designed for sequential content presentation. Each card displays a numbered label (01, 02, 03...), a heading, description text, and up to 2 action buttons. Perfect for feature lists, step-by-step guides, or numbered content sections.
|
||||
|
||||
## Features
|
||||
|
||||
- **Sequential Numbering**: Auto-increments based on index prop with zero-padding (01, 02, 03...)
|
||||
- **Minimal HTML Structure**: Flat DOM hierarchy for optimal performance
|
||||
- **ButtonGroup Integration**: Supports up to 2 buttons (Primary + Tertiary layout)
|
||||
- **Fixed Button Color**: Green buttons for brand consistency
|
||||
- **Light/Dark Mode**: Full theming support
|
||||
- **Responsive Design**: Adaptive spacing and typography across breakpoints
|
||||
- **Top Divider**: Border-top styling with theme-aware colors
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage (2 Buttons)
|
||||
|
||||
```tsx
|
||||
<LinkTextCard
|
||||
index={0}
|
||||
heading="Fast Settlement and Low Fees"
|
||||
description="Settle transactions in 3-5 seconds for a fraction of a cent, ideal for large-scale, high-volume RWA tokenization"
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "/start" },
|
||||
{ label: "Learn More", href: "/docs" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### Single Button
|
||||
|
||||
```tsx
|
||||
<LinkTextCard
|
||||
index={1}
|
||||
heading="Secure and Reliable"
|
||||
description="Built on proven blockchain technology with enterprise-grade security"
|
||||
buttons={[
|
||||
{ label: "Read Documentation", href: "/docs" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### With Click Handlers
|
||||
|
||||
```tsx
|
||||
<LinkTextCard
|
||||
index={2}
|
||||
heading="Developer Friendly"
|
||||
description="Comprehensive APIs and SDKs for seamless integration"
|
||||
buttons={[
|
||||
{ label: "View API", onClick: () => navigate('/api') },
|
||||
{ label: "See Examples", href: "/examples" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### In a List
|
||||
|
||||
```tsx
|
||||
{features.map((feature, index) => (
|
||||
<LinkTextCard
|
||||
key={feature.id}
|
||||
index={index}
|
||||
heading={feature.heading}
|
||||
description={feature.description}
|
||||
buttons={feature.buttons}
|
||||
/>
|
||||
))}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
### LinkTextCardProps
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `index` | `number` | Required | Card index for numbering (displays as index + 1) |
|
||||
| `heading` | `string` | Required | Main heading text |
|
||||
| `description` | `string` | Required | Description/body text |
|
||||
| `buttons` | `ButtonConfig[]` | Required | Array of button configurations (max 2) |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
|
||||
### ButtonConfig (from ButtonGroup)
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `label` | `string` | Required | Button text |
|
||||
| `href` | `string` | - | Link destination (renders as anchor) |
|
||||
| `onClick` | `() => void` | - | Click handler (renders as button) |
|
||||
| `forceColor` | `boolean` | `false` | Force color regardless of theme |
|
||||
|
||||
**Note**: Only the first 2 buttons will be rendered. Button color is fixed to 'green'.
|
||||
|
||||
## Component Structure
|
||||
|
||||
```tsx
|
||||
<li className="bds-link-text-card">
|
||||
<div className="bds-link-text-card__header">
|
||||
<p className="body-l">01</p>
|
||||
<h5 className="sh-lg-l">Fast Settlement and Low Fees</h5>
|
||||
</div>
|
||||
<div className="bds-link-text-card__content">
|
||||
<p className="body-l">Settle transactions in 3-5 seconds...</p>
|
||||
<ButtonGroup buttons={[...]} color="green" />
|
||||
</div>
|
||||
</li>
|
||||
```
|
||||
|
||||
**Key Design Decisions:**
|
||||
- **List Item Element**: Renders as `<li>` for semantic list markup
|
||||
- **Two-Section Layout**: Header (number + heading) and Content (description + buttons)
|
||||
- **Border-Top Divider**: Applied directly to container (no separate element)
|
||||
- **Typography Classes**: Uses existing `body-l` and `sh-lg-l` classes
|
||||
- **Flexbox Gap**: Responsive spacing via gap property
|
||||
- **75% Width at MD+**: Content is constrained to 75% width on tablet and desktop
|
||||
|
||||
## Responsive Spacing
|
||||
|
||||
| Breakpoint | Card Gap | Header Gap | Content Gap | Padding Top | Padding Bottom |
|
||||
|------------|----------|------------|-------------|-------------|----------------|
|
||||
| Base (< 576px) | 24px | 8px | 16px | 8px | 24px |
|
||||
| MD (576px - 991px) | 32px | 12px | 24px | 12px | 32px |
|
||||
| LG (≥ 992px) | 40px | 16px | 32px | 16px | 40px |
|
||||
|
||||
**Card Gap**: Space between header and content sections
|
||||
**Header Gap**: Space between number and heading
|
||||
**Content Gap**: Space between description and ButtonGroup
|
||||
**Padding Top**: Space from top border to content
|
||||
**Padding Bottom**: Space after content (removed on last card)
|
||||
|
||||
## Styling
|
||||
|
||||
### CSS Classes
|
||||
|
||||
- `.bds-link-text-card` - Main list item container with flexbox layout and border-top
|
||||
- `.bds-link-text-card__header` - Header section (number + heading)
|
||||
- `.bds-link-text-card__content` - Content section (description + buttons)
|
||||
- Typography utilities: `body-l`, `sh-lg-l`
|
||||
|
||||
### Color Variables
|
||||
|
||||
- `$gray-300` - Border color (light mode)
|
||||
- `$gray-500` - Number text color
|
||||
- `$white` - Text color in dark mode (applied to entire card)
|
||||
|
||||
## Number Formatting
|
||||
|
||||
The component automatically formats numbers with zero-padding:
|
||||
|
||||
```typescript
|
||||
index: 0 → "01"
|
||||
index: 1 → "02"
|
||||
index: 9 → "10"
|
||||
index: 99 → "100"
|
||||
```
|
||||
|
||||
## Button Behavior
|
||||
|
||||
### 1-2 Buttons
|
||||
- **1 button**: Renders as primary button
|
||||
- **2 buttons**: First as primary, second as tertiary
|
||||
|
||||
### 3+ Buttons
|
||||
If more than 2 buttons are passed, only the first 2 will be rendered (automatically sliced).
|
||||
|
||||
## Files
|
||||
|
||||
- `LinkTextCard.tsx` - React component with TypeScript
|
||||
- `LinkTextCard.scss` - Minimal SCSS with BEM naming
|
||||
- `index.ts` - Barrel exports
|
||||
- `README.md` - This file
|
||||
|
||||
## Related Components
|
||||
|
||||
- **ButtonGroup**: Used for rendering action buttons
|
||||
- **Button**: Atomic button component used by ButtonGroup
|
||||
|
||||
## Import
|
||||
|
||||
```tsx
|
||||
import { LinkTextCard } from 'shared/patterns/LinkTextCard';
|
||||
// or
|
||||
import { LinkTextCard, type LinkTextCardProps } from 'shared/patterns/LinkTextCard';
|
||||
```
|
||||
|
||||
## Design System
|
||||
|
||||
Part of the Brand Design System (BDS) with `bds-` namespace prefix.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Sequential Indices**: Pass indices 0, 1, 2... for proper numbering
|
||||
2. **Limit Buttons**: Design works best with 1-2 buttons
|
||||
3. **Clear Descriptions**: Keep descriptions concise but informative
|
||||
4. **Consistent Length**: Try to keep similar text lengths across cards in a group
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Semantic HTML: Renders as `<li>` for use within `<ul>` lists
|
||||
- Heading hierarchy: Uses `<h5>` for card headings (adjust based on parent context)
|
||||
- Button labels should be descriptive
|
||||
- Maintains focus order: number → heading → description → buttons
|
||||
|
||||
**Note on Heading Semantics**: The component uses `<h5>` inside a list item, which is acceptable when the parent section has appropriate heading hierarchy. The heading level can be adjusted based on your specific use case.
|
||||
1
shared/patterns/LinkTextCard/index.ts
Normal file
1
shared/patterns/LinkTextCard/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { LinkTextCard, type LinkTextCardProps } from './LinkTextCard';
|
||||
@@ -1,115 +0,0 @@
|
||||
// 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__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
|
||||
//
|
||||
// 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'
|
||||
|
||||
// Spacing tokens - responsive
|
||||
// Mobile (<768px)
|
||||
$bds-lsg-header-gap-mobile: 24px;
|
||||
$bds-lsg-text-gap-mobile: 8px;
|
||||
|
||||
// Tablet (768px-1023px)
|
||||
$bds-lsg-header-gap-tablet: 32px;
|
||||
|
||||
// Desktop (≥1024px)
|
||||
$bds-lsg-header-gap-desktop: 40px;
|
||||
$bds-lsg-text-gap-desktop: 16px;
|
||||
|
||||
// =============================================================================
|
||||
// Base Component Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-logo-square-grid {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
@extend .w-100;
|
||||
|
||||
// Mobile-first gap
|
||||
gap: $bds-lsg-header-gap-mobile;
|
||||
|
||||
// Tablet breakpoint
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-lsg-header-gap-tablet;
|
||||
}
|
||||
|
||||
// Desktop breakpoint
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-lsg-header-gap-desktop;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Section
|
||||
// =============================================================================
|
||||
|
||||
.bds-logo-square-grid__header {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
// Mobile-first gap
|
||||
gap: $bds-lsg-header-gap-mobile;
|
||||
|
||||
// Tablet breakpoint
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-lsg-header-gap-tablet;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
// Desktop breakpoint
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-lsg-header-gap-desktop;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Text Content
|
||||
// =============================================================================
|
||||
|
||||
.bds-logo-square-grid__text {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
|
||||
// Mobile-first gap
|
||||
gap: $bds-lsg-text-gap-mobile;
|
||||
|
||||
// Desktop breakpoint
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-lsg-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)
|
||||
// Tile rendering and styling is handled by the TileLogo component
|
||||
70
shared/patterns/SectionHeader/SectionHeader.scss
Normal file
70
shared/patterns/SectionHeader/SectionHeader.scss
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
.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
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Light Mode Styles
|
||||
// =============================================================================
|
||||
|
||||
html.light {
|
||||
.bds-section-header__heading {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.bds-section-header__description {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dark Mode Styles
|
||||
// =============================================================================
|
||||
|
||||
html.dark {
|
||||
.bds-section-header__heading {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.bds-section-header__description {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
80
shared/patterns/SectionHeader/SectionHeader.tsx
Normal file
80
shared/patterns/SectionHeader/SectionHeader.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid } from '../../components/PageGrid/page-grid';
|
||||
import type { ResponsiveValue, PageGridSpanValue } from '../../components/PageGrid/page-grid';
|
||||
|
||||
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.
|
||||
*
|
||||
* Returns null if both heading and description are falsy and no children provided.
|
||||
*/
|
||||
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) {
|
||||
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;
|
||||
1
shared/patterns/SectionHeader/index.ts
Normal file
1
shared/patterns/SectionHeader/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { SectionHeader, type SectionHeaderProps } from './SectionHeader';
|
||||
@@ -1,110 +0,0 @@
|
||||
// BDS StandardCardGroupSection Pattern Styles
|
||||
// Brand Design System - Section with headline, description, and grid of StandardCard components
|
||||
//
|
||||
// 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:
|
||||
// - Headline: Neutral Black (#141414) → $black
|
||||
// - Description: Neutral Black (#141414) → $black
|
||||
//
|
||||
// Dark Mode:
|
||||
// - Headline: Neutral White (#FFFFFF) → $white
|
||||
// - Description: Neutral White (#FFFFFF) → $white
|
||||
//
|
||||
// - Header content max-width: 808px (approximately 8 columns at desktop)
|
||||
// - Gap between headline and description: 16px
|
||||
// - Gap between cards: handled by PageGrid gutter
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens (from Figma)
|
||||
// =============================================================================
|
||||
|
||||
// Spacing - Header gap (between headline and description)
|
||||
$bds-standard-card-group-section-header-gap-sm: 8px; // Mobile: 8px
|
||||
$bds-standard-card-group-section-header-gap-md: 8px; // Tablet: 8px
|
||||
$bds-standard-card-group-section-header-gap-lg: 16px; // Desktop: 16px
|
||||
|
||||
// Spacing - Section gap (between header and cards)
|
||||
$bds-standard-card-group-section-section-gap-sm: 24px; // Mobile
|
||||
$bds-standard-card-group-section-section-gap-md: 32px; // Tablet
|
||||
$bds-standard-card-group-section-section-gap-lg: 40px; // Desktop
|
||||
|
||||
// Spacing - Section padding (vertical)
|
||||
$bds-standard-card-group-section-padding-y-sm: 48px; // Mobile
|
||||
$bds-standard-card-group-section-padding-y-md: 64px; // Tablet
|
||||
$bds-standard-card-group-section-padding-y-lg: 80px; // Desktop
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Section Container
|
||||
// =============================================================================
|
||||
|
||||
.bds-standard-card-group-section {
|
||||
width: 100%;
|
||||
padding-top: $bds-standard-card-group-section-padding-y-sm;
|
||||
padding-bottom: $bds-standard-card-group-section-padding-y-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: $bds-standard-card-group-section-padding-y-md;
|
||||
padding-bottom: $bds-standard-card-group-section-padding-y-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: $bds-standard-card-group-section-padding-y-lg;
|
||||
padding-bottom: $bds-standard-card-group-section-padding-y-lg;
|
||||
}
|
||||
|
||||
@include bds-theme-mode(light) {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
@include bds-theme-mode(dark) {
|
||||
background-color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
110
shared/patterns/TileLinks/README.md
Normal file
110
shared/patterns/TileLinks/README.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# TileLink Component
|
||||
|
||||
A clickable tile component for link grids, featuring text content with an arrow icon.
|
||||
|
||||
## Overview
|
||||
|
||||
TileLink is an atomic component designed for use in grid layouts. It provides a consistent, interactive tile with support for both links (`<a>`) and buttons (`<button>`), featuring smooth animations and full accessibility support.
|
||||
|
||||
## Features
|
||||
|
||||
- **Two Color Variants**: Gray and Lilac
|
||||
- **Light/Dark Mode**: Full theming support
|
||||
- **Five Interaction States**: Default, Hover, Focused, Pressed, Disabled
|
||||
- **Window Shade Animation**: Hover color wipes from bottom to top on enter, top to bottom on exit
|
||||
- **Arrow Icon**: Animated arrow that moves on hover
|
||||
- **Responsive**: Adapts padding and font size across breakpoints
|
||||
- **Accessible**: Proper ARIA labels, focus states, and semantic HTML
|
||||
|
||||
## Responsive Sizing
|
||||
|
||||
| Breakpoint | Height | Padding | Font Size | Line Height |
|
||||
|------------|--------|---------|-----------|-------------|
|
||||
| Base (< 576px) | 64px | 12px | 16px | 26.1px |
|
||||
| MD (576px - 991px) | 64px | 16px | 16px | 26.1px |
|
||||
| LG (≥ 992px) | 64px | 20px | 18px | 26.1px |
|
||||
|
||||
## Color Variants
|
||||
|
||||
### Gray Variant
|
||||
- **Light Mode**: gray-200 background (#E6EAF0), black text
|
||||
- **Dark Mode**: gray-500 background (#5A5A5E), white text
|
||||
|
||||
### Lilac Variant
|
||||
- **Both Modes**: lilac-100 background, black text (consistent across light/dark)
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Link (Gray Variant - Default)
|
||||
```tsx
|
||||
<TileLink
|
||||
variant="gray"
|
||||
label="Documentation"
|
||||
href="/docs"
|
||||
/>
|
||||
```
|
||||
|
||||
### Lilac Variant with Click Handler
|
||||
```tsx
|
||||
<TileLink
|
||||
variant="lilac"
|
||||
label="Get Started"
|
||||
onClick={() => navigate('/start')}
|
||||
/>
|
||||
```
|
||||
|
||||
### Disabled State
|
||||
```tsx
|
||||
<TileLink
|
||||
variant="gray"
|
||||
label="Coming Soon"
|
||||
disabled
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `variant` | `'gray' \| 'lilac'` | `'gray'` | Color variant |
|
||||
| `label` | `string` | Required | Link text/label |
|
||||
| `href` | `string` | - | Link destination (renders as `<a>`) |
|
||||
| `onClick` | `() => void` | - | Click handler (renders as `<button>`) |
|
||||
| `disabled` | `boolean` | `false` | Disabled state |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
|
||||
## Animation Details
|
||||
|
||||
- **Transition Duration**: 200ms
|
||||
- **Timing Function**: cubic-bezier(0.98, 0.12, 0.12, 0.98)
|
||||
- **Hover Effect**: Window shade animation using `clip-path`
|
||||
- **Arrow Animation**: Translates 4px to the right on hover
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Renders as semantic `<a>` when `href` is provided
|
||||
- Renders as semantic `<button>` when `onClick` is provided
|
||||
- Includes `aria-label` for screen readers
|
||||
- Includes `aria-disabled` for disabled states
|
||||
- 2px focus outline with proper offset
|
||||
- Keyboard navigable
|
||||
|
||||
## Files
|
||||
|
||||
- `TileLink.tsx` - React component
|
||||
- `TileLink.scss` - Styles with BEM naming convention
|
||||
- `README.md` - This file
|
||||
|
||||
## Related Components
|
||||
|
||||
- **LinkSmallGrid**: Pattern component that uses TileLink in a responsive grid layout
|
||||
- **LinkArrow**: Arrow icon component used within TileLink
|
||||
|
||||
## Design System
|
||||
|
||||
Part of the Brand Design System (BDS) with `bds-` namespace prefix.
|
||||
|
||||
## Showcase
|
||||
|
||||
See `about/tile-link-showcase.page.tsx` for comprehensive examples of all variants and states.
|
||||
|
||||
302
shared/patterns/TileLinks/TileLink.scss
Normal file
302
shared/patterns/TileLinks/TileLink.scss
Normal file
@@ -0,0 +1,302 @@
|
||||
// BDS TileLink Component Styles
|
||||
// Brand Design System - Tile link component with responsive sizing
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-tile-link - Base tile (responsive layout)
|
||||
// .bds-tile-link--gray - Gray color variant
|
||||
// .bds-tile-link--lilac - Lilac color variant
|
||||
// .bds-tile-link--hovered - Hovered state (triggers overlay animation)
|
||||
// .bds-tile-link--disabled - Disabled state modifier
|
||||
// .bds-tile-link__overlay - Hover gradient overlay (window shade animation)
|
||||
// .bds-tile-link__content - Content wrapper (label + arrow)
|
||||
// .bds-tile-link__label - Text label
|
||||
// .bds-tile-link__arrow - Arrow icon wrapper
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
|
||||
// Focus border
|
||||
$bds-tile-link-focus-border-color-light: $black;
|
||||
$bds-tile-link-focus-border-color-dark: $white;
|
||||
$bds-tile-link-focus-border-width: $bds-focus-border-width; // 2px - from _spacing.scss
|
||||
|
||||
// Animation (matching TileLogo/CardIcon)
|
||||
$bds-tile-link-transition-duration: 200ms;
|
||||
$bds-tile-link-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Responsive Size Tokens
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// SM breakpoint (mobile - default)
|
||||
$bds-tile-link-height-base: $bds-space-6xl; // 64px - spacing('6xl')
|
||||
$bds-tile-link-padding-sm: $bds-space-md; // 12px - spacing('md')
|
||||
$bds-tile-link-font-size-sm: 16px;
|
||||
$bds-tile-link-line-height-sm: 26.1px;
|
||||
|
||||
// MD breakpoint (tablet)
|
||||
$bds-tile-link-padding-md: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-tile-link-font-size-md: 16px;
|
||||
$bds-tile-link-line-height-md: 26.1px;
|
||||
|
||||
// LG breakpoint (desktop)
|
||||
$bds-tile-link-padding-lg: $bds-space-xl; // 20px - spacing('xl')
|
||||
$bds-tile-link-font-size-lg: 18px;
|
||||
$bds-tile-link-line-height-lg: 26.1px;
|
||||
|
||||
// =============================================================================
|
||||
// Color Variants Map
|
||||
// =============================================================================
|
||||
// Structure: variant-name: (light-bg, light-hover, light-pressed, light-text, dark-bg, dark-hover, dark-pressed, dark-text)
|
||||
|
||||
$bds-tile-link-variants: (
|
||||
'gray': (
|
||||
light-bg: $gray-200, // #E6EAF0
|
||||
light-hover: $gray-300, // #CAD4DF
|
||||
light-pressed: $gray-400, // #A8B4C4
|
||||
light-text: $black, // #141414
|
||||
dark-bg: $gray-500, // #5A5A5E
|
||||
dark-hover: $gray-400, // #A8B4C4
|
||||
dark-pressed: rgba($gray-500, 0.7), // 70% opacity
|
||||
dark-text: $white // #FFFFFF
|
||||
),
|
||||
'lilac': (
|
||||
light-bg: $lilac-100, // Light mode background
|
||||
light-hover: $lilac-200, // Light mode hover
|
||||
light-pressed: $lilac-300, // Light mode pressed
|
||||
light-text: $black, // Light mode text
|
||||
dark-bg: $lilac-100, // Dark mode background (same as light)
|
||||
dark-hover: $lilac-200, // Dark mode hover (same as light)
|
||||
dark-pressed: $lilac-300, // Dark mode pressed (same as light)
|
||||
dark-text: $black // Dark mode text (same as light)
|
||||
)
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// Base Tile Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-tile-link {
|
||||
// Reset button/anchor styles
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: none;
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
|
||||
// Layout
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
// Responsive sizing - SM (mobile-first)
|
||||
height: $bds-tile-link-height-base;
|
||||
padding: $bds-tile-link-padding-sm;
|
||||
|
||||
@media (min-width: map-get($grid-breakpoints, md)) {
|
||||
padding: $bds-tile-link-padding-md;
|
||||
}
|
||||
|
||||
@media (min-width: map-get($grid-breakpoints, lg)) {
|
||||
padding: $bds-tile-link-padding-lg;
|
||||
}
|
||||
|
||||
// Interaction
|
||||
cursor: pointer;
|
||||
|
||||
// Transitions
|
||||
transition:
|
||||
background-color $bds-tile-link-transition-duration $bds-tile-link-transition-timing,
|
||||
opacity $bds-tile-link-transition-duration $bds-tile-link-transition-timing;
|
||||
|
||||
// Hover styles - prevent text underline
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// Focus styles (light mode default)
|
||||
&:focus {
|
||||
outline: $bds-tile-link-focus-border-width solid $bds-tile-link-focus-border-color-light;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: $bds-tile-link-focus-border-width solid $bds-tile-link-focus-border-color-light;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
// Dark mode focus styles
|
||||
@include bds-theme-mode(dark) {
|
||||
&:focus {
|
||||
outline-color: $bds-tile-link-focus-border-color-dark;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline-color: $bds-tile-link-focus-border-color-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Overlay (Color wipe animation - "Window Shade" effect)
|
||||
// =============================================================================
|
||||
|
||||
.bds-tile-link__overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
|
||||
// Default: hidden (shade is "rolled up" at bottom)
|
||||
clip-path: inset(100% 0 0 0);
|
||||
transition: clip-path $bds-tile-link-transition-duration $bds-tile-link-transition-timing;
|
||||
}
|
||||
|
||||
// Hovered state: shade fully raised (visible)
|
||||
.bds-tile-link--hovered .bds-tile-link__overlay {
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Content Wrapper
|
||||
// =============================================================================
|
||||
|
||||
.bds-tile-link__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bds-tile-link__arrow {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Arrow animation on hover - works for both <a> and <button> elements
|
||||
.bds-tile-link:hover .bds-tile-link__arrow .bds-link-icon--internal:not(.bds-link-icon--disabled) svg .arrow-horizontal,
|
||||
.bds-tile-link:focus .bds-tile-link__arrow .bds-link-icon--internal:not(.bds-link-icon--disabled) svg .arrow-horizontal,
|
||||
.bds-tile-link--hovered .bds-tile-link__arrow .bds-link-icon--internal:not(.bds-link-icon--disabled) svg .arrow-horizontal {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Color Variants (Light Mode - Default)
|
||||
// =============================================================================
|
||||
|
||||
@each $variant-name, $variant-colors in $bds-tile-link-variants {
|
||||
.bds-tile-link--#{$variant-name} {
|
||||
background-color: map-get($variant-colors, light-bg);
|
||||
|
||||
.bds-tile-link__label,
|
||||
.bds-tile-link__arrow {
|
||||
color: map-get($variant-colors, light-text);
|
||||
}
|
||||
|
||||
// Overlay color for hover wipe
|
||||
.bds-tile-link__overlay {
|
||||
background-color: map-get($variant-colors, light-hover);
|
||||
}
|
||||
|
||||
// Pressed state
|
||||
&:active:not(.bds-tile-link--disabled) {
|
||||
.bds-tile-link__overlay {
|
||||
background-color: map-get($variant-colors, light-pressed);
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode overrides
|
||||
@include bds-theme-mode(dark) {
|
||||
background-color: map-get($variant-colors, dark-bg);
|
||||
|
||||
.bds-tile-link__label,
|
||||
.bds-tile-link__arrow {
|
||||
color: map-get($variant-colors, dark-text);
|
||||
}
|
||||
|
||||
// Overlay color for hover wipe
|
||||
.bds-tile-link__overlay {
|
||||
background-color: map-get($variant-colors, dark-hover);
|
||||
}
|
||||
|
||||
// Pressed state
|
||||
&:active:not(.bds-tile-link--disabled) {
|
||||
.bds-tile-link__overlay {
|
||||
background-color: map-get($variant-colors, dark-pressed);
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Disabled State
|
||||
// =============================================================================
|
||||
|
||||
.bds-tile-link--disabled {
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// Light mode disabled (default)
|
||||
&.bds-tile-link--gray {
|
||||
background-color: $gray-100;
|
||||
|
||||
.bds-tile-link__label,
|
||||
.bds-tile-link__arrow {
|
||||
color: $gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
&.bds-tile-link--lilac {
|
||||
background-color: $lilac-100;
|
||||
|
||||
.bds-tile-link__label,
|
||||
.bds-tile-link__arrow {
|
||||
color: $gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode disabled - 30% opacity
|
||||
@include bds-theme-mode(dark) {
|
||||
opacity: 0.3;
|
||||
|
||||
&.bds-tile-link--gray {
|
||||
background-color: $gray-500;
|
||||
|
||||
.bds-tile-link__label,
|
||||
.bds-tile-link__arrow {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
&.bds-tile-link--lilac {
|
||||
background-color: $lilac-400;
|
||||
|
||||
.bds-tile-link__label,
|
||||
.bds-tile-link__arrow {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
138
shared/patterns/TileLinks/TileLink.tsx
Normal file
138
shared/patterns/TileLinks/TileLink.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import React, { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { LinkArrow } from '../../components/Link/LinkArrow';
|
||||
|
||||
export interface TileLinkProps {
|
||||
/** Color variant: 'gray' (default) or 'lilac' */
|
||||
variant?: 'gray' | 'lilac';
|
||||
/** Link text/label */
|
||||
label: string;
|
||||
/** Link destination - renders as <a> */
|
||||
href?: string;
|
||||
/** Click handler - renders as <button> */
|
||||
onClick?: () => void;
|
||||
/** Disabled state - prevents interaction */
|
||||
disabled?: boolean;
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TileLink Component
|
||||
*
|
||||
* A clickable tile component for link grids, featuring text content with an arrow icon.
|
||||
* Supports two color variants (Gray and Lilac) with full light/dark mode theming and
|
||||
* five interaction states (Default, Hover, Focused, Pressed, Disabled).
|
||||
*
|
||||
* Features a "window shade" hover animation where the hover color wipes from
|
||||
* bottom to top on mouse enter, and top to bottom on mouse leave.
|
||||
*
|
||||
* Responsive Sizing:
|
||||
* - Base (< 576px): 64px height, 12px padding, 16px font, 26.1px line-height
|
||||
* - MD (576px - 991px): 64px height, 16px padding, 16px font, 26.1px line-height
|
||||
* - LG (≥ 992px): 64px height, 20px padding, 18px font, 26.1px line-height
|
||||
*
|
||||
* Color Variants:
|
||||
* - Gray: Light mode (gray-200 bg), Dark mode (gray-500 bg)
|
||||
* - Lilac: Same in both light and dark modes (lilac-100 bg)
|
||||
*
|
||||
* @example
|
||||
* // Basic usage with link (gray variant - default)
|
||||
* <TileLink
|
||||
* variant="gray"
|
||||
* label="Documentation"
|
||||
* href="/docs"
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Lilac variant with click handler
|
||||
* <TileLink
|
||||
* variant="lilac"
|
||||
* label="Get Started"
|
||||
* onClick={() => navigate('/start')}
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Disabled state
|
||||
* <TileLink
|
||||
* variant="gray"
|
||||
* label="Coming Soon"
|
||||
* disabled
|
||||
* />
|
||||
*/
|
||||
export const TileLink: React.FC<TileLinkProps> = ({
|
||||
variant = 'gray',
|
||||
label,
|
||||
href,
|
||||
onClick,
|
||||
disabled = false,
|
||||
className,
|
||||
}) => {
|
||||
// Track hover state for animation
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
// Build class names using BEM convention
|
||||
const classNames = clsx(
|
||||
'bds-tile-link',
|
||||
`bds-tile-link--${variant}`,
|
||||
{
|
||||
'bds-tile-link--disabled': disabled,
|
||||
'bds-tile-link--hovered': isHovered && !disabled,
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
// Hover handlers
|
||||
const handleMouseEnter = () => !disabled && setIsHovered(true);
|
||||
const handleMouseLeave = () => setIsHovered(false);
|
||||
|
||||
// Common content (overlay + label + arrow)
|
||||
const content = (
|
||||
<>
|
||||
{/* Hover overlay for window shade animation */}
|
||||
<div className="bds-tile-link__overlay" aria-hidden="true" />
|
||||
|
||||
{/* Content wrapper */}
|
||||
<div className="bds-tile-link__content">
|
||||
<span className="bds-tile-link__label mb-0 body-r">{label}</span>
|
||||
<span className="bds-tile-link__arrow">
|
||||
<LinkArrow variant="internal" size="medium" />
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
// Render as anchor tag when href is provided
|
||||
if (href && !disabled) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className={classNames}
|
||||
aria-label={label}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
// Render as button (for onClick or disabled state)
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
aria-disabled={disabled}
|
||||
aria-label={label}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default TileLink;
|
||||
|
||||
2
shared/patterns/TileLinks/index.ts
Normal file
2
shared/patterns/TileLinks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { TileLink, type TileLinkProps } from './TileLink';
|
||||
|
||||
@@ -58,20 +58,21 @@ $bds-cmb-variants: (
|
||||
);
|
||||
|
||||
// Spacing tokens - responsive
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss where applicable.
|
||||
// Mobile (<768px)
|
||||
$bds-cmb-gap-mobile: 48px;
|
||||
$bds-cmb-padding-mobile: 16px;
|
||||
$bds-cmb-gap-mobile: $bds-space-5xl; // 48px - spacing('5xl')
|
||||
$bds-cmb-padding-mobile: $bds-space-lg; // 16px - spacing('lg')
|
||||
|
||||
// Tablet (768px-1023px)
|
||||
$bds-cmb-gap-tablet: 64px;
|
||||
$bds-cmb-padding-tablet: 24px;
|
||||
$bds-cmb-gap-tablet: $bds-space-6xl; // 64px - spacing('6xl')
|
||||
$bds-cmb-padding-tablet: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
|
||||
// Desktop (≥1024px)
|
||||
$bds-cmb-gap-desktop: 80px;
|
||||
$bds-cmb-padding-desktop: 40px 32px;
|
||||
$bds-cmb-gap-desktop: $bds-space-8xl; // 80px - spacing('8xl')
|
||||
$bds-cmb-padding-desktop: $bds-space-4xl $bds-space-3xl; // 40px 32px
|
||||
|
||||
// Button spacing
|
||||
$bds-cmb-button-gap: 24px;
|
||||
$bds-cmb-button-gap: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
|
||||
// Image overlay for readability
|
||||
$bds-cmb-image-overlay: rgba(0, 0, 0, 0.3);
|
||||
@@ -161,18 +162,18 @@ $bds-cmb-image-overlay: rgba(0, 0, 0, 0.3);
|
||||
@extend .flex-column;
|
||||
flex-direction: column;
|
||||
color: var(--bds-cmb-text-color, #232021);
|
||||
|
||||
|
||||
// Mobile-first gap
|
||||
gap: 16px;
|
||||
|
||||
gap: $bds-space-lg; // 16px - spacing('lg')
|
||||
|
||||
// Tablet breakpoint
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: 24px;
|
||||
gap: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
}
|
||||
|
||||
|
||||
// Desktop breakpoint
|
||||
@include media-breakpoint-up(xl) {
|
||||
gap: 32px;
|
||||
gap: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
|
||||
import { ButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from 'shared/patterns/ButtonGroup/ButtonGroup';
|
||||
|
||||
export interface CalloutMediaBannerProps {
|
||||
/** Color variant - determines background color (ignored if backgroundImage is provided) */
|
||||
@@ -12,57 +12,62 @@ 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;
|
||||
/** Primary button configuration */
|
||||
primaryButton?: {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
/** Tertiary button configuration */
|
||||
tertiaryButton?: {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
/** Button configurations (1-2 buttons supported) */
|
||||
buttons?: ButtonConfig[];
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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"
|
||||
* subheading="A decentralized public Layer 1 blockchain..."
|
||||
* primaryButton={{ label: "Get Started", href: "/docs" }}
|
||||
* tertiaryButton={{ label: "Learn More", href: "/about" }}
|
||||
* buttons={[
|
||||
* { label: "Get Started", href: "/docs" },
|
||||
* { label: "Learn More", href: "/about" }
|
||||
* ]}
|
||||
* />
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // With 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"
|
||||
* heading="Build on XRPL"
|
||||
* subheading="Start building your next project"
|
||||
* primaryButton={{ label: "Start Building", onClick: handleClick }}
|
||||
* buttons={[{ label: "Start Building", onClick: handleClick }]}
|
||||
* />
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // With background image and black text (fixed across light/dark modes)
|
||||
* <CalloutMediaBanner
|
||||
* backgroundImage="/images/light-hero-bg.jpg"
|
||||
* textColor="black"
|
||||
* heading="Build on XRPL"
|
||||
* headingAs="h2"
|
||||
* subheading="Start building your next project"
|
||||
* primaryButton={{ label: "Start Building", onClick: handleClick }}
|
||||
* buttons={[{ label: "Start Building", onClick: handleClick }]}
|
||||
* />
|
||||
*/
|
||||
export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
|
||||
@@ -70,14 +75,15 @@ export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
|
||||
backgroundImage,
|
||||
textColor = 'white',
|
||||
heading,
|
||||
headingAs = 'h4',
|
||||
subheading,
|
||||
primaryButton,
|
||||
tertiaryButton,
|
||||
buttons,
|
||||
className = '',
|
||||
}) => {
|
||||
// Check if there are any buttons
|
||||
const hasButtons = !!(primaryButton || tertiaryButton);
|
||||
|
||||
// Validate buttons if provided (max 2 buttons supported)
|
||||
const buttonValidation = validateButtonGroup(buttons, 2);
|
||||
const hasButtons = buttonValidation.hasButtons;
|
||||
|
||||
// Check if we should center content: no buttons OR (no heading but has buttons)
|
||||
const shouldCenter = !hasButtons || (!heading && hasButtons);
|
||||
|
||||
@@ -104,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}>
|
||||
@@ -111,17 +120,18 @@ 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>
|
||||
|
||||
{/* Buttons */}
|
||||
<ButtonGroup
|
||||
primaryButton={primaryButton}
|
||||
tertiaryButton={tertiaryButton}
|
||||
color={buttonColor}
|
||||
gap="none"
|
||||
/>
|
||||
{hasButtons && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color={buttonColor}
|
||||
gap="none"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
@@ -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
|
||||
48
shared/sections/CardStatsList/CardStatsList.scss
Normal file
48
shared/sections/CardStatsList/CardStatsList.scss
Normal 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
|
||||
@@ -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}
|
||||
2
shared/sections/CardStatsList/index.ts
Normal file
2
shared/sections/CardStatsList/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { CardStats, type CardStatsProps, type CardStatsCardConfig } from './CardStatsList';
|
||||
export { default } from './CardStatsList';
|
||||
@@ -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"
|
||||
@@ -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,46 +15,29 @@
|
||||
// - 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)
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens (from Figma)
|
||||
// =============================================================================
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
// $bds-grid-gutter is defined in _spacing.scss (8px).
|
||||
|
||||
$bds-grid-gutter: 8px;
|
||||
|
||||
// Spacing - Header gap (between heading and description)
|
||||
$bds-cards-featured-header-gap-sm: 8px; // Mobile: 8px
|
||||
$bds-cards-featured-header-gap-md: 8px; // Tablet: 8px
|
||||
$bds-cards-featured-header-gap-lg: 16px; // Desktop: 16px
|
||||
|
||||
// Spacing - Section gap (between header and cards)
|
||||
$bds-cards-featured-section-gap-sm: 24px; // Mobile
|
||||
$bds-cards-featured-section-gap-md: 32px; // Tablet
|
||||
$bds-cards-featured-section-gap-lg: 40px; // Desktop
|
||||
// Spacing - Section gap handled by SectionHeader margin-bottom
|
||||
|
||||
// Spacing - Cards gap
|
||||
$bds-cards-featured-cards-gap-sm: 48px; // Mobile: 48px vertical
|
||||
$bds-cards-featured-cards-gap-md: 8px; // Tablet: 8px
|
||||
$bds-cards-featured-cards-gap-lg: 8px; // Desktop: 8px
|
||||
$bds-cards-featured-cards-gap-sm: $bds-space-5xl; // 48px - spacing('5xl')
|
||||
$bds-cards-featured-cards-gap-md: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-cards-featured-cards-gap-lg: $bds-space-sm; // 8px - spacing('sm')
|
||||
|
||||
// Spacing - Section padding (vertical)
|
||||
$bds-cards-featured-padding-y-sm: 48px; // Mobile
|
||||
$bds-cards-featured-padding-y-md: 64px; // Tablet
|
||||
$bds-cards-featured-padding-y-lg: 80px; // Desktop
|
||||
$bds-cards-featured-padding-y-sm: $bds-space-5xl; // 48px - spacing('5xl')
|
||||
$bds-cards-featured-padding-y-md: $bds-space-6xl; // 64px - spacing('6xl')
|
||||
$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
|
||||
// =============================================================================
|
||||
@@ -114,22 +66,19 @@ $bds-cards-featured-description-color-dark: $white; // #FFFFFF - Neutral white
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
column-gap: $bds-cards-featured-cards-gap-sm;
|
||||
row-gap: 48px;
|
||||
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;
|
||||
margin-top: $bds-cards-featured-section-gap-md;
|
||||
row-gap: 52px; // Non-standard value, kept as-is
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
column-gap: $bds-cards-featured-cards-gap-lg;
|
||||
row-gap: 56px;
|
||||
margin-top: $bds-cards-featured-section-gap-lg;
|
||||
row-gap: 56px; // Non-standard value, kept as-is
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
53
shared/sections/CardsIconGrid/CardsIconGrid.scss
Normal file
53
shared/sections/CardsIconGrid/CardsIconGrid.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
// BDS CardsIconGrid Pattern Styles
|
||||
// Brand Design System - Section with heading, description, and grid of CardTextIconCard
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-cards-icon-grid - Base section container
|
||||
// .bds-cards-icon-grid__list - Grid list (ul as PageGrid.Row)
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens (uses BDS spacing from _spacing.scss)
|
||||
// =============================================================================
|
||||
|
||||
$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;
|
||||
|
||||
// =============================================================================
|
||||
// Section Container
|
||||
// =============================================================================
|
||||
|
||||
.bds-cards-icon-grid {
|
||||
padding-top: $bds-cards-icon-grid-padding-base;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: $bds-cards-icon-grid-padding-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: $bds-cards-icon-grid-padding-lg;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@include bds-theme-mode(dark) {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// List Grid - Aspect Ratios
|
||||
// =============================================================================
|
||||
|
||||
.bds-cards-icon-grid__list > li {
|
||||
aspect-ratio: 3 / 2;
|
||||
@include media-breakpoint-up(md) {
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
@include media-breakpoint-up(lg) {
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
}
|
||||
62
shared/sections/CardsIconGrid/CardsIconGrid.tsx
Normal file
62
shared/sections/CardsIconGrid/CardsIconGrid.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
|
||||
import { SectionHeader } from 'shared/patterns/SectionHeader';
|
||||
import { CardTextIconCard, CardTextIconCardProps } from 'shared/components/CardTextIcon';
|
||||
|
||||
export interface CardsIconGridProps {
|
||||
/** Section heading (required) */
|
||||
heading: string;
|
||||
/** Optional description text */
|
||||
description?: string;
|
||||
/** Array of card data to display */
|
||||
cards: CardTextIconCardProps[];
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* CardsIconGrid Component
|
||||
*
|
||||
* A section pattern with a header (heading + description) and a grid of
|
||||
* CardTextIconCard components. Uses PageGrid.Row as="ul" with cards as
|
||||
* PageGrid.Col as="li". Aspect ratios: sm 3:2, md/lg 4:3.
|
||||
*
|
||||
* @example
|
||||
* <CardsIconGrid
|
||||
* heading="Explore Tools"
|
||||
* description="Choose a tool to get started"
|
||||
* cards={[
|
||||
* { icon: "/icons/docs.svg", heading: "Documentation", description: "..." },
|
||||
* ]}
|
||||
* />
|
||||
*/
|
||||
export const CardsIconGrid: React.FC<CardsIconGridProps> = ({
|
||||
heading,
|
||||
description,
|
||||
cards,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<PageGrid className={clsx('bds-cards-icon-grid', className)}>
|
||||
<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) => (
|
||||
<CardTextIconCard
|
||||
key={card.heading || index}
|
||||
icon={card.icon}
|
||||
iconAlt={card.iconAlt}
|
||||
heading={card.heading}
|
||||
description={card.description}
|
||||
height={card.height}
|
||||
width={card.width}
|
||||
gridColSpan={{ base: 4, md: 4, lg: 4 }}
|
||||
/>
|
||||
))}
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardsIconGrid;
|
||||
53
shared/sections/CardsIconGrid/README.md
Normal file
53
shared/sections/CardsIconGrid/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# CardsIconGrid Pattern
|
||||
|
||||
A section pattern with a header (heading + description) and a grid of CardTextIconCard components.
|
||||
|
||||
## Overview
|
||||
|
||||
CardsIconGrid mirrors the LinkTextDirectory header structure and renders cards in a responsive grid using PageGrid.Row as="ul" and CardTextIconCard with gridColSpan. Each card is a PageGrid.Col as="li"—no div wrapper.
|
||||
|
||||
## Features
|
||||
|
||||
- Header with heading and optional description
|
||||
- Responsive grid: 3 cards/row at all breakpoints (base, md, lg)
|
||||
- Aspect ratios: sm 3:2, md/lg 4:3
|
||||
- Light/dark mode support
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { CardsIconGrid } from 'shared/patterns/CardsIconGrid';
|
||||
|
||||
<CardsIconGrid
|
||||
heading="Explore Tools"
|
||||
description="Choose a tool to get started"
|
||||
cards={[
|
||||
{
|
||||
icon: "/icons/docs.svg",
|
||||
iconAlt: "Documentation",
|
||||
heading: "Documentation",
|
||||
description: "Access everything you need.",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `heading` | `string` | Section heading |
|
||||
| `description` | `string` | Optional description |
|
||||
| `cards` | `CardTextIconCardData[]` | Array of card configs |
|
||||
| `className` | `string` | Additional CSS classes |
|
||||
|
||||
## Grid Column Spans
|
||||
|
||||
- base: 4, md: 4, lg: 4 (3 cards per row)
|
||||
|
||||
## Files
|
||||
|
||||
- `CardsIconGrid.tsx` - React component
|
||||
- `CardsIconGrid.scss` - Styles
|
||||
- `index.ts` - Barrel exports
|
||||
- `README.md` - This file
|
||||
4
shared/sections/CardsIconGrid/index.ts
Normal file
4
shared/sections/CardsIconGrid/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
CardsIconGrid,
|
||||
type CardsIconGridProps,
|
||||
} from './CardsIconGrid';
|
||||
53
shared/sections/CardsTextGrid/CardsTextGrid.scss
Normal file
53
shared/sections/CardsTextGrid/CardsTextGrid.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
// BDS CardsTextGrid Pattern Styles
|
||||
// Brand Design System - Section with heading, description, and grid of CardTextIconCard
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-cards-text-grid - Base section container
|
||||
// .bds-cards-text-grid__list - Grid list (ul as PageGrid.Row)
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens (uses BDS spacing from _spacing.scss)
|
||||
// =============================================================================
|
||||
|
||||
$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;
|
||||
|
||||
// =============================================================================
|
||||
// Section Container
|
||||
// =============================================================================
|
||||
|
||||
.bds-cards-text-grid {
|
||||
padding-top: $bds-cards-text-grid-padding-base;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: $bds-cards-text-grid-padding-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: $bds-cards-text-grid-padding-lg;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@include bds-theme-mode(dark) {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// List Grid - Aspect Ratios
|
||||
// =============================================================================
|
||||
|
||||
.bds-cards-text-grid__list > li {
|
||||
aspect-ratio: 16 / 9;
|
||||
@include media-breakpoint-up(md) {
|
||||
aspect-ratio: 3 / 2;
|
||||
}
|
||||
@include media-breakpoint-up(lg) {
|
||||
aspect-ratio: 3 / 1;
|
||||
}
|
||||
}
|
||||
58
shared/sections/CardsTextGrid/CardsTextGrid.tsx
Normal file
58
shared/sections/CardsTextGrid/CardsTextGrid.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
|
||||
import { SectionHeader } from 'shared/patterns/SectionHeader';
|
||||
import { CardTextIconCard, CardTextIconCardProps } from 'shared/components/CardTextIcon';
|
||||
|
||||
export interface CardsTextGridProps {
|
||||
/** Section heading (required) */
|
||||
heading: string;
|
||||
/** Optional description text */
|
||||
description?: string;
|
||||
/** Array of card data to display */
|
||||
cards: CardTextIconCardProps[];
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* CardsTextGrid Component
|
||||
*
|
||||
* A section pattern with a header (heading + description) and a grid of
|
||||
* CardTextIconCard components. Uses PageGrid.Row as="ul" with cards as
|
||||
* PageGrid.Col as="li". Aspect ratios: sm 16:9, md 3:2, lg 3:1.
|
||||
*
|
||||
* @example
|
||||
* <CardsTextGrid
|
||||
* heading="Explore Tools"
|
||||
* description="Choose a tool to get started"
|
||||
* cards={[
|
||||
* { icon: "/icons/docs.svg", heading: "Documentation", description: "..." },
|
||||
* ]}
|
||||
* />
|
||||
*/
|
||||
export const CardsTextGrid: React.FC<CardsTextGridProps> = ({
|
||||
heading,
|
||||
description,
|
||||
cards,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<PageGrid className={clsx('bds-cards-text-grid', className)}>
|
||||
<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) => (
|
||||
<CardTextIconCard
|
||||
key={card.heading || index}
|
||||
heading={card.heading}
|
||||
description={card.description}
|
||||
gridColSpan={{ base: 4, md: 4, lg: 6 }}
|
||||
/>
|
||||
))}
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardsTextGrid;
|
||||
53
shared/sections/CardsTextGrid/README.md
Normal file
53
shared/sections/CardsTextGrid/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# CardsTextGrid Pattern
|
||||
|
||||
A section pattern with a header (heading + description) and a grid of CardTextIconCard components.
|
||||
|
||||
## Overview
|
||||
|
||||
CardsTextGrid mirrors the LinkTextDirectory header structure and renders cards in a responsive grid using PageGrid.Row as="ul" and CardTextIconCard with gridColSpan. Each card is a PageGrid.Col as="li"—no div wrapper.
|
||||
|
||||
## Features
|
||||
|
||||
- Header with heading and optional description
|
||||
- Responsive grid: 3 cards/row at base+md, 2 cards/row at lg
|
||||
- Aspect ratios: sm 16:9, md 3:2, lg 3:1
|
||||
- Light/dark mode support
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { CardsTextGrid } from 'shared/patterns/CardsTextGrid';
|
||||
|
||||
<CardsTextGrid
|
||||
heading="Explore Tools"
|
||||
description="Choose a tool to get started"
|
||||
cards={[
|
||||
{
|
||||
icon: "/icons/docs.svg",
|
||||
iconAlt: "Documentation",
|
||||
heading: "Documentation",
|
||||
description: "Access everything you need.",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `heading` | `string` | Section heading |
|
||||
| `description` | `string` | Optional description |
|
||||
| `cards` | `CardTextIconCardData[]` | Array of card configs |
|
||||
| `className` | `string` | Additional CSS classes |
|
||||
|
||||
## Grid Column Spans
|
||||
|
||||
- base: 4, md: 4, lg: 6 (3 cards/row at base+md, 2 at lg)
|
||||
|
||||
## Files
|
||||
|
||||
- `CardsTextGrid.tsx` - React component
|
||||
- `CardsTextGrid.scss` - Styles
|
||||
- `index.ts` - Barrel exports
|
||||
- `README.md` - This file
|
||||
4
shared/sections/CardsTextGrid/index.ts
Normal file
4
shared/sections/CardsTextGrid/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
CardsTextGrid,
|
||||
type CardsTextGridProps,
|
||||
} from './CardsTextGrid';
|
||||
@@ -25,16 +25,17 @@
|
||||
// =============================================================================
|
||||
// Design Tokens from Figma
|
||||
// =============================================================================
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
|
||||
// Section vertical padding (horizontal handled by PageGrid)
|
||||
$bds-section-padding-y-mobile: 24px;
|
||||
$bds-section-padding-y-tablet: 32px;
|
||||
$bds-section-padding-y-desktop: 40px;
|
||||
$bds-section-padding-y-mobile: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-section-padding-y-tablet: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-section-padding-y-desktop: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
|
||||
// Gap between header row and cards row
|
||||
$bds-section-row-gap-mobile: 24px;
|
||||
$bds-section-row-gap-tablet: 32px;
|
||||
$bds-section-row-gap-desktop: 40px;
|
||||
$bds-section-row-gap-mobile: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-section-row-gap-tablet: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-section-row-gap-desktop: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
|
||||
// Card height for header alignment (desktop only)
|
||||
$bds-text-card-height-desktop: 340px;
|
||||
@@ -124,7 +125,7 @@ $bds-text-color-muted: $gray-500; // #72777E - Neutral/500 for description
|
||||
margin: 0;
|
||||
|
||||
& + p {
|
||||
margin-top: 16px; // Paragraph spacing from Figma
|
||||
margin-top: $bds-space-lg; // 16px - spacing('lg')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -19,32 +19,32 @@
|
||||
// =============================================================================
|
||||
// Design Tokens (from Figma)
|
||||
// =============================================================================
|
||||
|
||||
$bds-grid-gutter: 8px;
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
// $bds-grid-gutter is defined in _spacing.scss (8px).
|
||||
|
||||
// Grid padding (matches PageGrid container padding)
|
||||
$bds-carousel-grid-padding-sm: 18px; // Mobile
|
||||
$bds-carousel-grid-padding-md: 24px; // Tablet
|
||||
$bds-carousel-grid-padding-lg: 32px; // Desktop (lg+)
|
||||
$bds-carousel-grid-padding-sm: 18px; // Mobile (non-standard)
|
||||
$bds-carousel-grid-padding-md: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-carousel-grid-padding-lg: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-carousel-grid-max-width: 1280px; // Max container width (per _breakpoints.scss $xl)
|
||||
|
||||
// Spacing - Header gap (between heading and description)
|
||||
$bds-carousel-header-gap-sm: 8px; // Mobile
|
||||
$bds-carousel-header-gap-md: 8px; // Tablet
|
||||
$bds-carousel-header-gap-lg: 16px; // Desktop
|
||||
$bds-carousel-header-gap-sm: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-carousel-header-gap-md: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-carousel-header-gap-lg: $bds-space-lg; // 16px - spacing('lg')
|
||||
|
||||
// Spacing - Section gap (between header content and buttons row on mobile)
|
||||
$bds-carousel-section-gap-sm: 24px; // Mobile
|
||||
$bds-carousel-section-gap-md: 32px; // Tablet
|
||||
$bds-carousel-section-gap-lg: 40px; // Desktop
|
||||
$bds-carousel-section-gap-sm: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-carousel-section-gap-md: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-carousel-section-gap-lg: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
|
||||
// Spacing - Gap between header and cards
|
||||
$bds-carousel-cards-gap-sm: 24px; // Mobile
|
||||
$bds-carousel-cards-gap-md: 32px; // Tablet
|
||||
$bds-carousel-cards-gap-lg: 40px; // Desktop
|
||||
$bds-carousel-cards-gap-sm: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-carousel-cards-gap-md: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-carousel-cards-gap-lg: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
|
||||
// Button gap (button styles are in shared/components/CarouselButton)
|
||||
$bds-carousel-button-gap: 8px;
|
||||
$bds-carousel-button-gap: $bds-space-sm; // 8px - spacing('sm')
|
||||
|
||||
// Card dimensions per breakpoint
|
||||
$bds-carousel-card-width-sm: 343px; // Mobile
|
||||
@@ -55,9 +55,9 @@ $bds-carousel-card-width-lg: 400px; // Desktop
|
||||
$bds-carousel-card-height-lg: 480px;
|
||||
|
||||
// Card padding per breakpoint
|
||||
$bds-carousel-card-padding-sm: 16px;
|
||||
$bds-carousel-card-padding-md: 20px;
|
||||
$bds-carousel-card-padding-lg: 24px;
|
||||
$bds-carousel-card-padding-sm: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-carousel-card-padding-md: $bds-space-xl; // 20px - spacing('xl')
|
||||
$bds-carousel-card-padding-lg: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
|
||||
// Transition
|
||||
$bds-carousel-transition: 200ms cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
@@ -131,20 +131,16 @@ export const CarouselCardList = React.forwardRef<HTMLElement, CarouselCardListPr
|
||||
<p className="bds-carousel-card-list__description body-l">{description}</p>
|
||||
</div>
|
||||
<div className="bds-carousel-card-list__nav">
|
||||
<CarouselButton
|
||||
direction="prev"
|
||||
variant={buttonVariant}
|
||||
disabled={!canScrollPrev}
|
||||
onClick={() => scroll('prev')}
|
||||
aria-label="Previous cards"
|
||||
/>
|
||||
<CarouselButton
|
||||
direction="next"
|
||||
variant={buttonVariant}
|
||||
disabled={!canScrollNext}
|
||||
onClick={() => scroll('next')}
|
||||
aria-label="Next cards"
|
||||
/>
|
||||
{(['prev', 'next'] as const).map((direction) => (
|
||||
<CarouselButton
|
||||
key={direction}
|
||||
direction={direction}
|
||||
variant={buttonVariant}
|
||||
disabled={direction === 'prev' ? !canScrollPrev : !canScrollNext}
|
||||
onClick={() => scroll(direction)}
|
||||
aria-label={direction === 'prev' ? 'Previous cards' : 'Next cards'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
261
shared/sections/FeatureSingleTopic/FeatureSingleTopic.scss
Normal file
261
shared/sections/FeatureSingleTopic/FeatureSingleTopic.scss
Normal file
@@ -0,0 +1,261 @@
|
||||
// =============================================================================
|
||||
// FeatureSingleTopic Pattern
|
||||
// =============================================================================
|
||||
// A feature section pattern with single topic layout for title and media.
|
||||
// Supports variants (default, accentSurface).
|
||||
// Orientation (left, right) is handled via Bootstrap utility classes in TSX.
|
||||
// Based on Figma: 1280px desktop design with 706px image + content area
|
||||
//
|
||||
// Note: Buttons are rendered using the ButtonGroup component.
|
||||
// =============================================================================
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
|
||||
// Background colors from _colors.scss
|
||||
$bds-single-topic-bg: $white; // #FFFFFF (Neutral-white)
|
||||
$bds-single-topic-title-bg: $gray-200; // #E6EAF0 (Neutral-200) for accentSurface variant
|
||||
|
||||
// Text colors from _colors.scss
|
||||
$bds-single-topic-title-color: $black; // #141414 (Neutral-black)
|
||||
$bds-single-topic-description-color: $gray-500; // #72777E (Neutral-500)
|
||||
|
||||
// Spacing - Desktop (≥992px) - based on Figma 1280px design
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
$bds-single-topic-desktop-py: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
$bds-single-topic-desktop-content-pl: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-single-topic-desktop-description-gap: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
$bds-single-topic-desktop-title-padding: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-single-topic-desktop-height: 565px; // Fixed height from Figma design
|
||||
|
||||
// Spacing - Tablet (576px - 991px)
|
||||
$bds-single-topic-tablet-py: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-single-topic-tablet-content-gap: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-single-topic-tablet-content-min-height: 320px; // Min height for content on tablet
|
||||
$bds-single-topic-tablet-title-description-gap: $bds-space-8xl; // 80px - spacing('8xl')
|
||||
|
||||
// Spacing - Mobile (<576px)
|
||||
$bds-single-topic-mobile-py: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-single-topic-mobile-content-gap: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-single-topic-mobile-content-min-height: 280px; // Min height for content on mobile
|
||||
$bds-single-topic-mobile-title-description-gap: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
|
||||
// =============================================================================
|
||||
// Base Styles
|
||||
// =============================================================================
|
||||
.bds-feature-single-topic {
|
||||
width: 100%;
|
||||
background-color: $bds-single-topic-bg;
|
||||
|
||||
// Container - uses PageGrid with vertical padding
|
||||
&__container {
|
||||
padding-top: $bds-single-topic-mobile-py;
|
||||
padding-bottom: $bds-single-topic-mobile-py;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: $bds-single-topic-tablet-py;
|
||||
padding-bottom: $bds-single-topic-tablet-py;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: $bds-single-topic-desktop-py;
|
||||
padding-bottom: $bds-single-topic-desktop-py;
|
||||
}
|
||||
}
|
||||
|
||||
// Row - align items stretch so columns match height
|
||||
// Use row-gap for spacing between image and content on mobile/tablet
|
||||
&__row {
|
||||
align-items: stretch;
|
||||
row-gap: $bds-single-topic-mobile-content-gap;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
row-gap: $bds-single-topic-tablet-content-gap;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
row-gap: 0;
|
||||
// Fixed height from Figma design
|
||||
height: $bds-single-topic-desktop-height;
|
||||
}
|
||||
}
|
||||
|
||||
// Media column
|
||||
&__media-col {
|
||||
@include media-breakpoint-up(lg) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Content column - flex container with left padding on desktop
|
||||
&__content-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-left: $bds-single-topic-desktop-content-pl;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Media container
|
||||
&__media {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Media image - responsive aspect ratios per Figma
|
||||
&__media-img {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
// Mobile: 343/193 aspect ratio
|
||||
aspect-ratio: 343 / 193;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
// Tablet: 16/9 aspect ratio
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
// Desktop: 701/561 aspect ratio (fills the 565px height)
|
||||
aspect-ratio: 701 / 561;
|
||||
height: $bds-single-topic-desktop-height;
|
||||
}
|
||||
}
|
||||
|
||||
// Content wrapper - uses space-between to push title to top, description/CTA to bottom
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
// Gap between accent/title section and description section
|
||||
gap: $bds-single-topic-mobile-title-description-gap; // 40px on mobile
|
||||
// Min height on mobile to prevent squished content
|
||||
min-height: $bds-single-topic-mobile-content-min-height;
|
||||
justify-content: space-between;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-single-topic-tablet-title-description-gap; // 80px on tablet
|
||||
min-height: $bds-single-topic-tablet-content-min-height;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
min-height: auto; // Desktop uses fixed height from row
|
||||
gap: 0; // space-between handles the gap on desktop
|
||||
}
|
||||
}
|
||||
|
||||
// Title section - at the top
|
||||
&__title-section {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Title - Heading MD from styles/_font.scss
|
||||
// Font: Tobias (secondary/monospace), Size: 40px, Weight: 300, Line-height: 46px, Letter-spacing: -1px
|
||||
&__title {
|
||||
@include type(heading-md);
|
||||
color: $bds-single-topic-title-color;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Description section - at the bottom, contains description + ButtonGroup
|
||||
&__description-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-single-topic-mobile-content-gap;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-single-topic-desktop-description-gap;
|
||||
}
|
||||
}
|
||||
|
||||
// Description - Label L from styles/_font.scss
|
||||
// Font: Booton (primary/sans-serif), Size: 16px, Weight: 300 (light), Line-height: 23.2px
|
||||
&__description {
|
||||
@include type(label-l);
|
||||
color: $bds-single-topic-description-color;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Variant Modifiers
|
||||
// =============================================================================
|
||||
|
||||
// Default variant - no background on title section
|
||||
.bds-feature-single-topic--default {
|
||||
.bds-feature-single-topic__title-section {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// AccentSurface variant - gray background on title section
|
||||
.bds-feature-single-topic--accentSurface {
|
||||
.bds-feature-single-topic__title-section {
|
||||
background-color: $bds-single-topic-title-bg;
|
||||
padding: $bds-single-topic-desktop-title-padding;
|
||||
// Mobile min-height
|
||||
min-height: 160px;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
// Tablet min-height
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
// Desktop min-height
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dark Mode Theme Overrides
|
||||
// =============================================================================
|
||||
|
||||
// Dark mode design tokens from Figma
|
||||
$bds-single-topic-dark-bg: $black; // #141414 (Neutral/black)
|
||||
$bds-single-topic-dark-title-bg: $gray-300; // #CAD4DF (Neutral/300default) for accentSurface variant
|
||||
$bds-single-topic-dark-title-color: $black; // #141414 - title stays black on light background
|
||||
$bds-single-topic-dark-description-color: $white; // #FFFFFF - description is white in dark mode
|
||||
|
||||
html.dark {
|
||||
.bds-feature-single-topic {
|
||||
background-color: $bds-single-topic-dark-bg;
|
||||
|
||||
&__title {
|
||||
color: $white; // White title on dark background for default variant
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: $bds-single-topic-dark-description-color;
|
||||
}
|
||||
}
|
||||
|
||||
// Default variant in dark mode - title is white on dark background
|
||||
.bds-feature-single-topic--default {
|
||||
.bds-feature-single-topic__title-section {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.bds-feature-single-topic__title {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
// AccentSurface variant in dark mode - title section has light background
|
||||
.bds-feature-single-topic--accentSurface {
|
||||
.bds-feature-single-topic__title-section {
|
||||
background-color: $bds-single-topic-dark-title-bg;
|
||||
}
|
||||
|
||||
// Title stays black on the light gray background
|
||||
.bds-feature-single-topic__title {
|
||||
color: $bds-single-topic-dark-title-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
146
shared/sections/FeatureSingleTopic/FeatureSingleTopic.tsx
Normal file
146
shared/sections/FeatureSingleTopic/FeatureSingleTopic.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid } from '../../components/PageGrid/page-grid';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from 'shared/patterns/ButtonGroup/ButtonGroup';
|
||||
|
||||
export interface FeatureSingleTopicProps {
|
||||
/** Background variant for the title section
|
||||
* - 'default': No background on title section
|
||||
* - 'accentSurface': Gray background (#E6EAF0) on title section
|
||||
*/
|
||||
variant?: 'default' | 'accentSurface';
|
||||
/** Content arrangement - controls position of image relative to content
|
||||
* - 'left': Image on left, content on right
|
||||
* - 'right': Image on right, content on left
|
||||
*/
|
||||
orientation?: 'left' | 'right';
|
||||
/** Feature title text (heading-md typography) */
|
||||
title: string;
|
||||
/** Feature description text (label-l typography) */
|
||||
description?: string;
|
||||
/** Array of links (1-5 links supported)
|
||||
* - 1 link: renders as primary or secondary button (based on singleButtonVariant)
|
||||
* - 2 links: renders as primary + tertiary buttons side by side
|
||||
* - 3+ links: all tertiary buttons stacked
|
||||
*/
|
||||
buttons?: ButtonConfig[];
|
||||
/** Button variant for single button configuration
|
||||
* - 'primary': Primary button (default)
|
||||
* - 'secondary': Secondary button
|
||||
*/
|
||||
singleButtonVariant?: 'primary' | 'secondary';
|
||||
/** Feature media (image) configuration */
|
||||
media: {
|
||||
src: string;
|
||||
alt: string;
|
||||
};
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* FeatureSingleTopic Pattern
|
||||
*
|
||||
* A feature section pattern that pairs a title/description with a media element
|
||||
* in a two-column layout. Supports two variants: default (no title background)
|
||||
* and accentSurface (gray background on title section).
|
||||
*
|
||||
* Layout based on Figma 1280px design:
|
||||
* - Desktop: Side-by-side with image 7 columns, content 5 columns
|
||||
* - Mobile/Tablet: Stacked layout (full width)
|
||||
*/
|
||||
export const FeatureSingleTopic: React.FC<FeatureSingleTopicProps> = ({
|
||||
variant = 'default',
|
||||
orientation = 'left',
|
||||
title,
|
||||
description,
|
||||
buttons = [],
|
||||
singleButtonVariant = 'primary',
|
||||
media,
|
||||
className,
|
||||
}) => {
|
||||
// Validate buttons if provided (max 5 buttons supported)
|
||||
const buttonValidation = validateButtonGroup(buttons, 5);
|
||||
const hasButtons = buttonValidation.hasButtons;
|
||||
|
||||
// Button color is always green for this component
|
||||
const buttonColor = 'green';
|
||||
const forceColor = false;
|
||||
|
||||
// Build root class names
|
||||
const rootClasses = clsx(
|
||||
'bds-feature-single-topic',
|
||||
`bds-feature-single-topic--${variant}`,
|
||||
className
|
||||
);
|
||||
|
||||
// Build row class names - column-reverse on mobile/tablet for both orientations
|
||||
const rowClasses = clsx(
|
||||
'bds-feature-single-topic__row',
|
||||
'flex-column-reverse flex-lg-row' // Content above image on mobile, side-by-side on desktop
|
||||
);
|
||||
|
||||
|
||||
// Render content section (title at top, description/CTA at bottom)
|
||||
const renderContent = () => (
|
||||
<div className="bds-feature-single-topic__content">
|
||||
<div className="bds-feature-single-topic__title-section">
|
||||
<h2 className="bds-feature-single-topic__title">{title}</h2>
|
||||
</div>
|
||||
<div className="bds-feature-single-topic__description-section">
|
||||
{description && (
|
||||
<p className="bds-feature-single-topic__description">{description}</p>
|
||||
)}
|
||||
{hasButtons && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color={buttonColor}
|
||||
forceColor={forceColor}
|
||||
singleButtonVariant={singleButtonVariant}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Render media section
|
||||
const renderMedia = () => (
|
||||
<div className="bds-feature-single-topic__media">
|
||||
<img
|
||||
src={media.src}
|
||||
alt={media.alt}
|
||||
className="bds-feature-single-topic__media-img"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className={rootClasses}>
|
||||
<PageGrid className="bds-feature-single-topic__container" containerType="standard">
|
||||
<PageGrid.Row className={rowClasses}>
|
||||
<PageGrid.Col
|
||||
span={{ base: 4, md: 8, lg: 7 }}
|
||||
className={clsx(
|
||||
'bds-feature-single-topic__media-col',
|
||||
orientation === 'left' ? 'order-lg-1' : 'order-lg-2'
|
||||
)}
|
||||
>
|
||||
{renderMedia()}
|
||||
</PageGrid.Col>
|
||||
<PageGrid.Col
|
||||
span={{ base: 4, md: 8, lg: 5 }}
|
||||
className={clsx(
|
||||
'bds-feature-single-topic__content-col',
|
||||
orientation === 'left' ? 'order-lg-2' : 'order-lg-1'
|
||||
)}
|
||||
>
|
||||
{renderContent()}
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
</PageGrid>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureSingleTopic;
|
||||
|
||||
185
shared/sections/FeatureSingleTopic/README.md
Normal file
185
shared/sections/FeatureSingleTopic/README.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# FeatureSingleTopic Pattern
|
||||
|
||||
A feature section pattern that pairs a title/description with a media element in a two-column layout. Supports two variants (default, accentSurface) and two orientations (left, right).
|
||||
|
||||
## Features
|
||||
|
||||
- Responsive two-column layout (image + content) that stacks on smaller screens
|
||||
- Two background variants: default (no background) and accentSurface (gray title background)
|
||||
- Two orientations: left (image left) and right (image right)
|
||||
- Flexible button layout supporting 1-5 links with automatic variant assignment
|
||||
- Responsive image aspect ratios per Figma design
|
||||
- Full dark mode support
|
||||
- Uses PageGrid for consistent spacing
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```tsx
|
||||
import { FeatureSingleTopic } from 'shared/patterns/FeatureSingleTopic';
|
||||
|
||||
<FeatureSingleTopic
|
||||
variant="default"
|
||||
orientation="left"
|
||||
title="Developer Spotlight"
|
||||
description="Are you building a peer-to-peer payments solution?"
|
||||
media={{
|
||||
src: "/img/feature-image.png",
|
||||
alt: "Feature image"
|
||||
}}
|
||||
links={[
|
||||
{ label: "Get Started", href: "/start" },
|
||||
{ label: "Learn More", href: "/learn" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `variant` | `'default' \| 'accentSurface'` | `'default'` | Background variant for title section |
|
||||
| `orientation` | `'left' \| 'right'` | `'left'` | Image position relative to content |
|
||||
| `title` | `string` | *required* | Feature title (heading-md typography) |
|
||||
| `description` | `string` | - | Feature description (label-l typography) |
|
||||
| `buttons` | `ButtonConfig[]` | `[]` | Array of button configurations (1-5 supported) |
|
||||
| `singleButtonVariant` | `'primary' \| 'secondary'` | `'primary'` | Button variant for single button configuration |
|
||||
| `media` | `{ src: string; alt: string }` | *required* | Image configuration |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
|
||||
### ButtonConfig
|
||||
|
||||
```tsx
|
||||
interface ButtonConfig {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
forceColor?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Button configurations are handled by the `ButtonGroup` component. See [ButtonGroup documentation](../ButtonGroup/README.md) for more details.
|
||||
|
||||
## Button Behavior
|
||||
|
||||
The component automatically determines button variants based on count:
|
||||
|
||||
| Count | Layout |
|
||||
|-------|--------|
|
||||
| 1 button | Primary or Secondary button (configurable via `singleButtonVariant` prop) |
|
||||
| 2 buttons | Primary + Tertiary side by side |
|
||||
| 3-5 buttons | All Tertiary buttons stacked |
|
||||
|
||||
**Note:** The component supports a maximum of 5 buttons. Additional buttons beyond 5 will trigger a validation warning in development mode and will be ignored. On mobile, the first two buttons (Primary + Tertiary) remain side by side.
|
||||
|
||||
## Variants
|
||||
|
||||
### Default
|
||||
No background on the title section. Clean, minimal look.
|
||||
|
||||
```tsx
|
||||
<FeatureSingleTopic variant="default" ... />
|
||||
```
|
||||
|
||||
### AccentSurface
|
||||
Gray background (#E6EAF0 light / #CAD4DF dark) on the title section.
|
||||
|
||||
```tsx
|
||||
<FeatureSingleTopic variant="accentSurface" ... />
|
||||
```
|
||||
|
||||
## Orientation
|
||||
|
||||
### Left (default)
|
||||
Image on left, content on right on desktop.
|
||||
|
||||
### Right
|
||||
Image on right, content on left on desktop.
|
||||
|
||||
**Note:** On mobile/tablet, content always appears above image regardless of orientation.
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
### Desktop (≥992px)
|
||||
- Side-by-side layout: 7-column image, 5-column content
|
||||
- Fixed height: 565px
|
||||
- Image aspect ratio: 701/561
|
||||
|
||||
### Tablet (768px - 991px)
|
||||
- Stacked layout with 32px gap between sections
|
||||
- Image aspect ratio: 16/9
|
||||
- Content min-height: 320px
|
||||
|
||||
### Mobile (<768px)
|
||||
- Stacked layout with 24px gap between sections
|
||||
- Image aspect ratio: 343/193
|
||||
- Content min-height: 280px
|
||||
|
||||
## CSS Classes
|
||||
|
||||
```
|
||||
.bds-feature-single-topic // Section container
|
||||
.bds-feature-single-topic--default // Default variant modifier
|
||||
.bds-feature-single-topic--accentSurface // AccentSurface variant modifier
|
||||
.bds-feature-single-topic__container // PageGrid container
|
||||
.bds-feature-single-topic__row // PageGrid row (uses flex-column-reverse flex-lg-row)
|
||||
.bds-feature-single-topic__media-col // Media column (uses order-lg-1 or order-lg-2)
|
||||
.bds-feature-single-topic__content-col // Content column (uses order-lg-1 or order-lg-2)
|
||||
.bds-feature-single-topic__media // Media wrapper
|
||||
.bds-feature-single-topic__media-img // Image element
|
||||
.bds-feature-single-topic__content // Content wrapper
|
||||
.bds-feature-single-topic__title-section // Title section
|
||||
.bds-feature-single-topic__title // Title element
|
||||
.bds-feature-single-topic__description-section // Description + buttons wrapper
|
||||
.bds-feature-single-topic__description // Description element
|
||||
```
|
||||
|
||||
**Note:**
|
||||
- Orientation logic is handled via Bootstrap utility classes (`order-lg-1`, `order-lg-2`) applied dynamically in TSX
|
||||
- Buttons are rendered by the `ButtonGroup` component with its own class structure
|
||||
- Mobile/tablet layout uses `flex-column-reverse` to show content above image
|
||||
- Desktop layout uses `flex-lg-row` for side-by-side display
|
||||
|
||||
## Typography Tokens
|
||||
|
||||
- **Title**: Uses `heading-md` type token (Tobias Light font)
|
||||
- Desktop: 40px / 46px line-height / -1px letter-spacing
|
||||
|
||||
- **Description**: Uses `label-l` type token (Booton Light font)
|
||||
- Desktop: 16px / 23.2px line-height
|
||||
|
||||
## Dark Mode
|
||||
|
||||
Full dark mode support with `html.dark` selector:
|
||||
|
||||
- **Section background**: #141414 (black)
|
||||
- **Title (default variant)**: #FFFFFF (white)
|
||||
- **Title (accentSurface)**: #141414 (black) on #CAD4DF background
|
||||
- **Description**: #FFFFFF (white)
|
||||
|
||||
## Files
|
||||
|
||||
- `FeatureSingleTopic.tsx` - Main pattern component
|
||||
- `FeatureSingleTopic.scss` - Styles with responsive breakpoints
|
||||
- `index.ts` - Barrel exports
|
||||
- `README.md` - This documentation
|
||||
|
||||
## Design References
|
||||
|
||||
- **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/`
|
||||
|
||||
## Related Components
|
||||
|
||||
- **Button**: Used for CTA buttons
|
||||
- **PageGrid**: Used for responsive grid layout
|
||||
|
||||
## Version History
|
||||
|
||||
- **February 2026**: Initial implementation
|
||||
- Two variants (default, accentSurface)
|
||||
- Two orientations (left, right)
|
||||
- Responsive image aspect ratios
|
||||
- 1-5 link support with automatic button variant assignment
|
||||
- Full dark mode support
|
||||
|
||||
3
shared/sections/FeatureSingleTopic/index.ts
Normal file
3
shared/sections/FeatureSingleTopic/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { FeatureSingleTopic, type FeatureSingleTopicProps } from './FeatureSingleTopic';
|
||||
export { default } from './FeatureSingleTopic';
|
||||
|
||||
@@ -36,48 +36,32 @@ $bds-feature-description-color: $black; // #141414 (Neutral-black)
|
||||
$bds-feature-description-color-dark: $black; // #141414 (same in dark mode)
|
||||
|
||||
// Spacing - Desktop (≥992px) - based on Figma 1280px design
|
||||
$bds-feature-desktop-py: 96px;
|
||||
$bds-feature-desktop-text-gap: 16px;
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
// $bds-grid-gutter is defined in _spacing.scss (8px).
|
||||
$bds-feature-desktop-py: 96px; // Non-standard value, kept as-is
|
||||
$bds-feature-desktop-text-gap: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-feature-desktop-cta-gap-col: 0; // Gap between button rows
|
||||
$bds-feature-desktop-content-gap: 0; // Gap between text-group and cta (space-between handles this)
|
||||
$bds-feature-desktop-button-gap: 16px; // Gap between buttons in ButtonGroup (consistent across all sizes)
|
||||
|
||||
// Spacing - Tablet (576px - 991px) - based on Figma 768px design
|
||||
$bds-feature-tablet-py: 80px;
|
||||
$bds-feature-tablet-pl: 115px; // Left padding from Figma
|
||||
$bds-feature-tablet-pr: 107px; // Right padding from Figma
|
||||
$bds-feature-tablet-text-gap: 8px;
|
||||
$bds-feature-tablet-cta-gap-row: 16px; // Gap between buttons in row from Figma
|
||||
$bds-feature-tablet-py: $bds-space-8xl; // 80px - spacing('8xl')
|
||||
$bds-feature-tablet-pl: 115px; // Left padding from Figma (non-standard)
|
||||
$bds-feature-tablet-pr: 107px; // Right padding from Figma (non-standard)
|
||||
$bds-feature-tablet-text-gap: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-feature-tablet-cta-gap-row: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-feature-tablet-cta-gap-col: 0;
|
||||
$bds-feature-tablet-content-gap: 32px;
|
||||
$bds-feature-tablet-button-gap: 16px; // Gap between buttons in ButtonGroup (consistent across all sizes)
|
||||
$bds-feature-tablet-content-gap: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
|
||||
// Spacing - Mobile (<576px) - based on Figma 375px design
|
||||
$bds-feature-mobile-py: 64px;
|
||||
$bds-feature-mobile-px: 16px;
|
||||
$bds-feature-mobile-text-gap: 8px;
|
||||
$bds-feature-mobile-cta-gap: 16px; // Gap between stacked buttons from Figma
|
||||
$bds-feature-mobile-content-gap: 24px;
|
||||
$bds-feature-mobile-button-gap: 16px; // Gap between buttons in ButtonGroup (consistent across all sizes)
|
||||
|
||||
// Grid gutter - consistent with PageGrid
|
||||
$bds-grid-gutter: 8px;
|
||||
$bds-feature-mobile-py: $bds-space-6xl; // 64px - spacing('6xl')
|
||||
$bds-feature-mobile-px: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-feature-mobile-text-gap: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-feature-mobile-cta-gap: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-feature-mobile-content-gap: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
|
||||
// =============================================================================
|
||||
// Base Styles
|
||||
// =============================================================================
|
||||
.bds-feature-two-column__button-group {
|
||||
.bds-btn--tertiary {
|
||||
padding-top: 0px !important;
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-feature-two-column__cta-row{
|
||||
.bds-btn--tertiary {
|
||||
padding-top: 16px !important;
|
||||
}
|
||||
}
|
||||
.bds-feature-two-column {
|
||||
width: 100%;
|
||||
|
||||
@@ -153,18 +137,18 @@ $bds-grid-gutter: 8px;
|
||||
// Mobile: 4 columns, content full width
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: $bds-grid-gutter;
|
||||
padding: 0 16px; // Mobile edge padding
|
||||
padding: 0 $bds-space-lg; // 16px - spacing('lg')
|
||||
|
||||
// Tablet: 8 columns
|
||||
@include media-breakpoint-up(md) {
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
padding: 0 24px; // Tablet edge padding
|
||||
padding: 0 $bds-space-2xl; // 24px - spacing('2xl')
|
||||
}
|
||||
|
||||
// Desktop: 6 columns (within the 50% content half)
|
||||
@include media-breakpoint-up(lg) {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
padding: 0 32px; // Desktop edge padding
|
||||
padding: 0 $bds-space-3xl; // 32px - spacing('3xl')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,11 +214,14 @@ $bds-grid-gutter: 8px;
|
||||
&__content--multiple {
|
||||
// Mobile: no gap since we only have 2 items (text-group and button-group)
|
||||
// The 24px gap is handled via button-group margin or flex gap
|
||||
gap: 0;
|
||||
gap: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
justify-content: flex-start;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: 0; // Desktop uses space-between for auto distribution
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@@ -244,39 +231,6 @@ $bds-grid-gutter: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// Button group - contains all buttons in the multiple links layout
|
||||
// 16px gap between buttons on ALL screen sizes
|
||||
&__button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: $bds-feature-mobile-button-gap; // 16px on mobile
|
||||
margin-top: $bds-feature-mobile-content-gap; // 24px spacing from text-group on mobile
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-feature-tablet-button-gap; // 16px on tablet
|
||||
margin-top: $bds-feature-tablet-content-gap; // 32px spacing from text-group on tablet
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-feature-desktop-button-gap; // 16px on desktop
|
||||
margin-top: 0; // Desktop uses space-between, no explicit margin needed
|
||||
}
|
||||
|
||||
// Tertiary links need left padding removed to align text with title
|
||||
>.bds-btn--tertiary {
|
||||
padding-left: 0 !important;
|
||||
margin-left: 0;
|
||||
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Text group - title + description
|
||||
&__text-group {
|
||||
display: flex;
|
||||
@@ -308,69 +262,7 @@ $bds-grid-gutter: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// CTA container - base styles
|
||||
&__cta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
// CTA variant: Single secondary button - maintains vertical layout
|
||||
&__cta--single {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// CTA variant: Primary + tertiary - horizontal on tablet+
|
||||
&__cta--double {
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
// CTA row - for first two buttons in multiple links layout (Primary + Tertiary)
|
||||
// Horizontal layout on tablet+ (md and lg share same styles)
|
||||
&__cta-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
@include media-breakpoint-up(lg) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Tertiary group - groups remaining tertiary links together as a single unit
|
||||
// No gap between tertiary links - they stack tightly as a group
|
||||
&__tertiary-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0; // No spacing between tertiary links in the group
|
||||
|
||||
// Tertiary links need left padding removed to align text with title
|
||||
.bds-btn--tertiary {
|
||||
padding-left: 0 !important;
|
||||
margin-left: 0;
|
||||
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Media container
|
||||
&__media {
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '../../components/Button/Button';
|
||||
import { PageGrid } from '../../components/PageGrid/page-grid';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from 'shared/patterns/ButtonGroup/ButtonGroup';
|
||||
|
||||
export interface FeatureTwoColumnLink {
|
||||
/** Link label text */
|
||||
@@ -59,6 +59,22 @@ export const FeatureTwoColumn: React.FC<FeatureTwoColumnProps> = ({
|
||||
media,
|
||||
className,
|
||||
}) => {
|
||||
// Determine button color based on background
|
||||
// Rule: Black buttons must be used for all backgrounds (including neutral)
|
||||
const buttonColor = 'black';
|
||||
const forceColor = true;
|
||||
|
||||
// Convert links to ButtonConfig format
|
||||
const buttonConfigs: ButtonConfig[] = links.map(link => ({
|
||||
label: link.label,
|
||||
href: link.href,
|
||||
forceColor: forceColor,
|
||||
}));
|
||||
|
||||
// Validate buttons (FeatureTwoColumn supports 1-5 links per design spec)
|
||||
const buttonValidation = validateButtonGroup(buttonConfigs, 5);
|
||||
const hasButtons = buttonValidation.hasButtons;
|
||||
|
||||
// Build root class names
|
||||
const rootClasses = clsx(
|
||||
'bds-feature-two-column',
|
||||
@@ -67,94 +83,30 @@ export const FeatureTwoColumn: React.FC<FeatureTwoColumnProps> = ({
|
||||
className
|
||||
);
|
||||
|
||||
// Determine button color based on background
|
||||
// Rule: Black buttons must be used for all backgrounds (including neutral)
|
||||
const buttonColor = 'black';
|
||||
const forceColor = true;
|
||||
|
||||
// Render content section with appropriate CTA layout based on link count
|
||||
// For 3-5 links, items are direct children for space-between distribution
|
||||
// Render content section with ButtonGroup
|
||||
const renderContent = () => {
|
||||
const linkCount = links.length;
|
||||
// Determine content class based on validated button count
|
||||
const contentClass = clsx(
|
||||
'bds-feature-two-column__content',
|
||||
{
|
||||
'bds-feature-two-column__content--multiple': hasButtons && buttonValidation.buttons.length >= 3,
|
||||
}
|
||||
);
|
||||
|
||||
// 1 link: Secondary button
|
||||
if (linkCount === 1) {
|
||||
return (
|
||||
<div className="bds-feature-two-column__content">
|
||||
<div className="bds-feature-two-column__text-group">
|
||||
<h2 className="bds-feature-two-column__title">{title}</h2>
|
||||
<p className="bds-feature-two-column__description">{description}</p>
|
||||
</div>
|
||||
<div className="bds-feature-two-column__cta bds-feature-two-column__cta--single">
|
||||
<Button variant="secondary" color={buttonColor} forceColor={forceColor} href={links[0].href}>
|
||||
{links[0].label}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 2 links: Primary + Tertiary in a row
|
||||
if (linkCount === 2) {
|
||||
return (
|
||||
<div className="bds-feature-two-column__content">
|
||||
<div className="bds-feature-two-column__text-group">
|
||||
<h2 className="bds-feature-two-column__title">{title}</h2>
|
||||
<p className="bds-feature-two-column__description">{description}</p>
|
||||
</div>
|
||||
<div className="bds-feature-two-column__cta bds-feature-two-column__cta--double">
|
||||
<Button variant="primary" color={buttonColor} forceColor={forceColor} href={links[0].href}>
|
||||
{links[0].label}
|
||||
</Button>
|
||||
<Button variant="tertiary" color={buttonColor} forceColor={forceColor} href={links[1].href}>
|
||||
{links[1].label}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 3-5 links: Text group + Button group (contains all buttons with consistent 16px spacing)
|
||||
// Desktop: space-between distribution between text-group and button-group
|
||||
// Tablet: 32px gap, Mobile: 24px gap
|
||||
return (
|
||||
<div className="bds-feature-two-column__content bds-feature-two-column__content--multiple">
|
||||
<div className={contentClass}>
|
||||
<div className="bds-feature-two-column__text-group">
|
||||
<h2 className="bds-feature-two-column__title">{title}</h2>
|
||||
<p className="bds-feature-two-column__description">{description}</p>
|
||||
</div>
|
||||
{/* Button group - all buttons grouped with 16px spacing between them */}
|
||||
<div className="bds-feature-two-column__button-group">
|
||||
{/* First two links in a row: Primary + Tertiary */}
|
||||
<div className="bds-feature-two-column__cta-row">
|
||||
<Button variant="primary" color={buttonColor} forceColor={forceColor} href={links[0].href}>
|
||||
{links[0].label}
|
||||
</Button>
|
||||
{links[1] && (
|
||||
<Button variant="tertiary" color={buttonColor} forceColor={forceColor} href={links[1].href}>
|
||||
{links[1].label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{/* Secondary button */}
|
||||
{links[2] && (
|
||||
<Button variant="secondary" color={buttonColor} forceColor={forceColor} href={links[2].href}>
|
||||
{links[2].label}
|
||||
</Button>
|
||||
)}
|
||||
{/* Remaining tertiary links */}
|
||||
{links.slice(3).map((link) => (
|
||||
<Button
|
||||
key={`${link.href}-${link.label}`}
|
||||
variant="tertiary"
|
||||
color={buttonColor}
|
||||
forceColor={forceColor}
|
||||
href={link.href}
|
||||
>
|
||||
{link.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{hasButtons && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color={buttonColor}
|
||||
forceColor={forceColor}
|
||||
singleButtonVariant="secondary"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
147
shared/sections/FeaturedVideoHero/FeaturedVideoHero.tsx
Normal file
147
shared/sections/FeaturedVideoHero/FeaturedVideoHero.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import React, { forwardRef, useCallback } from "react";
|
||||
import clsx from "clsx";
|
||||
import { PageGrid } from "shared/components/PageGrid/page-grid";
|
||||
import { Video, type VideoSource } from "shared/components/Video";
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from "shared/patterns/ButtonGroup/ButtonGroup";
|
||||
import { isEmpty, isEnvironment } from "shared/utils";
|
||||
import {
|
||||
DesignConstrainedLinksProps,
|
||||
DesignConstrainedVideoProps,
|
||||
} from "shared/utils/types";
|
||||
|
||||
/** Video config for embed (YouTube/Vimeo/Wistia) with optional cover image */
|
||||
export interface FeaturedVideoHeroVideoConfig {
|
||||
source: VideoSource;
|
||||
coverImage?: {
|
||||
src: string;
|
||||
alt: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FeaturedVideoHeroProps
|
||||
extends
|
||||
React.ComponentPropsWithoutRef<"header">,
|
||||
DesignConstrainedLinksProps {
|
||||
headline: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
/** Native HTML video props (use when video is not provided) */
|
||||
videoElement?: DesignConstrainedVideoProps;
|
||||
/** Video config for embed + optional cover modal (use when videoElement is not provided) */
|
||||
video?: FeaturedVideoHeroVideoConfig;
|
||||
}
|
||||
const FeaturedVideoHero = forwardRef<HTMLElement, FeaturedVideoHeroProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
headline,
|
||||
subtitle,
|
||||
videoElement,
|
||||
video,
|
||||
links,
|
||||
className,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const hasVideo = !isEmpty(video) || !isEmpty(videoElement);
|
||||
|
||||
const validateProps = useCallback<() => boolean>(() => {
|
||||
if (isEmpty(headline)) {
|
||||
if (isEnvironment(["development", "test"])) {
|
||||
console.warn("headline is required for FeaturedVideoHero");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!hasVideo) {
|
||||
if (isEnvironment(["development", "test"])) {
|
||||
console.warn("videoElement or video is required for FeaturedVideoHero");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [headline, hasVideo]);
|
||||
|
||||
if (!validateProps()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Map links to ButtonConfig format for ButtonGroup
|
||||
const buttonConfigs: ButtonConfig[] = (links ?? [])
|
||||
.filter((link) => !isEmpty(link) && link.label && link.href)
|
||||
.map((link) => ({
|
||||
label: link.label,
|
||||
href: link.href,
|
||||
forceColor: true,
|
||||
}));
|
||||
|
||||
// Validate buttons (max 2 CTAs supported)
|
||||
const buttonValidation = validateButtonGroup(
|
||||
buttonConfigs,
|
||||
2,
|
||||
isEnvironment(["development", "test"]) // Only log warnings in dev/test
|
||||
);
|
||||
const hasLinks = buttonValidation.hasButtons;
|
||||
|
||||
return (
|
||||
<header
|
||||
ref={ref}
|
||||
className={clsx("bds-featured-video-hero", className)}
|
||||
{...rest}
|
||||
>
|
||||
<PageGrid>
|
||||
<PageGrid.Row>
|
||||
<PageGrid.Col span={{ base: 4, md: 8, lg: 6 }}>
|
||||
<div className="bds-featured-video-hero__content">
|
||||
<h1 className="mb-0 h-md">
|
||||
{headline}
|
||||
</h1>
|
||||
|
||||
<div className="bds-featured-video-hero__bottom-group">
|
||||
{subtitle && (
|
||||
<PageGrid.Row className="bds-featured-video-hero__subtitle body-l">
|
||||
<PageGrid.Col
|
||||
span={{ base: "fill", md: 6, lg: 10 }}
|
||||
className="bds-featured-video-hero__subtitle-col"
|
||||
as="p"
|
||||
>
|
||||
{subtitle}
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
)}
|
||||
{hasLinks && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color="green"
|
||||
forceColor
|
||||
gap="small"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageGrid.Col>
|
||||
<PageGrid.Col
|
||||
span={{ base: 4, md: 8, lg: 6 }}
|
||||
>
|
||||
<div className="bds-featured-video-hero__video-container">
|
||||
{video ? (
|
||||
<Video
|
||||
source={video.source}
|
||||
coverImage={video.coverImage}
|
||||
className="bds-featured-video-hero__video"
|
||||
/>
|
||||
) : (
|
||||
<Video
|
||||
source={{ type: "native", props: videoElement! }}
|
||||
className="bds-featured-video-hero__video"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
</PageGrid>
|
||||
</header>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FeaturedVideoHero.displayName = "FeaturedVideoHero";
|
||||
|
||||
export default FeaturedVideoHero;
|
||||
161
shared/sections/FeaturedVideoHero/README.md
Normal file
161
shared/sections/FeaturedVideoHero/README.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# FeaturedVideoHero Pattern
|
||||
|
||||
A page-level hero pattern featuring a headline, optional subtitle, call-to-action buttons, and a featured video. The video uses native HTML `<video>` props and is displayed in a responsive two-column layout with content on the left and video on the right.
|
||||
|
||||
## Overview
|
||||
|
||||
The FeaturedVideoHero component provides a structured hero section with:
|
||||
|
||||
- Responsive two-column layout (content left, video right) that stacks on smaller screens
|
||||
- Required headline and video; optional subtitle and call-to-action buttons
|
||||
- Design-constrained CTAs: primary and optional secondary, with variant and color set by the component
|
||||
- Development-time validation: returns `null` when required props are missing and logs warnings in development/test
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```tsx
|
||||
import { FeaturedVideoHero } from "shared/patterns/FeaturedVideoHero";
|
||||
|
||||
function MyPage() {
|
||||
return (
|
||||
<FeaturedVideoHero
|
||||
headline="Build on XRPL"
|
||||
subtitle={
|
||||
<p>
|
||||
Issue, manage, and trade real-world assets without needing to build
|
||||
smart contracts.
|
||||
</p>
|
||||
}
|
||||
links={[{ label: "Get Started", href: "/docs" }]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
| --------------- | --------------------------------- | -------- | ---------------------------------------------------------------------------- |
|
||||
| `headline` | `React.ReactNode` | Yes | Hero headline text (h-md typography) |
|
||||
| `subtitle` | `React.ReactNode` | No | Hero subtitle content |
|
||||
| `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 |
|
||||
|
||||
### Links (ButtonGroup)
|
||||
|
||||
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:
|
||||
|
||||
- **First link**: `variant="primary"`, `color="green"`, `forceColor={true}`
|
||||
- **Second link**: `variant="tertiary"`, `color="green"`, `forceColor={true}`
|
||||
- Max 2 links supported (ButtonGroup validation)
|
||||
|
||||
### Video Element
|
||||
|
||||
`videoElement` accepts native HTML video element props. Required and commonly used props:
|
||||
|
||||
- `src` (required) – Video URL
|
||||
- `autoPlay`, `loop`, `muted`, `playsInline` – Typical for background/hero autoplay
|
||||
- `controls`, `preload`, `poster` – Optional; use for user-controlled playback
|
||||
|
||||
The video is rendered with `object-fit: cover` and a 16:9 aspect ratio container.
|
||||
|
||||
## Examples
|
||||
|
||||
### With primary and secondary CTAs
|
||||
|
||||
```tsx
|
||||
<FeaturedVideoHero
|
||||
headline="Real-world asset tokenization"
|
||||
subtitle="Learn how to issue crypto tokens and build tokenization solutions."
|
||||
links={[
|
||||
{ label: "Get Started", href: "/docs" },
|
||||
{ label: "Learn More", href: "/about" },
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/tokenization.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Without subtitle
|
||||
|
||||
```tsx
|
||||
<FeaturedVideoHero
|
||||
headline="Headline Only"
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### With video controls
|
||||
|
||||
```tsx
|
||||
<FeaturedVideoHero
|
||||
headline="Watch and Learn"
|
||||
subtitle="Explore our video tutorials and guides."
|
||||
links={[{ label: "Watch Tutorials", href: "/tutorials" }]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: false,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
controls: true,
|
||||
preload: "metadata",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
- **Required props**: `headline`, `videoElement`. If either is missing or empty, the component returns `null` and (in development/test) logs a console warning.
|
||||
- **Optional props**: `subtitle`, `links`. Omit `links` or pass an empty array to hide the button section.
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
- **Mobile / small screens**: Content and video stack vertically; video appears below the content block with top margin.
|
||||
- **Large (lg+)**: Two-column layout: content (5 cols) on the left, video (6 cols, offset 1) on the right. Video container uses 16:9 aspect ratio and `object-fit: cover`.
|
||||
|
||||
## CSS Classes
|
||||
|
||||
- `bds-featured-video-hero` – Root header element
|
||||
- `bds-featured-video-hero__content` – Content column (headline, subtitle, CTAs)
|
||||
- `bds-featured-video-hero__title` – Headline (`h1`)
|
||||
- `bds-featured-video-hero__subtitle` – Subtitle row
|
||||
- `bds-featured-video-hero__subtitle-col` – Subtitle column
|
||||
- `bds-featured-video-hero__cta-buttons` – CTA buttons wrapper
|
||||
- `bds-featured-video-hero__video-container` – Video wrapper (16:9)
|
||||
- `bds-featured-video-hero__video` – Video element
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Video format**: Use MP4 with H.264 for broad compatibility; keep file sizes reasonable for fast loading.
|
||||
2. **Autoplay**: Use `muted` and `playsInline` with `autoPlay` for reliable autoplay on mobile.
|
||||
3. **CTAs**: Keep CTA text concise and action-oriented; primary CTA should be the main action.
|
||||
4. **Headlines**: Keep headlines concise; use the subtitle for additional context.
|
||||
5. **Accessibility**: Provide an `aria-label` (or other accessible name) on the video when it conveys meaningful content.
|
||||
|
||||
## Showcase
|
||||
|
||||
An interactive showcase with more examples and prop documentation is available at:
|
||||
|
||||
- **Showcase page**: `/about/featured-video-hero-showcase.page.tsx`
|
||||
91
shared/sections/FeaturedVideoHero/_featured-video-hero.scss
Normal file
91
shared/sections/FeaturedVideoHero/_featured-video-hero.scss
Normal file
@@ -0,0 +1,91 @@
|
||||
// BDS FeaturedVideoHero Pattern Styles
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
|
||||
.bds-featured-video-hero{
|
||||
padding: $bds-space-2xl 0; // 24px - spacing('2xl')
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding: $bds-space-3xl 0; // 32px - spacing('3xl')
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding: $bds-space-4xl 0; // 40px - spacing('4xl')
|
||||
}
|
||||
|
||||
@include bds-theme-mode(light) {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
@include bds-theme-mode(dark) {
|
||||
background-color: $black;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
height: 100%;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
}
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__video-container {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
margin-top: $bds-space-lg; // 16px - spacing('lg')
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-top: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-top: $bds-space-none; // 0px - spacing('none')
|
||||
}
|
||||
}
|
||||
|
||||
&__video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__bottom-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
gap: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,27 @@
|
||||
import React, { forwardRef, memo, useEffect } from "react";
|
||||
import clsx from "clsx";
|
||||
import { PageGrid } from "shared/components/PageGrid/page-grid";
|
||||
import { Button, ButtonProps } from "shared/components/Button/Button";
|
||||
import { ButtonGroup, validateButtonGroup } from "shared/patterns/ButtonGroup/ButtonGroup";
|
||||
import { isEmpty, isEnvironment } from "shared/utils";
|
||||
import {
|
||||
isEmpty,
|
||||
DesignConstrainedButtonProps,
|
||||
isEnvironment,
|
||||
} from "shared/utils";
|
||||
|
||||
/**
|
||||
* Base props that all media elements must have to ensure proper styling.
|
||||
* These props are automatically applied to maintain the 9:16 aspect ratio
|
||||
* and object-fit: cover behavior.
|
||||
*/
|
||||
type MediaStyleProps = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
DesignConstrainedImageProps,
|
||||
DesignConstrainedVideoProps,
|
||||
DesignConstrainedLinksProps,
|
||||
} from "shared/utils/types";
|
||||
|
||||
/**
|
||||
* Image media type - extends native img element props
|
||||
*/
|
||||
type ImageMediaProps = {
|
||||
type: "image";
|
||||
} & Omit<
|
||||
React.ComponentPropsWithoutRef<"img">,
|
||||
keyof MediaStyleProps | "src" | "alt"
|
||||
> & {
|
||||
src: string; // Required for image media
|
||||
alt: string; // Required for image media
|
||||
};
|
||||
} & DesignConstrainedImageProps;
|
||||
|
||||
/**
|
||||
* Video media type - extends native video element props
|
||||
*/
|
||||
type VideoMediaProps = {
|
||||
type: "video";
|
||||
} & Omit<
|
||||
React.ComponentPropsWithoutRef<"video">,
|
||||
keyof MediaStyleProps | "src"
|
||||
> & {
|
||||
src: string; // Required for video media
|
||||
alt?: string; // Optional for video, but recommended for accessibility
|
||||
};
|
||||
} & DesignConstrainedVideoProps;
|
||||
|
||||
/**
|
||||
* Custom element media type - allows passing any React element
|
||||
@@ -63,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;
|
||||
}
|
||||
@@ -95,6 +73,7 @@ const MediaRenderer: React.FC<{ media: HeaderHeroMedia }> = memo(
|
||||
}
|
||||
|
||||
case "video": {
|
||||
// alt here is being used as a aria label value
|
||||
const { type, alt, ...videoProps } = media;
|
||||
return (
|
||||
<div className={mediaContainerClassName}>
|
||||
@@ -127,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) {
|
||||
@@ -149,7 +132,7 @@ const HeaderHeroPrimaryMedia = forwardRef<
|
||||
|
||||
const propsToValidate = {
|
||||
subtitle,
|
||||
callsToAction,
|
||||
links,
|
||||
media,
|
||||
};
|
||||
|
||||
@@ -158,7 +141,7 @@ const HeaderHeroPrimaryMedia = forwardRef<
|
||||
console.warn(`${key} is required for HeaderHeroPrimaryMedia`);
|
||||
}
|
||||
});
|
||||
}, [subtitle, callsToAction, media]);
|
||||
}, [subtitle, links, media]);
|
||||
|
||||
return (
|
||||
<header
|
||||
@@ -183,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>
|
||||
@@ -21,7 +21,7 @@ function MyPage() {
|
||||
<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
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
// BDS HeaderHeroPrimaryMedia Pattern Styles
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
|
||||
.bds-header-hero-primary-media {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
padding-top: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
padding-bottom: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: 32px;
|
||||
padding-bottom: 32px;
|
||||
padding-top: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
padding-bottom: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: 170px;
|
||||
padding-bottom: 40px;
|
||||
padding-top: 170px; // Non-standard value, kept as-is
|
||||
padding-bottom: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
}
|
||||
|
||||
@include bds-theme-mode(light) {
|
||||
@@ -22,10 +25,10 @@
|
||||
}
|
||||
|
||||
&__headline-container {
|
||||
margin-bottom: 32px; // this margin is also default with the class - however, to avoid regressive changes we are reinforcing here
|
||||
margin-bottom: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-bottom: 0px;
|
||||
margin-bottom: $bds-space-none; // 0px - spacing('none')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,12 +59,12 @@
|
||||
&__cta-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
gap: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
justify-content: flex-end;
|
||||
min-height: 100%;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: 40px;
|
||||
gap: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
}
|
||||
|
||||
}
|
||||
@@ -74,7 +77,7 @@
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
gap: $bds-space-lg; // 16px - spacing('lg')
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row;
|
||||
@@ -83,7 +86,7 @@
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: 24px;
|
||||
gap: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
}
|
||||
|
||||
& .bds-btn--tertiary {
|
||||
@@ -101,19 +104,19 @@
|
||||
|
||||
&__media-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9; // Design req uirement: 16/9 aspect ratio
|
||||
aspect-ratio: 16 / 9; // Design requirement: 16/9 aspect ratio
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin-top: 24px;
|
||||
margin-top: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
height: auto;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-top: 32px;
|
||||
margin-top: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
aspect-ratio: 2 / 1;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-top: 40px;
|
||||
margin-top: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
aspect-ratio: 3 / 1;
|
||||
}
|
||||
}
|
||||
@@ -73,39 +73,40 @@ $bds-hero-description-light: $gray-500; // #72777E - Description text in ligh
|
||||
$bds-hero-description-dark: $gray-200; // #E6EAF0 - Description text in dark mode (neutral/200)
|
||||
|
||||
// Spacing - Desktop (≥992px)
|
||||
$bds-hero-desktop-container-py: 40px;
|
||||
$bds-hero-desktop-title-surface-pt: 16px;
|
||||
$bds-hero-desktop-title-surface-px: 16px;
|
||||
$bds-hero-desktop-title-surface-pb: 24px;
|
||||
$bds-hero-desktop-title-gap: 16px;
|
||||
$bds-hero-desktop-description-gap: 40px;
|
||||
$bds-hero-desktop-cta-gap: 24px;
|
||||
$bds-hero-desktop-content-gap: 8px;
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
$bds-hero-desktop-container-py: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
$bds-hero-desktop-title-surface-pt: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-hero-desktop-title-surface-px: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-hero-desktop-title-surface-pb: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-hero-desktop-title-gap: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-hero-desktop-description-gap: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
$bds-hero-desktop-cta-gap: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-hero-desktop-content-gap: $bds-space-sm; // 8px - spacing('sm')
|
||||
|
||||
// Spacing - Tablet (576px - 991px)
|
||||
// Base values for DEFAULT surface
|
||||
$bds-hero-tablet-container-py: 32px;
|
||||
$bds-hero-tablet-title-surface-pt: 16px;
|
||||
$bds-hero-tablet-title-surface-px: 16px;
|
||||
$bds-hero-tablet-title-surface-pb: 24px;
|
||||
$bds-hero-tablet-title-gap: 8px; // Default: 8px, Accent: 16px
|
||||
$bds-hero-tablet-description-gap: 32px; // Default: 32px, Accent: 24px
|
||||
$bds-hero-tablet-cta-gap: 16px; // Both: 16px
|
||||
$bds-hero-tablet-content-gap: 32px; // Default: 32px, Accent: 40px
|
||||
$bds-hero-tablet-container-py: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-hero-tablet-title-surface-pt: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-hero-tablet-title-surface-px: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-hero-tablet-title-surface-pb: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-hero-tablet-title-gap: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-hero-tablet-description-gap: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-hero-tablet-cta-gap: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-hero-tablet-content-gap: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
// Accent-specific tablet spacing
|
||||
$bds-hero-tablet-accent-title-gap: 16px;
|
||||
$bds-hero-tablet-accent-description-gap: 24px;
|
||||
$bds-hero-tablet-accent-content-gap: 40px;
|
||||
$bds-hero-tablet-accent-title-gap: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-hero-tablet-accent-description-gap: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-hero-tablet-accent-content-gap: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
|
||||
// Spacing - Mobile (<576px)
|
||||
$bds-hero-mobile-container-py: 24px;
|
||||
$bds-hero-mobile-title-surface-pt: 8px;
|
||||
$bds-hero-mobile-title-surface-px: 8px;
|
||||
$bds-hero-mobile-title-surface-pb: 16px;
|
||||
$bds-hero-mobile-title-gap: 8px;
|
||||
$bds-hero-mobile-description-gap: 24px;
|
||||
$bds-hero-mobile-cta-gap: 16px;
|
||||
$bds-hero-mobile-content-gap: 32px;
|
||||
$bds-hero-mobile-container-py: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-hero-mobile-title-surface-pt: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-hero-mobile-title-surface-px: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-hero-mobile-title-surface-pb: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-hero-mobile-title-gap: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-hero-mobile-description-gap: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-hero-mobile-cta-gap: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-hero-mobile-content-gap: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
|
||||
// =============================================================================
|
||||
// Base Styles
|
||||
46
shared/sections/LinkSmallGrid/LinkSmallGrid.scss
Normal file
46
shared/sections/LinkSmallGrid/LinkSmallGrid.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
// BDS LinkSmallGrid Pattern Styles
|
||||
// Brand Design System - Link grid section pattern with heading and responsive tile grid
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-link-small-grid - Base section container
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
|
||||
// Spacing tokens
|
||||
$bds-link-small-grid-spacing-base: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-link-small-grid-spacing-md: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-link-small-grid-spacing-lg: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
|
||||
// Typography tokens (using existing typography classes)
|
||||
// - Heading: h-md class (handled in component)
|
||||
// - Description: body-l class (handled in component)
|
||||
|
||||
// =============================================================================
|
||||
// Base Section Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-link-small-grid {
|
||||
// Section spacing
|
||||
padding-top: $bds-link-small-grid-spacing-base;
|
||||
padding-bottom: $bds-link-small-grid-spacing-base;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: $bds-link-small-grid-spacing-md;
|
||||
padding-bottom: $bds-link-small-grid-spacing-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: $bds-link-small-grid-spacing-lg;
|
||||
padding-bottom: $bds-link-small-grid-spacing-lg;
|
||||
}
|
||||
|
||||
// Background color - default to transparent
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
// Header section uses SectionHeader component
|
||||
|
||||
|
||||
121
shared/sections/LinkSmallGrid/LinkSmallGrid.tsx
Normal file
121
shared/sections/LinkSmallGrid/LinkSmallGrid.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
|
||||
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'> {}
|
||||
|
||||
export interface LinkSmallGridProps {
|
||||
/** Color variant - determines tile background color */
|
||||
variant?: 'gray' | 'lilac';
|
||||
/** Heading text (required) */
|
||||
heading: string;
|
||||
/** Optional description text */
|
||||
description?: string;
|
||||
/** Array of link items to display in the grid */
|
||||
links: LinkItem[];
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* LinkSmallGrid Component
|
||||
*
|
||||
* A responsive grid section pattern for displaying navigational links using TileLink components.
|
||||
* Features a heading, optional description, and a grid of clickable tiles with 2 color variants
|
||||
* and full light/dark mode support.
|
||||
*
|
||||
* Grid Layout (12-column grid system):
|
||||
* - Base (< 576px): 1 tile per row (each tile spans 4 of 4 columns = full width)
|
||||
* - MD (576px - 991px): 2 tiles per row (each tile spans 4 of 8 columns = 50% width)
|
||||
* - LG (≥ 992px): 4 tiles per row (each tile spans 3 of 12 columns = 25% width)
|
||||
*
|
||||
* Right-Alignment Logic (applied when < 10 total tiles):
|
||||
* The first tile of each row gets an offset to right-align the grid at LG breakpoint only:
|
||||
* - LG: 1 tile = offset 9, 2 tiles = offset 6, 3 tiles = offset 3, 4 tiles = offset 0
|
||||
* - 10+ tiles: no offset (left-aligned grid)
|
||||
* - MD and Base: no offset applied
|
||||
*
|
||||
* Each tile uses the TileLink component which features:
|
||||
* - Window shade hover animation
|
||||
* - Arrow icon with animation
|
||||
* - Responsive sizing (64px height at all breakpoints)
|
||||
* - Support for both links (href) and buttons (onClick)
|
||||
* - Gray and Lilac color variants
|
||||
*
|
||||
* @example
|
||||
* // Basic usage with gray variant
|
||||
* <LinkSmallGrid
|
||||
* variant="gray"
|
||||
* heading="Quick Links"
|
||||
* description="Navigate to key sections"
|
||||
* links={[
|
||||
* { label: "Documentation", href: "/docs" },
|
||||
* { label: "Tutorials", href: "/tutorials" }
|
||||
* ]}
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Lilac variant with click handlers
|
||||
* <LinkSmallGrid
|
||||
* variant="lilac"
|
||||
* heading="Get Started"
|
||||
* links={[
|
||||
* { label: "Quick Start", onClick: () => navigate('/start') },
|
||||
* { label: "Examples", href: "/examples" }
|
||||
* ]}
|
||||
* />
|
||||
*/
|
||||
export const LinkSmallGrid: React.FC<LinkSmallGridProps> = ({
|
||||
variant = 'gray',
|
||||
heading,
|
||||
description,
|
||||
links,
|
||||
className,
|
||||
}) => {
|
||||
// Build class names using BEM with bds namespace
|
||||
const classNames = clsx(
|
||||
'bds-link-small-grid',
|
||||
`bds-link-small-grid--${variant}`,
|
||||
className
|
||||
);
|
||||
|
||||
// Memoize offset calculations - only recalculate when links array changes
|
||||
const linkOffsets = useMemo(() => {
|
||||
const total = links.length;
|
||||
return links.map((_, index) => calculateTileOffset(index, total));
|
||||
}, [links]);
|
||||
|
||||
return (
|
||||
<section className={classNames}>
|
||||
<PageGrid>
|
||||
<SectionHeader heading={heading} description={description} span={{ base: 4, md: 6, lg: 8 }} />
|
||||
<PageGridRow>
|
||||
{links.map((link, index) => {
|
||||
const offset = linkOffsets[index];
|
||||
const hasOffset = offset.lg > 0;
|
||||
// Use href or label as key, fallback to index
|
||||
const key = link.href || link.label || index;
|
||||
return (
|
||||
<PageGridCol
|
||||
key={key}
|
||||
span={{ base: 4, md: 4, lg: 3 }}
|
||||
offset={hasOffset ? { lg: offset.lg } : undefined}
|
||||
>
|
||||
<TileLink
|
||||
variant={variant}
|
||||
{...link}
|
||||
/>
|
||||
</PageGridCol>
|
||||
);
|
||||
})}
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkSmallGrid;
|
||||
|
||||
154
shared/sections/LinkSmallGrid/README.md
Normal file
154
shared/sections/LinkSmallGrid/README.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# LinkSmallGrid Component
|
||||
|
||||
A responsive grid section pattern for displaying navigational links using TileLink components.
|
||||
|
||||
## Overview
|
||||
|
||||
LinkSmallGrid is a pattern component that combines a heading, optional description, and a grid of TileLink components. It provides a consistent layout for presenting multiple navigation options or quick links.
|
||||
|
||||
## Features
|
||||
|
||||
- **Responsive Grid Layout**: Adapts from 1 column (mobile) to 4 columns (desktop)
|
||||
- **Two Color Variants**: Gray and Lilac (applied to all tiles)
|
||||
- **Light/Dark Mode**: Full theming support
|
||||
- **Right-Alignment**: Automatically right-aligns grids with fewer than 10 tiles at desktop
|
||||
- **Flexible Content**: Supports both links and click handlers
|
||||
- **Accessible**: Semantic HTML with proper heading hierarchy
|
||||
|
||||
## Grid Layout
|
||||
|
||||
Based on a 12-column grid system:
|
||||
|
||||
| Breakpoint | Tiles per Row | Tile Span | Total Columns |
|
||||
|------------|---------------|-----------|---------------|
|
||||
| Base (< 576px) | 1 | 4 of 4 | Full width |
|
||||
| MD (576px - 991px) | 2 | 4 of 8 | 50% width each |
|
||||
| LG (≥ 992px) | 4 | 3 of 12 | 25% width each |
|
||||
|
||||
## Right-Alignment Logic
|
||||
|
||||
When there are **fewer than 10 total tiles**, the grid is right-aligned at the **LG breakpoint only**:
|
||||
|
||||
- **1 tile**: offset 9 columns
|
||||
- **2 tiles**: offset 6 columns
|
||||
- **3 tiles**: offset 3 columns
|
||||
- **4+ tiles**: no offset (fills row)
|
||||
- **10+ tiles**: no offset (left-aligned grid)
|
||||
|
||||
**Note**: MD and Base breakpoints never apply offset (always left-aligned).
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage (Gray Variant)
|
||||
```tsx
|
||||
<LinkSmallGrid
|
||||
variant="gray"
|
||||
heading="Quick Links"
|
||||
description="Navigate to key sections"
|
||||
links={[
|
||||
{ label: "Documentation", href: "/docs" },
|
||||
{ label: "Tutorials", href: "/tutorials" },
|
||||
{ label: "API Reference", href: "/api" },
|
||||
{ label: "Examples", href: "/examples" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### Lilac Variant with Click Handlers
|
||||
```tsx
|
||||
<LinkSmallGrid
|
||||
variant="lilac"
|
||||
heading="Get Started"
|
||||
links={[
|
||||
{ label: "Quick Start", onClick: () => navigate('/start') },
|
||||
{ label: "Examples", href: "/examples" },
|
||||
{ label: "Templates", href: "/templates" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### Without Description
|
||||
```tsx
|
||||
<LinkSmallGrid
|
||||
variant="gray"
|
||||
heading="Resources"
|
||||
links={[
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{ label: "Community", href: "/community" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
### LinkSmallGridProps
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `variant` | `'gray' \| 'lilac'` | `'gray'` | Color variant for all tiles |
|
||||
| `heading` | `string` | Required | Section heading |
|
||||
| `description` | `string` | - | Optional description text |
|
||||
| `links` | `LinkItem[]` | Required | Array of link items |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
|
||||
### LinkItem (extends TileLinkProps)
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `label` | `string` | Required | Link text/label |
|
||||
| `href` | `string` | - | Link destination |
|
||||
| `onClick` | `() => void` | - | Click handler |
|
||||
| `disabled` | `boolean` | `false` | Disabled state |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
|
||||
**Note**: `variant` is controlled by the parent LinkSmallGrid component.
|
||||
|
||||
## Layout Structure
|
||||
|
||||
```
|
||||
<section className="bds-link-small-grid">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 6, lg: 8 }}>
|
||||
<header>
|
||||
<h2>{heading}</h2>
|
||||
<p>{description}</p>
|
||||
</header>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
{links.map(link => (
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }} offset={...}>
|
||||
<TileLink {...link} />
|
||||
</PageGridCol>
|
||||
))}
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</section>
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
- **Memoized Offset Calculations**: Uses `useMemo` to avoid recalculating offsets on every render
|
||||
- **Optimized Keys**: Uses `href` or `label` as React keys instead of array index for better reconciliation
|
||||
|
||||
## Files
|
||||
|
||||
- `LinkSmallGrid.tsx` - React component
|
||||
- `LinkSmallGrid.scss` - Styles with BEM naming convention
|
||||
- `README.md` - This file
|
||||
|
||||
## Related Components
|
||||
|
||||
- **TileLink**: Atomic component used for each tile in the grid
|
||||
- **PageGrid/PageGridRow/PageGridCol**: Grid system components
|
||||
- **calculateTileOffset**: Utility function for offset calculations (in `shared/utils/helpers.ts`)
|
||||
|
||||
## Design System
|
||||
|
||||
Part of the Brand Design System (BDS) with `bds-` namespace prefix.
|
||||
|
||||
## Showcase
|
||||
|
||||
See `about/link-small-grid-showcase.page.tsx` for examples with different link counts and variants.
|
||||
|
||||
2
shared/sections/LinkSmallGrid/index.ts
Normal file
2
shared/sections/LinkSmallGrid/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { LinkSmallGrid, type LinkSmallGridProps, type LinkItem } from './LinkSmallGrid';
|
||||
|
||||
43
shared/sections/LinkTextDirectory/LinkTextDirectory.scss
Normal file
43
shared/sections/LinkTextDirectory/LinkTextDirectory.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
// BDS LinkTextDirectory Pattern Styles
|
||||
// Brand Design System - Section with heading and list of numbered cards
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-link-text-directory - Base section container
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
|
||||
// Section padding
|
||||
$bds-link-text-directory-padding-base: 24px;
|
||||
$bds-link-text-directory-padding-md: 32px;
|
||||
$bds-link-text-directory-padding-lg: 40px;
|
||||
|
||||
// =============================================================================
|
||||
// Section Container
|
||||
// =============================================================================
|
||||
|
||||
.bds-link-text-directory {
|
||||
padding-top: $bds-link-text-directory-padding-base;
|
||||
padding-bottom: $bds-link-text-directory-padding-base;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: $bds-link-text-directory-padding-md;
|
||||
padding-bottom: $bds-link-text-directory-padding-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: $bds-link-text-directory-padding-lg;
|
||||
padding-bottom: $bds-link-text-directory-padding-lg;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
@include bds-theme-mode(dark) {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
// Header section uses SectionHeader component
|
||||
101
shared/sections/LinkTextDirectory/LinkTextDirectory.tsx
Normal file
101
shared/sections/LinkTextDirectory/LinkTextDirectory.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
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 */
|
||||
heading: string;
|
||||
/** Description text for the card */
|
||||
description: string;
|
||||
/** Array of button configurations (max 2) */
|
||||
buttons: ButtonConfig[];
|
||||
}
|
||||
|
||||
export interface LinkTextDirectoryProps {
|
||||
/** Section heading (required) */
|
||||
heading: string;
|
||||
/** Optional description text */
|
||||
description?: string;
|
||||
/** Array of card data to display */
|
||||
cards: LinkTextCardData[];
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* LinkTextDirectory Component
|
||||
*
|
||||
* A section pattern that displays a numbered list of LinkTextCard components.
|
||||
* Features a heading, optional description, and a vertically stacked list of cards
|
||||
* with automatic sequential numbering (01, 02, 03...).
|
||||
*
|
||||
* Layout:
|
||||
* - Header section with heading + description
|
||||
* - Responsive vertical spacing between cards
|
||||
* - Desktop: Right-aligned cards with 40px gaps
|
||||
* - Tablet: Left-aligned cards with 32px gaps
|
||||
* - Mobile: Left-aligned cards with 24px gaps
|
||||
*
|
||||
* @example
|
||||
* // Basic usage
|
||||
* <LinkTextDirectory
|
||||
* heading="Explore XRPL Developer Tools"
|
||||
* description="XRP Ledger is a compliance-focused blockchain where financial applications come to life"
|
||||
* cards={[
|
||||
* {
|
||||
* heading: "Fast Settlement and Low Fees",
|
||||
* description: "Settle transactions in 3-5 seconds for a fraction of a cent",
|
||||
* buttons: [
|
||||
* { label: "Get Started", href: "/start" },
|
||||
* { label: "Learn More", href: "/docs" }
|
||||
* ]
|
||||
* },
|
||||
* {
|
||||
* heading: "Secure and Reliable",
|
||||
* description: "Built on proven blockchain technology",
|
||||
* buttons: [{ label: "Read Docs", href: "/docs" }]
|
||||
* }
|
||||
* ]}
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Without description
|
||||
* <LinkTextDirectory
|
||||
* heading="Features"
|
||||
* cards={cardData}
|
||||
* />
|
||||
*/
|
||||
export const LinkTextDirectory: React.FC<LinkTextDirectoryProps> = ({
|
||||
heading,
|
||||
description,
|
||||
cards,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<PageGrid className={clsx('bds-link-text-directory', className)}>
|
||||
<SectionHeader heading={heading} description={description} span={{ base: 12, md: 6, lg: 8 }} />
|
||||
|
||||
{/* Cards List */}
|
||||
<PageGridRow className="bds-link-text-directory__list">
|
||||
<PageGridCol span={{ base: 12, md: 8, lg: 8}} offset={{ lg: 4 }}>
|
||||
<ul className="list-none pl-0">
|
||||
{cards.map((card, index) => (
|
||||
<LinkTextCard
|
||||
key={card.heading || index}
|
||||
index={index}
|
||||
heading={card.heading}
|
||||
description={card.description}
|
||||
buttons={card.buttons}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</PageGridCol>
|
||||
</PageGridRow >
|
||||
</PageGrid>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkTextDirectory;
|
||||
307
shared/sections/LinkTextDirectory/README.md
Normal file
307
shared/sections/LinkTextDirectory/README.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# LinkTextDirectory Component
|
||||
|
||||
A section pattern that displays a numbered list of LinkTextCard components with a heading and optional description.
|
||||
|
||||
## Overview
|
||||
|
||||
LinkTextDirectory is a section-level pattern that combines a header (heading + description) with a vertically stacked list of LinkTextCard components. Each card is automatically numbered sequentially (01, 02, 03...), making it perfect for feature lists, step-by-step guides, or numbered content sections.
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic Numbering**: Cards are numbered sequentially starting from 01
|
||||
- **Responsive Layout**: Adaptive spacing and alignment across breakpoints
|
||||
- **Desktop Right-Alignment**: Cards are right-aligned on desktop for visual interest
|
||||
- **Minimal HTML Structure**: Flat, efficient DOM hierarchy
|
||||
- **Light/Dark Mode**: Full theming support
|
||||
- **Flexible Content**: Supports any number of cards
|
||||
|
||||
## Layout Behavior
|
||||
|
||||
| Breakpoint | Card Alignment | Gap Between Cards | Header Gap |
|
||||
|------------|----------------|-------------------|------------|
|
||||
| Base (< 576px) | Left | 24px | 8px |
|
||||
| MD (576px - 991px) | Left | 32px | 8px |
|
||||
| LG (≥ 992px) | Right | 40px | 16px |
|
||||
|
||||
**Desktop Behavior**: Cards are right-aligned using `align-items: flex-end`, creating a visually distinct layout compared to mobile/tablet.
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```tsx
|
||||
<LinkTextDirectory
|
||||
heading="Explore XRPL Developer Tools"
|
||||
description="XRP Ledger is a compliance-focused blockchain where financial applications come to life"
|
||||
cards={[
|
||||
{
|
||||
heading: "Fast Settlement and Low Fees",
|
||||
description: "Settle transactions in 3-5 seconds for a fraction of a cent, ideal for large-scale, high-volume RWA tokenization",
|
||||
buttons: [
|
||||
{ label: "Get Started", href: "/start" },
|
||||
{ label: "Learn More", href: "/docs" }
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: "Secure and Reliable",
|
||||
description: "Built on proven blockchain technology with enterprise-grade security",
|
||||
buttons: [
|
||||
{ label: "Read Documentation", href: "/docs" }
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: "Developer Friendly",
|
||||
description: "Comprehensive APIs and SDKs for seamless integration",
|
||||
buttons: [
|
||||
{ label: "View API", href: "/api" },
|
||||
{ label: "See Examples", href: "/examples" }
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### Without Description
|
||||
|
||||
```tsx
|
||||
<LinkTextDirectory
|
||||
heading="Key Features"
|
||||
cards={featuresList}
|
||||
/>
|
||||
```
|
||||
|
||||
### With Dynamic Data
|
||||
|
||||
```tsx
|
||||
const features = [
|
||||
{
|
||||
heading: "Feature One",
|
||||
description: "Description for feature one",
|
||||
buttons: [{ label: "Learn More", href: "/feature-1" }]
|
||||
},
|
||||
// ... more features
|
||||
];
|
||||
|
||||
<LinkTextDirectory
|
||||
heading="Platform Features"
|
||||
description="Everything you need to build amazing applications"
|
||||
cards={features}
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
### LinkTextDirectoryProps
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `heading` | `string` | Required | Section heading |
|
||||
| `description` | `string` | - | Optional description text |
|
||||
| `cards` | `LinkTextCardData[]` | Required | Array of card data |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
|
||||
### LinkTextCardData
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `heading` | `string` | Required | Card heading |
|
||||
| `description` | `string` | Required | Card description |
|
||||
| `buttons` | `ButtonConfig[]` | Required | Array of button configs (max 2) |
|
||||
|
||||
### ButtonConfig (from ButtonGroup)
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `label` | `string` | Required | Button text |
|
||||
| `href` | `string` | - | Link destination |
|
||||
| `onClick` | `() => void` | - | Click handler |
|
||||
|
||||
## Component Structure
|
||||
|
||||
```tsx
|
||||
<PageGrid className="bds-link-text-directory">
|
||||
<PageGridRow>
|
||||
<PageGridCol className="bds-link-text-directory__header" span={{ base: 12, md: 6, lg: 8 }}>
|
||||
<h2 className="h-md">{heading}</h2>
|
||||
<p className="body-l">{description}</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 12, md: 8, lg: 8 }} offset={{ lg: 4 }}>
|
||||
<ul>
|
||||
{cards.map((card, index) => (
|
||||
<LinkTextCard
|
||||
key={index}
|
||||
index={index}
|
||||
heading={card.heading}
|
||||
description={card.description}
|
||||
buttons={card.buttons}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
```
|
||||
|
||||
**Key Design Decisions:**
|
||||
- **PageGrid Integration**: Uses PageGrid system for responsive layout
|
||||
- **Typography Classes**: Uses existing `h-md` and `body-l` utility classes
|
||||
- **Flexbox Header**: Header uses flexbox with gap for spacing between heading and description
|
||||
- **Desktop Right-Alignment**: Cards offset by 4 columns at LG breakpoint (right-aligned)
|
||||
- **Semantic List**: Cards wrapped in `<ul>` with each card as `<li>`
|
||||
|
||||
## Responsive Spacing
|
||||
|
||||
| Breakpoint | Section Padding | Header Gap | Header Margin-Bottom |
|
||||
|------------|-----------------|------------|----------------------|
|
||||
| Base (< 576px) | 24px | 8px | 24px |
|
||||
| MD (576px - 991px) | 32px | 8px | 32px |
|
||||
| LG (≥ 992px) | 40px | 16px | 40px |
|
||||
|
||||
**Section Padding**: Top and bottom padding on the entire section
|
||||
**Header Gap**: Space between heading and description (via flexbox gap)
|
||||
**Header Margin-Bottom**: Space between header and cards list
|
||||
|
||||
## Styling
|
||||
|
||||
### CSS Classes
|
||||
|
||||
- `.bds-link-text-directory` - Main PageGrid container with section padding
|
||||
- `.bds-link-text-directory__header` - Header section (flexbox column with gap)
|
||||
|
||||
### Typography
|
||||
|
||||
- **Heading**: `h-md` class (responsive heading)
|
||||
- **Description**: `body-l` class (large body text)
|
||||
- Card content uses LinkTextCard's built-in typography
|
||||
|
||||
### Grid Layout
|
||||
|
||||
- **Header Column**: `span={{ base: 12, md: 6, lg: 8 }}`
|
||||
- **Cards Column**: `span={{ base: 12, md: 8, lg: 8 }}` with `offset={{ lg: 4 }}`
|
||||
- Cards are right-aligned on desktop via the 4-column offset
|
||||
|
||||
### Dark Mode
|
||||
|
||||
- Text color changes to white in dark mode
|
||||
- Applied to entire section via `bds-theme-mode(dark)` mixin
|
||||
|
||||
## Card Numbering
|
||||
|
||||
Cards are automatically numbered based on their array index:
|
||||
|
||||
```typescript
|
||||
cards[0] → LinkTextCard(index: 0) → displays "01"
|
||||
cards[1] → LinkTextCard(index: 1) → displays "02"
|
||||
cards[2] → LinkTextCard(index: 2) → displays "03"
|
||||
// ... and so on
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- `LinkTextDirectory.tsx` - React component
|
||||
- `LinkTextDirectory.scss` - SCSS styles
|
||||
- `index.ts` - Barrel exports
|
||||
- `README.md` - This file
|
||||
|
||||
## Related Components
|
||||
|
||||
- **LinkTextCard**: Used for each card in the list
|
||||
- **ButtonGroup**: Used by LinkTextCard for action buttons
|
||||
|
||||
## Import
|
||||
|
||||
```tsx
|
||||
import { LinkTextDirectory } from 'shared/sections/LinkTextDirectory';
|
||||
// or
|
||||
import {
|
||||
LinkTextDirectory,
|
||||
type LinkTextDirectoryProps,
|
||||
type LinkTextCardData
|
||||
} from 'shared/sections/LinkTextDirectory';
|
||||
```
|
||||
|
||||
## Design System
|
||||
|
||||
Part of the Brand Design System (BDS) with `bds-` namespace prefix.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Consistent Card Content**: Try to keep similar text lengths across cards for visual balance
|
||||
2. **Limit Cards**: 3-6 cards works best for readability
|
||||
3. **Clear Descriptions**: Keep descriptions concise but informative
|
||||
4. **Button Labels**: Use clear, action-oriented button labels
|
||||
5. **Logical Ordering**: Order cards by priority or logical sequence
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Semantic HTML with proper heading hierarchy (`<h2>` for section, `<h5>` for cards)
|
||||
- Semantic list structure: `<ul>` containing `<li>` elements
|
||||
- Sequential tab order through cards and buttons
|
||||
- ARIA-compliant button and link elements (via ButtonGroup)
|
||||
- Maintains focus order: heading → description → card 1 → card 2 → etc.
|
||||
|
||||
## Best Practices for React Keys
|
||||
|
||||
When mapping over cards, use a stable identifier instead of array index:
|
||||
|
||||
```tsx
|
||||
// ❌ Avoid using index as key
|
||||
{cards.map((card, index) => (
|
||||
<LinkTextCard key={index} ... />
|
||||
))}
|
||||
|
||||
// ✅ Better: Use a unique identifier
|
||||
{cards.map((card, index) => (
|
||||
<LinkTextCard key={card.id || card.heading} ... />
|
||||
))}
|
||||
|
||||
// ✅ Best: Add an id field to LinkTextCardData
|
||||
interface LinkTextCardData {
|
||||
id: string; // Unique identifier
|
||||
heading: string;
|
||||
description: string;
|
||||
buttons: ButtonConfig[];
|
||||
}
|
||||
|
||||
{cards.map((card) => (
|
||||
<LinkTextCard key={card.id} ... />
|
||||
))}
|
||||
```
|
||||
|
||||
## Example with All Features
|
||||
|
||||
```tsx
|
||||
<LinkTextDirectory
|
||||
heading="Why Choose XRPL"
|
||||
description="The most efficient blockchain for real-world applications"
|
||||
cards={[
|
||||
{
|
||||
heading: "Lightning Fast",
|
||||
description: "Process thousands of transactions per second with sub-3-second finality",
|
||||
buttons: [
|
||||
{ label: "View Benchmarks", href: "/performance" },
|
||||
{ label: "Try Demo", onClick: () => openDemo() }
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: "Cost Effective",
|
||||
description: "Minimal transaction fees make XRPL perfect for micro-transactions and high-volume use cases",
|
||||
buttons: [
|
||||
{ label: "See Pricing", href: "/pricing" }
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: "Battle Tested",
|
||||
description: "Over 10 years of continuous operation with billions of transactions processed",
|
||||
buttons: [
|
||||
{ label: "Read Case Studies", href: "/case-studies" },
|
||||
{ label: "View Stats", href: "/statistics" }
|
||||
]
|
||||
}
|
||||
]}
|
||||
className="my-custom-class"
|
||||
/>
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user