mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2026-09-27 07:36:49 +00:00
* 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>
196 lines
5.0 KiB
TypeScript
196 lines
5.0 KiB
TypeScript
import React, { useState, useCallback } from 'react';
|
|
import clsx from 'clsx';
|
|
|
|
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]);
|
|
|
|
// A card with neither `href` nor `onClick` has nothing to activate, so it gets
|
|
// none of the interactive affordances: no pointer cursor, no color wipe, no
|
|
// pressed state. Hover tracking is what drives the wipe, so a static card
|
|
// doesn't even listen for it.
|
|
const isInteractive = !disabled && Boolean(href || onClick);
|
|
|
|
const hoverHandlers = isInteractive
|
|
? { onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave }
|
|
: {};
|
|
|
|
// Build class names using BEM with bds namespace
|
|
const classNames = clsx(
|
|
'bds-card-offgrid',
|
|
`bds-card-offgrid--${variant}`,
|
|
{
|
|
'bds-card-offgrid--disabled': disabled,
|
|
'bds-card-offgrid--static': !isInteractive && !disabled,
|
|
'bds-card-offgrid--hovered': isHovered,
|
|
},
|
|
className
|
|
);
|
|
|
|
// 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"
|
|
loading="lazy"
|
|
decoding="async"
|
|
/>
|
|
);
|
|
}
|
|
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 - interactive cards only */}
|
|
{isInteractive && (
|
|
<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 sh-lg-l">
|
|
{renderTitle()}
|
|
</span>
|
|
<span className="bds-card-offgrid__description body-l">
|
|
{description}
|
|
</span>
|
|
</span>
|
|
</>
|
|
);
|
|
|
|
// Render as anchor if href is provided
|
|
if (href && !disabled) {
|
|
return (
|
|
<a
|
|
href={href}
|
|
className={classNames}
|
|
aria-disabled={disabled}
|
|
{...hoverHandlers}
|
|
>
|
|
{content}
|
|
</a>
|
|
);
|
|
}
|
|
|
|
// Render as button when an onClick handler exists (or the card is disabled and
|
|
// needs to be visible as a disabled control).
|
|
if (onClick || disabled) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
className={classNames}
|
|
onClick={disabled ? undefined : onClick}
|
|
disabled={disabled}
|
|
aria-disabled={disabled}
|
|
{...hoverHandlers}
|
|
>
|
|
{content}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
// Static (non-interactive) card — no href, no onClick. Render a plain div so
|
|
// the card isn't focusable / announced as a control.
|
|
return (
|
|
<div
|
|
className={classNames}
|
|
{...hoverHandlers}
|
|
>
|
|
{content}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CardOffgrid;
|