Files
xrpl-dev-portal/shared/components/CardOffgrid/CardOffgrid.scss
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

435 lines
15 KiB
SCSS

// 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--static - Non-interactive card (no href / no onClick)
// .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 (uses .subhead-lg-l)
// .bds-card-offgrid__description - Card description (uses .body-l)
//
// 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
// =============================================================================
// Note: Uses centralized spacing tokens from _spacing.scss where applicable.
// Component-specific values (dimensions, icon sizes) remain local.
// Dimensions (from Figma design spec)
$bds-card-offgrid-width: 400px;
$bds-card-offgrid-height: 480px;
$bds-card-offgrid-padding: $bds-space-2xl; // 24px - spacing('2xl')
$bds-card-offgrid-icon-container: 84px;
$bds-card-offgrid-icon-size: 68px;
$bds-card-offgrid-content-gap: $bds-space-4xl; // 40px - spacing('4xl')
// Animation - uses centralized tokens from _animations.scss
$bds-card-offgrid-transition-duration: $bds-transition-duration; // 200ms
$bds-card-offgrid-transition-timing: $bds-transition-timing;
// Typography - Title (now using .subhead-lg-l from _font.scss)
// Typography - Description (now using .body-l from _font.scss)
// -----------------------------------------------------------------------------
// 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: $gray-500-pressed-dark; // #72777E + 70% black → #56595E
$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;
}
// Match hover background: reveal overlay on keyboard focus (same clip-path as --hovered)
&:focus-visible:not(.bds-card-offgrid--disabled) {
.bds-card-offgrid__overlay {
clip-path: inset(0 0 0 0);
}
}
}
// =============================================================================
// Static (Non-Interactive) Modifier
// =============================================================================
// Applied when the card has neither `href` nor `onClick` — it renders as a plain
// <div> with nothing to activate, so the click affordance has to go. The
// component also skips rendering __overlay for these cards and never attaches
// the hover listeners, so the color wipe and pressed state can't fire; this
// block only has to undo the base `cursor: pointer`.
.bds-card-offgrid--static {
cursor: default;
}
// =============================================================================
// 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 {
// Typography handled by .subhead-lg-l class from _font.scss
margin-bottom: 0;
white-space: pre-wrap;
}
// =============================================================================
// 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;
.bds-card-offgrid__icon-image {
filter: brightness(0) invert(1);
}
// 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;
.bds-card-offgrid__icon-image {
filter: none;
}
// Overlay color for hover wipe
.bds-card-offgrid__overlay {
background-color: $bds-card-offgrid-neutral-hover-light;
}
// Text color on hover and keyboard focus (matches hover treatment)
&.bds-card-offgrid--hovered,
&:focus-visible:not(.bds-card-offgrid--disabled) {
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
// =============================================================================
// Tablet + mobile: no clip-path wipe (touch :hover is sticky / fights the animation).
// Jump straight to pressed overlay color; instant show/hide.
@media (max-width: 991px) {
.bds-card-offgrid__overlay {
transition: none;
}
.bds-card-offgrid--neutral:not(.bds-card-offgrid--disabled) {
&.bds-card-offgrid--hovered .bds-card-offgrid__overlay,
&:focus-visible .bds-card-offgrid__overlay {
background-color: $bds-card-offgrid-neutral-pressed-dark;
clip-path: inset(0 0 0 0);
}
}
.bds-card-offgrid--green:not(.bds-card-offgrid--disabled) {
&.bds-card-offgrid--hovered .bds-card-offgrid__overlay,
&:focus-visible .bds-card-offgrid__overlay {
background-color: $bds-card-offgrid-green-pressed-dark;
clip-path: inset(0 0 0 0);
}
}
html.light {
.bds-card-offgrid--neutral:not(.bds-card-offgrid--disabled) {
&.bds-card-offgrid--hovered .bds-card-offgrid__overlay,
&:focus-visible .bds-card-offgrid__overlay {
background-color: $bds-card-offgrid-neutral-pressed-light;
clip-path: inset(0 0 0 0);
}
}
.bds-card-offgrid--green:not(.bds-card-offgrid--disabled) {
&.bds-card-offgrid--hovered .bds-card-offgrid__overlay,
&:focus-visible .bds-card-offgrid__overlay {
background-color: $bds-card-offgrid-green-pressed-light;
clip-path: inset(0 0 0 0);
}
}
}
}
@media (max-width: 767px) {
.bds-card-offgrid {
width: 100%;
min-height: $bds-card-offgrid-height;
height: auto;
}
}