mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-12-06 17:27:57 +00:00
Add CardOffgrid component with showcase and documentation
- Introduced the CardOffgrid component, designed for displaying feature highlights with customizable icons, titles, and descriptions. - Implemented two color variants: neutral and green, with interactive states and a unique hover animation. - Created a comprehensive showcase page demonstrating all variants and states of the CardOffgrid component. - Added detailed documentation covering usage guidelines, best practices, and API reference. - Included SCSS styles for the CardOffgrid component, ensuring compatibility with both light and dark themes.
This commit is contained in:
436
shared/components/CardOffgrid/CardOffgrid.md
Normal file
436
shared/components/CardOffgrid/CardOffgrid.md
Normal file
@@ -0,0 +1,436 @@
|
||||
# CardOffgrid Component - Usage Guide
|
||||
|
||||
## Overview
|
||||
|
||||
`CardOffgrid` is a feature highlight card component designed to showcase key capabilities, features, or resources. It combines an icon, title, and description in a visually engaging, interactive card format with smooth hover animations.
|
||||
|
||||
**Use CardOffgrid when:**
|
||||
- Highlighting key features or capabilities
|
||||
- Creating feature grids or showcases
|
||||
- Linking to important documentation or resources
|
||||
- Presenting product/service highlights
|
||||
|
||||
**Don't use CardOffgrid for:**
|
||||
- Simple content cards (use standard Bootstrap cards)
|
||||
- Navigation items (use navigation components)
|
||||
- Data display (use tables or data components)
|
||||
- Long-form content (use article/page layouts)
|
||||
|
||||
---
|
||||
|
||||
## When to Use Each Variant
|
||||
|
||||
### Neutral Variant (`variant="neutral"`)
|
||||
|
||||
**Use for:**
|
||||
- General feature highlights
|
||||
- Standard content cards
|
||||
- Secondary or supporting features
|
||||
- When you want subtle, professional presentation
|
||||
|
||||
**Example use cases:**
|
||||
- Documentation sections
|
||||
- Feature lists
|
||||
- Service offerings
|
||||
- Standard informational cards
|
||||
|
||||
### Green Variant (`variant="green"`)
|
||||
|
||||
**Use for:**
|
||||
- Primary or featured highlights
|
||||
- Call-to-action cards
|
||||
- Important announcements
|
||||
- Brand-emphasized content
|
||||
|
||||
**Example use cases:**
|
||||
- Hero feature cards
|
||||
- Primary CTAs
|
||||
- Featured resources
|
||||
- Branded highlights
|
||||
|
||||
---
|
||||
|
||||
## Content Best Practices
|
||||
|
||||
### Title Guidelines
|
||||
|
||||
✅ **Do:**
|
||||
- Keep titles concise (1-3 words ideal)
|
||||
- Use line breaks (`\n`) for multi-word titles when needed
|
||||
- Make titles action-oriented or descriptive
|
||||
- Examples: "Onchain Metadata", "Token\nManagement", "Cross-Chain\nBridges"
|
||||
|
||||
❌ **Don't:**
|
||||
- Write long sentences as titles
|
||||
- Use more than 2 lines
|
||||
- Include punctuation (periods, commas)
|
||||
- Make titles too generic ("Feature", "Service")
|
||||
|
||||
### Description Guidelines
|
||||
|
||||
✅ **Do:**
|
||||
- Write 1-2 sentences (15-25 words ideal)
|
||||
- Focus on benefits or key information
|
||||
- Use clear, simple language
|
||||
- Keep descriptions scannable
|
||||
|
||||
❌ **Don't:**
|
||||
- Write paragraphs (save for full pages)
|
||||
- Use jargon without context
|
||||
- Include multiple ideas in one description
|
||||
- Make descriptions too short (< 10 words) or too long (> 40 words)
|
||||
|
||||
### Icon Guidelines
|
||||
|
||||
✅ **Do:**
|
||||
- Use SVG icons for crisp rendering
|
||||
- Choose icons that represent the feature clearly
|
||||
- Ensure icons are recognizable at 68×68px
|
||||
- Use consistent icon style across cards
|
||||
|
||||
❌ **Don't:**
|
||||
- Use low-resolution raster images
|
||||
- Choose overly complex icons
|
||||
- Mix icon styles within a single grid
|
||||
- Use icons that don't relate to the content
|
||||
|
||||
---
|
||||
|
||||
## Interaction Patterns
|
||||
|
||||
### Using `onClick` vs `href`
|
||||
|
||||
**Use `onClick` when:**
|
||||
- Triggering JavaScript actions (modals, analytics, state changes)
|
||||
- Opening external links in new tabs
|
||||
- Performing client-side navigation
|
||||
- Handling complex interactions
|
||||
|
||||
```tsx
|
||||
<CardOffgrid
|
||||
variant="neutral"
|
||||
icon={<AnalyticsIcon />}
|
||||
title="View Analytics"
|
||||
description="See detailed usage statistics and insights."
|
||||
onClick={() => {
|
||||
trackEvent('analytics_viewed');
|
||||
openModal('analytics');
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
**Use `href` when:**
|
||||
- Navigating to internal pages
|
||||
- Linking to documentation
|
||||
- Simple page navigation
|
||||
- SEO-friendly links
|
||||
|
||||
```tsx
|
||||
<CardOffgrid
|
||||
variant="green"
|
||||
icon="/icons/docs.svg"
|
||||
title="API\nReference"
|
||||
description="Complete API documentation and examples."
|
||||
href="/docs/api"
|
||||
/>
|
||||
```
|
||||
|
||||
### Disabled State
|
||||
|
||||
Use `disabled` when:
|
||||
- Feature is coming soon
|
||||
- Feature requires authentication
|
||||
- Feature is temporarily unavailable
|
||||
- You want to show but not allow interaction
|
||||
|
||||
```tsx
|
||||
<CardOffgrid
|
||||
variant="neutral"
|
||||
icon={<BetaIcon />}
|
||||
title="Coming\nSoon"
|
||||
description="This feature will be available in the next release."
|
||||
disabled
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layout Best Practices
|
||||
|
||||
### Grid Layouts
|
||||
|
||||
**Recommended grid patterns:**
|
||||
|
||||
```tsx
|
||||
// 2-column grid (desktop)
|
||||
<div className="row">
|
||||
<div className="col-md-6 mb-4">
|
||||
<CardOffgrid {...props1} />
|
||||
</div>
|
||||
<div className="col-md-6 mb-4">
|
||||
<CardOffgrid {...props2} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// 3-column grid (desktop)
|
||||
<div className="row">
|
||||
{cards.map(card => (
|
||||
<div key={card.id} className="col-md-4 mb-4">
|
||||
<CardOffgrid {...card} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Spacing:**
|
||||
- Use Bootstrap spacing utilities (`mb-4`, `mb-5`) between cards
|
||||
- Maintain consistent spacing in grids
|
||||
- Cards are responsive and stack on mobile automatically
|
||||
|
||||
### Single Card Usage
|
||||
|
||||
For hero sections or featured highlights:
|
||||
|
||||
```tsx
|
||||
<div className="d-flex justify-content-center">
|
||||
<CardOffgrid
|
||||
variant="green"
|
||||
icon={<FeaturedIcon />}
|
||||
title="New Feature"
|
||||
description="Introducing our latest capability..."
|
||||
href="/features/new"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Best Practices
|
||||
|
||||
### Semantic HTML
|
||||
|
||||
The component automatically renders as:
|
||||
- `<button>` when using `onClick`
|
||||
- `<a>` when using `href`
|
||||
|
||||
This ensures proper semantic meaning for screen readers.
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
✅ **Always test:**
|
||||
- Tab navigation moves focus to cards
|
||||
- Enter/Space activates cards
|
||||
- Focus ring is clearly visible
|
||||
- Focus order follows logical reading order
|
||||
|
||||
### Screen Reader Content
|
||||
|
||||
✅ **Ensure:**
|
||||
- Titles are descriptive and unique
|
||||
- Descriptions provide context
|
||||
- Icons have appropriate `aria-hidden="true"` (handled automatically)
|
||||
- Disabled cards communicate their state
|
||||
|
||||
### Color Contrast
|
||||
|
||||
All variants meet WCAG AA standards:
|
||||
- Dark mode: White text on colored backgrounds
|
||||
- Light mode: Dark text on light backgrounds
|
||||
- Focus rings provide sufficient contrast
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Feature Showcase Grid
|
||||
|
||||
```tsx
|
||||
const features = [
|
||||
{
|
||||
variant: 'green',
|
||||
icon: <TokenIcon />,
|
||||
title: 'Token\nManagement',
|
||||
description: 'Create and manage fungible and non-fungible tokens.',
|
||||
href: '/docs/tokens'
|
||||
},
|
||||
{
|
||||
variant: 'neutral',
|
||||
icon: <MetadataIcon />,
|
||||
title: 'Onchain\nMetadata',
|
||||
description: 'Store key asset information using simple APIs.',
|
||||
href: '/docs/metadata'
|
||||
},
|
||||
// ... more features
|
||||
];
|
||||
|
||||
<div className="row">
|
||||
{features.map((feature, index) => (
|
||||
<div key={index} className="col-md-4 mb-4">
|
||||
<CardOffgrid {...feature} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Mixed Variants for Hierarchy
|
||||
|
||||
Use green variant for primary features, neutral for supporting:
|
||||
|
||||
```tsx
|
||||
<div className="row">
|
||||
<div className="col-md-6 mb-4">
|
||||
<CardOffgrid
|
||||
variant="green" // Primary feature
|
||||
icon={<PrimaryIcon />}
|
||||
title="Main Feature"
|
||||
description="Our flagship capability..."
|
||||
href="/feature/main"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6 mb-4">
|
||||
<CardOffgrid
|
||||
variant="neutral" // Supporting feature
|
||||
icon={<SupportIcon />}
|
||||
title="Supporting Feature"
|
||||
description="Complementary capability..."
|
||||
href="/feature/support"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Coming Soon Pattern
|
||||
|
||||
```tsx
|
||||
<CardOffgrid
|
||||
variant="neutral"
|
||||
icon={<ComingSoonIcon />}
|
||||
title="Coming\nSoon"
|
||||
description="This feature is currently in development and will be available soon."
|
||||
disabled
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Icon Optimization
|
||||
|
||||
✅ **Best practices:**
|
||||
- Use SVG React components (inlined) for small icons
|
||||
- Use optimized SVG files for image icons
|
||||
- Avoid large raster images
|
||||
- Consider lazy loading for below-the-fold cards
|
||||
|
||||
### Rendering Performance
|
||||
|
||||
- Cards are lightweight components
|
||||
- Hover animations use CSS transforms (GPU-accelerated)
|
||||
- No heavy JavaScript calculations
|
||||
- Suitable for grids with 10+ cards
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Card not clickable:**
|
||||
- Ensure `onClick` or `href` is provided
|
||||
- Check that `disabled` is not set to `true`
|
||||
- Verify no parent element is blocking pointer events
|
||||
|
||||
**Icon not displaying:**
|
||||
- Verify icon path is correct (if using string)
|
||||
- Check icon component is properly imported
|
||||
- Ensure icon fits within 68×68px bounds
|
||||
|
||||
**Hover animation not working:**
|
||||
- Check browser supports CSS `clip-path`
|
||||
- Verify no conflicting CSS is overriding transitions
|
||||
- Test in different browsers
|
||||
|
||||
**Focus ring not visible:**
|
||||
- Ensure keyboard navigation (Tab key)
|
||||
- Check focus ring color contrasts with background
|
||||
- Verify `outline-offset: 2px` is applied
|
||||
|
||||
---
|
||||
|
||||
## Design System Integration
|
||||
|
||||
### Color Tokens
|
||||
|
||||
All colors reference `styles/_colors.scss`:
|
||||
- Dark mode (default): Uses `$gray-500`, `$gray-400`, `$green-300`, `$green-200`
|
||||
- Light mode (`html.light`): Uses `$gray-200`, `$gray-300`, `$green-200`, `$green-300`
|
||||
|
||||
### Typography
|
||||
|
||||
- Title: Booton Light, 32px, -1px letter-spacing
|
||||
- Description: Booton Light, 18px, -0.5px letter-spacing
|
||||
|
||||
### Spacing
|
||||
|
||||
- Card padding: 24px
|
||||
- Content gap: 40px (between title and description)
|
||||
- Icon container: 84×84px
|
||||
|
||||
---
|
||||
|
||||
## Figma References
|
||||
|
||||
- **Light Mode Colors**: [Figma Design - Light Mode](https://www.figma.com/design/vwDwMJ3mFrAklj5zvZwX5M/Card---OffGrid?node-id=8001-1963&m=dev)
|
||||
- **Dark Mode Colors**: [Figma Design - Dark Mode](https://www.figma.com/design/vwDwMJ3mFrAklj5zvZwX5M/Card---OffGrid?node-id=8001-2321&m=dev)
|
||||
- **Animation Specs**: [Figma Design - Storyboard](https://www.figma.com/design/vwDwMJ3mFrAklj5zvZwX5M/Card---OffGrid?node-id=8007-1096&m=dev)
|
||||
|
||||
---
|
||||
|
||||
## Component API
|
||||
|
||||
```typescript
|
||||
interface CardOffgridProps {
|
||||
/** Color variant: 'neutral' (default) or 'green' */
|
||||
variant?: 'neutral' | 'green';
|
||||
|
||||
/** Icon element (ReactNode) or image path (string) */
|
||||
icon: React.ReactNode | string;
|
||||
|
||||
/** Card title - use \n for line breaks */
|
||||
title: string;
|
||||
|
||||
/** Card description text (1-2 sentences) */
|
||||
description: 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;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Use Case | Variant | Interaction |
|
||||
|----------|---------|-------------|
|
||||
| Standard feature | `neutral` | `href` or `onClick` |
|
||||
| Primary feature | `green` | `href` or `onClick` |
|
||||
| Coming soon | `neutral` | `disabled` |
|
||||
| Feature grid | Mix both | `href` preferred |
|
||||
| Hero section | `green` | `href` |
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
See the [CardOffgrid Showcase](/about/card-offgrid-showcase) for live examples and interactive demos.
|
||||
376
shared/components/CardOffgrid/CardOffgrid.scss
Normal file
376
shared/components/CardOffgrid/CardOffgrid.scss
Normal file
@@ -0,0 +1,376 @@
|
||||
// BDS CardOffgrid Component Styles
|
||||
// Brand Design System - Feature card with icon, title, and description
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-card-offgrid - Base card (resets button/anchor styles)
|
||||
// .bds-card-offgrid--neutral - Neutral gray color variant (default)
|
||||
// .bds-card-offgrid--green - Green color variant
|
||||
// .bds-card-offgrid--disabled - Disabled state
|
||||
// .bds-card-offgrid__overlay - Hover gradient overlay
|
||||
// .bds-card-offgrid__icon-container - Icon wrapper (84x84px)
|
||||
// .bds-card-offgrid__icon-image - Image icon styling
|
||||
// .bds-card-offgrid__content - Title and description wrapper
|
||||
// .bds-card-offgrid__title - Card title (32px)
|
||||
// .bds-card-offgrid__description - Card description (18px)
|
||||
//
|
||||
// Note: This file is imported within xrpl.scss after Bootstrap and project
|
||||
// variables are loaded, so $grid-breakpoints, colors, and mixins are available.
|
||||
//
|
||||
// Theme: Site defaults to DARK mode. Light mode uses html.light selector.
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
|
||||
// Dimensions (from Figma design spec)
|
||||
$bds-card-offgrid-width: 400px;
|
||||
$bds-card-offgrid-height: 480px;
|
||||
$bds-card-offgrid-padding: 24px;
|
||||
$bds-card-offgrid-icon-container: 84px;
|
||||
$bds-card-offgrid-icon-size: 68px;
|
||||
$bds-card-offgrid-content-gap: 40px;
|
||||
|
||||
// Animation (from Figma design spec)
|
||||
$bds-card-offgrid-transition-duration: 200ms;
|
||||
$bds-card-offgrid-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
|
||||
// Typography - Title
|
||||
$bds-card-offgrid-title-size: 32px;
|
||||
$bds-card-offgrid-title-line-height: 40px;
|
||||
$bds-card-offgrid-title-letter-spacing: -1px;
|
||||
|
||||
// Typography - Description
|
||||
$bds-card-offgrid-desc-size: 18px;
|
||||
$bds-card-offgrid-desc-line-height: 26.1px;
|
||||
$bds-card-offgrid-desc-letter-spacing: -0.5px;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Dark Mode Colors (Default)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Neutral variant - Dark Mode
|
||||
$bds-card-offgrid-neutral-default-dark: $gray-500; // #72777E
|
||||
$bds-card-offgrid-neutral-hover-dark: $gray-400; // #8A919A
|
||||
$bds-card-offgrid-neutral-pressed-dark: rgba($gray-500, 0.7); // 70% opacity
|
||||
$bds-card-offgrid-neutral-text-dark: $white; // #FFFFFF
|
||||
|
||||
// Green variant - Dark Mode
|
||||
$bds-card-offgrid-green-default-dark: $green-300; // #21E46B
|
||||
$bds-card-offgrid-green-hover-dark: $green-200; // #70EE97
|
||||
$bds-card-offgrid-green-pressed-dark: $green-400; // #0DAA3E
|
||||
$bds-card-offgrid-green-text-dark: $black; // #000000
|
||||
|
||||
// Disabled - Dark Mode (30% opacity on gray-500)
|
||||
$bds-card-offgrid-disabled-opacity-dark: 0.3;
|
||||
$bds-card-offgrid-disabled-text-dark: $white; // #FFFFFF
|
||||
|
||||
// Focus ring - Dark Mode
|
||||
$bds-card-offgrid-focus-color-dark: $white; // #FFFFFF
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Light Mode Colors
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Neutral variant - Light Mode
|
||||
$bds-card-offgrid-neutral-default-light: $gray-200; // #E6EAF0
|
||||
$bds-card-offgrid-neutral-hover-light: $gray-300; // #CAD4DF
|
||||
$bds-card-offgrid-neutral-pressed-light: $gray-400; // #8A919A
|
||||
$bds-card-offgrid-neutral-text-light: $gray-900; // #111112
|
||||
$bds-card-offgrid-neutral-text-hover-light: $black; // #000000
|
||||
|
||||
// Green variant - Light Mode
|
||||
$bds-card-offgrid-green-default-light: $green-200; // #70EE97
|
||||
$bds-card-offgrid-green-hover-light: $green-300; // #21E46B
|
||||
$bds-card-offgrid-green-pressed-light: $green-400; // #0DAA3E
|
||||
$bds-card-offgrid-green-text-light: $black; // #000000
|
||||
|
||||
// Disabled - Light Mode
|
||||
$bds-card-offgrid-disabled-bg-light: $gray-100; // #F0F3F7
|
||||
$bds-card-offgrid-disabled-text-light: $gray-500; // #72777E
|
||||
|
||||
// Focus ring - Light Mode
|
||||
$bds-card-offgrid-focus-color-light: $gray-900; // #111112
|
||||
|
||||
// =============================================================================
|
||||
// Base Card Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-card-offgrid {
|
||||
// Reset button/anchor styles
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
||||
// Card layout
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: $bds-card-offgrid-width;
|
||||
height: $bds-card-offgrid-height;
|
||||
padding: $bds-card-offgrid-padding;
|
||||
overflow: hidden;
|
||||
|
||||
// Animation
|
||||
transition: background-color $bds-card-offgrid-transition-duration $bds-card-offgrid-transition-timing,
|
||||
opacity $bds-card-offgrid-transition-duration $bds-card-offgrid-transition-timing;
|
||||
|
||||
// Focus styles - Dark Mode (default)
|
||||
// 1px gap between card and focus ring
|
||||
&:focus {
|
||||
outline: 2px solid $bds-card-offgrid-focus-color-dark;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $bds-card-offgrid-focus-color-dark;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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)
|
||||
|
||||
.bds-card-offgrid__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 $bds-card-offgrid-transition-duration $bds-card-offgrid-transition-timing;
|
||||
}
|
||||
|
||||
// Hovered state: shade fully raised (visible)
|
||||
// When transitioning TO this state, the top inset decreases = shade rises up
|
||||
.bds-card-offgrid--hovered .bds-card-offgrid__overlay {
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Icon Container
|
||||
// =============================================================================
|
||||
|
||||
.bds-card-offgrid__icon-container {
|
||||
position: relative;
|
||||
z-index: 1; // Above overlay
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: $bds-card-offgrid-icon-container;
|
||||
height: $bds-card-offgrid-icon-container;
|
||||
flex-shrink: 0;
|
||||
|
||||
// Icon sizing
|
||||
> * {
|
||||
max-width: $bds-card-offgrid-icon-size;
|
||||
max-height: $bds-card-offgrid-icon-size;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-card-offgrid__icon-image {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: $bds-card-offgrid-icon-size;
|
||||
max-height: $bds-card-offgrid-icon-size;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Content (Title + Description)
|
||||
// =============================================================================
|
||||
|
||||
.bds-card-offgrid__content {
|
||||
position: relative;
|
||||
z-index: 1; // Above overlay
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-card-offgrid-content-gap;
|
||||
}
|
||||
|
||||
.bds-card-offgrid__title {
|
||||
font-size: $bds-card-offgrid-title-size;
|
||||
font-weight: 300; // Light
|
||||
line-height: $bds-card-offgrid-title-line-height;
|
||||
letter-spacing: $bds-card-offgrid-title-letter-spacing;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.bds-card-offgrid__description {
|
||||
font-size: $bds-card-offgrid-desc-size;
|
||||
font-weight: 300; // Light
|
||||
line-height: $bds-card-offgrid-desc-line-height;
|
||||
letter-spacing: $bds-card-offgrid-desc-letter-spacing;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DARK MODE (Default)
|
||||
// =============================================================================
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Neutral Variant - Dark Mode
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.bds-card-offgrid--neutral {
|
||||
background-color: $bds-card-offgrid-neutral-default-dark;
|
||||
color: $bds-card-offgrid-neutral-text-dark;
|
||||
|
||||
// Overlay color for hover wipe
|
||||
.bds-card-offgrid__overlay {
|
||||
background-color: $bds-card-offgrid-neutral-hover-dark;
|
||||
}
|
||||
|
||||
// Pressed state
|
||||
&:active:not(.bds-card-offgrid--disabled) {
|
||||
.bds-card-offgrid__overlay {
|
||||
background-color: $bds-card-offgrid-neutral-pressed-dark;
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Green Variant - Dark Mode
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.bds-card-offgrid--green {
|
||||
background-color: $bds-card-offgrid-green-default-dark;
|
||||
color: $bds-card-offgrid-green-text-dark;
|
||||
|
||||
// Overlay color for hover wipe
|
||||
.bds-card-offgrid__overlay {
|
||||
background-color: $bds-card-offgrid-green-hover-dark;
|
||||
}
|
||||
|
||||
// Pressed state
|
||||
&:active:not(.bds-card-offgrid--disabled) {
|
||||
.bds-card-offgrid__overlay {
|
||||
background-color: $bds-card-offgrid-green-pressed-dark;
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Disabled State - Dark Mode
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.bds-card-offgrid--disabled {
|
||||
background-color: $bds-card-offgrid-neutral-default-dark;
|
||||
color: $bds-card-offgrid-disabled-text-dark;
|
||||
opacity: $bds-card-offgrid-disabled-opacity-dark;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LIGHT MODE (html.light)
|
||||
// =============================================================================
|
||||
|
||||
html.light {
|
||||
// Focus styles - Light Mode
|
||||
.bds-card-offgrid {
|
||||
&:focus {
|
||||
outline-color: $bds-card-offgrid-focus-color-light;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline-color: $bds-card-offgrid-focus-color-light;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Neutral Variant - Light Mode
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.bds-card-offgrid--neutral {
|
||||
background-color: $bds-card-offgrid-neutral-default-light;
|
||||
color: $bds-card-offgrid-neutral-text-light;
|
||||
|
||||
// Overlay color for hover wipe
|
||||
.bds-card-offgrid__overlay {
|
||||
background-color: $bds-card-offgrid-neutral-hover-light;
|
||||
}
|
||||
|
||||
// Text color on hover
|
||||
&.bds-card-offgrid--hovered {
|
||||
color: $bds-card-offgrid-neutral-text-hover-light;
|
||||
}
|
||||
|
||||
// Pressed state
|
||||
&:active:not(.bds-card-offgrid--disabled) {
|
||||
.bds-card-offgrid__overlay {
|
||||
background-color: $bds-card-offgrid-neutral-pressed-light;
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Green Variant - Light Mode
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.bds-card-offgrid--green {
|
||||
background-color: $bds-card-offgrid-green-default-light;
|
||||
color: $bds-card-offgrid-green-text-light;
|
||||
|
||||
// Overlay color for hover wipe
|
||||
.bds-card-offgrid__overlay {
|
||||
background-color: $bds-card-offgrid-green-hover-light;
|
||||
}
|
||||
|
||||
// Pressed state
|
||||
&:active:not(.bds-card-offgrid--disabled) {
|
||||
.bds-card-offgrid__overlay {
|
||||
background-color: $bds-card-offgrid-green-pressed-light;
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Disabled State - Light Mode
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.bds-card-offgrid--disabled {
|
||||
background-color: $bds-card-offgrid-disabled-bg-light;
|
||||
color: $bds-card-offgrid-disabled-text-light;
|
||||
opacity: 1; // Reset opacity, use background color instead
|
||||
|
||||
.bds-card-offgrid__icon-container {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Responsive Styles
|
||||
// =============================================================================
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.bds-card-offgrid {
|
||||
width: 100%;
|
||||
min-height: $bds-card-offgrid-height;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
167
shared/components/CardOffgrid/CardOffgrid.tsx
Normal file
167
shared/components/CardOffgrid/CardOffgrid.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
|
||||
export interface CardOffgridProps {
|
||||
/** Color variant of the card */
|
||||
variant?: 'neutral' | 'green';
|
||||
/** Icon element or image source */
|
||||
icon: React.ReactNode | string;
|
||||
/** Card title (supports multi-line via \n) */
|
||||
title: string;
|
||||
/** Card description text */
|
||||
description: string;
|
||||
/** Click handler */
|
||||
onClick?: () => void;
|
||||
/** Link destination (renders as anchor if provided) */
|
||||
href?: string;
|
||||
/** Disabled state */
|
||||
disabled?: boolean;
|
||||
/** Optional className for custom styling */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* BDS CardOffgrid Component
|
||||
*
|
||||
* A versatile card component for displaying feature highlights with an icon,
|
||||
* title, and description. Supports neutral and green color variants with
|
||||
* interactive states (hover, focus, pressed, disabled).
|
||||
*
|
||||
* Features a "window shade" color wipe animation:
|
||||
* - Hover in: shade rises from bottom to top (reveals hover color)
|
||||
* - Hover out: shade falls from top to bottom (hides hover color)
|
||||
*
|
||||
* @example
|
||||
* // Basic neutral card
|
||||
* <CardOffgrid
|
||||
* variant="neutral"
|
||||
* icon={<MetadataIcon />}
|
||||
* title="Onchain Metadata"
|
||||
* description="Easily store key asset information."
|
||||
* onClick={() => console.log('clicked')}
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Green card with link
|
||||
* <CardOffgrid
|
||||
* variant="green"
|
||||
* icon="/icons/metadata.svg"
|
||||
* title="Onchain Metadata"
|
||||
* description="Easily store key asset information."
|
||||
* href="/docs/metadata"
|
||||
* />
|
||||
*/
|
||||
export const CardOffgrid: React.FC<CardOffgridProps> = ({
|
||||
variant = 'neutral',
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
onClick,
|
||||
href,
|
||||
disabled = false,
|
||||
className = '',
|
||||
}) => {
|
||||
// Track hover state for animation
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
if (!disabled) {
|
||||
setIsHovered(true);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
if (!disabled) {
|
||||
setIsHovered(false);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
// Build class names using BEM with bds namespace
|
||||
const classNames = [
|
||||
'bds-card-offgrid',
|
||||
`bds-card-offgrid--${variant}`,
|
||||
disabled && 'bds-card-offgrid--disabled',
|
||||
isHovered && 'bds-card-offgrid--hovered',
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
// Render icon - supports both React nodes and image URLs
|
||||
const renderIcon = () => {
|
||||
if (typeof icon === 'string') {
|
||||
return (
|
||||
<img
|
||||
src={icon}
|
||||
alt=""
|
||||
className="bds-card-offgrid__icon-image"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
);
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
|
||||
// Split title by newline for multi-line support
|
||||
const renderTitle = () => {
|
||||
const lines = title.split('\n');
|
||||
return lines.map((line, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{line}
|
||||
{index < lines.length - 1 && <br />}
|
||||
</React.Fragment>
|
||||
));
|
||||
};
|
||||
|
||||
// Common content for both button and anchor
|
||||
const content = (
|
||||
<>
|
||||
{/* Hover color wipe overlay */}
|
||||
<span className="bds-card-offgrid__overlay" aria-hidden="true" />
|
||||
|
||||
<span className="bds-card-offgrid__icon-container">
|
||||
{renderIcon()}
|
||||
</span>
|
||||
|
||||
<span className="bds-card-offgrid__content">
|
||||
<span className="bds-card-offgrid__title">
|
||||
{renderTitle()}
|
||||
</span>
|
||||
<span className="bds-card-offgrid__description">
|
||||
{description}
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
// Render as anchor if href is provided
|
||||
if (href && !disabled) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className={classNames}
|
||||
aria-disabled={disabled}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
// Render as button for onClick or disabled state
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
disabled={disabled}
|
||||
aria-disabled={disabled}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CardOffgrid;
|
||||
2
shared/components/CardOffgrid/index.ts
Normal file
2
shared/components/CardOffgrid/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { CardOffgrid, type CardOffgridProps } from './CardOffgrid';
|
||||
export { default } from './CardOffgrid';
|
||||
Reference in New Issue
Block a user