Files
Calvin 32e89c1299 Component Library Refactor & New Components (#3510)
* adding showcase page

* adding CardStatsList

* clean up, tighter code

* code review and code clean up

* update import, clean up env for error message

* tweak some css code

* less css, rebuilt

* re-adding bem, modifier for bds variants
2026-02-23 15:00:18 -08:00

9.0 KiB

LinkTextDirectory Component

A section pattern that displays a numbered list of LinkTextCard components with a heading and optional description.

Overview

LinkTextDirectory is a section-level pattern that combines a header (heading + description) with a vertically stacked list of LinkTextCard components. Each card is automatically numbered sequentially (01, 02, 03...), making it perfect for feature lists, step-by-step guides, or numbered content sections.

Features

  • Automatic Numbering: Cards are numbered sequentially starting from 01
  • Responsive Layout: Adaptive spacing and alignment across breakpoints
  • Desktop Right-Alignment: Cards are right-aligned on desktop for visual interest
  • Minimal HTML Structure: Flat, efficient DOM hierarchy
  • Light/Dark Mode: Full theming support
  • Flexible Content: Supports any number of cards

Layout Behavior

Breakpoint Card Alignment Gap Between Cards Header Gap
Base (< 576px) Left 24px 8px
MD (576px - 991px) Left 32px 8px
LG (≥ 992px) Right 40px 16px

Desktop Behavior: Cards are right-aligned using align-items: flex-end, creating a visually distinct layout compared to mobile/tablet.

Usage

Basic Usage

<LinkTextDirectory
  heading="Explore XRPL Developer Tools"
  description="XRP Ledger is a compliance-focused blockchain where financial applications come to life"
  cards={[
    {
      heading: "Fast Settlement and Low Fees",
      description: "Settle transactions in 3-5 seconds for a fraction of a cent, ideal for large-scale, high-volume RWA tokenization",
      buttons: [
        { label: "Get Started", href: "/start" },
        { label: "Learn More", href: "/docs" }
      ]
    },
    {
      heading: "Secure and Reliable",
      description: "Built on proven blockchain technology with enterprise-grade security",
      buttons: [
        { label: "Read Documentation", href: "/docs" }
      ]
    },
    {
      heading: "Developer Friendly",
      description: "Comprehensive APIs and SDKs for seamless integration",
      buttons: [
        { label: "View API", href: "/api" },
        { label: "See Examples", href: "/examples" }
      ]
    }
  ]}
/>

Without Description

<LinkTextDirectory
  heading="Key Features"
  cards={featuresList}
/>

With Dynamic Data

const features = [
  {
    heading: "Feature One",
    description: "Description for feature one",
    buttons: [{ label: "Learn More", href: "/feature-1" }]
  },
  // ... more features
];

<LinkTextDirectory
  heading="Platform Features"
  description="Everything you need to build amazing applications"
  cards={features}
/>

Props

LinkTextDirectoryProps

Prop Type Default Description
heading string Required Section heading
description string - Optional description text
cards LinkTextCardData[] Required Array of card data
className string - Additional CSS classes

LinkTextCardData

Prop Type Default Description
heading string Required Card heading
description string Required Card description
buttons ButtonConfig[] Required Array of button configs (max 2)

ButtonConfig (from ButtonGroup)

Prop Type Default Description
label string Required Button text
href string - Link destination
onClick () => void - Click handler

Component Structure

<PageGrid className="bds-link-text-directory">
  <PageGridRow>
    <PageGridCol className="bds-link-text-directory__header" span={{ base: 12, md: 6, lg: 8 }}>
      <h2 className="h-md">{heading}</h2>
      <p className="body-l">{description}</p>
    </PageGridCol>
  </PageGridRow>

  <PageGridRow>
    <PageGridCol span={{ base: 12, md: 8, lg: 8 }} offset={{ lg: 4 }}>
      <ul>
        {cards.map((card, index) => (
          <LinkTextCard
            key={index}
            index={index}
            heading={card.heading}
            description={card.description}
            buttons={card.buttons}
          />
        ))}
      </ul>
    </PageGridCol>
  </PageGridRow>
</PageGrid>

Key Design Decisions:

  • PageGrid Integration: Uses PageGrid system for responsive layout
  • Typography Classes: Uses existing h-md and body-l utility classes
  • Flexbox Header: Header uses flexbox with gap for spacing between heading and description
  • Desktop Right-Alignment: Cards offset by 4 columns at LG breakpoint (right-aligned)
  • Semantic List: Cards wrapped in <ul> with each card as <li>

Responsive Spacing

Breakpoint Section Padding Header Gap Header Margin-Bottom
Base (< 576px) 24px 8px 24px
MD (576px - 991px) 32px 8px 32px
LG (≥ 992px) 40px 16px 40px

Section Padding: Top and bottom padding on the entire section Header Gap: Space between heading and description (via flexbox gap) Header Margin-Bottom: Space between header and cards list

Styling

CSS Classes

  • .bds-link-text-directory - Main PageGrid container with section padding
  • .bds-link-text-directory__header - Header section (flexbox column with gap)

Typography

  • Heading: h-md class (responsive heading)
  • Description: body-l class (large body text)
  • Card content uses LinkTextCard's built-in typography

Grid Layout

  • Header Column: span={{ base: 12, md: 6, lg: 8 }}
  • Cards Column: span={{ base: 12, md: 8, lg: 8 }} with offset={{ lg: 4 }}
  • Cards are right-aligned on desktop via the 4-column offset

Dark Mode

  • Text color changes to white in dark mode
  • Applied to entire section via bds-theme-mode(dark) mixin

Card Numbering

Cards are automatically numbered based on their array index:

cards[0]  LinkTextCard(index: 0)  displays "01"
cards[1]  LinkTextCard(index: 1)  displays "02"
cards[2]  LinkTextCard(index: 2)  displays "03"
// ... and so on

Files

  • LinkTextDirectory.tsx - React component
  • LinkTextDirectory.scss - SCSS styles
  • index.ts - Barrel exports
  • README.md - This file
  • LinkTextCard: Used for each card in the list
  • ButtonGroup: Used by LinkTextCard for action buttons

Import

import { LinkTextDirectory } from 'shared/sections/LinkTextDirectory';
// or
import { 
  LinkTextDirectory, 
  type LinkTextDirectoryProps,
  type LinkTextCardData 
} from 'shared/sections/LinkTextDirectory';

Design System

Part of the Brand Design System (BDS) with bds- namespace prefix.

Best Practices

  1. Consistent Card Content: Try to keep similar text lengths across cards for visual balance
  2. Limit Cards: 3-6 cards works best for readability
  3. Clear Descriptions: Keep descriptions concise but informative
  4. Button Labels: Use clear, action-oriented button labels
  5. Logical Ordering: Order cards by priority or logical sequence

Accessibility

  • Semantic HTML with proper heading hierarchy (<h2> for section, <h5> for cards)
  • Semantic list structure: <ul> containing <li> elements
  • Sequential tab order through cards and buttons
  • ARIA-compliant button and link elements (via ButtonGroup)
  • Maintains focus order: heading → description → card 1 → card 2 → etc.

Best Practices for React Keys

When mapping over cards, use a stable identifier instead of array index:

// ❌ Avoid using index as key
{cards.map((card, index) => (
  <LinkTextCard key={index} ... />
))}

// ✅ Better: Use a unique identifier
{cards.map((card, index) => (
  <LinkTextCard key={card.id || card.heading} ... />
))}

// ✅ Best: Add an id field to LinkTextCardData
interface LinkTextCardData {
  id: string;  // Unique identifier
  heading: string;
  description: string;
  buttons: ButtonConfig[];
}

{cards.map((card) => (
  <LinkTextCard key={card.id} ... />
))}

Example with All Features

<LinkTextDirectory
  heading="Why Choose XRPL"
  description="The most efficient blockchain for real-world applications"
  cards={[
    {
      heading: "Lightning Fast",
      description: "Process thousands of transactions per second with sub-3-second finality",
      buttons: [
        { label: "View Benchmarks", href: "/performance" },
        { label: "Try Demo", onClick: () => openDemo() }
      ]
    },
    {
      heading: "Cost Effective",
      description: "Minimal transaction fees make XRPL perfect for micro-transactions and high-volume use cases",
      buttons: [
        { label: "See Pricing", href: "/pricing" }
      ]
    },
    {
      heading: "Battle Tested",
      description: "Over 10 years of continuous operation with billions of transactions processed",
      buttons: [
        { label: "Read Case Studies", href: "/case-studies" },
        { label: "View Stats", href: "/statistics" }
      ]
    }
  ]}
  className="my-custom-class"
/>