mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-12-06 17:27:57 +00:00
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.
This commit is contained in:
310
about/link-showcase.page.tsx
Normal file
310
about/link-showcase.page.tsx
Normal file
@@ -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 (
|
||||
<div className="landing">
|
||||
<div className="overflow-hidden">
|
||||
<section className="py-26 text-center">
|
||||
<div className="col-lg-8 mx-auto">
|
||||
<h6 className="eyebrow mb-3">Component Showcase</h6>
|
||||
<h1 className="mb-4">Link Component</h1>
|
||||
<p className="longform">
|
||||
A comprehensive showcase of all Link component variants, sizes, and states.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Size by Variant Matrix</h2>
|
||||
<div className="mb-10">
|
||||
{/* Header Row */}
|
||||
<div className="d-flex flex-row mb-4" style={{ gap: '2rem' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}>
|
||||
<h6 className="mb-0">Size</h6>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>
|
||||
<h6 className="mb-0">Internal Links</h6>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>
|
||||
<h6 className="mb-0">External Links</h6>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>
|
||||
<h6 className="mb-0">Disabled State</h6>
|
||||
</div>
|
||||
</div>
|
||||
{/* Small Row */}
|
||||
<div className="d-flex flex-row mb-5 align-items-center" style={{ gap: '2rem' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}>
|
||||
<strong>Small</strong>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>
|
||||
<Link href="/docs" variant="internal" size="small">
|
||||
Small Internal Link
|
||||
</Link>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>
|
||||
<Link href="https://example.com" variant="external" size="small" target="_blank" rel="noopener noreferrer">
|
||||
Small External Link
|
||||
</Link>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>
|
||||
<Link href="#" variant="internal" size="small" disabled>
|
||||
Disabled Internal Link
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* Medium Row */}
|
||||
<div className="d-flex flex-row mb-5 align-items-center" style={{ gap: '2rem' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}>
|
||||
<strong>Medium</strong>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>
|
||||
<Link href="/docs" variant="internal" size="medium">
|
||||
Medium Internal Link
|
||||
</Link>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>
|
||||
<Link href="https://example.com" variant="external" size="medium" target="_blank" rel="noopener noreferrer">
|
||||
Medium External Link
|
||||
</Link>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>
|
||||
<Link href="#" variant="external" size="medium" disabled>
|
||||
Disabled External Link
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* Large Row */}
|
||||
<div className="d-flex flex-row align-items-center" style={{ gap: '2rem' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}>
|
||||
<strong>Large</strong>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>
|
||||
<Link href="/docs" variant="internal" size="large">
|
||||
Large Internal Link
|
||||
</Link>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>
|
||||
<Link href="https://example.com" variant="external" size="large" target="_blank" rel="noopener noreferrer">
|
||||
Large External Link
|
||||
</Link>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>
|
||||
<Link href="#" variant="internal" size="large" disabled>
|
||||
Disabled Internal Link
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Sizes</h2>
|
||||
<div className="d-flex flex-column gap-4 mb-10">
|
||||
<div>
|
||||
<h6 className="mb-3">Small</h6>
|
||||
<Link href="/docs" size="small">
|
||||
Small Link
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="mb-3">Medium</h6>
|
||||
<Link href="/docs" size="medium">
|
||||
Medium Link
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="mb-3">Large</h6>
|
||||
<Link href="/docs" size="large">
|
||||
Large Link
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Color States</h2>
|
||||
<p className="mb-4">Links automatically handle color states via CSS per theme:</p>
|
||||
|
||||
<div className="d-flex flex-row gap-6 mb-6" style={{ flexWrap: 'wrap' }}>
|
||||
{/* Light Mode Colors */}
|
||||
<div style={{ flex: '1 1 300px', minWidth: '280px' }}>
|
||||
<h6 className="mb-3">Light Mode</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Enabled:</strong> Green 400 <code style={{ color: '#0DAA3E' }}>#0DAA3E</code></li>
|
||||
<li><strong>Hover/Focus:</strong> Green 500 <code style={{ color: '#078139' }}>#078139</code> + underline</li>
|
||||
<li><strong>Active:</strong> Green 400 <code style={{ color: '#0DAA3E' }}>#0DAA3E</code> + underline</li>
|
||||
<li><strong>Visited:</strong> Lilac 400 <code style={{ color: '#7649E3' }}>#7649E3</code></li>
|
||||
<li><strong>Disabled:</strong> Gray 400 <code style={{ color: '#A2A2A4' }}>#A2A2A4</code></li>
|
||||
<li><strong>Focus Outline:</strong> Black <code style={{ color: '#000000' }}>#000000</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Dark Mode Colors */}
|
||||
<div style={{ flex: '1 1 300px', minWidth: '280px' }}>
|
||||
<h6 className="mb-3">Dark Mode</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Enabled:</strong> Green 300 <code style={{ color: '#21E46B' }}>#21E46B</code></li>
|
||||
<li><strong>Hover/Focus:</strong> Green 200 <code style={{ color: '#70EE97' }}>#70EE97</code> + underline</li>
|
||||
<li><strong>Active:</strong> Green 300 <code style={{ color: '#21E46B' }}>#21E46B</code> + underline</li>
|
||||
<li><strong>Visited:</strong> Lilac 300 <code style={{ color: '#C0A7FF' }}>#C0A7FF</code></li>
|
||||
<li><strong>Disabled:</strong> Gray 500 <code style={{ color: '#838386' }}>#838386</code></li>
|
||||
<li><strong>Focus Outline:</strong> White <code style={{ color: '#FFFFFF', backgroundColor: '#333', padding: '0 4px' }}>#FFFFFF</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-column gap-4 mb-10">
|
||||
<div>
|
||||
<h6 className="mb-3">Default (hover to see state changes and arrow animation)</h6>
|
||||
<Link href="/docs" size="medium">
|
||||
Default Link
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="mb-3">Disabled</h6>
|
||||
<Link href="#" size="medium" disabled>
|
||||
Disabled Link
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Icon Types</h2>
|
||||
<p className="mb-4">Arrow icons animate to chevron shape on hover (150ms cubic-bezier transition).</p>
|
||||
<div className="d-flex flex-column gap-4 mb-10">
|
||||
<div>
|
||||
<h6 className="mb-3">Arrow (Internal) - animates to chevron on hover</h6>
|
||||
<Link href="/docs" size="medium" icon="arrow">
|
||||
Arrow Link
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="mb-3">External</h6>
|
||||
<Link href="https://example.com" size="medium" variant="external" target="_blank" rel="noopener noreferrer">
|
||||
External Link
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="mb-3">Inline (No Icon)</h6>
|
||||
<p>
|
||||
This is a paragraph with an{" "}
|
||||
<Link href="/docs" variant="inline">
|
||||
inline link
|
||||
</Link>{" "}
|
||||
embedded within the text.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Variants</h2>
|
||||
<div className="d-flex flex-column gap-4 mb-10">
|
||||
<div>
|
||||
<h6 className="mb-3">Internal</h6>
|
||||
<Link href="/docs" variant="internal" size="medium">
|
||||
Internal Link
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="mb-3">External</h6>
|
||||
<Link href="https://example.com" variant="external" size="medium" target="_blank" rel="noopener noreferrer">
|
||||
External Link
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="mb-3">Inline</h6>
|
||||
<p>
|
||||
This is a paragraph with an{" "}
|
||||
<Link href="/docs" variant="inline">
|
||||
inline link
|
||||
</Link>{" "}
|
||||
that appears within the text flow.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Real-World Examples</h2>
|
||||
<div className="d-flex flex-column gap-6 mb-10">
|
||||
<div>
|
||||
<h6 className="mb-4">Navigation Links</h6>
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<Link href="/docs" size="medium">
|
||||
View Documentation
|
||||
</Link>
|
||||
<Link href="/about" size="medium">
|
||||
Learn More About XRPL
|
||||
</Link>
|
||||
<Link href="https://github.com/XRPLF/xrpl-dev-portal" variant="external" size="medium" target="_blank" rel="noopener noreferrer">
|
||||
GitHub Repository
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="mb-4">Inline Text Links</h6>
|
||||
<p className="longform">
|
||||
The XRP Ledger is a decentralized public blockchain. You can{" "}
|
||||
<Link href="/docs" variant="inline">
|
||||
read the technical documentation
|
||||
</Link>{" "}
|
||||
to learn more about how it works. The network is maintained by a{" "}
|
||||
<Link href="/about" variant="inline">
|
||||
global community
|
||||
</Link>{" "}
|
||||
of developers and validators.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h6 className="mb-4">Call-to-Action Links</h6>
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<Link href="/docs" size="large">
|
||||
Get Started with XRPL
|
||||
</Link>
|
||||
<Link href="/about/uses" size="large">
|
||||
Explore Use Cases
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
395
shared/components/Link/Link.md
Normal file
395
shared/components/Link/Link.md
Normal file
@@ -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)
|
||||
<Link href="/docs">View Documentation</Link>
|
||||
|
||||
// External link
|
||||
<Link href="https://example.com" variant="external" target="_blank" rel="noopener noreferrer">
|
||||
External Resource
|
||||
</Link>
|
||||
|
||||
// Inline link (no icon)
|
||||
<p>
|
||||
Learn more about <Link href="/about" variant="inline">our mission</Link>.
|
||||
</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
<Link href="/docs" variant="internal">
|
||||
Internal Documentation
|
||||
</Link>
|
||||
```
|
||||
|
||||
### 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
|
||||
<Link
|
||||
href="https://github.com/XRPLF"
|
||||
variant="external"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub Repository
|
||||
</Link>
|
||||
```
|
||||
|
||||
### Inline
|
||||
|
||||
For links embedded within body text. No icon is displayed, making the link flow naturally within paragraphs.
|
||||
|
||||
```tsx
|
||||
<p>
|
||||
The XRP Ledger is a decentralized blockchain. You can{" "}
|
||||
<Link href="/docs" variant="inline">read the documentation</Link>{" "}
|
||||
to learn more.
|
||||
</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sizes
|
||||
|
||||
| Size | Font Size | Line Height | Icon Gap |
|
||||
|------|-----------|-------------|----------|
|
||||
| `small` | 14px | 1.5 | 6px |
|
||||
| `medium` | 16px | 1.5 | 8px |
|
||||
| `large` | 20px | 1.5 | 10px |
|
||||
|
||||
```tsx
|
||||
<Link href="/docs" size="small">Small Link</Link>
|
||||
<Link href="/docs" size="medium">Medium Link</Link>
|
||||
<Link href="/docs" size="large">Large Link</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
|
||||
<Link href="/pricing">View pricing plans</Link>
|
||||
|
||||
// Bad - Non-descriptive
|
||||
<Link href="/pricing">Click here</Link>
|
||||
|
||||
// External with screen reader context
|
||||
<Link
|
||||
href="https://example.com"
|
||||
variant="external"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="GitHub Repository (opens in new tab)"
|
||||
>
|
||||
GitHub Repository
|
||||
</Link>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Do's
|
||||
|
||||
1. **Use appropriate variants**
|
||||
```tsx
|
||||
// Internal navigation
|
||||
<Link href="/about" variant="internal">About Us</Link>
|
||||
|
||||
// External sites
|
||||
<Link href="https://github.com" variant="external" target="_blank" rel="noopener noreferrer">
|
||||
GitHub
|
||||
</Link>
|
||||
|
||||
// Within paragraphs
|
||||
<p>Learn about <Link href="/xrp" variant="inline">XRP</Link> today.</p>
|
||||
```
|
||||
|
||||
2. **Match size to context**
|
||||
```tsx
|
||||
// Navigation/CTA - use large
|
||||
<Link href="/get-started" size="large">Get Started</Link>
|
||||
|
||||
// Body content - use medium
|
||||
<Link href="/docs" size="medium">Documentation</Link>
|
||||
|
||||
// Footnotes/captions - use small
|
||||
<Link href="/terms" size="small">Terms of Service</Link>
|
||||
```
|
||||
|
||||
3. **Always use security attributes for external links**
|
||||
```tsx
|
||||
<Link
|
||||
href="https://external-site.com"
|
||||
variant="external"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
External Site
|
||||
</Link>
|
||||
```
|
||||
|
||||
### Don'ts
|
||||
|
||||
1. **Don't use disabled for navigation prevention** - Use proper routing instead
|
||||
```tsx
|
||||
// Bad - Using disabled for auth gate
|
||||
<Link href="/dashboard" disabled={!isAuthenticated}>Dashboard</Link>
|
||||
|
||||
// Good - Handle in onClick or router
|
||||
<Link href="/dashboard" onClick={handleAuthCheck}>Dashboard</Link>
|
||||
```
|
||||
|
||||
2. **Don't mix variants inappropriately**
|
||||
```tsx
|
||||
// Bad - External link with internal variant
|
||||
<Link href="https://example.com" variant="internal">External Site</Link>
|
||||
|
||||
// Good
|
||||
<Link href="https://example.com" variant="external">External Site</Link>
|
||||
```
|
||||
|
||||
3. **Don't use inline variant for standalone links**
|
||||
```tsx
|
||||
// Bad - Standalone inline link
|
||||
<Link href="/docs" variant="inline">Documentation</Link>
|
||||
|
||||
// Good - Use internal for standalone
|
||||
<Link href="/docs" variant="internal">Documentation</Link>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Navigation Menu
|
||||
|
||||
```tsx
|
||||
<nav className="d-flex flex-column gap-3">
|
||||
<Link href="/docs" size="medium">Documentation</Link>
|
||||
<Link href="/tutorials" size="medium">Tutorials</Link>
|
||||
<Link href="/api" size="medium">API Reference</Link>
|
||||
<Link
|
||||
href="https://github.com/XRPLF"
|
||||
variant="external"
|
||||
size="medium"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</Link>
|
||||
</nav>
|
||||
```
|
||||
|
||||
### Call-to-Action Section
|
||||
|
||||
```tsx
|
||||
<div className="d-flex flex-column gap-4">
|
||||
<Link href="/get-started" size="large">
|
||||
Get Started with XRPL
|
||||
</Link>
|
||||
<Link href="/use-cases" size="large">
|
||||
Explore Use Cases
|
||||
</Link>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Rich Text Content
|
||||
|
||||
```tsx
|
||||
<article>
|
||||
<p>
|
||||
The XRP Ledger (XRPL) is a decentralized, public blockchain led by a{" "}
|
||||
<Link href="/community" variant="inline">global community</Link>{" "}
|
||||
of businesses and developers. It supports a wide variety of{" "}
|
||||
<Link href="/use-cases" variant="inline">use cases</Link>{" "}
|
||||
including payments, tokenization, and DeFi.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To learn more, check out the{" "}
|
||||
<Link href="/docs" variant="inline">official documentation</Link>{" "}
|
||||
or visit the{" "}
|
||||
<Link
|
||||
href="https://github.com/XRPLF"
|
||||
variant="inline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub repository
|
||||
</Link>.
|
||||
</p>
|
||||
</article>
|
||||
```
|
||||
|
||||
### Disabled State
|
||||
|
||||
```tsx
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<Link href="/premium" size="medium" disabled>
|
||||
Premium Features (Coming Soon)
|
||||
</Link>
|
||||
<span className="text-muted">This feature is not yet available.</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
168
shared/components/Link/Link.tsx
Normal file
168
shared/components/Link/Link.tsx
Normal file
@@ -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<HTMLAnchorElement> {
|
||||
/**
|
||||
* 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)
|
||||
* <Link href="/docs" size="medium">
|
||||
* View documentation
|
||||
* </Link>
|
||||
*
|
||||
* // External link
|
||||
* <Link href="https://example.com" variant="external" size="large">
|
||||
* External resource
|
||||
* </Link>
|
||||
*
|
||||
* // Disabled link
|
||||
* <Link href="#" disabled>
|
||||
* Coming soon
|
||||
* </Link>
|
||||
*
|
||||
* // Inline link (no icon)
|
||||
* <Link href="/docs" variant="inline">
|
||||
* Learn more
|
||||
* </Link>
|
||||
* ```
|
||||
*/
|
||||
export const Link: React.FC<LinkProps> = ({
|
||||
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<HTMLAnchorElement>) => {
|
||||
if (disabled) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
onClick?.(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<a
|
||||
href={disabled ? '#' : href}
|
||||
className={classes}
|
||||
onClick={handleClick}
|
||||
aria-disabled={disabled}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
{shouldShowIcon && (
|
||||
<LinkArrow
|
||||
variant={iconType as LinkArrowVariant}
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
Link.displayName = 'Link';
|
||||
165
shared/components/Link/LinkArrow.tsx
Normal file
165
shared/components/Link/LinkArrow.tsx
Normal file
@@ -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<SVGSVGElement> {
|
||||
/**
|
||||
* 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<LinkArrowSize, { width: number; height: number }> = {
|
||||
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<LinkArrowSize, { width: number; height: number }> = {
|
||||
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
|
||||
* <LinkArrow variant="internal" size="medium" />
|
||||
* <LinkArrow variant="external" size="large" color="#0DAA3E" />
|
||||
* <LinkArrow variant="internal" size="small" disabled />
|
||||
* ```
|
||||
*/
|
||||
export const LinkArrow: React.FC<LinkArrowProps> = ({
|
||||
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 = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
viewBox="0 0 26 22"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
{...svgProps}
|
||||
>
|
||||
{/* Chevron part (static) */}
|
||||
<path
|
||||
d="M14.0019 1.00191L24.0015 11.0015L14.0019 21.001"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
{/* Horizontal line (animates away on hover) */}
|
||||
<path
|
||||
d="M23.999 10.999H0"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeMiterlimit="10"
|
||||
strokeLinecap="round"
|
||||
className="arrow-horizontal"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// External arrow (↗) - diagonal arrow with corner bracket
|
||||
// Diagonal line animates away on hover, leaving just the chevron bracket
|
||||
const renderExternalArrow = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
viewBox="0 0 21 21"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
{...svgProps}
|
||||
>
|
||||
{/* Corner bracket - horizontal line (static) */}
|
||||
<path
|
||||
d="M4.0031 2L19 2"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
{/* Corner bracket - vertical line (static) */}
|
||||
<path
|
||||
d="M19 2V17"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
{/* Diagonal arrow line (animates away on hover) */}
|
||||
<path
|
||||
d="M18.9963 2L1 20"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeMiterlimit="10"
|
||||
className="arrow-horizontal"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
return (
|
||||
<span className={classes}>
|
||||
{variant === 'external' ? renderExternalArrow() : renderInternalArrow()}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
LinkArrow.displayName = 'LinkArrow';
|
||||
124
shared/components/Link/_link-icons.scss
Normal file
124
shared/components/Link/_link-icons.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
243
shared/components/Link/_link.scss
Normal file
243
shared/components/Link/_link.scss
Normal file
@@ -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
|
||||
5
shared/components/Link/index.ts
Normal file
5
shared/components/Link/index.ts
Normal file
@@ -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';
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user