Files
xrpl-dev-portal/shared/patterns/CarouselFeatured/CarouselFeatured.scss
2026-08-24 12:45:48 -07:00

657 lines
20 KiB
SCSS

// BDS CarouselFeatured Pattern Styles
// Brand Design System - Featured image carousel with two-column layout
//
// Layout:
// - Desktop (lg+): Two-column layout - Image LEFT (50%), Content RIGHT (50%)
// - Tablet/Mobile: Single column - Content TOP, Image BOTTOM
//
// Naming Convention: BEM with 'bds' namespace
// .bds-carousel-featured - Base section container
// .bds-carousel-featured__media-col - Image/media column wrapper
// .bds-carousel-featured__content-col - Content column wrapper
// .bds-carousel-featured__content - Content column
// .bds-carousel-featured__header - Header row (heading + nav)
// .bds-carousel-featured__heading - Section heading
// .bds-carousel-featured__nav - Navigation buttons wrapper
// .bds-carousel-featured__bottom - Bottom section (features + CTA)
// .bds-carousel-featured__features - Feature list container
// .bds-carousel-featured__feature - Individual feature item
// .bds-carousel-featured__feature-title - Feature title
// .bds-carousel-featured__feature-description - Feature description
// .bds-carousel-featured__cta - CTA section (buttons + mobile nav)
// .bds-carousel-featured__buttons - Button group wrapper
// .bds-carousel-featured__slides - Slides container
// .bds-carousel-featured__slide-track - Sliding track
// .bds-carousel-featured__slide - Individual slide
// .bds-carousel-featured__slide--active - Active slide modifier
// .bds-carousel-featured__image - Slide image
//
// Note: This file is imported within xrpl.scss after Bootstrap and project
// variables are loaded, so $grid-breakpoints, colors, and mixins are available.
// =============================================================================
// Design Tokens (from Figma)
// =============================================================================
// Note: Uses centralized spacing tokens from _spacing.scss.
// Spacing
$bds-carousel-featured-padding-sm: $bds-space-2xl $bds-space-lg; // 24px 16px
$bds-carousel-featured-padding-md: $bds-space-3xl $bds-space-2xl; // 32px 24px
$bds-carousel-featured-padding-lg: $bds-space-4xl $bds-space-3xl; // 40px 32px
// Content gap between image and content columns
$bds-carousel-featured-column-gap: $bds-space-sm; // 8px - spacing('sm')
// Transition - horizontal wipe (default 'slide' variant)
$bds-carousel-featured-transition: 400ms cubic-bezier(0.4, 0, 0.2, 1);
// Transition - in-place crossfade ('fade' variant)
$bds-carousel-featured-fade-in: 260ms;
$bds-carousel-featured-fade-out: 200ms;
$bds-carousel-featured-fade-stagger: 60ms; // Offset between image, features, buttons
$bds-carousel-featured-fade-timing: cubic-bezier(0.4, 0, 0.2, 1);
// =============================================================================
// Color Variant Configuration Map
// =============================================================================
// Define all background variants with their color properties (Dark Mode)
//
// Note on link colors: the green is picked from the VARIANT BACKGROUND, not the
// theme mode. Only the "neutral" variant is actually dark in dark mode - "grey"
// and "yellow" stay light in both modes - so a bright green would be unreadable
// on them. Light backgrounds get $green-500, the dark background gets $green-300.
$bds-carousel-featured-variants: (
"grey": (
"bg-color": $gray-300,
"text-color": $black,
"divider-color": $black,
"link-color": $green-500,
"link-hover": $green-400,
"link-visited": $lilac-400,
"link-focus": $black,
"button-variant": "black",
"button-bg": $black,
"button-color": $white,
"button-hover": $gray-500,
"button-active": $black,
),
"neutral": (
"bg-color": $black,
"text-color": $white,
"divider-color": $white,
"link-color": $green-300,
"link-hover": $green-200,
"link-visited": $lilac-300,
"link-focus": $white,
"button-variant": "green",
"button-bg": $green-300,
"button-color": $black,
"button-hover": $green-200,
"button-active": $green-300,
),
"yellow": (
"bg-color": $yellow-100,
"text-color": $black,
"divider-color": $black,
"link-color": $green-500,
"link-hover": $green-400,
"link-visited": $lilac-400,
"link-focus": $black,
"button-variant": "black",
"button-bg": $black,
"button-color": $white,
"button-hover": $gray-500,
"button-active": $black,
),
);
// Define light mode variant overrides
$bds-carousel-featured-variants-light: (
"grey": (
"bg-color": $gray-200,
"text-color": $black,
"divider-color": $black,
"link-color": $green-500,
"link-hover": $green-400,
"link-visited": $lilac-400,
"link-focus": $black,
),
"neutral": (
"bg-color": $white,
"text-color": $black,
"divider-color": $black,
"link-color": $green-500,
"link-hover": $green-400,
"link-visited": $lilac-400,
"link-focus": $black,
),
"yellow": (
"bg-color": $yellow-100,
"text-color": $black,
"divider-color": $black,
"link-color": $green-500,
"link-hover": $green-400,
"link-visited": $lilac-400,
"link-focus": $black,
),
);
// =============================================================================
// Mixins: Apply Background Variant Styles
// =============================================================================
// Links inside feature descriptions.
//
// Without this, these anchors fall through to the global Bootstrap rule
// `a { color: rgba(var(--bs-link-color-rgb), ...) }`, where :root sets
// --bs-link-color-rgb to 255,255,255 - so they render WHITE in both light and
// dark mode, which is invisible on every carousel background except neutral/dark.
//
// The `:not()` chain excludes anchors that already carry their own component
// styling (BDS links, buttons, card-shaped anchors), and it also lifts our
// specificity clear of the other global themed link rules in the codebase.
@mixin carousel-featured-links($config) {
.bds-carousel-featured__feature-description {
a:not(.bds-link):not(.bds-btn):not(.bds-card-offgrid):not(.bds-card-icon):not(.xrpl-link) {
color: map-get($config, "link-color");
font-weight: 400;
&:hover {
color: map-get($config, "link-hover");
text-decoration: underline;
}
&:active {
color: map-get($config, "link-color");
}
&:visited {
color: map-get($config, "link-visited");
}
&:focus-visible {
outline: 2px solid map-get($config, "link-focus");
outline-offset: 1px;
}
}
}
}
// A neutral Link here needs to match THIS PANEL's own text color, not the
// site theme: "grey" and "yellow" stay light-colored (text stays black) in
// both light and dark mode, so the sitewide neutral color (black light /
// white dark) goes white-on-light-yellow in dark mode. Inheriting tracks
// whichever color the surrounding text already resolves to, correct for
// every variant, without a per-variant override. Brand links are unaffected
// -- they already carry their own fixed-per-variant green above.
//
// Every state needs its own selector here, not just rest: the sitewide
// neutral mixin's :hover/:active/:focus/:visited blocks are separate,
// equally (or more) specific rules that this rest-only override doesn't
// touch, so without repeating it per state, hover/focus stayed white in dark
// mode even after rest was fixed.
.bds-carousel-featured .bds-carousel-featured__feature-description .xrpl-link.xrpl-link--neutral,
.bds-carousel-featured .bds-carousel-featured__feature-description .xrpl-link.xrpl-link--neutral:hover,
.bds-carousel-featured .bds-carousel-featured__feature-description .xrpl-link.xrpl-link--neutral:active,
.bds-carousel-featured .bds-carousel-featured__feature-description .xrpl-link.xrpl-link--neutral:focus,
.bds-carousel-featured .bds-carousel-featured__feature-description .xrpl-link.xrpl-link--neutral:visited {
color: inherit;
}
// Same problem, same fix, for the focus ring: .xrpl-link--ctx-on-theme's
// outline color also flips with the site theme (black light / white dark),
// not this panel's own fixed-light background. $black matches the brand
// links' own "link-focus" color for grey/yellow above, so both intentions
// get the same outline here.
.bds-carousel-featured .bds-carousel-featured__feature-description .xrpl-link.xrpl-link--ctx-on-theme:focus-visible {
outline-color: $black;
}
// Full variant mixin (for dark mode with button styles)
@mixin carousel-featured-variant($variant-name, $config) {
&--bg-#{$variant-name} {
background-color: map-get($config, "bg-color");
// Text colors
.bds-carousel-featured__heading,
.bds-carousel-featured__feature-title,
.bds-carousel-featured__feature-description {
color: map-get($config, "text-color");
}
// Divider color
.bds-divider {
background-color: map-get($config, "divider-color");
}
// Feature description links
@include carousel-featured-links($config);
// Carousel nav buttons - enabled states only
// Disabled states are handled by CarouselButton component styles
.bds-carousel-button--#{map-get($config, "button-variant")} {
background-color: map-get($config, "button-bg");
color: map-get($config, "button-color");
&:hover:not(:disabled) {
background-color: map-get($config, "button-hover");
}
&:active:not(:disabled) {
background-color: map-get($config, "button-active");
}
}
}
}
// Light mode variant mixin (only colors, no button states)
@mixin carousel-featured-variant-light($variant-name, $config) {
.bds-carousel-featured--bg-#{$variant-name} {
background-color: map-get($config, "bg-color");
.bds-carousel-featured__heading,
.bds-carousel-featured__feature-title,
.bds-carousel-featured__feature-description {
color: map-get($config, "text-color");
}
.bds-divider {
background-color: map-get($config, "divider-color");
}
// Feature description links
@include carousel-featured-links($config);
}
}
// =============================================================================
// Base Container Styles
// =============================================================================
.bds-carousel-featured {
width: 100%;
overflow: hidden;
position: relative;
// Default background - dark mode default (grey variant)
background-color: $gray-300;
// Mobile (default)
padding: $bds-carousel-featured-padding-sm;
// Tablet
@include media-breakpoint-up(md) {
padding: $bds-carousel-featured-padding-md;
}
// Desktop
@include media-breakpoint-up(lg) {
padding: $bds-carousel-featured-padding-lg;
}
// Max width constraint
@include media-breakpoint-up(xl) {
max-width: 1280px;
margin-left: auto;
margin-right: auto;
}
& > .bds-grid__row {
gap: $bds-space-2xl;
@include media-breakpoint-up(md) {
gap: $bds-space-3xl;
}
@include media-breakpoint-up(lg) {
gap: $bds-space-sm;
}
}
// ---------------------------------------------------------------------------
// Background Color Variants
// ---------------------------------------------------------------------------
// Dark Mode (default) - Generate all variant styles using the mixin
@include bds-theme-mode(dark) {
@each $variant-name, $config in $bds-carousel-featured-variants {
@include carousel-featured-variant($variant-name, $config);
}
}
}
// =============================================================================
// Content Column
// =============================================================================
.bds-carousel-featured__content-col {
display: flex;
flex-direction: column;
@include media-breakpoint-up(lg) {
// Stretch to match image height
align-self: stretch;
// Add 8px padding-left to create 16px total gap (8px row gap + 8px padding)
padding-left: $bds-space-sm; // 8px - spacing('sm')
}
}
.bds-carousel-featured__content {
display: flex;
flex-direction: column;
width: 100%;
gap: 0; // Use space-between instead
justify-content: space-between; // Header at top, features+CTA at bottom
@include media-breakpoint-up(lg) {
flex: 1;
min-height: auto; // Reset min-height on desktop
}
}
// =============================================================================
// Header Section (Heading + Nav)
// =============================================================================
.bds-carousel-featured__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
flex-direction: column-reverse;
@include media-breakpoint-down(lg) {
margin-bottom: $bds-space-xl;
}
@include media-breakpoint-up(md) {
flex-direction: row;
}
}
.bds-carousel-featured__heading {
margin: 0;
// Dark mode default: light text on dark background
color: $white;
max-width: 392px;
@include wordbreak("break-word");
}
// =============================================================================
// Bottom Section (Features + CTA grouped together)
// =============================================================================
.bds-carousel-featured__bottom {
display: flex;
flex-direction: column;
gap: $bds-space-2xl; // 24px - spacing('2xl')
@include media-breakpoint-up(md) {
gap: $bds-space-3xl; // 32px - spacing('3xl')
}
@include media-breakpoint-up(lg) {
gap: $bds-space-4xl; // 40px - spacing('4xl')
}
}
// =============================================================================
// Navigation Buttons
// =============================================================================
.bds-carousel-featured__nav {
display: flex;
gap: $bds-space-sm; // 8px - spacing('sm')
flex-shrink: 0;
z-index: 2;
padding: inherit;
// @include media-breakpoint-up(lg) {
// top: 0;
// bottom: auto;
// }
&.clickable {
position: absolute;
right: 0;
top: 0;
}
}
.bds-carousel-featured__nav-buttons-placeholder {
visibility: hidden;
pointer-events: none;
}
// =============================================================================
// Feature List
// =============================================================================
.bds-carousel-featured__features {
display: flex;
flex-direction: column;
width: 100%;
margin: 0;
padding: 0;
}
.bds-carousel-featured__feature {
display: flex;
flex-direction: column;
gap: $bds-space-sm; // 8px - spacing('sm')
width: 100%;
// Spacing between description and next divider
// Mobile/Tablet: 16px, Desktop: 24px
&:not(:first-child) {
padding-top: $bds-space-lg; // 16px - spacing('lg')
@include media-breakpoint-up(lg) {
padding-top: $bds-space-2xl; // 24px - spacing('2xl')
}
}
}
.bds-carousel-featured__feature-title {
margin: 0;
// Dark mode default: light text on dark background
color: $white;
}
.bds-carousel-featured__feature-description {
margin: 0;
// Dark mode default: muted light text on dark background
color: $gray-400;
}
// =============================================================================
// CTA Section (Buttons + Mobile Nav)
// =============================================================================
.bds-carousel-featured__cta {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-end;
width: 100%;
gap: $bds-space-lg; // 16px - spacing('lg')
// Tablet+: no wrap needed
@include media-breakpoint-up(md) {
flex-wrap: nowrap;
gap: 0;
}
// Desktop: nav is hidden, so buttons just align left
@include media-breakpoint-up(lg) {
justify-content: flex-start;
}
}
// =============================================================================
// Slides Container
// =============================================================================
.bds-carousel-featured__slides {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
}
.bds-carousel-featured__slide-track {
display: flex;
transition: transform $bds-carousel-featured-transition;
will-change: transform;
}
// =============================================================================
// Individual Slides
// =============================================================================
.bds-carousel-featured__slide {
flex: 0 0 100%;
width: 100%;
position: relative;
}
// =============================================================================
// Fade Variant - In-Place Crossfade
// =============================================================================
// Instead of wiping the track sideways, every slide is stacked in a single grid
// cell and the content crossfades in place. Because all slides still take part in
// layout, the carousel keeps sizing to the tallest slide exactly as the flex track
// does - so slides of differing heights don't cause the carousel to jump.
.bds-carousel-featured--fade {
.bds-carousel-featured__slide-track {
display: grid;
transition: none;
will-change: auto;
}
.bds-carousel-featured__slide {
grid-area: 1 / 1;
// Inactive slides stay in layout but out of reach. The visibility switch is
// held until the fade-out finishes so it doesn't cut the transition short.
visibility: hidden;
transition: visibility 0ms linear $bds-carousel-featured-fade-out;
}
.bds-carousel-featured__slide--active {
visibility: visible;
transition-delay: 0ms;
}
// The parts that crossfade. The outgoing slide fades out as a single unit; the
// stagger below applies only to the incoming slide.
.bds-carousel-featured__heading,
.bds-carousel-featured__media,
.bds-carousel-featured__features,
.bds-carousel-featured__cta {
opacity: 0;
transition: opacity $bds-carousel-featured-fade-out
$bds-carousel-featured-fade-timing;
}
.bds-carousel-featured__slide--active {
.bds-carousel-featured__heading,
.bds-carousel-featured__media {
opacity: 1;
transition: opacity $bds-carousel-featured-fade-in
$bds-carousel-featured-fade-timing;
}
.bds-carousel-featured__features {
opacity: 1;
transition: opacity $bds-carousel-featured-fade-in
$bds-carousel-featured-fade-timing $bds-carousel-featured-fade-stagger;
}
.bds-carousel-featured__cta {
opacity: 1;
transition: opacity $bds-carousel-featured-fade-in
$bds-carousel-featured-fade-timing
($bds-carousel-featured-fade-stagger * 2);
}
}
}
// =============================================================================
// Reduced Motion
// =============================================================================
// Both transition styles collapse to an instant swap.
@include bds-reduced-motion {
.bds-carousel-featured__slide-track {
transition: none;
}
.bds-carousel-featured--fade {
.bds-carousel-featured__slide,
.bds-carousel-featured__heading,
.bds-carousel-featured__media,
.bds-carousel-featured__features,
.bds-carousel-featured__cta {
transition: none;
}
}
}
.bds-carousel-featured__slide > .bds-grid__row {
gap: $bds-space-2xl;
@include media-breakpoint-up(md) {
gap: $bds-space-3xl;
}
@include media-breakpoint-up(lg) {
gap: $bds-space-sm;
}
}
.bds-carousel-featured__media {
position: relative;
width: 100%;
aspect-ratio: 343 / 193;
@include media-breakpoint-up(md) {
aspect-ratio: 16 / 9;
}
@include media-breakpoint-up(lg) {
aspect-ratio: 1 / 1;
}
}
.bds-carousel-featured__image {
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
}
// =============================================================================
// LIGHT MODE (html.light) - Color Overrides
// =============================================================================
html.light {
// Default (no variant class) - Light mode: gray-200 background
.bds-carousel-featured {
background-color: $gray-200;
.bds-carousel-featured__heading,
.bds-carousel-featured__feature-title,
.bds-carousel-featured__feature-description {
color: $black;
}
.bds-divider {
background-color: $black;
}
}
// Generate all light mode variant overrides using the mixin
@each $variant-name, $config in $bds-carousel-featured-variants-light {
@include carousel-featured-variant-light($variant-name, $config);
}
}