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:
akcodez
2025-12-05 11:41:18 -08:00
parent 2dbb111943
commit 57898ab010
8 changed files with 2042 additions and 218 deletions

View File

@@ -0,0 +1,668 @@
import * as React from "react";
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
import { CardOffgrid } from "shared/components/CardOffgrid";
import { Divider } from "shared/components/Divider";
export const frontmatter = {
seo: {
title: 'CardOffgrid Component Showcase',
description: "A comprehensive showcase of all CardOffgrid component variants, states, and interactions in the XRPL.org Design System.",
}
};
// Sample icon component for demonstration
const SampleIcon = () => (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M34 8L58 20V44L34 56L10 44V20L34 8Z" stroke="currentColor" strokeWidth="2" fill="none"/>
<path d="M34 8V32M34 32L58 20M34 32L10 20" stroke="currentColor" strokeWidth="2"/>
<path d="M34 32V56" stroke="currentColor" strokeWidth="2"/>
<circle cx="34" cy="32" r="6" fill="currentColor"/>
</svg>
);
// Alternative icon for variety
const MetadataIcon = () => (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 18C14 15.7909 15.7909 14 18 14H50C52.2091 14 54 15.7909 54 18V50C54 52.2091 52.2091 54 50 54H18C15.7909 54 14 52.2091 14 50V18Z" stroke="currentColor" strokeWidth="2"/>
<path d="M22 26H46M22 34H46M22 42H34" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
// Chain icon
const ChainIcon = () => (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28 34H40M24 28C24 25.7909 25.7909 24 28 24H32C34.2091 24 36 25.7909 36 28V40C36 42.2091 34.2091 44 32 44H28C25.7909 44 24 42.2091 24 40V28Z" stroke="currentColor" strokeWidth="2"/>
<path d="M32 28C32 25.7909 33.7909 24 36 24H40C42.2091 24 44 25.7909 44 28V40C44 42.2091 42.2091 44 40 44H36C33.7909 44 32 42.2091 32 40V28Z" stroke="currentColor" strokeWidth="2"/>
</svg>
);
export default function CardOffgridShowcase() {
const [clickedCard, setClickedCard] = React.useState<string | null>(null);
const handleCardClick = (cardName: string) => {
setClickedCard(cardName);
setTimeout(() => setClickedCard(null), 1500);
};
return (
<div className="landing">
<div className="overflow-hidden">
{/* Hero Section */}
<section className="py-26 text-center">
<div className="col-lg-8 mx-auto">
<h6 className="eyebrow mb-3">Component Showcase</h6>
<h1 className="mb-4">CardOffgrid Component</h1>
<p className="longform">
A versatile card component for displaying feature highlights with an icon, title, and description.
Supports neutral and green color variants with interactive states and bottom-to-top gradient hover animation.
</p>
</div>
</section>
{/* Variant Showcase */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Color Variants</h2>
<p className="mb-6">CardOffgrid supports two color variants: <strong>neutral</strong> (default) and <strong>green</strong>.</p>
<div className="d-flex flex-row gap-6 mb-6" style={{ flexWrap: 'wrap' }}>
<div>
<h6 className="mb-3">Neutral Variant (Default)</h6>
<CardOffgrid
variant="neutral"
icon={<SampleIcon />}
title={"Onchain\nMetadata"}
description="Easily store key asset information or link to off-chain data using simple APIs, giving token holders transparency."
onClick={() => handleCardClick('neutral')}
/>
{clickedCard === 'neutral' && (
<p className="mt-2 text-success"> Card clicked!</p>
)}
</div>
<div>
<h6 className="mb-3">Green Variant</h6>
<CardOffgrid
variant="green"
icon={<SampleIcon />}
title={"Onchain\nMetadata"}
description="Easily store key asset information or link to off-chain data using simple APIs, giving token holders transparency."
onClick={() => handleCardClick('green')}
/>
{clickedCard === 'green' && (
<p className="mt-2 text-success"> Card clicked!</p>
)}
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Interactive States */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Interactive States</h2>
<p className="mb-6">Hover, focus, and press the cards below to see the state transitions.</p>
{/* Neutral States */}
<h5 className="mb-4">Neutral Variant States</h5>
<div className="d-flex flex-row gap-4 mb-8" style={{ flexWrap: 'wrap' }}>
<div className="text-center">
<small className="d-block mb-2 text-muted">Default</small>
<CardOffgrid
variant="neutral"
icon={<MetadataIcon />}
title={"Token\nManagement"}
description="Create and manage fungible and non-fungible tokens with built-in compliance features."
onClick={() => handleCardClick('neutral-default')}
/>
</div>
<div className="text-center">
<small className="d-block mb-2 text-muted">Disabled</small>
<CardOffgrid
variant="neutral"
icon={<MetadataIcon />}
title={"Token\nManagement"}
description="Create and manage fungible and non-fungible tokens with built-in compliance features."
disabled
/>
</div>
</div>
{/* Green States */}
<h5 className="mb-4">Green Variant States</h5>
<div className="d-flex flex-row gap-4 mb-6" style={{ flexWrap: 'wrap' }}>
<div className="text-center">
<small className="d-block mb-2 text-muted">Default</small>
<CardOffgrid
variant="green"
icon={<ChainIcon />}
title={"Cross-Chain\nBridges"}
description="Connect XRPL with other blockchain networks through secure and efficient bridge protocols."
onClick={() => handleCardClick('green-default')}
/>
</div>
<div className="text-center">
<small className="d-block mb-2 text-muted">Disabled</small>
<CardOffgrid
variant="green"
icon={<ChainIcon />}
title={"Cross-Chain\nBridges"}
description="Connect XRPL with other blockchain networks through secure and efficient bridge protocols."
disabled
/>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Color Palette */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Color Palette</h2>
<p className="mb-6">
All colors are mapped from <code>styles/_colors.scss</code>.
The site defaults to <strong>dark mode</strong>. Light mode is activated via <code>html.light</code>.
</p>
{/* Dark Mode Colors */}
<h5 className="mb-4">Dark Mode (Default)</h5>
<div className="d-flex flex-row gap-6 mb-6" style={{ flexWrap: 'wrap' }}>
{/* Neutral Colors - Dark */}
<div style={{ flex: '1 1 400px', minWidth: '320px' }}>
<h6 className="mb-4">Neutral Variant</h6>
<div className="d-flex flex-column gap-3">
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: '#72777E', borderRadius: '4px', flexShrink: 0, border: '1px solid #444' }}></div>
<div>
<strong>Default:</strong> <code>$gray-500</code>
<br />
<small className="text-muted">#72777E (white text)</small>
</div>
</div>
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: '#8A919A', borderRadius: '4px', flexShrink: 0, border: '1px solid #444' }}></div>
<div>
<strong>Hover/Focus:</strong> <code>$gray-400</code>
<br />
<small className="text-muted">#8A919A (white text)</small>
</div>
</div>
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: 'rgba(114, 119, 126, 0.7)', borderRadius: '4px', flexShrink: 0, border: '1px solid #444' }}></div>
<div>
<strong>Pressed:</strong> <code>rgba($gray-500, 0.7)</code>
<br />
<small className="text-muted">70% opacity</small>
</div>
</div>
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: '#72777E', borderRadius: '4px', flexShrink: 0, border: '1px solid #444', opacity: 0.3 }}></div>
<div>
<strong>Disabled:</strong> <code>$gray-500 @ 30%</code>
<br />
<small className="text-muted">opacity: 0.3</small>
</div>
</div>
</div>
</div>
{/* Green Colors - Dark */}
<div style={{ flex: '1 1 400px', minWidth: '320px' }}>
<h6 className="mb-4">Green Variant</h6>
<div className="d-flex flex-column gap-3">
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: '#21E46B', borderRadius: '4px', flexShrink: 0, border: '1px solid #444' }}></div>
<div>
<strong>Default:</strong> <code>$green-300</code>
<br />
<small className="text-muted">#21E46B (black text)</small>
</div>
</div>
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: '#70EE97', borderRadius: '4px', flexShrink: 0, border: '1px solid #444' }}></div>
<div>
<strong>Hover/Focus:</strong> <code>$green-200</code>
<br />
<small className="text-muted">#70EE97 (black text)</small>
</div>
</div>
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: '#0DAA3E', borderRadius: '4px', flexShrink: 0, border: '1px solid #444' }}></div>
<div>
<strong>Pressed:</strong> <code>$green-400</code>
<br />
<small className="text-muted">#0DAA3E (black text)</small>
</div>
</div>
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: '#72777E', borderRadius: '4px', flexShrink: 0, border: '1px solid #444', opacity: 0.3 }}></div>
<div>
<strong>Disabled:</strong> <code>$gray-500 @ 30%</code>
<br />
<small className="text-muted">opacity: 0.3 (white text)</small>
</div>
</div>
</div>
</div>
</div>
<Divider color="gray" className="my-6" />
{/* Light Mode Colors */}
<h5 className="mb-4">Light Mode (<code>html.light</code>)</h5>
<div className="d-flex flex-row gap-6 mb-6" style={{ flexWrap: 'wrap' }}>
{/* Neutral Colors - Light */}
<div style={{ flex: '1 1 400px', minWidth: '320px' }}>
<h6 className="mb-4">Neutral Variant</h6>
<div className="d-flex flex-column gap-3">
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: '#E6EAF0', borderRadius: '4px', flexShrink: 0, border: '1px solid #ccc' }}></div>
<div>
<strong>Default:</strong> <code>$gray-200</code>
<br />
<small className="text-muted">#E6EAF0 (dark text)</small>
</div>
</div>
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: '#CAD4DF', borderRadius: '4px', flexShrink: 0, border: '1px solid #ccc' }}></div>
<div>
<strong>Hover/Focus:</strong> <code>$gray-300</code>
<br />
<small className="text-muted">#CAD4DF (black text)</small>
</div>
</div>
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: '#8A919A', borderRadius: '4px', flexShrink: 0, border: '1px solid #ccc' }}></div>
<div>
<strong>Pressed:</strong> <code>$gray-400</code>
<br />
<small className="text-muted">#8A919A (black text)</small>
</div>
</div>
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: '#F0F3F7', borderRadius: '4px', flexShrink: 0, border: '1px solid #ccc' }}></div>
<div>
<strong>Disabled:</strong> <code>$gray-100</code>
<br />
<small className="text-muted">#F0F3F7 (gray text)</small>
</div>
</div>
</div>
</div>
{/* Green Colors - Light */}
<div style={{ flex: '1 1 400px', minWidth: '320px' }}>
<h6 className="mb-4">Green Variant</h6>
<div className="d-flex flex-column gap-3">
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: '#70EE97', borderRadius: '4px', flexShrink: 0, border: '1px solid #ccc' }}></div>
<div>
<strong>Default:</strong> <code>$green-200</code>
<br />
<small className="text-muted">#70EE97 (black text)</small>
</div>
</div>
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: '#21E46B', borderRadius: '4px', flexShrink: 0, border: '1px solid #ccc' }}></div>
<div>
<strong>Hover/Focus:</strong> <code>$green-300</code>
<br />
<small className="text-muted">#21E46B (black text)</small>
</div>
</div>
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: '#0DAA3E', borderRadius: '4px', flexShrink: 0, border: '1px solid #ccc' }}></div>
<div>
<strong>Pressed:</strong> <code>$green-400</code>
<br />
<small className="text-muted">#0DAA3E (black text)</small>
</div>
</div>
<div className="d-flex flex-row align-items-center gap-3">
<div style={{ width: '60px', height: '40px', backgroundColor: '#F0F3F7', borderRadius: '4px', flexShrink: 0, border: '1px solid #ccc' }}></div>
<div>
<strong>Disabled:</strong> <code>$gray-100</code>
<br />
<small className="text-muted">#F0F3F7 (gray text)</small>
</div>
</div>
</div>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Animation Details */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Animation Specifications</h2>
<div className="d-flex flex-row gap-6 mb-6" style={{ flexWrap: 'wrap' }}>
<div style={{ flex: '1 1 300px' }}>
<h6 className="mb-3">Timing</h6>
<ul className="mb-0">
<li><strong>Duration:</strong> 200ms</li>
<li><strong>Easing:</strong> <code>cubic-bezier(0.98, 0.12, 0.12, 0.98)</code></li>
</ul>
</div>
<div style={{ flex: '1 1 300px' }}>
<h6 className="mb-3">Hover Effect ("Window Shade")</h6>
<ul className="mb-0">
<li><strong>Hover in:</strong> Shade rises up (bottom top)</li>
<li><strong>Hover out:</strong> Shade falls down (top bottom)</li>
<li>Darker pressed state on click</li>
</ul>
</div>
<div style={{ flex: '1 1 300px' }}>
<h6 className="mb-3">State Flow</h6>
<ul className="mb-0">
<li>Default Hover Pressed</li>
<li>Full card area is clickable</li>
<li>Focus ring on keyboard navigation</li>
</ul>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Link vs Button */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Link vs Button Rendering</h2>
<p className="mb-6">The component renders as an <code>&lt;a&gt;</code> tag when <code>href</code> is provided, otherwise as a <code>&lt;button&gt;</code>.</p>
<div className="d-flex flex-row gap-6 mb-6" style={{ flexWrap: 'wrap' }}>
<div>
<h6 className="mb-3">As Button (onClick)</h6>
<CardOffgrid
variant="neutral"
icon={<SampleIcon />}
title={"Click Me"}
description="This card renders as a button element and triggers an onClick handler."
onClick={() => handleCardClick('button-demo')}
/>
{clickedCard === 'button-demo' && (
<p className="mt-2 text-success"> Button clicked!</p>
)}
</div>
<div>
<h6 className="mb-3">As Link (href)</h6>
<CardOffgrid
variant="green"
icon={<SampleIcon />}
title={"Navigate"}
description="This card renders as an anchor element and navigates to the specified href."
href="#link-demo"
/>
<p className="mt-2 text-muted"> Click to navigate to #link-demo</p>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Dimensions */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Dimensions</h2>
<div className="mb-6">
{/* Header Row */}
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
<div style={{ width: '180px', flexShrink: 0 }}><strong>Property</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Value</strong></div>
</div>
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '180px', flexShrink: 0 }}>Card Width</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>400px</code> (full-width on mobile)</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '180px', flexShrink: 0 }}>Card Height</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>480px</code></div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '180px', flexShrink: 0 }}>Padding</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>24px</code></div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '180px', flexShrink: 0 }}>Icon Container</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>84px × 84px</code></div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '180px', flexShrink: 0 }}>Icon Size</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>~68px × 68px</code></div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '180px', flexShrink: 0 }}>Content Gap</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>40px</code> (between title and description)</div>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Typography */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Typography</h2>
<div className="d-flex flex-row gap-6 mb-6" style={{ flexWrap: 'wrap' }}>
<div style={{ flex: '1 1 300px' }}>
<h6 className="mb-3">Title</h6>
<ul className="mb-0">
<li><strong>Font Size:</strong> 32px</li>
<li><strong>Font Weight:</strong> 300 (light)</li>
<li><strong>Line Height:</strong> 40px</li>
<li><strong>Letter Spacing:</strong> -1px</li>
</ul>
</div>
<div style={{ flex: '1 1 300px' }}>
<h6 className="mb-3">Description</h6>
<ul className="mb-0">
<li><strong>Font Size:</strong> 18px</li>
<li><strong>Font Weight:</strong> 300 (light)</li>
<li><strong>Line Height:</strong> 26.1px</li>
<li><strong>Letter Spacing:</strong> -0.5px</li>
</ul>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* API Reference */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Component API</h2>
<div className="mb-10">
{/* Header Row */}
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
<div style={{ width: '120px', flexShrink: 0 }}><strong>Prop</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Type</strong></div>
<div style={{ width: '100px', flexShrink: 0 }}><strong>Default</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Description</strong></div>
</div>
{/* variant */}
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>variant</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>'neutral' | 'green'</code></div>
<div style={{ width: '100px', flexShrink: 0 }}><code>'neutral'</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Color variant of the card</div>
</div>
<Divider weight="thin" color="gray" />
{/* icon */}
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>icon</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>ReactNode | string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Icon element or image URL</div>
</div>
<Divider weight="thin" color="gray" />
{/* title */}
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>title</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Card title (use \n for line breaks)</div>
</div>
<Divider weight="thin" color="gray" />
{/* description */}
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>description</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Card description text</div>
</div>
<Divider weight="thin" color="gray" />
{/* onClick */}
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>onClick</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>() =&gt; void</code></div>
<div style={{ width: '100px', flexShrink: 0 }}><code>undefined</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Click handler (renders as button)</div>
</div>
<Divider weight="thin" color="gray" />
{/* href */}
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>href</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}><code>undefined</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Link destination (renders as anchor)</div>
</div>
<Divider weight="thin" color="gray" />
{/* disabled */}
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>disabled</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>boolean</code></div>
<div style={{ width: '100px', flexShrink: 0 }}><code>false</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Disabled state</div>
</div>
<Divider weight="thin" color="gray" />
{/* className */}
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>className</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}><code>''</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Additional CSS classes</div>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Usage Examples */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Usage Examples</h2>
<div className="d-flex flex-column gap-6">
{/* Basic Usage */}
<div className="card p-4">
<h6 className="mb-3">Basic Usage</h6>
<pre className="mb-0" style={{ backgroundColor: 'var(--bs-gray-800)', padding: '1rem', borderRadius: '4px', overflow: 'auto' }}>
{`import { CardOffgrid } from 'shared/components/CardOffgrid';
<CardOffgrid
variant="neutral"
icon={<MyIcon />}
title="Onchain\\nMetadata"
description="Easily store key asset information..."
onClick={() => console.log('clicked')}
/>`}
</pre>
</div>
{/* With Link */}
<div className="card p-4">
<h6 className="mb-3">With Link</h6>
<pre className="mb-0" style={{ backgroundColor: 'var(--bs-gray-800)', padding: '1rem', borderRadius: '4px', overflow: 'auto' }}>
{`<CardOffgrid
variant="green"
icon="/icons/metadata.svg"
title="Learn More"
description="Click to navigate to documentation..."
href="/docs/metadata"
/>`}
</pre>
</div>
{/* Disabled State */}
<div className="card p-4">
<h6 className="mb-3">Disabled State</h6>
<pre className="mb-0" style={{ backgroundColor: 'var(--bs-gray-800)', padding: '1rem', borderRadius: '4px', overflow: 'auto' }}>
{`<CardOffgrid
variant="neutral"
icon={<MyIcon />}
title="Coming Soon"
description="This feature is not yet available..."
disabled
/>`}
</pre>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Figma References */}
<PageGrid className="py-26" id="link-demo">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Figma References</h2>
<ul>
<li>
<a href="https://www.figma.com/design/vwDwMJ3mFrAklj5zvZwX5M/Card---OffGrid?node-id=8001-1963&m=dev" target="_blank" rel="noopener noreferrer">
Light Mode Color States
</a>
</li>
<li>
<a href="https://www.figma.com/design/vwDwMJ3mFrAklj5zvZwX5M/Card---OffGrid?node-id=8001-2321&m=dev" target="_blank" rel="noopener noreferrer">
Dark Mode Color States
</a>
</li>
<li>
<a href="https://www.figma.com/design/vwDwMJ3mFrAklj5zvZwX5M/Card---OffGrid?node-id=8007-1096&m=dev" target="_blank" rel="noopener noreferrer">
Animation Specifications
</a>
</li>
</ul>
</PageGridCol>
</PageGridRow>
</PageGrid>
</div>
</div>
);
}

View 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.

View 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;
}
}

View 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;

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -9,13 +9,14 @@
$white: #FFFFFF;
$black: #000000;
// Gray (Neutral) - Original values (design tokens not ready)
$gray-050: #FCFCFD;
$gray-100: #F5F5F7;
$gray-200: #E0E0E1;
$gray-300: #C1C1C2;
$gray-400: #A2A2A4;
$gray-500: #838386;
// Gray (Neutral) - New Design Tokens
$gray-100: #F0F3F7; // New design token
$gray-200: #E6EAF0; // New design token
$gray-300: #CAD4DF; // New design token (default)
$gray-400: #8A919A; // New design token
$gray-500: #72777E; // New design token
// Legacy gray values (600-900 for dark mode)
$gray-600: #454549;
$gray-700: #343437;
$gray-800: #232325;
@@ -190,4 +191,4 @@ $light-fg-disabled: $gray-400;
$light-form-bg: $gray-200;
$light-box-shadow: 0px 5px 20px 0px $gray-300;
$light-link-hover-color: $blue-purple-500;
$light-standout-bg: $gray-050;
$light-standout-bg: $gray-100;

View File

@@ -91,6 +91,7 @@ $line-height-base: 1.5;
@import "../shared/components/Link/_link-icons.scss";
@import "../shared/components/Link/_link.scss";
@import "../shared/components/Divider/Divider.scss";
@import "../shared/components/CardOffgrid/CardOffgrid.scss";
@import "_code-tabs.scss";
@import "_code-walkthrough.scss";
@import "_diagrams.scss";