Files
Calvin 5021556c8e BDS component polish: link contrast, static cards, uniform CTAs, stacked hero (#3851)
* BDS component polish: link visibility, static cards, uniform CTAs

Component-level fixes and refinements across the 2026 brand components.

Accessibility / contrast
- StandardCard: pin description link colors. Every card variant has a light
  background in both themes, so theme-level link colors were wrong inside it —
  dark mode painted plain anchors white and BdsLink lilac-300, both of which
  wash out on a pale card. Links (including :visited) now use the card's own
  text color with an underline carrying the affordance; hover uses lilac-500,
  which clears 4.5:1 on all four card backgrounds.
- Breadcrumbs: point the open dropdown menu at the breadcrumb color tokens so
  the trail, trigger, and menu read as one color in both themes, and give dark
  mode gray-6 (7.41:1, matching light mode's 7.23:1).

Interaction
- CardOffgrid: drop every hover affordance when a card has neither href nor
  onClick. Static cards no longer get a pointer cursor, the color-wipe overlay,
  or a pressed state — the tokenization and trading carousels pass link-less
  cards and were advertising a click target that did not exist.
- CarouselFeatured: add a 'fade' transition alongside the default 'slide', for
  decks whose slides share a heading. Inactive slides are now inert, keeping
  focus and pointer events out of them in both styles.
- Add a bds-reduced-motion mixin so components can collapse motion to an
  instant state change.

Design consistency
- ButtonGroup: add forceVariant and forceNoPadding overrides, letting a section
  opt out of the count-based variant defaults and render one uniform treatment.
  Existing consumers are unaffected.
- FeatureTwoColumn: render every link as a tertiary button regardless of count,
  flush with the title and description.
- CardImage: render an all-bullet subtitle as a real <ul> so wrapped text hangs
  under the first character and screen readers announce it as a list.
- docs: give the node-installation card descriptions a paragraph break before
  their "Learn More" link.

Dependencies
- Bump @codemirror/state and view, add lang-javascript, lang-json, and lint,
  with overrides pinning state/view to one copy.

Docs updated alongside each component. Built CSS regenerated with the
production script to match the committed artifact's minified format.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* ButtonGroup: don't strip padding from forced filled variants

`noPadding` was derived as `forceNoPadding || isMultiButton`, which was safe
while the 3+ block layout was always tertiary. `forceVariant` also accepts
`primary` and `secondary`, so a 3-button group forcing a filled variant had
`padding: 0 !important` (Button.scss) applied with no way to opt out.

Tie the implied no-padding to the resolved variant being tertiary. Behavior is
unchanged for every caller that doesn't pass `forceVariant`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Stacked hero variant, fade-by-default carousel, component docs

Layout
- HeaderHeroPrimaryMedia: add a `stacked` prop that puts the headline, subtitle,
  and buttons in one column (full width at base, 7/8 at md, 9/12 at lg) instead
  of the default headline-left / CTA-right split. The headline and CTA block
  bottom-align against each other in the two-column layout, so the stacked
  variant undoes that. Both arrangements now share the same headline and CTA
  elements, so they can't drift apart. Used on the docs landing hero.

Motion
- CarouselFeatured: make `fade` the default transition. Slides that share a
  heading are the common case, and a horizontal wipe drags the identical heading
  across the screen only to set it back down. `slide` is now opt-in for decks
  whose panels are genuinely distinct. Nudge the crossfade to 260ms in / 200ms
  out. The home page carousel opts into fade explicitly; developer-funding now
  inherits it.

Fixes
- CardTextIconCard: pass `headingAs` through to `cardContent`, which was
  accepting the prop but never receiving it.

Docs
- Expand the Button, CardImage, CardTextIcon, PageGrid, CalloutMediaBanner,
  LogoSquareGrid, and HeaderHeroPrimaryMedia references.

Built CSS regenerated with the production script.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:49:10 -07:00

286 lines
7.8 KiB
Markdown

# CardImage Component Documentation
## Overview
The CardImage component is a responsive card implementation following the XRPL Brand Design System (BDS). It displays an image, title, subtitle, and call-to-action button with three responsive size variants that adapt to viewport width.
## Features
- **Three Responsive Variants**: LG (≥992px), MD (576px-991px), SM (<576px)
- **Interactive States**: Default, Hover, Focus, Pressed, Disabled
- **Button Animation on Card Hover**: Hovering the card triggers the button's hover animation
- **Flexible Usage**: Supports both link navigation and click handlers
- **Accessibility**: Keyboard navigation and screen reader support
## Props API
```typescript
interface CardImageProps {
/** Image source URL */
image: string;
/** Alt text for the image */
imageAlt: string;
/** Card title (1 line only) */
title: string;
/** Card subtitle (max 3 lines). `\n` renders as a line break; an all-bullet subtitle renders as a list. */
subtitle: string;
/** Button label text */
buttonLabel: string;
/** Link destination (renders card as clickable link) */
href?: string;
/** Click handler for the button */
onClick?: () => void;
/** Disabled state */
disabled?: boolean;
/** Optional className for custom styling */
className?: string;
/** When true, image fills entire container with object-fit: cover (no visible background) */
fullBleed?: boolean;
/** Custom background color for image container (defaults to gray-100) */
backgroundColor?: string;
}
```
### Default Values
- `disabled`: `false`
- `className`: `''`
- `fullBleed`: `false`
- `backgroundColor`: `undefined` (falls back to the gray-100 image background)
## Subtitle Formatting
`subtitle` is a plain string, but it is not rendered blindly:
- Lines are split on `\n`, and blank lines are dropped.
- If **every** line begins with a bullet marker (`•`, `-`, or `*`), the subtitle
renders as a `<ul>` (`bds-card-image__subtitle--list`) with one `<li>` per line.
- Anything else renders as a single `<p>`, where `white-space: pre-line` means
`\n` still produces a visible line break.
```tsx
// Renders as a bulleted list
<CardImage subtitle={"• Fast settlement\n• Low fees\n• Built-in DEX"} ... />
// Renders as a paragraph with a line break
<CardImage subtitle={"Fast settlement.\nLow fees."} ... />
```
## Image Sizing
By default the image sits in a fixed-height container against a gray-100
background, preserving its aspect ratio.
- **`fullBleed`** — sets `bds-card-image--full-bleed`, which drops the card's
fixed height (`height: auto`) and makes the image fill the container using a
1:1 aspect ratio with `object-fit: cover`. Use this when the image is the whole
visual and you don't want letterboxing.
- **`backgroundColor`** — sets the `--bds-card-image-bg` custom property on the
image container, overriding the gray-100 default. Has no visible effect
alongside `fullBleed`, since the image covers the container.
```tsx
<CardImage fullBleed image="/images/hero.png" ... />
<CardImage backgroundColor="#F3F1EB" image="/images/logo.svg" ... />
```
## Responsive Variants
### LG (Large) - Desktop (≥992px)
- Card height: 620px
- Image height: 400px (1:1 aspect ratio preserved)
- 3-column grid width
### MD (Medium) - Tablet (576px - 991px)
- Card height: 560px
- Image height: 280px
- 2-column grid width
### SM (Small) - Mobile (<576px)
- Card height: 536px
- Image height: 268px
- 1-column grid width (full width)
## States
### Default State
The default interactive state with the button showing its primary green background.
### Hover State
When the user hovers over the card:
- Button background fills from bottom-to-top (green-300 → green-200)
- Arrow icon line shrinks
- Gap between label and icon increases
### Focus State
When the card receives keyboard focus:
- Black border outline around the card
- Button shows focus styling
### Pressed State
When the button is being pressed:
- Button returns to default styling momentarily
### Disabled State
When `disabled={true}`:
- Card is non-interactive
- Button shows disabled styling (gray background, no icon)
- Text colors muted
- Cursor changes to `not-allowed`
## Usage Examples
### Basic Card with Link
```tsx
import { CardImage } from 'shared/components/CardImage';
<CardImage
image="/images/docs-hero.png"
imageAlt="Documentation illustration"
title="Documentation"
subtitle="Access everything you need to get started working with the XRPL."
buttonLabel="Get Started"
href="/docs"
/>
```
### Card with Click Handler
```tsx
<CardImage
image="/images/feature.png"
imageAlt="Feature illustration"
title="New Feature"
subtitle="Learn about our latest feature and how it can help you build better applications."
buttonLabel="Learn More"
onClick={() => console.log('clicked')}
/>
```
### Disabled Card
```tsx
<CardImage
image="/images/coming-soon.png"
imageAlt="Coming soon"
title="Coming Soon"
subtitle="This feature is not yet available."
buttonLabel="Unavailable"
disabled
/>
```
### Card with Custom Class
```tsx
<CardImage
image="/images/hero.png"
imageAlt="Hero image"
title="Custom Styled"
subtitle="Card with additional custom styling."
buttonLabel="Explore"
href="/explore"
className="my-custom-card"
/>
```
## Design Tokens
### Spacing
| Token | Value | Description |
|-------|-------|-------------|
| Image-to-content gap | 24px | Gap between image and content area |
| Title-to-subtitle gap | 12px | Gap between title and subtitle |
| Content horizontal padding | 8px | Left/right padding for content |
| Button margin-top | 30px | Button locked to bottom |
| Border radius | 16px | Card corner radius |
### Colors
| Element | Light Mode | Dark Mode |
|---------|------------|-----------|
| Card background | #FFFFFF | Gray 800 |
| Card border | Gray 300 (#CAD4DF) | Gray 700 |
| Image background | Gray 100 (#F0F3F7) | Gray 700 |
| Text color | #141414 | White |
### Typography
| Element | Token | Specs |
|---------|-------|-------|
| Title | `.sh-md-l` | 28px/35px, -0.5px letter-spacing, light weight |
| Subtitle | `.body-l` | 18px/26.1px, -0.5px letter-spacing, light weight |
## How It Works
### Hover Animation
The card tracks hover state via React's `useState`. When hovered:
1. A `bds-card-image--hovered` class is added to the card
2. CSS rules target the nested Button component and apply hover styles
3. The button's `::before` pseudo-element animates (background fill)
4. The arrow icon line shrinks via `scaleX(0)`
This approach ensures the button animation triggers even when hovering areas of the card outside the button itself.
### Link Navigation
When `href` is provided:
- The entire card becomes clickable
- Clicking anywhere on the card navigates to the href
- The card is keyboard accessible (Enter/Space to activate)
### Button Integration
The component uses the existing BDS Button component with:
- `variant="primary"` - Solid green background
- `color="green"` - Green color theme
## Accessibility
### Keyboard Navigation
- **Tab**: Focus the card
- **Enter/Space**: Activate the card (navigate to href or trigger onClick)
### Screen Reader Support
- Card has appropriate `role="link"` when href is provided
- Image has descriptive alt text
- `aria-disabled` attribute for disabled state
## File Structure
```
shared/components/CardImage/
├── CardImage.tsx # Component implementation
├── CardImage.scss # Component styles
├── CardImage.md # This documentation
└── index.ts # Exports
```
## Related Components
- **Button**: Used internally for the CTA button
- **CardOffgrid**: Similar card component with different layout
## Browser Support
The component uses modern CSS features:
- CSS Grid/Flexbox
- CSS transforms and transitions
- `:focus-visible` pseudo-class
- `-webkit-line-clamp` for text truncation
All features are widely supported in modern browsers.