Add CardsIconGrid and CardsTextGrid Section Patterns (#3505)

* CardsIconGrid and CardsTextGrid + showcase pages

* cleaning up code

* more code clean up

* using the spacing tokens for values
This commit is contained in:
Calvin
2026-02-18 16:19:01 -08:00
committed by Calvin Jhunjhuwala
parent c8bed8b698
commit fe1aa0ecb1
19 changed files with 1083 additions and 29929 deletions

View File

@@ -0,0 +1,130 @@
// BDS CardTextIconCard Pattern Styles
// Brand Design System - Card with icon, heading, and description
//
// Naming Convention: BEM with 'bds' namespace
// .bds-card-text-icon-card - Base card container
// .bds-card-text-icon-card__icon - Icon container
// .bds-card-text-icon-card__icon-img - Icon image
// .bds-card-text-icon-card__heading - Card heading
// .bds-card-text-icon-card__description - Card description (supports ReactNode/links)
//
// Built from Section Cards - Icon and Section Cards - Text Grid Figma designs
// =============================================================================
// Design Tokens
// =============================================================================
// Icon sizes (responsive)
$bds-card-text-icon-icon-size-sm: 32px;
$bds-card-text-icon-icon-size-md: 36px;
$bds-card-text-icon-icon-size-lg: 40px;
// Padding (uses BDS card presets from _spacing.scss)
$bds-card-text-icon-padding-sm: $bds-padding-card-sm;
$bds-card-text-icon-padding-md: $bds-padding-card-md;
$bds-card-text-icon-padding-lg: $bds-padding-card-lg;
// Gaps (between icon, heading, description)
$bds-card-text-icon-gap-sm: $bds-padding-card-sm;
$bds-card-text-icon-gap-md: $bds-padding-card-md;
$bds-card-text-icon-gap-lg: $bds-padding-card-lg;
// =============================================================================
// Card Component
// =============================================================================
.bds-card-text-icon-card {
display: flex !important;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
text-decoration: none;
box-sizing: border-box;
border-left: 1px solid $gray-300;
margin-bottom: $bds-space-2xl;
// When used standalone (not as grid column), take full width
&:not(.bds-card-text-icon-card--grid-col) {
width: 100%;
}
padding: $bds-card-text-icon-padding-sm;
gap: $bds-card-text-icon-gap-sm;
@include media-breakpoint-up(md) {
padding: $bds-card-text-icon-padding-md;
gap: $bds-card-text-icon-gap-md;
margin-bottom: $bds-space-3xl;
}
@include media-breakpoint-up(lg) {
padding: $bds-card-text-icon-padding-lg;
gap: $bds-card-text-icon-gap-lg;
margin-bottom: $bds-space-4xl;
}
// Aspect ratio foundation - when CSS variable is set via aspectRatio prop
&[style*="--bds-card-text-icon-aspect-ratio"] {
aspect-ratio: var(--bds-card-text-icon-aspect-ratio);
}
@include bds-theme-mode(dark) {
color: $white;
}
}
// =============================================================================
// Icon
// =============================================================================
.bds-card-text-icon-card__icon {
width: 100%;
display: flex;
flex-direction: column;
gap: $bds-space-lg;
&-img {
object-fit: contain;
display: block;
flex-shrink: 0;
width: $bds-card-text-icon-icon-size-sm;
height: $bds-card-text-icon-icon-size-sm;
@include media-breakpoint-up(md) {
width: $bds-card-text-icon-icon-size-md;
height: $bds-card-text-icon-icon-size-md;
}
@include media-breakpoint-up(lg) {
width: $bds-card-text-icon-icon-size-lg;
height: $bds-card-text-icon-icon-size-lg;
}
}
}
// =============================================================================
// Heading
// =============================================================================
.bds-card-text-icon-card__heading {
margin: 0;
color: inherit;
}
// =============================================================================
// Description (supports ReactNode - links, formatted text, etc.)
// =============================================================================
.bds-card-text-icon-card__description {
margin: 0;
color: inherit;
min-width: 0;
position: relative;
a {
color: inherit;
text-decoration: underline;
text-underline-offset: 2px;
&:hover {
text-decoration-thickness: 2px;
}
}
}

View File

@@ -0,0 +1,121 @@
import React from 'react';
import clsx from 'clsx';
import { PageGrid } from '../PageGrid/page-grid';
import type { ResponsiveValue, PageGridSpanValue } from '../PageGrid/page-grid';
export interface CardTextIconCardProps {
/** Icon image URL */
icon?: string;
/** Alt text for the icon image */
iconAlt?: string;
/** Card heading */
heading: string;
/** Card description; accepts rich content (e.g., text with inline links) */
description: React.ReactNode | string;
/** Optional aspect ratio for future use; applied via CSS variable */
aspectRatio?: number;
/** When provided, renders as PageGrid.Col as="li" with this span—card becomes the grid column */
gridColSpan?: ResponsiveValue<PageGridSpanValue>;
/** Additional CSS classes */
className?: string;
/** Optional height and width for the icon image */
height?: number;
width?: number;
}
/**
* CardTextIconCard Component
*
* A card component featuring an icon, heading, and description.
* Built from Section Cards - Icon and Section Cards - Text Grid Figma designs.
*
* The description accepts ReactNode so it can include hyperlinks and other rich content.
*
* @example
* // Basic usage
* <CardTextIconCard
* icon="/icons/docs.svg"
* iconAlt="Documentation"
* heading="Documentation"
* description="Access everything you need to get started with the XRPL."
* />
*
* @example
* // With inline link in description
* <CardTextIconCard
* icon="/icons/docs.svg"
* heading="Documentation"
* description={
* <>
* Learn more in our{' '}
* <a href="/docs">documentation</a>.
* </>
* }
* />
*/
const cardContent = (
heading: string,
description: React.ReactNode | string,
icon?: string,
iconAlt?: string,
iconHeight?: number,
iconWidth?: number
) => (
<>
<div className="bds-card-text-icon-card__icon">
{icon && (
<img
src={icon}
alt={iconAlt}
{...(iconHeight != null && { height: iconHeight })}
{...(iconWidth != null && { width: iconWidth })}
className="bds-card-text-icon-card__icon-img"
/>
)}
<strong className="bds-card-text-icon-card__heading sh-md-r">{heading}</strong>
</div>
<p className="bds-card-text-icon-card__description body-l">
{description}
</p>
</>
);
export const CardTextIconCard: React.FC<CardTextIconCardProps> = ({
icon,
iconAlt = '',
heading,
description,
aspectRatio,
gridColSpan,
className,
height,
width
}) => {
const style = aspectRatio
? ({ '--bds-card-text-icon-aspect-ratio': aspectRatio } as React.CSSProperties)
: undefined;
if (gridColSpan) {
return (
<PageGrid.Col
as="li"
span={gridColSpan}
className={clsx('bds-card-text-icon-card', 'bds-card-text-icon-card--grid-col', className)}
style={style}
>
{cardContent(heading, description, icon, iconAlt, height, width)}
</PageGrid.Col>
);
}
return (
<div
className={clsx('bds-card-text-icon-card', className)}
style={style}
>
{cardContent(heading, description, icon, iconAlt, height, width)}
</div>
);
};
export default CardTextIconCard;

View File

@@ -0,0 +1,116 @@
# CardTextIconCard Component
A card component featuring an icon, heading, and description. Built from Section Cards - Icon and Section Cards - Text Grid Figma designs.
## Overview
CardTextIconCard displays an icon at the top, followed by a heading and description. The description accepts `ReactNode`, so it can include hyperlinks and other rich content. No buttons; links are inline within the description.
## Features
- **Icon + Text Layout**: Icon container, heading, and description in a vertical stack (optional)
- **Rich Description**: `description` accepts `ReactNode` for inline links and formatted content
- **Aspect Ratio Foundation**: Optional `aspectRatio` prop for future responsive sizing
- **Light/Dark Mode**: Full theming support
- **Responsive Design**: Adaptive icon size and spacing across breakpoints
## Usage
### Basic Usage
```tsx
<CardTextIconCard
icon="/icons/docs.svg"
iconAlt="Documentation"
heading="Documentation"
description="Access everything you need to get started with the XRPL."
/>
```
### With Inline Link in Description
```tsx
<CardTextIconCard
icon="/icons/docs.svg"
heading="Documentation"
description={
<>
Learn more in our{' '}
<a href="/docs">documentation</a>.
</>
}
/>
```
### With Aspect Ratio
```tsx
<CardTextIconCard
icon="/icons/docs.svg"
heading="Documentation"
description="Access everything you need."
aspectRatio={4 / 3}
/>
```
## Props
### CardTextIconCardProps
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `icon` | `string` | Optional | Icon image URL |
| `iconAlt` | `string` | `''` | Optional | Alt text for the icon image |
| `heading` | `string` | Required | Card heading |
| `description` | `React.ReactNode` | Required | Card description; accepts rich content (e.g., text with inline links) |
| `aspectRatio` | `number` | - | Optional ratio for future use; applied via CSS variable |
| `className` | `string` | - | Additional CSS classes |
## Component Structure
```tsx
<>
<div className="bds-card-text-icon-card__icon">
{icon && (
<img
src={icon}
alt={iconAlt}
{...(iconHeight != null && { height: iconHeight })}
{...(iconWidth != null && { width: iconWidth })}
className="bds-card-text-icon-card__icon-img"
/>
)}
<strong className="bds-card-text-icon-card__heading sh-md-r">{heading}</strong>
</div>
<p className="bds-card-text-icon-card__description body-l">
{description}
</p>
</>
```
## Responsive Sizing
| Breakpoint | Icon Size | Padding | Gap |
|------------|-----------|---------|-----|
| Base (< 576px) | 56px | 16px | 16px |
| MD (576px - 991px) | 60px | 20px | 20px |
| LG (≥ 992px) | 64px | 24px | 24px |
## Files
- `CardTextIconCard.tsx` - React component with TypeScript
- `CardTextIconCard.scss` - Styles with BEM naming
- `index.ts` - Barrel exports
- `README.md` - This file
## Import
```tsx
import { CardTextIconCard } from 'shared/components/CardTextIcon';
// or
import { CardTextIconCard, type CardTextIconCardProps } from 'shared/components/CardTextIcon';
```
## Design System
Part of the Brand Design System (BDS) with `bds-` namespace prefix.

View File

@@ -0,0 +1 @@
export { CardTextIconCard, type CardTextIconCardProps } from './CardTextIconCard';

View File

@@ -186,4 +186,18 @@
@include bds-grid-generate-cols(12, 'xl');
@include bds-grid-generate-auto('xl');
@include bds-grid-generate-offsets(12, 'xl');
}
}
// Polymorphic as="li" — when Col renders as li, override list-item defaults
// so flex layout and width calculations apply correctly
li.bds-grid__col {
list-style: none;
display: block;
}
// Polymorphic as="ul" — when Row renders as ul, reset list styling
ul.bds-grid__row {
list-style: none;
padding-left: 0;
margin: 0;
}

View File

@@ -7,19 +7,24 @@ type PageGridElementProps = React.HTMLAttributes<HTMLDivElement>;
export type PageGridBreakpoint = "base" | "sm" | "md" | "lg" | "xl";
// Define the ResponsiveValue type using Partial<Record> for breakpoints
type ResponsiveValue<T> = T | Partial<Record<PageGridBreakpoint, T>>;
export type ResponsiveValue<T> = T | Partial<Record<PageGridBreakpoint, T>>;
export interface PageGridProps extends PageGridElementProps {
/** Container layout type - "standard" (default) or "wide" (1504px max-width, 144px padding at xl breakpoint) */
containerType?: "standard" | "wide";
}
export interface PageGridRowProps extends PageGridElementProps {}
export interface PageGridRowProps extends PageGridElementProps {
/** Polymorphic element - e.g. "ul" for semantic list markup */
as?: React.ElementType;
}
type PageGridSpanValue = number | "auto" | "fill";
export type PageGridSpanValue = number | "auto" | "fill";
type PageGridOffsetValue = number;
export interface PageGridColProps extends PageGridElementProps {
/** Polymorphic element - e.g. "li" for semantic list markup */
as?: React.ElementType;
span?: ResponsiveValue<PageGridSpanValue>;
offset?: ResponsiveValue<PageGridOffsetValue>;
}
@@ -85,17 +90,17 @@ PageGridRoot.displayName = "PageGrid";
// --- PageGrid.Row Component ---
const PageGridRow = React.forwardRef<HTMLDivElement, PageGridRowProps>(
({ className, ...rest }, ref) => (
<div ref={ref} className={clsx("bds-grid__row", className)} {...rest} />
const PageGridRow = React.forwardRef<HTMLElement, PageGridRowProps>(
({ as: Component = "div", className, ...rest }, ref) => (
<Component ref={ref} className={clsx("bds-grid__row", className)} {...rest} />
)
);
PageGridRow.displayName = "PageGridRow"; // Renamed display name for clarity
PageGridRow.displayName = "PageGridRow";
// --- PageGrid.Col Component ---
const PageGridCol = React.forwardRef<HTMLDivElement, PageGridColProps>((props, ref) => {
const { className, span, offset, ...rest } = props;
const PageGridCol = React.forwardRef<HTMLElement, PageGridColProps>((props, ref) => {
const { as: Component = "div", className, span, offset, ...rest } = props;
const spanClasses: string[] = [];
const offsetClasses: string[] = [];
@@ -151,10 +156,9 @@ const PageGridCol = React.forwardRef<HTMLDivElement, PageGridColProps>((props, r
// Ensure the base class is always applied for styling
return (
<div
<Component
ref={ref}
// Note: Added "bds-grid__col" base class for consistent column initialization
className={clsx("bds-grid__col", className, spanClasses, offsetClasses)}
className={clsx("bds-grid__col", className, spanClasses, offsetClasses)}
{...rest}
/>
);