Compare commits

...

1 Commits

Author SHA1 Message Date
Calvin Jhunjhuwala
1e61c71c94 logo square grid ready for review 2026-01-17 17:52:46 -08:00
13 changed files with 1479 additions and 58 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,43 @@
// BDS ButtonGroup Component Styles
// Brand Design System - Responsive button group pattern
//
// Naming Convention: BEM with 'bds' namespace
// .bds-button-group - Base component
// .bds-button-group--gap-none - No gap between buttons on tablet+ (0px)
// .bds-button-group--gap-small - Small gap between buttons on tablet+ (8px)
// =============================================================================
// Base Component Styles
// =============================================================================
.bds-button-group {
@extend .d-flex;
@extend .flex-column;
@extend .align-items-start;
@extend .flex-wrap;
gap: 8px;
// Tablet breakpoint - horizontal layout
@include media-breakpoint-up(md) {
flex-direction: row !important;
align-items: center;
}
}
// =============================================================================
// Gap Modifiers
// =============================================================================
.bds-button-group--gap-none {
// Tablet breakpoint - no gap
@include media-breakpoint-up(md) {
gap: 0px;
}
}
.bds-button-group--gap-small {
// Tablet breakpoint - keep 8px gap
@include media-breakpoint-up(md) {
gap: 4px;
}
}

View File

@@ -0,0 +1,90 @@
import React from 'react';
import clsx from 'clsx';
import { Button } from '../../components/Button/Button';
export interface ButtonConfig {
label: string;
href?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
export interface ButtonGroupProps {
/** Primary button configuration */
primaryButton?: ButtonConfig;
/** Tertiary button configuration */
tertiaryButton?: ButtonConfig;
/** Button color theme */
color?: 'green' | 'black';
/** Gap between buttons on tablet+ (0px or 8px) */
gap?: 'none' | 'small';
/** Additional CSS classes */
className?: string;
}
/**
* ButtonGroup Component
*
* A responsive button group container that displays primary and/or tertiary buttons.
* Stacks vertically on mobile and horizontally on tablet+.
*
* @example
* // Basic usage with both buttons
* <ButtonGroup
* primaryButton={{ label: "Get Started", href: "/start" }}
* tertiaryButton={{ label: "Learn More", href: "/learn" }}
* color="green"
* />
*
* @example
* // With custom gap
* <ButtonGroup
* primaryButton={{ label: "Action", onClick: handleClick }}
* color="black"
* gap="small"
* />
*/
export const ButtonGroup: React.FC<ButtonGroupProps> = ({
primaryButton,
tertiaryButton,
color = 'green',
gap = 'small',
className = '',
}) => {
// Don't render if no buttons are provided
if (!primaryButton && !tertiaryButton) {
return null;
}
const classNames = clsx(
'bds-button-group',
`bds-button-group--gap-${gap}`,
className
);
return (
<div className={classNames}>
{primaryButton && (
<Button
variant="primary"
color={color}
href={primaryButton.href}
onClick={primaryButton?.onClick as (() => void) | undefined}
>
{primaryButton.label}
</Button>
)}
{tertiaryButton && (
<Button
variant="tertiary"
color={color}
href={tertiaryButton.href}
onClick={tertiaryButton?.onClick as (() => void) | undefined}
>
{tertiaryButton.label}
</Button>
)}
</div>
);
};
export default ButtonGroup;

View File

@@ -0,0 +1,68 @@
# ButtonGroup Component
A responsive button group container that displays primary and/or tertiary buttons. Stacks vertically on mobile and horizontally on tablet+.
## Features
- **Responsive Layout**: Vertical stack on mobile, horizontal row on tablet+
- **Flexible Configuration**: Support for primary, tertiary, or both buttons
- **Customizable Spacing**: Control gap between buttons on tablet+ (none or small)
- **Theme Support**: Green or black color themes
## Usage
```tsx
import { ButtonGroup } from 'shared/patterns/ButtonGroup';
// Basic usage with both buttons
<ButtonGroup
primaryButton={{ label: "Get Started", href: "/start" }}
tertiaryButton={{ label: "Learn More", href: "/learn" }}
color="green"
/>
// With no gap on tablet+
<ButtonGroup
primaryButton={{ label: "Action", onClick: handleClick }}
color="black"
gap="none"
/>
// With small gap on tablet+ (4px - default)
<ButtonGroup
primaryButton={{ label: "Primary Action", href: "/action" }}
tertiaryButton={{ label: "Secondary", href: "/secondary" }}
gap="small"
/>
```
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `primaryButton` | `ButtonConfig` | - | Primary button configuration |
| `tertiaryButton` | `ButtonConfig` | - | Tertiary button configuration |
| `color` | `'green' \| 'black'` | `'green'` | Button color theme |
| `gap` | `'none' \| 'small'` | `'small'` | Gap between buttons on tablet+ (0px or 4px) |
| `className` | `string` | `''` | Additional CSS classes |
### ButtonConfig
```tsx
interface ButtonConfig {
label: string;
href?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
```
## Responsive Behavior
- **Mobile (<768px)**: Buttons stack vertically with 8px gap, aligned to start
- **Tablet+ (≥768px)**: Buttons align horizontally, centered, with configurable gap (0px or 4px)
## CSS Classes
- `.bds-button-group` - Base component
- `.bds-button-group--gap-none` - No gap on tablet+ (0px)
- `.bds-button-group--gap-small` - Small gap on tablet+ (4px)

View File

@@ -0,0 +1,2 @@
export { ButtonGroup } from './ButtonGroup';
export type { ButtonGroupProps, ButtonConfig } from './ButtonGroup';

View File

@@ -15,7 +15,8 @@
// .bds-callout-media-banner__text - Text content container
// .bds-callout-media-banner__heading - Heading element
// .bds-callout-media-banner__subheading - Subheading element
// .bds-callout-media-banner__actions - Button container
//
// Note: Button layout is handled by the ButtonGroup component
// =============================================================================
// Design Tokens
@@ -191,21 +192,8 @@ $bds-cmb-image-overlay: rgba(0, 0, 0, 0.3);
// =============================================================================
// Action Buttons
// ==================================================================== c=========
.bds-callout-media-banner__actions {
@extend .d-flex;
@extend .flex-column;
@extend .align-items-start;
@extend .flex-wrap;
gap: 8px;
// Tablet breakpoint
@include media-breakpoint-up(md) {
flex-direction: row;
align-items: center;
gap: 0px;
}
}
// =============================================================================
// Note: Button layout is now handled by the ButtonGroup component
// =============================================================================
// Color Variants (only applied when NO backgroundImage is provided)

View File

@@ -1,7 +1,7 @@
import React from 'react';
import clsx from 'clsx';
import { Button } from '../../components/Button/Button';
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
import { ButtonGroup } from '../ButtonGroup/ButtonGroup';
export interface CalloutMediaBannerProps {
/** Color variant - determines background color (ignored if backgroundImage is provided) */
@@ -116,30 +116,12 @@ export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
</div>
{/* Buttons */}
{(primaryButton || tertiaryButton) && (
<div className="bds-callout-media-banner__actions">
{primaryButton && (
<Button
variant="primary"
color={buttonColor}
href={primaryButton.href}
onClick={primaryButton?.onClick as (() => void) | undefined}
>
{primaryButton.label}
</Button>
)}
{tertiaryButton && (
<Button
variant="tertiary"
color={buttonColor}
href={tertiaryButton.href}
onClick={tertiaryButton?.onClick as (() => void) | undefined}
>
{tertiaryButton.label}
</Button>
)}
</div>
)}
<ButtonGroup
primaryButton={primaryButton}
tertiaryButton={tertiaryButton}
color={buttonColor}
gap="none"
/>
</div>
</PageGridCol>
</PageGridRow>

View File

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

View File

@@ -0,0 +1,140 @@
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';
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 LogoSquareGridProps {
/** Color variant - determines background color */
variant?: 'gray' | 'green';
/** Optional heading text */
heading?: string;
/** Optional description text */
description?: string;
/** Primary button configuration */
primaryButton?: {
label: string;
href?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
};
/** Tertiary button configuration */
tertiaryButton?: {
label: string;
href?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
};
/** Array of logo items to display in the grid */
logos: LogoItem[];
/** Additional CSS classes */
className?: string;
}
/**
* LogoSquareGrid Component
*
* A responsive grid pattern for displaying company/partner logos with an optional header section.
* Features square tiles arranged in a responsive grid with 2 color variants and dark mode support.
*
* @example
* // Basic usage with gray variant
* <LogoSquareGrid
* 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 buttons and clickable logos
* <LogoSquareGrid
* variant="green"
* heading="Our Partners"
* description="Leading companies building on XRPL."
* primaryButton={{ label: "View All Partners", href: "/partners" }}
* tertiaryButton={{ label: "Become a Partner", href: "/partner-program" }}
* logos={[
* { src: "/logos/partner1.svg", alt: "Partner 1", href: "https://partner1.com" }
* ]}
* />
*/
export const LogoSquareGrid: React.FC<LogoSquareGridProps> = ({
variant = 'gray',
heading,
description,
primaryButton,
tertiaryButton,
logos,
className = '',
}) => {
// Build class names using BEM with bds namespace
const classNames = clsx(
'bds-logo-square-grid',
`bds-logo-square-grid--${variant}`,
className
);
// Determine if we should show the header section
const hasHeader = !!(heading || description || primaryButton || tertiaryButton);
return (
<PageGrid className="">
<PageGridRow>
<PageGridCol span={{ base: 4, md: 8, 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 */}
<ButtonGroup
primaryButton={primaryButton}
tertiaryButton={tertiaryButton}
color="green"
gap="small"
/>
</div>
)}
</PageGridCol>
</PageGridRow>
<PageGridRow>
{logos.map((logo, index) => (
<PageGridCol key={index} span={{ base: 2, lg: 3 }}>
<TileLogo
shape="square"
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 LogoSquareGrid;

View File

@@ -0,0 +1,405 @@
# LogoSquareGrid Component
A responsive grid pattern for displaying company/partner logos with an optional header section. Built on top of the TileLogo component, featuring square tiles arranged in a responsive grid with 2 color variants and full dark mode support.
## Features
- **2 Color Variants**: Gray and Green backgrounds
- **Responsive Grid**: Automatically adapts from 2 columns (mobile) to 4 columns (tablet/desktop)
- **Optional Header**: Includes heading, description, and action buttons
- **Clickable Logos**: Support for optional links on individual logos
- **Dark Mode Support**: Full light and dark mode compatibility
- **Square Tiles**: Maintains perfect square 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 Size |
|------------|---------|-----|-----------|
| Mobile (< 768px) | 2 | 8px | ~183px |
| Tablet (768px - 1023px) | 4 | 8px | ~178px |
| Desktop (≥ 1024px) | 4 | 8px | ~298px |
## Color Variants
The LogoSquareGrid pattern uses two color variants that map directly to TileLogo component variants:
| LogoSquareGrid 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 LogoSquareGridProps {
/** Color variant - determines background color */
variant?: 'gray' | 'green';
/** Optional heading text */
heading?: string;
/** Optional description text */
description?: string;
/** Primary button configuration */
primaryButton?: {
label: string;
href?: string;
onClick?: () => void;
};
/** Tertiary button configuration */
tertiaryButton?: {
label: string;
href?: string;
onClick?: () => void;
};
/** Array of logo items to display in the grid */
logos: LogoItem[];
/** Additional CSS classes */
className?: string;
}
```
### Default Values
- `variant`: `'gray'`
- `heading`: `undefined`
- `description`: `undefined`
- `primaryButton`: `undefined`
- `tertiaryButton`: `undefined`
- `className`: `''`
### Required Props
- `logos`: Array of logo items (required)
## Usage Examples
### Basic Usage with Gray Variant
```tsx
import { LogoSquareGrid } from 'shared/patterns/LogoSquareGrid';
<LogoSquareGrid
variant="gray"
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 Header Section
```tsx
<LogoSquareGrid
variant="green"
heading="Developer tools & APIs"
description="Streamline development and build powerful RWA tokenization solutions with XRP Ledger's comprehensive developer toolset."
primaryButton={{ label: "View Documentation", href: "/docs" }}
tertiaryButton={{ label: "Explore Tools", href: "/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" },
{ src: "/img/logos/tool7.svg", alt: "Tool 7" },
{ src: "/img/logos/tool8.svg", alt: "Tool 8" }
]}
/>
```
### With Clickable Logos
```tsx
<LogoSquareGrid
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
<LogoSquareGrid
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
<LogoSquareGrid
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
}
]}
/>
```
### Without Header (Logo Grid Only)
```tsx
<LogoSquareGrid
variant="gray"
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" }
]}
/>
```
### With Single Button
```tsx
<LogoSquareGrid
variant="green"
heading="Featured Integrations"
description="Connect with leading platforms and services."
primaryButton={{ label: "See All Integrations", href: "/integrations" }}
logos={[
{ src: "/img/logos/integration1.svg", alt: "Integration 1" },
{ src: "/img/logos/integration2.svg", alt: "Integration 2" }
]}
/>
```
### With Click Handler
```tsx
<LogoSquareGrid
variant="gray"
heading="Developer Resources"
description="Access comprehensive tools and libraries."
primaryButton={{
label: "Get Started",
onClick: () => console.log('Primary clicked')
}}
tertiaryButton={{
label: "Learn More",
href: "/learn"
}}
logos={[
{ src: "/img/logos/resource1.svg", alt: "Resource 1" }
]}
/>
```
## 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: 120-200px recommended)
- 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, lg: 3 }}` (2 cols on mobile out of 4, 3 cols on desktop out of 12)
- Tiles maintain a 1:1 aspect ratio using `aspect-ratio: 1`
- Gaps between tiles are handled by PageGrid's built-in gutter system
- Grid automatically wraps to new rows as needed
### 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
### Header Section Logic
The header section only renders if at least one of the following is provided:
- `heading`
- `description`
- `primaryButton`
- `tertiaryButton`
### Button Styling
- Both primary and tertiary buttons use green color scheme
- Buttons stack vertically on mobile, horizontal on tablet+
- Button spacing: 8px gap on mobile, 4px gap on tablet+
- Button layout is handled by the shared ButtonGroup component
## Styling
### BEM Class Structure
```scss
.bds-logo-square-grid // Base component
.bds-logo-square-grid--gray // Gray variant (maps to TileLogo 'neutral')
.bds-logo-square-grid--green // Green variant (maps to TileLogo 'green')
.bds-logo-square-grid__header // Header section container
.bds-logo-square-grid__text // Text content container
.bds-logo-square-grid__heading // Heading element
.bds-logo-square-grid__description // Description element
```
**Note**: Individual logo tiles are rendered using the TileLogo component with its own BEM structure (`bds-tile-logo`). Grid layout is handled by PageGridRow and PageGridCol components. Button layout is handled by the ButtonGroup component (`bds-button-group`).
### 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
- ARIA labels provided through Button component
- 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**: Keep concise (1 line preferred), use sentence case
- **Description**: Provide context (2-3 lines max), complete sentences
- **Logo Count**: Aim for multiples of 4 for visual balance on desktop
- **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. **Color**: Consider providing light/dark variants if needed
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, lg: 3 }}` for responsive layout (2 cols mobile, 4 cols desktop)
- **Tile Rendering**: Leverages TileLogo component for all logo tiles
- **Variant Mapping**: LogoSquareGrid 'gray' TileLogo 'neutral', LogoSquareGrid 'green' TileLogo 'green'
- **Interactive States**: TileLogo handles href (links), onClick (buttons), and disabled states
- **Aspect Ratio**: Square tiles maintained by TileLogo with CSS `aspect-ratio: 1`
- **Animations**: Window shade hover effect managed by TileLogo component
- **Button Layout**: Uses shared ButtonGroup component with `gap="small"` (4px on tablet+)
## Files
- `LogoSquareGrid.tsx` - Component implementation
- `LogoSquareGrid.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
- **ButtonGroup**: Shared pattern used for responsive button layout in the header
- **PageGrid**: Used internally for responsive grid structure and standard container support
## Design References
- **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/`
## Version History
- **January 2026**: Initial implementation
- Figma design alignment with 2 color variants
- Responsive grid with 2/4 column layout
- Optional header section with buttons
- Clickable logo support
- Refactored to use shared ButtonGroup component for button layout

View File

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

View File

@@ -5332,7 +5332,7 @@ textarea.form-control-lg {
display: table-cell !important;
}
.d-flex, .bds-callout-media-banner__actions, .bds-callout-media-banner__text, .bds-callout-media-banner__content, .bds-callout-media-banner > [class*=bds-grid__col], .bds-callout-media-banner {
.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 {
display: flex !important;
}
@@ -5646,7 +5646,7 @@ textarea.form-control-lg {
width: 75% !important;
}
.w-100, .bds-callout-media-banner__content, .bds-callout-media-banner {
.w-100, .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-callout-media-banner__actions, .bds-callout-media-banner__text, .bds-callout-media-banner__content, .bds-callout-media-banner > [class*=bds-grid__col] {
.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-direction: column !important;
}
@@ -5734,7 +5734,7 @@ textarea.form-control-lg {
flex-shrink: 1 !important;
}
.flex-wrap, .bds-callout-media-banner__actions {
.flex-wrap, .bds-button-group {
flex-wrap: wrap !important;
}
@@ -5770,7 +5770,7 @@ textarea.form-control-lg {
justify-content: space-evenly !important;
}
.align-items-start, .bds-callout-media-banner__actions {
.align-items-start, .bds-button-group {
align-items: flex-start !important;
}
@@ -11295,7 +11295,7 @@ aside .active-parent > a {
width: 48px;
}
.w-100, .bds-callout-media-banner__content, .bds-callout-media-banner {
.w-100, .bds-logo-square-grid, .bds-callout-media-banner__content, .bds-callout-media-banner {
width: 100%;
}
@@ -19588,6 +19588,28 @@ html.light .bds-hero-split-media--accent .bds-hero-split-media__subtitle {
gap: 0;
}
.bds-button-group {
gap: 8px;
}
@media (min-width: 576px) {
.bds-button-group {
flex-direction: row !important;
align-items: center;
}
}
@media (min-width: 576px) {
.bds-button-group--gap-none {
gap: 0px;
}
}
@media (min-width: 576px) {
.bds-button-group--gap-small {
gap: 4px;
}
}
.bds-callout-media-banner {
box-sizing: border-box;
min-height: 280px;
@@ -19702,17 +19724,6 @@ html.light .bds-hero-split-media--accent .bds-hero-split-media__subtitle {
color: inherit;
}
.bds-callout-media-banner__actions {
gap: 8px;
}
@media (min-width: 576px) {
.bds-callout-media-banner__actions {
flex-direction: row;
align-items: center;
gap: 0px;
}
}
.bds-callout-media-banner--default {
background-color: #FFFFFF;
}
@@ -19829,6 +19840,49 @@ html.light .bds-callout-media-banner--image-text-black .bds-callout-media-banner
color: #141414 !important;
}
.bds-logo-square-grid {
gap: 24px;
}
@media (min-width: 576px) {
.bds-logo-square-grid {
gap: 32px;
}
}
@media (min-width: 992px) {
.bds-logo-square-grid {
gap: 40px;
}
}
.bds-logo-square-grid__header {
margin-top: 24px;
margin-bottom: 24px;
gap: 24px;
}
@media (min-width: 576px) {
.bds-logo-square-grid__header {
gap: 32px;
margin-top: 32px;
margin-bottom: 32px;
}
}
@media (min-width: 992px) {
.bds-logo-square-grid__header {
gap: 40px;
margin-top: 40px;
margin-bottom: 40px;
}
}
.bds-logo-square-grid__text {
gap: 8px;
}
@media (min-width: 992px) {
.bds-logo-square-grid__text {
gap: 16px;
}
}
pre {
color: #FFFFFF;
background-color: #232325;

View File

@@ -99,7 +99,9 @@ $line-height-base: 1.5;
@import "../shared/components/CardIcon/CardIcon.scss";
@import "../shared/components/SmallTilesSection/_small-tiles-section.scss";
@import "../shared/patterns/HeaderHeroSplitMedia/HeaderHeroSplitMedia.scss";
@import "../shared/patterns/ButtonGroup/ButtonGroup.scss";
@import "../shared/patterns/CalloutMediaBanner/CalloutMediaBanner.scss";
@import "../shared/patterns/LogoSquareGrid/LogoSquareGrid.scss";
@import "_code-tabs.scss";
@import "_code-walkthrough.scss";
@import "_diagrams.scss";