Files
xrpl-dev-portal/shared/sections/FeatureTwoColumn/FeatureTwoColumn.tsx
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

140 lines
4.7 KiB
TypeScript

import React from "react";
import clsx from "clsx";
import {
ButtonGroup,
ButtonConfig,
validateButtonGroup,
} from "shared/patterns/ButtonGroup/ButtonGroup";
export interface FeatureTwoColumnLink {
/** Link label text */
label: string;
/** Link URL */
href: string;
}
export interface FeatureTwoColumnProps {
/** Color theme variant */
color?: "neutral" | "lilac" | "yellow" | "green";
/** Content arrangement - left places content on left side, right places content on right side */
arrange?: "left" | "right";
/** Feature title text (heading-md typography) */
title: string;
/** Feature description text (body-l typography) */
description: string;
/** Array of links (1-5 links supported).
* Always rendered as tertiary buttons, whatever the count — see the note on
* the component below.
*/
links: FeatureTwoColumnLink[];
/** Feature media (image) configuration */
media: {
src: string;
alt: string;
};
/** Additional CSS classes */
className?: string;
}
/**
* FeatureTwoColumn Pattern
*
* A feature section pattern that pairs editorial content with a media element
* in a two-column layout. Designed for showcasing features, products, or use cases.
*
* Responsive layout (single flex row/column):
* - Mobile / tablet: media always on top, then content (`arrange` ignored) for consistent
* rhythm when multiple sections stack (media, content; media, content; …).
* - Desktop (≥992px): side-by-side (50% / 50%); `arrange` controls left vs right placement.
*
* Button behavior:
* - Every link renders as a tertiary button, regardless of count. This section
* pairs its links with editorial copy, so the design calls for a uniform
* text-link treatment rather than ButtonGroup's default count-based mix
* (1 → secondary, 2 → primary + tertiary, 3+ → tertiary).
* - Buttons are flush-left with the title and description (no button padding).
* - Layout still follows the count: inline at 1-2 links, stacked block at 3+.
*/
export const FeatureTwoColumn: React.FC<FeatureTwoColumnProps> = ({
color = "neutral",
arrange = "left",
title,
description,
links = [],
media,
className,
}) => {
// Determine button color based on background
// Rule: Black buttons must be used for all backgrounds (including neutral)
const buttonColor = "black";
const forceColor = true;
// Convert links to ButtonConfig format
const buttonConfigs: ButtonConfig[] = links.map((link) => ({
label: link.label,
href: link.href,
forceColor: forceColor,
}));
// Validate buttons (FeatureTwoColumn supports 1-5 links per design spec)
const buttonValidation = validateButtonGroup(buttonConfigs, 5);
const hasButtons = buttonValidation.hasButtons;
const rootClasses = clsx(
"bds-feature-two-column",
`bds-feature-two-column--${color}`,
`bds-feature-two-column--${arrange}`,
className,
);
const contentClass = clsx("bds-feature-two-column__content", {
"bds-feature-two-column__content--multiple":
hasButtons && buttonValidation.buttons.length >= 3,
});
return (
<section className={rootClasses}>
<div className="bds-feature-two-column__layout">
<div className="bds-feature-two-column__content-col">
<div className="bds-feature-two-column__content-grid">
<div className="bds-feature-two-column__content-wrapper">
<div className={contentClass}>
<div className="bds-feature-two-column__text-group">
<h2 className="bds-feature-two-column__title">{title}</h2>
<p className="bds-feature-two-column__description">
{description}
</p>
</div>
{hasButtons && (
<ButtonGroup
buttons={buttonValidation.buttons}
color={buttonColor}
forceColor={forceColor}
// Uniform tertiary treatment for this section, flush with
// the text above it. `singleButtonVariant` is intentionally
// omitted — `forceVariant` supersedes it.
forceVariant="tertiary"
forceNoPadding
gap="medium"
/>
)}
</div>
</div>
</div>
</div>
<div className="bds-feature-two-column__media-col">
<div className="bds-feature-two-column__media">
<img
src={media.src}
alt={media.alt}
className="bds-feature-two-column__media-img"
/>
</div>
</div>
</div>
</section>
);
};
export default FeatureTwoColumn;