From 2ff14e4224abdce76a6982edeaf8ea12f11a56ad Mon Sep 17 00:00:00 2001 From: akcodez Date: Tue, 2 Dec 2025 10:21:52 -0800 Subject: [PATCH] Add BDS link styles and update existing link styles - Introduced new styles for BDS link icons, including hover and focus states. - Updated existing link styles to exclude BDS links from certain color and hover effects. - Ensured consistent styling across light and dark themes for BDS links. - Refactored landing page link styles to accommodate new BDS link classes. --- about/link-showcase.page.tsx | 310 +++++++++++++++++++ shared/components/Link/Link.md | 395 ++++++++++++++++++++++++ shared/components/Link/Link.tsx | 168 ++++++++++ shared/components/Link/LinkArrow.tsx | 165 ++++++++++ shared/components/Link/_link-icons.scss | 124 ++++++++ shared/components/Link/_link.scss | 243 +++++++++++++++ shared/components/Link/index.ts | 5 + static/css/devportal2024-v1.css | 344 +++++++++++++++++++-- styles/_landings.scss | 4 +- styles/light/_light-theme.scss | 10 +- styles/xrpl.scss | 2 + 11 files changed, 1736 insertions(+), 34 deletions(-) create mode 100644 about/link-showcase.page.tsx create mode 100644 shared/components/Link/Link.md create mode 100644 shared/components/Link/Link.tsx create mode 100644 shared/components/Link/LinkArrow.tsx create mode 100644 shared/components/Link/_link-icons.scss create mode 100644 shared/components/Link/_link.scss create mode 100644 shared/components/Link/index.ts diff --git a/about/link-showcase.page.tsx b/about/link-showcase.page.tsx new file mode 100644 index 0000000000..d45d14b607 --- /dev/null +++ b/about/link-showcase.page.tsx @@ -0,0 +1,310 @@ +import * as React from "react"; +import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid"; +import { Link } from "shared/components/Link/Link"; + +export const frontmatter = { + seo: { + title: 'Link Component Showcase', + description: "A comprehensive showcase of all Link component variants, sizes, and states in the XRPL.org Design System.", + } +}; + +export default function LinkShowcase() { + return ( +
+
+
+
+
Component Showcase
+

Link Component

+

+ A comprehensive showcase of all Link component variants, sizes, and states. +

+
+
+ + + + +

Size by Variant Matrix

+
+ {/* Header Row */} +
+
+
Size
+
+
+
Internal Links
+
+
+
External Links
+
+
+
Disabled State
+
+
+ {/* Small Row */} +
+
+ Small +
+
+ + Small Internal Link + +
+
+ + Small External Link + +
+
+ + Disabled Internal Link + +
+
+ {/* Medium Row */} +
+
+ Medium +
+
+ + Medium Internal Link + +
+
+ + Medium External Link + +
+
+ + Disabled External Link + +
+
+ {/* Large Row */} +
+
+ Large +
+
+ + Large Internal Link + +
+
+ + Large External Link + +
+
+ + Disabled Internal Link + +
+
+
+
+
+
+ + + + +

Sizes

+
+
+
Small
+ + Small Link + +
+
+
Medium
+ + Medium Link + +
+
+
Large
+ + Large Link + +
+
+
+
+
+ + + + +

Color States

+

Links automatically handle color states via CSS per theme:

+ +
+ {/* Light Mode Colors */} +
+
Light Mode
+
    +
  • Enabled: Green 400 #0DAA3E
  • +
  • Hover/Focus: Green 500 #078139 + underline
  • +
  • Active: Green 400 #0DAA3E + underline
  • +
  • Visited: Lilac 400 #7649E3
  • +
  • Disabled: Gray 400 #A2A2A4
  • +
  • Focus Outline: Black #000000
  • +
+
+ + {/* Dark Mode Colors */} +
+
Dark Mode
+
    +
  • Enabled: Green 300 #21E46B
  • +
  • Hover/Focus: Green 200 #70EE97 + underline
  • +
  • Active: Green 300 #21E46B + underline
  • +
  • Visited: Lilac 300 #C0A7FF
  • +
  • Disabled: Gray 500 #838386
  • +
  • Focus Outline: White #FFFFFF
  • +
+
+
+ +
+
+
Default (hover to see state changes and arrow animation)
+ + Default Link + +
+
+
Disabled
+ + Disabled Link + +
+
+
+
+
+ + + + +

Icon Types

+

Arrow icons animate to chevron shape on hover (150ms cubic-bezier transition).

+
+
+
Arrow (Internal) - animates to chevron on hover
+ + Arrow Link + +
+
+
External
+ + External Link + +
+
+
Inline (No Icon)
+

+ This is a paragraph with an{" "} + + inline link + {" "} + embedded within the text. +

+
+
+
+
+
+ + + + +

Variants

+
+
+
Internal
+ + Internal Link + +
+
+
External
+ + External Link + +
+
+
Inline
+

+ This is a paragraph with an{" "} + + inline link + {" "} + that appears within the text flow. +

+
+
+
+
+
+ + + + +

Real-World Examples

+
+
+
Navigation Links
+
+ + View Documentation + + + Learn More About XRPL + + + GitHub Repository + +
+
+
+
Inline Text Links
+

+ The XRP Ledger is a decentralized public blockchain. You can{" "} + + read the technical documentation + {" "} + to learn more about how it works. The network is maintained by a{" "} + + global community + {" "} + of developers and validators. +

+
+
+
Call-to-Action Links
+
+ + Get Started with XRPL + + + Explore Use Cases + +
+
+
+
+
+
+
+
+ ); +} diff --git a/shared/components/Link/Link.md b/shared/components/Link/Link.md new file mode 100644 index 0000000000..c54e201f2b --- /dev/null +++ b/shared/components/Link/Link.md @@ -0,0 +1,395 @@ +# Link Component + +A comprehensive, accessible link component from the XRPL.org Brand Design System (BDS). Supports multiple variants, sizes, and automatic theme-aware color states with animated arrow icons. + +## Table of Contents + +- [Installation](#installation) +- [Basic Usage](#basic-usage) +- [Props API](#props-api) +- [Variants](#variants) +- [Sizes](#sizes) +- [Color States](#color-states) +- [Icon Animations](#icon-animations) +- [Accessibility](#accessibility) +- [Best Practices](#best-practices) +- [Examples](#examples) +- [Related Components](#related-components) + +--- + +## Installation + +```tsx +import { Link } from 'shared/components/Link'; +// or +import { Link } from 'shared/components/Link/Link'; +``` + +--- + +## Basic Usage + +```tsx +// Internal link (default) +View Documentation + +// External link + + External Resource + + +// Inline link (no icon) +

+ Learn more about our mission. +

+``` + +--- + +## Props API + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `href` | `string` | **required** | The URL the link points to | +| `variant` | `'internal' \| 'external' \| 'inline'` | `'internal'` | Link variant determining icon and behavior | +| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Size of the link text and icon | +| `icon` | `'arrow' \| 'external' \| null` | `null` | Override icon type (auto-determined by variant if `null`) | +| `disabled` | `boolean` | `false` | Disables the link, preventing navigation | +| `children` | `React.ReactNode` | **required** | Link text content | +| `className` | `string` | - | Additional CSS classes | +| `...rest` | `AnchorHTMLAttributes` | - | All standard anchor attributes (`target`, `rel`, etc.) | + +--- + +## Variants + +### Internal (Default) + +For navigation within the same website. Displays a horizontal arrow (→) that animates to a chevron (>) on hover. + +```tsx + + Internal Documentation + +``` + +### External + +For links to external websites. Displays a diagonal arrow with corner bracket (↗) that animates on hover. Always use with `target="_blank"` and `rel="noopener noreferrer"` for security. + +```tsx + + GitHub Repository + +``` + +### Inline + +For links embedded within body text. No icon is displayed, making the link flow naturally within paragraphs. + +```tsx +

+ The XRP Ledger is a decentralized blockchain. You can{" "} + read the documentation{" "} + to learn more. +

+``` + +--- + +## Sizes + +| Size | Font Size | Line Height | Icon Gap | +|------|-----------|-------------|----------| +| `small` | 14px | 1.5 | 6px | +| `medium` | 16px | 1.5 | 8px | +| `large` | 20px | 1.5 | 10px | + +```tsx +Small Link +Medium Link +Large Link +``` + +--- + +## Color States + +The Link component automatically handles color states based on the current theme. Colors are applied via CSS and follow the Figma design specifications. + +### Light Mode + +| State | Color Token | Hex Value | Additional Styles | +|-------|-------------|-----------|-------------------| +| **Enabled** | Green 400 | `#0DAA3E` | No underline | +| **Hover** | Green 500 | `#078139` | Underline, arrow animates | +| **Focus** | Green 500 | `#078139` | Underline, black outline | +| **Active** | Green 400 | `#0DAA3E` | Underline | +| **Visited** | Lilac 400 | `#7649E3` | No underline | +| **Disabled** | Gray 400 | `#A2A2A4` | No underline, no pointer | + +### Dark Mode + +| State | Color Token | Hex Value | Additional Styles | +|-------|-------------|-----------|-------------------| +| **Enabled** | Green 300 | `#21E46B` | No underline | +| **Hover** | Green 200 | `#70EE97` | Underline, arrow animates | +| **Focus** | Green 200 | `#70EE97` | Underline, white outline | +| **Active** | Green 300 | `#21E46B` | Underline | +| **Visited** | Lilac 300 | `#C0A7FF` | No underline | +| **Disabled** | Gray 500 | `#838386` | No underline, no pointer | + +### Focus Outline + +- **Light Mode**: 2px solid black (`#000000`) +- **Dark Mode**: 2px solid white (`#FFFFFF`) + +--- + +## Icon Animations + +Both internal and external arrow icons feature a smooth animation on hover/focus: + +- **Animation Duration**: 150ms +- **Timing Function**: `cubic-bezier(0.98, 0.12, 0.12, 0.98)` + +### Internal Arrow Animation + +The horizontal line of the arrow (→) shrinks from left to right, revealing a chevron shape (>). + +### External Arrow Animation + +The diagonal line of the external arrow (↗) scales down toward the top-right corner, leaving just the corner bracket. + +### Disabled State + +When disabled, the animation is disabled and the icon opacity is reduced to 50%. + +--- + +## Accessibility + +The Link component follows accessibility best practices: + +### Keyboard Navigation + +- Focusable via Tab key +- Activatable via Enter key +- Clear focus indicator with high-contrast outline + +### ARIA Attributes + +- `aria-disabled="true"` is applied when the link is disabled +- Icons are marked with `aria-hidden="true"` to prevent screen reader announcement + +### Best Practices + +1. **Use descriptive link text** - Avoid "click here" or "read more" +2. **External links** - Consider adding "(opens in new tab)" for screen readers +3. **Disabled state** - Provide context for why the link is disabled + +```tsx +// Good - Descriptive link text +View pricing plans + +// Bad - Non-descriptive +Click here + +// External with screen reader context + + GitHub Repository + +``` + +--- + +## Best Practices + +### Do's + +1. **Use appropriate variants** + ```tsx + // Internal navigation + About Us + + // External sites + + GitHub + + + // Within paragraphs +

Learn about XRP today.

+ ``` + +2. **Match size to context** + ```tsx + // Navigation/CTA - use large + Get Started + + // Body content - use medium + Documentation + + // Footnotes/captions - use small + Terms of Service + ``` + +3. **Always use security attributes for external links** + ```tsx + + External Site + + ``` + +### Don'ts + +1. **Don't use disabled for navigation prevention** - Use proper routing instead + ```tsx + // Bad - Using disabled for auth gate + Dashboard + + // Good - Handle in onClick or router + Dashboard + ``` + +2. **Don't mix variants inappropriately** + ```tsx + // Bad - External link with internal variant + External Site + + // Good + External Site + ``` + +3. **Don't use inline variant for standalone links** + ```tsx + // Bad - Standalone inline link + Documentation + + // Good - Use internal for standalone + Documentation + ``` + +--- + +## Examples + +### Navigation Menu + +```tsx + +``` + +### Call-to-Action Section + +```tsx +
+ + Get Started with XRPL + + + Explore Use Cases + +
+``` + +### Rich Text Content + +```tsx +
+

+ The XRP Ledger (XRPL) is a decentralized, public blockchain led by a{" "} + global community{" "} + of businesses and developers. It supports a wide variety of{" "} + use cases{" "} + including payments, tokenization, and DeFi. +

+ +

+ To learn more, check out the{" "} + official documentation{" "} + or visit the{" "} + + GitHub repository + . +

+
+``` + +### Disabled State + +```tsx +
+ + Premium Features (Coming Soon) + + This feature is not yet available. +
+``` + +--- + +## Related Components + +- **LinkArrow** - The animated arrow icon component used internally by Link +- **Button** - For actions that don't navigate (forms, modals, etc.) + +--- + +## File Structure + +``` +shared/components/Link/ +├── index.ts # Exports +├── Link.tsx # Main component +├── Link.md # This documentation +├── LinkArrow.tsx # Arrow icon component +├── _link.scss # Link styles +└── _link-icons.scss # Arrow icon styles & animations +``` + +--- + +## Changelog + +### v1.0.0 + +- Initial release with internal, external, and inline variants +- Three size options (small, medium, large) +- Theme-aware color states (light/dark mode) +- Animated arrow icons +- Full accessibility support diff --git a/shared/components/Link/Link.tsx b/shared/components/Link/Link.tsx new file mode 100644 index 0000000000..0d2b6fe399 --- /dev/null +++ b/shared/components/Link/Link.tsx @@ -0,0 +1,168 @@ +import React from 'react'; +import clsx from 'clsx'; +import { LinkArrow, LinkArrowVariant } from './LinkArrow'; + +export type LinkVariant = 'internal' | 'external' | 'inline'; +export type LinkSize = 'small' | 'medium' | 'large'; +export type LinkIconType = 'arrow' | 'external' | null; + +export interface LinkProps extends React.AnchorHTMLAttributes { + /** + * Link variant - internal, external, or inline + * @default 'internal' + */ + variant?: LinkVariant; + + /** + * Size of the link + * @default 'medium' + */ + size?: LinkSize; + + /** + * Icon type - arrow, external, or null + * If null, icon is determined by variant (internal/external) + * Arrow icons animate to chevron shape on hover + * @default null + */ + icon?: LinkIconType; + + /** + * Disabled state - prevents navigation and applies disabled styles + * @default false + */ + disabled?: boolean; + + /** + * Link URL (required) + */ + href: string; + + /** + * Link text content + */ + children: React.ReactNode; +} + +/** + * Link Component + * + * A comprehensive link component supporting multiple sizes, icon types, and states. + * Arrow icons animate to chevron shape on hover. + * + * Color states are handled automatically via CSS per theme: + * + * Light Mode: + * - Enabled: Green 400 (#0DAA3E) + * - Hover/Focus: Green 500 (#078139) + underline + arrow animates to chevron + * - Active: Green 400 (#0DAA3E) + underline + * - Visited: Lilac 400 (#7649E3) + * - Disabled: Gray 400 (#A2A2A4) + * - Focus outline: Black (#000000) + * + * Dark Mode: + * - Enabled: Green 300 (#21E46B) + * - Hover/Focus: Green 200 (#70EE97) + underline + arrow animates to chevron + * - Active: Green 300 (#21E46B) + underline + * - Visited: Lilac 300 (#C0A7FF) + * - Disabled: Gray 500 (#838386) + * - Focus outline: White (#FFFFFF) + * + * @see Link.md for full documentation + * + * @example + * ```tsx + * // Basic internal link (arrow animates to chevron on hover) + * + * View documentation + * + * + * // External link + * + * External resource + * + * + * // Disabled link + * + * Coming soon + * + * + * // Inline link (no icon) + * + * Learn more + * + * ``` + */ +export const Link: React.FC = ({ + variant = 'internal', + size = 'medium', + icon = null, + disabled = false, + href, + children, + className, + onClick, + ...rest +}) => { + // Determine icon type based on variant if not explicitly provided + const getIconType = (): LinkArrowVariant | null => { + if (icon === null) { + // Auto-determine icon based on variant + if (variant === 'external') { + return 'external'; + } + if (variant === 'internal') { + return 'internal'; // Default to internal arrow for internal variant + } + return null; // Inline links have no icon + } + + // Map icon prop to LinkArrow variant + if (icon === 'arrow') return 'internal'; + if (icon === 'external') return 'external'; + return null; + }; + + const iconType = getIconType(); + const shouldShowIcon = variant !== 'inline' && iconType !== null; + + const classes = clsx( + 'bds-link', + `bds-link--${variant}`, + `bds-link--${size}`, + { + 'bds-link--disabled': disabled, + }, + className + ); + + const handleClick = (e: React.MouseEvent) => { + if (disabled) { + e.preventDefault(); + e.stopPropagation(); + return; + } + onClick?.(e); + }; + + return ( + + {children} + {shouldShowIcon && ( + + )} + + ); +}; + +Link.displayName = 'Link'; diff --git a/shared/components/Link/LinkArrow.tsx b/shared/components/Link/LinkArrow.tsx new file mode 100644 index 0000000000..dfe85c8d36 --- /dev/null +++ b/shared/components/Link/LinkArrow.tsx @@ -0,0 +1,165 @@ +import React from 'react'; +import clsx from 'clsx'; + +export type LinkArrowVariant = 'internal' | 'external'; +export type LinkArrowSize = 'small' | 'medium' | 'large'; + +export interface LinkArrowProps extends React.SVGProps { + /** + * Arrow variant - internal (→) or external (↗) + * Both variants animate on hover (horizontal line shrinks to show chevron) + * @default 'internal' + */ + variant?: LinkArrowVariant; + + /** + * Size of the arrow icon + * @default 'medium' + */ + size?: LinkArrowSize; + + /** + * Color of the arrow (hex color or CSS color value) + * @default 'currentColor' (inherits from parent) + */ + color?: string; + + /** + * Disabled state - reduces opacity and prevents hover animation + * @default false + */ + disabled?: boolean; + + /** + * Additional CSS classes + */ + className?: string; +} + +// Size mappings for internal arrow (viewBox 0 0 26 22) +const internalSizeMap: Record = { + small: { width: 15, height: 14 }, + medium: { width: 17, height: 16 }, + large: { width: 26, height: 22 }, +}; + +// Size mappings for external arrow (viewBox 0 0 21 21, square aspect ratio) +const externalSizeMap: Record = { + small: { width: 14, height: 14 }, + medium: { width: 16, height: 16 }, + large: { width: 21, height: 21 }, +}; + +/** + * LinkArrow Component + * + * A customizable SVG arrow icon for use in link components. + * Supports internal (→) and external (↗) variants with three size options. + * Both variants animate on hover - horizontal line shrinks to reveal chevron shape. + * + * @example + * ```tsx + * + * + * + * ``` + */ +export const LinkArrow: React.FC = ({ + variant = 'internal', + size = 'medium', + color = 'currentColor', + disabled = false, + className, + ...svgProps +}) => { + const dimensions = variant === 'external' + ? externalSizeMap[size] + : internalSizeMap[size]; + + const classes = clsx( + 'bds-link-icon', + `bds-link-icon--${variant}`, + `bds-link-icon--${size}`, + { + 'bds-link-icon--disabled': disabled, + }, + className + ); + + // Internal arrow (→) - horizontal arrow pointing right + // Horizontal line animates away on hover to show chevron + const renderInternalArrow = () => ( + + ); + + // External arrow (↗) - diagonal arrow with corner bracket + // Diagonal line animates away on hover, leaving just the chevron bracket + const renderExternalArrow = () => ( + + ); + + return ( + + {variant === 'external' ? renderExternalArrow() : renderInternalArrow()} + + ); +}; + +LinkArrow.displayName = 'LinkArrow'; diff --git a/shared/components/Link/_link-icons.scss b/shared/components/Link/_link-icons.scss new file mode 100644 index 0000000000..b98e36dd13 --- /dev/null +++ b/shared/components/Link/_link-icons.scss @@ -0,0 +1,124 @@ +// Link Arrow Icon Styles and Animations +// ----------------------------------------------------------------------------- +// Styles for link arrow icons with hover animations that transform arrow to chevron +// Animation: 150ms with custom cubic-bezier per Figma specs + +@import "../../../styles/_colors.scss"; + +// Animation timing per Figma +$bds-link-transition-duration: 150ms; +$bds-link-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98); + +// Base styles for link icons +.bds-link-icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + svg { + display: block; + + path { + stroke: currentColor; // Inherits color from parent link + } + + .arrow-horizontal { + transition: transform $bds-link-transition-duration $bds-link-transition-timing; + } + } +} + +// Internal arrow: horizontal line shrinks from left toward right (toward the chevron) +.bds-link-icon--internal svg .arrow-horizontal { + transform-origin: right center; +} + +// External arrow: diagonal line shrinks toward top-right corner (leaving just the bracket) +.bds-link-icon--external svg .arrow-horizontal { + transform-origin: 19px 2px; // Top-right corner where diagonal starts +} + +// Hover state - shrink/hide the horizontal line to create chevron effect +// Applies to both internal and external icons when not disabled + +// Internal: scale the horizontal line from right +a:hover .bds-link-icon--internal:not(.bds-link-icon--disabled) svg .arrow-horizontal, +a:focus .bds-link-icon--internal:not(.bds-link-icon--disabled) svg .arrow-horizontal { + transform: scaleX(0); +} + +// External: scale the diagonal line toward the corner (uniform scale for diagonal) +a:hover .bds-link-icon--external:not(.bds-link-icon--disabled) svg .arrow-horizontal, +a:focus .bds-link-icon--external:not(.bds-link-icon--disabled) svg .arrow-horizontal { + transform: scale(0); +} + +// Disabled state +.bds-link-icon--disabled { + opacity: 0.5; + cursor: not-allowed; + + svg { + path { + // Disabled icons use gray color from theme + stroke: currentColor; + } + + .arrow-horizontal { + transition: none; // Disable animation when disabled + } + } +} + +// Theme-specific disabled colors +html.light, +.light { + .bds-link-icon--disabled svg path { + stroke: $gray-400; + } +} + +html.dark, +.dark, +html:not(.light) { + .bds-link-icon--disabled svg path { + stroke: $gray-500; + } +} + +// Size variants for internal arrows (wider aspect ratio) +.bds-link-icon--internal { + &.bds-link-icon--small { + width: 15px; + height: 14px; + } + + &.bds-link-icon--medium { + width: 17px; + height: 16px; + } + + &.bds-link-icon--large { + width: 26px; + height: 22px; + } +} + +// Size variants for external arrows (square aspect ratio) +.bds-link-icon--external { + &.bds-link-icon--small { + width: 14px; + height: 14px; + } + + &.bds-link-icon--medium { + width: 16px; + height: 16px; + } + + &.bds-link-icon--large { + width: 21px; + height: 21px; + } +} diff --git a/shared/components/Link/_link.scss b/shared/components/Link/_link.scss new file mode 100644 index 0000000000..bf48f23c57 --- /dev/null +++ b/shared/components/Link/_link.scss @@ -0,0 +1,243 @@ +// Link Component Styles +// ----------------------------------------------------------------------------- +// Styles for the Link component with support for sizes, states, and themes +// Light mode colors per Figma: Enabled=green-400, Hover/Focus=green-500+underline, +// Active=green-400+underline, Visited=lilac-400, Disabled=gray-400 +// Dark mode colors per Figma: Enabled=green-300, Hover/Focus=green-200+underline, +// Active=green-300+underline, Visited=lilac-300, Disabled=gray-500 + +@import "../../../styles/_colors.scss"; + +// Base link styles +.bds-link { + display: inline-flex; + align-items: center; + gap: 8px; + text-decoration: none; + transition: color 0.2s ease, text-decoration 0.2s ease; + cursor: pointer; + + // Focus styles for accessibility (outline color set per theme below) + &:focus-visible { + outline: 2px solid $white; // Default to white (dark mode) + outline-offset: 2px; + } + + // Icon spacing + .bds-link-icon { + margin-left: 0; + flex-shrink: 0; + } +} + +// Size variants +.bds-link--small { + font-size: 14px; + line-height: 1.5; + gap: 6px; +} + +.bds-link--medium { + font-size: 16px; + line-height: 1.5; + gap: 8px; +} + +.bds-link--large { + font-size: 20px; + line-height: 1.5; + gap: 10px; +} + +// Link color states (Light Mode - per Figma specs) +// Use element + class selector for higher specificity to override html.light a rules +a.bds-link, +.bds-link { + // Enabled state: Green 400 + color: $green-400; + text-decoration: none; + + // Hover state: Green 500 + underline + &:hover:not(.bds-link--disabled) { + color: $green-500; + text-decoration: underline; + } + + // Focus state: Green 500 + underline + &:focus:not(.bds-link--disabled) { + color: $green-500; + text-decoration: underline; + } + + // Active state: Green 400 + underline + &:active:not(.bds-link--disabled) { + color: $green-400; + text-decoration: underline; + } + + // Visited state: Lilac 400 (purple) + &:visited:not(.bds-link--disabled) { + color: $purple; + } +} + +// Light theme overrides - BDS links are excluded from general light theme rules +// so these rules will apply naturally without needing !important +html.light { + a.bds-link, + nav a.bds-link { + // Enabled state: Green 400 + color: $green-400; + text-decoration: none; + + // Focus outline: Black for light mode + &:focus-visible { + outline-color: $black; + } + + // Hover state: Green 500 + underline + &:hover:not(.bds-link--disabled) { + color: $green-500; + text-decoration: underline; + } + + // Focus state: Green 500 + underline + &:focus:not(.bds-link--disabled) { + color: $green-500; + text-decoration: underline; + } + + // Active state: Green 400 + underline + &:active:not(.bds-link--disabled) { + color: $green-400; + text-decoration: underline; + } + + // Visited state: Lilac 400 (purple) + &:visited:not(.bds-link--disabled) { + color: $purple; + } + + // Disabled state - needs to be here for specificity + &.bds-link--disabled { + color: $gray-400; + cursor: not-allowed; + pointer-events: none; + text-decoration: none; + + &:hover, + &:focus, + &:active, + &:visited { + color: $gray-400; + text-decoration: none; + } + } + } +} + +// Dark theme styles (per Figma specs) +html.dark { + a.bds-link, + nav a.bds-link { + // Enabled state: Green 300 + color: $green-300; + text-decoration: none; + + // Focus outline: White for dark mode + &:focus-visible { + outline-color: $white; + } + + // Hover state: Green 200 + underline + &:hover:not(.bds-link--disabled) { + color: $green-200; + text-decoration: underline; + } + + // Focus state: Green 200 + underline + &:focus:not(.bds-link--disabled) { + color: $green-200; + text-decoration: underline; + } + + // Active state: Green 300 + underline + &:active:not(.bds-link--disabled) { + color: $green-300; + text-decoration: underline; + } + + // Visited state: Lilac 300 + &:visited:not(.bds-link--disabled) { + color: $lilac-300; + } + + // Disabled state - needs to be here for specificity + &.bds-link--disabled { + color: $gray-500; + cursor: not-allowed; + pointer-events: none; + text-decoration: none; + + &:hover, + &:focus, + &:active, + &:visited { + color: $gray-500; + text-decoration: none; + } + } + } +} + +// Disabled state (base/dark theme) +// Use element + class selector for higher specificity +a.bds-link.bds-link--disabled, +.bds-link.bds-link--disabled { + color: $gray-400; + cursor: not-allowed; + pointer-events: none; + text-decoration: none; + + .bds-link-icon { + opacity: 0.5; + } + + &:hover, + &:focus, + &:active, + &:visited { + color: $gray-400; + text-decoration: none; + } +} + +// Dark theme adjustments for disabled (fallback for non-html.dark contexts) +html.dark, +.dark, +html:not(.light) { + a.bds-link.bds-link--disabled, + .bds-link.bds-link--disabled { + color: $gray-500; + + &:hover, + &:focus, + &:active, + &:visited { + color: $gray-500; + } + } +} + +// Inline variant (no icon spacing adjustment needed) +.bds-link--inline { + display: inline; + gap: 0; + + .bds-link-icon { + display: none; + } +} + +// Standalone variants (internal/external) +// These variants use icons, spacing is handled by gap property in .bds-link diff --git a/shared/components/Link/index.ts b/shared/components/Link/index.ts new file mode 100644 index 0000000000..cca8b78c79 --- /dev/null +++ b/shared/components/Link/index.ts @@ -0,0 +1,5 @@ +export { Link } from './Link'; +export type { LinkProps, LinkVariant, LinkSize, LinkColor, LinkIconType } from './Link'; + +export { LinkArrow } from './LinkArrow'; +export type { LinkArrowProps, LinkArrowVariant, LinkArrowSize } from './LinkArrow'; diff --git a/static/css/devportal2024-v1.css b/static/css/devportal2024-v1.css index a8c5235107..430dc9fe52 100644 --- a/static/css/devportal2024-v1.css +++ b/static/css/devportal2024-v1.css @@ -14087,6 +14087,296 @@ h1:hover .hover_anchor, .h1:hover .hover_anchor, h2:hover .hover_anchor, .h2:hov margin-left: 91.66666667%; } } +.bds-link-icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} +.bds-link-icon svg { + display: block; +} +.bds-link-icon svg path { + stroke: currentColor; +} +.bds-link-icon svg .arrow-horizontal { + transition: transform 150ms cubic-bezier(0.98, 0.12, 0.12, 0.98); +} + +.bds-link-icon--internal svg .arrow-horizontal { + transform-origin: right center; +} + +.bds-link-icon--external svg .arrow-horizontal { + transform-origin: 19px 2px; +} + +a:hover .bds-link-icon--internal:not(.bds-link-icon--disabled) svg .arrow-horizontal, +a:focus .bds-link-icon--internal:not(.bds-link-icon--disabled) svg .arrow-horizontal { + transform: scaleX(0); +} + +a:hover .bds-link-icon--external:not(.bds-link-icon--disabled) svg .arrow-horizontal, +a:focus .bds-link-icon--external:not(.bds-link-icon--disabled) svg .arrow-horizontal { + transform: scale(0); +} + +.bds-link-icon--disabled { + opacity: 0.5; + cursor: not-allowed; +} +.bds-link-icon--disabled svg path { + stroke: currentColor; +} +.bds-link-icon--disabled svg .arrow-horizontal { + transition: none; +} + +html.light .bds-link-icon--disabled svg path, +.light .bds-link-icon--disabled svg path { + stroke: #A2A2A4; +} + +html.dark .bds-link-icon--disabled svg path, +.dark .bds-link-icon--disabled svg path, +html:not(.light) .bds-link-icon--disabled svg path { + stroke: #838386; +} + +.bds-link-icon--internal.bds-link-icon--small { + width: 15px; + height: 14px; +} +.bds-link-icon--internal.bds-link-icon--medium { + width: 17px; + height: 16px; +} +.bds-link-icon--internal.bds-link-icon--large { + width: 26px; + height: 22px; +} + +.bds-link-icon--external.bds-link-icon--small { + width: 14px; + height: 14px; +} +.bds-link-icon--external.bds-link-icon--medium { + width: 16px; + height: 16px; +} +.bds-link-icon--external.bds-link-icon--large { + width: 21px; + height: 21px; +} + +.bds-link { + display: inline-flex; + align-items: center; + gap: 8px; + text-decoration: none; + transition: color 0.2s ease, text-decoration 0.2s ease; + cursor: pointer; +} +.bds-link:focus-visible { + outline: 2px solid #FFFFFF; + outline-offset: 2px; +} +.bds-link .bds-link-icon { + margin-left: 0; + flex-shrink: 0; +} + +.bds-link--small { + font-size: 14px; + line-height: 1.5; + gap: 6px; +} + +.bds-link--medium { + font-size: 16px; + line-height: 1.5; + gap: 8px; +} + +.bds-link--large { + font-size: 20px; + line-height: 1.5; + gap: 10px; +} + +a.bds-link, +.bds-link { + color: #0DAA3E; + text-decoration: none; +} +a.bds-link:hover:not(.bds-link--disabled), +.bds-link:hover:not(.bds-link--disabled) { + color: #078139; + text-decoration: underline; +} +a.bds-link:focus:not(.bds-link--disabled), +.bds-link:focus:not(.bds-link--disabled) { + color: #078139; + text-decoration: underline; +} +a.bds-link:active:not(.bds-link--disabled), +.bds-link:active:not(.bds-link--disabled) { + color: #0DAA3E; + text-decoration: underline; +} +a.bds-link:visited:not(.bds-link--disabled), +.bds-link:visited:not(.bds-link--disabled) { + color: #7649E3; +} + +html.light a.bds-link, +html.light nav a.bds-link { + color: #0DAA3E; + text-decoration: none; +} +html.light a.bds-link:focus-visible, +html.light nav a.bds-link:focus-visible { + outline-color: #000000; +} +html.light a.bds-link:hover:not(.bds-link--disabled), +html.light nav a.bds-link:hover:not(.bds-link--disabled) { + color: #078139; + text-decoration: underline; +} +html.light a.bds-link:focus:not(.bds-link--disabled), +html.light nav a.bds-link:focus:not(.bds-link--disabled) { + color: #078139; + text-decoration: underline; +} +html.light a.bds-link:active:not(.bds-link--disabled), +html.light nav a.bds-link:active:not(.bds-link--disabled) { + color: #0DAA3E; + text-decoration: underline; +} +html.light a.bds-link:visited:not(.bds-link--disabled), +html.light nav a.bds-link:visited:not(.bds-link--disabled) { + color: #7649E3; +} +html.light a.bds-link.bds-link--disabled, +html.light nav a.bds-link.bds-link--disabled { + color: #A2A2A4; + cursor: not-allowed; + pointer-events: none; + text-decoration: none; +} +html.light a.bds-link.bds-link--disabled:hover, html.light a.bds-link.bds-link--disabled:focus, html.light a.bds-link.bds-link--disabled:active, html.light a.bds-link.bds-link--disabled:visited, +html.light nav a.bds-link.bds-link--disabled:hover, +html.light nav a.bds-link.bds-link--disabled:focus, +html.light nav a.bds-link.bds-link--disabled:active, +html.light nav a.bds-link.bds-link--disabled:visited { + color: #A2A2A4; + text-decoration: none; +} + +html.dark a.bds-link, +html.dark nav a.bds-link { + color: #21E46B; + text-decoration: none; +} +html.dark a.bds-link:focus-visible, +html.dark nav a.bds-link:focus-visible { + outline-color: #FFFFFF; +} +html.dark a.bds-link:hover:not(.bds-link--disabled), +html.dark nav a.bds-link:hover:not(.bds-link--disabled) { + color: #70EE97; + text-decoration: underline; +} +html.dark a.bds-link:focus:not(.bds-link--disabled), +html.dark nav a.bds-link:focus:not(.bds-link--disabled) { + color: #70EE97; + text-decoration: underline; +} +html.dark a.bds-link:active:not(.bds-link--disabled), +html.dark nav a.bds-link:active:not(.bds-link--disabled) { + color: #21E46B; + text-decoration: underline; +} +html.dark a.bds-link:visited:not(.bds-link--disabled), +html.dark nav a.bds-link:visited:not(.bds-link--disabled) { + color: #C0A7FF; +} +html.dark a.bds-link.bds-link--disabled, +html.dark nav a.bds-link.bds-link--disabled { + color: #838386; + cursor: not-allowed; + pointer-events: none; + text-decoration: none; +} +html.dark a.bds-link.bds-link--disabled:hover, html.dark a.bds-link.bds-link--disabled:focus, html.dark a.bds-link.bds-link--disabled:active, html.dark a.bds-link.bds-link--disabled:visited, +html.dark nav a.bds-link.bds-link--disabled:hover, +html.dark nav a.bds-link.bds-link--disabled:focus, +html.dark nav a.bds-link.bds-link--disabled:active, +html.dark nav a.bds-link.bds-link--disabled:visited { + color: #838386; + text-decoration: none; +} + +a.bds-link.bds-link--disabled, +.bds-link.bds-link--disabled { + color: #A2A2A4; + cursor: not-allowed; + pointer-events: none; + text-decoration: none; +} +a.bds-link.bds-link--disabled .bds-link-icon, +.bds-link.bds-link--disabled .bds-link-icon { + opacity: 0.5; +} +a.bds-link.bds-link--disabled:hover, a.bds-link.bds-link--disabled:focus, a.bds-link.bds-link--disabled:active, a.bds-link.bds-link--disabled:visited, +.bds-link.bds-link--disabled:hover, +.bds-link.bds-link--disabled:focus, +.bds-link.bds-link--disabled:active, +.bds-link.bds-link--disabled:visited { + color: #A2A2A4; + text-decoration: none; +} + +html.dark a.bds-link.bds-link--disabled, +html.dark .bds-link.bds-link--disabled, +.dark a.bds-link.bds-link--disabled, +.dark .bds-link.bds-link--disabled, +html:not(.light) a.bds-link.bds-link--disabled, +html:not(.light) .bds-link.bds-link--disabled { + color: #838386; +} +html.dark a.bds-link.bds-link--disabled:hover, html.dark a.bds-link.bds-link--disabled:focus, html.dark a.bds-link.bds-link--disabled:active, html.dark a.bds-link.bds-link--disabled:visited, +html.dark .bds-link.bds-link--disabled:hover, +html.dark .bds-link.bds-link--disabled:focus, +html.dark .bds-link.bds-link--disabled:active, +html.dark .bds-link.bds-link--disabled:visited, +.dark a.bds-link.bds-link--disabled:hover, +.dark a.bds-link.bds-link--disabled:focus, +.dark a.bds-link.bds-link--disabled:active, +.dark a.bds-link.bds-link--disabled:visited, +.dark .bds-link.bds-link--disabled:hover, +.dark .bds-link.bds-link--disabled:focus, +.dark .bds-link.bds-link--disabled:active, +.dark .bds-link.bds-link--disabled:visited, +html:not(.light) a.bds-link.bds-link--disabled:hover, +html:not(.light) a.bds-link.bds-link--disabled:focus, +html:not(.light) a.bds-link.bds-link--disabled:active, +html:not(.light) a.bds-link.bds-link--disabled:visited, +html:not(.light) .bds-link.bds-link--disabled:hover, +html:not(.light) .bds-link.bds-link--disabled:focus, +html:not(.light) .bds-link.bds-link--disabled:active, +html:not(.light) .bds-link.bds-link--disabled:visited { + color: #838386; +} + +.bds-link--inline { + display: inline; + gap: 0; +} +.bds-link--inline .bds-link-icon { + display: none; +} + pre { color: #FFFFFF; background-color: #232325; @@ -15297,15 +15587,15 @@ main article .card-grid.card-grid-3xN:nth-of-type(3) .card:nth-child(9) .card-fo margin-left: 6rem; list-style-type: square; } -.landing p a, -.landing h5 a, -.landing .h5 a { +.landing p a:not(.bds-link), +.landing h5 a:not(.bds-link), +.landing .h5 a:not(.bds-link) { color: #7649E3; font-weight: 600; } -.landing p a:hover, -.landing h5 a:hover, -.landing .h5 a:hover { +.landing p a:not(.bds-link):hover, +.landing h5 a:not(.bds-link):hover, +.landing .h5 a:not(.bds-link):hover { text-decoration: underline; } .landing { @@ -20453,33 +20743,33 @@ html.light .progress { html.light [data-component-name="Search/SearchIcon"] > path { fill: black; } -html.light a, -html.light nav a, -html.light a:not([role=button]) { +html.light a:not(.bds-link), +html.light nav a:not(.bds-link), +html.light a:not([role=button]):not(.bds-link) { color: #000000; } -html.light a.btn-primary, -html.light nav a.btn-primary, -html.light a:not([role=button]).btn-primary { +html.light a:not(.bds-link).btn-primary, +html.light nav a:not(.bds-link).btn-primary, +html.light a:not([role=button]):not(.bds-link).btn-primary { color: #FFFFFF; } -html.light a.btn-primary:hover, -html.light nav a.btn-primary:hover, -html.light a:not([role=button]).btn-primary:hover { +html.light a:not(.bds-link).btn-primary:hover, +html.light nav a:not(.bds-link).btn-primary:hover, +html.light a:not([role=button]):not(.bds-link).btn-primary:hover { color: #FFFFFF; } -html.light a:hover, html.light a:active, html.light a.active, -html.light nav a:hover, -html.light nav a:active, -html.light nav a.active, -html.light a:not([role=button]):hover, -html.light a:not([role=button]):active, -html.light a:not([role=button]).active { +html.light a:not(.bds-link):hover, html.light a:not(.bds-link):active, html.light a:not(.bds-link).active, +html.light nav a:not(.bds-link):hover, +html.light nav a:not(.bds-link):active, +html.light nav a:not(.bds-link).active, +html.light a:not([role=button]):not(.bds-link):hover, +html.light a:not([role=button]):not(.bds-link):active, +html.light a:not([role=button]):not(.bds-link).active { color: #5429A1; } -html.light a:not(.btn):focus, -html.light nav a:not(.btn):focus, -html.light a:not([role=button]):not(.btn):focus { +html.light a:not(.bds-link):not(.btn):focus, +html.light nav a:not(.bds-link):not(.btn):focus, +html.light a:not([role=button]):not(.bds-link):not(.btn):focus { background-color: transparent; } html.light a.card:hover, html.light:active, html.light.active { @@ -20677,8 +20967,8 @@ html.light .landing .circled-logo { html.light .landing .circled-logo img[src="assets/img/logos/globe.svg"] { filter: invert(100%); } -html.light .landing p a, -html.light .landing .longform a { +html.light .landing p a:not(.bds-link), +html.light .landing .longform a:not(.bds-link) { color: #5429A1; } html.light .devportal-callout.caution, diff --git a/styles/_landings.scss b/styles/_landings.scss index 137efcf8dc..5ae5ccd42b 100644 --- a/styles/_landings.scss +++ b/styles/_landings.scss @@ -67,8 +67,8 @@ } } - p a, - h5 a { + p a:not(.bds-link), + h5 a:not(.bds-link) { color: $blue-purple-400; font-weight: 600; &:hover { diff --git a/styles/light/_light-theme.scss b/styles/light/_light-theme.scss index 040b919ba1..bf695b32ab 100644 --- a/styles/light/_light-theme.scss +++ b/styles/light/_light-theme.scss @@ -123,9 +123,9 @@ h6, // Navigation ------------------------------------------------------------------ -a, -nav a, -a:not([role="button"]) { +a:not(.bds-link), +nav a:not(.bds-link), +a:not([role="button"]):not(.bds-link) { color: $light-fg; &.btn-primary { @@ -429,8 +429,8 @@ aside .card { } } - p a, - .longform a { + p a:not(.bds-link), + .longform a:not(.bds-link) { color: $light-link-hover-color; } } diff --git a/styles/xrpl.scss b/styles/xrpl.scss index 5fcb375574..2549d24255 100644 --- a/styles/xrpl.scss +++ b/styles/xrpl.scss @@ -93,6 +93,8 @@ $grid-breakpoints: ( @import "_top-banner.scss"; @import "_content.scss"; @import "../shared/components/PageGrid/page-grid"; +@import "../shared/components/Link/_link-icons.scss"; +@import "../shared/components/Link/_link.scss"; @import "_code-tabs.scss"; @import "_code-walkthrough.scss"; @import "_diagrams.scss";