mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2026-07-30 10:30:16 +00:00
Component Library Refactor & New Components (#3510)
* adding showcase page * adding CardStatsList * clean up, tighter code * code review and code clean up * update import, clean up env for error message * tweak some css code * less css, rebuilt * re-adding bem, modifier for bds variants
This commit is contained in:
committed by
Calvin Jhunjhuwala
parent
fe1aa0ecb1
commit
176fb3cbc7
@@ -2,6 +2,7 @@ navbar.about: Acerca de
|
||||
navbar.docs: Docs
|
||||
navbar.resources: Recursos
|
||||
navbar.community: Comunidad
|
||||
navbar.showcase: Escaparate
|
||||
footer.about: Acerca de
|
||||
footer.docs: Docs
|
||||
footer.resources: Recursos
|
||||
|
||||
@@ -16,6 +16,7 @@ navbar.about: 概要
|
||||
navbar.docs: ドキュメント
|
||||
navbar.resources: リソース
|
||||
navbar.community: コミュニティ
|
||||
navbar.showcase: ショーケース
|
||||
footer.about: 概要
|
||||
footer.docs: ドキュメント
|
||||
footer.resources: リソース
|
||||
|
||||
@@ -14,6 +14,7 @@ export const navItems: NavItem[] = [
|
||||
{ label: "Use Cases", labelTranslationKey: "navbar.usecases", href: "/use-cases", hasSubmenu: true },
|
||||
{ label: "Community", labelTranslationKey: "navbar.community", href: "/community", hasSubmenu: true },
|
||||
{ label: "Network", labelTranslationKey: "navbar.network", href: "/resources", hasSubmenu: true },
|
||||
{ label: "Showcase (Temporary)", labelTranslationKey: "navbar.showcase", href: "/showcase", hasSubmenu: false },
|
||||
];
|
||||
|
||||
// Develop submenu data structure
|
||||
|
||||
@@ -19,13 +19,14 @@ module.exports = {
|
||||
...(isProduction || process.env.PURGECSS === 'true'
|
||||
? [
|
||||
purgecss({
|
||||
// Scan all content files for class names
|
||||
// Scan all content files for class names (TSX/TS/HTML = usage; SCSS = BEM modifier definitions)
|
||||
content: [
|
||||
'./**/*.tsx',
|
||||
'./**/*.ts',
|
||||
'./**/*.md',
|
||||
'./**/*.yaml',
|
||||
'./**/*.html',
|
||||
'./**/*.scss',
|
||||
'./static/js/**/*.js',
|
||||
'./static/vendor/**/*.js',
|
||||
// Ignore node_modules except for specific libraries that inject classes
|
||||
@@ -42,7 +43,11 @@ module.exports = {
|
||||
const m = match.match(/["']([^"']*)["']/);
|
||||
return m ? m[1].split(/\s+/) : [];
|
||||
});
|
||||
return [...broadMatches, ...classes];
|
||||
// Extract BEM modifier classes from SCSS (e.g. .bds-callout-media-banner--green)
|
||||
// These are dynamically applied in TSX via template literals so PurgeCSS won't find them as literals
|
||||
const scssBemMatches = content.match(/\.(bds-[a-z0-9-]+--[a-z0-9-]+)/g) || [];
|
||||
const bemModifiers = [...new Set(scssBemMatches.map(m => m.replace(/^\./, '')))];
|
||||
return [...new Set([...broadMatches, ...classes, ...bemModifiers])];
|
||||
},
|
||||
|
||||
// Safelist - classes that should never be removed
|
||||
@@ -68,6 +73,9 @@ module.exports = {
|
||||
/^col-/, // Column classes
|
||||
/^bds-grid__col/, // PageGrid column classes (dynamic span values)
|
||||
/^bds-grid__offset/, // PageGrid offset classes
|
||||
// BDS BEM modifier classes - applied dynamically via template literals (e.g. bds-callout-media-banner--green)
|
||||
// Required: PurgeCSS cannot find these in TSX/SCSS due to interpolation
|
||||
/^bds-[a-z0-9-]+--/,
|
||||
/^g-/, // Gap utilities
|
||||
/^p-/, // Padding utilities
|
||||
/^m-/, // Margin utilities
|
||||
|
||||
@@ -44,7 +44,8 @@ $bds-card-stat-transition-timing: $bds-transition-timing;
|
||||
|
||||
.bds-card-stat {
|
||||
// Layout
|
||||
display: flex;
|
||||
// !important needed to override PageGridCol's default display (e.g. grid/flex from parent)
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
|
||||
@@ -87,43 +87,41 @@ export const CardStat: React.FC<CardStatProps> = ({
|
||||
const isNumericSuperscript = superscript && /^[0-9]+$/.test(superscript);
|
||||
|
||||
return (
|
||||
<PageGridCol span={span}>
|
||||
<div className={classNames}>
|
||||
{/* Text section */}
|
||||
<div className="bds-card-stat__text">
|
||||
<div className="bds-card-stat__statistic">
|
||||
{statistic}{superscript && <sup className={isNumericSuperscript ? 'bds-card-stat__superscript--numeric' : ''}>{superscript}</sup>}</div>
|
||||
<div className="body-r">{label}</div>
|
||||
</div>
|
||||
|
||||
{/* Buttons section */}
|
||||
{hasButtons && (
|
||||
<div className="bds-card-stat__buttons">
|
||||
{primaryButton && (
|
||||
<Button
|
||||
// forceColor - prop to be added once feature two column is merged
|
||||
variant="primary"
|
||||
color="black"
|
||||
href={primaryButton.href}
|
||||
onClick={primaryButton.onClick}
|
||||
>
|
||||
{primaryButton.label}
|
||||
</Button>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<Button
|
||||
// forceColor - prop to be added once feature two column is merged
|
||||
variant="secondary"
|
||||
color="black"
|
||||
href={secondaryButton.href}
|
||||
onClick={secondaryButton.onClick}
|
||||
>
|
||||
{secondaryButton.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<PageGridCol span={span} as="li" className={classNames}>
|
||||
{/* Text section */}
|
||||
<div className="bds-card-stat__text">
|
||||
<div className="bds-card-stat__statistic">
|
||||
{statistic}{superscript && <sup className={isNumericSuperscript ? 'bds-card-stat__superscript--numeric' : ''}>{superscript}</sup>}</div>
|
||||
<div className="body-r">{label}</div>
|
||||
</div>
|
||||
|
||||
{/* Buttons section */}
|
||||
{hasButtons && (
|
||||
<div className="bds-card-stat__buttons">
|
||||
{primaryButton && (
|
||||
<Button
|
||||
forceColor
|
||||
variant="primary"
|
||||
color="black"
|
||||
href={primaryButton.href}
|
||||
onClick={primaryButton.onClick}
|
||||
>
|
||||
{primaryButton.label}
|
||||
</Button>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<Button
|
||||
forceColor
|
||||
variant="secondary"
|
||||
color="black"
|
||||
href={secondaryButton.href}
|
||||
onClick={secondaryButton.onClick}
|
||||
>
|
||||
{secondaryButton.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PageGridCol>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -92,9 +92,9 @@ CardTextIconCard displays an icon at the top, followed by a heading and descript
|
||||
|
||||
| Breakpoint | Icon Size | Padding | Gap |
|
||||
|------------|-----------|---------|-----|
|
||||
| Base (< 576px) | 56px | 16px | 16px |
|
||||
| MD (576px - 991px) | 60px | 20px | 20px |
|
||||
| LG (≥ 992px) | 64px | 24px | 24px |
|
||||
| Base (< 576px) | 32px | 16px | 16px |
|
||||
| MD (576px - 991px) | 36px | 20px | 20px |
|
||||
| LG (≥ 992px) | 40px | 32px | 24px |
|
||||
|
||||
## Files
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
278
shared/components/Video/Video.tsx
Normal file
278
shared/components/Video/Video.tsx
Normal file
@@ -0,0 +1,278 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import type { DesignConstrainedVideoProps } from 'shared/utils/types';
|
||||
|
||||
/** Native HTML video source */
|
||||
export type VideoSourceNative = {
|
||||
type: 'native';
|
||||
props: DesignConstrainedVideoProps;
|
||||
};
|
||||
|
||||
/**
|
||||
* Embed source - raw HTML iframe code from YouTube/Vimeo/Wistia
|
||||
*
|
||||
* ⚠️ **SECURITY WARNING**: embedCode uses regex-based parsing which can be bypassed
|
||||
* with malformed HTML (e.g., newlines in attributes, missing quotes, unusual spacing).
|
||||
* **Always prefer `embedUrl` over `embedCode` when possible.**
|
||||
*
|
||||
* Only use embedCode when:
|
||||
* - The HTML comes from a trusted source you control
|
||||
* - You cannot extract the URL beforehand
|
||||
*
|
||||
* The component validates against TRUSTED_EMBED_ORIGINS, but regex parsing is not
|
||||
* foolproof against all HTML variations.
|
||||
*/
|
||||
export type VideoSourceEmbedCode = {
|
||||
type: 'embed';
|
||||
embedCode: string;
|
||||
embedUrl?: never;
|
||||
};
|
||||
|
||||
/**
|
||||
* Embed source - direct embed URL (safer, preferred)
|
||||
*
|
||||
* ✅ **RECOMMENDED**: This is the preferred and safer method for embedding videos.
|
||||
* Directly validates the URL against TRUSTED_EMBED_ORIGINS without HTML parsing.
|
||||
*/
|
||||
export type VideoSourceEmbedUrl = {
|
||||
type: 'embed';
|
||||
embedUrl: string;
|
||||
embedCode?: never;
|
||||
};
|
||||
|
||||
export type VideoSource =
|
||||
| VideoSourceNative
|
||||
| VideoSourceEmbedCode
|
||||
| VideoSourceEmbedUrl;
|
||||
|
||||
export interface VideoProps {
|
||||
/**
|
||||
* Video source: native HTML video or embed (YouTube/Vimeo/Wistia)
|
||||
*
|
||||
* **For embeds, prefer `embedUrl` over `embedCode`:**
|
||||
* - ✅ `embedUrl`: Direct URL validation (safer, recommended)
|
||||
* - ⚠️ `embedCode`: Regex-based HTML parsing (can be bypassed, use with caution)
|
||||
*/
|
||||
source: VideoSource;
|
||||
/** Optional cover image - when provided, video shows in modal on click */
|
||||
coverImage?: {
|
||||
src: string;
|
||||
alt: string;
|
||||
};
|
||||
/** Aspect ratio for container (default 16/9) */
|
||||
aspectRatio?: '16/9' | '4/3' | '1/1';
|
||||
/** Additional className for container */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** Trusted embed origins for embedCode sanitization */
|
||||
const TRUSTED_EMBED_ORIGINS = [
|
||||
'youtube.com',
|
||||
'www.youtube.com',
|
||||
'youtube-nocookie.com',
|
||||
'www.youtube-nocookie.com',
|
||||
'vimeo.com',
|
||||
'player.vimeo.com',
|
||||
'fast.wistia.net',
|
||||
'fast.wistia.com',
|
||||
];
|
||||
|
||||
function isTrustedEmbedUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const host = parsed.hostname.toLowerCase();
|
||||
return TRUSTED_EMBED_ORIGINS.some(
|
||||
(origin) => host === origin || host.endsWith('.' + origin)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract iframe src from embed code if from trusted origin
|
||||
*
|
||||
* ⚠️ **SECURITY WARNING**: This regex-based parser is NOT foolproof and can be bypassed
|
||||
* with malformed HTML. Known bypass vectors include:
|
||||
* - Newlines or unusual whitespace in attributes
|
||||
* - Missing or unusual quote characters
|
||||
* - HTML comments or CDATA sections
|
||||
* - Encoded characters or HTML entities
|
||||
*
|
||||
* This function should only be used as a fallback when embedUrl is not available.
|
||||
* Always prefer using embedUrl directly when possible.
|
||||
*
|
||||
* @param embedCode - Raw HTML iframe embed code (from trusted sources only)
|
||||
* @returns Extracted URL if valid and from trusted origin, null otherwise
|
||||
*/
|
||||
function extractEmbedUrlFromCode(embedCode: string): string | null {
|
||||
const iframeMatch = embedCode.match(/<iframe[^>]+src=["']([^"']+)["']/i);
|
||||
if (!iframeMatch) return null;
|
||||
const url = iframeMatch[1];
|
||||
return isTrustedEmbedUrl(url) ? url : null;
|
||||
}
|
||||
|
||||
export const Video = React.forwardRef<HTMLDivElement, VideoProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
source,
|
||||
coverImage,
|
||||
aspectRatio = '16/9',
|
||||
className,
|
||||
} = props;
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const previousActiveElementRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
setIsModalOpen(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isModalOpen) return;
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
};
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
return () => document.removeEventListener('keydown', handleEscape);
|
||||
}, [isModalOpen, closeModal]);
|
||||
|
||||
// Focus trap: capture previous focus, focus close button, restore on close
|
||||
useEffect(() => {
|
||||
if (isModalOpen) {
|
||||
previousActiveElementRef.current = document.activeElement as HTMLElement | null;
|
||||
closeButtonRef.current?.focus();
|
||||
} else {
|
||||
previousActiveElementRef.current?.focus();
|
||||
}
|
||||
}, [isModalOpen]);
|
||||
|
||||
|
||||
|
||||
const renderVideoContent = () => {
|
||||
if (source.type === 'native') {
|
||||
return (
|
||||
<video
|
||||
{...source.props}
|
||||
className="bds-video__element"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Embed: prefer embedUrl, else extract from embedCode (trusted origins only)
|
||||
let embedUrl: string | null = null;
|
||||
|
||||
if ('embedUrl' in source && source.embedUrl) {
|
||||
embedUrl = isTrustedEmbedUrl(source.embedUrl)
|
||||
? source.embedUrl
|
||||
: null;
|
||||
} else if ('embedCode' in source && source.embedCode) {
|
||||
embedUrl = extractEmbedUrlFromCode(source.embedCode);
|
||||
}
|
||||
|
||||
if (embedUrl) {
|
||||
return (
|
||||
<iframe
|
||||
src={embedUrl}
|
||||
title="Video"
|
||||
className="bds-video__iframe"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const videoContent = renderVideoContent();
|
||||
if (!videoContent) return null;
|
||||
|
||||
const containerClass = clsx(
|
||||
'bds-video',
|
||||
`bds-video--aspect-${aspectRatio.replace('/', '-')}`,
|
||||
className
|
||||
);
|
||||
|
||||
if (coverImage) {
|
||||
return (
|
||||
<>
|
||||
<div ref={ref} className={containerClass}>
|
||||
<button
|
||||
type="button"
|
||||
className="bds-video__cover-button"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
aria-label="Play video"
|
||||
>
|
||||
<img
|
||||
src={coverImage.src}
|
||||
alt={coverImage.alt}
|
||||
className="bds-video__cover-image"
|
||||
/>
|
||||
<span className="bds-video__play-icon" aria-hidden>
|
||||
<svg
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx="32"
|
||||
cy="32"
|
||||
r="32"
|
||||
fill="rgba(0,0,0,0.5)"
|
||||
/>
|
||||
<path
|
||||
d="M26 20v24l18-12-18-12z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isModalOpen && (
|
||||
<div
|
||||
className="bds-video-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Video"
|
||||
>
|
||||
<div
|
||||
className="bds-video-modal__backdrop"
|
||||
onClick={closeModal}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="bds-video-modal__content">
|
||||
<button
|
||||
ref={closeButtonRef}
|
||||
type="button"
|
||||
className="bds-video-modal__close"
|
||||
onClick={closeModal}
|
||||
aria-label="Close video"
|
||||
>
|
||||
<span aria-hidden>×</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';
|
||||
@@ -1,104 +0,0 @@
|
||||
// BDS CardStats Pattern Styles
|
||||
// Brand Design System - Section with heading, description, and grid of CardStat components
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-card-stats - Base section container
|
||||
// .bds-card-stats__header - Header wrapper for heading and description
|
||||
// .bds-card-stats__heading - Section heading (uses .h-md)
|
||||
// .bds-card-stats__description - Section description (uses .body-l)
|
||||
// .bds-card-stats__cards - Cards grid container
|
||||
// .bds-card-stats__card-wrapper - Individual card wrapper
|
||||
//
|
||||
// Design tokens from Figma:
|
||||
// Light Mode:
|
||||
// - Background: White (#FFFFFF)
|
||||
// - Heading: Neutral Black (#141414) → $black
|
||||
// - Description: Neutral Black (#141414) → $black
|
||||
//
|
||||
// Dark Mode:
|
||||
// - Background: transparent (inherits page background)
|
||||
// - Heading: Neutral White (#FFFFFF) → $white
|
||||
// - Description: Neutral White (#FFFFFF) → $white
|
||||
//
|
||||
// - Header content max-width: 808px (approximately 8 columns at desktop)
|
||||
// - Gap between heading and description: 16px
|
||||
// - Gap between cards: 8px (matches $bds-grid-gutter)
|
||||
|
||||
|
||||
// Color tokens - Light Mode (from Figma: node 32051-2839)
|
||||
$bds-card-stats-heading-light: $black; // --neutral/black (#141414)
|
||||
$bds-card-stats-description-light: $black; // --neutral/black (#141414)
|
||||
|
||||
// Color tokens - Dark Mode (from Figma: node 32051-2524)
|
||||
$bds-card-stats-bg-dark: transparent; // Inherits page background
|
||||
$bds-card-stats-heading-dark: $white; // --neutral/white (#FFFFFF)
|
||||
$bds-card-stats-description-dark: $white; // --neutral/white (#FFFFFF)
|
||||
|
||||
// Spacing - Header gap (between heading and description)
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
$bds-card-stats-header-gap-base: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-card-stats-header-gap-lg: $bds-space-lg; // 16px - spacing('lg')
|
||||
|
||||
// Spacing - Section gap (between header and cards)
|
||||
$bds-card-stats-section-gap-sm: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-card-stats-section-gap-md: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-card-stats-section-gap-lg: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
|
||||
// Spacing - Section padding
|
||||
$bds-card-stats-padding-y-sm: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-card-stats-padding-y-md: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-card-stats-padding-y-lg: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
|
||||
// =============================================================================
|
||||
// Base Section Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-card-stats {
|
||||
// Vertical padding
|
||||
padding-top: $bds-card-stats-padding-y-sm;
|
||||
padding-bottom: $bds-card-stats-padding-y-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: $bds-card-stats-padding-y-md;
|
||||
padding-bottom: $bds-card-stats-padding-y-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: $bds-card-stats-padding-y-lg;
|
||||
padding-bottom: $bds-card-stats-padding-y-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-card-stats__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-card-stats-header-gap-base;
|
||||
margin-bottom: $bds-card-stats-section-gap-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-bottom: $bds-card-stats-section-gap-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-card-stats-header-gap-lg;
|
||||
margin-bottom: $bds-card-stats-section-gap-lg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Dark Mode Styles
|
||||
// =============================================================================
|
||||
|
||||
html.dark {
|
||||
.bds-card-stats__heading,
|
||||
.bds-card-stats__description {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export { CardStats, type CardStatsProps, type CardStatsCardConfig } from './CardStats';
|
||||
export { default } from './CardStats';
|
||||
|
||||
@@ -5,34 +5,36 @@
|
||||
// .bds-link-text-card - Base card container with border-top divider
|
||||
// .bds-link-text-card__header - Header section (number + heading)
|
||||
// .bds-link-text-card__content - Content section (description + buttons)
|
||||
//
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// Design Tokens (from _spacing.scss)
|
||||
// =============================================================================
|
||||
|
||||
// Header gap (between number and heading)
|
||||
$bds-link-text-card-header-gap-base: 8px;
|
||||
$bds-link-text-card-header-gap-md: 12px;
|
||||
$bds-link-text-card-header-gap-lg: 16px;
|
||||
$bds-link-text-card-header-gap-base: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-link-text-card-header-gap-md: $bds-space-md; // 12px - spacing('md')
|
||||
$bds-link-text-card-header-gap-lg: $bds-space-lg; // 16px - spacing('lg')
|
||||
|
||||
// Content gap (between description and buttons)
|
||||
$bds-link-text-card-content-gap-base: 16px;
|
||||
$bds-link-text-card-content-gap-md: 24px;
|
||||
$bds-link-text-card-content-gap-lg: 32px;
|
||||
$bds-link-text-card-content-gap-base: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-link-text-card-content-gap-md: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-link-text-card-content-gap-lg: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
|
||||
// Card gap (between header and content sections)
|
||||
$bds-link-text-card-gap-base: 24px;
|
||||
$bds-link-text-card-gap-md: 32px;
|
||||
$bds-link-text-card-gap-lg: 40px;
|
||||
$bds-link-text-card-gap-base: $bds-gap-section-sm; // 24px - spacing('2xl')
|
||||
$bds-link-text-card-gap-md: $bds-gap-section-md; // 32px - spacing('3xl')
|
||||
$bds-link-text-card-gap-lg: $bds-gap-section-lg; // 40px - spacing('4xl')
|
||||
|
||||
// Padding
|
||||
$bds-link-text-card-padding-top-base: 8px;
|
||||
$bds-link-text-card-padding-top-md: 12px;
|
||||
$bds-link-text-card-padding-top-lg: 16px;
|
||||
$bds-link-text-card-padding-top-base: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-link-text-card-padding-top-md: $bds-space-md; // 12px - spacing('md')
|
||||
$bds-link-text-card-padding-top-lg: $bds-space-lg; // 16px - spacing('lg')
|
||||
|
||||
$bds-link-text-card-padding-bottom-base: 24px;
|
||||
$bds-link-text-card-padding-bottom-md: 32px;
|
||||
$bds-link-text-card-padding-bottom-lg: 40px;
|
||||
$bds-link-text-card-padding-bottom-base: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-link-text-card-padding-bottom-md: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-link-text-card-padding-bottom-lg: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
|
||||
// Border
|
||||
$bds-link-text-card-border-width: 1px;
|
||||
@@ -98,6 +100,9 @@ $bds-link-text-card-content-width-md: calc(75% - 2px);
|
||||
p {
|
||||
color: $bds-link-text-card-number-color;
|
||||
}
|
||||
@include bds-theme-mode(dark) {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -121,4 +126,7 @@ $bds-link-text-card-content-width-md: calc(75% - 2px);
|
||||
p {
|
||||
color: $bds-link-text-card-number-color;
|
||||
}
|
||||
@include bds-theme-mode(dark) {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
50
shared/patterns/SectionHeader/SectionHeader.scss
Normal file
50
shared/patterns/SectionHeader/SectionHeader.scss
Normal file
@@ -0,0 +1,50 @@
|
||||
// BDS SectionHeader Pattern Styles
|
||||
// Brand Design System - Consolidated section header (heading + description)
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-section-header - Header wrapper for heading and description
|
||||
// .bds-section-header__heading - Section heading (uses .h-md)
|
||||
// .bds-section-header__description - Section description (uses .body-l)
|
||||
//
|
||||
// Design tokens from _spacing.scss:
|
||||
// - Gap between heading and description: $bds-gap-header-*
|
||||
// - Light/Dark mode colors: $black / $white
|
||||
|
||||
// =============================================================================
|
||||
// Header Section
|
||||
// =============================================================================
|
||||
|
||||
.bds-section-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-gap-header-sm;
|
||||
margin-bottom: $bds-gap-section-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-gap-header-md;
|
||||
margin-bottom: $bds-gap-section-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-gap-header-lg;
|
||||
margin-bottom: $bds-gap-section-lg;
|
||||
}
|
||||
|
||||
@include bds-theme-mode(light) {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
@include bds-theme-mode(dark) {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-section-header__heading {
|
||||
margin: 0;
|
||||
// Typography handled by .h-md class from _font.scss
|
||||
}
|
||||
|
||||
.bds-section-header__description {
|
||||
margin: 0;
|
||||
// Typography handled by .body-l class from _font.scss
|
||||
}
|
||||
111
shared/patterns/SectionHeader/SectionHeader.tsx
Normal file
111
shared/patterns/SectionHeader/SectionHeader.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid } from '../../components/PageGrid/page-grid';
|
||||
import type { ResponsiveValue, PageGridSpanValue } from '../../components/PageGrid/page-grid';
|
||||
import { isEnvironment } from '../../utils/helpers';
|
||||
|
||||
const DEFAULT_SPAN = {
|
||||
base: 'fill' as const,
|
||||
md: 6,
|
||||
lg: 8,
|
||||
};
|
||||
|
||||
export interface SectionHeaderProps {
|
||||
/** Section heading text */
|
||||
heading?: React.ReactNode;
|
||||
/** Section description text */
|
||||
description?: React.ReactNode;
|
||||
/** Polymorphic heading element - h1 through h6 */
|
||||
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||
/** PageGrid.Col span - defaults to { base: 'fill', md: 6, lg: 8 } */
|
||||
span?: ResponsiveValue<PageGridSpanValue>;
|
||||
/** Optional slot for trailing content (e.g. ButtonGroup) */
|
||||
children?: React.ReactNode;
|
||||
/** Additional CSS classes for the header wrapper */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* SectionHeader - Consolidated section header pattern
|
||||
*
|
||||
* Renders a PageGrid.Row + Col with heading (polymorphic h1-h6) and optional description.
|
||||
* Used across CardsFeatured, StandardCardGroupSection, CardsIconGrid, and other sections.
|
||||
*
|
||||
* **Behavior:**
|
||||
* - Returns `null` if no content is provided (no heading, description, or children)
|
||||
* - Logs a development warning when returning null to help catch missing props
|
||||
* - At least one of `heading`, `description`, or `children` should be provided
|
||||
*
|
||||
* @example
|
||||
* // Typical usage with heading and description
|
||||
* <SectionHeader
|
||||
* heading="Our Features"
|
||||
* description="Explore what we offer"
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // With custom heading level
|
||||
* <SectionHeader
|
||||
* heading="Main Title"
|
||||
* as="h1"
|
||||
* description="Subtitle text"
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // With children (e.g., ButtonGroup)
|
||||
* <SectionHeader heading="Products">
|
||||
* <ButtonGroup buttons={[...]} />
|
||||
* </SectionHeader>
|
||||
*/
|
||||
export const SectionHeader = React.forwardRef<HTMLDivElement, SectionHeaderProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
heading,
|
||||
description,
|
||||
as = 'h2',
|
||||
span = DEFAULT_SPAN,
|
||||
children,
|
||||
className,
|
||||
} = props;
|
||||
|
||||
const hasContent = heading || description || children;
|
||||
if (!hasContent) {
|
||||
if (isEnvironment(["development", "test"])) {
|
||||
console.warn(
|
||||
'SectionHeader: No content provided. Component requires at least one of: heading, description, or children. ' +
|
||||
'Returning null - this may indicate a missing prop or data issue.'
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const HeadingTag = as;
|
||||
|
||||
return (
|
||||
<PageGrid.Row>
|
||||
<PageGrid.Col span={span}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx('bds-section-header', className)}
|
||||
>
|
||||
{heading != null && heading !== '' && (
|
||||
<HeadingTag className="bds-section-header__heading h-md">
|
||||
{heading}
|
||||
</HeadingTag>
|
||||
)}
|
||||
{description != null && description !== '' && (
|
||||
<p className="bds-section-header__description body-l">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SectionHeader.displayName = 'SectionHeader';
|
||||
|
||||
export default SectionHeader;
|
||||
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,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from 'shared/patterns/ButtonGroup/ButtonGroup';
|
||||
|
||||
export interface CalloutMediaBannerProps {
|
||||
/** Color variant - determines background color (ignored if backgroundImage is provided) */
|
||||
@@ -12,6 +12,8 @@ export interface CalloutMediaBannerProps {
|
||||
textColor?: 'white' | 'black';
|
||||
/** Main heading text */
|
||||
heading?: string;
|
||||
/** Heading element type - h1 through h6 (defaults to h6) */
|
||||
headingAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||
/** Subheading/description text */
|
||||
subheading: string;
|
||||
/** Button configurations (1-2 buttons supported) */
|
||||
@@ -22,12 +24,12 @@ export interface CalloutMediaBannerProps {
|
||||
|
||||
/**
|
||||
* CalloutMediaBanner Component
|
||||
*
|
||||
*
|
||||
* A full-width banner component featuring a heading, subheading, and optional action buttons.
|
||||
* Supports 5 color variants or a custom background image. Responsive across mobile, tablet, and desktop.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // Color variant
|
||||
* // Color variant with default h6 heading
|
||||
* <CalloutMediaBanner
|
||||
* variant="green"
|
||||
* heading="The Compliant Ledger Protocol"
|
||||
@@ -39,6 +41,16 @@ export interface CalloutMediaBannerProps {
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // With custom heading level (h1)
|
||||
* <CalloutMediaBanner
|
||||
* variant="green"
|
||||
* heading="The Compliant Ledger Protocol"
|
||||
* headingAs="h1"
|
||||
* subheading="A decentralized public Layer 1 blockchain..."
|
||||
* buttons={[{ label: "Get Started", href: "/docs" }]}
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // With background image (white text - default)
|
||||
* <CalloutMediaBanner
|
||||
* backgroundImage="/images/hero-bg.jpg"
|
||||
@@ -53,6 +65,7 @@ export interface CalloutMediaBannerProps {
|
||||
* backgroundImage="/images/light-hero-bg.jpg"
|
||||
* textColor="black"
|
||||
* heading="Build on XRPL"
|
||||
* headingAs="h2"
|
||||
* subheading="Start building your next project"
|
||||
* buttons={[{ label: "Start Building", onClick: handleClick }]}
|
||||
* />
|
||||
@@ -62,6 +75,7 @@ export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
|
||||
backgroundImage,
|
||||
textColor = 'white',
|
||||
heading,
|
||||
headingAs = 'h6',
|
||||
subheading,
|
||||
buttons,
|
||||
className = '',
|
||||
@@ -96,6 +110,9 @@ export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
|
||||
? { backgroundImage: `url(${backgroundImage})` }
|
||||
: {};
|
||||
|
||||
// Create the heading element dynamically
|
||||
const HeadingElement = headingAs;
|
||||
|
||||
return (
|
||||
<PageGrid containerType="wide">
|
||||
<PageGridRow className={classNames} style={inlineStyle}>
|
||||
@@ -103,7 +120,7 @@ export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
|
||||
<div className="bds-callout-media-banner__content">
|
||||
{/* Text Content */}
|
||||
<div className="bds-callout-media-banner__text">
|
||||
{heading && <h2 className="bds-callout-media-banner__heading">{heading}</h2>}
|
||||
{heading && <HeadingElement className="bds-callout-media-banner__heading">{heading}</HeadingElement>}
|
||||
<p className="bds-callout-media-banner__subheading">{subheading}</p>
|
||||
</div>
|
||||
|
||||
@@ -94,7 +94,7 @@ interface CalloutMediaBannerProps {
|
||||
### Basic Usage with Color Variant
|
||||
|
||||
```tsx
|
||||
import { CalloutMediaBanner } from 'shared/patterns/CalloutMediaBanner';
|
||||
import { CalloutMediaBanner } from 'shared/sections/CalloutMediaBanner';
|
||||
|
||||
<CalloutMediaBanner
|
||||
variant="green"
|
||||
@@ -313,7 +313,7 @@ $gray-600 // Gray (dark)
|
||||
|
||||
- **Figma Design**: [Callout - Media Banner](https://www.figma.com/design/i4OuOX6QSBauMaJE4iY4kV/Callout---Media-Banner?node-id=1-2&m=dev)
|
||||
- **Showcase Page**: `/about/callout-media-banner-showcase.page.tsx`
|
||||
- **Component Location**: `shared/patterns/CalloutMediaBanner/`
|
||||
- **Component Location**: `shared/sections/CalloutMediaBanner/`
|
||||
|
||||
## Version History
|
||||
|
||||
@@ -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,8 +15,7 @@
|
||||
// - Heading: Neutral White (#FFFFFF) → $white
|
||||
// - Description: Neutral White (#FFFFFF) → $white
|
||||
//
|
||||
// - Header content max-width: 808px (approximately 8 columns at desktop)
|
||||
// - Gap between heading and description: 16px
|
||||
// - Header: uses SectionHeader component
|
||||
// - Gap between cards: 8px (matches $bds-grid-gutter)
|
||||
|
||||
// =============================================================================
|
||||
@@ -28,15 +24,7 @@
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
// $bds-grid-gutter is defined in _spacing.scss (8px).
|
||||
|
||||
// Spacing - Header gap (between heading and description)
|
||||
$bds-cards-featured-header-gap-sm: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-cards-featured-header-gap-md: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-cards-featured-header-gap-lg: $bds-space-lg; // 16px - spacing('lg')
|
||||
|
||||
// Spacing - Section gap (between header and cards)
|
||||
$bds-cards-featured-section-gap-sm: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-cards-featured-section-gap-md: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-cards-featured-section-gap-lg: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
// Spacing - Section gap handled by SectionHeader margin-bottom
|
||||
|
||||
// Spacing - Cards gap
|
||||
$bds-cards-featured-cards-gap-sm: $bds-space-5xl; // 48px - spacing('5xl')
|
||||
@@ -50,14 +38,6 @@ $bds-cards-featured-padding-y-lg: $bds-space-8xl; // 80px - spacing('8xl')
|
||||
|
||||
// Spacing - Section padding (horizontal) - handled by PageGrid
|
||||
|
||||
// Colors - Light Mode (default)
|
||||
$bds-cards-featured-heading-color: $black; // #141414 - Neutral black
|
||||
$bds-cards-featured-description-color: $black; // #141414 - Neutral black
|
||||
|
||||
// Colors - Dark Mode (from Figma node 27020-3045)
|
||||
$bds-cards-featured-heading-color-dark: $white; // #FFFFFF - Neutral white
|
||||
$bds-cards-featured-description-color-dark: $white; // #FFFFFF - Neutral white
|
||||
|
||||
// =============================================================================
|
||||
// Section Container
|
||||
// =============================================================================
|
||||
@@ -78,34 +58,6 @@ $bds-cards-featured-description-color-dark: $white; // #FFFFFF - Neutral white
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Section
|
||||
// =============================================================================
|
||||
|
||||
.bds-cards-featured__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-cards-featured-header-gap-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-cards-featured-header-gap-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-cards-featured-header-gap-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-cards-featured__heading {
|
||||
margin: 0;
|
||||
// Typography handled by .h-md class from _font.scss
|
||||
}
|
||||
|
||||
.bds-cards-featured__description {
|
||||
margin: 0;
|
||||
// Typography handled by .body-l class from _font.scss
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Cards Grid
|
||||
// =============================================================================
|
||||
@@ -116,20 +68,17 @@ $bds-cards-featured-description-color-dark: $white; // #FFFFFF - Neutral white
|
||||
column-gap: $bds-cards-featured-cards-gap-sm;
|
||||
row-gap: $bds-space-5xl; // 48px - spacing('5xl')
|
||||
width: 100%;
|
||||
margin-top: $bds-cards-featured-section-gap-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
column-gap: $bds-cards-featured-cards-gap-md;
|
||||
row-gap: 52px; // Non-standard value, kept as-is
|
||||
margin-top: $bds-cards-featured-section-gap-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
column-gap: $bds-cards-featured-cards-gap-lg;
|
||||
row-gap: 56px; // Non-standard value, kept as-is
|
||||
margin-top: $bds-cards-featured-section-gap-lg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,27 +101,10 @@ html.light {
|
||||
.bds-cards-featured {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.bds-cards-featured__heading {
|
||||
color: $bds-cards-featured-heading-color;
|
||||
}
|
||||
|
||||
.bds-cards-featured__description {
|
||||
color: $bds-cards-featured-description-color;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dark Mode Styles (from Figma node 27020-3045)
|
||||
// =============================================================================
|
||||
|
||||
html.dark {
|
||||
.bds-cards-featured__heading {
|
||||
color: $bds-cards-featured-heading-color-dark;
|
||||
}
|
||||
|
||||
.bds-cards-featured__description {
|
||||
color: $bds-cards-featured-description-color-dark;
|
||||
}
|
||||
}
|
||||
// Header colors handled by SectionHeader component
|
||||
|
||||
@@ -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>
|
||||
@@ -3,7 +3,6 @@
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-cards-icon-grid - Base section container
|
||||
// .bds-cards-icon-grid__header - Header section (heading + description)
|
||||
// .bds-cards-icon-grid__list - Grid list (ul as PageGrid.Row)
|
||||
|
||||
// =============================================================================
|
||||
@@ -14,14 +13,6 @@ $bds-cards-icon-grid-padding-base: $bds-space-2xl;
|
||||
$bds-cards-icon-grid-padding-md: $bds-space-3xl;
|
||||
$bds-cards-icon-grid-padding-lg: $bds-space-4xl;
|
||||
|
||||
$bds-cards-icon-grid-header-gap-base: $bds-gap-header-sm;
|
||||
$bds-cards-icon-grid-header-gap-md: $bds-gap-header-md;
|
||||
$bds-cards-icon-grid-header-gap-lg: $bds-gap-header-lg;
|
||||
|
||||
$bds-cards-icon-grid-header-margin-base: $bds-gap-section-sm;
|
||||
$bds-cards-icon-grid-header-margin-md: $bds-gap-section-md;
|
||||
$bds-cards-icon-grid-header-margin-lg: $bds-gap-section-lg;
|
||||
|
||||
// =============================================================================
|
||||
// Section Container
|
||||
// =============================================================================
|
||||
@@ -47,31 +38,6 @@ $bds-cards-icon-grid-header-margin-lg: $bds-gap-section-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Section
|
||||
// =============================================================================
|
||||
|
||||
.bds-cards-icon-grid__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-cards-icon-grid-header-gap-base;
|
||||
margin-bottom: $bds-cards-icon-grid-header-margin-base;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-cards-icon-grid-header-gap-md;
|
||||
margin-bottom: $bds-cards-icon-grid-header-margin-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-cards-icon-grid-header-gap-lg;
|
||||
margin-bottom: $bds-cards-icon-grid-header-margin-lg;
|
||||
}
|
||||
|
||||
h2, p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// List Grid - Aspect Ratios
|
||||
// =============================================================================
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
|
||||
import { PageGrid, PageGridRow } from 'shared/components/PageGrid/page-grid';
|
||||
import { SectionHeader } from 'shared/patterns/SectionHeader';
|
||||
import { CardTextIconCard, CardTextIconCardProps } from 'shared/components/CardTextIcon';
|
||||
|
||||
export interface CardsIconGridProps {
|
||||
@@ -38,12 +39,7 @@ export const CardsIconGrid: React.FC<CardsIconGridProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<PageGrid className={clsx('bds-cards-icon-grid', className)}>
|
||||
<PageGridRow>
|
||||
<PageGridCol className="bds-cards-icon-grid__header" span={{ base: 12, md: 6, lg: 8 }}>
|
||||
<h2 className="h-md">{heading}</h2>
|
||||
{description && <p className="body-l">{description}</p>}
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
<SectionHeader heading={heading} description={description} span={{ base: 12, md: 6, lg: 8 }} />
|
||||
|
||||
<PageGridRow as="ul" className="bds-cards-icon-grid__list list-none pl-0">
|
||||
{cards.map((card, index) => (
|
||||
@@ -16,7 +16,7 @@ CardsIconGrid mirrors the LinkTextDirectory header structure and renders cards i
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { CardsIconGrid } from 'shared/patterns/CardsIconGrid';
|
||||
import { CardsIconGrid } from 'shared/sections/CardsIconGrid';
|
||||
|
||||
<CardsIconGrid
|
||||
heading="Explore Tools"
|
||||
@@ -3,7 +3,6 @@
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-cards-text-grid - Base section container
|
||||
// .bds-cards-text-grid__header - Header section (heading + description)
|
||||
// .bds-cards-text-grid__list - Grid list (ul as PageGrid.Row)
|
||||
|
||||
// =============================================================================
|
||||
@@ -14,14 +13,6 @@ $bds-cards-text-grid-padding-base: $bds-space-2xl;
|
||||
$bds-cards-text-grid-padding-md: $bds-space-3xl;
|
||||
$bds-cards-text-grid-padding-lg: $bds-space-4xl;
|
||||
|
||||
$bds-cards-text-grid-header-gap-base: $bds-gap-header-sm;
|
||||
$bds-cards-text-grid-header-gap-md: $bds-gap-header-md;
|
||||
$bds-cards-text-grid-header-gap-lg: $bds-gap-header-lg;
|
||||
|
||||
$bds-cards-text-grid-header-margin-base: $bds-gap-section-sm;
|
||||
$bds-cards-text-grid-header-margin-md: $bds-gap-section-md;
|
||||
$bds-cards-text-grid-header-margin-lg: $bds-gap-section-lg;
|
||||
|
||||
// =============================================================================
|
||||
// Section Container
|
||||
// =============================================================================
|
||||
@@ -47,31 +38,6 @@ $bds-cards-text-grid-header-margin-lg: $bds-gap-section-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Section
|
||||
// =============================================================================
|
||||
|
||||
.bds-cards-text-grid__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-cards-text-grid-header-gap-base;
|
||||
margin-bottom: $bds-cards-text-grid-header-margin-base;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-cards-text-grid-header-gap-md;
|
||||
margin-bottom: $bds-cards-text-grid-header-margin-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-cards-text-grid-header-gap-lg;
|
||||
margin-bottom: $bds-cards-text-grid-header-margin-lg;
|
||||
}
|
||||
|
||||
h2, p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// List Grid - Aspect Ratios
|
||||
// =============================================================================
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
|
||||
import { PageGrid, PageGridRow } from 'shared/components/PageGrid/page-grid';
|
||||
import { SectionHeader } from 'shared/patterns/SectionHeader';
|
||||
import { CardTextIconCard, CardTextIconCardProps } from 'shared/components/CardTextIcon';
|
||||
|
||||
export interface CardsTextGridProps {
|
||||
@@ -38,12 +39,7 @@ export const CardsTextGrid: React.FC<CardsTextGridProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<PageGrid className={clsx('bds-cards-text-grid', className)}>
|
||||
<PageGridRow>
|
||||
<PageGridCol className="bds-cards-text-grid__header" span={{ base: 12, md: 6, lg: 8 }}>
|
||||
<h2 className="h-md">{heading}</h2>
|
||||
{description && <p className="body-l">{description}</p>}
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
<SectionHeader heading={heading} description={description} span={{ base: 12, md: 6, lg: 8 }} />
|
||||
|
||||
<PageGridRow as="ul" className="bds-cards-text-grid__list list-none pl-0">
|
||||
{cards.map((card, index) => (
|
||||
@@ -16,7 +16,7 @@ CardsTextGrid mirrors the LinkTextDirectory header structure and renders cards i
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { CardsTextGrid } from 'shared/patterns/CardsTextGrid';
|
||||
import { CardsTextGrid } from 'shared/sections/CardsTextGrid';
|
||||
|
||||
<CardsTextGrid
|
||||
heading="Explore Tools"
|
||||
@@ -15,7 +15,7 @@ A section pattern featuring a header with title and description, plus a 2×2 gri
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { CardsTwoColumn } from 'shared/patterns/CardsTwoColumn';
|
||||
import { CardsTwoColumn } from 'shared/sections/CardsTwoColumn';
|
||||
|
||||
<CardsTwoColumn
|
||||
title="The Future of Finance is Already Onchain"
|
||||
@@ -153,7 +153,7 @@ Each color variant has four interactive states with a "window shade" hover anima
|
||||
|
||||
- **Figma Design**: [Section Cards - Two Column](https://www.figma.com/design/MP5gjNp7yPBnKBKleb8LRL/Section-Cards---Two-Column)
|
||||
- **Showcase Page**: `/about/cards-two-column-showcase`
|
||||
- **Component Location**: `shared/patterns/CardsTwoColumn/`
|
||||
- **Component Location**: `shared/sections/CardsTwoColumn/`
|
||||
|
||||
## Version History
|
||||
|
||||
@@ -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"
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid } from '../../components/PageGrid/page-grid';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from 'shared/patterns/ButtonGroup/ButtonGroup';
|
||||
|
||||
export interface FeatureSingleTopicProps {
|
||||
/** Background variant for the title section
|
||||
@@ -15,7 +15,7 @@ A feature section pattern that pairs a title/description with a media element in
|
||||
## Basic Usage
|
||||
|
||||
```tsx
|
||||
import { FeatureSingleTopic } from 'shared/patterns/FeatureSingleTopic';
|
||||
import { FeatureSingleTopic } from 'shared/sections/FeatureSingleTopic';
|
||||
|
||||
<FeatureSingleTopic
|
||||
variant="default"
|
||||
@@ -167,7 +167,7 @@ Full dark mode support with `html.dark` selector:
|
||||
|
||||
- **Figma Design**: [Section Feature - Single Topic](https://www.figma.com/design/sg6T5EptbN0V2olfCSHzcx/Section-Feature---Single-Topic?node-id=18030-2250&m=dev)
|
||||
- **Showcase Page**: `/about/feature-single-topic-showcase`
|
||||
- **Component Location**: `shared/patterns/FeatureSingleTopic/`
|
||||
- **Component Location**: `shared/sections/FeatureSingleTopic/`
|
||||
|
||||
## Related Components
|
||||
|
||||
@@ -210,7 +210,7 @@ FeatureTwoColumn
|
||||
## Design References
|
||||
|
||||
- **Figma Design**: [Pattern - Feature - Two Column](https://www.figma.com/design/3tmqxMrEvOVvpYhgOCxv2D/Pattern-Feature---Two-Column?node-id=20017-3501&m=dev)
|
||||
- **Component Location**: `shared/patterns/FeatureTwoColumn/`
|
||||
- **Component Location**: `shared/sections/FeatureTwoColumn/`
|
||||
- **Color Tokens**: `styles/_colors.scss`
|
||||
- **Typography**: `styles/_font.scss`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid } from '../../components/PageGrid/page-grid';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from 'shared/patterns/ButtonGroup/ButtonGroup';
|
||||
|
||||
export interface FeatureTwoColumnLink {
|
||||
/** Link label text */
|
||||
@@ -14,7 +14,7 @@ The FeaturedVideoHero component provides a structured hero section with:
|
||||
## Basic Usage
|
||||
|
||||
```tsx
|
||||
import { FeaturedVideoHero } from "shared/patterns/FeaturedVideoHero";
|
||||
import { FeaturedVideoHero } from "shared/sections/FeaturedVideoHero";
|
||||
|
||||
function MyPage() {
|
||||
return (
|
||||
@@ -26,7 +26,7 @@ function MyPage() {
|
||||
smart contracts.
|
||||
</p>
|
||||
}
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
links={[{ label: "Get Started", href: "/docs" }]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
@@ -45,19 +45,18 @@ function MyPage() {
|
||||
| --------------- | --------------------------------- | -------- | ---------------------------------------------------------------------------- |
|
||||
| `headline` | `React.ReactNode` | Yes | Hero headline text (h-md typography) |
|
||||
| `subtitle` | `React.ReactNode` | No | Hero subtitle content |
|
||||
| `callsToAction` | `DesignConstrainedCallsToActions` | No | Array with primary CTA and optional secondary CTA. Omit to hide CTA section. |
|
||||
| `links` | `DesignConstrainedLink[]` | No | Array of `{ label, href }` for ButtonGroup. Omit to hide button section. |
|
||||
| `videoElement` | `DesignConstrainedVideoProps` | Yes | Native `<video>` element props (e.g. `src`, `autoPlay`, `loop`, `muted`) |
|
||||
| `className` | `string` | No | Additional CSS classes for the header element |
|
||||
| `...rest` | `HTMLHeaderElement` attributes | No | Any other HTML header attributes |
|
||||
|
||||
### Calls to Action
|
||||
### Links (ButtonGroup)
|
||||
|
||||
The `callsToAction` prop is optional. When provided, at least one non-empty CTA is required to show the CTA section. The component uses design-constrained Button props; `variant` and `color` are set automatically:
|
||||
The `links` prop is optional. When provided, at least one non-empty link (`label` and `href`) is required to show the button section. Uses `{ label, href }` format for consistent ButtonGroup rendering; `variant` and `color` are set by the component:
|
||||
|
||||
- **Primary CTA**: `variant="primary"`, `color="green"`, `forceColor={true}`
|
||||
- **Secondary CTA**: `variant="tertiary"`, `color="green"`, `forceColor={true}`
|
||||
|
||||
All other Button props are supported (e.g., `children`, `href`, `onClick`). Do not pass `variant` or `color` in the CTA objects.
|
||||
- **First link**: `variant="primary"`, `color="green"`, `forceColor={true}`
|
||||
- **Second link**: `variant="tertiary"`, `color="green"`, `forceColor={true}`
|
||||
- Max 2 links supported (ButtonGroup validation)
|
||||
|
||||
### Video Element
|
||||
|
||||
@@ -77,9 +76,9 @@ The video is rendered with `object-fit: cover` and a 16:9 aspect ratio container
|
||||
<FeaturedVideoHero
|
||||
headline="Real-world asset tokenization"
|
||||
subtitle="Learn how to issue crypto tokens and build tokenization solutions."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" },
|
||||
{ children: "Learn More", href: "/about" },
|
||||
links={[
|
||||
{ label: "Get Started", href: "/docs" },
|
||||
{ label: "Learn More", href: "/about" },
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/tokenization.mp4",
|
||||
@@ -96,7 +95,7 @@ The video is rendered with `object-fit: cover` and a 16:9 aspect ratio container
|
||||
```tsx
|
||||
<FeaturedVideoHero
|
||||
headline="Headline Only"
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
links={[{ label: "Get Started", href: "/docs" }]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
@@ -113,7 +112,7 @@ The video is rendered with `object-fit: cover` and a 16:9 aspect ratio container
|
||||
<FeaturedVideoHero
|
||||
headline="Watch and Learn"
|
||||
subtitle="Explore our video tutorials and guides."
|
||||
callsToAction={[{ children: "Watch Tutorials", href: "/tutorials" }]}
|
||||
links={[{ label: "Watch Tutorials", href: "/tutorials" }]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: false,
|
||||
@@ -129,7 +128,7 @@ The video is rendered with `object-fit: cover` and a 16:9 aspect ratio container
|
||||
## Validation
|
||||
|
||||
- **Required props**: `headline`, `videoElement`. If either is missing or empty, the component returns `null` and (in development/test) logs a console warning.
|
||||
- **Optional props**: `subtitle`, `callsToAction`. Omit `callsToAction` or pass an array with no renderable CTAs to hide the CTA section.
|
||||
- **Optional props**: `subtitle`, `links`. Omit `links` or pass an empty array to hide the button section.
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import React, { forwardRef, memo, useEffect } from "react";
|
||||
import clsx from "clsx";
|
||||
import { PageGrid } from "shared/components/PageGrid/page-grid";
|
||||
import { Button } from "shared/components/Button/Button";
|
||||
import {
|
||||
isEmpty,
|
||||
DesignConstrainedButtonProps,
|
||||
isEnvironment,
|
||||
} from "shared/utils";
|
||||
import { ButtonGroup, validateButtonGroup } from "shared/patterns/ButtonGroup/ButtonGroup";
|
||||
import { isEmpty, isEnvironment } from "shared/utils";
|
||||
import {
|
||||
DesignConstrainedImageProps,
|
||||
DesignConstrainedVideoProps,
|
||||
DesignConstrainedLinksProps,
|
||||
} from "shared/utils/types";
|
||||
|
||||
/**
|
||||
@@ -45,12 +42,11 @@ export type HeaderHeroMedia =
|
||||
| VideoMediaProps
|
||||
| CustomMediaProps;
|
||||
|
||||
export interface HeaderHeroPrimaryMediaProps extends React.ComponentPropsWithoutRef<"header"> {
|
||||
export interface HeaderHeroPrimaryMediaProps extends React.ComponentPropsWithoutRef<"header">, DesignConstrainedLinksProps {
|
||||
/** Hero title text (display-md typography) */
|
||||
headline: React.ReactNode;
|
||||
/** Hero subtitle text (subhead-sm-l typography) */
|
||||
subtitle: React.ReactNode;
|
||||
callsToAction: [DesignConstrainedButtonProps, DesignConstrainedButtonProps?];
|
||||
/** Media element - supports image, video, or custom React element */
|
||||
media: HeaderHeroMedia;
|
||||
}
|
||||
@@ -110,10 +106,14 @@ const HeaderHeroPrimaryMedia = forwardRef<
|
||||
HTMLElement,
|
||||
HeaderHeroPrimaryMediaProps
|
||||
>((props, ref) => {
|
||||
const { headline, subtitle, callsToAction, media, className, ...restProps } =
|
||||
const { headline, subtitle, links, media, className, ...restProps } =
|
||||
props;
|
||||
|
||||
const [primaryCta, secondaryCta] = callsToAction;
|
||||
const buttonValidation = validateButtonGroup(
|
||||
(links ?? []).map((l) => ({ label: l.label, href: l.href, forceColor: true })),
|
||||
2,
|
||||
isEnvironment(["development", "test"])
|
||||
);
|
||||
|
||||
// Headline is critical - exit early if missing
|
||||
if (!headline) {
|
||||
@@ -132,7 +132,7 @@ const HeaderHeroPrimaryMedia = forwardRef<
|
||||
|
||||
const propsToValidate = {
|
||||
subtitle,
|
||||
callsToAction,
|
||||
links,
|
||||
media,
|
||||
};
|
||||
|
||||
@@ -141,7 +141,7 @@ const HeaderHeroPrimaryMedia = forwardRef<
|
||||
console.warn(`${key} is required for HeaderHeroPrimaryMedia`);
|
||||
}
|
||||
});
|
||||
}, [subtitle, callsToAction, media]);
|
||||
}, [subtitle, links, media]);
|
||||
|
||||
return (
|
||||
<header
|
||||
@@ -166,28 +166,14 @@ const HeaderHeroPrimaryMedia = forwardRef<
|
||||
{subtitle}
|
||||
</div>
|
||||
)}
|
||||
{(!isEmpty(primaryCta) || !isEmpty(secondaryCta)) && (
|
||||
{buttonValidation.hasButtons && (
|
||||
<div className="bds-header-hero-primary-media__cta-buttons">
|
||||
{!isEmpty(primaryCta) && (
|
||||
<Button
|
||||
{...primaryCta!}
|
||||
variant="primary"
|
||||
color="green"
|
||||
showIcon={true}
|
||||
/>
|
||||
)}
|
||||
{!isEmpty(secondaryCta) && (
|
||||
<Button
|
||||
{...secondaryCta!}
|
||||
className={clsx(
|
||||
"bds-header-hero-primary-media__cta-button-tertiary",
|
||||
secondaryCta?.className,
|
||||
)}
|
||||
variant="tertiary"
|
||||
color="green"
|
||||
showIcon={true}
|
||||
/>
|
||||
)}
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color="green"
|
||||
forceColor
|
||||
gap="small"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -14,14 +14,14 @@ The HeaderHeroPrimaryMedia component provides a structured hero section with:
|
||||
## Basic Usage
|
||||
|
||||
```tsx
|
||||
import HeaderHeroPrimaryMedia from "shared/patterns/HeaderHeroPrimaryMedia/HeaderHeroPrimaryMedia";
|
||||
import HeaderHeroPrimaryMedia from "shared/sections/HeaderHeroPrimaryMedia/HeaderHeroPrimaryMedia";
|
||||
|
||||
function MyPage() {
|
||||
return (
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Build on XRPL"
|
||||
subtitle="Start developing today with our comprehensive developer tools."
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
links={[{ label: "Get Started", href: "/docs" }]}
|
||||
media={{
|
||||
type: "image",
|
||||
src: "/img/hero.png",
|
||||
@@ -38,19 +38,18 @@ function MyPage() {
|
||||
| --------------- | ------------------------------ | -------- | ------------------------------------------------------------ |
|
||||
| `headline` | `React.ReactNode` | Yes | Hero headline text (display-md typography) |
|
||||
| `subtitle` | `React.ReactNode` | Yes | Hero subtitle text (label-l typography) |
|
||||
| `callsToAction` | `[ButtonProps, ButtonProps?]` | Yes | Array with primary CTA (required) and optional secondary CTA |
|
||||
| `links` | `DesignConstrainedLink[]` | No | Array of `{ label, href }` for ButtonGroup |
|
||||
| `media` | `HeaderHeroMedia` | Yes | Media element (image, video, or custom) |
|
||||
| `className` | `string` | No | Additional CSS classes for the header element |
|
||||
| `...rest` | `HTMLHeaderElement attributes` | No | Any other HTML header attributes |
|
||||
|
||||
### Calls to Action
|
||||
### Links (ButtonGroup)
|
||||
|
||||
The `callsToAction` prop accepts Button component props, but `variant` and `color` are automatically set:
|
||||
The `links` prop accepts an array of `{ label, href }` objects for consistent ButtonGroup rendering; `variant` and `color` are set by the component:
|
||||
|
||||
- **Primary CTA**: `variant="primary"`, `color="green"`
|
||||
- **Secondary CTA**: `variant="tertiary"`, `color="green"`
|
||||
|
||||
All other Button props are supported (e.g., `children`, `href`, `onClick`, etc.).
|
||||
- **First link**: `variant="primary"`, `color="green"`
|
||||
- **Second link**: `variant="tertiary"`, `color="green"`
|
||||
- Max 2 links supported (ButtonGroup validation)
|
||||
|
||||
## Media Types
|
||||
|
||||
@@ -130,9 +129,9 @@ media={{
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Real-world asset tokenization"
|
||||
subtitle="Learn how to issue crypto tokens and build solutions."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" },
|
||||
{ children: "Learn More", href: "/about" },
|
||||
links={[
|
||||
{ label: "Get Started", href: "/docs" },
|
||||
{ label: "Learn More", href: "/about" },
|
||||
]}
|
||||
media={{
|
||||
type: "image",
|
||||
@@ -148,7 +147,7 @@ media={{
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Watch and Learn"
|
||||
subtitle="Explore our video tutorials."
|
||||
callsToAction={[{ children: "Watch Tutorials", href: "/tutorials" }]}
|
||||
links={[{ label: "Watch Tutorials", href: "/tutorials" }]}
|
||||
media={{
|
||||
type: "video",
|
||||
src: "/video/intro.mp4",
|
||||
@@ -166,7 +165,7 @@ media={{
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Interactive Experience"
|
||||
subtitle="Engage with custom media."
|
||||
callsToAction={[{ children: "Explore", href: "/interactive" }]}
|
||||
links={[{ label: "Explore", href: "/interactive" }]}
|
||||
media={{
|
||||
type: "custom",
|
||||
element: <MyAnimationComponent />,
|
||||
@@ -190,7 +189,7 @@ The component enforces specific design requirements:
|
||||
The component includes development-time validation that logs warnings to the console when required props are missing:
|
||||
|
||||
- Missing `headline`: Component returns `null` (error logged)
|
||||
- Missing `subtitle`, `callsToAction`, or `media`: Warning logged, component still renders
|
||||
- Missing `subtitle`, `links`, or `media`: Warning logged, component still renders
|
||||
|
||||
## CSS Classes
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-link-small-grid - Base section container
|
||||
// .bds-link-small-grid__header - Header container (heading + description)
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
@@ -42,23 +41,6 @@ $bds-link-small-grid-spacing-lg: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Section
|
||||
// =============================================================================
|
||||
|
||||
.bds-link-small-grid__header {
|
||||
gap: $bds-space-sm; // 8px - spacing('sm')
|
||||
margin-bottom: $bds-link-small-grid-spacing-base;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-space-lg; // 16px - spacing('lg')
|
||||
margin-bottom: $bds-link-small-grid-spacing-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-space-lg; // 16px - spacing('lg')
|
||||
margin-bottom: $bds-link-small-grid-spacing-lg;
|
||||
}
|
||||
}
|
||||
// Header section uses SectionHeader component
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
|
||||
import { TileLink, TileLinkProps } from '../TileLinks/TileLink';
|
||||
import { SectionHeader } from 'shared/patterns/SectionHeader';
|
||||
import { TileLink, TileLinkProps } from 'shared/patterns/TileLinks';
|
||||
import { calculateTileOffset } from 'shared/utils/helpers';
|
||||
|
||||
export interface LinkItem extends Omit<TileLinkProps, 'variant'> {}
|
||||
@@ -90,17 +91,7 @@ export const LinkSmallGrid: React.FC<LinkSmallGridProps> = ({
|
||||
return (
|
||||
<section className={classNames}>
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 6, lg: 8 }}>
|
||||
{/* Header Section */}
|
||||
<div className="bds-link-small-grid__header">
|
||||
<h2 className="bds-link-small-grid__heading h-md">{heading}</h2>
|
||||
{description && (
|
||||
<p className="body-l mb-0">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
<SectionHeader heading={heading} description={description} span={{ base: 4, md: 6, lg: 8 }} />
|
||||
<PageGridRow>
|
||||
{links.map((link, index) => {
|
||||
const offset = linkOffsets[index];
|
||||
@@ -3,7 +3,6 @@
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-link-text-directory - Base section container
|
||||
// .bds-link-text-directory__header - Header section (heading + description)
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
@@ -14,16 +13,6 @@ $bds-link-text-directory-padding-base: 24px;
|
||||
$bds-link-text-directory-padding-md: 32px;
|
||||
$bds-link-text-directory-padding-lg: 40px;
|
||||
|
||||
// Header gap (between heading and description)
|
||||
$bds-link-text-directory-header-gap-base: 8px;
|
||||
$bds-link-text-directory-header-gap-md: 8px;
|
||||
$bds-link-text-directory-header-gap-lg: 16px;
|
||||
|
||||
// Header margin-bottom (space before cards list)
|
||||
$bds-link-text-directory-header-margin-base: 24px;
|
||||
$bds-link-text-directory-header-margin-md: 32px;
|
||||
$bds-link-text-directory-header-margin-lg: 40px;
|
||||
|
||||
// =============================================================================
|
||||
// Section Container
|
||||
// =============================================================================
|
||||
@@ -51,26 +40,4 @@ $bds-link-text-directory-header-margin-lg: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Section
|
||||
// =============================================================================
|
||||
|
||||
.bds-link-text-directory__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-link-text-directory-header-gap-base;
|
||||
margin-bottom: $bds-link-text-directory-header-margin-base;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-link-text-directory-header-gap-md;
|
||||
margin-bottom: $bds-link-text-directory-header-margin-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-link-text-directory-header-gap-lg;
|
||||
margin-bottom: $bds-link-text-directory-header-margin-lg;
|
||||
}
|
||||
h2, p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
// Header section uses SectionHeader component
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { LinkTextCard } from '../LinkTextCard';
|
||||
import { ButtonConfig } from '../ButtonGroup';
|
||||
import { LinkTextCard } from 'shared/patterns/LinkTextCard';
|
||||
import { ButtonConfig } from 'shared/patterns/ButtonGroup';
|
||||
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
|
||||
import { SectionHeader } from 'shared/patterns/SectionHeader';
|
||||
|
||||
export interface LinkTextCardData {
|
||||
/** Heading text for the card */
|
||||
@@ -75,13 +76,7 @@ export const LinkTextDirectory: React.FC<LinkTextDirectoryProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<PageGrid className={clsx('bds-link-text-directory', className)}>
|
||||
{/* Header Section */}
|
||||
<PageGridRow>
|
||||
<PageGridCol className="bds-link-text-directory__header" span={{ base: 12, md: 6, lg: 8}}>
|
||||
<h2 className="h-md">{heading}</h2>
|
||||
{description && <p className="body-l">{description}</p>}
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
<SectionHeader heading={heading} description={description} span={{ base: 12, md: 6, lg: 8 }} />
|
||||
|
||||
{/* Cards List */}
|
||||
<PageGridRow className="bds-link-text-directory__list">
|
||||
@@ -214,13 +214,13 @@ cards[2] → LinkTextCard(index: 2) → displays "03"
|
||||
## Import
|
||||
|
||||
```tsx
|
||||
import { LinkTextDirectory } from 'shared/patterns/LinkTextDirectory';
|
||||
import { LinkTextDirectory } from 'shared/sections/LinkTextDirectory';
|
||||
// or
|
||||
import {
|
||||
LinkTextDirectory,
|
||||
type LinkTextDirectoryProps,
|
||||
type LinkTextCardData
|
||||
} from 'shared/patterns/LinkTextDirectory';
|
||||
} from 'shared/sections/LinkTextDirectory';
|
||||
```
|
||||
|
||||
## Design System
|
||||
@@ -1,71 +1,82 @@
|
||||
// BDS LogoSquareGrid Component Styles
|
||||
// Brand Design System - Logo grid pattern with optional header section
|
||||
// BDS LogoRectangleGrid Component Styles
|
||||
// Brand Design System - Logo grid pattern with rectangle tiles
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-logo-square-grid - Base component
|
||||
// .bds-logo-square-grid--gray - Gray variant (maps to TileLogo 'neutral')
|
||||
// .bds-logo-square-grid--green - Green variant (maps to TileLogo 'green')
|
||||
// .bds-logo-square-grid__header - Header section container
|
||||
// .bds-logo-square-grid__text - Text content container
|
||||
// .bds-logo-square-grid__heading - Heading element
|
||||
// .bds-logo-square-grid__description - Description element
|
||||
// .bds-logo-rectangle-grid - Base component
|
||||
// .bds-logo-rectangle-grid--gray - Gray variant (maps to TileLogo 'neutral')
|
||||
// .bds-logo-rectangle-grid--green - Green variant (maps to TileLogo 'green')
|
||||
// .bds-logo-rectangle-grid__header - Header section container
|
||||
// .bds-logo-rectangle-grid__text - Text content container
|
||||
//
|
||||
// Note: Individual logo tiles are rendered using the TileLogo component
|
||||
// Note: Button layout is handled by the ButtonGroup component
|
||||
// Note: Individual logo tiles are rendered using the TileLogo component with shape="rectangle"
|
||||
// Note: Alignment is handled dynamically by PageGridCol offset prop
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
// Note: Color variants are now handled by the TileLogo component
|
||||
// LogoSquareGrid 'gray' maps to TileLogo 'neutral'
|
||||
// LogoSquareGrid 'green' maps to TileLogo 'green'
|
||||
// LogoRectangleGrid 'gray' maps to TileLogo 'neutral'
|
||||
// LogoRectangleGrid 'green' maps to TileLogo 'green'
|
||||
|
||||
// Spacing tokens - responsive
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
// Mobile (<768px)
|
||||
$bds-lsg-header-gap-mobile: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-lsg-text-gap-mobile: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-lrg-header-gap-mobile: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-lrg-text-gap-mobile: $bds-space-sm; // 8px - spacing('sm')
|
||||
|
||||
// Tablet (768px-1023px)
|
||||
$bds-lsg-header-gap-tablet: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-lrg-header-gap-tablet: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
|
||||
// Desktop (≥1024px)
|
||||
$bds-lsg-header-gap-desktop: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
$bds-lsg-text-gap-desktop: $bds-space-lg; // 16px - spacing('lg')
|
||||
$bds-lrg-header-gap-desktop: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
$bds-lrg-text-gap-desktop: $bds-space-lg; // 16px - spacing('lg')
|
||||
|
||||
// =============================================================================
|
||||
// Base Component Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-logo-square-grid {
|
||||
.bds-logo-rectangle-grid {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
@extend .w-100;
|
||||
|
||||
// Mobile-first gap
|
||||
gap: $bds-lrg-header-gap-mobile;
|
||||
|
||||
// Tablet breakpoint
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-lrg-header-gap-tablet;
|
||||
}
|
||||
|
||||
// Desktop breakpoint
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-lrg-header-gap-desktop;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Section
|
||||
// =============================================================================
|
||||
|
||||
.bds-logo-square-grid__header {
|
||||
.bds-logo-rectangle-grid__header {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
margin-top: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
margin-bottom: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
|
||||
// Mobile-first gap
|
||||
gap: $bds-lsg-header-gap-mobile;
|
||||
gap: $bds-lrg-header-gap-mobile;
|
||||
|
||||
// Tablet breakpoint
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-lsg-header-gap-tablet;
|
||||
gap: $bds-lrg-header-gap-tablet;
|
||||
margin-top: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
margin-bottom: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
}
|
||||
|
||||
// Desktop breakpoint
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-lsg-header-gap-desktop;
|
||||
gap: $bds-lrg-header-gap-desktop;
|
||||
margin-top: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
margin-bottom: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
}
|
||||
@@ -75,29 +86,25 @@ $bds-lsg-text-gap-desktop: $bds-space-lg; // 16px - spacing('lg')
|
||||
// Text Content
|
||||
// =============================================================================
|
||||
|
||||
.bds-logo-square-grid__text {
|
||||
.bds-logo-rectangle-grid__text {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
|
||||
// Mobile-first gap
|
||||
gap: $bds-lsg-text-gap-mobile;
|
||||
gap: $bds-lrg-text-gap-mobile;
|
||||
|
||||
// Desktop breakpoint
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-lsg-text-gap-desktop;
|
||||
gap: $bds-lrg-text-gap-desktop;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Action Buttons
|
||||
// =============================================================================
|
||||
// Note: Button layout is now handled by the ButtonGroup component
|
||||
|
||||
// =============================================================================
|
||||
// Logo Grid Row
|
||||
// =============================================================================
|
||||
// Note: Grid layout is now handled by PageGridRow/PageGridCol
|
||||
// Each tile uses PageGridCol with span={{ base: 2, lg: 3 }}
|
||||
// This gives us 2 columns on mobile (2/4) and 4 columns on desktop (3/12)
|
||||
// Each tile uses PageGridCol with span={{ base: 2, md: 2, lg: 3 }}
|
||||
// This gives us 2 columns on mobile (2/4), 3 columns on tablet (2/6),
|
||||
// and 4 columns on desktop (3/12)
|
||||
// Alignment is handled by offset prop based on logo count
|
||||
// Tile rendering and styling is handled by the TileLogo component
|
||||
119
shared/sections/LogoRectangleGrid/LogoRectangleGrid.tsx
Normal file
119
shared/sections/LogoRectangleGrid/LogoRectangleGrid.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
|
||||
import { TileLogo, TileLogoProps } from '../../components/TileLogo/TileLogo';
|
||||
import { calculateTileOffset } from 'shared/utils/helpers';
|
||||
|
||||
export interface LogoItem extends TileLogoProps {}
|
||||
|
||||
export interface LogoRectangleGridProps {
|
||||
/** Color variant - determines background color */
|
||||
variant?: 'gray' | 'green';
|
||||
/** Heading text (required) */
|
||||
heading: string;
|
||||
/** Optional description text */
|
||||
description?: string;
|
||||
/** Array of logo items to display in the grid */
|
||||
logos: LogoItem[];
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* LogoRectangleGrid Component
|
||||
*
|
||||
* A responsive grid pattern for displaying company/partner logos with rectangle tiles
|
||||
* and dynamic offset based on tile count. Features 9:5 aspect ratio rectangle tiles
|
||||
* with 2 color variants and dark mode support.
|
||||
*
|
||||
* Offset Logic (lg breakpoint only, applied to first tile):
|
||||
* - 1 tile: offset 9
|
||||
* - 2 tiles: offset 6
|
||||
* - 3 tiles: offset 3
|
||||
* - 4 tiles: offset 9
|
||||
* - 5 tiles: offset 6
|
||||
* - 6 tiles: offset 3
|
||||
* - 7 tiles: offset 9
|
||||
* - 8 tiles: offset 6
|
||||
* - 9 tiles: offset 3
|
||||
* - 10+ tiles: no offset
|
||||
*
|
||||
* @example
|
||||
* // Basic usage with gray variant
|
||||
* <LogoRectangleGrid
|
||||
* variant="gray"
|
||||
* heading="Developer tools & APIs"
|
||||
* description="Streamline development with comprehensive tools."
|
||||
* logos={[
|
||||
* { src: "/logos/company1.svg", alt: "Company 1" },
|
||||
* { src: "/logos/company2.svg", alt: "Company 2" }
|
||||
* ]}
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // With clickable logos
|
||||
* <LogoRectangleGrid
|
||||
* variant="green"
|
||||
* heading="Our Partners"
|
||||
* description="Leading companies building on XRPL."
|
||||
* logos={[
|
||||
* { src: "/logos/partner1.svg", alt: "Partner 1", href: "https://partner1.com" }
|
||||
* ]}
|
||||
* />
|
||||
*/
|
||||
export const LogoRectangleGrid: React.FC<LogoRectangleGridProps> = ({
|
||||
variant = 'gray',
|
||||
heading,
|
||||
description,
|
||||
logos,
|
||||
className,
|
||||
}) => {
|
||||
// Build class names using BEM with bds namespace
|
||||
const classNames = clsx(
|
||||
'bds-logo-rectangle-grid',
|
||||
`bds-logo-rectangle-grid--${variant}`,
|
||||
);
|
||||
|
||||
// Memoize offset calculations - only recalculate when logos array changes
|
||||
const logoOffsets = useMemo(() => {
|
||||
const total = logos.length;
|
||||
return logos.map((_, index) => calculateTileOffset(index, total));
|
||||
}, [logos]);
|
||||
|
||||
return (
|
||||
<PageGrid className={classNames}>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 6, lg: 8 }}>
|
||||
{/* Header Section */}
|
||||
<div className="bds-logo-rectangle-grid__header">
|
||||
<div className="bds-logo-rectangle-grid__text">
|
||||
<h4 className="h-md mb-0">{heading}</h4>
|
||||
{description && <p className="body-l mb-0">{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
{logos.map((logo, index) => {
|
||||
const offset = logoOffsets[index];
|
||||
const hasOffset = offset.md > 0 || offset.lg > 0;
|
||||
return (
|
||||
<PageGridCol
|
||||
key={index}
|
||||
span={{ base: 2, md: 2, lg: 3 }}
|
||||
offset={hasOffset ? { md: offset.md, lg: offset.lg } : undefined}
|
||||
>
|
||||
<TileLogo
|
||||
shape="rectangle"
|
||||
variant={variant === 'gray' ? 'neutral' : 'green'}
|
||||
{...logo}
|
||||
/>
|
||||
</PageGridCol>
|
||||
);
|
||||
})}
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogoRectangleGrid;
|
||||
424
shared/sections/LogoRectangleGrid/README.md
Normal file
424
shared/sections/LogoRectangleGrid/README.md
Normal file
@@ -0,0 +1,424 @@
|
||||
# LogoRectangleGrid Component
|
||||
|
||||
A responsive grid pattern for displaying company/partner logos with rectangle tiles and dynamic alignment based on tile count. Built on top of the TileLogo component, featuring 9:5 aspect ratio rectangle tiles with 2 color variants and full dark mode support.
|
||||
|
||||
## Features
|
||||
|
||||
- **2 Color Variants**: Gray and Green backgrounds
|
||||
- **Dynamic Alignment**: Grid alignment changes based on logo count (1-3: right, 4: left, 5-9: right, 9+: left)
|
||||
- **Responsive Grid**: Automatically adapts from 2 columns (mobile) to 3 columns (tablet) to 4 columns (desktop)
|
||||
- **Required Header**: Heading is required, description is optional
|
||||
- **Clickable Logos**: Support for optional links on individual logos
|
||||
- **Dark Mode Support**: Full light and dark mode compatibility
|
||||
- **Rectangle Tiles**: Maintains 9:5 aspect ratio at all breakpoints
|
||||
- **Grid Integration**: Built-in PageGrid wrapper with standard container support
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
The component automatically adapts its grid layout based on viewport width:
|
||||
|
||||
| Breakpoint | Columns | Gap | Tile Aspect Ratio |
|
||||
|------------|---------|-----|-------------------|
|
||||
| Mobile (< 768px) | 2 | 8px | 9:5 |
|
||||
| Tablet (768px - 1023px) | 3 | 8px | 9:5 |
|
||||
| Desktop (≥ 1024px) | 4 | 8px | 9:5 |
|
||||
|
||||
## Dynamic Alignment
|
||||
|
||||
The grid alignment changes based on the number of logos:
|
||||
|
||||
| Logo Count | Alignment | Offset |
|
||||
|------------|-----------|--------|
|
||||
| 1-3 | Right | 4 columns |
|
||||
| 4 | Left | 0 columns |
|
||||
| 5-9 | Right | 4 columns |
|
||||
| 9+ | Left | 0 columns |
|
||||
|
||||
This creates a visually balanced layout that adapts to different content volumes.
|
||||
|
||||
## Color Variants
|
||||
|
||||
The LogoRectangleGrid pattern uses two color variants that map directly to TileLogo component variants:
|
||||
|
||||
| LogoRectangleGrid Variant | TileLogo Variant | Description |
|
||||
|---------------------------|------------------|-------------|
|
||||
| `gray` | `neutral` | Subtle, professional appearance for general partner showcases |
|
||||
| `green` | `green` | Highlights featured or primary partners |
|
||||
|
||||
Colors are managed by the TileLogo component and automatically adapt between light and dark modes with proper hover states and animations.
|
||||
|
||||
## Props API
|
||||
|
||||
```typescript
|
||||
interface LogoItem {
|
||||
/** Logo image source URL */
|
||||
logo: string;
|
||||
/** Alt text for the logo image */
|
||||
alt: string;
|
||||
/** Optional link URL - makes the logo clickable */
|
||||
href?: string;
|
||||
/** Optional click handler - makes the logo a button */
|
||||
onClick?: () => void;
|
||||
/** Disabled state */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface LogoRectangleGridProps {
|
||||
/** Color variant - determines background color */
|
||||
variant?: 'gray' | 'green';
|
||||
/** Heading text (required) */
|
||||
heading: string;
|
||||
/** Optional description text */
|
||||
description?: string;
|
||||
/** Array of logo items to display in the grid */
|
||||
logos: LogoItem[];
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: `LogoItem` extends `TileLogoProps` from the TileLogo component. The `variant` property is NOT included in individual logo items - it's controlled at the component level via the `LogoRectangleGridProps.variant` prop, which applies to all tiles in the grid.
|
||||
|
||||
### Default Values
|
||||
|
||||
- `variant`: `'gray'`
|
||||
- `description`: `undefined`
|
||||
- `className`: `''`
|
||||
|
||||
### Required Props
|
||||
|
||||
- `heading`: Heading text (required)
|
||||
- `logos`: Array of logo items (required)
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage with Gray Variant
|
||||
|
||||
```tsx
|
||||
import { LogoRectangleGrid } from 'shared/sections/LogoRectangleGrid';
|
||||
|
||||
<LogoRectangleGrid
|
||||
variant="gray"
|
||||
heading="Developer tools & APIs"
|
||||
logos={[
|
||||
{ logo: "/img/logos/company1.svg", alt: "Company 1" },
|
||||
{ logo: "/img/logos/company2.svg", alt: "Company 2" },
|
||||
{ logo: "/img/logos/company3.svg", alt: "Company 3" },
|
||||
{ logo: "/img/logos/company4.svg", alt: "Company 4" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### With Description
|
||||
|
||||
```tsx
|
||||
<LogoRectangleGrid
|
||||
variant="green"
|
||||
heading="Developer tools & APIs"
|
||||
description="Streamline development and build powerful RWA tokenization solutions with XRP Ledger's comprehensive developer toolset."
|
||||
logos={[
|
||||
{ logo: "/img/logos/tool1.svg", alt: "Tool 1" },
|
||||
{ logo: "/img/logos/tool2.svg", alt: "Tool 2" },
|
||||
{ logo: "/img/logos/tool3.svg", alt: "Tool 3" },
|
||||
{ logo: "/img/logos/tool4.svg", alt: "Tool 4" },
|
||||
{ logo: "/img/logos/tool5.svg", alt: "Tool 5" },
|
||||
{ logo: "/img/logos/tool6.svg", alt: "Tool 6" },
|
||||
{ logo: "/img/logos/tool7.svg", alt: "Tool 7" },
|
||||
{ logo: "/img/logos/tool8.svg", alt: "Tool 8" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### With Clickable Logos
|
||||
|
||||
```tsx
|
||||
<LogoRectangleGrid
|
||||
variant="gray"
|
||||
heading="Our Partners"
|
||||
description="Leading companies building on XRPL."
|
||||
logos={[
|
||||
{
|
||||
logo: "/img/logos/partner1.svg",
|
||||
alt: "Partner 1",
|
||||
href: "https://partner1.com"
|
||||
},
|
||||
{
|
||||
logo: "/img/logos/partner2.svg",
|
||||
alt: "Partner 2",
|
||||
href: "https://partner2.com"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### With Button Handlers
|
||||
|
||||
```tsx
|
||||
<LogoRectangleGrid
|
||||
variant="green"
|
||||
heading="Interactive Partners"
|
||||
description="Click any logo to learn more."
|
||||
logos={[
|
||||
{
|
||||
logo: "/img/logos/partner1.svg",
|
||||
alt: "Partner 1",
|
||||
onClick: () => openModal('partner1')
|
||||
},
|
||||
{
|
||||
logo: "/img/logos/partner2.svg",
|
||||
alt: "Partner 2",
|
||||
onClick: () => openModal('partner2')
|
||||
}
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### With Disabled State
|
||||
|
||||
```tsx
|
||||
<LogoRectangleGrid
|
||||
variant="gray"
|
||||
heading="Coming Soon"
|
||||
description="New partners joining the ecosystem."
|
||||
logos={[
|
||||
{
|
||||
logo: "/img/logos/partner1.svg",
|
||||
alt: "Partner 1",
|
||||
href: "/partners/partner1"
|
||||
},
|
||||
{
|
||||
logo: "/img/logos/coming-soon.svg",
|
||||
alt: "Coming Soon",
|
||||
disabled: true
|
||||
}
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### Heading Only (No Description)
|
||||
|
||||
```tsx
|
||||
<LogoRectangleGrid
|
||||
variant="gray"
|
||||
heading="Ecosystem Members"
|
||||
logos={[
|
||||
{ logo: "/img/logos/sponsor1.svg", alt: "Sponsor 1" },
|
||||
{ logo: "/img/logos/sponsor2.svg", alt: "Sponsor 2" },
|
||||
{ logo: "/img/logos/sponsor3.svg", alt: "Sponsor 3" },
|
||||
{ logo: "/img/logos/sponsor4.svg", alt: "Sponsor 4" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### Demonstrating Alignment Logic
|
||||
|
||||
```tsx
|
||||
// 1-3 logos: Right-aligned
|
||||
<LogoRectangleGrid
|
||||
variant="green"
|
||||
heading="Featured Partners"
|
||||
logos={[
|
||||
{ logo: "/img/logos/partner1.svg", alt: "Partner 1" },
|
||||
{ logo: "/img/logos/partner2.svg", alt: "Partner 2" }
|
||||
]}
|
||||
/>
|
||||
|
||||
// 4 logos: Left-aligned
|
||||
<LogoRectangleGrid
|
||||
variant="gray"
|
||||
heading="Core Technologies"
|
||||
logos={[
|
||||
{ logo: "/img/logos/tech1.svg", alt: "Tech 1" },
|
||||
{ logo: "/img/logos/tech2.svg", alt: "Tech 2" },
|
||||
{ logo: "/img/logos/tech3.svg", alt: "Tech 3" },
|
||||
{ logo: "/img/logos/tech4.svg", alt: "Tech 4" }
|
||||
]}
|
||||
/>
|
||||
|
||||
// 5-9 logos: Right-aligned
|
||||
<LogoRectangleGrid
|
||||
variant="green"
|
||||
heading="Developer Tools"
|
||||
logos={[
|
||||
{ logo: "/img/logos/tool1.svg", alt: "Tool 1" },
|
||||
{ logo: "/img/logos/tool2.svg", alt: "Tool 2" },
|
||||
{ logo: "/img/logos/tool3.svg", alt: "Tool 3" },
|
||||
{ logo: "/img/logos/tool4.svg", alt: "Tool 4" },
|
||||
{ logo: "/img/logos/tool5.svg", alt: "Tool 5" },
|
||||
{ logo: "/img/logos/tool6.svg", alt: "Tool 6" }
|
||||
]}
|
||||
/>
|
||||
|
||||
// 9+ logos: Left-aligned
|
||||
<LogoRectangleGrid
|
||||
variant="gray"
|
||||
heading="Partner Ecosystem"
|
||||
logos={[
|
||||
// ... 12 logos
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Important Implementation Details
|
||||
|
||||
### Logo Image Requirements
|
||||
|
||||
For best results, logo images should:
|
||||
- Be SVG format for crisp scaling
|
||||
- Have transparent backgrounds
|
||||
- Be reasonably sized (width: 150-250px recommended for 9:5 aspect ratio)
|
||||
- Use monochrome or simple color schemes
|
||||
- Have consistent visual weight across all logos
|
||||
|
||||
### Grid Behavior
|
||||
|
||||
- The grid uses PageGridCol components for responsive layout
|
||||
- Each tile uses `span={{ base: 2, md: 2, lg: 3 }}` (2 cols on mobile out of 4, 2 cols on tablet out of 6, 3 cols on desktop out of 12)
|
||||
- This creates 2 columns on mobile, 3 columns on tablet, and 4 columns on desktop
|
||||
- Tiles maintain a 9:5 aspect ratio using the TileLogo rectangle shape
|
||||
- Gaps between tiles are handled by PageGrid's built-in gutter system
|
||||
- Grid automatically wraps to new rows as needed
|
||||
- Grid alignment changes dynamically based on logo count
|
||||
|
||||
### Alignment Logic Implementation
|
||||
|
||||
The alignment is controlled by the `alignRight` variable:
|
||||
|
||||
```typescript
|
||||
const logoCount = logos.length;
|
||||
const alignRight =
|
||||
(logoCount >= 1 && logoCount <= 3) ||
|
||||
(logoCount >= 5 && logoCount <= 9);
|
||||
```
|
||||
|
||||
When `alignRight` is true:
|
||||
- Grid container spans 8 columns on desktop (lg breakpoint)
|
||||
- Grid container has 4-column offset on desktop (pushes content right)
|
||||
|
||||
When `alignRight` is false:
|
||||
- Grid container spans full width
|
||||
- Grid container has 0 offset (content aligns left)
|
||||
|
||||
### Clickable Logo Behavior
|
||||
|
||||
Logo tiles leverage the TileLogo component's interactive capabilities:
|
||||
- **With `href` property**: Renders as a link (`<a>` tag) with window shade hover animation
|
||||
- **With `onClick` property**: Renders as a button with the same interactive states
|
||||
- **With `disabled` property**: Prevents interaction and applies disabled styling
|
||||
- **Interactive states**: Default, Hover, Focused, Pressed, and Disabled
|
||||
- **Animation**: Window shade effect that wipes from bottom to top on hover
|
||||
- All tiles automatically maintain focus states for keyboard accessibility
|
||||
|
||||
## Styling
|
||||
|
||||
### BEM Class Structure
|
||||
|
||||
```scss
|
||||
.bds-logo-rectangle-grid // Base component
|
||||
.bds-logo-rectangle-grid--gray // Gray variant (maps to TileLogo 'neutral')
|
||||
.bds-logo-rectangle-grid--green // Green variant (maps to TileLogo 'green')
|
||||
.bds-logo-rectangle-grid__header // Header section container
|
||||
.bds-logo-rectangle-grid__text // Text content container
|
||||
```
|
||||
|
||||
**Note**: Individual logo tiles are rendered using the TileLogo component with its own BEM structure (`bds-tile-logo`) and `shape="rectangle"`. Grid layout is handled by PageGridRow and PageGridCol components.
|
||||
|
||||
### Typography Tokens
|
||||
|
||||
- **Heading**: Uses `heading-md` type token (Tobias Light font)
|
||||
- Desktop: 40px / 46px line-height / -1px letter-spacing
|
||||
- Tablet: 36px / 45px line-height / -0.5px letter-spacing
|
||||
- Mobile: 32px / 40px line-height / 0px letter-spacing
|
||||
|
||||
- **Description**: Uses `body-l` type token (Booton Light font)
|
||||
- Desktop: 18px / 26.1px line-height / -0.5px letter-spacing
|
||||
- Tablet: 18px / 26.1px line-height / -0.5px letter-spacing
|
||||
- Mobile: 18px / 26.1px line-height / -0.5px letter-spacing
|
||||
|
||||
### Color Tokens
|
||||
|
||||
All colors are sourced from `styles/_colors.scss`:
|
||||
|
||||
```scss
|
||||
// Tile backgrounds
|
||||
$gray-200 // Gray variant (light mode)
|
||||
$gray-700 // Gray variant (dark mode)
|
||||
$green-200 // Green variant (light mode)
|
||||
$green-300 // Green variant (dark mode)
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Semantic HTML structure with proper heading hierarchy
|
||||
- All logos include descriptive alt text
|
||||
- Clickable logos have proper link semantics
|
||||
- Keyboard navigation support with visible focus states
|
||||
- Color contrast meets WCAG AA standards in all variants
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use Each Variant
|
||||
|
||||
- **Gray**: General-purpose logo grids, subtle integration
|
||||
- **Green**: Featured partnerships, brand-focused sections
|
||||
|
||||
### Content Guidelines
|
||||
|
||||
- **Heading**: Required, keep concise (1-2 lines preferred), use sentence case
|
||||
- **Description**: Optional, provide context (2-3 lines max), complete sentences
|
||||
- **Logo Count**: Consider the alignment logic when choosing how many logos to display
|
||||
- **Alt Text**: Use company/product names, not generic "logo"
|
||||
|
||||
### Logo Preparation
|
||||
|
||||
1. **Consistent Sizing**: Ensure all logos have similar visual weight
|
||||
2. **Format**: Use SVG for scalability and crisp rendering
|
||||
3. **Background**: Transparent backgrounds work best
|
||||
4. **Aspect Ratio**: Rectangle tiles work well with horizontal logos
|
||||
5. **Padding**: Include minimal internal padding in the SVG itself
|
||||
|
||||
### Performance
|
||||
|
||||
- Use optimized SVG files (run through SVGO or similar)
|
||||
- Consider lazy loading for grids with many logos
|
||||
- Provide appropriate alt text for all images
|
||||
- Use `width` and `height` attributes on img tags when possible
|
||||
|
||||
### Technical Implementation
|
||||
|
||||
- **Grid System**: Uses PageGridCol with `span={{ base: 2, md: 2, lg: 3 }}` for responsive layout
|
||||
- **Tile Rendering**: Leverages TileLogo component with `shape="rectangle"` for all logo tiles
|
||||
- **Variant Mapping**: LogoRectangleGrid 'gray' → TileLogo 'neutral', LogoRectangleGrid 'green' → TileLogo 'green'
|
||||
- **Interactive States**: TileLogo handles href (links), onClick (buttons), and disabled states
|
||||
- **Aspect Ratio**: Rectangle tiles maintained by TileLogo with CSS `aspect-ratio: 9/5`
|
||||
- **Animations**: Window shade hover effect managed by TileLogo component
|
||||
- **Dynamic Alignment**: Grid alignment controlled by conditional offset based on logo count
|
||||
|
||||
## Files
|
||||
|
||||
- `LogoRectangleGrid.tsx` - Component implementation
|
||||
- `LogoRectangleGrid.scss` - Styles with color variants and responsive breakpoints
|
||||
- `index.ts` - Barrel exports
|
||||
- `README.md` - This documentation
|
||||
|
||||
## Related Components
|
||||
|
||||
- **TileLogo**: Core component used to render individual logo tiles with interactive states and rectangle shape
|
||||
- **LogoSquareGrid**: Similar pattern but with square tiles instead of rectangle tiles
|
||||
- **PageGrid**: Used internally for responsive grid structure and standard container support
|
||||
|
||||
## Design References
|
||||
|
||||
- **Figma Design**: [Section Logo - Rectangle Grid](https://www.figma.com/design/gaTsImoTRsiRXAGzbGKcCd/Section-Logo---Rectangle-Grid?node-id=1-2)
|
||||
- **Showcase Page**: `/about/logo-rectangle-grid-showcase.page.tsx`
|
||||
- **Component Location**: `shared/sections/LogoRectangleGrid/`
|
||||
|
||||
## Version History
|
||||
|
||||
- **January 2026**: Initial implementation
|
||||
- Figma design alignment with 2 color variants
|
||||
- Responsive grid with 2/3/4 column layout
|
||||
- Dynamic alignment based on logo count
|
||||
- Required header section with optional description
|
||||
- Clickable logo support
|
||||
- Rectangle tiles with 9:5 aspect ratio
|
||||
2
shared/sections/LogoRectangleGrid/index.ts
Normal file
2
shared/sections/LogoRectangleGrid/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { LogoRectangleGrid } from './LogoRectangleGrid';
|
||||
export type { LogoRectangleGridProps, LogoItem } from './LogoRectangleGrid';
|
||||
52
shared/sections/LogoSquareGrid/LogoSquareGrid.scss
Normal file
52
shared/sections/LogoSquareGrid/LogoSquareGrid.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
// BDS LogoSquareGrid Component Styles
|
||||
// Brand Design System - Logo grid pattern with optional header section
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-logo-square-grid - Base component
|
||||
// .bds-logo-square-grid--gray - Gray variant (maps to TileLogo 'neutral')
|
||||
// .bds-logo-square-grid--green - Green variant (maps to TileLogo 'green')
|
||||
// .bds-logo-square-grid__section-header - SectionHeader with optional top margin
|
||||
//
|
||||
// Note: Individual logo tiles are rendered using the TileLogo component
|
||||
// Note: Button layout is handled by the ButtonGroup component
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
// Note: Color variants are now handled by the TileLogo component
|
||||
// LogoSquareGrid 'gray' maps to TileLogo 'neutral'
|
||||
// LogoSquareGrid 'green' maps to TileLogo 'green'
|
||||
|
||||
// =============================================================================
|
||||
// Base Component Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-logo-square-grid {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
@extend .w-100;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Section (uses SectionHeader component)
|
||||
// =============================================================================
|
||||
// LogoSquareGrid needs additional top margin for the header row
|
||||
.bds-logo-square-grid__section-header {
|
||||
margin-top: $bds-space-2xl;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-top: $bds-space-3xl;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-top: $bds-space-4xl;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Logo Grid Row
|
||||
// =============================================================================
|
||||
// Note: Grid layout is now handled by PageGridRow/PageGridCol
|
||||
// Each tile uses PageGridCol with span={{ base: 2, lg: 3 }}
|
||||
// This gives us 2 columns on mobile (2/4) and 4 columns on desktop (3/12)
|
||||
// Tile rendering and styling is handled by the TileLogo component
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
|
||||
import { TileLogo, TileLogoProps } from '../../components/TileLogo/TileLogo';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
import { SectionHeader } from 'shared/patterns/SectionHeader';
|
||||
import { TileLogo, TileLogoProps } from 'shared/components/TileLogo/TileLogo';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from 'shared/patterns/ButtonGroup/ButtonGroup';
|
||||
|
||||
export interface LogoItem extends TileLogoProps {}
|
||||
|
||||
@@ -73,36 +74,23 @@ export const LogoSquareGrid: React.FC<LogoSquareGridProps> = ({
|
||||
className
|
||||
);
|
||||
|
||||
// Determine if we should show the header section
|
||||
const hasHeader = !!(heading || description || hasButtons);
|
||||
|
||||
return (
|
||||
<PageGrid className={classNames}>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 6, lg: 8 }}>
|
||||
{/* Header Section */}
|
||||
{hasHeader && (
|
||||
<div className="bds-logo-square-grid__header">
|
||||
{/* Text Content */}
|
||||
{(heading || description) && (
|
||||
<div className="bds-logo-square-grid__text">
|
||||
{heading && <h4 className="h-md mb-0">{heading}</h4>}
|
||||
{description && <p className="body-l mb-0">{description}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
{hasButtons && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color="green"
|
||||
gap="small"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
<SectionHeader
|
||||
heading={heading}
|
||||
description={description}
|
||||
as="h4"
|
||||
span={{ base: 4, md: 6, lg: 8 }}
|
||||
className="bds-logo-square-grid__section-header"
|
||||
>
|
||||
{hasButtons && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color="green"
|
||||
gap="small"
|
||||
/>
|
||||
)}
|
||||
</SectionHeader>
|
||||
<PageGridRow>
|
||||
{logos.map((logo, index) => (
|
||||
<PageGridCol key={index} span={{ base: 2, lg: 3 }}>
|
||||
@@ -95,7 +95,7 @@ interface LogoSquareGridProps {
|
||||
### Basic Usage with Gray Variant
|
||||
|
||||
```tsx
|
||||
import { LogoSquareGrid } from 'shared/patterns/LogoSquareGrid';
|
||||
import { LogoSquareGrid } from 'shared/sections/LogoSquareGrid';
|
||||
|
||||
<LogoSquareGrid
|
||||
variant="gray"
|
||||
@@ -395,7 +395,7 @@ $green-300 // Green variant (dark mode)
|
||||
|
||||
- **Figma Design**: [Pattern Logo - Square Grid](https://www.figma.com/design/ThBcoYLNKsBGw3r9g1L6Z8/Pattern-Logo---Square-Grid?node-id=1-2)
|
||||
- **Showcase Page**: `/about/logo-square-grid-showcase.page.tsx`
|
||||
- **Component Location**: `shared/patterns/LogoSquareGrid/`
|
||||
- **Component Location**: `shared/sections/LogoSquareGrid/`
|
||||
|
||||
## Version History
|
||||
|
||||
@@ -15,7 +15,7 @@ The StandardCardGroupSection component provides a structured section with:
|
||||
## Basic Usage
|
||||
|
||||
```tsx
|
||||
import StandardCardGroupSection from "shared/patterns/StandardCardGroupSection/StandardCardGroupSection";
|
||||
import StandardCardGroupSection from "shared/sections/StandardCardGroupSection/StandardCardGroupSection";
|
||||
|
||||
function MyPage() {
|
||||
return (
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import { PageGrid } from "../../components/PageGrid/page-grid";
|
||||
import { SectionHeader } from 'shared/patterns/SectionHeader';
|
||||
import StandardCard, {
|
||||
StandardCardPropsWithoutVariant,
|
||||
StandardCardVariant,
|
||||
@@ -79,29 +80,7 @@ export const StandardCardGroupSection = React.forwardRef<
|
||||
{...rest}
|
||||
>
|
||||
<PageGrid>
|
||||
{/* Header content row */}
|
||||
<PageGrid.Row>
|
||||
<PageGrid.Col
|
||||
span={{
|
||||
base: "fill",
|
||||
md: 6,
|
||||
lg: 8,
|
||||
}}
|
||||
>
|
||||
<div className="bds-standard-card-group-section__header">
|
||||
{headline && (
|
||||
<h2 className="bds-standard-card-group-section__headline h-md">
|
||||
{headline}
|
||||
</h2>
|
||||
)}
|
||||
{description && (
|
||||
<div className="bds-standard-card-group-section__description body-l">
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
<SectionHeader heading={headline} description={description} />
|
||||
|
||||
{/* Cards grid row */}
|
||||
<PageGrid.Row
|
||||
@@ -3,9 +3,6 @@
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-standard-card-group-section - Base section container
|
||||
// .bds-standard-card-group-section__header - Header wrapper for headline and description
|
||||
// .bds-standard-card-group-section__headline - Section headline (uses .h-md)
|
||||
// .bds-standard-card-group-section__description - Section description (uses .body-l)
|
||||
//
|
||||
// Design tokens from Figma:
|
||||
// Light Mode:
|
||||
@@ -25,16 +22,6 @@
|
||||
// =============================================================================
|
||||
// Note: Uses centralized spacing tokens from _spacing.scss.
|
||||
|
||||
// Spacing - Header gap (between headline and description)
|
||||
$bds-standard-card-group-section-header-gap-sm: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-standard-card-group-section-header-gap-md: $bds-space-sm; // 8px - spacing('sm')
|
||||
$bds-standard-card-group-section-header-gap-lg: $bds-space-lg; // 16px - spacing('lg')
|
||||
|
||||
// Spacing - Section gap (between header and cards)
|
||||
$bds-standard-card-group-section-section-gap-sm: $bds-space-2xl; // 24px - spacing('2xl')
|
||||
$bds-standard-card-group-section-section-gap-md: $bds-space-3xl; // 32px - spacing('3xl')
|
||||
$bds-standard-card-group-section-section-gap-lg: $bds-space-4xl; // 40px - spacing('4xl')
|
||||
|
||||
// Spacing - Section padding (vertical)
|
||||
$bds-standard-card-group-section-padding-y-sm: $bds-space-5xl; // 48px - spacing('5xl')
|
||||
$bds-standard-card-group-section-padding-y-md: $bds-space-6xl; // 64px - spacing('6xl')
|
||||
@@ -69,43 +56,8 @@ $bds-standard-card-group-section-padding-y-lg: $bds-space-8xl; // 80px - spaci
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Section
|
||||
// =============================================================================
|
||||
|
||||
.bds-standard-card-group-section__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-standard-card-group-section-header-gap-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-standard-card-group-section-header-gap-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-standard-card-group-section-header-gap-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-standard-card-group-section__headline {
|
||||
margin: 0;
|
||||
// Typography handled by .h-md class from _font.scss
|
||||
}
|
||||
|
||||
.bds-standard-card-group-section__description {
|
||||
margin: 0;
|
||||
// Typography handled by .body-l class from _font.scss
|
||||
}
|
||||
|
||||
// Header section uses SectionHeader component
|
||||
|
||||
.bds-standard-card-group-section__cards {
|
||||
margin-top: $bds-standard-card-group-section-section-gap-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-top: $bds-standard-card-group-section-section-gap-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-top: $bds-standard-card-group-section-section-gap-lg;
|
||||
}
|
||||
// Section gap handled by SectionHeader margin-bottom
|
||||
}
|
||||
@@ -26,6 +26,16 @@ export type DesignConstrainedCallToActionsProps = {
|
||||
callsToAction?: DesignConstrainedCallsToActions;
|
||||
};
|
||||
|
||||
/** Link config for ButtonGroup - consistent with ButtonConfig (label + href) */
|
||||
export interface DesignConstrainedLink {
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export type DesignConstrainedLinksProps = {
|
||||
links?: DesignConstrainedLink[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Base props that all media elements must have to ensure proper styling.
|
||||
* These props are automatically applied to maintain the 9:16 aspect ratio
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
import { CalloutMediaBanner } from "shared/patterns/CalloutMediaBanner";
|
||||
import { CalloutMediaBanner } from "shared/sections/CalloutMediaBanner";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
@@ -39,6 +39,7 @@ export default function CalloutMediaBannerShowcase() {
|
||||
<CalloutMediaBanner
|
||||
variant="green"
|
||||
heading="The Compliant Ledger Protocol"
|
||||
headingAs="h1"
|
||||
subheading="A decentralized public Layer 1 blockchain for creating, transferring, and exchanging digital assets with a focus on compliance."
|
||||
buttons={[
|
||||
{ label: "Get Started", onClick: () => handleClick('responsive-demo-primary') },
|
||||
@@ -290,6 +291,7 @@ export default function CalloutMediaBannerShowcase() {
|
||||
backgroundImage={sampleLightBackgroundImage}
|
||||
textColor="black"
|
||||
heading="Build the Future of Finance"
|
||||
headingAs="h2"
|
||||
subheading="Create powerful decentralized applications with XRPL's fast, efficient, and sustainable blockchain technology."
|
||||
buttons={[
|
||||
{ label: "Start Building", onClick: () => handleClick('image-black-primary') },
|
||||
@@ -574,10 +576,18 @@ export default function CalloutMediaBannerShowcase() {
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>heading</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><em>required</em></div>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>undefined</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Main heading text</div>
|
||||
</div>
|
||||
|
||||
{/* headingAs */}
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>headingAs</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'</code></div>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>'h6'</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Heading element type - allows semantic HTML customization while maintaining visual styling</div>
|
||||
</div>
|
||||
|
||||
{/* subheading */}
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>subheading</code></div>
|
||||
@@ -620,7 +630,7 @@ export default function CalloutMediaBannerShowcase() {
|
||||
</div>
|
||||
<div>
|
||||
<strong>Component Location:</strong>{' '}
|
||||
<code>shared/patterns/CalloutMediaBanner/</code>
|
||||
<code>shared/sections/CalloutMediaBanner/</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Color Tokens:</strong>{' '}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
import { CardStats, CardStatsCardConfig } from "shared/patterns/CardStats";
|
||||
import { CardStats, CardStatsCardConfig } from "shared/sections/CardStatsList";
|
||||
import { Divider } from "shared/components/Divider";
|
||||
|
||||
export const frontmatter = {
|
||||
@@ -208,7 +208,7 @@ export default function CardStatsShowcase() {
|
||||
|
||||
<h5 className="mb-4">Basic Usage</h5>
|
||||
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`import { CardStats } from 'shared/patterns/CardStats';
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`import { CardStats } from 'shared/sections/CardStatsList';
|
||||
|
||||
<CardStats
|
||||
heading="Blockchain Trusted at Scale"
|
||||
@@ -258,7 +258,7 @@ export default function CardStatsShowcase() {
|
||||
</div>
|
||||
<div>
|
||||
<strong>Pattern Location:</strong>{' '}
|
||||
<code>shared/patterns/CardStats/</code>
|
||||
<code>shared/sections/CardStatsList/</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Component Used:</strong>{' '}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
import { CardsFeatured } from "shared/patterns/CardsFeatured";
|
||||
import { CardsFeatured } from "shared/sections/CardsFeatured";
|
||||
import { Divider } from "shared/components/Divider";
|
||||
|
||||
export const frontmatter = {
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { CardsIconGrid } from "shared/patterns/CardsIconGrid";
|
||||
import { CardsIconGrid } from "shared/sections/CardsIconGrid";
|
||||
import { Divider } from "shared/components/Divider";
|
||||
|
||||
export const frontmatter = {
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CardsTextGrid } from "shared/patterns/CardsTextGrid";
|
||||
import * as React from "react";
|
||||
import { CardsTextGrid } from "shared/sections/CardsTextGrid";
|
||||
import { Divider } from "shared/components/Divider";
|
||||
|
||||
export const frontmatter = {
|
||||
@@ -1,6 +1,5 @@
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
import { CardsTwoColumn } from "shared/patterns/CardsTwoColumn";
|
||||
import { TextCard } from "shared/patterns/CardsTwoColumn";
|
||||
import { CardsTwoColumn, TextCard } from "shared/sections/CardsTwoColumn";
|
||||
import { Divider } from "shared/components/Divider";
|
||||
|
||||
export const frontmatter = {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
import { CarouselCardList } from "shared/patterns/CarouselCardList";
|
||||
import { CarouselCardList } from "shared/sections/CarouselCardList";
|
||||
import { CarouselButton } from "shared/components/CarouselButton";
|
||||
import { Divider } from "shared/components/Divider";
|
||||
|
||||
@@ -615,7 +615,7 @@ export default function CarouselCardListShowcase() {
|
||||
<h2 className="h4 mb-6">Usage Example</h2>
|
||||
<div className="card p-4">
|
||||
<pre className="mb-0" style={{ backgroundColor: 'var(--bs-gray-800)', padding: '1rem', borderRadius: '4px', overflow: 'auto' }}>
|
||||
{`import { CarouselCardList } from 'shared/patterns/CarouselCardList';
|
||||
{`import { CarouselCardList } from 'shared/sections/CarouselCardList';
|
||||
|
||||
// Basic usage - button color matches card color by default
|
||||
<CarouselCardList
|
||||
@@ -669,11 +669,11 @@ export default function CarouselCardListShowcase() {
|
||||
</div>
|
||||
<div>
|
||||
<strong>Component Location:</strong>{' '}
|
||||
<code>shared/patterns/CarouselCardList/</code>
|
||||
<code>shared/sections/CarouselCardList/</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Documentation:</strong>{' '}
|
||||
<code>shared/patterns/CarouselCardList/CarouselCardList.md</code>
|
||||
<code>shared/sections/CarouselCardList/CarouselCardList.md</code>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user