Add TileLogo component with showcase and documentation

- Introduced the TileLogo component, designed for displaying brand logos with interactive states.
- Implemented two shape variants (Square and Rectangle) and two color variants (Neutral and Green), with responsive sizing.
- Created a comprehensive showcase page demonstrating all variants, states, and usage examples.
- Added detailed documentation covering usage guidelines, best practices, and API reference.
- Included SCSS styles for the TileLogo component, ensuring compatibility with both light and dark themes.
This commit is contained in:
akcodez
2025-12-08 10:52:37 -08:00
committed by Calvin Jhunjhuwala
parent 3391f69927
commit 354d0c797b
7 changed files with 1782 additions and 0 deletions

View File

@@ -0,0 +1,348 @@
# TileLogo Component - Usage Guidelines
## Overview
`TileLogo` is a tile/card component designed to display brand logos with interactive states. It supports two shape variants (Square and Rectangle), two color variants (Neutral and Green), and responsive sizing that adapts to different breakpoints.
**Use TileLogo when:**
- Displaying partner or ecosystem brand logos
- Creating logo grids or showcases
- Highlighting integrations or collaborations
- Building clickable logo galleries
- Creating featured partner banners
**Don't use TileLogo for:**
- Feature cards with text content (use CardOffgrid)
- Navigation items (use navigation components)
- Image galleries (use gallery components)
- Non-interactive logo displays (use simple image elements)
---
## Shape Variants
### Square Shape (Default)
- **Aspect Ratio:** 1:1 (maintains square proportions)
- **Use Cases:** Logo grids, partner showcases, general logo displays
- **Responsive Padding:**
- SM: 36px vertical / 20px horizontal
- MD: 40px vertical / 24px horizontal
- LG: 72px vertical / 48px horizontal
### Rectangle Shape
- **Height:** Fixed height with full width
- **Use Cases:** Horizontal layouts, featured partner banners, wide logo displays
- **Responsive Sizing:**
- SM: 96px height, 2 columns width
- MD: 96px height, 4 columns width
- LG: 160px height, 4 columns width
- **Responsive Padding:**
- SM/MD: 20px vertical / 36px horizontal
- LG: 32px vertical / 64px horizontal
---
## Color Variants
### Neutral Variant (Default)
Use for general partner showcases and standard logo displays. Provides a subtle, professional appearance.
**Best for:**
- Standard partner logos
- General logo grids
- Non-featured content
### Green Variant
Use to highlight featured or primary partners. Creates visual hierarchy and draws attention.
**Best for:**
- Featured partners
- Primary integrations
- Important collaborations
- Creating visual emphasis
---
## Component API
```typescript
interface TileLogoProps {
/** Shape variant: 'square' (default) or 'rectangle' */
shape?: 'square' | 'rectangle';
/** Color variant: 'neutral' (default) or 'green' */
variant?: 'neutral' | 'green';
/** Logo image source (URL or path) */
logo: string;
/** Alt text for accessibility */
alt: string;
/** Click handler - renders as <button> */
onClick?: () => void;
/** Link destination - renders as <a> */
href?: string;
/** Disabled state - prevents interaction */
disabled?: boolean;
/** Additional CSS classes */
className?: string;
}
```
---
## Usage Examples
### Basic Usage with Link
```tsx
<TileLogo
variant="neutral"
logo="/logos/partner-logo.svg"
alt="Partner Name"
href="/partners/partner-name"
/>
```
### Square Shape with Click Handler
```tsx
<TileLogo
shape="square"
variant="green"
logo="/logos/featured-logo.svg"
alt="Featured Partner"
onClick={() => openPartnerModal('featured')}
/>
```
### Rectangle Shape for Banners
```tsx
<TileLogo
shape="rectangle"
variant="neutral"
logo="/logos/partner-logo.svg"
alt="Partner Banner"
href="/partners/partner-name"
/>
```
### Logo Grid with Mixed Variants
```tsx
<PageGrid>
<PageGridRow>
{partners.map((partner) => (
<PageGridCol key={partner.id} span={{ base: 4, sm: 4, lg: 3 }}>
<TileLogo
shape="square"
variant={partner.featured ? 'green' : 'neutral'}
logo={partner.logo}
alt={partner.name}
href={partner.url}
/>
</PageGridCol>
))}
</PageGridRow>
</PageGrid>
```
### Rectangle Banner Layout
```tsx
<PageGrid>
<PageGridRow>
<PageGridCol span={{ base: 4, sm: 8, lg: 12 }}>
<div className="d-flex flex-column gap-4">
<TileLogo
shape="rectangle"
variant="green"
logo="/logos/featured-partner.svg"
alt="Featured Partner"
href="/partners/featured"
/>
<TileLogo
shape="rectangle"
variant="neutral"
logo="/logos/partner.svg"
alt="Partner"
href="/partners/partner"
/>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
```
---
## Best Practices
### Choosing the Right Shape
- **Use Square** when:
- Creating grid layouts
- Displaying multiple logos in equal-sized tiles
- Maintaining consistent visual rhythm
- Logo aspect ratios vary significantly
- **Use Rectangle** when:
- Creating horizontal banner layouts
- Displaying logos in a single column
- Need wider display area for horizontal logos
- Creating featured partner sections
### Choosing the Right Color Variant
- **Use Neutral** for:
- Standard partner showcases
- General logo grids
- Non-featured content
- Maintaining visual balance
- **Use Green** for:
- Featured or primary partners
- Creating visual hierarchy
- Highlighting important integrations
- Drawing user attention
### Logo Image Guidelines
- **Format:** Use SVG when possible for crisp rendering at all sizes
- **Aspect Ratio:** Logos should fit within the padded bounding box
- **Size:** Logo size is variable (designer recommendation) - ensure logos scale appropriately
- **Contrast:** Ensure logos have sufficient contrast with background colors
- **Alt Text:** Always provide meaningful, descriptive alt text
### Layout Recommendations
- **Square Grids:** Use PageGrid with responsive column spans (e.g., `span={{ base: 4, sm: 4, lg: 3 }}`)
- **Rectangle Layouts:** Use full-width containers (`span={{ base: 4, sm: 8, lg: 12 }}`)
- **Spacing:** Use PageGrid's built-in gap spacing for consistent layouts
- **Responsive:** Let the component handle responsive sizing automatically
### Interaction Patterns
- **Links vs Buttons:** Use `href` for navigation, `onClick` for actions (modals, dropdowns)
- **Disabled State:** Use for "coming soon" partners or unavailable content
- **Accessibility:** Always provide meaningful alt text and ensure keyboard navigation works
---
## Accessibility Considerations
### Semantic HTML
The component automatically renders as:
- `<a>` when `href` is provided
- `<button>` when `onClick` is provided or `disabled` is true
### Keyboard Navigation
- **Tab:** Navigates to the tile
- **Enter/Space:** Activates the tile (for buttons)
- **Focus Ring:** Visible 2px border appears on keyboard focus
### Screen Readers
- Always provide meaningful `alt` text describing the partner/brand
- Disabled tiles communicate their state via `aria-disabled`
- Links include proper `href` attributes for navigation context
### Color Contrast
- All component states meet WCAG AA contrast requirements
- Ensure logo images have sufficient contrast with background colors
- Test in both light and dark modes
---
## Design References
- **Square SM:** [Figma - Square SM](https://www.figma.com/design/12qdRGujqX7DWuY8IYmAnh/Tile---Logo?node-id=4047-907&m=dev)
- **Square MD:** [Figma - Square MD](https://www.figma.com/design/12qdRGujqX7DWuY8IYmAnh/Tile---Logo?node-id=4047-819&m=dev)
- **Square LG:** [Figma - Square LG](https://www.figma.com/design/12qdRGujqX7DWuY8IYmAnh/Tile---Logo?node-id=4047-495&m=dev)
- **Rectangle SM:** [Figma - Rectangle SM](https://www.figma.com/design/12qdRGujqX7DWuY8IYmAnh/Tile---Logo?node-id=4049-1350&m=dev)
- **Rectangle MD:** [Figma - Rectangle MD](https://www.figma.com/design/12qdRGujqX7DWuY8IYmAnh/Tile---Logo?node-id=4049-1329&m=dev)
- **Rectangle LG:** [Figma - Rectangle LG](https://www.figma.com/design/12qdRGujqX7DWuY8IYmAnh/Tile---Logo?node-id=4049-1309&m=dev)
---
## Common Patterns
### Partner Showcase Grid
```tsx
<PageGrid>
<PageGridRow>
{partners.map((partner) => (
<PageGridCol key={partner.id} span={{ base: 4, sm: 4, lg: 3 }}>
<TileLogo
shape="square"
variant="neutral"
logo={partner.logo}
alt={partner.name}
href={`/partners/${partner.slug}`}
/>
</PageGridCol>
))}
</PageGridRow>
</PageGrid>
```
### Featured Partners Section
```tsx
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<h2>Featured Partners</h2>
<PageGridRow>
{featuredPartners.map((partner) => (
<PageGridCol key={partner.id} span={{ base: 4, sm: 4, lg: 3 }}>
<TileLogo
shape="square"
variant="green"
logo={partner.logo}
alt={partner.name}
href={`/partners/${partner.slug}`}
/>
</PageGridCol>
))}
</PageGridRow>
</PageGridCol>
</PageGridRow>
</PageGrid>
```
### Partner Banner List
```tsx
<PageGrid>
<PageGridRow>
<PageGridCol span={{ base: 4, sm: 8, lg: 12 }}>
<div className="d-flex flex-column gap-4">
{partners.map((partner) => (
<TileLogo
key={partner.id}
shape="rectangle"
variant={partner.featured ? 'green' : 'neutral'}
logo={partner.logo}
alt={partner.name}
href={`/partners/${partner.slug}`}
/>
))}
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
```

View File

@@ -0,0 +1,379 @@
// TileLogo Component Styles
// Brand Design System - Logo tile/card component
//
// Naming Convention: BEM with 'tile-logo' namespace
// .tile-logo - Base tile (shared layout, transitions)
// .tile-logo--square - Square shape variant (1:1 aspect ratio, default)
// .tile-logo--rectangle - Rectangle shape variant (fixed height)
// .tile-logo--neutral - Neutral variant (gray tones)
// .tile-logo--green - Green variant (brand green tones)
// .tile-logo--hovered - Hovered state (triggers overlay animation)
// .tile-logo--disabled - Disabled state modifier
// .tile-logo__overlay - Hover gradient overlay (window shade animation)
// .tile-logo__image - Logo image element
@import '../../../styles/breakpoints';
// Grid gutter (matching PageGrid)
$bds-grid-gutter: 8px;
// =============================================================================
// Design Tokens
// =============================================================================
// Focus border colors
$tile-logo-focus-border-light: $black;
$tile-logo-focus-border-dark: $white;
// Focus border width
$tile-logo-focus-border-width: 2px;
// Animation (matching CardOffgrid)
$tile-logo-transition-duration: 200ms;
$tile-logo-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
// -----------------------------------------------------------------------------
// Shape Tokens - Square (1:1 aspect ratio)
// -----------------------------------------------------------------------------
$tile-logo-square-padding-sm: 36px 20px; // SM: vertical 36px, horizontal 20px
$tile-logo-square-padding-md: 40px 24px; // MD: vertical 40px, horizontal 24px
$tile-logo-square-padding-lg: 72px 48px; // LG: vertical 72px, horizontal 48px
// -----------------------------------------------------------------------------
// Shape Tokens - Rectangle (fixed height)
// -----------------------------------------------------------------------------
$tile-logo-rect-height-sm: 96px;
$tile-logo-rect-height-lg: 160px;
$tile-logo-rect-padding-sm: 20px 36px; // SM/MD: vertical 20px, horizontal 36px
$tile-logo-rect-padding-lg: 32px 64px; // LG: vertical 32px, horizontal 64px
// =============================================================================
// Base Tile Styles
// =============================================================================
.tile-logo {
// Reset button/anchor styles
appearance: none;
border: none;
background: none;
margin: 0;
font: inherit;
color: inherit;
text-decoration: none;
text-align: center;
// Layout
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
box-sizing: border-box;
// Dimensions controlled by shape modifiers
width: 100%;
// Interaction
cursor: pointer;
// Transitions
transition:
background-color $tile-logo-transition-duration $tile-logo-transition-timing,
opacity $tile-logo-transition-duration $tile-logo-transition-timing;
// Focus styles - Dark Mode (default)
&:focus {
outline: $tile-logo-focus-border-width solid $tile-logo-focus-border-dark;
outline-offset: 1px;
}
&:focus:not(:focus-visible) {
outline: none;
}
&:focus-visible {
outline: $tile-logo-focus-border-width solid $tile-logo-focus-border-dark;
outline-offset: 2px;
}
}
// =============================================================================
// Shape Variants
// =============================================================================
// -----------------------------------------------------------------------------
// Square Shape (default) - 1:1 aspect ratio with responsive padding
// -----------------------------------------------------------------------------
.tile-logo--square {
aspect-ratio: 1;
padding: $tile-logo-square-padding-sm;
@media (min-width: map-get($grid-breakpoints, md)) {
padding: $tile-logo-square-padding-md;
}
@media (min-width: map-get($grid-breakpoints, lg)) {
padding: $tile-logo-square-padding-lg;
}
}
// -----------------------------------------------------------------------------
// Rectangle Shape - fixed height with responsive sizing
// Respects PageGrid column widths: SM (2/4), MD (4/8), LG (4/12)
// Formula: ((100% - (gap * (columns - 1))) / columns) * span + (gap * (span - 1))
// -----------------------------------------------------------------------------
.tile-logo--rectangle {
height: $tile-logo-rect-height-sm;
padding: $tile-logo-rect-padding-sm;
// SM breakpoint: 2 columns out of 4-column grid
// calc(((100% - (8px * (4 - 1))) / 4) * 2 + (8px * (2 - 1)))
width: calc(((100% - (#{$bds-grid-gutter} * 3)) / 4) * 2 + (#{$bds-grid-gutter} * 1));
@media (min-width: map-get($grid-breakpoints, md)) {
// MD breakpoint: 4 columns out of 8-column grid
// calc(((100% - (8px * (8 - 1))) / 8) * 4 + (8px * (4 - 1)))
width: calc(((100% - (#{$bds-grid-gutter} * 7)) / 8) * 4 + (#{$bds-grid-gutter} * 3));
}
@media (min-width: map-get($grid-breakpoints, lg)) {
height: $tile-logo-rect-height-lg;
padding: $tile-logo-rect-padding-lg;
// LG breakpoint: 4 columns out of 12-column grid
// calc(((100% - (8px * (12 - 1))) / 12) * 4 + (8px * (4 - 1)))
width: calc(((100% - (#{$bds-grid-gutter} * 11)) / 12) * 4 + (#{$bds-grid-gutter} * 3));
}
}
// =============================================================================
// Overlay (Color wipe animation - "Window Shade" effect)
// =============================================================================
// Hover in: shade rises from bottom to top (reveals)
// Hover out: shade falls from top to bottom (hides)
.tile-logo__overlay {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
// Default: hidden (shade is "rolled up" at bottom, top is 100% clipped)
// When transitioning TO this state, the top inset increases = shade falls down
clip-path: inset(100% 0 0 0);
transition: clip-path $tile-logo-transition-duration $tile-logo-transition-timing;
}
// Hovered state: shade fully raised (visible)
// When transitioning TO this state, the top inset decreases = shade rises up
.tile-logo--hovered .tile-logo__overlay {
clip-path: inset(0 0 0 0);
}
// =============================================================================
// Logo Image
// =============================================================================
.tile-logo__image {
position: relative;
z-index: 1; // Above overlay
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
}
// =============================================================================
// Light Mode Styles (Default - site uses dark mode as default)
// =============================================================================
// -----------------------------------------------------------------------------
// Neutral Variant - Light Mode
// -----------------------------------------------------------------------------
.tile-logo--neutral {
background-color: $gray-200;
// Overlay color for hover wipe
.tile-logo__overlay {
background-color: $gray-300;
}
// Pressed state
&:active:not(.tile-logo--disabled) {
.tile-logo__overlay {
background-color: $gray-400;
clip-path: inset(0 0 0 0);
}
}
}
// -----------------------------------------------------------------------------
// Green Variant - Light Mode
// -----------------------------------------------------------------------------
.tile-logo--green {
background-color: $green-200;
// Overlay color for hover wipe
.tile-logo__overlay {
background-color: $green-300;
}
// Pressed state
&:active:not(.tile-logo--disabled) {
.tile-logo__overlay {
background-color: $green-400;
clip-path: inset(0 0 0 0);
}
}
}
// -----------------------------------------------------------------------------
// Disabled State - Light Mode
// -----------------------------------------------------------------------------
.tile-logo--disabled {
background-color: $gray-100;
cursor: not-allowed;
pointer-events: none;
&:focus,
&:focus-visible {
outline: none;
}
.tile-logo__image {
opacity: 0.5;
}
}
// =============================================================================
// Dark Mode Styles (using html.dark selector)
// =============================================================================
html.dark {
// Focus styles - Dark Mode
.tile-logo {
&:focus {
outline-color: $tile-logo-focus-border-dark;
}
&:focus-visible {
outline-color: $tile-logo-focus-border-dark;
}
}
// ---------------------------------------------------------------------------
// Neutral Variant - Dark Mode
// ---------------------------------------------------------------------------
.tile-logo--neutral {
background-color: $gray-500;
// Overlay color for hover wipe
.tile-logo__overlay {
background-color: $gray-400;
}
// Pressed state
&:active:not(.tile-logo--disabled) {
.tile-logo__overlay {
background-color: rgba($gray-500, 0.7);
clip-path: inset(0 0 0 0);
}
}
}
// ---------------------------------------------------------------------------
// Green Variant - Dark Mode
// ---------------------------------------------------------------------------
.tile-logo--green {
background-color: $green-200;
// Overlay color for hover wipe
.tile-logo__overlay {
background-color: $green-300;
}
// Pressed state
&:active:not(.tile-logo--disabled) {
.tile-logo__overlay {
background-color: $green-400;
clip-path: inset(0 0 0 0);
}
}
}
// ---------------------------------------------------------------------------
// Disabled State - Dark Mode
// ---------------------------------------------------------------------------
.tile-logo--disabled {
background-color: rgba($gray-500, 0.3);
opacity: 1; // Reset opacity, use background color instead
.tile-logo__image {
opacity: 0.5;
}
}
}
// =============================================================================
// Light Mode Styles (html.light)
// =============================================================================
html.light {
// Focus styles - Light Mode
.tile-logo {
&:focus {
outline-color: $tile-logo-focus-border-light;
}
&:focus-visible {
outline-color: $tile-logo-focus-border-light;
}
}
// ---------------------------------------------------------------------------
// Neutral Variant - Light Mode (explicit for html.light)
// ---------------------------------------------------------------------------
.tile-logo--neutral {
background-color: $gray-200;
.tile-logo__overlay {
background-color: $gray-300;
}
&:active:not(.tile-logo--disabled) {
.tile-logo__overlay {
background-color: $gray-400;
clip-path: inset(0 0 0 0);
}
}
}
// ---------------------------------------------------------------------------
// Green Variant - Light Mode (explicit for html.light)
// ---------------------------------------------------------------------------
.tile-logo--green {
background-color: $green-200;
.tile-logo__overlay {
background-color: $green-300;
}
&:active:not(.tile-logo--disabled) {
.tile-logo__overlay {
background-color: $green-400;
clip-path: inset(0 0 0 0);
}
}
}
// ---------------------------------------------------------------------------
// Disabled State - Light Mode
// ---------------------------------------------------------------------------
.tile-logo--disabled {
background-color: $gray-100;
opacity: 1;
.tile-logo__image {
opacity: 0.5;
}
}
}

View File

@@ -0,0 +1,140 @@
import React, { useState } from 'react';
export interface TileLogoProps {
/** Shape variant: 'square' (default) or 'rectangle' */
shape?: 'square' | 'rectangle';
/** Color variant: 'neutral' (default) or 'green' */
variant?: 'neutral' | 'green';
/** Logo image source (URL or path) */
logo: string;
/** Alt text for accessibility */
alt: string;
/** Click handler - renders as <button> */
onClick?: () => void;
/** Link destination - renders as <a> */
href?: string;
/** Disabled state - prevents interaction */
disabled?: boolean;
/** Additional CSS classes */
className?: string;
}
/**
* TileLogo Component
*
* A tile/card component designed to display brand logos with interactive states.
* Supports two shape variants (Square and Rectangle) and two color variants
* (Neutral and Green) with five interaction states (Default, Hover, Focused,
* Pressed, Disabled).
*
* Features a "window shade" hover animation where the hover color wipes from
* bottom to top on mouse enter, and top to bottom on mouse leave.
*
* Shape sizes are responsive and change based on breakpoints:
* - Square: 1:1 aspect ratio with responsive padding
* - Rectangle: Fixed height (96px SM/MD, 160px LG) with responsive padding
*
* @example
* // Basic usage with link (square shape - default)
* <TileLogo
* variant="neutral"
* logo="/logos/partner-logo.svg"
* alt="Partner Name"
* href="/partners/partner-name"
* />
*
* @example
* // Rectangle shape with click handler
* <TileLogo
* shape="rectangle"
* variant="green"
* logo="/logos/featured-logo.svg"
* alt="Featured Partner"
* onClick={() => openPartnerModal('featured')}
* />
*
* @example
* // Disabled state
* <TileLogo
* variant="neutral"
* logo="/logos/coming-soon.svg"
* alt="Coming Soon"
* disabled
* />
*/
export const TileLogo: React.FC<TileLogoProps> = ({
shape = 'square',
variant = 'neutral',
logo,
alt,
onClick,
href,
disabled = false,
className = '',
}) => {
// Track hover state for animation
const [isHovered, setIsHovered] = useState(false);
// Build class names using BEM convention
const classNames = [
'tile-logo',
`tile-logo--${shape}`,
`tile-logo--${variant}`,
disabled ? 'tile-logo--disabled' : '',
isHovered && !disabled ? 'tile-logo--hovered' : '',
className,
]
.filter(Boolean)
.join(' ');
// Hover handlers
const handleMouseEnter = () => !disabled && setIsHovered(true);
const handleMouseLeave = () => setIsHovered(false);
// Common content (overlay + logo image)
const content = (
<>
{/* Hover overlay for window shade animation */}
<div className="tile-logo__overlay" aria-hidden="true" />
<img
src={logo}
alt={alt}
className="tile-logo__image"
aria-hidden="false"
/>
</>
);
// Render as anchor tag when href is provided
if (href && !disabled) {
return (
<a
href={href}
className={classNames}
aria-label={alt}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{content}
</a>
);
}
// Render as button (for onClick or disabled state)
return (
<button
type="button"
className={classNames}
onClick={onClick}
disabled={disabled}
aria-disabled={disabled}
aria-label={alt}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{content}
</button>
);
};
export default TileLogo;

View File

@@ -0,0 +1,2 @@
export { TileLogo, type TileLogoProps } from './TileLogo';
export { default } from './TileLogo';