Compare commits

...

6 Commits

Author SHA1 Message Date
Calvin Jhunjhuwala
df1ab88ef7 cleaning up duplicate, cleaning up button group, useMemo for rectangle grid 2026-01-21 18:41:19 -08:00
Calvin Jhunjhuwala
8f931a2a4c working ofset for large and medium 2026-01-21 17:06:57 -08:00
Calvin Jhunjhuwala
be46c362cf merging master 2026-01-21 16:02:20 -08:00
Calvin Jhunjhuwala
b9410305ef merging master, fixing merge conflicts 2026-01-21 15:19:30 -08:00
Calvin Jhunjhuwala
9d4ed9a477 updates to logo rectangle, need to work on the offsetting 2026-01-21 11:46:18 -08:00
Calvin Jhunjhuwala
4da20f1ac1 correct layout, working on right align next 2026-01-20 17:50:08 -08:00
13 changed files with 1332 additions and 226 deletions

File diff suppressed because one or more lines are too long

View File

@@ -3,9 +3,14 @@ import clsx from 'clsx';
import { Button } from '../../components/Button/Button';
export interface ButtonConfig {
/** Button text label */
label: string;
/** URL to navigate to - renders button as a link */
href?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
/** Force the color to remain constant regardless of theme mode */
forceColor?: boolean;
/** Click handler - matches Button component's onClick signature */
onClick?: () => void;
}
export interface ButtonGroupProps {
@@ -15,7 +20,9 @@ export interface ButtonGroupProps {
tertiaryButton?: ButtonConfig;
/** Button color theme */
color?: 'green' | 'black';
/** Gap between buttons on tablet+ (0px or 8px) */
/** Whether to force the color to remain constant regardless of theme mode */
forceColor?: boolean;
/** Gap between buttons on tablet+ (0px or 4px) */
gap?: 'none' | 'small';
/** Additional CSS classes */
className?: string;
@@ -47,6 +54,7 @@ export const ButtonGroup: React.FC<ButtonGroupProps> = ({
primaryButton,
tertiaryButton,
color = 'green',
forceColor = false,
gap = 'small',
className = '',
}) => {
@@ -67,8 +75,9 @@ export const ButtonGroup: React.FC<ButtonGroupProps> = ({
<Button
variant="primary"
color={color}
forceColor={forceColor}
href={primaryButton.href}
onClick={primaryButton?.onClick as (() => void) | undefined}
onClick={primaryButton.onClick}
>
{primaryButton.label}
</Button>
@@ -77,8 +86,9 @@ export const ButtonGroup: React.FC<ButtonGroupProps> = ({
<Button
variant="tertiary"
color={color}
forceColor={forceColor}
href={tertiaryButton.href}
onClick={tertiaryButton?.onClick as (() => void) | undefined}
onClick={tertiaryButton.onClick}
>
{tertiaryButton.label}
</Button>

View File

@@ -1,7 +1,7 @@
import React from 'react';
import clsx from 'clsx';
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
import { ButtonGroup } from '../ButtonGroup/ButtonGroup';
import { ButtonGroup } from '../../components/ButtonGroup/ButtonGroup';
export interface CalloutMediaBannerProps {
/** Color variant - determines background color (ignored if backgroundImage is provided) */
@@ -18,13 +18,13 @@ export interface CalloutMediaBannerProps {
primaryButton?: {
label: string;
href?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onClick?: () => void;
};
/** Tertiary button configuration */
tertiaryButton?: {
label: string;
href?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onClick?: () => void;
};
/** Additional CSS classes */
className?: string;

View File

@@ -0,0 +1,109 @@
// BDS LogoRectangleGrid Component Styles
// Brand Design System - Logo grid pattern with rectangle tiles
//
// Naming Convention: BEM with 'bds' namespace
// .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 shape="rectangle"
// Note: Alignment is handled dynamically by PageGridCol offset prop
// =============================================================================
// Design Tokens
// =============================================================================
// Note: Color variants are now handled by the TileLogo component
// LogoRectangleGrid 'gray' maps to TileLogo 'neutral'
// LogoRectangleGrid 'green' maps to TileLogo 'green'
// Spacing tokens - responsive
// Mobile (<768px)
$bds-lrg-header-gap-mobile: 24px;
$bds-lrg-text-gap-mobile: 8px;
// Tablet (768px-1023px)
$bds-lrg-header-gap-tablet: 32px;
// Desktop (≥1024px)
$bds-lrg-header-gap-desktop: 40px;
$bds-lrg-text-gap-desktop: 16px;
// =============================================================================
// Base Component Styles
// =============================================================================
.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-rectangle-grid__header {
@extend .d-flex;
@extend .flex-column;
margin-top: 24px;
margin-bottom: 24px;
// Mobile-first gap
gap: $bds-lrg-header-gap-mobile;
// Tablet breakpoint
@include media-breakpoint-up(md) {
gap: $bds-lrg-header-gap-tablet;
margin-top: 32px;
margin-bottom: 32px;
}
// Desktop breakpoint
@include media-breakpoint-up(lg) {
gap: $bds-lrg-header-gap-desktop;
margin-top: 40px;
margin-bottom: 40px;
}
}
// =============================================================================
// Text Content
// =============================================================================
.bds-logo-rectangle-grid__text {
@extend .d-flex;
@extend .flex-column;
// Mobile-first gap
gap: $bds-lrg-text-gap-mobile;
// Desktop breakpoint
@include media-breakpoint-up(lg) {
gap: $bds-lrg-text-gap-desktop;
}
}
// =============================================================================
// Logo Grid Row
// =============================================================================
// Note: Grid layout is now handled by PageGridRow/PageGridCol
// Each tile uses PageGridCol with span={{ base: 2, md: 2, lg: 3 }}
// This gives us 2 columns on mobile (2/4), 3 columns on tablet (2/6),
// and 4 columns on desktop (3/12)
// Alignment is handled by offset prop based on logo count
// Tile rendering and styling is handled by the TileLogo component

View File

@@ -0,0 +1,177 @@
import React, { useMemo } from 'react';
import clsx from 'clsx';
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
import { TileLogo } from '../../components/TileLogo/TileLogo';
export interface LogoItem {
/** Logo image source URL */
src: 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;
}
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;
}
/**
* Calculates the md and lg offsets for the first tile of each row to right-align the grid.
*
* This is a 3-tile-per-row grid. To right-align, we offset based on how many tiles are in that row:
*
* lg (12 columns, each tile = 3 cols):
* - 3 tiles in row: offset 3
* - 2 tiles in row: offset 6
* - 1 tile in row: offset 9
*
* md (8 columns, each tile = 2 cols):
* - 3 tiles in row: offset 2
* - 2 tiles in row: offset 4
* - 1 tile in row: offset 6
*
* Only tiles 1-9 (positions 0-8) are right-aligned. 10+ tiles = no offset.
*
* @param index - The tile's position (0-based)
* @param total - Total number of tiles
* @returns Object with md and lg offset values for this tile (both 0 if not first of row)
*/
const calculateTileOffset = (index: number, total: number): { md: number; lg: number } => {
// No offset if 10+ tiles total
if (total >= 10) return { md: 0, lg: 0 };
// Only first tile of each row gets offset (every 3rd position starting at 0)
if (index % 3 !== 0) return { md: 0, lg: 0 };
// Calculate which row this tile is in
const row = Math.floor(index / 3);
// Calculate how many tiles are in this row
const tilesInThisRow = Math.min(3, total - row * 3);
// Calculate offset to right-align
// lg: (4 - tilesInRow) * 3 → 3 tiles = 3, 2 tiles = 6, 1 tile = 9
// md: (4 - tilesInRow) * 2 → 3 tiles = 2, 2 tiles = 4, 1 tile = 6
const lgOffset = (4 - tilesInThisRow) * 3;
const mdOffset = (4 - tilesInThisRow) * 2;
return { md: mdOffset, lg: lgOffset };
};
/**
* 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}`,
className
);
// 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={logo.src}
alt={logo.alt}
href={logo.href}
onClick={logo.onClick}
disabled={logo.disabled}
/>
</PageGridCol>
);
})}
</PageGridRow>
</PageGrid>
);
};
export default LogoRectangleGrid;

View File

@@ -0,0 +1,422 @@
# 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 */
src: 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;
}
```
### 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/patterns/LogoRectangleGrid';
<LogoRectangleGrid
variant="gray"
heading="Developer tools & APIs"
logos={[
{ src: "/img/logos/company1.svg", alt: "Company 1" },
{ src: "/img/logos/company2.svg", alt: "Company 2" },
{ src: "/img/logos/company3.svg", alt: "Company 3" },
{ src: "/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={[
{ src: "/img/logos/tool1.svg", alt: "Tool 1" },
{ src: "/img/logos/tool2.svg", alt: "Tool 2" },
{ src: "/img/logos/tool3.svg", alt: "Tool 3" },
{ src: "/img/logos/tool4.svg", alt: "Tool 4" },
{ src: "/img/logos/tool5.svg", alt: "Tool 5" },
{ src: "/img/logos/tool6.svg", alt: "Tool 6" },
{ src: "/img/logos/tool7.svg", alt: "Tool 7" },
{ src: "/img/logos/tool8.svg", alt: "Tool 8" }
]}
/>
```
### With Clickable Logos
```tsx
<LogoRectangleGrid
variant="gray"
heading="Our Partners"
description="Leading companies building on XRPL."
logos={[
{
src: "/img/logos/partner1.svg",
alt: "Partner 1",
href: "https://partner1.com"
},
{
src: "/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={[
{
src: "/img/logos/partner1.svg",
alt: "Partner 1",
onClick: () => openModal('partner1')
},
{
src: "/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={[
{
src: "/img/logos/partner1.svg",
alt: "Partner 1",
href: "/partners/partner1"
},
{
src: "/img/logos/coming-soon.svg",
alt: "Coming Soon",
disabled: true
}
]}
/>
```
### Heading Only (No Description)
```tsx
<LogoRectangleGrid
variant="gray"
heading="Ecosystem Members"
logos={[
{ src: "/img/logos/sponsor1.svg", alt: "Sponsor 1" },
{ src: "/img/logos/sponsor2.svg", alt: "Sponsor 2" },
{ src: "/img/logos/sponsor3.svg", alt: "Sponsor 3" },
{ src: "/img/logos/sponsor4.svg", alt: "Sponsor 4" }
]}
/>
```
### Demonstrating Alignment Logic
```tsx
// 1-3 logos: Right-aligned
<LogoRectangleGrid
variant="green"
heading="Featured Partners"
logos={[
{ src: "/img/logos/partner1.svg", alt: "Partner 1" },
{ src: "/img/logos/partner2.svg", alt: "Partner 2" }
]}
/>
// 4 logos: Left-aligned
<LogoRectangleGrid
variant="gray"
heading="Core Technologies"
logos={[
{ src: "/img/logos/tech1.svg", alt: "Tech 1" },
{ src: "/img/logos/tech2.svg", alt: "Tech 2" },
{ src: "/img/logos/tech3.svg", alt: "Tech 3" },
{ src: "/img/logos/tech4.svg", alt: "Tech 4" }
]}
/>
// 5-9 logos: Right-aligned
<LogoRectangleGrid
variant="green"
heading="Developer Tools"
logos={[
{ src: "/img/logos/tool1.svg", alt: "Tool 1" },
{ src: "/img/logos/tool2.svg", alt: "Tool 2" },
{ src: "/img/logos/tool3.svg", alt: "Tool 3" },
{ src: "/img/logos/tool4.svg", alt: "Tool 4" },
{ src: "/img/logos/tool5.svg", alt: "Tool 5" },
{ src: "/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/patterns/LogoRectangleGrid/`
## Version History
- **January 2026**: Initial implementation
- Figma design alignment with 2 color variants
- Responsive grid with 2/3/4 column layout
- Dynamic alignment based on logo count
- Required header section with optional description
- Clickable logo support
- Rectangle tiles with 9:5 aspect ratio

View File

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

View File

@@ -2,7 +2,7 @@ import React from 'react';
import clsx from 'clsx';
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
import { TileLogo } from '../../components/TileLogo/TileLogo';
import { ButtonGroup } from '../ButtonGroup/ButtonGroup';
import { ButtonGroup } from '../../components/ButtonGroup/ButtonGroup';
export interface LogoItem {
/** Logo image source URL */
@@ -28,13 +28,13 @@ export interface LogoSquareGridProps {
primaryButton?: {
label: string;
href?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onClick?: () => void;
};
/** Tertiary button configuration */
tertiaryButton?: {
label: string;
href?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onClick?: () => void;
};
/** Array of logo items to display in the grid */
logos: LogoItem[];

View File

@@ -5332,7 +5332,7 @@ textarea.form-control-lg {
display: table-cell !important;
}
.d-flex, .bds-logo-square-grid__text, .bds-logo-square-grid__header, .bds-logo-square-grid, .bds-callout-media-banner__text, .bds-callout-media-banner__content, .bds-callout-media-banner > [class*=bds-grid__col], .bds-callout-media-banner, .bds-button-group {
.d-flex, .bds-logo-rectangle-grid__text, .bds-logo-rectangle-grid__header, .bds-logo-rectangle-grid, .bds-logo-square-grid__text, .bds-logo-square-grid__header, .bds-logo-square-grid, .bds-callout-media-banner__text, .bds-callout-media-banner__content, .bds-callout-media-banner > [class*=bds-grid__col], .bds-callout-media-banner, .bds-button-group {
display: flex !important;
}
@@ -5646,7 +5646,7 @@ textarea.form-control-lg {
width: 75% !important;
}
.w-100, .bds-logo-square-grid, .bds-callout-media-banner__content, .bds-callout-media-banner {
.w-100, .bds-logo-rectangle-grid, .bds-logo-square-grid, .bds-callout-media-banner__content, .bds-callout-media-banner {
width: 100% !important;
}
@@ -5706,7 +5706,7 @@ textarea.form-control-lg {
flex-direction: row !important;
}
.flex-column, .bds-logo-square-grid__text, .bds-logo-square-grid__header, .bds-logo-square-grid, .bds-callout-media-banner__text, .bds-callout-media-banner__content, .bds-callout-media-banner > [class*=bds-grid__col], .bds-button-group {
.flex-column, .bds-logo-rectangle-grid__text, .bds-logo-rectangle-grid__header, .bds-logo-rectangle-grid, .bds-logo-square-grid__text, .bds-logo-square-grid__header, .bds-logo-square-grid, .bds-callout-media-banner__text, .bds-callout-media-banner__content, .bds-callout-media-banner > [class*=bds-grid__col], .bds-button-group {
flex-direction: column !important;
}
@@ -11295,7 +11295,7 @@ aside .active-parent > a {
width: 48px;
}
.w-100, .bds-logo-square-grid, .bds-callout-media-banner__content, .bds-callout-media-banner {
.w-100, .bds-logo-rectangle-grid, .bds-logo-square-grid, .bds-callout-media-banner__content, .bds-callout-media-banner {
width: 100%;
}
@@ -20248,235 +20248,48 @@ html.dark .bds-cards-featured__description {
color: #FFFFFF;
}
.bds-callout-media-banner {
box-sizing: border-box;
min-height: 280px;
}
.bds-callout-media-banner {
background-size: cover;
background-position: center;
background-repeat: no-repeat;
padding: 16px;
.bds-logo-rectangle-grid {
gap: 24px;
}
@media (min-width: 576px) {
.bds-callout-media-banner {
padding: 24px;
}
}
@media (min-width: 992px) {
.bds-callout-media-banner {
padding: 40px 32px;
min-height: 360px;
}
}
.bds-callout-media-banner__content {
gap: 48px;
}
@media (min-width: 576px) {
.bds-callout-media-banner__content {
gap: 64px;
}
}
@media (min-width: 1280px) {
.bds-callout-media-banner__content {
gap: 80px;
}
}
.bds-callout-media-banner--centered .bds-callout-media-banner__content {
justify-content: center;
}
.bds-callout-media-banner__text {
flex-direction: column;
color: var(--bds-cmb-text-color, #232021);
gap: 16px;
}
@media (min-width: 576px) {
.bds-callout-media-banner__text {
gap: 24px;
}
}
@media (min-width: 1280px) {
.bds-callout-media-banner__text {
.bds-logo-rectangle-grid {
gap: 32px;
}
}
@media (min-width: 992px) {
.bds-logo-rectangle-grid {
gap: 40px;
}
}
.bds-callout-media-banner__heading {
font-family: "Tobias", "Noto Serif", monospace;
font-weight: 300;
font-size: 32px;
line-height: 40px;
letter-spacing: 0px;
margin-bottom: 16px;
.bds-logo-rectangle-grid__header {
margin-top: 24px;
margin-bottom: 24px;
gap: 24px;
}
@media (min-width: 576px) {
.bds-callout-media-banner__heading {
font-size: 36px;
line-height: 45px;
letter-spacing: -0.5px;
margin-bottom: 16px;
.bds-logo-rectangle-grid__header {
gap: 32px;
margin-top: 32px;
margin-bottom: 32px;
}
}
@media (min-width: 992px) {
.bds-callout-media-banner__heading {
font-size: 40px;
line-height: 46px;
letter-spacing: -1px;
margin-bottom: 16px;
.bds-logo-rectangle-grid__header {
gap: 40px;
margin-top: 40px;
margin-bottom: 40px;
}
}
.bds-callout-media-banner__heading {
margin: 0;
color: inherit !important;
}
.bds-callout-media-banner__subheading {
font-family: "Booton", "Noto Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 400;
font-size: 24px;
line-height: 30px;
letter-spacing: -1px;
margin-bottom: 16px;
}
@media (min-width: 576px) {
.bds-callout-media-banner__subheading {
font-size: 28px;
line-height: 35px;
letter-spacing: -0.75px;
margin-bottom: 16px;
}
.bds-logo-rectangle-grid__text {
gap: 8px;
}
@media (min-width: 992px) {
.bds-callout-media-banner__subheading {
font-size: 32px;
line-height: 40px;
letter-spacing: -0.5px;
margin-bottom: 16px;
.bds-logo-rectangle-grid__text {
gap: 16px;
}
}
.bds-callout-media-banner__subheading {
margin: 0;
color: inherit;
}
.bds-callout-media-banner--default {
background-color: #FFFFFF;
}
.bds-callout-media-banner--light-gray {
background-color: #E6EAF0;
}
.bds-callout-media-banner--lilac {
background-color: #C0A7FF;
}
.bds-callout-media-banner--green {
background-color: #70EE97;
}
.bds-callout-media-banner--gray {
background-color: #CAD4DF;
}
.bds-callout-media-banner--image {
background-color: transparent;
}
.bds-callout-media-banner--image::before {
content: "";
top: 0;
left: 0;
right: 0;
bottom: 0;
inset: 0;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.2) 50%, rgba(0, 0, 0, 0.1) 100%);
z-index: 0;
pointer-events: none;
}
.bds-callout-media-banner--image-text-black::before {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.1) 100%);
}
.bds-callout-media-banner--image-text-black .bds-callout-media-banner__text {
color: #141414 !important;
}
html.dark .bds-callout-media-banner--default {
background-color: #232325;
}
html.dark .bds-callout-media-banner--default .bds-callout-media-banner__text {
color: #FFFFFF;
}
html.dark .bds-callout-media-banner--light-gray {
background-color: #343437;
}
html.dark .bds-callout-media-banner--light-gray .bds-callout-media-banner__text {
color: #FFFFFF;
}
html.dark .bds-callout-media-banner--lilac {
background-color: #7649E3;
}
html.dark .bds-callout-media-banner--lilac .bds-callout-media-banner__text {
color: #FFFFFF;
}
html.dark .bds-callout-media-banner--green {
background-color: #21E46B;
}
html.dark .bds-callout-media-banner--green .bds-callout-media-banner__text {
color: #141414;
}
html.dark .bds-callout-media-banner--gray {
background-color: #454549;
}
html.dark .bds-callout-media-banner--gray .bds-callout-media-banner__text {
color: #FFFFFF;
}
html.dark .bds-callout-media-banner--image .bds-callout-media-banner__text {
color: #FFFFFF;
}
html.dark .bds-callout-media-banner--image-text-black .bds-callout-media-banner__text {
color: #141414 !important;
}
html.light .bds-callout-media-banner--default {
background-color: #FFFFFF;
}
html.light .bds-callout-media-banner--default .bds-callout-media-banner__text {
color: #141414;
}
html.light .bds-callout-media-banner--light-gray {
background-color: #E6EAF0;
}
html.light .bds-callout-media-banner--light-gray .bds-callout-media-banner__text {
color: #141414;
}
html.light .bds-callout-media-banner--lilac {
background-color: #C0A7FF;
}
html.light .bds-callout-media-banner--lilac .bds-callout-media-banner__text {
color: #141414;
}
html.light .bds-callout-media-banner--green {
background-color: #70EE97;
}
html.light .bds-callout-media-banner--green .bds-callout-media-banner__text {
color: #141414;
}
html.light .bds-callout-media-banner--gray {
background-color: #CAD4DF;
}
html.light .bds-callout-media-banner--gray .bds-callout-media-banner__text {
color: #141414;
}
html.light .bds-callout-media-banner--image .bds-callout-media-banner__text {
color: #FFFFFF;
}
html.light .bds-callout-media-banner--image-text-black .bds-callout-media-banner__text {
color: #141414 !important;
}
.bds-feature-two-column__button-group .bds-btn--tertiary {
padding-top: 0px !important;

View File

@@ -103,7 +103,7 @@ $line-height-base: 1.5;
@import "../shared/patterns/CalloutMediaBanner/CalloutMediaBanner.scss";
@import "../shared/patterns/LogoSquareGrid/LogoSquareGrid.scss";
@import "../shared/patterns/CardsFeatured/CardsFeatured.scss";
@import "../shared/patterns/CalloutMediaBanner/CalloutMediaBanner.scss";
@import "../shared/patterns/LogoRectangleGrid/LogoRectangleGrid.scss";
@import "../shared/patterns/FeatureTwoColumn/FeatureTwoColumn.scss";
@import "_code-tabs.scss";
@import "_code-walkthrough.scss";