mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2026-02-09 00:12:26 +00:00
Compare commits
1 Commits
pattern/ti
...
section/ic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1aa2092bb0 |
@@ -124,9 +124,9 @@ XRP Ledgerは、スパム対策として、需要に基づいて[トランザク
|
||||
|
||||
XRP Ledgerネットワークはオープンネットワークであり、すべての取引はオープンに公開されています。
|
||||
|
||||
Rippleは XRP Ledgerネットワーク全体のAMLフラグを監視・報告し、該当する疑わしい活動をFinCENに報告することにコミットしています。
|
||||
Rippleは Ledgerネットワーク全体のAMLフラグを監視・報告し、該当する疑わしい活動をFinCENに報告することにコミットしています。
|
||||
|
||||
[XRP Forensics / xrplorer](https://xrplorer.com/)は、XRP Ledgerのマネーロンダリング、詐欺、不正使用を追跡し、最小限に抑えるための勧告リストを維持しています。取引所やその他のサービス・プロバイダは、金融犯罪を防止し対応するためにこのサービスを利用することができます。
|
||||
[XRP Forensics / xrplorer](https://xrplorer.com/)は、XRP Ledgerのマネーロンダリング、詐欺、詐欺、不正使用を追跡し、最小限に抑えるための勧告リストを維持しています。取引所やその他のサービス・プロバイダは、金融犯罪を防止し対応するためにこのサービスを利用することができます。
|
||||
|
||||
|
||||
## セキュリティ上の懸念
|
||||
|
||||
@@ -26,14 +26,14 @@ labels:
|
||||
- `RippleState`
|
||||
- `Check`
|
||||
- アカウントがレジャー内に所有するオブジェクトが1000個未満であること。
|
||||
- トランザクションの送信時、少なくとも1つ分の[所有者準備金](reserves.md)(現在{% $env.PUBLIC_OWNER_RESERVE %})に相当する特別な[トランザクションコスト][]を支払う必要があります。
|
||||
- トランザクションの送信時、少なくとも1つ分の[所有者準備金](reserves.md)(現在2XRP)に相当する特別な[トランザクションコスト][]を支払う必要があります。
|
||||
|
||||
|
||||
## 削除コスト
|
||||
|
||||
{% admonition type="warning" name="注意" %}アカウントの削除要件を満たしていないためにトランザクションが失敗した場合でも、[AccountDeleteトランザクション][]のトランザクションコストは、トランザクションが検証済みレジャーに含まれる場合常に発生します。アカウントを削除できなかった場合に高いトランザクションコストを支払う可能性を減らすには、AccountDeleteトランザクションを送信するときに`fail_hard`オプションを使用してください。{% /admonition %}
|
||||
|
||||
ビットコインや他の多くの暗号通貨とは異なり、XRP Ledgerの公開レジャーチェーンのそれぞれの新しいレジャーバージョンは、レジャーの完全な状態を含んでおり、新しいアカウントが増えるごとにサイズが増加します。そのため、必要な場合を除き、新しいXRP Ledgerアカウントを作成すべきではありません。アカウントを削除することで、アカウントの{% $env.PUBLIC_BASE_RESERVE %}の[準備金](reserves.md)の一部を回復することができますが、そのためには少なくとも{% $env.PUBLIC_OWNER_RESERVE %}を破棄する必要があります。
|
||||
ビットコインや他の多くの暗号通貨とは異なり、XRP Ledgerの公開レジャーチェーンのそれぞれの新しいレジャーバージョンは、レジャーの完全な状態を含んでおり、新しいアカウントが増えるごとにサイズが増加します。そのため、必要な場合を除き、新しいXRP Ledgerアカウントを作成すべきではありません。アカウントを削除することで、アカウントの10XRPの[準備金](reserves.md)の一部を回復することができますが、そのためには少なくとも2XRPを破棄する必要があります。
|
||||
|
||||
取引所など、多くのユーザのために価値の送受信を行う組織は、[**送信元タグ**と**宛先タグ**](../transactions/source-and-destination-tags.md)を使用することで、XRP Ledgerのアカウントを1つだけ(または少数)使用するだけで、ユーザの支払いを区別することができます。
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ Deposit Authorizationが有効化されているアカウントの特徴は次
|
||||
|
||||
- [Paymentトランザクション][]の送信先には**できません**。ただし**以下の例外**は除きます。
|
||||
- 送金先により、支払の送金元が[事前承認](#事前承認)されている場合。{% amendment-disclaimer name="DepositPreauth" /%}
|
||||
- アカウントのXRP残高がアカウントの最低[必要準備金](reserves.md)以下で、XRP PaymentのAmountがアカウントの最低準備金(現時点では{% $env.PUBLIC_BASE_RESERVE %})以下である場合は、このアカウントを送金先に指定できます。これにより、アカウントがトランザクションを送信することも、XRPを受領することもできずに操作不可能な状態になるのを防ぎます。この場合、アカウントの所有者の準備金は関係ありません。
|
||||
- アカウントのXRP残高がアカウントの最低[必要準備金](reserves.md)以下で、XRP PaymentのAmountがアカウントの最低準備金(現時点では10XRP)以下である場合は、このアカウントを送金先に指定できます。これにより、アカウントがトランザクションを送信することも、XRPを受領することもできずに操作不可能な状態になるのを防ぎます。この場合、アカウントの所有者の準備金は関係ありません。
|
||||
- **以下に該当する場合にのみ**[PaymentChannelClaimトランザクション][]からXRPを受領できます。
|
||||
- PaymentChannelClaimトランザクションの送金元がPayment Channelの送金先である場合。
|
||||
- PaymentChannelClaimトランザクションの送金先がPaymentChannelClaimの送金元を[事前承認している](#事前承認)場合。{% amendment-disclaimer name="DepositPreauth" /%}
|
||||
|
||||
@@ -46,7 +46,7 @@ XRP Ledgerでアカウントを取得する一般的な方法は次のとおり
|
||||
|
||||
- 例えば、一般的な取引所でXRPを購入し、その取引所から、指定したアドレスにXRPを出金することができます。
|
||||
|
||||
{% admonition type="warning" name="注意" %}自身のXRP Ledgerアドレスで初めてXRPを受け取る場合は[アカウントの準備金](reserves.md)(現在は{% $env.PUBLIC_BASE_RESERVE %})を支払う必要があります。この金額のXRPは無期限に使用できなくなります。一方で、一般的な取引所では通常、顧客のXRPはすべて、共有されたいくつかのXRP Ledgerアカウントに保有されているため、顧客はその取引所で個々のアカウントの準備金を支払う必要はありません。引き出す前に、XRP Ledgerに直接アカウントを保有することが、金額に見合う価値があるかどうかを検討してください。{% /admonition %}
|
||||
{% admonition type="warning" name="注意" %}自身のXRP Ledgerアドレスで初めてXRPを受け取る場合は[アカウントの準備金](reserves.md)(現在は10XRP)を支払う必要があります。この金額のXRPは無期限に使用できなくなります。一方で、一般的な取引所では通常、顧客のXRPはすべて、共有されたいくつかのXRP Ledgerアカウントに保有されているため、顧客はその取引所で個々のアカウントの準備金を支払う必要はありません。引き出す前に、XRP Ledgerに直接アカウントを保有することが、金額に見合う価値があるかどうかを検討してください。{% /admonition %}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ XRP Ledgerのチケットは、取引をすぐに送信せずに、その取引
|
||||
|
||||
上記の例では、シーケンス番号105または作成した3つのチケットのいずれかを使用してトランザクションを送信できます。チケット103を使ってトランザクションを送信すると、それによってチケット103は元帳から削除されます。その後の次のトランザクションでは、シーケンス番号105、チケット102、またはチケット104を使用できます。
|
||||
|
||||
{% admonition type="warning" name="注意" %}チケットは1枚ごとに[所有者準備金](reserves.md#所有者準備金)としてカウントされますので、チケット1枚につき{% $env.PUBLIC_OWNER_RESERVE %}を確保する必要があります。 (このXRPは、チケットを使用した後に再び使用可能になります)一度に多くのチケットを作成すると、このコストはすぐに膨れ上がります。{% /admonition %}
|
||||
{% admonition type="warning" name="注意" %}チケットは1枚ごとに[所有者準備金](reserves.md#所有者準備金)としてカウントされますので、チケット1枚につき2XRPを確保する必要があります。 (このXRPは、チケットを使用した後に再び使用可能になります)一度に多くのチケットを作成すると、このコストはすぐに膨れ上がります。{% /admonition %}
|
||||
|
||||
シーケンス番号と同様に、トランザクションの送信は、そのトランザクションが[コンセンサス](../consensus-protocol/index.md)によって確認された場合にのみ、チケットを使用します。しかし、意図した通りにならなかった取引でも、[`tec`クラスの結果コード](../../references/protocol/transactions/transaction-results/tec-codes.md)を用いてコンセンサスで確認することができます。
|
||||
|
||||
@@ -51,7 +51,7 @@ XRP Ledgerのチケットは、取引をすぐに送信せずに、その取引
|
||||
- 各チケットは一度しか使用できません。同じチケットシーケンスを使用する複数の異なるトランザクション候補があることは可能ですが、コンセンサスで検証できるのはそのうちの1つだけです。
|
||||
- 各アカウントでは、一度に250枚以上のチケットをレジャーに登録することはできません。また、一度に250枚以上のチケットを作成することもできません。
|
||||
- チケットを使って別のチケットを作ることは _できます_。その場合、使用したチケットは、一度に所持できるチケットの合計数にはカウントされません。
|
||||
- 各チケットは[所有者準備金](reserves.md#所有者準備金)にカウントされるため、まだ使用していないチケット1枚につき{% $env.PUBLIC_OWNER_RESERVE %}を確保する必要があります。このXRPは、チケットを使用した後、再び使用することができます。
|
||||
- 各チケットは[所有者準備金](reserves.md#所有者準備金)にカウントされるため、まだ使用していないチケット1枚につき2XRPを確保する必要があります。このXRPは、チケットを使用した後、再び使用することができます。
|
||||
- 個々の元帳の中では、チケットを使用した取引は、同じ送信者からの他の取引の後に実行されます。1つのアカウントが同じ元帳のバージョンでTicketを使用する複数のトランザクションを持つ場合、それらのTicketは最も低いTicket Sequenceから最も高いTicket Sequenceの順に実行されます。 (詳細については、コンセンサスの[正規順序](../consensus-protocol/consensus-structure.md#xrp-ledgerプロトコル-コンセンサスと検証)に関するドキュメントをご覧ください)。
|
||||
- 個々の元帳の中では、チケットを使用した取引は、同じ送信者からの他の取引の後に実行されます。1つのアカウントが同じ元帳のバージョンでチケットを使用する複数のトランザクションを持つ場合、それらのチケットは最も低いチケット シーケンス番号から最も高いチケット シーケンス番号の順に実行されます。 (詳細については、コンセンサスの[正規順序](../consensus-protocol/consensus-structure.md#xrp-ledgerプロトコル-コンセンサスと検証)に関するドキュメントをご覧ください)。
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
html: consensus-protections.html
|
||||
parent: consensus.html
|
||||
seo:
|
||||
description: 分散型金融システムで発生する可能性のあるさまざまな問題や攻撃から、XRP Ledgerコンセンサスプロトコルがどのように保護されているかを学びます。
|
||||
description: Learn how the XRP Ledger Consensus Protocol is protected against various problems and attacks that may occur in a decentralized financial system. #TODO: translate
|
||||
labels:
|
||||
- ブロックチェーン
|
||||
---
|
||||
|
||||
@@ -12,11 +12,11 @@ NFTをミントし、保有し、販売するためには、XRPを準備金と
|
||||
|
||||
## 基本準備金
|
||||
|
||||
アカウントでは、基本準備金(現在{% $env.PUBLIC_BASE_RESERVE %})を用意する必要があります。基本準備金のXRPの金額は変更される可能性があります。[基本準備金と所有者準備金](../../accounts/reserves.md#基本準備金と所有者準備金)をご覧ください。
|
||||
アカウントでは、基本準備金(現在10XRP)を用意する必要があります。基本準備金のXRPの金額は変更される可能性があります。[基本準備金と所有者準備金](../../accounts/reserves.md#基本準備金と所有者準備金)をご覧ください。
|
||||
|
||||
## 所有者準備金
|
||||
|
||||
XRP Ledgerで所有する各オブジェクトには、現在{% $env.PUBLIC_OWNER_RESERVE %}の所有者準備金が必要とされています。これは、ユーザが不必要なデータで台帳にスパムをかけることを抑制し、不要になったデータを削除することを促すためのものです。所有者準備金の額は変更される可能性があります。[基本準備金と所有者準備金](../../accounts/reserves.md#基本準備金と所有者準備金)をご覧ください。
|
||||
XRP Ledgerで所有する各オブジェクトには、現在2XRPの所有者準備金が必要とされています。これは、ユーザが不必要なデータで台帳にスパムをかけることを抑制し、不要になったデータを削除することを促すためのものです。所有者準備金の額は変更される可能性があります。[基本準備金と所有者準備金](../../accounts/reserves.md#基本準備金と所有者準備金)をご覧ください。
|
||||
|
||||
NFTの場合、 _オブジェクト_ はそれぞれのNFTを指すのではなく、アカウントが所有する`NFTokenPage`オブジェクトを指します。`NFTokenPage`オブジェクトは最大32個のNFTを格納することができます。
|
||||
|
||||
@@ -38,7 +38,7 @@ NFTの保有枚数や保有ページ数によって、所有者準備金の総
|
||||
|
||||
## `NFTokenOffer`の準備金
|
||||
|
||||
各`NFTokenOffer`オブジェクトは、オファーを出すアカウントに対して準備金の1つの増加を必要とします。この記事の執筆時点では、準備金の増分は{% $env.PUBLIC_OWNER_RESERVE %}です。準備金は、オファーをキャンセルすることで取り戻すことができます。また、オファーが受け入れられると、XRP Ledgerからオファーが削除され、準備金は取り戻されます。
|
||||
各`NFTokenOffer`オブジェクトは、オファーを出すアカウントに対して準備金の1つの増加を必要とします。この記事の執筆時点では、準備金の増分は2XRPです。準備金は、オファーをキャンセルすることで取り戻すことができます。また、オファーが受け入れられると、XRP Ledgerからオファーが削除され、準備金は取り戻されます。
|
||||
|
||||
## Practical Considerations
|
||||
|
||||
@@ -55,7 +55,7 @@ NFTをミントし、保有し、売買のオファーをする場合、必要
|
||||
|
||||
{% admonition type="info" name="注記" %}準備金要件ではありませんが、ミントと売却のプロセスにおけるトランザクションの些細な手数料(通常12drops、または.000012XRP)を負担するために、少なくとも必要準備金より1XRPより多く用意しておきくべきです。{% /admonition %}
|
||||
|
||||
仮に200個のNFTをミントし、それぞれに「NFTokenSellOffer」を作成すると、43.6XRPもの準備金が必要になります。
|
||||
仮に200個のNFTをミントし、それぞれに「NFTokenSellOffer」を作成すると、436XRPもの準備金が必要になります。
|
||||
|
||||
| 準備金の種類 | 準備金の額 |
|
||||
|:--------------------|--------:|
|
||||
|
||||
@@ -72,7 +72,7 @@ labels:
|
||||
|
||||
### `NFTokenOffer`の準備金
|
||||
|
||||
各`NFTokenOffer`オブジェクトは、オファーを出すアカウントに1つ分の準備金の増額を要求します。執筆時点では、準備金の増分は{% $env.PUBLIC_OWNER_RESERVE %}です。この準備金は、オファーをキャンセルすることで取り戻すことができます。
|
||||
各`NFTokenOffer`オブジェクトは、オファーを出すアカウントに1つ分の準備金の増額を要求します。執筆時点では、準備金の増分は2XRPです。この準備金は、オファーをキャンセルすることで取り戻すことができます。
|
||||
|
||||
|
||||
### `NFTokenOfferID`のフォーマット
|
||||
|
||||
@@ -48,7 +48,7 @@ AMMを表す[AMMエントリ][]と[特殊なAccountRootエントリ](../../ledge
|
||||
|
||||
## 特殊なトランザクションコスト
|
||||
|
||||
各AMMインスタンスはAccountRootレジャーエントリ、AMMレジャーエントリ、プール内の各トークンのトラストラインを含むため、AMMCreateトランザクションは台帳スパムを抑止するために通常よりもはるかに高い[トランザクションコスト][]を必要とします。標準的な最低0.00001XRPの代わりに、AMMCreateは少なくとも所有者準備金の増分(現在は{% $env.PUBLIC_OWNER_RESERVE %})を破棄しなければなりません。これは[AccountDeleteトランザクション][]と同じ特別なトランザクションコストです。
|
||||
各AMMインスタンスはAccountRootレジャーエントリ、AMMレジャーエントリ、プール内の各トークンのトラストラインを含むため、AMMCreateトランザクションは台帳スパムを抑止するために通常よりもはるかに高い[トランザクションコスト][]を必要とします。標準的な最低0.00001XRPの代わりに、AMMCreateは少なくとも所有者準備金の増分(現在は2XRP)を破棄しなければなりません。これは[AccountDeleteトランザクション][]と同じ特別なトランザクションコストです。
|
||||
|
||||
## エラーケース
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ labels:
|
||||
|
||||
- トランザクションを送信するための十分なXRPが供給されていて、新しい署名者リストの[必要準備金](../../../concepts/accounts/reserves.md)を満たしている資金供給のあるXRP Ledger[アドレス](../../../concepts/accounts/index.md)が必要です。
|
||||
|
||||
- [MultiSignReserve Amendment][]が有効な場合、マルチシグを使用するには、使用する署名と署名者の数に関わらず、アカウントの準備金として{% $env.PUBLIC_OWNER_RESERVE %}が必要です。(MultiSignReserve Amendmentは**2019年4月7日**以降、本番環境のXRP Ledgerで有効になっています。)
|
||||
- [MultiSignReserve Amendment][]が有効な場合、マルチシグを使用するには、使用する署名と署名者の数に関わらず、アカウントの準備金として2 XRPが必要です。(MultiSignReserve Amendmentは**2019年4月7日**以降、本番環境のXRP Ledgerで有効になっています。)
|
||||
|
||||
- [MultiSignReserve Amendment][]が有効ではないテストネットワークでは、マルチシグを使用するには[アカウント準備金](../../../concepts/accounts/reserves.md)に通常よりも多くのXRPが必要となります。必要額は、リストの署名者の数に応じて増加します。
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ XRP Ledgerの分散型取引所(DEX)には、「アルゴリズムトレード
|
||||
|
||||
裁定取引を行うには、XRP Ledgerの内部と関連する部分の両方で、多くの方法があります。以下の例は潜在的な戦略を説明するためのものですが、他の方法も可能です。
|
||||
|
||||
**循環支払い**を利用して、マルチアセットトレードを完了し利益を得ることができます。XRP Ledgerは、XRPが真ん中のアセットである3つのアセットセットと同様に、アセットペア間の重複した取引を自動的に接続します。しかし、XRP Ledgerのプロトコルは、他のより長い、あるいはより複雑な経路のトレードを自動的に見つけて競うことはしません。(可能な限り最善の経路を見つけることは、計算集約型な問題のカテゴリとして知られています。)したがって、自分で経路探索(PathFinding)を行えば、このような有益な裁定取引の機会を見つけることが可能です。その場合は、[Paymentトランザクション](../../references/protocol/transactions/types/payment.md)でそれらの[経路(Paths)](../../concepts/tokens/fungible-tokens/paths.md)を明示的に指定できます。1つのFOOを使って2つのBARを買い、その2つのBARを使って3つのTSTを買い、最後に3つのTSTを使って1.1 FOOを買えば、0.1 FOOから取引に関わるトークンの[送金手数料](../../concepts/tokens/fungible-tokens/transfer-fees.md)などのコストを差し引いた利益を得ることができます。
|
||||
**循環支払い**を利用して、マルチアセットトレードを完了し利益を得ることができます。XRP Ledgerは、XRPが真ん中のアセットである3つのアセットセットセットと同様に、アセットペア間の重複した取引を自動的に接続します。しかし、XRP Ledgerのプロトコルは、他のより長い、あるいはより複雑な経路のトレードを自動的に見つけて競うことはしません。(可能な限り最善の経路を見つけることは、計算集約型な問題のカテゴリとして知られています。)したがって、XRP Ledgerが独自の経路を見つける場合、XRP Ledgerのプロトコルは自動的に他の、より長い、あるいはより複雑な経路の取引を見つけ、競争させることはありません。したがって、自分で経路探索(PathFinding)を行えば、このような有益な裁定取引の機会を見つけることが可能です。その場合は、[Paymentトランザクション](../../references/protocol/transactions/types/payment.md)でそれらの[経路(Paths)](../../concepts/tokens/fungible-tokens/paths.md)を明示的に指定できます。1つのFOOを使って2つのBARを買い、その2つのBARを使って3つのTSTを買い、最後に3つのTSTを使って1.1 FOOを買えば、0.1 FOOから取引に関わるトークンの[送金手数料](../../concepts/tokens/fungible-tokens/transfer-fees.md)などのコストを差し引いた利益を得ることができます。
|
||||
|
||||
資産の価格が異なる複数の取引所(CEX)に口座を持っている場合、**取引所間の裁定取引**を行うことができます。例えば、ACME取引所でXRPを1XRPあたり0.45ドルで購入し、そのXRPをWayGate取引所に移動して1XRPあたり0.50ドルで売却した場合、XRPあたり0.05ドルの利益を得ることができます。より複雑な例として、ACME取引所でBTC:ETHの価格が変動し、BTCに対してETHが安くなった場合、ある取引所でETH→XRPを売却し、そのXRPをACME取引所に移動し、XRP→BTC→ETHを取引して利益を得ることで、この価格変動を利用できる可能性があります。XRP Ledgerの取引は数秒で決済されますが、イーサリアムの取引は数分、ビットコインの取引は数時間かかることがあるため、XRPをブリッジ通貨として使用することで、ACME取引所でETH→BTC→BTC→ETHと取引するよりも早くこの機会を利用できる可能性があります。(これはもちろん、XRPへの交換が利益以上のコストにならないだけの十分な流動性と狭いスプレッドがある場合にのみ機能します)
|
||||
|
||||
@@ -48,7 +48,7 @@ XRP Ledgerの分散型取引所(DEX)には、「アルゴリズムトレード
|
||||
|
||||
## テストとよくある間違い
|
||||
|
||||
どのような取引でもそうですが、アルゴリズムトレードは確実に儲かる方法ではありません。手作業によるトレードと比べると、アルゴリズムトレードはエラーの余地が非常に少なくなります。小さなミスを犯しても、そのミスを大量のトレードで倍増させようとすれば、問題を修正する前に損失があっという間に膨らんでしまいます。したがって、自分のトレード戦略が実際に利益を上げるかどうかを確認するために、さまざまなテストを行うのが賢明です。戦略やその実際の実装(よく _ボット_ と呼ばれます)をテストするために、次のようなことを行うことができます。
|
||||
どのような取引でもそうですが、アルゴリズムトレー ドは確実に儲かる方法ではありません。手作業によるトレードと比べると、アルゴリズムトレードはエラーの余地が非常に少なくなります。小さなミスを犯しても、そのミスを大量のトレードで倍増させようとすれば、問題を修正する前に損失があっという間に膨らんでしまいます。したがって、自分のトレード戦略が実際に利益を上げるかどうかを確認するために、さまざまなテストを行うのが賢明です。戦略やその実際の実装(よく _ボット_ と呼ばれます)をテストするために、次のようなことを行うことができます。
|
||||
|
||||
- 現在のレジャーの状態または過去のトレードに基づいて、潜在的な利益を手動で計算します。
|
||||
- 過去のデータを記録してボットに送り、ボットがどのようなアクションを取ったかを記録し、実際の過去の値動きと結果を比較します。
|
||||
@@ -61,7 +61,7 @@ XRP Ledgerの分散型取引所(DEX)には、「アルゴリズムトレード
|
||||
- 通常、四捨五入の違いや、計算時と約定時の値動きの違いを考慮し、金額を調整する必要があります。この金額は「スリッページ」と呼ばれ、適切な金額を設定することが重要です。スリッページが低すぎると、トレードがまったく約定しない可能性があります。一方、スリッページが高すぎると、フロントランニングの影響を受けやすくなり、スリッページが高ければ高いほど、値動きによって利益が削られる可能性が高くなります。
|
||||
- **余分なコストと遅延を考慮しないこと**: 例えば、2つのステーブルコインの裏付けが米ドルであるにもかかわらず、ある発行者が0.5%の送金手数料を請求し、別の発行者が0.25%の[送金手数料](../../concepts/tokens/fungible-tokens/transfer-fees.md)を請求した場合、そのステーブルコインの取引価格には約0.25%の差が生じます。トランザクションを送信するためのコストは、通常は少額ですが、その他の潜在的な遅延の影響も忘れないでください。例えば、オフレジャーの取引所が現時点で有利な価格を示していたとしても、その取引所の入金処理に数時間から数日かかる場合、その取引所で事前に流動性を持っていない限り、その価格を利用することはできません。
|
||||
- **稀な事象を考慮していないこと**: 前例のない出来事(「ブラック・スワン」)はさておき、個々の異常値によって計算結果がゆがむことがあります。一例として(これは実話ですが)、あるトレーダーが、ある戦略の潜在的な利益を特定の時間帯で計算したところ、利益の80%以上が、他のユーザが誤って価格にゼロを追加してしまった1つの「入力ミス」の取引によるものであったと報じました。同じ戦略を、これらの異常値の取引を含まない時間範囲に対して計算すると、利益ははるかに少なくなりました。
|
||||
- **トランザクションのフラグを確認しないこと**: XRP Ledgerのトランザクションのフラグは、そのトランザクションの処理方法や、プロトコルがそれを「成功」とマークするタイミングに大きな影響を与える可能性があります。例えば、"Offer"トランザクションのフラグは、全額がすぐに得られる場合にのみトレードされる"Fill or Kill"注文にすることができます。"Payment"トランザクションのフラグは、意図した宛先に全額を届けることができなくても成功する[partial payments](../../concepts/payment-types/partial-payments.md)にすることができます。トランザクションの`Flags`フィールドを解析するためにビット演算をする必要がありますが、それをスキップしてしまうと、予想と結果が全く異なったものとなってしまう可能性があります。
|
||||
- **トランザクションのフラグを確認しないこ**と: XRP Ledgerのトランザクションのフラグは、そのトランザクションの処理方法や、プロトコルがそれを「成功」とマークするタイミングに大きな影響を与える可能性があります。例えば、"Offer"トランザクションのフラグは、全額がすぐに得られる場合にのみトレードされる"Fill or Kill"注文にすることができます。"Payment"トランザクションのフラグは、意図した宛先に全額を届けることができなくても成功する[partial payments](../../concepts/payment-types/partial-payments.md)にすることができます。トランザクションの`Flags`フィールドを解析するためにビット演算をする必要がありますが、それをスキップしてしまうと、予想と結果が全く異なったものとなってしまう可能性があります。
|
||||
|
||||
## 税金とライセンス
|
||||
|
||||
@@ -72,7 +72,7 @@ XRP Ledgerの分散型取引所(DEX)には、「アルゴリズムトレード
|
||||
|
||||
### トレードの発注
|
||||
|
||||
XRP Ledgerの分散型取引所で _代替可能_ トークンとXRPを売買するには、通常[OfferCreateトランザクション](../../references/protocol/transactions/types/offercreate.md)を送信します。この方法でトレードを行うためのコードと技術的ステップの詳細なウォークスルーについては、[分散型取引所でのトレード](../../tutorials/how-tos/use-tokens/trade-in-the-decentralized-exchange.md)をご覧ください。[Paymentトランザクション](../../references/protocol/transactions/types/payment.md)を使用して通貨を両替することも可能です。[クロスカレンシー支払い](../../concepts/payment-types/cross-currency-payments.md)を他のユーザに送ったり、長い[パス](../../concepts/tokens/fungible-tokens/paths.md)を使って裁定取引の機会を1つの操作にまとめることで、自分自身に送り返すこともできます。
|
||||
XRP Ledgerの分散型取引所で _代替可能_ トークンとXRPを売買するには、通常[OfferCreateトランザクション](../../references/protocol/transactions/types/offercreate.md)を送信します。この方法でトレードを行うためのコードと技術的ステップの詳細なウォークスルーについては、[分散型取引所でのトレード](../../tutorials/how-tos/use-tokens/trade-in-the-decentralized-exchange.md)をご覧ください。[Paymentトランザクション](../../references/protocol/transactions/types/payment.md)を使用して通貨を両替することも可能です。[クロスカレンしー支払い](../../concepts/payment-types/cross-currency-payments.md)を他のユーザに送ったり、長い[パス](../../concepts/tokens/fungible-tokens/paths.md)を使って裁定取引の機会を1つの操作にまとめることで、自分自身に送り返すこともできます。
|
||||
|
||||
NFTをトレードするためのコードと技術的な手順については、[JavaScriptを使用したNFTokenの送信](../../tutorials/javascript/nfts/transfer-nfts.md)をご覧ください。
|
||||
|
||||
|
||||
@@ -48,9 +48,9 @@ NFTをオークション形式で販売することができます。[NFTオー
|
||||
|
||||
### 準備金要件
|
||||
|
||||
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。NFTokenページは16~32個のNFTを保管することができます。
|
||||
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、2XRPの準備金が必要です。NFTokenページは16~32個のNFTを保管することができます。
|
||||
|
||||
各`NFTokenOffer`オブジェクトは、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。
|
||||
各`NFTokenOffer`オブジェクトは、2XRPの準備金が必要です。
|
||||
|
||||
`NFTokenOffer`を作成したり、NFTを売却したりする際には、些細な送金手数料(およそ6000ドロップ、または0.006 XRP)が発生します。大量に販売する場合、こうした少額の手数料はすぐにかさみますので、ビジネスのコストとして考慮する必要があります。
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ NFTokenのURLは、NFTのコンテンツが保存されている場所へのリ
|
||||
|
||||
[認可Minter](../../concepts/tokens/nfts/authorizing-another-minter.md)をご覧ください。
|
||||
|
||||
ミント済みのNFTは、`NFTokenPage`に記録されます。アカウント上の`NFTokenPage`1つにつき{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。[NFT準備金](../../concepts/tokens/nfts/reserve-requirements.md)をご覧ください。
|
||||
ミント済みのNFTは、`NFTokenPage`に記録されます。アカウント上の`NFTokenPage`1つにつき2XRPの準備金が必要です。[NFT準備金](../../concepts/tokens/nfts/reserve-requirements.md)をご覧ください。
|
||||
|
||||
各「NFTokenPage」は16~32個のNFTを保持します。大量のNFTをミントすると、あなたのXRPを大量に準備金としてロックすることになります。オンデマンドミント(または _遅延ミント_ )を行うことで、XRPを柔軟に維持することができます。[遅延ミント](../../concepts/tokens/nfts/batch-minting.md#mint-on-demand-lazy-minting)と[スクリプトミント](../../concepts/tokens/nfts/batch-minting.md#scripted-minting)をご覧下さい。
|
||||
|
||||
@@ -89,9 +89,9 @@ NFTをオークション形式で販売することができます。[NFTオー
|
||||
|
||||
#### 準備金要件
|
||||
|
||||
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。NFTokenページは16~32個のNFTを保管することができます。
|
||||
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、2XRPの準備金が必要です。NFTokenページは16~32個のNFTを保管することができます。
|
||||
|
||||
各`NFTokenOffer`オブジェクトは、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。
|
||||
各`NFTokenOffer`オブジェクトは、2XRPの準備金が必要です。
|
||||
|
||||
`NFTokenOffer`を作成したり、NFTを売却したりする際には、些細な送金手数料(およそ6000ドロップ、または0.006 XRP)が発生します。大量に販売する場合、こうした少額の手数料はすぐにかさみますので、ビジネスのコストとして考慮する必要があります。
|
||||
|
||||
|
||||
@@ -44,9 +44,9 @@ NFTをオークション形式で販売することができます。[NFTオー
|
||||
|
||||
### 準備金要件
|
||||
|
||||
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。NFTokenページは16~32個のNFTを保管することができます。
|
||||
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、2XRPの準備金が必要です。NFTokenページは16~32個のNFTを保管することができます。
|
||||
|
||||
各`NFTokenOffer`オブジェクトは、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。
|
||||
各`NFTokenOffer`オブジェクトは、2XRPの準備金が必要です。
|
||||
|
||||
`NFTokenOffer`を作成したり、NFTを売却したりする際には、些細な送金手数料(およそ6000ドロップ、または0.006 XRP)が発生します。大量に販売する場合、こうした少額の手数料はすぐにかさみますので、ビジネスのコストとして考慮する必要があります。
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
---
|
||||
# リソース
|
||||
|
||||
XRP Ledgerの理解や開発ためのリソース。
|
||||
XRP Ledgerの理解や開発ためのリソース。Other resources to help understand the XRPL and develop on it.
|
||||
|
||||
|
||||
{% child-pages /%}
|
||||
|
||||
@@ -38,9 +38,11 @@ export { default as communityIcon } from "../../../../static/img/navbar/communit
|
||||
export { default as insightsIcon } from "../../../../static/img/navbar/insights.svg";
|
||||
export { default as resourcesIcon } from "../../../../static/img/navbar/resources.svg";
|
||||
|
||||
// Network submenu pattern images (used for both light and dark mode)
|
||||
export { default as resourcesIconPattern } from "../../../../static/img/navbar/resources-icon.svg";
|
||||
export { default as insightsIconPattern } from "../../../../static/img/navbar/insights-icon.svg";
|
||||
// Network submenu pattern images
|
||||
export { default as resourcesPurplePattern } from "../../../../static/img/navbar/resources-purple.svg";
|
||||
export { default as insightsGreenPattern } from "../../../../static/img/navbar/insights-green.svg";
|
||||
export { default as darkInsightsGreenPattern } from "../../../../static/img/navbar/dark-insights-green.svg";
|
||||
export { default as darkLilacPattern } from "../../../../static/img/navbar/dark-lilac.svg";
|
||||
|
||||
// Wallet icon mapping for dynamic icon lookup
|
||||
import greenWallet from "../../../../static/img/navbar/green-wallet.svg";
|
||||
|
||||
@@ -10,10 +10,10 @@ export const alertBanner = {
|
||||
|
||||
// Main navigation items
|
||||
export const navItems: NavItem[] = [
|
||||
{ label: "Develop", labelTranslationKey: "navbar.develop", href: "/develop", hasSubmenu: true },
|
||||
{ label: "Use Cases", labelTranslationKey: "navbar.usecases", href: "/use-cases", hasSubmenu: true },
|
||||
{ label: "Develop", labelTranslationKey: "navbar.develop", href: "/docs", hasSubmenu: true },
|
||||
{ label: "Use Cases", labelTranslationKey: "navbar.usecases", href: "/about/uses", hasSubmenu: true },
|
||||
{ label: "Community", labelTranslationKey: "navbar.community", href: "/community", hasSubmenu: true },
|
||||
{ label: "Network", labelTranslationKey: "navbar.network", href: "/resources", hasSubmenu: true },
|
||||
{ label: "Network", labelTranslationKey: "navbar.network", href: "/docs/concepts/networks-and-servers", hasSubmenu: true },
|
||||
];
|
||||
|
||||
// Develop submenu data structure
|
||||
@@ -22,9 +22,9 @@ export const developSubmenuData: {
|
||||
right: SubmenuItemWithChildren[];
|
||||
} = {
|
||||
left: [
|
||||
{ label: "Developer's Home", href: "/develop", icon: "dev_home" },
|
||||
{ label: "Learn", href: "https://learn.xrpl.org", icon: "learn" },
|
||||
{ label: "Code Samples", href: "/resources/code-samples", icon: "code_samples" },
|
||||
{ label: "Developer's Home", href: "/docs", icon: "dev_home" },
|
||||
{ label: "Learn", href: "/docs/tutorials", icon: "learn" },
|
||||
{ label: "Code Samples", href: "/_code-samples", icon: "code_samples" },
|
||||
],
|
||||
right: [
|
||||
{
|
||||
@@ -32,21 +32,21 @@ export const developSubmenuData: {
|
||||
href: "/docs",
|
||||
icon: "docs",
|
||||
children: [
|
||||
{ label: "API Reference", href: "/references" },
|
||||
{ label: "API Reference", href: "/docs/references" },
|
||||
{ label: "Tutorials", href: "/docs/tutorials" },
|
||||
{ label: "Concepts", href: "/concepts" },
|
||||
{ label: "Concepts", href: "/docs/concepts" },
|
||||
{ label: "Infrastructure", href: "/docs/infrastructure" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Client Libraries",
|
||||
href: "#",
|
||||
href: "/docs/references/client-libraries",
|
||||
icon: "client_lib",
|
||||
children: [
|
||||
{ label: "JavaScript", href: "#" },
|
||||
{ label: "Python", href: "#" },
|
||||
{ label: "PHP", href: "#" },
|
||||
{ label: "Go", href: "#" },
|
||||
{ label: "JavaScript", href: "/docs/references/xrpljs" },
|
||||
{ label: "Python", href: "/docs/references/xrpl-py" },
|
||||
{ label: "PHP", href: "/docs/references/xrpl-php" },
|
||||
{ label: "Go", href: "/docs/references/xrpl-go" },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -60,44 +60,44 @@ export const useCasesSubmenuData: {
|
||||
left: [
|
||||
{
|
||||
label: "Payments",
|
||||
href: "/use-cases/payments",
|
||||
href: "/about/uses/payments",
|
||||
icon: "payments",
|
||||
children: [
|
||||
{ label: "Direct XRP Payments", href: "/use-cases/payments/direct-xrp-payments" },
|
||||
{ label: "Cross-currency Payments", href: "/use-cases/payments/cross-currency-payments" },
|
||||
{ label: "Escrow", href: "/use-cases/payments/escrow" },
|
||||
{ label: "Checks", href: "/use-cases/payments/checks" },
|
||||
{ label: "Direct XRP Payments", href: "/about/uses/direct-xrp-payments" },
|
||||
{ label: "Cross-currency Payments", href: "/about/uses/cross-currency-payments" },
|
||||
{ label: "Escrow", href: "/about/uses/escrow" },
|
||||
{ label: "Checks", href: "/about/uses/checks" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Tokenization",
|
||||
href: "/use-cases/tokenization",
|
||||
href: "/about/uses/tokenization",
|
||||
icon: "tokenization",
|
||||
children: [
|
||||
{ label: "Stablecoin", href: "/use-cases/tokenization/stablecoin" },
|
||||
{ label: "NFT", href: "/use-cases/tokenization/nft" },
|
||||
{ label: "Stablecoin", href: "/about/uses/stablecoin" },
|
||||
{ label: "NFT", href: "/about/uses/nft" },
|
||||
],
|
||||
},
|
||||
],
|
||||
right: [
|
||||
{
|
||||
label: "Credit",
|
||||
href: "/use-cases/credit",
|
||||
href: "/about/uses/credit",
|
||||
icon: "credit",
|
||||
children: [
|
||||
{ label: "Lending", href: "/use-cases/credit/lending" },
|
||||
{ label: "Collateralization", href: "/use-cases/credit/collateralization" },
|
||||
{ label: "Sustainability", href: "/use-cases/credit/sustainability" },
|
||||
{ label: "Lending", href: "/about/uses/lending" },
|
||||
{ label: "Collateralization", href: "/about/uses/collateralization" },
|
||||
{ label: "Sustainability", href: "/about/uses/sustainability" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Trading",
|
||||
href: "/use-cases/trading",
|
||||
href: "/about/uses/trading",
|
||||
icon: "trading",
|
||||
children: [
|
||||
{ label: "DEX", href: "/use-cases/trading/dex" },
|
||||
{ label: "Permissioned Trading", href: "/use-cases/trading/permissioned-trading" },
|
||||
{ label: "AMM", href: "/use-cases/trading/amm" },
|
||||
{ label: "DEX", href: "/about/uses/dex" },
|
||||
{ label: "Permissioned Trading", href: "/about/uses/permissioned-trading" },
|
||||
{ label: "AMM", href: "/about/uses/amm" },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -115,7 +115,10 @@ export const communitySubmenuData: {
|
||||
icon: "community",
|
||||
children: [
|
||||
{ label: "Events", href: "/community/events" },
|
||||
{ label: "News", href: "/blog", active: true },
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{ label: "Marketplace", href: "/community/marketplace" },
|
||||
{ label: "Partner Connect", href: "/community/partner-connect" },
|
||||
],
|
||||
},
|
||||
{ label: "Funding", href: "/community/developer-funding", icon: "code_samples" },
|
||||
@@ -123,14 +126,15 @@ export const communitySubmenuData: {
|
||||
right: [
|
||||
{
|
||||
label: "Contribute",
|
||||
href: "/community/contribute",
|
||||
href: "/resources/contribute-documentation",
|
||||
icon: "client_lib",
|
||||
children: [
|
||||
{ label: "Bug Bounty", href: "/blog/2020/rippled-1.5.0#bug-bounties-and-responsible-disclosures" },
|
||||
{ label: "Research", href: "https://xls.xrpl.org/" },
|
||||
{ label: "Ecosystem Map", href: "/community/ecosystem-map" },
|
||||
{ label: "Bug Bounty", href: "/community/bug-bounty" },
|
||||
{ label: "Research", href: "/community/research" },
|
||||
],
|
||||
},
|
||||
{ label: "Ecosystem Map", href: "/about/uses", icon: "learn" },
|
||||
{ label: "Creators", href: "/community/ambassadors", icon: "learn" },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -138,21 +142,23 @@ export const communitySubmenuData: {
|
||||
export const networkSubmenuData: NetworkSubmenuSection[] = [
|
||||
{
|
||||
label: "Resources",
|
||||
href: "/resources",
|
||||
href: "/docs/concepts/networks-and-servers",
|
||||
icon: "resources",
|
||||
children: [
|
||||
{ label: "About", href: "/about/history" },
|
||||
{ label: "XRPL Brand Kit", href: "/community/brand-kit" },
|
||||
{ label: "Validators", href: "/docs/concepts/networks-and-servers/validators" },
|
||||
{ label: "Governance", href: "/docs/concepts/networks-and-servers/governance", active: true },
|
||||
{ label: "XRPL Roadmap", href: "/docs/concepts/networks-and-servers/xrpl-roadmap" },
|
||||
],
|
||||
patternColor: 'lilac',
|
||||
},
|
||||
{
|
||||
label: "Insights",
|
||||
href: "/insights",
|
||||
href: "/docs/concepts/networks-and-servers/insights",
|
||||
icon: "insights",
|
||||
children: [
|
||||
{ label: "Explorer", href: "https://livenet.xrpl.org" },
|
||||
{ label: "Amendment Voting Status", href: "https://xrpl.org/resources/known-amendments" },
|
||||
{ label: "Data Dashboard", href: "/docs/concepts/networks-and-servers/data-dashboard" },
|
||||
{ label: "Amendment Voting Status", href: "/docs/concepts/networks-and-servers/amendments" },
|
||||
],
|
||||
patternColor: 'green',
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from "react";
|
||||
import { useThemeHooks } from "@redocly/theme/core/hooks";
|
||||
import { SubmenuSection } from "./SubmenuSection";
|
||||
import { ArrowIcon } from "../icons";
|
||||
import { walletIcons, resourcesIconPattern, insightsIconPattern } from "../constants/icons";
|
||||
import { walletIcons, resourcesPurplePattern, insightsGreenPattern, darkInsightsGreenPattern, darkLilacPattern } from "../constants/icons";
|
||||
import { developSubmenuData, useCasesSubmenuData, communitySubmenuData, networkSubmenuData } from "../constants/navigation";
|
||||
import type { SubmenuItem, SubmenuItemWithChildren, NetworkSubmenuSection } from "../types";
|
||||
|
||||
@@ -161,10 +161,13 @@ export function Submenu({ variant, isActive, isClosing, onClose }: SubmenuProps)
|
||||
);
|
||||
}
|
||||
|
||||
/** Network submenu with pattern images (same for light and dark mode) */
|
||||
/** Network submenu with theme-aware pattern images */
|
||||
function NetworkSubmenuContent({ isActive, isClosing, onClose }: { isActive: boolean; isClosing: boolean; onClose?: () => void }) {
|
||||
const { useTranslate } = useThemeHooks();
|
||||
const { translate } = useTranslate();
|
||||
// Start with null to indicate "not yet determined" - avoids hydration mismatch
|
||||
// by ensuring server and client both render the same initial state
|
||||
const [isDarkMode, setIsDarkMode] = React.useState<boolean | null>(null);
|
||||
|
||||
// Handle keyboard events for accessibility
|
||||
const handleKeyDown = React.useCallback((event: KeyboardEvent) => {
|
||||
@@ -220,11 +223,21 @@ function NetworkSubmenuContent({ isActive, isClosing, onClose }: { isActive: boo
|
||||
}
|
||||
}, [isActive, handleKeyDown]);
|
||||
|
||||
// Use same pattern images for both light and dark mode
|
||||
const patternImages = {
|
||||
lilac: resourcesIconPattern,
|
||||
green: insightsIconPattern,
|
||||
};
|
||||
React.useEffect(() => {
|
||||
const checkTheme = () => {
|
||||
setIsDarkMode(document.documentElement.classList.contains('dark'));
|
||||
};
|
||||
checkTheme();
|
||||
const observer = new MutationObserver(checkTheme);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// Default to light mode patterns until client-side detection runs
|
||||
const patternImages = React.useMemo(() => ({
|
||||
lilac: isDarkMode === true ? darkLilacPattern : resourcesPurplePattern,
|
||||
green: isDarkMode === true ? darkInsightsGreenPattern : insightsGreenPattern,
|
||||
}), [isDarkMode]);
|
||||
|
||||
const classNames = [
|
||||
'bds-submenu',
|
||||
|
||||
@@ -40,10 +40,8 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="green"
|
||||
heading="The Compliant Ledger Protocol"
|
||||
subheading="A decentralized public Layer 1 blockchain for creating, transferring, and exchanging digital assets with a focus on compliance."
|
||||
buttons={[
|
||||
{ label: "Get Started", onClick: () => handleClick('responsive-demo-primary') },
|
||||
{ label: "Learn More", onClick: () => handleClick('responsive-demo-tertiary') }
|
||||
]}
|
||||
primaryButton={{ label: "Get Started", onClick: () => handleClick('responsive-demo-primary') }}
|
||||
tertiaryButton={{ label: "Learn More", onClick: () => handleClick('responsive-demo-tertiary') }}
|
||||
/>
|
||||
|
||||
{/* Responsive Behavior */}
|
||||
@@ -124,9 +122,7 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="default"
|
||||
heading="Build on XRPL"
|
||||
subheading="Start building your next decentralized application on the XRP Ledger."
|
||||
buttons={[
|
||||
{ label: "Start Building", href: "#start" }
|
||||
]}
|
||||
primaryButton={{ label: "Start Building", href: "#start" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -147,10 +143,8 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="light-gray"
|
||||
heading="Developer Resources"
|
||||
subheading="Access comprehensive documentation, tutorials, and code samples."
|
||||
buttons={[
|
||||
{ label: "View Docs", href: "#docs" },
|
||||
{ label: "Browse Tutorials", href: "#tutorials" }
|
||||
]}
|
||||
primaryButton={{ label: "View Docs", href: "#docs" }}
|
||||
tertiaryButton={{ label: "Browse Tutorials", href: "#tutorials" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -171,9 +165,7 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="lilac"
|
||||
heading="New Feature Release"
|
||||
subheading="Discover the latest enhancements and capabilities added to the XRP Ledger."
|
||||
buttons={[
|
||||
{ label: "Learn More", href: "#features" }
|
||||
]}
|
||||
primaryButton={{ label: "Learn More", href: "#features" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -194,10 +186,8 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="green"
|
||||
heading="The Compliant Ledger Protocol"
|
||||
subheading="A decentralized public Layer 1 blockchain for creating, transferring, and exchanging digital assets with a focus on compliance."
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "#get-started" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
primaryButton={{ label: "Get Started", href: "#get-started" }}
|
||||
tertiaryButton={{ label: "Learn More", href: "#learn" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -218,10 +208,8 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="gray"
|
||||
heading="Join the Community"
|
||||
subheading="Connect with developers building on XRPL."
|
||||
buttons={[
|
||||
{ label: "Join Discord", href: "#discord" },
|
||||
{ label: "View Events", href: "#events" }
|
||||
]}
|
||||
primaryButton={{ label: "Join Discord", href: "#discord" }}
|
||||
tertiaryButton={{ label: "View Events", href: "#events" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -267,9 +255,7 @@ export default function CalloutMediaBannerShowcase() {
|
||||
<CalloutMediaBanner
|
||||
backgroundImage={sampleBackgroundImage}
|
||||
subheading="A decentralized public Layer 1 blockchain for creating, transferring, and exchanging digital assets with a focus on compliance."
|
||||
buttons={[
|
||||
{ label: "Start Building", onClick: () => handleClick('image-white-primary') }
|
||||
]}
|
||||
primaryButton={{ label: "Start Building", onClick: () => handleClick('image-white-primary') }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -291,10 +277,8 @@ export default function CalloutMediaBannerShowcase() {
|
||||
textColor="black"
|
||||
heading="Build the Future of Finance"
|
||||
subheading="Create powerful decentralized applications with XRPL's fast, efficient, and sustainable blockchain technology."
|
||||
buttons={[
|
||||
{ label: "Start Building", onClick: () => handleClick('image-black-primary') },
|
||||
{ label: "Explore Features", onClick: () => handleClick('image-black-tertiary') }
|
||||
]}
|
||||
primaryButton={{ label: "Start Building", onClick: () => handleClick('image-black-primary') }}
|
||||
tertiaryButton={{ label: "Explore Features", onClick: () => handleClick('image-black-tertiary') }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -337,10 +321,8 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="default"
|
||||
heading="Complete Feature Set"
|
||||
subheading="Access all the tools you need to build on XRPL."
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "#start" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
primaryButton={{ label: "Get Started", href: "#start" }}
|
||||
tertiaryButton={{ label: "Learn More", href: "#learn" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -361,9 +343,7 @@ export default function CalloutMediaBannerShowcase() {
|
||||
variant="light-gray"
|
||||
heading="Simple Call-to-Action"
|
||||
subheading="Focus user attention on a single primary action."
|
||||
buttons={[
|
||||
{ label: "Take Action", href: "#action" }
|
||||
]}
|
||||
primaryButton={{ label: "Take Action", href: "#action" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -403,10 +383,8 @@ export default function CalloutMediaBannerShowcase() {
|
||||
<CalloutMediaBanner
|
||||
variant="green"
|
||||
subheading="Important information or announcement without requiring user action."
|
||||
buttons={[
|
||||
{ label: "Take Action", href: "#action" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
primaryButton={{ label: "Take Action", href: "#action" }}
|
||||
tertiaryButton={{ label: "Learn More", href: "#learn" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -586,12 +564,20 @@ export default function CalloutMediaBannerShowcase() {
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Subheading/description text</div>
|
||||
</div>
|
||||
|
||||
{/* buttons */}
|
||||
{/* primaryButton */}
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>buttons</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>{`Array<{ label, href?, onClick?, forceColor? }>`}</code></div>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>primaryButton</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>{`{ label, href?, onClick? }`}</code></div>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>undefined</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Button configurations (1-2 buttons supported)</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Primary button configuration</div>
|
||||
</div>
|
||||
|
||||
{/* tertiaryButton */}
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>tertiaryButton</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>{`{ label, href?, onClick? }`}</code></div>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>undefined</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Tertiary button configuration</div>
|
||||
</div>
|
||||
|
||||
{/* className */}
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
import { CardStats, CardStatsCardConfig } from "shared/patterns/CardStats";
|
||||
import { Divider } from "shared/components/Divider";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: 'CardStats Pattern Showcase',
|
||||
description: "A comprehensive showcase of the CardStats pattern component demonstrating different configurations and color variants in the XRPL.org Design System.",
|
||||
}
|
||||
};
|
||||
|
||||
// Sample cards data matching Figma design (node 32051:2839)
|
||||
const sampleCards: CardStatsCardConfig[] = [
|
||||
{
|
||||
statistic: "12",
|
||||
superscript: "+",
|
||||
label: "Continuous uptime years",
|
||||
variant: "lilac",
|
||||
primaryButton: { label: "Learn More", href: "#uptime" },
|
||||
},
|
||||
{
|
||||
statistic: "6M",
|
||||
superscript: "2",
|
||||
label: "Active wallets",
|
||||
variant: "light-gray",
|
||||
primaryButton: { label: "Explore", href: "#wallets" },
|
||||
},
|
||||
{
|
||||
statistic: "$1T",
|
||||
superscript: "+",
|
||||
label: "Value transferred",
|
||||
variant: "green",
|
||||
primaryButton: { label: "View Stats", href: "#value" },
|
||||
},
|
||||
{
|
||||
statistic: "3-5s",
|
||||
label: "Transaction finality",
|
||||
variant: "green",
|
||||
primaryButton: { label: "Learn More", href: "#speed" },
|
||||
},
|
||||
{
|
||||
statistic: "70",
|
||||
superscript: "+",
|
||||
label: "Ecosystem partners",
|
||||
variant: "dark-gray",
|
||||
primaryButton: { label: "Meet Partners", href: "#partners" },
|
||||
},
|
||||
{
|
||||
statistic: "100K",
|
||||
superscript: "+",
|
||||
label: "Developer community",
|
||||
variant: "lilac",
|
||||
primaryButton: { label: "Join Us", href: "#community" },
|
||||
},
|
||||
];
|
||||
|
||||
export default function CardStatsShowcase() {
|
||||
return (
|
||||
<div className="landing">
|
||||
<div className="overflow-hidden">
|
||||
{/* Hero Section */}
|
||||
<section className="py-26 text-center">
|
||||
<div className="col-lg-8 mx-auto">
|
||||
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
|
||||
<h1 className="mb-4">CardStats Pattern</h1>
|
||||
<p className="longform">
|
||||
A section pattern that displays a heading, optional description, and a responsive
|
||||
grid of CardStat components. Designed for showcasing key statistics and metrics.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Design Tokens Info */}
|
||||
<PageGrid className="py-10">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Design Specifications</h2>
|
||||
<div className="d-flex flex-wrap gap-6">
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Typography</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Heading:</strong> heading-md (Tobias Light)</li>
|
||||
<li><strong>Description:</strong> body-l (Booton Light)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Grid Layout</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Mobile:</strong> 2 columns</li>
|
||||
<li><strong>Tablet:</strong> 2 columns</li>
|
||||
<li><strong>Desktop:</strong> 3 columns</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Color Variants</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Lilac:</strong> #C0A7FF</li>
|
||||
<li><strong>Green:</strong> #21E46B</li>
|
||||
<li><strong>Light Gray:</strong> #E6EAF0</li>
|
||||
<li><strong>Dark Gray:</strong> #CAD4DF</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Full Example - 6 Cards with Heading and Description */}
|
||||
<CardStats
|
||||
heading="Blockchain Trusted at Scale"
|
||||
description="Streamline development and build powerful RWA tokenization solutions with XRP Ledger's comprehensive developer toolset."
|
||||
cards={sampleCards}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Heading Only - No Description */}
|
||||
<CardStats
|
||||
heading="XRPL Network Statistics"
|
||||
cards={[
|
||||
{
|
||||
statistic: "12",
|
||||
superscript: "+",
|
||||
label: "Continuous uptime years",
|
||||
variant: "lilac",
|
||||
primaryButton: { label: "Learn More", href: "#uptime" },
|
||||
span: { base: 4, md: 4, lg: 6 },
|
||||
},
|
||||
{
|
||||
statistic: "6M",
|
||||
superscript: "2",
|
||||
label: "Active wallets",
|
||||
variant: "light-gray",
|
||||
primaryButton: { label: "Explore", href: "#wallets" },
|
||||
span: { base: 4, md: 4, lg: 6 },
|
||||
},
|
||||
{
|
||||
statistic: "$1T",
|
||||
superscript: "+",
|
||||
label: "Value transferred",
|
||||
variant: "green",
|
||||
primaryButton: { label: "View Stats", href: "#value" },
|
||||
span: { base: 4, md: 8, lg: 12 },
|
||||
}]}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 4 Cards Example */}
|
||||
<CardStats
|
||||
heading="Why Build on XRPL?"
|
||||
description="The XRP Ledger provides enterprise-grade infrastructure for building the future of finance."
|
||||
cards={sampleCards.slice(0, 4)}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Two Buttons Example */}
|
||||
<PageGrid className="py-10">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-4">Two Button Cards</h2>
|
||||
<p className="mb-8">Cards can include both primary and secondary buttons for multiple CTAs.</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<CardStats
|
||||
heading="Get Started with XRPL"
|
||||
description="Explore the XRP Ledger ecosystem with comprehensive documentation and developer resources."
|
||||
cards={[
|
||||
{
|
||||
statistic: "12",
|
||||
superscript: "+",
|
||||
label: "Continuous uptime years",
|
||||
variant: "lilac",
|
||||
primaryButton: { label: "Learn More", href: "#learn" },
|
||||
secondaryButton: { label: "View Docs", href: "#docs" },
|
||||
},
|
||||
{
|
||||
statistic: "6M",
|
||||
superscript: "+",
|
||||
label: "Active wallets",
|
||||
variant: "green",
|
||||
primaryButton: { label: "Get Started", href: "#start" },
|
||||
secondaryButton: { label: "Explore", href: "#explore" },
|
||||
},
|
||||
{
|
||||
statistic: "$1T",
|
||||
superscript: "+",
|
||||
label: "Value transferred",
|
||||
variant: "light-gray",
|
||||
primaryButton: { label: "View Stats", href: "#stats" },
|
||||
secondaryButton: { label: "Learn More", href: "#about" },
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Code Examples */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-8">Code Examples</h2>
|
||||
|
||||
<h5 className="mb-4">Basic Usage</h5>
|
||||
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`import { CardStats } from 'shared/patterns/CardStats';
|
||||
|
||||
<CardStats
|
||||
heading="Blockchain Trusted at Scale"
|
||||
description="Optional description text here."
|
||||
cards={[
|
||||
{
|
||||
statistic: "12",
|
||||
superscript: "+",
|
||||
label: "Continuous uptime years",
|
||||
variant: "lilac",
|
||||
primaryButton: { label: "Learn More", href: "/docs" }
|
||||
},
|
||||
{
|
||||
statistic: "6M",
|
||||
label: "Active wallets",
|
||||
variant: "green"
|
||||
},
|
||||
// ... more cards
|
||||
]}
|
||||
/>`}</pre>
|
||||
</div>
|
||||
|
||||
<h5 className="mb-4">Without Description</h5>
|
||||
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`<CardStats
|
||||
heading="XRPL Network Statistics"
|
||||
cards={statsCards}
|
||||
/>`}</pre>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Design References */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Design References</h2>
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<div>
|
||||
<strong>Figma Design:</strong>{' '}
|
||||
<a href="https://www.figma.com/design/drnQQXnK9Q67MTPPKQsY9l/Section-Cards---Stats?node-id=32051-2839&m=dev" target="_blank" rel="noopener noreferrer">
|
||||
Section Cards - Stats (Figma)
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Pattern Location:</strong>{' '}
|
||||
<code>shared/patterns/CardStats/</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Component Used:</strong>{' '}
|
||||
<code>shared/components/CardStat/</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Color Tokens:</strong>{' '}
|
||||
<code>styles/_colors.scss</code>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
257
about/cards-icon-grid-showcase.page.tsx
Normal file
257
about/cards-icon-grid-showcase.page.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
import { CardsIconGrid } from "shared/patterns/CardsIconGrid";
|
||||
import { Divider } from "shared/components/Divider";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: 'CardsIconGrid Pattern Showcase',
|
||||
description: "A comprehensive showcase of the CardsIconGrid pattern component demonstrating light and dark mode variations in the XRPL.org Design System.",
|
||||
}
|
||||
};
|
||||
|
||||
// Sample icon SVG for demonstration
|
||||
const SAMPLE_ICON = "/img/icons/card-icon-placeholder.svg";
|
||||
|
||||
// Sample cards data - Green variant
|
||||
const greenCards = [
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "Digital Wallets icon",
|
||||
label: "Digital Wallets",
|
||||
href: "#wallets",
|
||||
variant: "green" as const,
|
||||
},
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "B2B Payment Rails icon",
|
||||
label: "B2B Payment Rails",
|
||||
href: "#payments",
|
||||
variant: "green" as const,
|
||||
},
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "Compliance-First Payments icon",
|
||||
label: "Compliance-First Payments",
|
||||
href: "#compliance",
|
||||
variant: "green" as const,
|
||||
},
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "Merchant Settlement icon",
|
||||
label: "Merchant Settlement",
|
||||
href: "#settlement",
|
||||
variant: "green" as const,
|
||||
},
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "Cross-Border Payments icon",
|
||||
label: "Cross-Border Payments",
|
||||
href: "#cross-border",
|
||||
variant: "green" as const,
|
||||
},
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "Treasury Management icon",
|
||||
label: "Treasury Management",
|
||||
href: "#treasury",
|
||||
variant: "green" as const,
|
||||
},
|
||||
];
|
||||
|
||||
// Sample cards data - Neutral variant
|
||||
const neutralCards = [
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "Documentation icon",
|
||||
label: "Documentation",
|
||||
href: "#docs",
|
||||
variant: "neutral" as const,
|
||||
},
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "Tutorials icon",
|
||||
label: "Tutorials",
|
||||
href: "#tutorials",
|
||||
variant: "neutral" as const,
|
||||
},
|
||||
{
|
||||
icon: SAMPLE_ICON,
|
||||
iconAlt: "API Reference icon",
|
||||
label: "API Reference",
|
||||
href: "#api",
|
||||
variant: "neutral" as const,
|
||||
},
|
||||
];
|
||||
|
||||
export default function CardsIconGridShowcase() {
|
||||
return (
|
||||
<div className="landing">
|
||||
<div className="overflow-hidden">
|
||||
{/* Hero Section */}
|
||||
<section className="py-26 text-center">
|
||||
<div className="col-lg-8 mx-auto">
|
||||
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
|
||||
<h1 className="mb-4">CardsIconGrid Pattern</h1>
|
||||
<p className="longform">
|
||||
A section pattern that displays a heading, optional description, and a responsive grid
|
||||
of CardIcon components. Follows the "CardIconGrid" design from Figma.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Design Tokens Reference */}
|
||||
<PageGrid className="py-10">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 8, lg: 12 }}>
|
||||
<h2 className="h4 mb-6">Design Specifications</h2>
|
||||
<div className="d-flex flex-wrap gap-8">
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Typography</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Heading:</strong> heading-md (Tobias Light)</li>
|
||||
<li><strong>Description:</strong> body-l (Booton Light)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Grid Layout</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Mobile:</strong> 1 column</li>
|
||||
<li><strong>Tablet:</strong> 2 columns</li>
|
||||
<li><strong>Desktop:</strong> 3 columns</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Colors</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Light Mode:</strong> $black (#141414)</li>
|
||||
<li><strong>Dark Mode:</strong> $white (#FFFFFF)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 6 Cards Example - Green Variant */}
|
||||
<section>
|
||||
<CardsIconGrid
|
||||
heading="Unlock new business models with embedded payments"
|
||||
description="Streamline development and build powerful RWA tokenization solutions with XRP Ledger's comprehensive developer toolset."
|
||||
cards={greenCards}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 3 Cards Example - Neutral Variant */}
|
||||
<section>
|
||||
<CardsIconGrid
|
||||
heading="Developer Resources"
|
||||
description="Everything you need to start building on the XRP Ledger."
|
||||
cards={neutralCards}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Without Description */}
|
||||
<PageGrid className="py-10">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 8, lg: 12 }}>
|
||||
<h2 className="h4 mb-4">Without Description</h2>
|
||||
<p className="mb-0">
|
||||
The description prop is optional. When omitted, only the heading appears above the cards.
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<section>
|
||||
<CardsIconGrid
|
||||
heading="Funding & Support Programs"
|
||||
cards={greenCards.slice(0, 3)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Code Examples */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 8, lg: 10 }}>
|
||||
<h2 className="h4 mb-6">Code Examples</h2>
|
||||
|
||||
<h5 className="mb-4">Basic Usage</h5>
|
||||
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#1a1a1a', fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#f8f8f2' }}>{`import { CardsIconGrid } from 'shared/patterns/CardsIconGrid';
|
||||
|
||||
<CardsIconGrid
|
||||
heading="Unlock new business models"
|
||||
description="Build powerful solutions with XRPL."
|
||||
cards={[
|
||||
{
|
||||
icon: "/icons/wallet.svg",
|
||||
label: "Digital Wallets",
|
||||
href: "/docs/wallets",
|
||||
variant: "green"
|
||||
},
|
||||
{
|
||||
icon: "/icons/payments.svg",
|
||||
label: "B2B Payment Rails",
|
||||
href: "/docs/payments",
|
||||
variant: "green"
|
||||
},
|
||||
{
|
||||
icon: "/icons/compliance.svg",
|
||||
label: "Compliance-First Payments",
|
||||
href: "/docs/compliance",
|
||||
variant: "green"
|
||||
}
|
||||
]}
|
||||
/>`}</pre>
|
||||
</div>
|
||||
|
||||
<h5 className="mb-4">Without Description</h5>
|
||||
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#1a1a1a', fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#f8f8f2' }}>{`<CardsIconGrid
|
||||
heading="Developer Resources"
|
||||
cards={[
|
||||
{ icon: "/icons/docs.svg", label: "Documentation", href: "/docs", variant: "neutral" },
|
||||
{ icon: "/icons/tutorials.svg", label: "Tutorials", href: "/tutorials", variant: "neutral" },
|
||||
{ icon: "/icons/api.svg", label: "API Reference", href: "/api", variant: "neutral" }
|
||||
]}
|
||||
/>`}</pre>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Design References */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 8, lg: 12 }}>
|
||||
<h2 className="h4 mb-6">Design References</h2>
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<div>
|
||||
<strong>Figma:</strong>{' '}
|
||||
<a href="https://www.figma.com/design/Ojj6UpFBw3HMb0QqRaKxAU/Section-Cards---Icon?node-id=30071-3082&m=dev" target="_blank" rel="noopener noreferrer">
|
||||
Section Cards - Icon Grid
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Documentation:</strong>{' '}
|
||||
<code>shared/patterns/CardsIconGrid/CardsIconGrid.md</code>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
import { CardsTwoColumn } from "shared/patterns/CardsTwoColumn";
|
||||
import { TextCard } from "shared/patterns/CardsTwoColumn";
|
||||
import { Divider } from "shared/components/Divider";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: 'CardsTwoColumn Pattern Showcase',
|
||||
description: "A comprehensive showcase of the CardsTwoColumn pattern component demonstrating different color combinations and arrangements in the XRPL.org Design System.",
|
||||
}
|
||||
};
|
||||
|
||||
export default function CardsTwoColumnShowcase() {
|
||||
return (
|
||||
<div className="landing">
|
||||
<div className="overflow-hidden">
|
||||
{/* Hero Section */}
|
||||
<section className="py-26 text-center">
|
||||
<div className="col-lg-8 mx-auto">
|
||||
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
|
||||
<h1 className="mb-4">CardsTwoColumn Pattern</h1>
|
||||
<p className="longform">
|
||||
A section pattern with a header (title + description) and a 2x2 grid of TextCard components.
|
||||
Features 4 color variants and responsive behavior across all breakpoints.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Design Specifications */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Design Specifications</h2>
|
||||
<div className="d-flex flex-row gap-6 mb-6" style={{ flexWrap: 'wrap' }}>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Section Typography</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Title:</strong> heading-md (Tobias Light)</li>
|
||||
<li><strong>Description:</strong> body-l (Booton Light, muted)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Card Typography</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Title:</strong> heading-lg (Tobias Light)</li>
|
||||
<li><strong>Description:</strong> body-l (Booton Light)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Card Heights</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Desktop:</strong> 340px</li>
|
||||
<li><strong>Tablet:</strong> 309px</li>
|
||||
<li><strong>Mobile:</strong> 274px</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Section Padding</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Desktop:</strong> 40px vertical, 32px horizontal</li>
|
||||
<li><strong>Tablet:</strong> 32px vertical, 24px horizontal</li>
|
||||
<li><strong>Mobile:</strong> 24px vertical, 16px horizontal</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Gap Between Header & Cards</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Desktop:</strong> 40px</li>
|
||||
<li><strong>Tablet:</strong> 32px</li>
|
||||
<li><strong>Mobile:</strong> 24px</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Grid Layout</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Desktop:</strong> 2×2 grid (8px gap)</li>
|
||||
<li><strong>Tablet:</strong> 1 column stacked (8px gap)</li>
|
||||
<li><strong>Mobile:</strong> 1 column stacked (8px gap)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Full Pattern Example */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Full Pattern Example</h2>
|
||||
<p className="mb-6">
|
||||
The CardsTwoColumn pattern includes a header section (title + description) and a 2×2 grid of TextCards.
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<CardsTwoColumn
|
||||
title="The Future of Finance is Already Onchain"
|
||||
description="XRP Ledger isn't about bold predictions. It's about delivering value now. Institutions, developers, and enterprises are already building on XRPL."
|
||||
secondaryDescription="On XRPL, you're not waiting for the future. You're building it."
|
||||
cards={[
|
||||
{
|
||||
title: "Institutions",
|
||||
description: "Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility.",
|
||||
href: "#institutions",
|
||||
color: "lilac"
|
||||
},
|
||||
{
|
||||
title: "Developers",
|
||||
description: "Build decentralized applications with comprehensive documentation, tutorials, and developer tools.",
|
||||
href: "#developers",
|
||||
color: "neutral-light"
|
||||
},
|
||||
{
|
||||
title: "Enterprise",
|
||||
description: "Scale your business with enterprise-grade blockchain solutions and dedicated support.",
|
||||
href: "#enterprise",
|
||||
color: "neutral-dark"
|
||||
},
|
||||
{
|
||||
title: "Community",
|
||||
description: "Join the global community of XRPL developers, validators, and enthusiasts.",
|
||||
href: "#community",
|
||||
color: "green"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Color Variants Section */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">All 6 Color Variants</h2>
|
||||
<p className="mb-6">
|
||||
TextCard supports 6 color variants with hover and pressed states. Hover over cards to see the window shade animation.
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* All Color Variants - Standalone TextCards */}
|
||||
<PageGrid className="pb-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="d-flex flex-wrap gap-3">
|
||||
<TextCard
|
||||
title="Green"
|
||||
description="Default: $green-200 → Hover: $green-300 → Pressed: $green-400"
|
||||
color="green"
|
||||
style={{ flex: '1 1 300px', minWidth: 280 }}
|
||||
/>
|
||||
<TextCard
|
||||
title="Neutral Light"
|
||||
description="Default: $gray-200 → Hover: $gray-300 → Pressed: $gray-400"
|
||||
color="neutral-light"
|
||||
style={{ flex: '1 1 300px', minWidth: 280 }}
|
||||
/>
|
||||
<TextCard
|
||||
title="Neutral Dark"
|
||||
description="Default: $gray-300 → Hover: $gray-400 → Pressed: $gray-500"
|
||||
color="neutral-dark"
|
||||
style={{ flex: '1 1 300px', minWidth: 280 }}
|
||||
/>
|
||||
<TextCard
|
||||
title="Lilac"
|
||||
description="Default: $lilac-200 → Hover: $lilac-300 → Pressed: $lilac-400"
|
||||
color="lilac"
|
||||
style={{ flex: '1 1 300px', minWidth: 280 }}
|
||||
/>
|
||||
<TextCard
|
||||
title="Yellow"
|
||||
description="Default: $yellow-100 → Hover: $yellow-200 → Pressed: $yellow-300"
|
||||
color="yellow"
|
||||
style={{ flex: '1 1 300px', minWidth: 280 }}
|
||||
/>
|
||||
<TextCard
|
||||
title="Blue"
|
||||
description="Default: $blue-100 → Hover: $blue-200 → Pressed: $blue-300"
|
||||
color="blue"
|
||||
style={{ flex: '1 1 300px', minWidth: 280 }}
|
||||
/>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Mixed Colors in Pattern */}
|
||||
<CardsTwoColumn
|
||||
title="All 6 Colors in Pattern"
|
||||
description="The CardsTwoColumn pattern accepts exactly 4 cards. Here we show various color combinations including the new blue variant."
|
||||
cards={[
|
||||
{ title: "Lilac", description: "Primary accent color for highlights.", color: "lilac" },
|
||||
{ title: "Blue", description: "Secondary accent for cool tones.", color: "blue" },
|
||||
{ title: "Green", description: "Brand color for positive actions.", color: "green" },
|
||||
{ title: "Yellow", description: "Secondary accent for warm tones.", color: "yellow" }
|
||||
]}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Alternative Color Combo */}
|
||||
<CardsTwoColumn
|
||||
title="Alternative Color Arrangement"
|
||||
description="Different colors can be used to create visual hierarchy and distinguish between content types."
|
||||
cards={[
|
||||
{ title: "Documentation", description: "Comprehensive guides and API references.", color: "neutral-dark" },
|
||||
{ title: "Tutorials", description: "Step-by-step learning resources.", color: "green" },
|
||||
{ title: "Use Cases", description: "Real-world applications and success stories.", color: "yellow" },
|
||||
{ title: "Resources", description: "Tools and libraries for development.", color: "lilac" }
|
||||
]}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Disabled State Section */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Disabled State</h2>
|
||||
<p className="mb-6">
|
||||
TextCards can be disabled. In light mode, disabled cards have a $gray-100 background with $gray-500 text.
|
||||
In dark mode, disabled cards have a $gray-500 background with 30% opacity. Toggle dark mode to see the difference.
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<PageGrid className="pb-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="d-flex flex-wrap gap-3">
|
||||
<TextCard
|
||||
title="Disabled Green"
|
||||
description="This card is disabled and cannot be interacted with."
|
||||
color="green"
|
||||
disabled
|
||||
style={{ flex: '1 1 300px', minWidth: 280 }}
|
||||
/>
|
||||
<TextCard
|
||||
title="Disabled Neutral Light"
|
||||
description="This card is disabled and cannot be interacted with."
|
||||
color="neutral-light"
|
||||
disabled
|
||||
style={{ flex: '1 1 300px', minWidth: 280 }}
|
||||
/>
|
||||
<TextCard
|
||||
title="Disabled Neutral Dark"
|
||||
description="This card is disabled and cannot be interacted with."
|
||||
color="neutral-dark"
|
||||
disabled
|
||||
style={{ flex: '1 1 300px', minWidth: 280 }}
|
||||
/>
|
||||
<TextCard
|
||||
title="Disabled Lilac"
|
||||
description="This card is disabled and cannot be interacted with."
|
||||
color="lilac"
|
||||
disabled
|
||||
style={{ flex: '1 1 300px', minWidth: 280 }}
|
||||
/>
|
||||
<TextCard
|
||||
title="Disabled Yellow"
|
||||
description="This card is disabled and cannot be interacted with."
|
||||
color="yellow"
|
||||
disabled
|
||||
style={{ flex: '1 1 300px', minWidth: 280 }}
|
||||
/>
|
||||
<TextCard
|
||||
title="Disabled Blue"
|
||||
description="This card is disabled and cannot be interacted with."
|
||||
color="blue"
|
||||
disabled
|
||||
style={{ flex: '1 1 300px', minWidth: 280 }}
|
||||
/>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<Divider />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
537
about/cardstat-showcase.page.tsx
Normal file
537
about/cardstat-showcase.page.tsx
Normal file
@@ -0,0 +1,537 @@
|
||||
import * as React from 'react';
|
||||
import { CardStat } from 'shared/components/CardStat';
|
||||
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: 'CardStat Component Showcase',
|
||||
description: 'Interactive showcase of the Brand Design System CardStat component with all variants and configurations.',
|
||||
},
|
||||
};
|
||||
|
||||
export default function CardStatShowcase() {
|
||||
const [clickCount, setClickCount] = React.useState<Record<string, number>>({});
|
||||
|
||||
const handleClick = (id: string) => {
|
||||
setClickCount((prev) => ({ ...prev, [id]: (prev[id] || 0) + 1 }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="landing">
|
||||
{/* Hero Section */}
|
||||
<PageGrid className="py-26">
|
||||
<div className="d-flex flex-column-reverse col-lg-8 mx-auto">
|
||||
<h1 className="mb-0">CardStat Component</h1>
|
||||
<h6 className="eyebrow mb-3">Brand Design System</h6>
|
||||
</div>
|
||||
<p className="col-lg-8 mx-auto mt-10">
|
||||
A statistics card component following the XRPL Brand Design System. This showcase demonstrates
|
||||
all color variants, button configurations, and responsive behavior using PageGrid.
|
||||
</p>
|
||||
</PageGrid>
|
||||
|
||||
{/* Basic Usage */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-100">
|
||||
<h2 className="h4 mb-8">Basic Usage</h2>
|
||||
<h6 className="eyebrow mb-3">Simple Statistics</h6>
|
||||
</div>
|
||||
<p className="mb-8">
|
||||
CardStat components display prominent statistics with descriptive labels. They adapt responsively
|
||||
and can be used without buttons for purely informational displays.
|
||||
</p>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="6 Million"
|
||||
superscript="2"
|
||||
label="Active wallets"
|
||||
variant="lilac"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="$1 Trillion"
|
||||
superscript="*"
|
||||
label="Value moved"
|
||||
variant="green"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="12"
|
||||
superscript="+"
|
||||
label="Continuous uptime years"
|
||||
variant="light-gray"
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Color Variants */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-100">
|
||||
<h2 className="h4 mb-8">Color Variants</h2>
|
||||
<h6 className="eyebrow mb-3">Visual Themes</h6>
|
||||
</div>
|
||||
<p className="mb-8">
|
||||
Four color variants are available to match different types of statistics and visual contexts.
|
||||
</p>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="6M"
|
||||
superscript="+"
|
||||
label="Active wallets"
|
||||
variant="lilac"
|
||||
/>
|
||||
<p className="mt-4 text-muted"><strong>Lilac</strong> - User metrics, community stats</p>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="$1T"
|
||||
superscript="+"
|
||||
label="Value moved"
|
||||
variant="green"
|
||||
/>
|
||||
<p className="mt-4 text-muted"><strong>Green</strong> - Financial metrics, growth</p>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="12"
|
||||
superscript="+"
|
||||
label="Uptime years"
|
||||
variant="light-gray"
|
||||
/>
|
||||
<p className="mt-4 text-muted"><strong>Light Gray</strong> - Technical stats, reliability</p>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="70+"
|
||||
label="Partners"
|
||||
variant="dark-gray"
|
||||
/>
|
||||
<p className="mt-4 text-muted"><strong>Dark Gray</strong> - Neutral metrics, secondary info</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* With Single Button */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-full">
|
||||
<h2 className="h4 mb-8">With Primary Button</h2>
|
||||
<h6 className="eyebrow mb-3">Single CTA</h6>
|
||||
</div>
|
||||
<p className="mb-8">
|
||||
Add a primary button for a main call-to-action. Buttons use the black variant for proper
|
||||
contrast on colored backgrounds.
|
||||
</p>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 6 }}>
|
||||
<CardStat
|
||||
statistic="6 Million"
|
||||
superscript="+"
|
||||
label="Active wallets"
|
||||
variant="lilac"
|
||||
primaryButton={{
|
||||
label: "Explore",
|
||||
onClick: () => handleClick('explore-1')
|
||||
}}
|
||||
/>
|
||||
{clickCount['explore-1'] > 0 && (
|
||||
<p className="mt-4 text-muted">Clicked {clickCount['explore-1']} time{clickCount['explore-1'] !== 1 ? 's' : ''}</p>
|
||||
)}
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 6 }}>
|
||||
<CardStat
|
||||
statistic="$1 Trillion"
|
||||
superscript="+"
|
||||
label="Value moved"
|
||||
variant="green"
|
||||
primaryButton={{
|
||||
label: "Learn More",
|
||||
onClick: () => handleClick('learn-1')
|
||||
}}
|
||||
/>
|
||||
{clickCount['learn-1'] > 0 && (
|
||||
<p className="mt-4 text-muted">Clicked {clickCount['learn-1']} time{clickCount['learn-1'] !== 1 ? 's' : ''}</p>
|
||||
)}
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 12 }}>
|
||||
<CardStat
|
||||
statistic="12"
|
||||
superscript="+"
|
||||
label="Continuous uptime years"
|
||||
variant="light-gray"
|
||||
primaryButton={{
|
||||
label: "View Details",
|
||||
onClick: () => handleClick('view-1')
|
||||
}}
|
||||
/>
|
||||
{clickCount['view-1'] > 0 && (
|
||||
<p className="mt-4 text-muted">Clicked {clickCount['view-1']} time{clickCount['view-1'] !== 1 ? 's' : ''}</p>
|
||||
)}
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* With Two Buttons */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-full">
|
||||
<h2 className="h4 mb-8">With Two Buttons</h2>
|
||||
<h6 className="eyebrow mb-3">Multiple CTAs</h6>
|
||||
</div>
|
||||
<p className="mb-8">
|
||||
Include both primary and secondary buttons for multiple action options. Buttons wrap responsively
|
||||
and maintain consistent spacing.
|
||||
</p>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="6 Million"
|
||||
superscript="+"
|
||||
label="Active wallets"
|
||||
variant="lilac"
|
||||
primaryButton={{
|
||||
label: "Learn More",
|
||||
onClick: () => handleClick('primary-1')
|
||||
}}
|
||||
secondaryButton={{
|
||||
label: "Get Started",
|
||||
onClick: () => handleClick('secondary-1')
|
||||
}}
|
||||
/>
|
||||
{(clickCount['primary-1'] > 0 || clickCount['secondary-1'] > 0) && (
|
||||
<p className="mt-4 text-muted">
|
||||
Primary: {clickCount['primary-1'] || 0}, Secondary: {clickCount['secondary-1'] || 0}
|
||||
</p>
|
||||
)}
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="$1 Trillion"
|
||||
superscript="+"
|
||||
label="Value moved"
|
||||
variant="green"
|
||||
primaryButton={{
|
||||
label: "Explore",
|
||||
onClick: () => handleClick('primary-2')
|
||||
}}
|
||||
secondaryButton={{
|
||||
label: "View Stats",
|
||||
onClick: () => handleClick('secondary-2')
|
||||
}}
|
||||
/>
|
||||
{(clickCount['primary-2'] > 0 || clickCount['secondary-2'] > 0) && (
|
||||
<p className="mt-4 text-muted">
|
||||
Primary: {clickCount['primary-2'] || 0}, Secondary: {clickCount['secondary-2'] || 0}
|
||||
</p>
|
||||
)}
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="12"
|
||||
superscript="+"
|
||||
label="Continuous uptime years"
|
||||
variant="light-gray"
|
||||
primaryButton={{
|
||||
label: "Read More",
|
||||
onClick: () => handleClick('primary-3')
|
||||
}}
|
||||
secondaryButton={{
|
||||
label: "Try It",
|
||||
onClick: () => handleClick('secondary-3')
|
||||
}}
|
||||
/>
|
||||
{(clickCount['primary-3'] > 0 || clickCount['secondary-3'] > 0) && (
|
||||
<p className="mt-4 text-muted">
|
||||
Primary: {clickCount['primary-3'] || 0}, Secondary: {clickCount['secondary-3'] || 0}
|
||||
</p>
|
||||
)}
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Responsive Behavior */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-full">
|
||||
<h2 className="h4 mb-8">Responsive Layout</h2>
|
||||
<h6 className="eyebrow mb-3">Adaptive Grid</h6>
|
||||
</div>
|
||||
<p className="mb-8">
|
||||
Cards adapt to different screen sizes. On mobile (base), cards stack vertically. On tablet (md),
|
||||
they can be arranged in 2 columns. On desktop (lg+), up to 3-4 columns are supported.
|
||||
</p>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="1M"
|
||||
superscript="+"
|
||||
label="Transactions daily"
|
||||
variant="lilac"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="150"
|
||||
superscript="+"
|
||||
label="Countries"
|
||||
variant="green"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="99.9"
|
||||
superscript="%"
|
||||
label="Uptime"
|
||||
variant="light-gray"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<CardStat
|
||||
statistic="24/7"
|
||||
label="Support"
|
||||
variant="dark-gray"
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Mixed Configurations */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-100">
|
||||
<h2 className="h4 mb-8">Mixed Configurations</h2>
|
||||
<h6 className="eyebrow mb-3">Flexible Usage</h6>
|
||||
</div>
|
||||
<p className="mb-8">
|
||||
Mix and match cards with different button configurations in the same layout.
|
||||
</p>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="6 Million"
|
||||
superscript="+"
|
||||
label="Active wallets"
|
||||
variant="lilac"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="$1 Trillion"
|
||||
superscript="+"
|
||||
label="Value moved"
|
||||
variant="green"
|
||||
primaryButton={{
|
||||
label: "Learn More",
|
||||
onClick: () => handleClick('mixed-1')
|
||||
}}
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="12"
|
||||
superscript="+"
|
||||
label="Continuous uptime years"
|
||||
variant="light-gray"
|
||||
primaryButton={{
|
||||
label: "Explore",
|
||||
onClick: () => handleClick('mixed-2')
|
||||
}}
|
||||
secondaryButton={{
|
||||
label: "Get Started",
|
||||
onClick: () => handleClick('mixed-3')
|
||||
}}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Wide Layout */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-100">
|
||||
<h2 className="h4 mb-8">Wide Card Layout</h2>
|
||||
<h6 className="eyebrow mb-3">Larger Spans</h6>
|
||||
</div>
|
||||
<p className="mb-8">
|
||||
Cards can span multiple columns for wider layouts on larger screens.
|
||||
</p>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 8, lg: 6 }}>
|
||||
<CardStat
|
||||
statistic="6 Million"
|
||||
superscript="+"
|
||||
label="Active wallets using XRPL"
|
||||
variant="lilac"
|
||||
primaryButton={{
|
||||
label: "Explore Wallets",
|
||||
onClick: () => handleClick('wide-1')
|
||||
}}
|
||||
secondaryButton={{
|
||||
label: "Get Started",
|
||||
onClick: () => handleClick('wide-2')
|
||||
}}
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 8, lg: 6 }}>
|
||||
<CardStat
|
||||
statistic="$1 Trillion"
|
||||
superscript="+"
|
||||
label="Total value moved on the network"
|
||||
variant="green"
|
||||
primaryButton={{
|
||||
label: "View Statistics",
|
||||
onClick: () => handleClick('wide-3')
|
||||
}}
|
||||
secondaryButton={{
|
||||
label: "Learn More",
|
||||
onClick: () => handleClick('wide-4')
|
||||
}}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Usage Guidelines */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="d-flex flex-column-reverse w-100">
|
||||
<h2 className="h4 mb-8">Usage Guidelines</h2>
|
||||
<h6 className="eyebrow mb-3">Best Practices</h6>
|
||||
</div>
|
||||
<div className="col-lg-8 mx-auto w-100">
|
||||
<h5 className="mb-4">When to Use</h5>
|
||||
<ul className="mb-8">
|
||||
<li><strong>Key metrics</strong> - Highlight important numbers prominently</li>
|
||||
<li><strong>Dashboard sections</strong> - Create stat-focused areas on landing pages</li>
|
||||
<li><strong>About pages</strong> - Showcase company or product statistics</li>
|
||||
<li><strong>Feature sections</strong> - Emphasize quantitative benefits</li>
|
||||
</ul>
|
||||
|
||||
<h5 className="mb-4">Color Variant Selection</h5>
|
||||
<ul className="mb-8">
|
||||
<li><strong>Lilac</strong> - User-focused statistics, community metrics</li>
|
||||
<li><strong>Green</strong> - Financial metrics, growth indicators</li>
|
||||
<li><strong>Light Gray</strong> - Technical statistics, reliability metrics</li>
|
||||
<li><strong>Dark Gray</strong> - Neutral or secondary information</li>
|
||||
</ul>
|
||||
|
||||
<h5 className="mb-4">Button Configuration</h5>
|
||||
<ul className="mb-8">
|
||||
<li><strong>No buttons</strong> - For purely informational displays</li>
|
||||
<li><strong>Single button</strong> - For one clear call-to-action</li>
|
||||
<li><strong>Two buttons</strong> - For multiple action options</li>
|
||||
</ul>
|
||||
|
||||
<h5 className="mb-4">Tips</h5>
|
||||
<ul>
|
||||
<li>Keep statistics concise using abbreviations (M, K, T, +)</li>
|
||||
<li>Use descriptive labels that clearly explain the metric</li>
|
||||
<li>Choose colors that match the type of statistic</li>
|
||||
<li>Test on all breakpoints to ensure proper responsive behavior</li>
|
||||
<li>Limit buttons to essential actions</li>
|
||||
</ul>
|
||||
</div>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Implementation Examples */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<div className="col-lg-10 mx-auto d-flex flex-column-reverse">
|
||||
<h2 className="h4 mb-8">Code Examples</h2>
|
||||
<h6 className="eyebrow mb-3">Implementation</h6>
|
||||
</div>
|
||||
<div className="col-lg-10 mx-auto">
|
||||
<h5 className="mb-4">Basic Card</h5>
|
||||
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`<CardStat
|
||||
statistic="6 Million"
|
||||
superscript="+"
|
||||
label="Active wallets"
|
||||
variant="lilac"
|
||||
/>`}</pre>
|
||||
</div>
|
||||
|
||||
<h5 className="mb-4">With Primary Button</h5>
|
||||
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`<CardStat
|
||||
statistic="$1 Trillion"
|
||||
superscript="+"
|
||||
label="Value moved"
|
||||
variant="green"
|
||||
primaryButton={{
|
||||
label: "Learn More",
|
||||
href: "/about"
|
||||
}}
|
||||
/>`}</pre>
|
||||
</div>
|
||||
|
||||
<h5 className="mb-4">With Two Buttons</h5>
|
||||
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`<CardStat
|
||||
statistic="12"
|
||||
superscript="+"
|
||||
label="Continuous uptime years"
|
||||
variant="light-gray"
|
||||
primaryButton={{
|
||||
label: "Learn More",
|
||||
onClick: handleLearnMore
|
||||
}}
|
||||
secondaryButton={{
|
||||
label: "Get Started",
|
||||
href: "/start"
|
||||
}}
|
||||
/>`}</pre>
|
||||
</div>
|
||||
|
||||
<h5 className="mb-4">In PageGrid Layout</h5>
|
||||
<div className="p-4 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="6 Million"
|
||||
superscript="+"
|
||||
label="Active wallets"
|
||||
variant="lilac"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="$1 Trillion"
|
||||
superscript="+"
|
||||
label="Value moved"
|
||||
variant="green"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
|
||||
<CardStat
|
||||
statistic="12"
|
||||
superscript="+"
|
||||
label="Uptime years"
|
||||
variant="light-gray"
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</ PageGridRow>
|
||||
</ PageGrid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,686 +0,0 @@
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
import { CarouselCardList } from "shared/patterns/CarouselCardList";
|
||||
import { CarouselButton } from "shared/components/CarouselButton";
|
||||
import { Divider } from "shared/components/Divider";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: 'CarouselCardList Pattern Showcase',
|
||||
description: "A comprehensive showcase of the CarouselCardList pattern component demonstrating horizontal scrolling, navigation buttons, and color variants in the XRPL.org Design System.",
|
||||
}
|
||||
};
|
||||
|
||||
// Sample icon components for demonstration
|
||||
const TokenIcon = () => (
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="34" cy="34" r="20" stroke="currentColor" strokeWidth="2" fill="none"/>
|
||||
<path d="M34 22V46M26 34H42" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const WalletIcon = () => (
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="14" y="20" width="40" height="28" rx="4" stroke="currentColor" strokeWidth="2" fill="none"/>
|
||||
<circle cx="46" cy="34" r="4" fill="currentColor"/>
|
||||
<path d="M14 28H54" stroke="currentColor" strokeWidth="2"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ChartIcon = () => (
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 50L26 38L34 46L54 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M46 18H54V26" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ShieldIcon = () => (
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M34 12L52 20V32C52 44 44 52 34 56C24 52 16 44 16 32V20L34 12Z" stroke="currentColor" strokeWidth="2" fill="none"/>
|
||||
<path d="M26 34L32 40L42 28" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const GlobeIcon = () => (
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="34" cy="34" r="20" stroke="currentColor" strokeWidth="2" fill="none"/>
|
||||
<ellipse cx="34" cy="34" rx="10" ry="20" stroke="currentColor" strokeWidth="2" fill="none"/>
|
||||
<path d="M14 34H54" stroke="currentColor" strokeWidth="2"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CodeIcon = () => (
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M26 24L14 34L26 44" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M42 24L54 34L42 44" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M38 18L30 50" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Sample cards data for neutral variant
|
||||
const neutralCards = [
|
||||
{
|
||||
icon: <TokenIcon />,
|
||||
title: "Native\nTokenization",
|
||||
description: "Issue and manage digital assets directly on the ledger without smart contracts.",
|
||||
href: "#tokenization",
|
||||
},
|
||||
{
|
||||
icon: <WalletIcon />,
|
||||
title: "Low Cost\nTransactions",
|
||||
description: "Transaction costs are a fraction of a cent, making microtransactions viable.",
|
||||
href: "#low-cost",
|
||||
},
|
||||
{
|
||||
icon: <ChartIcon />,
|
||||
title: "Built-in\nDEX",
|
||||
description: "Trade any token for any other token using the native decentralized exchange.",
|
||||
href: "#dex",
|
||||
},
|
||||
{
|
||||
icon: <ShieldIcon />,
|
||||
title: "Enterprise\nSecurity",
|
||||
description: "Multi-signature support and advanced key management for institutional needs.",
|
||||
href: "#security",
|
||||
},
|
||||
{
|
||||
icon: <GlobeIcon />,
|
||||
title: "Global\nReach",
|
||||
description: "Connect to a worldwide network of validators in seconds, not minutes.",
|
||||
href: "#global",
|
||||
},
|
||||
{
|
||||
icon: <CodeIcon />,
|
||||
title: "Developer\nFriendly",
|
||||
description: "Comprehensive SDKs and APIs for JavaScript, Python, Java, and more.",
|
||||
href: "#developer",
|
||||
},
|
||||
];
|
||||
|
||||
// Sample cards data for green variant
|
||||
const greenCards = [
|
||||
{
|
||||
icon: <TokenIcon />,
|
||||
title: "Stablecoin\nIssuance",
|
||||
description: "Create and manage compliant stablecoins with built-in freeze and clawback capabilities.",
|
||||
href: "#stablecoin",
|
||||
},
|
||||
{
|
||||
icon: <WalletIcon />,
|
||||
title: "Institutional\nCustody",
|
||||
description: "Multi-signature accounts and escrow features for enterprise-grade custody solutions.",
|
||||
href: "#custody",
|
||||
},
|
||||
{
|
||||
icon: <ChartIcon />,
|
||||
title: "Real-Time\nSettlement",
|
||||
description: "Transactions settle in 3-5 seconds with finality, enabling real-time payments.",
|
||||
href: "#settlement",
|
||||
},
|
||||
{
|
||||
icon: <ShieldIcon />,
|
||||
title: "Regulatory\nCompliance",
|
||||
description: "Built-in features for AML/KYC compliance and regulatory reporting requirements.",
|
||||
href: "#compliance",
|
||||
},
|
||||
{
|
||||
icon: <GlobeIcon />,
|
||||
title: "Cross-Border\nPayments",
|
||||
description: "Seamless international transfers without correspondent banking delays.",
|
||||
href: "#cross-border",
|
||||
},
|
||||
];
|
||||
|
||||
export default function CarouselCardListShowcase() {
|
||||
return (
|
||||
<div className="landing">
|
||||
<div className="overflow-hidden">
|
||||
{/* Hero Section */}
|
||||
<section className="py-26 text-center">
|
||||
<div className="col-lg-8 mx-auto">
|
||||
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
|
||||
<h1 className="mb-4">CarouselCardList Pattern</h1>
|
||||
<p className="longform">
|
||||
A horizontal scrolling carousel that displays CardOffgrid components with navigation buttons.
|
||||
Supports neutral and green color variants, responsive sizing, and dark/light mode theming.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Feature Overview */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Features</h2>
|
||||
<div className="d-flex flex-row gap-6" style={{ flexWrap: 'wrap' }}>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Layout</h6>
|
||||
<ul className="mb-0">
|
||||
<li>Horizontal scrolling cards</li>
|
||||
<li>Navigation buttons (prev/next)</li>
|
||||
<li>Title constrained to grid</li>
|
||||
<li>Hidden scrollbar</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Responsive Behavior</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Mobile:</strong> 343×400px cards</li>
|
||||
<li><strong>Tablet:</strong> 356×440px cards</li>
|
||||
<li><strong>Desktop:</strong> 400×480px cards</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Button States</h6>
|
||||
<ul className="mb-0">
|
||||
<li>Enabled / Disabled</li>
|
||||
<li>Hover / Active states</li>
|
||||
<li>Focus ring for keyboard nav</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Theming</h6>
|
||||
<ul className="mb-0">
|
||||
<li>Dark mode (default)</li>
|
||||
<li>Light mode (<code>html.light</code>)</li>
|
||||
<li>Neutral & Green card variants</li>
|
||||
<li>Independent button colors (neutral, green, black)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
|
||||
|
||||
{/* Neutral Cards + Neutral Buttons (Default) */}
|
||||
<section className="py-10">
|
||||
<PageGrid className="mb-6">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-3">Neutral Cards + Neutral Buttons (Default)</h2>
|
||||
<p className="mb-0">
|
||||
<code>variant="neutral"</code> - Gray cards with matching gray navigation buttons.
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<CarouselCardList
|
||||
variant="neutral"
|
||||
buttonVariant="neutral"
|
||||
heading="Why Build on the XRP Ledger"
|
||||
description="Discover the unique features that make XRPL the ideal blockchain for building tokenization, payments, and DeFi applications."
|
||||
cards={neutralCards}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
{/* Neutral Cards + Green Buttons */}
|
||||
<section className="py-10">
|
||||
<PageGrid className="mb-6">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-3">Neutral Cards + Green Buttons</h2>
|
||||
<p className="mb-0">
|
||||
<code>variant="neutral" buttonVariant="green"</code> - Gray cards with green navigation buttons.
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<CarouselCardList
|
||||
variant="neutral"
|
||||
buttonVariant="green"
|
||||
heading="Platform Features"
|
||||
description="Gray cards paired with vibrant green buttons for emphasis."
|
||||
cards={neutralCards}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
{/* Neutral Cards + Black Buttons */}
|
||||
<section className="py-10">
|
||||
<PageGrid className="mb-6">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-3">Neutral Cards + Black Buttons</h2>
|
||||
<p className="mb-0">
|
||||
<code>variant="neutral" buttonVariant="black"</code> - Gray cards with black navigation buttons.
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<CarouselCardList
|
||||
variant="neutral"
|
||||
buttonVariant="black"
|
||||
heading="Developer Tools"
|
||||
description="Gray cards paired with black buttons for high contrast."
|
||||
cards={neutralCards}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
{/* Green Cards + Green Buttons */}
|
||||
<section className="py-10">
|
||||
<PageGrid className="mb-6">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-3">Green Cards + Green Buttons</h2>
|
||||
<p className="mb-0">
|
||||
<code>variant="green" buttonVariant="green"</code> - Green cards with matching green navigation buttons.
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<CarouselCardList
|
||||
variant="green"
|
||||
buttonVariant="green"
|
||||
heading="Enterprise Solutions"
|
||||
description="Purpose-built features for institutional adoption with cohesive green theming."
|
||||
cards={greenCards}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
{/* Green Cards + Black Buttons */}
|
||||
<section className="py-10">
|
||||
<PageGrid className="mb-6">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-3">Green Cards + Black Buttons</h2>
|
||||
<p className="mb-0">
|
||||
<code>variant="green" buttonVariant="black"</code> - Green cards with black navigation buttons.
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<CarouselCardList
|
||||
variant="green"
|
||||
buttonVariant="black"
|
||||
heading="Cross-Border Payments"
|
||||
description="Green cards with contrasting black buttons for visual interest."
|
||||
cards={greenCards}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Navigation Button Specifications</h2>
|
||||
|
||||
<div className="d-flex flex-row gap-6 mb-6" style={{ flexWrap: 'wrap' }}>
|
||||
<div style={{ flex: '1 1 220px' }}>
|
||||
<h6 className="mb-3">Dimensions</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Desktop:</strong> 40px × 40px</li>
|
||||
<li><strong>Tablet/Mobile:</strong> 37px × 37px</li>
|
||||
<li><strong>Gap:</strong> 8px between buttons</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 220px' }}>
|
||||
<h6 className="mb-3">Neutral Colors (Dark Mode)</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Enabled:</strong> $gray-500 (#72777E)</li>
|
||||
<li><strong>Hover:</strong> $gray-400 (#8A919A)</li>
|
||||
<li><strong>Disabled:</strong> $gray-500 @ 50%</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 220px' }}>
|
||||
<h6 className="mb-3">Green Colors (Dark Mode)</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Enabled:</strong> $green-300 (#21E46B)</li>
|
||||
<li><strong>Hover:</strong> $green-200 (#70EE97)</li>
|
||||
<li><strong>Disabled:</strong> $green-500 @ 50%</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 220px' }}>
|
||||
<h6 className="mb-3">Black Colors (Dark Mode)</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Enabled:</strong> $black (#141414)</li>
|
||||
<li><strong>Hover:</strong> $gray-500 (#72777E)</li>
|
||||
<li><strong>Disabled:</strong> $black @ 50%</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Button Visual Showcase */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Button Visual Showcase</h2>
|
||||
<p className="mb-6">Interactive carousel buttons showing all variants and states.</p>
|
||||
|
||||
{/* Grey/Neutral Buttons */}
|
||||
<div className="mb-8">
|
||||
<h5 className="mb-4">Grey (Neutral) Buttons</h5>
|
||||
<div className="d-flex flex-row gap-4 align-items-center mb-3" style={{ padding: '24px', backgroundColor: 'var(--bs-body-bg)', borderRadius: '8px' }}>
|
||||
<div className="text-center">
|
||||
<CarouselButton direction="prev" variant="neutral" aria-label="Previous" />
|
||||
<div className="mt-2" style={{ fontSize: '12px' }}>Enabled</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<CarouselButton direction="next" variant="neutral" aria-label="Next" />
|
||||
<div className="mt-2" style={{ fontSize: '12px' }}>Enabled</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<CarouselButton direction="prev" variant="neutral" disabled aria-label="Previous disabled" />
|
||||
<div className="mt-2" style={{ fontSize: '12px' }}>Disabled</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<CarouselButton direction="next" variant="neutral" disabled aria-label="Next disabled" />
|
||||
<div className="mt-2" style={{ fontSize: '12px' }}>Disabled</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Green Buttons */}
|
||||
<div className="mb-8">
|
||||
<h5 className="mb-4">Green Buttons</h5>
|
||||
<div className="d-flex flex-row gap-4 align-items-center mb-3" style={{ padding: '24px', backgroundColor: 'var(--bs-body-bg)', borderRadius: '8px' }}>
|
||||
<div className="text-center">
|
||||
<CarouselButton direction="prev" variant="green" aria-label="Previous" />
|
||||
<div className="mt-2" style={{ fontSize: '12px' }}>Enabled</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<CarouselButton direction="next" variant="green" aria-label="Next" />
|
||||
<div className="mt-2" style={{ fontSize: '12px' }}>Enabled</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<CarouselButton direction="prev" variant="green" disabled aria-label="Previous disabled" />
|
||||
<div className="mt-2" style={{ fontSize: '12px' }}>Disabled</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<CarouselButton direction="next" variant="green" disabled aria-label="Next disabled" />
|
||||
<div className="mt-2" style={{ fontSize: '12px' }}>Disabled</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Black Buttons */}
|
||||
<div className="mb-8">
|
||||
<h5 className="mb-4">Black Buttons</h5>
|
||||
<div className="d-flex flex-row gap-4 align-items-center mb-3" style={{ padding: '24px', backgroundColor: 'var(--bs-body-bg)', borderRadius: '8px' }}>
|
||||
<div className="text-center">
|
||||
<CarouselButton direction="prev" variant="black" aria-label="Previous" />
|
||||
<div className="mt-2" style={{ fontSize: '12px' }}>Enabled</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<CarouselButton direction="next" variant="black" aria-label="Next" />
|
||||
<div className="mt-2" style={{ fontSize: '12px' }}>Enabled</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<CarouselButton direction="prev" variant="black" disabled aria-label="Previous disabled" />
|
||||
<div className="mt-2" style={{ fontSize: '12px' }}>Disabled</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<CarouselButton direction="next" variant="black" disabled aria-label="Next disabled" />
|
||||
<div className="mt-2" style={{ fontSize: '12px' }}>Disabled</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Spacing Tokens */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Spacing Tokens</h2>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '200px', flexShrink: 0 }}><strong>Token</strong></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Mobile</strong></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Tablet</strong></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Desktop</strong></div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '200px', flexShrink: 0 }}>Header Gap</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>8px</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>8px</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>16px</code></div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '200px', flexShrink: 0 }}>Section Gap</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>24px</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>32px</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>40px</code></div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '200px', flexShrink: 0 }}>Cards Gap</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>8px</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>8px</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>8px</code></div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '200px', flexShrink: 0 }}>Card Dimensions</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>343×400px</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>356×440px</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>400×480px</code></div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '200px', flexShrink: 0 }}>Card Padding</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>16px</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>20px</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>24px</code></div>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
|
||||
|
||||
{/* API Reference */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Component API</h2>
|
||||
|
||||
<h5 className="mb-4">CarouselCardListProps</h5>
|
||||
<div className="mb-8">
|
||||
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><strong>Prop</strong></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Type</strong></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}><strong>Default</strong></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Description</strong></div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>variant</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>'neutral' | 'green'</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}><code>'neutral'</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Color variant for cards</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>buttonVariant</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>'neutral' | 'green' | 'black'</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}><code>'neutral'</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Color variant for navigation buttons (independent of cards)</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>heading</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>ReactNode</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Section heading text</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>description</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>ReactNode</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Section description text</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>cards</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>CarouselCardConfig[]</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Array of card configurations</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 className="mb-4">CarouselCardConfig</h5>
|
||||
<p className="mb-4">Each card in the <code>cards</code> array accepts the following properties (same as CardOffgrid, without variant):</p>
|
||||
<div className="mb-6">
|
||||
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><strong>Prop</strong></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Type</strong></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}><strong>Required</strong></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Description</strong></div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>icon</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>ReactNode | string</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Icon component or image URL</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>title</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Card title (use \n for line breaks)</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>description</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Card description text</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>href</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>No</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Link destination URL</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>onClick</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>() => void</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>No</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Click handler function</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>disabled</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>boolean</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>No</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Disabled state</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
|
||||
|
||||
{/* Usage Example */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Usage Example</h2>
|
||||
<div className="card p-4">
|
||||
<pre className="mb-0" style={{ backgroundColor: 'var(--bs-gray-800)', padding: '1rem', borderRadius: '4px', overflow: 'auto' }}>
|
||||
{`import { CarouselCardList } from 'shared/patterns/CarouselCardList';
|
||||
|
||||
// Basic usage - button color matches card color by default
|
||||
<CarouselCardList
|
||||
variant="neutral"
|
||||
heading="Why Build on the XRP Ledger"
|
||||
description="Discover the unique features that make XRPL ideal for your project."
|
||||
cards={[
|
||||
{
|
||||
icon: <TokenIcon />,
|
||||
title: "Native\\nTokenization",
|
||||
description: "Issue and manage digital assets directly on the ledger.",
|
||||
href: "/docs/tokenization",
|
||||
},
|
||||
// ... more cards
|
||||
]}
|
||||
/>
|
||||
|
||||
// With independent button color
|
||||
<CarouselCardList
|
||||
variant="neutral"
|
||||
buttonVariant="black" // Button color independent of card color
|
||||
heading="Developer Tools"
|
||||
description="Gray cards with black navigation buttons."
|
||||
cards={cards}
|
||||
/>`}
|
||||
</pre>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
|
||||
|
||||
{/* Design References */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Design References</h2>
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<div>
|
||||
<strong>Main Carousel Design:</strong>{' '}
|
||||
<a href="https://www.figma.com/design/w0CVv1c40nWDRD27mLiMWS/Section-Carousel---Card-List?node-id=15055-3730&m=dev" target="_blank" rel="noopener noreferrer">
|
||||
Section Carousel - Card List (Figma)
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Button States:</strong>{' '}
|
||||
<a href="https://www.figma.com/design/w0CVv1c40nWDRD27mLiMWS/Section-Carousel---Card-List?node-id=15055-1033&m=dev" target="_blank" rel="noopener noreferrer">
|
||||
Carousel Button States (Figma)
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Component Location:</strong>{' '}
|
||||
<code>shared/patterns/CarouselCardList/</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Documentation:</strong>{' '}
|
||||
<code>shared/patterns/CarouselCardList/CarouselCardList.md</code>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,365 +0,0 @@
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
import { CarouselFeatured, type CarouselSlide, type CarouselFeatureItem } from "shared/patterns/CarouselFeatured";
|
||||
import { Divider } from "shared/components/Divider";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: 'CarouselFeatured Pattern Showcase',
|
||||
description: "A comprehensive showcase of the CarouselFeatured pattern component demonstrating featured image carousels with navigation, background variants, and responsive behavior in the XRPL.org Design System.",
|
||||
}
|
||||
};
|
||||
|
||||
// Sample image URL for demonstration
|
||||
const SAMPLE_IMAGE = "/img/demo-bg.png";
|
||||
|
||||
// Sample slides data
|
||||
const sampleSlides: CarouselSlide[] = [
|
||||
{
|
||||
id: 1,
|
||||
imageSrc: SAMPLE_IMAGE,
|
||||
imageAlt: "Featured slide 1 - XRPL Overview",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
imageSrc: SAMPLE_IMAGE,
|
||||
imageAlt: "Featured slide 2 - Developer Tools",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
imageSrc: SAMPLE_IMAGE,
|
||||
imageAlt: "Featured slide 3 - Enterprise Solutions",
|
||||
},
|
||||
];
|
||||
|
||||
// Sample features data (matching Figma design)
|
||||
const sampleFeatures: CarouselFeatureItem[] = [
|
||||
{
|
||||
title: "Easy-to-Integrate APIs",
|
||||
description: "Build with common languages and skip complex smart contract development",
|
||||
},
|
||||
{
|
||||
title: "Full Lifecycle Support",
|
||||
description: "From dev tools and testnets to deployment and growth-stage",
|
||||
},
|
||||
{
|
||||
title: "Enterprise-Grade Security",
|
||||
description: "Battle-tested infrastructure with 12+ years of continuous uptime",
|
||||
},
|
||||
];
|
||||
|
||||
export default function CarouselFeaturedShowcase() {
|
||||
return (
|
||||
<div className="landing">
|
||||
<div className="overflow-hidden">
|
||||
{/* Hero Section */}
|
||||
<section className="py-26 text-center">
|
||||
<div className="col-lg-8 mx-auto">
|
||||
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
|
||||
<h1 className="mb-4">CarouselFeatured Pattern</h1>
|
||||
<p className="longform">
|
||||
A featured image carousel with two-column layout on desktop (image left, content right)
|
||||
and single-column layout on tablet/mobile (content top, image bottom).
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Feature Overview */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Features</h2>
|
||||
<div className="d-flex flex-row gap-6" style={{ flexWrap: 'wrap' }}>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Layout</h6>
|
||||
<ul className="mb-0">
|
||||
<li>Two-column layout on desktop</li>
|
||||
<li>Image left, content right</li>
|
||||
<li>Feature list with dividers</li>
|
||||
<li>Primary + tertiary buttons</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Background Colors</h6>
|
||||
<ul className="mb-0">
|
||||
<li><code>gray-200</code> (#E6EAF0) - default</li>
|
||||
<li><code>gray-300</code> (#CAD4DF) - neutral</li>
|
||||
<li><code>black</code> (#141414) - dark</li>
|
||||
<li><code>yellow-100</code> (#F3F1EB) - warm</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Content</h6>
|
||||
<ul className="mb-0">
|
||||
<li>Heading (h-md typography)</li>
|
||||
<li>Feature list items</li>
|
||||
<li>Primary button (black pill)</li>
|
||||
<li>Tertiary link (optional)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 250px' }}>
|
||||
<h6 className="mb-3">Responsive</h6>
|
||||
<ul className="mb-0">
|
||||
<li><strong>Mobile:</strong> Single column, content top</li>
|
||||
<li><strong>Tablet:</strong> Single column, content top</li>
|
||||
<li><strong>Desktop:</strong> Two columns, image left</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<Divider weight="strong" color="gray" />
|
||||
|
||||
{/* Default: gray-200 background */}
|
||||
<section className="py-10">
|
||||
<PageGrid className="mb-6">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-3">Grey Background</h2>
|
||||
<p className="mb-0">
|
||||
<code>background="grey"</code> - Light neutral background, the default option.
|
||||
Light mode: gray-200 (#E6EAF0), Dark mode: gray-300 (#CAD4DF).
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<CarouselFeatured
|
||||
background="grey"
|
||||
heading="Powered by Developers"
|
||||
features={sampleFeatures}
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "#get-started" },
|
||||
{ label: "Learn More", href: "#learn-more" }
|
||||
]}
|
||||
slides={sampleSlides.slice(0,1)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Divider weight="strong" color="gray" />
|
||||
|
||||
{/* neutral background */}
|
||||
<section className="py-10">
|
||||
<PageGrid className="mb-6">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-3">Neutral Background</h2>
|
||||
<p className="mb-0">
|
||||
<code>background="neutral"</code> - High contrast neutral background.
|
||||
Light mode: white (#FFF), Dark mode: black (#141414).
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<CarouselFeatured
|
||||
background="neutral"
|
||||
heading="Platform Updates"
|
||||
features={sampleFeatures}
|
||||
buttons={[
|
||||
{ label: "View Updates", href: "#updates" },
|
||||
{ label: "See All", href: "#all" }
|
||||
]}
|
||||
slides={sampleSlides}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Divider weight="strong" color="gray" />
|
||||
|
||||
{/* yellow background */}
|
||||
<section className="py-10">
|
||||
<PageGrid className="mb-6">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-3">Yellow Background</h2>
|
||||
<p className="mb-0">
|
||||
<code>background="yellow"</code> - Warm secondary background color.
|
||||
Same in both modes: yellow-100 (#F3F1EB).
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<CarouselFeatured
|
||||
background="yellow"
|
||||
heading="Community Highlights"
|
||||
features={sampleFeatures}
|
||||
buttons={[
|
||||
{ label: "Join Community", href: "#community" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
slides={sampleSlides}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Divider weight="strong" color="gray" />
|
||||
|
||||
{/* Single button example */}
|
||||
<section className="py-10">
|
||||
<PageGrid className="mb-6">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-3">Single Button (Same Line on Mobile)</h2>
|
||||
<p className="mb-0">
|
||||
When only one button is provided, the button and carousel navigation
|
||||
stay on the same line on mobile instead of stacking.
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<CarouselFeatured
|
||||
background="grey"
|
||||
heading="Single Button Example"
|
||||
features={sampleFeatures}
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "#get-started" }
|
||||
]}
|
||||
slides={sampleSlides}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Divider weight="strong" color="gray" />
|
||||
|
||||
{/* API Reference */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Component API</h2>
|
||||
|
||||
<h5 className="mb-4">CarouselFeaturedProps</h5>
|
||||
<div className="mb-8">
|
||||
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><strong>Prop</strong></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Type</strong></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}><strong>Default</strong></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Description</strong></div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>heading</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Section heading text</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>features</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>CarouselFeatureItem[]</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Array of feature items with title and description</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>buttons</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>ButtonConfig[]</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>optional</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Array of button configurations (1-2 buttons supported, uses ButtonGroup)</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>slides</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>CarouselSlide[]</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Array of slide configurations</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '140px', flexShrink: 0 }}><code>background</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>'grey' | 'neutral' | 'yellow'</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}><code>'grey'</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Background color variant (adapts to light/dark mode)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 className="mb-4">CarouselSlide</h5>
|
||||
<div className="mb-6">
|
||||
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><strong>Prop</strong></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Type</strong></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}><strong>Required</strong></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Description</strong></div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>id</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string | number</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Unique identifier for the slide</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>imageSrc</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Image source URL</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>imageAlt</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Alt text for the image</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 className="mb-4">CarouselFeatureItem</h5>
|
||||
<div className="mb-6">
|
||||
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><strong>Prop</strong></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Type</strong></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}><strong>Required</strong></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Description</strong></div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>title</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Feature title text</div>
|
||||
</div>
|
||||
<Divider weight="thin" color="gray" />
|
||||
|
||||
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
|
||||
<div style={{ width: '120px', flexShrink: 0 }}><code>description</code></div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
|
||||
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
|
||||
<div style={{ flex: '1 1 0', minWidth: 0 }}>Feature description text</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Design References */}
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Design References</h2>
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<div>
|
||||
<strong>Figma:</strong>{' '}
|
||||
<a href="https://www.figma.com/design/OO2UYKTmDZ7PJIekfaCGAg/Section-Carousel---Feature-Image?node-id=19075-4106" target="_blank" rel="noopener noreferrer">
|
||||
Section Carousel - Feature Image
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Component Location:</strong>{' '}
|
||||
<code>shared/patterns/CarouselFeatured/</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Shared Button Component:</strong>{' '}
|
||||
<code>shared/components/CarouselButton/</code>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,413 +0,0 @@
|
||||
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
|
||||
import { FeatureSingleTopic } from 'shared/patterns/FeatureSingleTopic';
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: 'FeatureSingleTopic Pattern Showcase',
|
||||
description: 'Interactive showcase of the FeatureSingleTopic pattern with all variants, orientations, and button configurations.',
|
||||
},
|
||||
};
|
||||
|
||||
export default function FeatureSingleTopicShowcase() {
|
||||
// Placeholder image
|
||||
const placeholderImage = '/img/demo-bg.png';
|
||||
|
||||
return (
|
||||
<div className="landing">
|
||||
<div className="overflow-hidden">
|
||||
{/* Hero Section */}
|
||||
<section className="my-5 text-center">
|
||||
<div className="col-lg-8 mx-auto">
|
||||
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
|
||||
<h1 className="mb-4">FeatureSingleTopic Pattern</h1>
|
||||
<p className="longform">
|
||||
A feature section pattern that pairs a title and description with a media element
|
||||
in a two-column layout. Supports two variants (default and accentSurface) and
|
||||
left/right orientation for flexible content positioning.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Variant Section */}
|
||||
<PageGrid className="my-5">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Variants</h2>
|
||||
<p className="mb-4">
|
||||
The component supports two variants that control the title section background:
|
||||
</p>
|
||||
<ul className="mb-6">
|
||||
<li><strong>default:</strong> No background on title section</li>
|
||||
<li><strong>accentSurface:</strong> Gray background (#E6EAF0) on title section</li>
|
||||
</ul>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Default Variant */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>Default Variant</strong> - <code>variant="default"</code>
|
||||
<br />
|
||||
<small className="text-muted">No background on title section. Clean, minimal look.</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureSingleTopic
|
||||
variant="default"
|
||||
orientation="left"
|
||||
title="Developer Spotlight"
|
||||
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
|
||||
media={{ src: placeholderImage, alt: "Feature illustration" }}
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "#start" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* AccentSurface Variant */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>AccentSurface Variant</strong> - <code>variant="accentSurface"</code>
|
||||
<br />
|
||||
<small className="text-muted">
|
||||
Gray background (<code>$gray-200</code> / #E6EAF0) on title section.
|
||||
</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureSingleTopic
|
||||
variant="accentSurface"
|
||||
orientation="left"
|
||||
title="Developer Spotlight"
|
||||
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
|
||||
media={{ src: placeholderImage, alt: "Feature illustration" }}
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "#start" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Orientation Section */}
|
||||
<PageGrid className="my-5">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Orientation Variants</h2>
|
||||
<p className="mb-6">
|
||||
Control image/content position with the <code>orientation</code> prop.
|
||||
Use alternating orientations for visual variety on pages with multiple sections.
|
||||
</p>
|
||||
<div className="mb-4 p-3" style={{ backgroundColor: '#f0f3f7', borderRadius: '8px' }}>
|
||||
<strong>📱 Responsive Behavior:</strong>
|
||||
<ul className="mb-0 mt-2">
|
||||
<li><code>orientation="left"</code>: Image left, content right on desktop</li>
|
||||
<li><code>orientation="right"</code>: Image right, content left on desktop</li>
|
||||
<li><strong>Mobile/Tablet:</strong> Content always appears above image regardless of orientation</li>
|
||||
</ul>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Orientation Left */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>Orientation Left (Default)</strong> - <code>orientation="left"</code>
|
||||
<br />
|
||||
<small className="text-muted">
|
||||
Desktop: Image left, content right | Mobile/Tablet: Content above image
|
||||
</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureSingleTopic
|
||||
variant="default"
|
||||
orientation="left"
|
||||
title="Image on Left"
|
||||
description="This layout places the image on the left side and content on the right on desktop screens."
|
||||
media={{ src: placeholderImage, alt: "Left orientation" }}
|
||||
buttons={[
|
||||
{ label: "Primary Action", href: "#primary" },
|
||||
{ label: "Secondary", href: "#secondary" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Orientation Right */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>Orientation Right</strong> - <code>orientation="right"</code>
|
||||
<br />
|
||||
<small className="text-muted">
|
||||
Desktop: Image right, content left | Mobile/Tablet: Content above image
|
||||
</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureSingleTopic
|
||||
variant="accentSurface"
|
||||
orientation="right"
|
||||
title="Image on Right"
|
||||
description="This layout places the image on the right side and content on the left on desktop screens."
|
||||
media={{ src: placeholderImage, alt: "Right orientation" }}
|
||||
buttons={[
|
||||
{ label: "Primary Action", href: "#primary" },
|
||||
{ label: "Secondary", href: "#secondary" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Button Behavior Section */}
|
||||
<PageGrid className="my-5">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Button Behavior</h2>
|
||||
<p className="mb-4">
|
||||
The component automatically adjusts button rendering based on the number of links provided:
|
||||
</p>
|
||||
<ul className="mb-6">
|
||||
<li><strong>1 link:</strong> Primary or Secondary button (configurable via <code>singleButtonVariant</code> prop)</li>
|
||||
<li><strong>2 links:</strong> Primary + Tertiary buttons side by side</li>
|
||||
<li><strong>3+ links:</strong> All Tertiary buttons stacked</li>
|
||||
</ul>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* 1 Link - Primary */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>ex: 1 button</strong> - Primary Button (default)
|
||||
<br />
|
||||
<small className="text-muted">Single action rendered as a primary (filled) button.</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureSingleTopic
|
||||
variant="default"
|
||||
orientation="left"
|
||||
title="Developer Spotlight"
|
||||
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
|
||||
media={{ src: placeholderImage, alt: "Single button" }}
|
||||
buttons={[
|
||||
{ label: "Primary Link", href: "#start" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 1 Link - Secondary */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>ex: 1 button</strong> - Secondary Button
|
||||
<br />
|
||||
<small className="text-muted">Single action rendered as a secondary (outlined) button using <code>singleButtonVariant="secondary"</code>.</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureSingleTopic
|
||||
variant="default"
|
||||
orientation="left"
|
||||
title="Developer Spotlight"
|
||||
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
|
||||
media={{ src: placeholderImage, alt: "Single button secondary" }}
|
||||
singleButtonVariant="secondary"
|
||||
buttons={[
|
||||
{ label: "Secondary Link", href: "#start" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 2 Links */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>ex: 2 button</strong> - Primary + Tertiary Side by Side
|
||||
<br />
|
||||
<small className="text-muted">Primary and tertiary buttons displayed side by side on all breakpoints.</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureSingleTopic
|
||||
variant="default"
|
||||
orientation="left"
|
||||
title="Developer Spotlight"
|
||||
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
|
||||
media={{ src: placeholderImage, alt: "Two buttons" }}
|
||||
buttons={[
|
||||
{ label: "Primary Link", href: "#primary" },
|
||||
{ label: "Tertiary Link", href: "#tertiary" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 5 Links */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>ex: 5 button</strong> - All Tertiary Stacked
|
||||
<br />
|
||||
<small className="text-muted">3+ links render as all tertiary buttons stacked vertically.</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureSingleTopic
|
||||
variant="accentSurface"
|
||||
orientation="left"
|
||||
title="Developer Spotlight"
|
||||
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
|
||||
media={{ src: placeholderImage, alt: "Multiple buttons" }}
|
||||
buttons={[
|
||||
{ label: "Tertiary Link", href: "#link1" },
|
||||
{ label: "Tertiary Link", href: "#link2" },
|
||||
{ label: "Tertiary Link", href: "#link3" },
|
||||
{ label: "Tertiary Link", href: "#link4" },
|
||||
{ label: "Tertiary Link", href: "#link5" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 3 Links */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>ex: 3 button</strong> - All Tertiary Stacked
|
||||
<br />
|
||||
<small className="text-muted">3+ links render as all tertiary buttons stacked vertically.</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureSingleTopic
|
||||
variant="default"
|
||||
orientation="left"
|
||||
title="Developer Spotlight"
|
||||
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
|
||||
media={{ src: placeholderImage, alt: "Three buttons" }}
|
||||
buttons={[
|
||||
{ label: "Tertiary Link", href: "#link1" },
|
||||
{ label: "Tertiary Link", href: "#link2" },
|
||||
{ label: "Tertiary Link", href: "#link3" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Alternating Pattern Example */}
|
||||
<PageGrid className="my-5">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Alternating Pattern</h2>
|
||||
<p className="mb-6">
|
||||
Use alternating orientations and variants to create visual rhythm on feature-heavy pages.
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<FeatureSingleTopic
|
||||
variant="default"
|
||||
orientation="left"
|
||||
title="First Feature"
|
||||
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products."
|
||||
buttons={[{ label: "Learn More", href: "#learn" }]}
|
||||
media={{ src: placeholderImage, alt: "First feature" }}
|
||||
/>
|
||||
|
||||
<FeatureSingleTopic
|
||||
variant="accentSurface"
|
||||
orientation="right"
|
||||
title="Second Feature"
|
||||
description="Build powerful applications on XRPL with comprehensive documentation and tools."
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "#start" },
|
||||
{ label: "Documentation", href: "#docs" }
|
||||
]}
|
||||
media={{ src: placeholderImage, alt: "Second feature" }}
|
||||
/>
|
||||
|
||||
<FeatureSingleTopic
|
||||
variant="default"
|
||||
orientation="left"
|
||||
title="Third Feature"
|
||||
description="Scale your business with blockchain technology and enterprise-grade solutions."
|
||||
buttons={[
|
||||
{ label: "Contact Sales", href: "#contact" },
|
||||
{ label: "View Plans", href: "#plans" }
|
||||
]}
|
||||
media={{ src: placeholderImage, alt: "Third feature" }}
|
||||
/>
|
||||
|
||||
{/* Design References */}
|
||||
<PageGrid className="my-5">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Design References</h2>
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<div>
|
||||
<strong>Figma Design (Default):</strong>{' '}
|
||||
<a href="https://www.figma.com/design/sg6T5EptbN0V2olfCSHzcx/Section-Feature---Single-Topic?node-id=18030-2250&m=dev" target="_blank" rel="noopener noreferrer">
|
||||
Section Feature - Single Topic (Default Variant)
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Figma Design (AccentSurface):</strong>{' '}
|
||||
<a href="https://www.figma.com/design/sg6T5EptbN0V2olfCSHzcx/Section-Feature---Single-Topic?node-id=18030-2251&m=dev" target="_blank" rel="noopener noreferrer">
|
||||
Section Feature - Single Topic (AccentSurface Variant)
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Component Location:</strong>{' '}
|
||||
<code>shared/patterns/FeatureSingleTopic/</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Color Tokens:</strong>{' '}
|
||||
<code>styles/_colors.scss</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Typography:</strong>{' '}
|
||||
<code>styles/_font.scss</code>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,427 +0,0 @@
|
||||
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
|
||||
import { FeatureTwoColumn } from 'shared/patterns/FeatureTwoColumn';
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: 'FeatureTwoColumn Pattern Showcase',
|
||||
description: 'Interactive showcase of the FeatureTwoColumn pattern with all color variants, arrangements, and button configurations.',
|
||||
},
|
||||
};
|
||||
|
||||
export default function FeatureTwoColumnShowcase() {
|
||||
// Placeholder image
|
||||
const placeholderImage = '/img/demo-bg.png';
|
||||
|
||||
return (
|
||||
<div className="landing">
|
||||
<div className="overflow-hidden">
|
||||
{/* Hero Section */}
|
||||
<section className="my-5 text-center">
|
||||
<div className="col-lg-8 mx-auto">
|
||||
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
|
||||
<h1 className="mb-4">FeatureTwoColumn Pattern</h1>
|
||||
<p className="longform">
|
||||
A feature section pattern that pairs editorial content with a media element
|
||||
in a two-column layout. Supports four color themes, left/right arrangements,
|
||||
and automatic button configuration based on link count.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Button Behavior Section */}
|
||||
<PageGrid className="my-5">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Button Behavior</h2>
|
||||
<p className="mb-4">
|
||||
The component uses the ButtonGroup pattern which automatically adjusts button rendering based on the number of links provided:
|
||||
</p>
|
||||
<ul className="mb-6">
|
||||
<li><strong>1 link:</strong> Secondary button</li>
|
||||
<li><strong>2 links:</strong> Primary + Tertiary buttons (responsive layout)</li>
|
||||
<li><strong>3+ links:</strong> All Tertiary buttons in block layout (vertical on all screen sizes)</li>
|
||||
</ul>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* 1 Link - Secondary Button */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>1 Link</strong> - Secondary Button
|
||||
<br />
|
||||
<small className="text-muted">Single action rendered as a secondary (outline) button.</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureTwoColumn
|
||||
color="lilac"
|
||||
arrange="left"
|
||||
title="Institutions"
|
||||
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
|
||||
links={[
|
||||
{ label: "Secondary Link", href: "#link1" }
|
||||
]}
|
||||
media={{ src: placeholderImage, alt: "Feature illustration" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 2 Links - Primary + Tertiary */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>2 Links</strong> - Primary + Tertiary Buttons
|
||||
<br />
|
||||
<small className="text-muted">Primary action with a secondary tertiary link.</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureTwoColumn
|
||||
color="neutral"
|
||||
arrange="left"
|
||||
title="Institutions"
|
||||
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
|
||||
links={[
|
||||
{ label: "Primary Link", href: "#link1" },
|
||||
{ label: "Tertiary Link", href: "#link2" }
|
||||
]}
|
||||
media={{ src: placeholderImage, alt: "Feature illustration" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 5 Links - Multiple Tertiary */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>5 Links</strong> - Multiple Links Configuration
|
||||
<br />
|
||||
<small className="text-muted">Primary + Tertiary in first row, Secondary below, remaining as Tertiary list.</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureTwoColumn
|
||||
color="neutral"
|
||||
arrange="left"
|
||||
title="Institutions"
|
||||
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
|
||||
links={[
|
||||
{ label: "Primary Link", href: "#link1" },
|
||||
{ label: "Tertiary Link", href: "#link2" },
|
||||
{ label: "Secondary Link", href: "#link3" },
|
||||
{ label: "Tertiary Link", href: "#link4" },
|
||||
{ label: "Tertiary Link", href: "#link5" }
|
||||
]}
|
||||
media={{ src: placeholderImage, alt: "Feature illustration" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Color Variants Section */}
|
||||
<PageGrid className="my-5">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Color Variants</h2>
|
||||
<p className="mb-6">
|
||||
Four color themes available: neutral, lilac, yellow, and green.
|
||||
Each adapts automatically for light and dark modes.
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Neutral Variant */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>Neutral</strong> - <code>color="neutral"</code>
|
||||
<br />
|
||||
<small className="text-muted">
|
||||
Light: <code>$gray-100</code> (#F0F3F7) | Dark: <code>$gray-200</code> (#E6EAF0)
|
||||
<br />
|
||||
From <code>styles/_colors.scss</code>
|
||||
</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureTwoColumn
|
||||
color="neutral"
|
||||
arrange="left"
|
||||
title="Institutions"
|
||||
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
|
||||
links={[
|
||||
{ label: "Get Started", href: "#start" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
media={{ src: placeholderImage, alt: "Neutral theme" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Lilac Variant */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>Lilac</strong> - <code>color="lilac"</code>
|
||||
<br />
|
||||
<small className="text-muted">
|
||||
Light: <code>$lilac-200</code> (#D9CAFF) | Dark: <code>$lilac-200</code> (#D9CAFF)
|
||||
<br />
|
||||
From <code>styles/_colors.scss</code>
|
||||
</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureTwoColumn
|
||||
color="lilac"
|
||||
arrange="left"
|
||||
title="Institutions"
|
||||
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
|
||||
links={[
|
||||
{ label: "Get Started", href: "#start" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
media={{ src: placeholderImage, alt: "Lilac theme" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Yellow Variant */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>Yellow</strong> - <code>color="yellow"</code>
|
||||
<br />
|
||||
<small className="text-muted">
|
||||
Light: <code>$yellow-100</code> (#F3F1EB) | Dark: <code>$yellow-100</code> (#F3F1EB)
|
||||
<br />
|
||||
From <code>styles/_colors.scss</code>
|
||||
</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureTwoColumn
|
||||
color="yellow"
|
||||
arrange="left"
|
||||
title="Institutions"
|
||||
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
|
||||
links={[
|
||||
{ label: "Get Started", href: "#start" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
media={{ src: placeholderImage, alt: "Yellow theme" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Green Variant */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>Green</strong> - <code>color="green"</code>
|
||||
<br />
|
||||
<small className="text-muted">
|
||||
Light: <code>$green-300</code> (#21E46B) | Dark: <code>$green-300</code> (#21E46B)
|
||||
<br />
|
||||
From <code>styles/_colors.scss</code>
|
||||
</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureTwoColumn
|
||||
color="green"
|
||||
arrange="left"
|
||||
title="Institutions"
|
||||
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
|
||||
links={[
|
||||
{ label: "Get Started", href: "#start" },
|
||||
{ label: "Learn More", href: "#learn" }
|
||||
]}
|
||||
media={{ src: placeholderImage, alt: "Green theme" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Arrangement Section */}
|
||||
<PageGrid className="my-5">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Arrangement Variants</h2>
|
||||
<p className="mb-6">
|
||||
Control content position with the <code>arrange</code> prop.
|
||||
Use alternating arrangements for visual variety on pages with multiple sections.
|
||||
</p>
|
||||
<div className="mb-4 p-3" style={{ backgroundColor: '#f0f3f7', borderRadius: '8px' }}>
|
||||
<strong>📱 Responsive Behavior:</strong>
|
||||
<ul className="mb-0 mt-2">
|
||||
<li><code>arrange="left"</code>: Content above media on mobile/tablet, content left on desktop</li>
|
||||
<li><code>arrange="right"</code>: Media above content on mobile/tablet, content right on desktop</li>
|
||||
</ul>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
{/* Arrange Left */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>Arrange Left (Default)</strong> - <code>arrange="left"</code>
|
||||
<br />
|
||||
<small className="text-muted">
|
||||
Desktop: Content left, media right | Mobile/Tablet: Content above media
|
||||
</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureTwoColumn
|
||||
color="lilac"
|
||||
arrange="left"
|
||||
title="Content Left"
|
||||
description="This content appears on the left side of the layout on desktop, and above the media on mobile/tablet. This is the default arrangement."
|
||||
links={[
|
||||
{ label: "Primary", href: "#primary" },
|
||||
{ label: "Learn More", href: "#secondary" }
|
||||
]}
|
||||
media={{ src: placeholderImage, alt: "Left arrangement" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Arrange Right */}
|
||||
<div className="mb-5">
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<div className="mb-3">
|
||||
<strong>Arrange Right</strong> - <code>arrange="right"</code>
|
||||
<br />
|
||||
<small className="text-muted">
|
||||
Desktop: Content right, media left | Mobile/Tablet: Media above content
|
||||
</small>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeatureTwoColumn
|
||||
color="yellow"
|
||||
arrange="right"
|
||||
title="Content Right"
|
||||
description="This content appears on the right side on desktop, and below the media on mobile/tablet. The media-first approach works well for visual hierarchy."
|
||||
links={[
|
||||
{ label: "Primary", href: "#primary" },
|
||||
{ label: "Learn More", href: "#secondary" }
|
||||
]}
|
||||
media={{ src: placeholderImage, alt: "Right arrangement" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Alternating Pattern Example */}
|
||||
<PageGrid className="my-5">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Alternating Pattern</h2>
|
||||
<p className="mb-6">
|
||||
Use alternating arrangements and colors to create visual rhythm on feature-heavy pages.
|
||||
</p>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
<FeatureTwoColumn
|
||||
color="neutral"
|
||||
arrange="left"
|
||||
title="First Feature"
|
||||
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products."
|
||||
links={[{ label: "Learn More", href: "#learn" }]}
|
||||
media={{ src: placeholderImage, alt: "First feature" }}
|
||||
/>
|
||||
|
||||
<FeatureTwoColumn
|
||||
color="lilac"
|
||||
arrange="right"
|
||||
title="Second Feature"
|
||||
description="Build powerful applications on XRPL with comprehensive documentation and tools."
|
||||
links={[
|
||||
{ label: "Get Started", href: "#start" },
|
||||
{ label: "Documentation", href: "#docs" }
|
||||
]}
|
||||
media={{ src: placeholderImage, alt: "Second feature" }}
|
||||
/>
|
||||
|
||||
<FeatureTwoColumn
|
||||
color="yellow"
|
||||
arrange="left"
|
||||
title="Third Feature"
|
||||
description="Scale your business with blockchain technology and enterprise-grade solutions."
|
||||
links={[
|
||||
{ label: "Contact Sales", href: "#contact" },
|
||||
{ label: "View Plans", href: "#plans" }
|
||||
]}
|
||||
media={{ src: placeholderImage, alt: "Third feature" }}
|
||||
/>
|
||||
|
||||
<FeatureTwoColumn
|
||||
color="green"
|
||||
arrange="right"
|
||||
title="Fourth Feature"
|
||||
description="Join thousands of developers building the future of finance on XRPL."
|
||||
links={[
|
||||
{ label: "Start Building", href: "#build" },
|
||||
{ label: "Tutorials", href: "#tutorials" },
|
||||
{ label: "API Reference", href: "#api" }
|
||||
]}
|
||||
media={{ src: placeholderImage, alt: "Fourth feature" }}
|
||||
/>
|
||||
|
||||
{/* Design References */}
|
||||
<PageGrid className="my-5">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={12}>
|
||||
<h2 className="h4 mb-6">Design References</h2>
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<div>
|
||||
<strong>Figma Design:</strong>{' '}
|
||||
<a href="https://www.figma.com/design/3tmqxMrEvOVvpYhgOCxv2D/Pattern-Feature---Two-Column?node-id=20017-3501&m=dev" target="_blank" rel="noopener noreferrer">
|
||||
Pattern - Feature - Two Column (Figma)
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Component Location:</strong>{' '}
|
||||
<code>shared/patterns/FeatureTwoColumn/</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Color Tokens:</strong>{' '}
|
||||
<code>styles/_colors.scss</code>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Typography:</strong>{' '}
|
||||
<code>styles/_font.scss</code>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,495 +0,0 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
PageGrid,
|
||||
PageGridRow,
|
||||
PageGridCol,
|
||||
} from "shared/components/PageGrid/page-grid";
|
||||
import FeaturedVideoHero from "shared/patterns/FeaturedVideoHero/FeaturedVideoHero";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: "FeaturedVideoHero Pattern Showcase",
|
||||
description:
|
||||
"Interactive showcase of the FeaturedVideoHero pattern with video hero, CTAs, and responsive behavior.",
|
||||
},
|
||||
};
|
||||
|
||||
const DemoCaption = ({
|
||||
title,
|
||||
description,
|
||||
code,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
code?: string;
|
||||
}) => (
|
||||
<div className="mb-6">
|
||||
<h3 className="h4 mb-4">{title}</h3>
|
||||
{description && <p className="mb-6">{description}</p>}
|
||||
{code && (
|
||||
<div
|
||||
className="mb-6 p-4 bg-light br-4 text-black"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0, whiteSpace: "pre-wrap", color: "#000" }}>
|
||||
{code}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const placeholderVideo =
|
||||
"https://cdn.sanity.io/files/ior4a5y3/production/6e2fcba46e3f045a5570c86fd5d20d5ba93d6aad.mp4";
|
||||
|
||||
export default function FeaturedVideoHeroShowcase() {
|
||||
const videoRef = React.useRef<HTMLVideoElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (videoRef.current) {
|
||||
console.log("FeaturedVideoHero video element:", videoRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="landing">
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="text-center mb-26">
|
||||
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
|
||||
<h1 className="h2 mb-4">FeaturedVideoHero Pattern</h1>
|
||||
<p className="longform">
|
||||
A page-level hero pattern featuring a headline, optional
|
||||
subtitle, call-to-action buttons, and a featured video. The
|
||||
video uses native HTML video element props and is displayed in a
|
||||
responsive two-column layout with content on the left and video
|
||||
on the right.
|
||||
</p>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Basic Usage */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<DemoCaption
|
||||
title="Basic Usage with Video"
|
||||
description="The simplest implementation with a headline, subtitle, primary CTA, and video. This example assigns a ref to the video element and logs it to the console on mount."
|
||||
code={`const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (videoRef.current) {
|
||||
console.log("FeaturedVideoHero video element:", videoRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
<FeaturedVideoHero
|
||||
headline="Build on XRPL"
|
||||
subtitle={
|
||||
<>
|
||||
<p>Issue, manage, and trade real-world assets without needing to build smart contracts.</p>
|
||||
<p>XRP Ledger's built-in functionality and compliance-enabling features allow asset tokenization without additional layers of complexity.</p>
|
||||
</>
|
||||
}
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" }
|
||||
]}
|
||||
videoElement={{
|
||||
ref: videoRef,
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true
|
||||
}}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeaturedVideoHero
|
||||
headline="Build on XRPL"
|
||||
subtitle={
|
||||
<>
|
||||
<p>
|
||||
Issue, manage, and trade real-world assets without needing to
|
||||
build smart contracts.
|
||||
</p>
|
||||
<p>
|
||||
XRP Ledger's built-in functionality and compliance-enabling
|
||||
features allow asset tokenization without additional layers of
|
||||
complexity.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
videoElement={{
|
||||
ref: videoRef,
|
||||
src: placeholderVideo,
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
<PageGrid className="py-26">
|
||||
{/* Primary + Secondary CTA */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<DemoCaption
|
||||
title="Primary and Secondary CTAs"
|
||||
description="Include both primary and secondary call-to-action buttons."
|
||||
code={`<FeaturedVideoHero
|
||||
headline="Real-world asset tokenization"
|
||||
subtitle="Learn how to issue crypto tokens and build tokenization solutions."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" },
|
||||
{ children: "Learn More", href: "/about" }
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/tokenization.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true
|
||||
}}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeaturedVideoHero
|
||||
headline="Real-world asset tokenization"
|
||||
subtitle="Learn how to issue crypto tokens and build tokenization solutions."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" },
|
||||
{ children: "Learn More", href: "/about" },
|
||||
]}
|
||||
videoElement={{
|
||||
src: placeholderVideo,
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
<PageGrid className="py-26">
|
||||
{/* Video with extended props */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<DemoCaption
|
||||
title="Extended Video Props"
|
||||
description="Use native video element props for controls, preload, poster, etc."
|
||||
code={`<FeaturedVideoHero
|
||||
headline="Watch and Learn"
|
||||
subtitle="Explore our video tutorials and guides."
|
||||
callsToAction={[
|
||||
{ children: "Watch Tutorials", href: "/tutorials" }
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
controls: true,
|
||||
preload: "metadata"
|
||||
}}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeaturedVideoHero
|
||||
headline="Watch and Learn"
|
||||
subtitle="Explore our video tutorials and guides."
|
||||
callsToAction={[{ children: "Watch Tutorials", href: "/tutorials" }]}
|
||||
videoElement={{
|
||||
src: placeholderVideo,
|
||||
autoPlay: false,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
controls: true,
|
||||
preload: "metadata",
|
||||
}}
|
||||
/>
|
||||
<PageGrid className="py-26">
|
||||
{/* Validation Examples */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Validation Examples</h2>
|
||||
<p className="mb-6">
|
||||
The component includes development-time validation that logs
|
||||
warnings to the console when required props (headline,
|
||||
videoElement) are missing. The component will return null and
|
||||
not render when validation fails. The callsToAction prop is
|
||||
optional; when provided, at least one non-empty CTA is needed to
|
||||
show the CTA section.
|
||||
</p>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Primary CTA Only */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<DemoCaption
|
||||
title="Primary CTA Only (No Secondary)"
|
||||
description="The secondary CTA is optional. When omitted, only the primary CTA button is displayed."
|
||||
code={`<FeaturedVideoHero
|
||||
headline="Single Call to Action"
|
||||
subtitle="Focus on one primary action for better conversion."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" }
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true
|
||||
}}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeaturedVideoHero
|
||||
headline="Single Call to Action"
|
||||
subtitle="Focus on one primary action for better conversion."
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
videoElement={{
|
||||
src: placeholderVideo,
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
<PageGrid className="py-26">
|
||||
{/* Without subtitle */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<DemoCaption
|
||||
title="Without Subtitle"
|
||||
description="Subtitle is optional. The component renders without a subtitle section when omitted."
|
||||
code={`<FeaturedVideoHero
|
||||
headline="Headline Only"
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" }
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true
|
||||
}}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
<FeaturedVideoHero
|
||||
headline="Headline Only"
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
videoElement={{
|
||||
src: placeholderVideo,
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
<PageGrid className="py-26">
|
||||
{/* Props Documentation */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Props Documentation</h2>
|
||||
|
||||
<h4 className="h5 mb-4">FeaturedVideoHeroProps</h4>
|
||||
<div className="mb-6">
|
||||
<ul>
|
||||
<li>
|
||||
<code>headline</code> (required) -{" "}
|
||||
<code>React.ReactNode</code> - Hero headline text
|
||||
</li>
|
||||
<li>
|
||||
<code>subtitle</code> (optional) -{" "}
|
||||
<code>React.ReactNode</code> - Hero subtitle text
|
||||
</li>
|
||||
<li>
|
||||
<code>callsToAction</code> (optional) -{" "}
|
||||
<code>DesignConstrainedCallsToActions</code> - Array with
|
||||
primary CTA and optional secondary CTA. Omit or pass
|
||||
empty/non-rendering CTAs to hide the CTA section.
|
||||
</li>
|
||||
<li>
|
||||
<code>videoElement</code> (required) - Native{" "}
|
||||
<code><video></code> element props (e.g. src,
|
||||
autoPlay, loop, muted, playsInline, controls, preload,
|
||||
poster)
|
||||
</li>
|
||||
<li>
|
||||
<code>className</code> (optional) - <code>string</code> -
|
||||
Additional CSS classes for the header element
|
||||
</li>
|
||||
<li>
|
||||
All standard HTML <code><header></code> attributes are
|
||||
supported
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h4 className="h5 mb-4">Button Props (callsToAction)</h4>
|
||||
<p className="mb-4">
|
||||
The <code>callsToAction</code> prop accepts design-constrained
|
||||
Button props; <code>variant</code> and <code>color</code> are
|
||||
set by the component:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Primary CTA: <code>variant="primary"</code>,{" "}
|
||||
<code>color="green"</code>
|
||||
</li>
|
||||
<li>
|
||||
Secondary CTA: <code>variant="tertiary"</code>,{" "}
|
||||
<code>color="green"</code>
|
||||
</li>
|
||||
<li>
|
||||
All other Button props are supported (e.g.,{" "}
|
||||
<code>children</code>, <code>href</code>, <code>onClick</code>
|
||||
, etc.)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Code Examples */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Code Examples</h2>
|
||||
|
||||
<h4 className="h5 mb-4">Import</h4>
|
||||
<div
|
||||
className="p-4 bg-light br-4 mb-6"
|
||||
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
||||
>
|
||||
<pre style={{ margin: 0, color: "#000" }}>
|
||||
{`import { FeaturedVideoHero } from "shared/patterns/FeaturedVideoHero";`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h4 className="h5 mb-4">Basic Example</h4>
|
||||
<div
|
||||
className="p-4 bg-light br-4 mb-6"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
overflow: "auto",
|
||||
backgroundColor: "#1e1e1e",
|
||||
color: "#d4d4d4",
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
|
||||
{`<FeaturedVideoHero
|
||||
headline="Build on XRPL"
|
||||
subtitle={
|
||||
<>
|
||||
<p>Issue, manage, and trade real-world assets without needing to build smart contracts.</p>
|
||||
<p>XRP Ledger's built-in functionality and compliance-enabling features allow asset tokenization without additional layers of complexity.</p>
|
||||
</>
|
||||
}
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" }
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true
|
||||
}}
|
||||
/>`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h4 className="h5 mb-4">With Secondary CTA</h4>
|
||||
<div
|
||||
className="p-4 bg-light br-4 mb-6"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
overflow: "auto",
|
||||
backgroundColor: "#1e1e1e",
|
||||
color: "#d4d4d4",
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
|
||||
{`<FeaturedVideoHero
|
||||
headline="Real-world asset tokenization"
|
||||
subtitle="Learn how to issue crypto tokens and build solutions."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" },
|
||||
{ children: "Learn More", href: "/about" }
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/tokenization.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true
|
||||
}}
|
||||
/>`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Best Practices */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Best Practices</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Video format:</strong> Use MP4 with H.264 for broad
|
||||
compatibility. Keep file sizes reasonable for fast loading.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Autoplay:</strong> Use <code>muted</code> and{" "}
|
||||
<code>playsInline</code> with <code>autoPlay</code> for
|
||||
reliable autoplay on mobile.
|
||||
</li>
|
||||
<li>
|
||||
<strong>CTAs:</strong> Keep CTA text concise and
|
||||
action-oriented. Primary CTA should be the main action.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Headlines:</strong> Keep headlines concise and
|
||||
impactful. Use the subtitle for additional context.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Responsive design:</strong> The layout stacks on
|
||||
smaller screens; test across breakpoints to ensure video and
|
||||
content display correctly.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,769 +0,0 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
PageGrid,
|
||||
PageGridRow,
|
||||
PageGridCol,
|
||||
} from "shared/components/PageGrid/page-grid";
|
||||
import HeaderHeroPrimaryMedia from "shared/patterns/HeaderHeroPrimaryMedia/HeaderHeroPrimaryMedia";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: "HeaderHeroPrimaryMedia Pattern Showcase",
|
||||
description:
|
||||
"Interactive showcase of the HeaderHeroPrimaryMedia pattern with all variants, media types, and responsive behavior.",
|
||||
},
|
||||
};
|
||||
|
||||
// Demo component for code examples
|
||||
const CodeDemo = ({
|
||||
title,
|
||||
description,
|
||||
code,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
code?: string;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<div className="mb-26">
|
||||
<h3 className="h4 mb-4">{title}</h3>
|
||||
{description && <p className="mb-6">{description}</p>}
|
||||
{code && (
|
||||
<div
|
||||
className="mb-6 p-4 bg-light br-4 text-black"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0, whiteSpace: "pre-wrap", color: "#000" }}>
|
||||
{code}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
border: "1px dashed #ccc",
|
||||
padding: "16px",
|
||||
backgroundColor: "#f9f9f9",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Sample placeholder images
|
||||
const placeholderImage =
|
||||
"https://cdn.sanity.io/images/ior4a5y3/production/6e150606bc0a051a83b90aa830cc32854cc3f7df-2928x1920.jpg";
|
||||
const placeholderVideo =
|
||||
"https://cdn.sanity.io/files/ior4a5y3/production/6e2fcba46e3f045a5570c86fd5d20d5ba93d6aad.mp4";
|
||||
|
||||
// Sample custom animation component
|
||||
const SampleAnimation = () => (
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#0069ff",
|
||||
color: "white",
|
||||
fontSize: "24px",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Custom Animation Element
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function HeaderHeroPrimaryMediaShowcase() {
|
||||
return (
|
||||
<div className="landing">
|
||||
<PageGrid className="py-26">
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="text-center mb-26">
|
||||
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
|
||||
<h1 className="h2 mb-4">HeaderHeroPrimaryMedia Pattern</h1>
|
||||
<p className="longform">
|
||||
A page-level hero pattern featuring a headline, subtitle,
|
||||
call-to-action buttons, and a primary media element. The media
|
||||
supports images, videos, or custom React elements, all
|
||||
constrained to maintain a 9:16 aspect ratio with object-fit:
|
||||
cover.
|
||||
</p>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Basic Usage */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Basic Usage with Image Media"
|
||||
description="The simplest implementation with an image, headline, subtitle, and primary CTA."
|
||||
code={`<HeaderHeroPrimaryMedia
|
||||
headline="Build on XRPL"
|
||||
subtitle="Start developing today with our comprehensive developer tools and APIs."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" }
|
||||
]}
|
||||
media={{
|
||||
type: "image",
|
||||
src: "/img/hero.png",
|
||||
alt: "XRPL Development"
|
||||
}}
|
||||
/>`}
|
||||
>
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Build on XRPL"
|
||||
subtitle="Start developing today with our comprehensive developer tools and APIs."
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
media={{
|
||||
type: "image",
|
||||
src: placeholderImage,
|
||||
alt: "XRPL Development",
|
||||
}}
|
||||
/>
|
||||
</CodeDemo>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Primary + Secondary CTA */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Primary and Secondary CTAs"
|
||||
description="Include both primary and secondary call-to-action buttons."
|
||||
code={`<HeaderHeroPrimaryMedia
|
||||
headline="Real-world asset tokenization"
|
||||
subtitle="Learn how to issue crypto tokens and build tokenization solutions."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" },
|
||||
{ children: "Learn More", href: "/about" }
|
||||
]}
|
||||
media={{
|
||||
type: "image",
|
||||
src: "/img/tokenization.png",
|
||||
alt: "Tokenization"
|
||||
}}
|
||||
/>`}
|
||||
>
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Real-world asset tokenization"
|
||||
subtitle="Learn how to issue crypto tokens and build tokenization solutions."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" },
|
||||
{ children: "Learn More", href: "/about" },
|
||||
]}
|
||||
media={{
|
||||
type: "image",
|
||||
src: placeholderImage,
|
||||
alt: "Tokenization",
|
||||
}}
|
||||
/>
|
||||
</CodeDemo>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Video Media */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Video Media"
|
||||
description="Use video elements with native video props support. The video will maintain the 9:16 aspect ratio and object-fit: cover."
|
||||
code={`<HeaderHeroPrimaryMedia
|
||||
headline="Watch and Learn"
|
||||
subtitle="Explore our video tutorials and guides."
|
||||
callsToAction={[
|
||||
{ children: "Watch Tutorials", href: "/tutorials" }
|
||||
]}
|
||||
media={{
|
||||
type: "video",
|
||||
src: "/video/intro.mp4",
|
||||
alt: "Introduction video",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true
|
||||
}}
|
||||
/>`}
|
||||
>
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Watch and Learn"
|
||||
subtitle="Explore our video tutorials and guides."
|
||||
callsToAction={[
|
||||
{ children: "Watch Tutorials", href: "/tutorials" },
|
||||
]}
|
||||
media={{
|
||||
type: "video",
|
||||
src: placeholderVideo,
|
||||
alt: "Introduction video",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
</CodeDemo>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Custom Element Media */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Custom Element Media"
|
||||
description="Use custom React elements for animations, interactive components, or any custom media type."
|
||||
code={`<HeaderHeroPrimaryMedia
|
||||
headline="Interactive Experience"
|
||||
subtitle="Engage with our custom interactive media."
|
||||
callsToAction={[
|
||||
{ children: "Explore", href: "/interactive" }
|
||||
]}
|
||||
media={{
|
||||
type: "custom",
|
||||
element: <MyAnimationComponent />
|
||||
}}
|
||||
/>`}
|
||||
>
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Interactive Experience"
|
||||
subtitle="Engage with our custom interactive media."
|
||||
callsToAction={[{ children: "Explore", href: "/interactive" }]}
|
||||
media={{
|
||||
type: "custom",
|
||||
element: <SampleAnimation />,
|
||||
}}
|
||||
/>
|
||||
</CodeDemo>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Extended Image Props */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Extended Image Props"
|
||||
description="Leverage native img element props like loading, crossOrigin, etc. className and style are omitted from img props and only available on the container."
|
||||
code={`<HeaderHeroPrimaryMedia
|
||||
headline="Optimized Images"
|
||||
subtitle="Use native image attributes for performance and security."
|
||||
callsToAction={[
|
||||
{ children: "View Gallery", href: "/gallery" }
|
||||
]}
|
||||
media={{
|
||||
type: "image",
|
||||
src: "/img/gallery.jpg",
|
||||
alt: "Image gallery",
|
||||
loading: "lazy",
|
||||
crossOrigin: "anonymous",
|
||||
decoding: "async"
|
||||
}}
|
||||
/>`}
|
||||
>
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Optimized Images"
|
||||
subtitle="Use native image attributes for performance and security."
|
||||
callsToAction={[{ children: "View Gallery", href: "/gallery" }]}
|
||||
media={{
|
||||
type: "image",
|
||||
src: placeholderImage,
|
||||
alt: "Image gallery",
|
||||
loading: "lazy",
|
||||
decoding: "async",
|
||||
}}
|
||||
/>
|
||||
</CodeDemo>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Extended Video Props */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Extended Video Props"
|
||||
description="Use native video element props for controls, preload, poster, etc."
|
||||
code={`<HeaderHeroPrimaryMedia
|
||||
headline="Video Content"
|
||||
subtitle="Rich video experiences with full control."
|
||||
callsToAction={[
|
||||
{ children: "Watch Now", href: "/videos" }
|
||||
]}
|
||||
media={{
|
||||
type: "video",
|
||||
src: "/video/demo.mp4",
|
||||
alt: "Demo video",
|
||||
controls: true,
|
||||
preload: "metadata",
|
||||
poster: "/img/video-poster.jpg"
|
||||
}}
|
||||
/>`}
|
||||
>
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Video Content"
|
||||
subtitle="Rich video experiences with full control."
|
||||
callsToAction={[{ children: "Watch Now", href: "/videos" }]}
|
||||
media={{
|
||||
type: "video",
|
||||
src: placeholderVideo,
|
||||
alt: "Demo video",
|
||||
controls: true,
|
||||
preload: "metadata",
|
||||
}}
|
||||
/>
|
||||
</CodeDemo>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Missing Optional Fields - Validation Examples */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Validation Examples</h2>
|
||||
<p className="mb-6">
|
||||
The component includes development-time validation that logs
|
||||
warnings to the console when required props are missing. The
|
||||
component will still render, but you'll see warnings in the
|
||||
browser console.
|
||||
</p>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Missing Subtitle */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Missing Subtitle (Warning Example)"
|
||||
description="When subtitle is missing, a warning will be logged to the console. The component still renders but the subtitle area will be empty."
|
||||
code={`<HeaderHeroPrimaryMedia
|
||||
headline="Build on XRPL"
|
||||
subtitle={undefined}
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" }
|
||||
]}
|
||||
media={{
|
||||
type: "image",
|
||||
src: "/img/hero.png",
|
||||
alt: "XRPL Development"
|
||||
}}
|
||||
/>`}
|
||||
>
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Build on XRPL"
|
||||
subtitle={undefined as any}
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
media={{
|
||||
type: "image",
|
||||
src: placeholderImage,
|
||||
alt: "XRPL Development",
|
||||
}}
|
||||
/>
|
||||
</CodeDemo>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Missing Secondary CTA */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Primary CTA Only (No Secondary)"
|
||||
description="The secondary CTA is optional. When omitted, only the primary CTA button is displayed. This is the recommended pattern when you want a single, focused call-to-action."
|
||||
code={`<HeaderHeroPrimaryMedia
|
||||
headline="Single Call to Action"
|
||||
subtitle="Focus on one primary action for better conversion."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" }
|
||||
// No secondary CTA - this is valid
|
||||
]}
|
||||
media={{
|
||||
type: "image",
|
||||
src: "/img/hero.png",
|
||||
alt: "Single CTA example"
|
||||
}}
|
||||
/>`}
|
||||
>
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Single Call to Action"
|
||||
subtitle="Focus on one primary action for better conversion."
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
media={{
|
||||
type: "image",
|
||||
src: placeholderImage,
|
||||
alt: "Single CTA example",
|
||||
}}
|
||||
/>
|
||||
</CodeDemo>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Missing Media */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Missing Media (Warning Example)"
|
||||
description="When media is missing, a warning will be logged to the console. The component still renders but the media section will not be displayed."
|
||||
code={`<HeaderHeroPrimaryMedia
|
||||
headline="Content Without Media"
|
||||
subtitle="Sometimes you may want to focus purely on the content without media."
|
||||
callsToAction={[
|
||||
{ children: "Learn More", href: "/about" }
|
||||
]}
|
||||
media={undefined}
|
||||
/>`}
|
||||
>
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Content Without Media"
|
||||
subtitle="Sometimes you may want to focus purely on the content without media."
|
||||
callsToAction={[{ children: "Learn More", href: "/about" }]}
|
||||
media={undefined as any}
|
||||
/>
|
||||
</CodeDemo>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Design Constraints */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Design Constraints</h2>
|
||||
<p className="mb-6">
|
||||
The HeaderHeroPrimaryMedia pattern enforces specific design
|
||||
requirements to maintain visual consistency across all
|
||||
implementations:
|
||||
</p>
|
||||
<ul className="mb-6">
|
||||
<li>
|
||||
<strong>Aspect Ratio:</strong> All media maintains a 9:16
|
||||
aspect ratio (portrait orientation)
|
||||
</li>
|
||||
<li>
|
||||
<strong>Object Fit:</strong> Media uses{" "}
|
||||
<code>object-fit: cover</code> to fill the container while
|
||||
maintaining aspect ratio
|
||||
</li>
|
||||
<li>
|
||||
<strong>Responsive Behavior:</strong> The media container
|
||||
adapts responsively while maintaining the aspect ratio
|
||||
constraint
|
||||
</li>
|
||||
<li>
|
||||
<strong>Type Safety:</strong> TypeScript ensures proper media
|
||||
type discrimination and prop validation
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
className="p-4 bg-light br-4"
|
||||
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
||||
>
|
||||
<pre style={{ margin: 0, color: "#000" }}>
|
||||
{`.bds-header-hero-primary-media__media-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 9 / 16; /* Design requirement */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bds-header-hero-primary-media__media-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover; /* Ensures media covers container */
|
||||
object-position: center;
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Props Documentation */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Props Documentation</h2>
|
||||
|
||||
<h4 className="h5 mb-4">HeaderHeroPrimaryMediaProps</h4>
|
||||
<div className="mb-6">
|
||||
<ul>
|
||||
<li>
|
||||
<code>headline</code> (required) -{" "}
|
||||
<code>React.ReactNode</code> - Hero headline text
|
||||
</li>
|
||||
<li>
|
||||
<code>subtitle</code> (required) -{" "}
|
||||
<code>React.ReactNode</code> - Hero subtitle text
|
||||
</li>
|
||||
<li>
|
||||
<code>callsToAction</code> (required) -{" "}
|
||||
<code>[ButtonProps, ButtonProps?]</code> - Array with
|
||||
primary CTA (required) and optional secondary CTA
|
||||
</li>
|
||||
<li>
|
||||
<code>media</code> (required) - <code>HeaderHeroMedia</code>{" "}
|
||||
- Media element (image, video, or custom)
|
||||
</li>
|
||||
<li>
|
||||
<code>className</code> (optional) - <code>string</code> -
|
||||
Additional CSS classes for the header element
|
||||
</li>
|
||||
<li>
|
||||
All standard HTML <code><header></code> attributes are
|
||||
supported
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h4 className="h5 mb-4">HeaderHeroMedia Types</h4>
|
||||
<div className="mb-6">
|
||||
<h5 className="h6 mb-3">ImageMediaProps</h5>
|
||||
<ul className="mb-4">
|
||||
<li>
|
||||
<code>type: "image"</code> (required)
|
||||
</li>
|
||||
<li>
|
||||
<code>src: string</code> (required) - Image source URL
|
||||
</li>
|
||||
<li>
|
||||
<code>alt: string</code> (required) - Alt text for
|
||||
accessibility
|
||||
</li>
|
||||
<li>
|
||||
All native <code><img></code> props except{" "}
|
||||
<code>className</code> and <code>style</code>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h5 className="h6 mb-3">VideoMediaProps</h5>
|
||||
<ul className="mb-4">
|
||||
<li>
|
||||
<code>type: "video"</code> (required)
|
||||
</li>
|
||||
<li>
|
||||
<code>src: string</code> (required) - Video source URL
|
||||
</li>
|
||||
<li>
|
||||
<code>alt?: string</code> (optional) - Alt text for
|
||||
accessibility
|
||||
</li>
|
||||
<li>
|
||||
All native <code><video></code> props except{" "}
|
||||
<code>className</code> and <code>style</code>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h5 className="h6 mb-3">CustomMediaProps</h5>
|
||||
<ul>
|
||||
<li>
|
||||
<code>type: "custom"</code> (required)
|
||||
</li>
|
||||
<li>
|
||||
<code>element: React.ReactElement</code> (required) - Custom
|
||||
React element to render
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h4 className="h5 mb-4">Button Props (callsToAction)</h4>
|
||||
<p className="mb-4">
|
||||
The <code>callsToAction</code> prop accepts Button component
|
||||
props, but <code>variant</code> and <code>color</code> are
|
||||
automatically set:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Primary CTA: <code>variant="primary"</code>,{" "}
|
||||
<code>color="green"</code>
|
||||
</li>
|
||||
<li>
|
||||
Secondary CTA: <code>variant="tertiary"</code>,{" "}
|
||||
<code>color="green"</code>
|
||||
</li>
|
||||
<li>
|
||||
All other Button props are supported (e.g.,{" "}
|
||||
<code>children</code>, <code>href</code>, <code>onClick</code>
|
||||
, etc.)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Code Examples */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Code Examples</h2>
|
||||
|
||||
<h4 className="h5 mb-4">Import</h4>
|
||||
<div
|
||||
className="p-4 bg-light br-4 mb-6"
|
||||
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
||||
>
|
||||
<pre style={{ margin: 0, color: "#000" }}>
|
||||
{`import HeaderHeroPrimaryMedia from "shared/patterns/HeaderHeroPrimaryMedia/HeaderHeroPrimaryMedia";`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h4 className="h5 mb-4">Basic Example</h4>
|
||||
<div
|
||||
className="p-4 bg-light br-4 mb-6"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
overflow: "auto",
|
||||
backgroundColor: "#1e1e1e",
|
||||
color: "#d4d4d4",
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
|
||||
{`<HeaderHeroPrimaryMedia
|
||||
headline="Build on XRPL"
|
||||
subtitle="Start developing today with our comprehensive developer tools."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" }
|
||||
]}
|
||||
media={{
|
||||
type: "image",
|
||||
src: "/img/hero.png",
|
||||
alt: "XRPL Development"
|
||||
}}
|
||||
/>`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h4 className="h5 mb-4">With Secondary CTA</h4>
|
||||
<div
|
||||
className="p-4 bg-light br-4 mb-6"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
overflow: "auto",
|
||||
backgroundColor: "#1e1e1e",
|
||||
color: "#d4d4d4",
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
|
||||
{`<HeaderHeroPrimaryMedia
|
||||
headline="Real-world asset tokenization"
|
||||
subtitle="Learn how to issue crypto tokens and build solutions."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" },
|
||||
{ children: "Learn More", href: "/about" }
|
||||
]}
|
||||
media={{
|
||||
type: "image",
|
||||
src: "/img/tokenization.png",
|
||||
alt: "Tokenization"
|
||||
}}
|
||||
/>`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h4 className="h5 mb-4">Video Media</h4>
|
||||
<div
|
||||
className="p-4 bg-light br-4 mb-6"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
overflow: "auto",
|
||||
backgroundColor: "#1e1e1e",
|
||||
color: "#d4d4d4",
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
|
||||
{`<HeaderHeroPrimaryMedia
|
||||
headline="Watch and Learn"
|
||||
subtitle="Explore our video tutorials."
|
||||
callsToAction={[
|
||||
{ children: "Watch Tutorials", href: "/tutorials" }
|
||||
]}
|
||||
media={{
|
||||
type: "video",
|
||||
src: "/video/intro.mp4",
|
||||
alt: "Introduction video",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true
|
||||
}}
|
||||
/>`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h4 className="h5 mb-4">Custom Element</h4>
|
||||
<div
|
||||
className="p-4 bg-light br-4 mb-6"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
overflow: "auto",
|
||||
backgroundColor: "#1e1e1e",
|
||||
color: "#d4d4d4",
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
|
||||
{`<HeaderHeroPrimaryMedia
|
||||
headline="Interactive Experience"
|
||||
subtitle="Engage with custom media."
|
||||
callsToAction={[
|
||||
{ children: "Explore", href: "/interactive" }
|
||||
]}
|
||||
media={{
|
||||
type: "custom",
|
||||
element: <MyAnimationComponent />
|
||||
}}
|
||||
/>`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Best Practices */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Best Practices</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Media Selection:</strong> Choose media that works well
|
||||
in a 9:16 portrait aspect ratio. Images and videos will be
|
||||
cropped to fit.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Alt Text:</strong> Always provide meaningful alt text
|
||||
for images. For videos, use the <code>alt</code> prop for
|
||||
accessibility.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Performance:</strong> Use <code>loading="lazy"</code>{" "}
|
||||
for images below the fold, and optimize video file sizes.
|
||||
</li>
|
||||
<li>
|
||||
<strong>CTAs:</strong> Keep CTA text concise and
|
||||
action-oriented. Primary CTA should be the main action.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Headlines:</strong> Keep headlines concise and
|
||||
impactful. Use the subtitle for additional context.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Type Safety:</strong> Leverage TypeScript's
|
||||
discriminated unions for type-safe media selection.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Responsive Design:</strong> Test your implementation
|
||||
across all breakpoints to ensure media displays correctly.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { LinkSmallGrid } from "shared/patterns/LinkSmallGrid";
|
||||
import { Divider } from "shared/components/Divider";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: 'LinkSmallGrid Component Showcase',
|
||||
description: "A comprehensive showcase of the LinkSmallGrid pattern component with responsive grid layouts, color variants, and usage examples.",
|
||||
}
|
||||
};
|
||||
|
||||
export default function LinkSmallGridShowcase() {
|
||||
const handleClick = (message: string) => {
|
||||
console.log(`Link clicked: ${message}`);
|
||||
};
|
||||
|
||||
// Sample links for demonstrations
|
||||
const sampleLinks = [
|
||||
{ label: "Documentation", href: "/docs" },
|
||||
{ label: "Tutorials", href: "/tutorials" },
|
||||
{ label: "API Reference", href: "/api" },
|
||||
{ label: "Examples", href: "/examples" },
|
||||
{ label: "Best Practices", href: "/best-practices" },
|
||||
{ label: "Tools", href: "/tools" },
|
||||
{ label: "Resources", href: "/resources" },
|
||||
{ label: "Community", href: "/community" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="landing">
|
||||
<div className="overflow-hidden">
|
||||
{/* Hero Section */}
|
||||
<section className="py-26 text-center">
|
||||
<div className="col-lg-8 mx-auto">
|
||||
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
|
||||
<h1 className="mb-4">LinkSmallGrid Pattern</h1>
|
||||
<p className="longform">
|
||||
A responsive grid section pattern for displaying navigational links using TileLink components.
|
||||
Features a heading, optional description, and a grid of clickable tiles with 2 color variants
|
||||
and full light/dark mode support.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Divider color="gray" />
|
||||
|
||||
{/* Full Example - Gray Variant */}
|
||||
<LinkSmallGrid
|
||||
variant="gray"
|
||||
heading="Quick Links"
|
||||
description="Navigate to key sections of our documentation and resources."
|
||||
links={sampleLinks}
|
||||
/>
|
||||
|
||||
<Divider color="gray" />
|
||||
|
||||
{/* Full Example - Lilac Variant */}
|
||||
<LinkSmallGrid
|
||||
variant="lilac"
|
||||
heading="Get Started"
|
||||
description="Explore tutorials and guides to begin your journey with XRPL."
|
||||
links={sampleLinks.slice(0, 4)}
|
||||
/>
|
||||
|
||||
<Divider color="gray" />
|
||||
|
||||
{/* Gray Variant - Heading Only */}
|
||||
<LinkSmallGrid
|
||||
variant="gray"
|
||||
heading="Developer Resources"
|
||||
links={sampleLinks.slice(0, 6)}
|
||||
/>
|
||||
|
||||
<Divider color="gray" />
|
||||
|
||||
{/* Lilac Variant - With Click Handlers */}
|
||||
<LinkSmallGrid
|
||||
variant="lilac"
|
||||
heading="Interactive Examples"
|
||||
description="Click any tile to see the onClick handler in action (check console)."
|
||||
links={[
|
||||
{ label: "Example 1", onClick: () => handleClick('Example 1') },
|
||||
{ label: "Example 2", onClick: () => handleClick('Example 2') },
|
||||
{ label: "Example 3", onClick: () => handleClick('Example 3') },
|
||||
{ label: "Example 4", onClick: () => handleClick('Example 4') },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Divider color="gray" />
|
||||
|
||||
{/* Different Link Counts */}
|
||||
<section className=" py-26">
|
||||
<div className="d-flex flex-column-reverse mb-10">
|
||||
<h2 className="h4 mb-8">Different Link Counts</h2>
|
||||
<h6 className="eyebrow mb-3">Responsive Behavior</h6>
|
||||
</div>
|
||||
|
||||
<div className="mb-10">
|
||||
<h6 className="mb-4">2 Links</h6>
|
||||
<LinkSmallGrid
|
||||
variant="gray"
|
||||
heading="Featured Sections"
|
||||
links={sampleLinks.slice(0, 2)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-10">
|
||||
<h6 className="mb-4">3 Links</h6>
|
||||
<LinkSmallGrid
|
||||
variant="lilac"
|
||||
heading="Core Topics"
|
||||
links={sampleLinks.slice(0, 3)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-10">
|
||||
<h6 className="mb-4">5 Links</h6>
|
||||
<LinkSmallGrid
|
||||
variant="gray"
|
||||
heading="Learning Paths"
|
||||
description="Choose a path to start learning."
|
||||
links={sampleLinks.slice(0, 5)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-10">
|
||||
<h6 className="mb-4">12 Links</h6>
|
||||
<LinkSmallGrid
|
||||
variant="lilac"
|
||||
heading="Complete Navigation"
|
||||
description="Full grid with multiple rows."
|
||||
links={[
|
||||
...sampleLinks,
|
||||
{ label: "Blog", href: "/blog" },
|
||||
{ label: "Events", href: "/events" },
|
||||
{ label: "Newsletter", href: "/newsletter" },
|
||||
{ label: "Support", href: "/support" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Divider color="gray" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@ import {
|
||||
PageGridRow,
|
||||
PageGridCol,
|
||||
} from "shared/components/PageGrid/page-grid";
|
||||
import { SmallTilesSection } from "shared/patterns/SmallTilesSection/SmallTilesSection";
|
||||
import { SmallTilesSection } from "shared/components/SmallTilesSection/SmallTilesSection";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
|
||||
@@ -1,559 +0,0 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
PageGrid,
|
||||
PageGridRow,
|
||||
PageGridCol,
|
||||
} from "shared/components/PageGrid/page-grid";
|
||||
import StandardCardGroupSection, {
|
||||
type StandardCardPropsWithoutVariant,
|
||||
} from "shared/patterns/StandardCardGroupSection/StandardCardGroupSection";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: "StandardCardGroupSection Pattern Showcase",
|
||||
description:
|
||||
"Interactive showcase of the StandardCardGroupSection pattern with all variants, responsive behavior, and composition examples.",
|
||||
},
|
||||
};
|
||||
|
||||
// Demo component for code examples
|
||||
const CodeDemo = ({
|
||||
title,
|
||||
description,
|
||||
code,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
code?: string;
|
||||
children?: React.ReactNode;
|
||||
}) => (
|
||||
<div className="mb-26">
|
||||
<h3 className="h4 mb-4">{title}</h3>
|
||||
{description && <p className="mb-6">{description}</p>}
|
||||
{code && (
|
||||
<div
|
||||
className="mb-6 p-4 bg-light br-4 text-black"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0, whiteSpace: "pre-wrap", color: "#000" }}>
|
||||
{code}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{children && (
|
||||
<div
|
||||
style={{
|
||||
border: "1px dashed #ccc",
|
||||
padding: "16px",
|
||||
backgroundColor: "#f9f9f9",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Module-level card data to avoid recreating on each render
|
||||
const BASIC_CARDS: readonly StandardCardPropsWithoutVariant[] = [
|
||||
{
|
||||
headline: "Feature 1",
|
||||
children: "Description of feature 1",
|
||||
callsToAction: [{ children: "Learn More", href: "/feature1" }],
|
||||
},
|
||||
{
|
||||
headline: "Feature 2",
|
||||
children: "Description of feature 2",
|
||||
callsToAction: [{ children: "Learn More", href: "/feature2" }],
|
||||
},
|
||||
];
|
||||
|
||||
const GREEN_VARIANT_CARDS: readonly StandardCardPropsWithoutVariant[] = [
|
||||
{
|
||||
headline: "Developer Tools",
|
||||
children: "Comprehensive APIs and SDKs for building on XRPL",
|
||||
callsToAction: [{ children: "Get Started", href: "/docs" }],
|
||||
},
|
||||
{
|
||||
headline: "Payment Solutions",
|
||||
children: "Fast, low-cost global payment infrastructure",
|
||||
callsToAction: [{ children: "Learn More", href: "/payments" }],
|
||||
},
|
||||
{
|
||||
headline: "Tokenization",
|
||||
children: "Issue and manage digital assets on XRPL",
|
||||
callsToAction: [{ children: "Explore", href: "/tokens" }],
|
||||
},
|
||||
{
|
||||
headline: "DeFi Protocols",
|
||||
children: "Decentralized finance applications and liquidity pools",
|
||||
callsToAction: [{ children: "Discover", href: "/defi" }],
|
||||
},
|
||||
{
|
||||
headline: "NFT Marketplace",
|
||||
children: "Create, trade, and manage non-fungible tokens",
|
||||
callsToAction: [{ children: "View Marketplace", href: "/nfts" }],
|
||||
},
|
||||
{
|
||||
headline: "Enterprise Solutions",
|
||||
children: "Scalable blockchain infrastructure for businesses",
|
||||
callsToAction: [{ children: "Contact Sales", href: "/enterprise" }],
|
||||
},
|
||||
];
|
||||
|
||||
const NEUTRAL_VARIANT_CARDS: readonly StandardCardPropsWithoutVariant[] = [
|
||||
{
|
||||
headline: "Documentation",
|
||||
children: "Comprehensive guides and API references",
|
||||
callsToAction: [{ children: "View Docs", href: "/docs" }],
|
||||
},
|
||||
{
|
||||
headline: "Tutorials",
|
||||
children: "Step-by-step guides and examples",
|
||||
callsToAction: [{ children: "Browse Tutorials", href: "/tutorials" }],
|
||||
},
|
||||
];
|
||||
|
||||
const YELLOW_VARIANT_CARDS: readonly StandardCardPropsWithoutVariant[] = [
|
||||
{
|
||||
headline: "New Features",
|
||||
children: "Latest updates and enhancements to the platform",
|
||||
callsToAction: [{ children: "See What's New", href: "/features" }],
|
||||
},
|
||||
{
|
||||
headline: "Special Offers",
|
||||
children: "Exclusive deals and promotions for early adopters",
|
||||
callsToAction: [{ children: "View Offers", href: "/offers" }],
|
||||
},
|
||||
{
|
||||
headline: "Community Events",
|
||||
children: "Join upcoming workshops, webinars, and meetups",
|
||||
callsToAction: [{ children: "Browse Events", href: "/events" }],
|
||||
},
|
||||
];
|
||||
|
||||
const BLUE_VARIANT_CARDS: readonly StandardCardPropsWithoutVariant[] = [
|
||||
{
|
||||
headline: "Cross-Border Payments",
|
||||
children: "Send money globally in seconds",
|
||||
callsToAction: [{ children: "Learn More", href: "/payments" }],
|
||||
},
|
||||
{
|
||||
headline: "NFT Marketplaces",
|
||||
children: "Create and trade digital collectibles",
|
||||
callsToAction: [{ children: "Explore", href: "/nfts" }],
|
||||
},
|
||||
{
|
||||
headline: "Central Bank Digital Currencies",
|
||||
children: "CBDC infrastructure and solutions",
|
||||
callsToAction: [{ children: "Read More", href: "/cbdc" }],
|
||||
},
|
||||
];
|
||||
|
||||
const SECONDARY_CTA_CARDS: readonly StandardCardPropsWithoutVariant[] = [
|
||||
{
|
||||
headline: "Enterprise Solutions",
|
||||
children: "Scalable infrastructure for large organizations",
|
||||
callsToAction: [
|
||||
{ children: "Contact Sales", href: "/contact" },
|
||||
{ children: "View Case Studies", href: "/cases" },
|
||||
],
|
||||
},
|
||||
{
|
||||
headline: "Developer Platform",
|
||||
children: "Tools and APIs for building on XRPL",
|
||||
callsToAction: [
|
||||
{ children: "Get Started", href: "/start" },
|
||||
{ children: "View Docs", href: "/docs" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const SINGLE_CTA_CARDS: readonly StandardCardPropsWithoutVariant[] = [
|
||||
{
|
||||
headline: "Documentation",
|
||||
children: "Complete API reference and guides",
|
||||
callsToAction: [{ children: "View Docs", href: "/docs" }],
|
||||
},
|
||||
{
|
||||
headline: "Community",
|
||||
children: "Join developers and builders",
|
||||
callsToAction: [{ children: "Join Now", href: "/community" }],
|
||||
},
|
||||
{
|
||||
headline: "Blog",
|
||||
children: "Latest news and updates",
|
||||
callsToAction: [{ children: "Read Blog", href: "/blog" }],
|
||||
},
|
||||
];
|
||||
|
||||
export default function StandardCardGroupSectionShowcase() {
|
||||
return (
|
||||
<div className="landing">
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="text-center mb-26">
|
||||
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
|
||||
<h1 className="h2 mb-4">StandardCardGroupSection Pattern</h1>
|
||||
<p className="longform">
|
||||
A section pattern that displays a headline, description, and a
|
||||
responsive grid of StandardCard components. All cards share a
|
||||
uniform variant determined by the section, ensuring visual
|
||||
consistency across the group.
|
||||
</p>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Basic Usage */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Basic Usage"
|
||||
description="The simplest implementation with a headline, description, variant, and array of cards."
|
||||
code={`<StandardCardGroupSection
|
||||
headline="Our Features"
|
||||
description="Explore what we offer"
|
||||
variant="neutral"
|
||||
cards={[
|
||||
{
|
||||
headline: "Feature 1",
|
||||
children: "Description of feature 1",
|
||||
callsToAction: [
|
||||
{ children: "Learn More", href: "/feature1" }
|
||||
]
|
||||
},
|
||||
{
|
||||
headline: "Feature 2",
|
||||
children: "Description of feature 2",
|
||||
callsToAction: [
|
||||
{ children: "Learn More", href: "/feature2" }
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>`}
|
||||
>
|
||||
<StandardCardGroupSection
|
||||
headline="Our Features"
|
||||
description="Explore what we offer"
|
||||
variant="neutral"
|
||||
cards={BASIC_CARDS}
|
||||
/>
|
||||
</CodeDemo>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Variant: Green */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Green Variant"
|
||||
description="Using the green variant for brand-focused content."
|
||||
code={`<StandardCardGroupSection
|
||||
headline="XRPL Solutions"
|
||||
description="Powerful tools and services built on XRPL"
|
||||
variant="green"
|
||||
cards={[...]}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
<StandardCardGroupSection
|
||||
headline="XRPL Solutions"
|
||||
description="Powerful tools and services built on XRPL"
|
||||
variant="green"
|
||||
cards={GREEN_VARIANT_CARDS}
|
||||
/>
|
||||
|
||||
{/* Variant: Light Gray */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Neutral Variant"
|
||||
description="Using the neutral variant for subtle, neutral content."
|
||||
code={`<StandardCardGroupSection
|
||||
headline="Resources"
|
||||
description="Everything you need to get started"
|
||||
variant="neutral"
|
||||
cards={[...]}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
<StandardCardGroupSection
|
||||
headline="Resources"
|
||||
description="Everything you need to get started"
|
||||
variant="neutral"
|
||||
cards={NEUTRAL_VARIANT_CARDS}
|
||||
/>
|
||||
|
||||
{/* Variant: Yellow */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Yellow Variant"
|
||||
description="Using the yellow variant for attention-grabbing, high-energy content."
|
||||
code={`<StandardCardGroupSection
|
||||
headline="Featured Highlights"
|
||||
description="Discover our most exciting features and opportunities"
|
||||
variant="yellow"
|
||||
cards={[...]}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
<StandardCardGroupSection
|
||||
headline="Featured Highlights"
|
||||
description="Discover our most exciting features and opportunities"
|
||||
variant="yellow"
|
||||
cards={YELLOW_VARIANT_CARDS}
|
||||
/>
|
||||
|
||||
{/* Variant: Blue */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Blue Variant"
|
||||
description="Using the blue variant for secondary content sections."
|
||||
code={`<StandardCardGroupSection
|
||||
headline="Use Cases"
|
||||
description="Real-world applications of XRPL technology"
|
||||
variant="blue"
|
||||
cards={[...]}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
<StandardCardGroupSection
|
||||
headline="Use Cases"
|
||||
description="Real-world applications of XRPL technology"
|
||||
variant="blue"
|
||||
cards={BLUE_VARIANT_CARDS}
|
||||
/>
|
||||
|
||||
{/* With Secondary CTA */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Cards with Secondary CTA"
|
||||
description="Cards can include both primary and secondary call-to-action buttons."
|
||||
code={`<StandardCardGroupSection
|
||||
headline="Services"
|
||||
description="Comprehensive solutions for your needs"
|
||||
variant="neutral"
|
||||
cards={[
|
||||
{
|
||||
headline: "Service 1",
|
||||
children: "Description",
|
||||
callsToAction: [
|
||||
{ children: "Get Started", href: "/start" },
|
||||
{ children: "Learn More", href: "/learn" }
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
<StandardCardGroupSection
|
||||
headline="Services"
|
||||
description="Comprehensive solutions for your needs"
|
||||
variant="neutral"
|
||||
cards={SECONDARY_CTA_CARDS}
|
||||
/>
|
||||
|
||||
{/* Single CTA Only */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<CodeDemo
|
||||
title="Single CTA Only"
|
||||
description="Cards can have just a primary call-to-action button."
|
||||
code={`<StandardCardGroupSection
|
||||
headline="Quick Links"
|
||||
description="Fast access to key resources"
|
||||
variant="green"
|
||||
cards={[
|
||||
{
|
||||
headline: "Link 1",
|
||||
children: "Description",
|
||||
callsToAction: [
|
||||
{ children: "Visit", href: "/link1" }
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>`}
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
<StandardCardGroupSection
|
||||
headline="Quick Links"
|
||||
description="Fast access to key resources"
|
||||
variant="green"
|
||||
cards={SINGLE_CTA_CARDS}
|
||||
/>
|
||||
|
||||
{/* Responsive Behavior */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Responsive Behavior</h2>
|
||||
<p className="mb-6">
|
||||
The StandardCardGroupSection automatically adapts its layout based
|
||||
on screen size:
|
||||
</p>
|
||||
<ul className="mb-6">
|
||||
<li>
|
||||
<strong>Mobile (base):</strong> 1 column - cards stack
|
||||
vertically
|
||||
</li>
|
||||
<li>
|
||||
<strong>Tablet (md):</strong> 3 columns - cards display in a
|
||||
3-column grid
|
||||
</li>
|
||||
<li>
|
||||
<strong>Desktop (lg):</strong> 3 columns - cards display in a
|
||||
3-column grid
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mb-6">
|
||||
Resize your browser window to see the responsive behavior in
|
||||
action.
|
||||
</p>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Code Examples */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Code Examples</h2>
|
||||
|
||||
<h4 className="h5 mb-4">Import</h4>
|
||||
<div
|
||||
className="p-4 bg-light br-4 mb-6"
|
||||
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
||||
>
|
||||
<pre style={{ margin: 0, color: "#000" }}>
|
||||
{`import StandardCardGroupSection from "shared/patterns/StandardCardGroupSection/StandardCardGroupSection";`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h4 className="h5 mb-4">Basic Example</h4>
|
||||
<div
|
||||
className="p-4 bg-light br-4 mb-6"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
overflow: "auto",
|
||||
backgroundColor: "#1e1e1e",
|
||||
color: "#d4d4d4",
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
|
||||
{`<StandardCardGroupSection
|
||||
headline="Our Features"
|
||||
description="Explore what we offer"
|
||||
variant="neutral"
|
||||
cards={[
|
||||
{
|
||||
headline: "Feature 1",
|
||||
children: "Description of feature 1",
|
||||
callsToAction: [
|
||||
{ children: "Learn More", href: "/feature1" }
|
||||
]
|
||||
},
|
||||
{
|
||||
headline: "Feature 2",
|
||||
children: "Description of feature 2",
|
||||
callsToAction: [
|
||||
{ children: "Learn More", href: "/feature2" }
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h4 className="h5 mb-4">With Secondary CTA</h4>
|
||||
<div
|
||||
className="p-4 bg-light br-4 mb-6"
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "14px",
|
||||
overflow: "auto",
|
||||
backgroundColor: "#1e1e1e",
|
||||
color: "#d4d4d4",
|
||||
}}
|
||||
>
|
||||
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
|
||||
{`<StandardCardGroupSection
|
||||
headline="Services"
|
||||
description="Comprehensive solutions"
|
||||
variant="green"
|
||||
cards={[
|
||||
{
|
||||
headline: "Service 1",
|
||||
children: "Description",
|
||||
callsToAction: [
|
||||
{ children: "Get Started", href: "/start" },
|
||||
{ children: "Learn More", href: "/learn" }
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
{/* Best Practices */}
|
||||
<PageGridRow>
|
||||
<PageGridCol>
|
||||
<div className="mb-26">
|
||||
<h2 className="h3 mb-6">Best Practices</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Variant Consistency:</strong> All cards in a section
|
||||
share the same variant. This ensures visual consistency and
|
||||
prevents individual cards from having different variants.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Card Count:</strong> Aim for multiples of 3 for best
|
||||
visual balance on desktop (3, 6, 9 cards).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Headlines:</strong> Keep card headlines concise and
|
||||
impactful (1-2 lines preferred).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Descriptions:</strong> Provide clear, actionable
|
||||
descriptions (2-3 lines max).
|
||||
</li>
|
||||
<li>
|
||||
<strong>CTAs:</strong> Use action-oriented button text ("Get
|
||||
Started" not "Click Here").
|
||||
</li>
|
||||
<li>
|
||||
<strong>Section Headline:</strong> Make it descriptive and
|
||||
specific to help users understand the card group's purpose.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Accessibility:</strong> The component includes ARIA
|
||||
roles and labels for screen readers. Ensure card headlines are
|
||||
descriptive.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,507 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
import { TileLink } from "shared/patterns/TileLinks";
|
||||
import { Divider } from "shared/components/Divider";
|
||||
|
||||
export const frontmatter = {
|
||||
seo: {
|
||||
title: 'TileLink Component Showcase',
|
||||
description: "A comprehensive showcase of all TileLink component variants, states, and responsive behavior in the XRPL.org Design System.",
|
||||
}
|
||||
};
|
||||
|
||||
export default function TileLinkShowcase() {
|
||||
const handleClick = (message: string) => {
|
||||
console.log(`TileLink clicked: ${message}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="landing">
|
||||
<div className="overflow-hidden">
|
||||
{/* Hero Section */}
|
||||
<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">TileLink Component</h1>
|
||||
<p className="longform">
|
||||
A clickable tile component for link grids, featuring text content with an arrow icon.
|
||||
Supports gray and lilac color variants with full light/dark mode theming.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Divider color="gray" />
|
||||
|
||||
{/* Gray Variant Section */}
|
||||
<section className="container-new py-10">
|
||||
<div className="d-flex flex-column-reverse">
|
||||
<h2 className="h4 mb-8">Gray Variant</h2>
|
||||
<h6 className="eyebrow mb-3">Color Variants</h6>
|
||||
</div>
|
||||
<p className="mb-6 text-muted">
|
||||
The gray variant uses neutral gray tones. In light mode, it displays with gray-200 background.
|
||||
In dark mode, it uses gray-500 with white text.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink
|
||||
variant="gray"
|
||||
label="Documentation"
|
||||
href="/docs"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink
|
||||
variant="gray"
|
||||
label="Get Started"
|
||||
href="/get-started"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink
|
||||
variant="gray"
|
||||
label="Tutorials"
|
||||
onClick={() => handleClick('Tutorials')}
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink
|
||||
variant="gray"
|
||||
label="API Reference"
|
||||
href="/api"
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
|
||||
|
||||
<Divider variant="gray" />
|
||||
|
||||
{/* Lilac Variant Section */}
|
||||
<section className="container-new py-26">
|
||||
<div className="d-flex flex-column-reverse">
|
||||
<h2 className="h4 mb-8">Lilac Variant</h2>
|
||||
<h6 className="eyebrow mb-3">Color Variants</h6>
|
||||
</div>
|
||||
<p className="mb-6 text-muted">
|
||||
The lilac variant uses purple/lilac tones. In light mode, it displays with lilac-300 background.
|
||||
In dark mode, it uses lilac-400 with white text.
|
||||
</p>
|
||||
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink
|
||||
variant="lilac"
|
||||
label="Community"
|
||||
href="/community"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink
|
||||
variant="lilac"
|
||||
label="Events"
|
||||
href="/events"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink
|
||||
variant="lilac"
|
||||
label="Blog"
|
||||
onClick={() => handleClick('Blog')}
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink
|
||||
variant="lilac"
|
||||
label="Newsletter"
|
||||
href="/newsletter"
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</section>
|
||||
|
||||
<Divider variant="gray" />
|
||||
|
||||
{/* Mixed Variants Section */}
|
||||
<section className="container-new py-26">
|
||||
<div className="d-flex flex-column-reverse">
|
||||
<h2 className="h4 mb-8">Mixed Variants</h2>
|
||||
<h6 className="eyebrow mb-3">Combinations</h6>
|
||||
</div>
|
||||
<p className="mb-6 text-muted">
|
||||
Gray and lilac variants can be mixed in the same grid for visual variety.
|
||||
</p>
|
||||
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink
|
||||
variant="gray"
|
||||
label="Introduction"
|
||||
href="/intro"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink
|
||||
variant="lilac"
|
||||
label="Quick Start"
|
||||
href="/quick-start"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink
|
||||
variant="gray"
|
||||
label="Concepts"
|
||||
href="/concepts"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink
|
||||
variant="lilac"
|
||||
label="Advanced Topics"
|
||||
href="/advanced"
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</section>
|
||||
|
||||
<Divider variant="gray" />
|
||||
|
||||
{/* Interactive States Section */}
|
||||
<section className="container-new py-26">
|
||||
<div className="d-flex flex-column-reverse">
|
||||
<h2 className="h4 mb-8">Interactive States</h2>
|
||||
<h6 className="eyebrow mb-3">States</h6>
|
||||
</div>
|
||||
<p className="mb-6 text-muted">
|
||||
TileLink supports multiple interaction states: default, hover, focus, pressed, and disabled.
|
||||
Hover over the tiles to see the window shade animation.
|
||||
</p>
|
||||
|
||||
<div className="mb-8">
|
||||
<h6 className="mb-4">Gray Variant States</h6>
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<div className="mb-2">
|
||||
<small className="text-muted">Default / Hover / Pressed</small>
|
||||
</div>
|
||||
<TileLink
|
||||
variant="gray"
|
||||
label="Interactive Link"
|
||||
href="/link"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<div className="mb-2">
|
||||
<small className="text-muted">Button with onClick</small>
|
||||
</div>
|
||||
<TileLink
|
||||
variant="gray"
|
||||
label="Click Handler"
|
||||
onClick={() => handleClick('Gray button clicked')}
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<div className="mb-2">
|
||||
<small className="text-muted">Disabled State</small>
|
||||
</div>
|
||||
<TileLink
|
||||
variant="gray"
|
||||
label="Coming Soon"
|
||||
disabled
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h6 className="mb-4">Lilac Variant States</h6>
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<div className="mb-2">
|
||||
<small className="text-muted">Default / Hover / Pressed</small>
|
||||
</div>
|
||||
<TileLink
|
||||
variant="lilac"
|
||||
label="Interactive Link"
|
||||
href="/link"
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<div className="mb-2">
|
||||
<small className="text-muted">Button with onClick</small>
|
||||
</div>
|
||||
<TileLink
|
||||
variant="lilac"
|
||||
label="Click Handler"
|
||||
onClick={() => handleClick('Lilac button clicked')}
|
||||
/>
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<div className="mb-2">
|
||||
<small className="text-muted">Disabled State</small>
|
||||
</div>
|
||||
<TileLink
|
||||
variant="lilac"
|
||||
label="Coming Soon"
|
||||
disabled
|
||||
/>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Divider variant="gray" />
|
||||
|
||||
{/* Responsive Behavior Section */}
|
||||
<section className="container-new py-26">
|
||||
<div className="d-flex flex-column-reverse">
|
||||
<h2 className="h4 mb-8">Responsive Behavior</h2>
|
||||
<h6 className="eyebrow mb-3">Layout</h6>
|
||||
</div>
|
||||
<p className="mb-6 text-muted">
|
||||
TileLink adapts to different screen sizes. Resize your browser to see the responsive behavior:
|
||||
</p>
|
||||
<ul className="mb-6 text-muted">
|
||||
<li><strong>Mobile (< 576px):</strong> 1 column, 80px height, 12px padding, 16px font</li>
|
||||
<li><strong>Tablet (576px - 991px):</strong> 2 columns, 88px height, 16px padding, 16px font</li>
|
||||
<li><strong>Desktop (≥ 992px):</strong> 4 columns, 96px height, 20px padding, 18px font</li>
|
||||
</ul>
|
||||
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="gray" label="Responsive Tile 1" href="#1" />
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="lilac" label="Responsive Tile 2" href="#2" />
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="gray" label="Responsive Tile 3" href="#3" />
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="lilac" label="Responsive Tile 4" href="#4" />
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</section>
|
||||
|
||||
<Divider variant="gray" />
|
||||
|
||||
{/* Large Grid Example */}
|
||||
<section className="container-new py-26">
|
||||
<div className="d-flex flex-column-reverse">
|
||||
<h2 className="h4 mb-8">Large Grid Example</h2>
|
||||
<h6 className="eyebrow mb-3">Real-World Usage</h6>
|
||||
</div>
|
||||
<p className="mb-6 text-muted">
|
||||
Example of a larger grid with multiple rows, demonstrating how TileLink works in a typical section layout.
|
||||
</p>
|
||||
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="gray" label="Getting Started" href="/start" />
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="lilac" label="Core Concepts" href="/concepts" />
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="gray" label="Tutorials" href="/tutorials" />
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="lilac" label="API Reference" href="/api" />
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="gray" label="Examples" href="/examples" />
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="lilac" label="Best Practices" href="/best-practices" />
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="gray" label="Tools" href="/tools" />
|
||||
</PageGridCol>
|
||||
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="lilac" label="Resources" href="/resources" />
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</section>
|
||||
|
||||
<Divider variant="gray" />
|
||||
|
||||
{/* Code Examples Section */}
|
||||
<section className="container-new py-26">
|
||||
<div className="d-flex flex-column-reverse">
|
||||
<h2 className="h4 mb-8">Code Examples</h2>
|
||||
<h6 className="eyebrow mb-3">Implementation</h6>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h6 className="mb-4">Basic Usage</h6>
|
||||
<div className="p-6-sm p-10-until-sm br-8" style={{ backgroundColor: '#1e1e1e', color: '#d4d4d4' }}>
|
||||
<pre style={{ margin: 0, overflow: 'auto' }}>
|
||||
<code>{`import { TileLink } from 'shared/patterns/TileLinks';
|
||||
|
||||
// Gray variant with link
|
||||
<TileLink
|
||||
variant="gray"
|
||||
label="Documentation"
|
||||
href="/docs"
|
||||
/>
|
||||
|
||||
// Lilac variant with click handler
|
||||
<TileLink
|
||||
variant="lilac"
|
||||
label="Get Started"
|
||||
onClick={() => navigate('/start')}
|
||||
/>
|
||||
|
||||
// Disabled state
|
||||
<TileLink
|
||||
variant="gray"
|
||||
label="Coming Soon"
|
||||
disabled
|
||||
/>`}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h6 className="mb-4">Grid Layout with PageGrid</h6>
|
||||
<div className="p-6-sm p-10-until-sm br-8" style={{ backgroundColor: '#1e1e1e', color: '#d4d4d4' }}>
|
||||
<pre style={{ margin: 0, overflow: 'auto' }}>
|
||||
<code>{`import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
|
||||
import { TileLink } from 'shared/patterns/TileLinks';
|
||||
|
||||
<PageGrid>
|
||||
<PageGrid.Row>
|
||||
{/* Mobile: 1 column, Tablet: 2 columns, Desktop: 4 columns */}
|
||||
<PageGrid.Col span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="gray" label="Link 1" href="/link1" />
|
||||
</PageGrid.Col>
|
||||
<PageGrid.Col span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="lilac" label="Link 2" href="/link2" />
|
||||
</PageGrid.Col>
|
||||
<PageGrid.Col span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="gray" label="Link 3" href="/link3" />
|
||||
</PageGrid.Col>
|
||||
<PageGrid.Col span={{ base: 4, md: 4, lg: 3 }}>
|
||||
<TileLink variant="lilac" label="Link 4" href="/link4" />
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
</PageGrid>`}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h6 className="mb-4">Props API</h6>
|
||||
<div className="p-6-sm p-10-until-sm br-8" style={{ backgroundColor: '#1e1e1e', color: '#d4d4d4' }}>
|
||||
<pre style={{ margin: 0, overflow: 'auto' }}>
|
||||
<code>{`interface TileLinkProps {
|
||||
/** Color variant: 'gray' (default) or 'lilac' */
|
||||
variant?: 'gray' | 'lilac';
|
||||
|
||||
/** Link text/label */
|
||||
label: string;
|
||||
|
||||
/** Link destination - renders as <a> */
|
||||
href?: string;
|
||||
|
||||
/** Click handler - renders as <button> */
|
||||
onClick?: () => void;
|
||||
|
||||
/** Disabled state - prevents interaction */
|
||||
disabled?: boolean;
|
||||
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}`}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Divider variant="gray" />
|
||||
|
||||
{/* Features Section */}
|
||||
<section className="container-new py-26">
|
||||
<div className="d-flex flex-column-reverse">
|
||||
<h2 className="h4 mb-8">Features</h2>
|
||||
<h6 className="eyebrow mb-3">Component Capabilities</h6>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-md-6 mb-6">
|
||||
<h6 className="mb-3">🎨 Color Variants</h6>
|
||||
<ul className="text-muted">
|
||||
<li>Gray variant with neutral tones</li>
|
||||
<li>Lilac variant with purple/lilac tones</li>
|
||||
<li>Full light and dark mode support</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6 mb-6">
|
||||
<h6 className="mb-3">✨ Animations</h6>
|
||||
<ul className="text-muted">
|
||||
<li>Window shade hover effect (bottom-to-top)</li>
|
||||
<li>Arrow animation on hover</li>
|
||||
<li>Smooth transitions (200ms cubic-bezier)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6 mb-6">
|
||||
<h6 className="mb-3">📱 Responsive Design</h6>
|
||||
<ul className="text-muted">
|
||||
<li>Mobile: 80px height, 12px padding</li>
|
||||
<li>Tablet: 88px height, 16px padding</li>
|
||||
<li>Desktop: 96px height, 20px padding</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6 mb-6">
|
||||
<h6 className="mb-3">♿ Accessibility</h6>
|
||||
<ul className="text-muted">
|
||||
<li>Proper ARIA labels and roles</li>
|
||||
<li>Keyboard navigation support</li>
|
||||
<li>Focus states with visible outlines</li>
|
||||
<li>Disabled state handling</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6 mb-6">
|
||||
<h6 className="mb-3">🔗 Flexible Rendering</h6>
|
||||
<ul className="text-muted">
|
||||
<li>Renders as <a> tag when href is provided</li>
|
||||
<li>Renders as <button> for onClick handlers</li>
|
||||
<li>Supports disabled state for both</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="col-md-6 mb-6">
|
||||
<h6 className="mb-3">🎯 Grid Integration</h6>
|
||||
<ul className="text-muted">
|
||||
<li>Designed to work with PageGrid system</li>
|
||||
<li>Responsive column spans</li>
|
||||
<li>Consistent 8px gap between tiles</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
---
|
||||
html: private-network-with-docker.html
|
||||
name: Run a Private Network with Docker
|
||||
parent: use-stand-alone-mode.html
|
||||
seo:
|
||||
description: Learn how to set up your own XRP private ledger network with Docker and Docker Compose.
|
||||
description: Learn how to set up your own XRP private ledger network with Docker and Docker Compose.
|
||||
labels:
|
||||
- Core Server
|
||||
---
|
||||
@@ -259,25 +261,17 @@ For each validator node, follow these steps:
|
||||
|
||||
Now that you have created the configuration files for your validators, you need to add a `validator.txt` file. This file defines which validators are trusted by your network.
|
||||
|
||||
Follow these steps to add validator configuration files to each validator:
|
||||
For each node, follow these steps:
|
||||
|
||||
1. Create a `validators.txt` file.
|
||||
1. Create a `validators.txt` file in the configuration directory.
|
||||
2. Copy the public keys from the `validator-keys.json` files that you generated at the [beginning](#generate-the-validator-keys) of the tutorial.
|
||||
3. Add the public keys of _all_ the validators. For example:
|
||||
|
||||
```
|
||||
[validators]
|
||||
nHBgaEDL8buUECuk4Rck4QBYtmUgbAoeYJLpWLzG9iXsznTRYrQu
|
||||
nHBCHX7iLDTyap3LumqBNuKgG7JLA5tc6MSJxpLs3gjkwpu836mY
|
||||
nHU5STUKTgWdreVqJDx6TopLUymzRUZshTSGcWNtjfByJkYdiiRc
|
||||
```
|
||||
|
||||
4. Copy the same `validators.txt` file into the `config` directory for _each_ validator.
|
||||
|
||||
```sh
|
||||
cp validators.txt validator_1/config/
|
||||
cp validators.txt validator_2/config/
|
||||
cp validators.txt validator_3/config/
|
||||
nHBgaEDL8buUECuk4Rck4QBYtmUgbAoeYJLpWLzG9iXsznTRYrQu
|
||||
nHBCHX7iLDTyap3LumqBNuKgG7JLA5tc6MSJxpLs3gjkwpu836mY
|
||||
nHU5STUKTgWdreVqJDx6TopLUymzRUZshTSGcWNtjfByJkYdiiRc
|
||||
```
|
||||
|
||||
## Start the Network
|
||||
@@ -291,6 +285,7 @@ To start running your private network, follow these steps:
|
||||
1. Create a `docker-compose.yml` file in the root of the private network directory, `xrpl-private-network`, and add the following content:
|
||||
|
||||
```
|
||||
version: "3.9"
|
||||
services:
|
||||
validator_1:
|
||||
platform: linux/amd64
|
||||
@@ -343,15 +338,9 @@ To start running your private network, follow these steps:
|
||||
|
||||
Now that the private ledger network is up, you need to verify that **each** validator node is running as expected:
|
||||
|
||||
1. Open a terminal in the validator_1 container:
|
||||
1. In your terminal, run `docker exec -it <validator_name> bin/bash` to execute commands in the validator Docker container. Replace `<validator_name>` with the name of the container (e.g., `validator_1`).
|
||||
|
||||
```
|
||||
docker exec -it validator_1 bin/bash
|
||||
```
|
||||
|
||||
{% admonition type="success" name="Tip" %}You can use the same syntax to execute commands in the other Docker containers. Replace `bin/bash` with the command to run and `validator_1` with the name of the container.{% /admonition %}
|
||||
|
||||
3. Run the `rippled server_info` command to check the state of the validator:
|
||||
2. Run the `rippled server_info` command to check the state of the validator:
|
||||
|
||||
```
|
||||
rippled server_info | grep server_state
|
||||
@@ -365,7 +354,7 @@ Now that the private ledger network is up, you need to verify that **each** vali
|
||||
|
||||
{% admonition type="info" name="Note" %}If the state is not updated to **proposing**, repeat step **2** after a few minutes as the ledger can take some time to update.{% /admonition %}
|
||||
|
||||
4. Verify the number of peers connected to the validator.
|
||||
3. Verify the number of peers connected to the validator.
|
||||
|
||||
```
|
||||
rippled server_info | grep peers
|
||||
@@ -377,10 +366,10 @@ Now that the private ledger network is up, you need to verify that **each** vali
|
||||
"peers" : 2
|
||||
```
|
||||
|
||||
5. Run the following command to check the genesis account information:
|
||||
4. Run the following command to check the genesis account information:
|
||||
|
||||
```
|
||||
rippled account_info rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh validated
|
||||
rippled account_info rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh validated strict
|
||||
```
|
||||
|
||||
Sample Output:
|
||||
@@ -407,9 +396,7 @@ Now that the private ledger network is up, you need to verify that **each** vali
|
||||
}
|
||||
```
|
||||
|
||||
If the ledger does not have `"validated": true`, double check that you put matching `validators.txt` files with all three public keys in each container's config directory, and restart the container if you need to make changes.
|
||||
|
||||
6. To leave the Docker container shell, enter `exit` in the terminal.
|
||||
5. To leave the Docker container shell, enter `exit` in the terminal.
|
||||
|
||||
### Perform a test transaction
|
||||
|
||||
@@ -452,7 +439,7 @@ Perform a **test** transaction to ensure you can send money to an account.
|
||||
|
||||
```
|
||||
docker exec -it validator_1 \
|
||||
rippled account_info r9wRwVgL2vWVnKhTPdtxva5vdH7FNw1zPs validated
|
||||
rippled account_info r9wRwVgL2vWVnKhTPdtxva5vdH7FNw1zPs validated strict
|
||||
```
|
||||
|
||||
Sample Output:
|
||||
|
||||
@@ -57,7 +57,6 @@ Flags are properties or other options associated with the `NFToken` object.
|
||||
| `lsfTrustLine` | `0x0004` | **DEPRECATED** If enabled, automatically create [trust lines](../../../concepts/tokens/fungible-tokens/index.md) to hold transfer fees. Otherwise, buying or selling this `NFToken` for a fungible token amount fails if the issuer does not have a trust line for that token. The [fixRemoveNFTokenAutoTrustLine amendment][] makes it invalid to enable this flag. |
|
||||
| `lsfTransferable` | `0x0008` | If enabled, this `NFToken` can be transferred from one holder to another. Otherwise, it can only be transferred to or from the issuer. |
|
||||
| `lsfReservedFlag` | `0x8000` | This flag is reserved for future use. Attempts to set this flag fail. |
|
||||
| `lsfMutable` | `0x0010` | If enabled, the `URI` field of the `NFToken` can be updated using the `NFTokenModify` transaction. |
|
||||
|
||||
`NFToken` flags are immutable: they can only be set during the [NFTokenMint transaction][] and cannot be changed later.
|
||||
|
||||
|
||||
@@ -23,12 +23,6 @@ interface ButtonProps {
|
||||
variant?: 'primary' | 'secondary' | 'tertiary';
|
||||
/** Color theme - green (default) or black */
|
||||
color?: 'green' | 'black';
|
||||
/**
|
||||
* Force the color to remain constant regardless of theme mode.
|
||||
* When true, the button color will not change between light/dark modes.
|
||||
* Use this for buttons on colored backgrounds where black should stay black.
|
||||
*/
|
||||
forceColor?: boolean;
|
||||
/** Button content/label */
|
||||
children: React.ReactNode;
|
||||
/** Click handler */
|
||||
@@ -54,7 +48,6 @@ interface ButtonProps {
|
||||
|
||||
- `variant`: `'primary'`
|
||||
- `color`: `'green'`
|
||||
- `forceColor`: `false`
|
||||
- `disabled`: `false`
|
||||
- `type`: `'button'`
|
||||
- `className`: `''`
|
||||
@@ -139,37 +132,6 @@ The black theme provides an alternative color scheme:
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Force Color (Theme-Independent)
|
||||
|
||||
By default, black buttons automatically switch to green in dark mode for better visibility. However, when placing buttons on colored backgrounds (e.g., lilac, yellow, green), you may want black buttons to remain black regardless of theme mode.
|
||||
|
||||
Use the `forceColor` prop to prevent automatic color switching:
|
||||
|
||||
**Usage:**
|
||||
```tsx
|
||||
{/* Black button that stays black in both light and dark modes */}
|
||||
<Button variant="primary" color="black" forceColor onClick={handleClick}>
|
||||
Always Black
|
||||
</Button>
|
||||
|
||||
{/* Useful for colored backgrounds like in FeatureTwoColumn pattern */}
|
||||
<FeatureTwoColumn color="lilac">
|
||||
<Button variant="primary" color="black" forceColor href="/get-started">
|
||||
Get Started
|
||||
</Button>
|
||||
<Button variant="tertiary" color="black" forceColor href="/learn-more">
|
||||
Learn More
|
||||
</Button>
|
||||
</FeatureTwoColumn>
|
||||
```
|
||||
|
||||
**When to use `forceColor`:**
|
||||
- Buttons on colored backgrounds (lilac, yellow, green variants)
|
||||
- When you need consistent button colors regardless of user's theme preference
|
||||
- Pattern components like `FeatureTwoColumn` where black text is required for readability
|
||||
|
||||
**Note:** The `forceColor` prop only affects the color behavior; all other button functionality (hover animations, focus states, etc.) remains the same.
|
||||
|
||||
## Link Buttons
|
||||
|
||||
The Button component can render as an anchor element for navigation by passing the `href` prop. When `href` is provided, the button is wrapped in a Redocly `Link` component for proper routing support within the application.
|
||||
|
||||
@@ -553,41 +553,41 @@ html.dark {
|
||||
stroke: $green-300;
|
||||
}
|
||||
|
||||
// Hover state - use !important to override light mode global styles
|
||||
// Hover state
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled) {
|
||||
color: $green-200 !important;
|
||||
|
||||
color: $green-200;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $green-200 !important;
|
||||
stroke: $green-200 !important;
|
||||
color: $green-200;
|
||||
stroke: $green-200;
|
||||
}
|
||||
}
|
||||
|
||||
// Focus state - use !important to override light mode global styles
|
||||
// Focus state
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
|
||||
color: $green-200 !important;
|
||||
color: $green-200;
|
||||
outline: $bds-btn-focus-border-width solid $white;
|
||||
outline-offset: 2px;
|
||||
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $green-200 !important;
|
||||
stroke: $green-200 !important;
|
||||
color: $green-200;
|
||||
stroke: $green-200;
|
||||
}
|
||||
}
|
||||
|
||||
// Active state - use !important to override light mode global styles
|
||||
// Active state
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
color: $green-300 !important;
|
||||
|
||||
color: $green-300;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $green-300 !important;
|
||||
stroke: $green-300 !important;
|
||||
color: $green-300;
|
||||
stroke: $green-300;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,315 +1109,6 @@ html.dark {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Force Color Modifier
|
||||
// =============================================================================
|
||||
// When .bds-btn--force-color is applied, the button color remains constant
|
||||
// regardless of theme mode. This is used for buttons on colored backgrounds
|
||||
// where black should stay black even in dark mode.
|
||||
//
|
||||
// Usage: <Button color="black" forceColor />
|
||||
// =============================================================================
|
||||
|
||||
.bds-btn--force-color {
|
||||
// Black buttons with force-color should maintain black styling in dark mode
|
||||
&.bds-btn--black {
|
||||
// Primary black - force black background
|
||||
&.bds-btn--primary {
|
||||
color: $bds-btn-primary-black-text !important;
|
||||
background-color: $bds-btn-primary-black-bg !important;
|
||||
|
||||
&::before {
|
||||
background-color: $bds-btn-primary-black-bg-hover !important;
|
||||
}
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-primary-black-text !important;
|
||||
stroke: $bds-btn-primary-black-text !important;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
color: $bds-btn-primary-black-text !important;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-primary-black-text !important;
|
||||
stroke: $bds-btn-primary-black-text !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
|
||||
outline-color: $bds-btn-neutral-black !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary black - force black text/border
|
||||
&.bds-btn--secondary {
|
||||
color: $bds-btn-secondary-black-text !important;
|
||||
border-color: $bds-btn-secondary-black-border !important;
|
||||
|
||||
&::before {
|
||||
background-color: $bds-btn-secondary-black-bg-hover !important;
|
||||
}
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-secondary-black-text !important;
|
||||
stroke: $bds-btn-secondary-black-text !important;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
color: $bds-btn-secondary-black-text !important;
|
||||
border-color: $bds-btn-secondary-black-border !important;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-secondary-black-text !important;
|
||||
stroke: $bds-btn-secondary-black-text !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
|
||||
outline-color: $bds-btn-neutral-black !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Tertiary black - force black text
|
||||
&.bds-btn--tertiary {
|
||||
color: $bds-btn-tertiary-black-text !important;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-tertiary-black-text !important;
|
||||
stroke: $bds-btn-tertiary-black-text !important;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
color: $bds-btn-tertiary-black-text !important;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-tertiary-black-text !important;
|
||||
stroke: $bds-btn-tertiary-black-text !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
|
||||
outline-color: $bds-btn-tertiary-black-focus-outline !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Force Color Modifier - Dark Mode Overrides
|
||||
// =============================================================================
|
||||
// These overrides must have higher specificity than the dark mode color swaps
|
||||
// to ensure black buttons stay black on colored backgrounds in dark mode.
|
||||
// =============================================================================
|
||||
|
||||
html.dark {
|
||||
// Primary black with force-color - override dark mode green swap
|
||||
.bds-btn.bds-btn.bds-btn--primary.bds-btn--primary.bds-btn--black.bds-btn--force-color {
|
||||
color: $bds-btn-primary-black-text !important;
|
||||
background-color: $bds-btn-primary-black-bg !important;
|
||||
|
||||
&::before {
|
||||
background-color: $bds-btn-primary-black-bg-hover !important;
|
||||
}
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-primary-black-text !important;
|
||||
stroke: $bds-btn-primary-black-text !important;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
color: $bds-btn-primary-black-text !important;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-primary-black-text !important;
|
||||
stroke: $bds-btn-primary-black-text !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
|
||||
outline-color: $white !important; // White outline for visibility on dark backgrounds
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary black with force-color - override dark mode green swap
|
||||
.bds-btn.bds-btn.bds-btn--secondary.bds-btn--secondary.bds-btn--black.bds-btn--force-color {
|
||||
color: $bds-btn-secondary-black-text !important;
|
||||
border-color: $bds-btn-secondary-black-border !important;
|
||||
background-color: transparent !important;
|
||||
|
||||
&::before {
|
||||
background-color: $bds-btn-secondary-black-bg-hover !important;
|
||||
}
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-secondary-black-text !important;
|
||||
stroke: $bds-btn-secondary-black-text !important;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
color: $bds-btn-secondary-black-text !important;
|
||||
border-color: $bds-btn-secondary-black-border !important;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-secondary-black-text !important;
|
||||
stroke: $bds-btn-secondary-black-text !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
|
||||
outline-color: $white !important; // White outline for visibility on dark backgrounds
|
||||
}
|
||||
}
|
||||
|
||||
// Tertiary black with force-color - override dark mode green swap
|
||||
.bds-btn.bds-btn.bds-btn--tertiary.bds-btn--tertiary.bds-btn--black.bds-btn--force-color {
|
||||
color: $bds-btn-tertiary-black-text !important;
|
||||
background-color: transparent !important;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-tertiary-black-text !important;
|
||||
stroke: $bds-btn-tertiary-black-text !important;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
color: $bds-btn-tertiary-black-text !important;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-tertiary-black-text !important;
|
||||
stroke: $bds-btn-tertiary-black-text !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
|
||||
outline-color: $white !important; // White outline for visibility on dark backgrounds
|
||||
}
|
||||
}
|
||||
|
||||
// Anchor link buttons with force-color - dark mode overrides
|
||||
a.bds-btn.bds-btn--force-color {
|
||||
&.bds-btn--primary.bds-btn--black {
|
||||
color: $bds-btn-primary-black-text !important;
|
||||
background-color: $bds-btn-primary-black-bg !important;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-primary-black-text !important;
|
||||
stroke: $bds-btn-primary-black-text !important;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus-visible,
|
||||
&:active,
|
||||
&:visited {
|
||||
color: $bds-btn-primary-black-text !important;
|
||||
background-color: $bds-btn-primary-black-bg !important;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-primary-black-text !important;
|
||||
stroke: $bds-btn-primary-black-text !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.bds-btn--secondary.bds-btn--black {
|
||||
color: $bds-btn-secondary-black-text !important;
|
||||
border-color: $bds-btn-secondary-black-border !important;
|
||||
background-color: transparent !important;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-secondary-black-text !important;
|
||||
stroke: $bds-btn-secondary-black-text !important;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus-visible,
|
||||
&:active,
|
||||
&:visited {
|
||||
color: $bds-btn-secondary-black-text !important;
|
||||
border-color: $bds-btn-secondary-black-border !important;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-secondary-black-text !important;
|
||||
stroke: $bds-btn-secondary-black-text !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.bds-btn--tertiary.bds-btn--black {
|
||||
color: $bds-btn-tertiary-black-text !important;
|
||||
background-color: transparent !important;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-tertiary-black-text !important;
|
||||
stroke: $bds-btn-tertiary-black-text !important;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus-visible,
|
||||
&:active,
|
||||
&:visited {
|
||||
color: $bds-btn-tertiary-black-text !important;
|
||||
|
||||
.bds-btn__icon,
|
||||
.bds-btn__icon-line,
|
||||
.bds-btn__icon-chevron {
|
||||
color: $bds-btn-tertiary-black-text !important;
|
||||
stroke: $bds-btn-tertiary-black-text !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tertiary Variant
|
||||
// =============================================================================
|
||||
@@ -1900,34 +1591,4 @@ html.dark {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// No Padding Modifier
|
||||
// =============================================================================
|
||||
// When .bds-btn--no-padding is applied, removes all padding and left-aligns content.
|
||||
// Useful for tertiary buttons in block layouts where left alignment is needed.
|
||||
// =============================================================================
|
||||
|
||||
.bds-btn--no-padding {
|
||||
padding: 0 !important;
|
||||
justify-content: flex-start !important;
|
||||
|
||||
// Override all state paddings
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
// Responsive overrides
|
||||
@include media-breakpoint-down(xl) {
|
||||
padding: 0 !important;
|
||||
|
||||
&:hover:not(:disabled):not(.bds-btn--disabled),
|
||||
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
|
||||
&:active:not(:disabled):not(.bds-btn--disabled) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,6 @@ export interface ButtonProps {
|
||||
variant?: 'primary' | 'secondary' | 'tertiary';
|
||||
/** Color theme - green (default) or black */
|
||||
color?: 'green' | 'black';
|
||||
/**
|
||||
* Force the color to remain constant regardless of theme mode.
|
||||
* When true, the button color will not change between light/dark modes.
|
||||
* Use this for buttons on colored backgrounds where black should stay black.
|
||||
*/
|
||||
forceColor?: boolean;
|
||||
/** Button content/label */
|
||||
children: React.ReactNode;
|
||||
/** Click handler */
|
||||
@@ -31,12 +25,6 @@ export interface ButtonProps {
|
||||
href?: string;
|
||||
/** Link target - only applies when href is provided */
|
||||
target?: '_self' | '_blank';
|
||||
/**
|
||||
* Force no padding and left-align text.
|
||||
* When true, removes all padding and aligns content to the left.
|
||||
* Useful for tertiary buttons in block layouts where left alignment is needed.
|
||||
*/
|
||||
forceNoPadding?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,7 +99,6 @@ const getTextFromChildren = (children: React.ReactNode): string => {
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
variant = 'primary',
|
||||
color = 'green',
|
||||
forceColor = false,
|
||||
children,
|
||||
onClick,
|
||||
disabled = false,
|
||||
@@ -121,7 +108,6 @@ export const Button: React.FC<ButtonProps> = ({
|
||||
ariaLabel,
|
||||
href,
|
||||
target = '_self',
|
||||
forceNoPadding = false,
|
||||
}) => {
|
||||
// Hide icon when disabled (per design spec)
|
||||
const shouldShowIcon = showIcon && !disabled;
|
||||
@@ -137,8 +123,6 @@ export const Button: React.FC<ButtonProps> = ({
|
||||
{
|
||||
'bds-btn--disabled': disabled,
|
||||
'bds-btn--no-icon': !shouldShowIcon,
|
||||
'bds-btn--force-color': forceColor,
|
||||
'bds-btn--no-padding': forceNoPadding,
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
@@ -18,17 +18,14 @@
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
|
||||
// Color variant map: (variant-name: (light-mode-bg, dark-mode-bg))
|
||||
// null for dark-mode-bg means no change in dark mode
|
||||
$bds-card-stat-variants: (
|
||||
'lilac': ($lilac-300, null),
|
||||
'green': ($green-300, null),
|
||||
'light-gray': (#E6EAF0, #CAD4DF),
|
||||
'dark-gray': (#CAD4DF, #8A919A)
|
||||
);
|
||||
// Color variants
|
||||
$bds-card-stat-lilac-bg: $lilac-300;
|
||||
$bds-card-stat-green-bg: $green-300; // #EAFCF1
|
||||
$bds-card-stat-light-gray-bg: #E6EAF0; // Light gray
|
||||
$bds-card-stat-dark-gray-bg: #CAD4DF; // Dark gray
|
||||
|
||||
// Text colors
|
||||
$bds-card-stat-text: $black; // Neutral black
|
||||
$bds-card-stat-text: $black; // Neutral black
|
||||
|
||||
// Spacing
|
||||
$bds-card-stat-gap: 8px;
|
||||
@@ -47,56 +44,53 @@ $bds-card-stat-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
gap: 4px;
|
||||
|
||||
// Visual - default to lilac
|
||||
background-color: nth(map-get($bds-card-stat-variants, 'lilac'), 1);
|
||||
|
||||
|
||||
// Visual
|
||||
background-color: $bds-card-stat-lilac-bg; // Default
|
||||
|
||||
// Typography
|
||||
color: $bds-card-stat-text;
|
||||
|
||||
|
||||
// Transitions
|
||||
transition: transform $bds-card-stat-transition-duration $bds-card-stat-transition-timing;
|
||||
|
||||
// Tablet (md) breakpoint
|
||||
@include media-breakpoint-up(md) {
|
||||
padding: 12px;
|
||||
|
||||
// Content wrapper
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
gap: 4px;
|
||||
|
||||
// Tablet (md) breakpoint
|
||||
@include media-breakpoint-up(md) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
// Desktop (lg+) breakpoint
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// Desktop (lg+) breakpoint
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
||||
// Text section
|
||||
&__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
// Statistic (large number)
|
||||
&__statistic {
|
||||
@include type(display-lg);
|
||||
margin-bottom: 0;
|
||||
|
||||
sup {
|
||||
top: -0.4em;
|
||||
font-size: 0.7em;
|
||||
|
||||
// Numeric superscript modifier - smaller size for numbers
|
||||
&.bds-card-stat__superscript--numeric {
|
||||
font-size: 0.5em;
|
||||
top: -0.75em;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Buttons section
|
||||
&__buttons {
|
||||
display: flex;
|
||||
@@ -107,21 +101,43 @@ $bds-card-stat-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Color Variants (generated from map)
|
||||
// Color Variants
|
||||
// =============================================================================
|
||||
|
||||
@each $variant, $colors in $bds-card-stat-variants {
|
||||
$light-bg: nth($colors, 1);
|
||||
$dark-bg: nth($colors, 2);
|
||||
// Lilac variant (default)
|
||||
.bds-card-stat--lilac {
|
||||
background-color: $bds-card-stat-lilac-bg;
|
||||
}
|
||||
|
||||
.bds-card-stat--#{$variant} {
|
||||
background-color: $light-bg;
|
||||
}
|
||||
// Green variant
|
||||
.bds-card-stat--green {
|
||||
background-color: $bds-card-stat-green-bg;
|
||||
}
|
||||
|
||||
// Dark mode override (only if dark-mode color is defined)
|
||||
@if $dark-bg != null {
|
||||
html.dark .bds-card-stat--#{$variant} {
|
||||
background-color: $dark-bg;
|
||||
// Light gray variant
|
||||
.bds-card-stat--light-gray {
|
||||
background-color: $bds-card-stat-light-gray-bg;
|
||||
}
|
||||
|
||||
// Dark gray variant
|
||||
.bds-card-stat--dark-gray {
|
||||
background-color: $bds-card-stat-dark-gray-bg;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dark Mode Styles
|
||||
// =============================================================================
|
||||
|
||||
html.dark {
|
||||
.bds-card-stat {
|
||||
// Light gray variant gets dark-gray background in dark mode
|
||||
&--light-gray {
|
||||
background-color: $bds-card-stat-dark-gray-bg; // Darker gray for dark mode
|
||||
}
|
||||
|
||||
// Dark gray variant gets darker gray background in dark mode
|
||||
&--dark-gray {
|
||||
background-color: #8A919A; // Darker gray for dark mode
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,12 +149,11 @@ $bds-card-stat-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
.bds-card-stat {
|
||||
// Base (mobile) - ~200px height
|
||||
min-height: 200px;
|
||||
|
||||
|
||||
// Tablet (md) - ~208px height
|
||||
@include media-breakpoint-up(md) {
|
||||
min-height: 208px;
|
||||
}
|
||||
|
||||
// Desktop (lg+) - ~298px height (more vertical space)
|
||||
@include media-breakpoint-up(lg) {
|
||||
min-height: 298px;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Button } from '../Button';
|
||||
import { PageGridCol } from '../PageGrid/page-grid';
|
||||
import type { PageGridBreakpoint } from '../PageGrid/page-grid';
|
||||
|
||||
interface ButtonConfig {
|
||||
/** Button label text */
|
||||
@@ -12,14 +10,11 @@ interface ButtonConfig {
|
||||
href?: string;
|
||||
}
|
||||
|
||||
/** Responsive span configuration for PageGridCol */
|
||||
type SpanConfig = Partial<Record<PageGridBreakpoint, number>>;
|
||||
|
||||
export interface CardStatProps {
|
||||
/** The main statistic to display (e.g., "6 Million+") */
|
||||
statistic: string;
|
||||
/** Superscript text for the statistic (symbols like '*', '+' or numeric strings like '1', '12') */
|
||||
superscript?: string;
|
||||
/** Superscript text for the statistic */
|
||||
superscript?: '*' | '+' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '0';
|
||||
/** Descriptive label for the statistic */
|
||||
label: string;
|
||||
/** Background color variant
|
||||
@@ -33,8 +28,6 @@ export interface CardStatProps {
|
||||
primaryButton?: ButtonConfig;
|
||||
/** Secondary button configuration */
|
||||
secondaryButton?: ButtonConfig;
|
||||
/** Grid column span configuration - defaults to { base: 4, md: 4, lg: 4 } */
|
||||
span?: SpanConfig;
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
@@ -59,9 +52,6 @@ export interface CardStatProps {
|
||||
* primaryButton={{ label: "Learn More", href: "/docs" }}
|
||||
* />
|
||||
*/
|
||||
/** Default span configuration */
|
||||
const DEFAULT_SPAN: SpanConfig = { base: 4, md: 4, lg: 4 };
|
||||
|
||||
export const CardStat: React.FC<CardStatProps> = ({
|
||||
statistic,
|
||||
superscript,
|
||||
@@ -69,7 +59,6 @@ export const CardStat: React.FC<CardStatProps> = ({
|
||||
variant = 'lilac',
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
span = DEFAULT_SPAN,
|
||||
className = '',
|
||||
}) => {
|
||||
// Build class names using BEM with bds namespace
|
||||
@@ -83,48 +72,57 @@ export const CardStat: React.FC<CardStatProps> = ({
|
||||
|
||||
const hasButtons = primaryButton || secondaryButton;
|
||||
|
||||
// Check if superscript is a number (one or more digits), excluding + or - signs
|
||||
const isNumericSuperscript = superscript && /^[0-9]+$/.test(superscript);
|
||||
|
||||
return (
|
||||
<PageGridCol span={span}>
|
||||
<div className={classNames}>
|
||||
<div className={classNames}>
|
||||
<div className="bds-card-stat__content">
|
||||
{/* Text section */}
|
||||
<div className="bds-card-stat__text">
|
||||
<div className="bds-card-stat__statistic">
|
||||
{statistic}{superscript && <sup className={isNumericSuperscript ? 'bds-card-stat__superscript--numeric' : ''}>{superscript}</sup>}</div>
|
||||
{statistic}{superscript && <sup>{superscript}</sup>}</div>
|
||||
<div className="body-r">{label}</div>
|
||||
</div>
|
||||
|
||||
{/* Buttons section */}
|
||||
{/* Buttons section */}
|
||||
{hasButtons && (
|
||||
<div className="bds-card-stat__buttons">
|
||||
{primaryButton && (
|
||||
<Button
|
||||
forceColor
|
||||
variant="primary"
|
||||
color="black"
|
||||
href={primaryButton.href}
|
||||
onClick={primaryButton.onClick}
|
||||
>
|
||||
{primaryButton.label}
|
||||
</Button>
|
||||
primaryButton.href ? (
|
||||
<a href={primaryButton.href}>
|
||||
<Button variant="primary" color="black">
|
||||
{primaryButton.label}
|
||||
</Button>
|
||||
</a>
|
||||
) : (
|
||||
<Button
|
||||
variant="primary"
|
||||
color="black"
|
||||
onClick={primaryButton.onClick}
|
||||
>
|
||||
{primaryButton.label}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<Button
|
||||
forceColor
|
||||
variant="secondary"
|
||||
color="black"
|
||||
href={secondaryButton.href}
|
||||
onClick={secondaryButton.onClick}
|
||||
>
|
||||
{secondaryButton.label}
|
||||
</Button>
|
||||
secondaryButton.href ? (
|
||||
<a href={secondaryButton.href}>
|
||||
<Button variant="secondary" color="black">
|
||||
{secondaryButton.label}
|
||||
</Button>
|
||||
</a>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
color="black"
|
||||
onClick={secondaryButton.onClick}
|
||||
>
|
||||
{secondaryButton.label}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
// BDS CarouselButton Component Styles
|
||||
// Brand Design System - Circular navigation button for carousels
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-carousel-button - Base button
|
||||
// .bds-carousel-button--prev - Previous/left direction
|
||||
// .bds-carousel-button--next - Next/right direction
|
||||
// .bds-carousel-button--neutral - Neutral/gray color variant
|
||||
// .bds-carousel-button--green - Green color variant
|
||||
// .bds-carousel-button--black - Black/white color variant
|
||||
// .bds-carousel-button--disabled - Disabled state modifier
|
||||
// .bds-carousel-button__arrow-icon - Arrow icon element
|
||||
//
|
||||
// Note: This file is imported within xrpl.scss after Bootstrap and project
|
||||
// variables are loaded, so $grid-breakpoints, colors, and mixins are available.
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
|
||||
// Button dimensions
|
||||
$bds-carousel-button-size-sm: 37px; // Mobile/Tablet
|
||||
$bds-carousel-button-size-lg: 40px; // Desktop
|
||||
|
||||
// Transition
|
||||
$bds-carousel-button-transition: 200ms cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
|
||||
// =============================================================================
|
||||
// Color Variant Configuration Maps
|
||||
// =============================================================================
|
||||
|
||||
// Dark Mode color variants
|
||||
$bds-carousel-button-variants-dark: (
|
||||
'green': (
|
||||
'bg': $green-300,
|
||||
'color': $black,
|
||||
'hover': $green-400,
|
||||
'active': $green-300,
|
||||
'disabled-bg': $green-500,
|
||||
'disabled-color': #F0F3F7,
|
||||
'disabled-opacity': 0.5
|
||||
),
|
||||
'neutral': (
|
||||
'bg': $gray-300,
|
||||
'color': $black,
|
||||
'hover': $gray-400,
|
||||
'active': $gray-300,
|
||||
'disabled-bg': $gray-500,
|
||||
'disabled-color': $gray-300,
|
||||
'disabled-opacity': 0.5
|
||||
),
|
||||
'black': (
|
||||
'bg': $white,
|
||||
'color': $black,
|
||||
'hover': $gray-300,
|
||||
'active': $white,
|
||||
'disabled-bg': $gray-500,
|
||||
'disabled-color': null,
|
||||
'disabled-opacity': 0.5
|
||||
)
|
||||
);
|
||||
|
||||
// Light Mode color variants
|
||||
$bds-carousel-button-variants-light: (
|
||||
'green': (
|
||||
'bg': $green-300,
|
||||
'color': $black,
|
||||
'hover': $green-200,
|
||||
'active': $green-300,
|
||||
'disabled-bg': $green-100,
|
||||
'disabled-color': $gray-300,
|
||||
'disabled-opacity': 1
|
||||
),
|
||||
'neutral': (
|
||||
'bg': $gray-300,
|
||||
'color': $black,
|
||||
'hover': $gray-200,
|
||||
'active': $gray-300,
|
||||
'disabled-bg': $gray-100,
|
||||
'disabled-color': $gray-300,
|
||||
'disabled-opacity': 1
|
||||
),
|
||||
'black': (
|
||||
'bg': $black,
|
||||
'color': $white,
|
||||
'hover': $gray-500,
|
||||
'active': $black,
|
||||
'disabled-bg': #F0F3F7,
|
||||
'disabled-color': $gray-300,
|
||||
'disabled-opacity': 1
|
||||
)
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// Mixin: Apply Color Variant Styles
|
||||
// =============================================================================
|
||||
|
||||
@mixin carousel-button-variant($variant-name, $config) {
|
||||
.bds-carousel-button--#{$variant-name} {
|
||||
background-color: map-get($config, 'bg');
|
||||
color: map-get($config, 'color');
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: map-get($config, 'hover');
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: map-get($config, 'active');
|
||||
}
|
||||
|
||||
&.bds-carousel-button--disabled,
|
||||
&:disabled {
|
||||
background-color: map-get($config, 'disabled-bg') !important;
|
||||
@if map-get($config, 'disabled-color') {
|
||||
color: map-get($config, 'disabled-color') !important;
|
||||
}
|
||||
opacity: map-get($config, 'disabled-opacity') !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Base Button Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-button {
|
||||
// Reset button styles
|
||||
appearance: none;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
|
||||
// Layout
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: $bds-carousel-button-size-sm;
|
||||
height: $bds-carousel-button-size-sm;
|
||||
|
||||
// Transition
|
||||
transition: background-color $bds-carousel-button-transition,
|
||||
opacity $bds-carousel-button-transition;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
width: $bds-carousel-button-size-lg;
|
||||
height: $bds-carousel-button-size-lg;
|
||||
}
|
||||
|
||||
// Focus styles
|
||||
&:focus {
|
||||
outline: 2px solid $white;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $white;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Arrow Icon
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-button__arrow-icon {
|
||||
width: 18px;
|
||||
height: 16px;
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
width: 18px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DARK MODE (Default) - Color Variants
|
||||
// =============================================================================
|
||||
|
||||
// Generate all dark mode variant styles using the mixin
|
||||
@each $variant-name, $config in $bds-carousel-button-variants-dark {
|
||||
@include carousel-button-variant($variant-name, $config);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LIGHT MODE (html.light) - Color Variants
|
||||
// =============================================================================
|
||||
|
||||
html.light {
|
||||
// Focus styles - Light Mode
|
||||
.bds-carousel-button {
|
||||
&:focus {
|
||||
outline-color: $gray-900;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline-color: $gray-900;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate all light mode variant overrides using the mixin
|
||||
@each $variant-name, $config in $bds-carousel-button-variants-light {
|
||||
@include carousel-button-variant($variant-name, $config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
/**
|
||||
* Props for the CarouselButton component
|
||||
*/
|
||||
export interface CarouselButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
/** Arrow direction */
|
||||
direction: 'prev' | 'next';
|
||||
/** Color variant */
|
||||
variant: 'neutral' | 'green' | 'black';
|
||||
}
|
||||
|
||||
/**
|
||||
* CarouselButton Component
|
||||
*
|
||||
* A circular navigation button for carousel components.
|
||||
* Displays left/right arrow icons and supports multiple color variants.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <CarouselButton
|
||||
* direction="prev"
|
||||
* variant="neutral"
|
||||
* onClick={() => scroll('prev')}
|
||||
* disabled={!canScrollPrev}
|
||||
* aria-label="Previous items"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const CarouselButton: React.FC<CarouselButtonProps> = ({
|
||||
direction,
|
||||
variant,
|
||||
disabled,
|
||||
className,
|
||||
...buttonProps
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(
|
||||
'bds-carousel-button',
|
||||
`bds-carousel-button--${direction}`,
|
||||
`bds-carousel-button--${variant}`,
|
||||
{ 'bds-carousel-button--disabled': disabled },
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
{...buttonProps}
|
||||
>
|
||||
{direction === 'prev' ? <CarouselArrowIconLeft /> : <CarouselArrowIconRight />}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
CarouselButton.displayName = 'CarouselButton';
|
||||
|
||||
/**
|
||||
* SVG Arrow Icon for carousel navigation - Right arrow
|
||||
*/
|
||||
export const CarouselArrowIconRight: React.FC = () => (
|
||||
<svg
|
||||
className="bds-carousel-button__arrow-icon"
|
||||
width="18"
|
||||
height="16"
|
||||
viewBox="0 0 18 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M9.33387 1.33461L15.9999 8.00058L9.33387 14.6666M15.9982 7.99893L-0.000149269 7.99893"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
CarouselArrowIconRight.displayName = 'CarouselArrowIconRight';
|
||||
|
||||
/**
|
||||
* SVG Arrow Icon for carousel navigation - Left arrow
|
||||
*/
|
||||
export const CarouselArrowIconLeft: React.FC = () => (
|
||||
<svg
|
||||
className="bds-carousel-button__arrow-icon"
|
||||
width="18"
|
||||
height="15"
|
||||
viewBox="0 0 18 15"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M7.72667 0.530285L1.0607 7.19626L7.72667 13.8622M1.06235 7.19461L17.0607 7.19461"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeMiterlimit="10"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
CarouselArrowIconLeft.displayName = 'CarouselArrowIconLeft';
|
||||
|
||||
export default CarouselButton;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export {
|
||||
CarouselButton,
|
||||
CarouselArrowIconLeft,
|
||||
CarouselArrowIconRight,
|
||||
type CarouselButtonProps
|
||||
} from './CarouselButton';
|
||||
export { default } from './CarouselButton';
|
||||
|
||||
@@ -17,21 +17,6 @@ $bds-grid-gutter: 8px;
|
||||
width: calc(((100% - (#{$bds-grid-gutter} * (#{$columns} - 1))) / #{$columns}) * #{$size} + (#{$bds-grid-gutter} * (#{$size} - 1)));
|
||||
}
|
||||
|
||||
// Custom mixin that accounts for gap spacing in offset calculations
|
||||
@mixin bds-make-col-offset($size, $columns) {
|
||||
// Calculate margin-left accounting for gap spacing
|
||||
// Formula: (width per column * offset) + (gap * offset)
|
||||
// This accounts for both the column widths AND the gaps between them
|
||||
// Total available width: 100% - (gap * total gaps in grid)
|
||||
// Width per column: available width / columns
|
||||
// For offset of $size: (width per column * size) + (gap * size)
|
||||
@if $size == 0 {
|
||||
margin-left: 0;
|
||||
} @else {
|
||||
margin-left: calc(((100% - (#{$bds-grid-gutter} * (#{$columns} - 1))) / #{$columns}) * #{$size} + (#{$bds-grid-gutter} * #{$size}));
|
||||
}
|
||||
}
|
||||
|
||||
@mixin bds-grid-generate-cols($columns, $suffix: null) {
|
||||
@for $i from 1 through $columns {
|
||||
$selector: if($suffix, ".bds-grid__col-#{$suffix}-#{$i}", ".bds-grid__col-#{$i}");
|
||||
@@ -64,7 +49,7 @@ $bds-grid-gutter: 8px;
|
||||
$selector: if($suffix, ".bds-grid__offset-#{$suffix}-#{$i}", ".bds-grid__offset-#{$i}");
|
||||
|
||||
#{$selector} {
|
||||
@include bds-make-col-offset($i, $columns);
|
||||
@include make-col-offset($i, $columns);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import clsx from "clsx";
|
||||
type PageGridElementProps = React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
// Define the standard PageGrid breakpoints
|
||||
export type PageGridBreakpoint = "base" | "sm" | "md" | "lg" | "xl";
|
||||
type PageGridBreakpoint = "base" | "sm" | "md" | "lg" | "xl";
|
||||
|
||||
// Define the ResponsiveValue type using Partial<Record> for breakpoints
|
||||
type ResponsiveValue<T> = T | Partial<Record<PageGridBreakpoint, T>>;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { forwardRef, useMemo, memo } from "react";
|
||||
import { CardIconProps, CardIcon } from "../../components/CardIcon";
|
||||
import { CardIconProps, CardIcon } from "../CardIcon";
|
||||
import clsx from "clsx";
|
||||
import { PageGrid } from "../../components/PageGrid/page-grid";
|
||||
import type { PageGridColProps } from "../../components/PageGrid/page-grid";
|
||||
import { getCardKey, isEnvironment } from "../../utils";
|
||||
import { PageGrid } from "../PageGrid/page-grid";
|
||||
import type { PageGridColProps } from "../PageGrid/page-grid";
|
||||
|
||||
/**
|
||||
* Card icon props without the variant prop (which is controlled at section level)
|
||||
@@ -31,6 +30,22 @@ export interface SmallTilesSectionProps
|
||||
*/
|
||||
const SPACER_THRESHOLD = 8 as const;
|
||||
|
||||
/**
|
||||
* Generates a stable key for a card based on its properties.
|
||||
* Falls back to index if no stable identifier is available.
|
||||
*/
|
||||
const getCardKey = (
|
||||
card: ConstrainedCardIconProps,
|
||||
index: number
|
||||
): string | number => {
|
||||
// Prefer href as a stable identifier
|
||||
if (card.href) return card.href;
|
||||
// Use label as identifier if available
|
||||
if (card.label) return `${card.label}-${index}`;
|
||||
// Fallback to index (acceptable since cards array is readonly)
|
||||
return index;
|
||||
};
|
||||
|
||||
/**
|
||||
* Memoized card item component to prevent unnecessary re-renders
|
||||
* Only re-renders when card data or variant changes
|
||||
@@ -82,9 +97,7 @@ export const SmallTilesSection = forwardRef<
|
||||
|
||||
// Early return for empty cards array
|
||||
if (cardsCount === 0) {
|
||||
if (isEnvironment("development")) {
|
||||
console.warn("SmallTilesSection: No cards provided");
|
||||
}
|
||||
console.warn("SmallTilesSection: No cards provided");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -128,7 +141,7 @@ export const SmallTilesSection = forwardRef<
|
||||
() =>
|
||||
cards.map((card, index) => (
|
||||
<CardListItem
|
||||
key={getCardKey(card.href || card.label, index, "small-tile")}
|
||||
key={getCardKey(card, index)}
|
||||
card={card}
|
||||
variant={cardVariant}
|
||||
/>
|
||||
@@ -1,83 +0,0 @@
|
||||
.bds-standard-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
height: 100%; // Stretch to fill parent column (ensures equal heights in grid rows)
|
||||
// aspect-ratio sets a preferred ratio but allows growth if content requires more space
|
||||
// The card will be at least this tall, but can grow taller if needed
|
||||
aspect-ratio: 4/3;
|
||||
gap: 16px;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
aspect-ratio: 1;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
&#{&}--neutral {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
|
||||
&#{&}--green {
|
||||
background-color: $green-300;
|
||||
}
|
||||
|
||||
&#{&}--yellow {
|
||||
background-color: $yellow-100;
|
||||
}
|
||||
|
||||
&#{&}--blue {
|
||||
background-color: $blue-100;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
color: $bds-btn-neutral-black;
|
||||
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&__headline {
|
||||
@include wordbreak('break-word');
|
||||
color: $bds-btn-neutral-black;
|
||||
|
||||
|
||||
margin: 0;
|
||||
|
||||
&:before{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import clsx from "clsx";
|
||||
import Button from "../Button/Button";
|
||||
import {
|
||||
DesignConstrainedButtonProps,
|
||||
isEnvironment,
|
||||
isEmpty,
|
||||
} from "../../utils";
|
||||
import { DesignConstrainedCallToActionsProps } from "shared/utils/types";
|
||||
|
||||
/**
|
||||
* Available background color variants for StandardCard:
|
||||
* - 'neutral': Default neutral background
|
||||
* - 'green': XRPL brand green background
|
||||
* - 'yellow': Yellow background
|
||||
* - 'blue': Blue background
|
||||
*/
|
||||
export type StandardCardVariant = "neutral" | "green" | "yellow" | "blue";
|
||||
|
||||
export interface StandardCardProps
|
||||
extends
|
||||
React.ComponentPropsWithoutRef<"article">,
|
||||
DesignConstrainedCallToActionsProps {
|
||||
headline: React.ReactNode;
|
||||
/** Background color variant */
|
||||
variant: StandardCardVariant;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* StandardCard props without the variant prop.
|
||||
* Used by StandardCardGroupSection to ensure uniform variant across all cards.
|
||||
*/
|
||||
export type StandardCardPropsWithoutVariant = Omit<
|
||||
StandardCardProps,
|
||||
"variant"
|
||||
>;
|
||||
|
||||
const StandardCard = forwardRef<HTMLElement, StandardCardProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
headline,
|
||||
variant = "neutral",
|
||||
callsToAction,
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const [primaryButton, secondaryButton] = callsToAction;
|
||||
|
||||
const hasButtons = callsToAction.some((button) => !isEmpty(button));
|
||||
|
||||
if (!headline) {
|
||||
if (isEnvironment("development")) {
|
||||
console.warn("Headline is required for StandardCard");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<article
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
"bds-standard-card",
|
||||
`bds-standard-card--${variant}`,
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<div className="bds-standard-card__content">
|
||||
<h2 className="bds-standard-card__headline sh-md-r">{headline}</h2>
|
||||
{!isEmpty(children) && (
|
||||
<div className="bds-standard-card__description body-l">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{hasButtons && (
|
||||
<div className="bds-standard-card__buttons">
|
||||
{primaryButton && (
|
||||
<Button
|
||||
{...primaryButton}
|
||||
variant="primary"
|
||||
color="black"
|
||||
forceColor={true}
|
||||
/>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<Button
|
||||
{...secondaryButton}
|
||||
variant="tertiary"
|
||||
color="black"
|
||||
forceColor={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default StandardCard;
|
||||
@@ -1,183 +0,0 @@
|
||||
# TextCard Component
|
||||
|
||||
A card component with a title at the top and description at the bottom. Used within the CardsTwoColumn pattern to display content in a 2×2 grid, but can also be used independently.
|
||||
|
||||
## Features
|
||||
|
||||
- **6 Color Variants**: Green, neutral-light, neutral-dark, lilac, yellow, and blue backgrounds
|
||||
- **Interactive States**: Default, hover (window shade animation), focus, and pressed states
|
||||
- **Responsive Design**: Adapts height and padding across breakpoints
|
||||
- **Optional Link**: Can be made clickable with an `href` prop
|
||||
- **Flexible Content**: Title and description support ReactNode
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { TextCard } from 'shared/components/TextCard';
|
||||
|
||||
// Basic usage
|
||||
<TextCard
|
||||
title="Institutions"
|
||||
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products."
|
||||
color="green"
|
||||
/>
|
||||
|
||||
// As a clickable card
|
||||
<TextCard
|
||||
title="Developers"
|
||||
description="Build decentralized applications with comprehensive documentation."
|
||||
href="/developers"
|
||||
color="lilac"
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `title` | `ReactNode` | *required* | Card title (heading-lg typography) |
|
||||
| `description` | `ReactNode` | - | Card description (body-l typography) |
|
||||
| `href` | `string` | - | Optional link URL (makes card clickable) |
|
||||
| `color` | `TextCardColor` | `'neutral-light'` | Background color variant |
|
||||
| `disabled` | `boolean` | `false` | Whether the card is disabled |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
|
||||
### TextCardColor
|
||||
|
||||
```tsx
|
||||
type TextCardColor = 'green' | 'neutral-light' | 'neutral-dark' | 'lilac' | 'yellow' | 'blue';
|
||||
```
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
| Breakpoint | Height | Padding |
|
||||
|------------|--------|---------|
|
||||
| Desktop (≥992px) | 340px | 24px |
|
||||
| Tablet (576-991px) | 309px | 20px |
|
||||
| Mobile (<576px) | 274px | 16px |
|
||||
|
||||
## Color Variants & States
|
||||
|
||||
Each color variant has four interactive states with a "window shade" hover animation.
|
||||
|
||||
### Light Mode
|
||||
|
||||
| Variant | Default | Hover | Focus | Pressed |
|
||||
|---------|---------|-------|-------|---------|
|
||||
| `green` | `$green-200` (#70EE97) | `$green-300` (#21E46B) | `$green-300` (#21E46B) | `$green-400` (#0DAA3E) |
|
||||
| `neutral-light` | `$gray-200` (#E6EAF0) | `$gray-300` (#CAD4DF) | `$gray-300` (#CAD4DF) | `$gray-400` (#8A919A) |
|
||||
| `neutral-dark` | `$gray-300` (#CAD4DF) | `$gray-200` (#E6EAF0) | `$gray-200` (#E6EAF0) | `$gray-400` (#8A919A) |
|
||||
| `lilac` | `$lilac-200` (#D9CAFF) | `$lilac-300` (#C0A7FF) | `$lilac-300` (#C0A7FF) | `$lilac-400` (#7649E3) |
|
||||
| `yellow` | `$yellow-100` (#F3F1EB) | `$yellow-200` (#E6F1A7) | `$yellow-200` (#E6F1A7) | `$yellow-300` (#DBF15E) |
|
||||
| `blue` | `$blue-100` (#EDF4FF) | `$blue-200` (#93BFF1) | `$blue-200` (#93BFF1) | `$blue-300` (#428CFF) |
|
||||
|
||||
### Dark Mode
|
||||
|
||||
| Variant | Default | Hover | Focus | Pressed |
|
||||
|---------|---------|-------|-------|---------|
|
||||
| `green` | `$green-200` (#70EE97) | `$green-300` (#21E46B) | `$green-300` (#21E46B) | `$green-400` (#0DAA3E) |
|
||||
| `neutral-light` | `$gray-300` (#CAD4DF) | `$gray-200` (#E6EAF0) | `$gray-200` (#E6EAF0) | `$gray-400` (#8A919A) |
|
||||
| `neutral-dark` | `$gray-400` (#8A919A) | `$gray-300` (#CAD4DF) | `$gray-300` (#CAD4DF) | `$gray-500` (#72777E) |
|
||||
| `lilac` | `$lilac-200` (#D9CAFF) | `$lilac-300` (#C0A7FF) | `$lilac-300` (#C0A7FF) | `$lilac-400` (#7649E3) |
|
||||
| `yellow` | `$yellow-100` (#F3F1EB) | `$yellow-200` (#E6F1A7) | `$yellow-200` (#E6F1A7) | `$yellow-300` (#DBF15E) |
|
||||
| `blue` | `$blue-100` (#EDF4FF) | `$blue-200` (#93BFF1) | `$blue-200` (#93BFF1) | `$blue-300` (#428CFF) |
|
||||
|
||||
### Disabled State
|
||||
|
||||
| Mode | Background | Text |
|
||||
|------|------------|------|
|
||||
| Light | `$gray-100` (#F0F3F7) | `$gray-500` (#72777E) |
|
||||
| Dark | `rgba($gray-500, 0.3)` | Default text color |
|
||||
|
||||
### Focus Outline
|
||||
|
||||
| Mode | Focus Outline |
|
||||
|------|---------------|
|
||||
| Light Mode | 2px solid black outline with 2px offset |
|
||||
| Dark Mode | 2px solid white outline with 2px offset |
|
||||
|
||||
## CSS Classes
|
||||
|
||||
```
|
||||
.bds-text-card // Base card container
|
||||
.bds-text-card--green // Green variant
|
||||
.bds-text-card--neutral-light // Neutral light variant
|
||||
.bds-text-card--neutral-dark // Neutral dark variant
|
||||
.bds-text-card--lilac // Lilac variant
|
||||
.bds-text-card--yellow // Yellow variant
|
||||
.bds-text-card--blue // Blue variant
|
||||
.bds-text-card__overlay // Hover animation overlay
|
||||
.bds-text-card__header // Title container
|
||||
.bds-text-card__title // Title element
|
||||
.bds-text-card__footer // Description container
|
||||
.bds-text-card__description // Description element
|
||||
```
|
||||
|
||||
## Typography
|
||||
|
||||
- **Title**: Uses `h-lg` class (heading-lg, Tobias Light font)
|
||||
- Desktop: 48px / 52.8px line-height
|
||||
- Tablet: 42px / 46.2px line-height
|
||||
- Mobile: 36px / 39.6px line-height
|
||||
|
||||
- **Description**: Uses `body-l` class (Booton Light font)
|
||||
- All breakpoints: 18px / 26.1px line-height
|
||||
- Max width: 478px (from Figma)
|
||||
|
||||
## Examples
|
||||
|
||||
### All Color Variants
|
||||
|
||||
```tsx
|
||||
<TextCard title="Green" description="$green-200" color="green" />
|
||||
<TextCard title="Neutral Light" description="$gray-200" color="neutral-light" />
|
||||
<TextCard title="Neutral Dark" description="$gray-300" color="neutral-dark" />
|
||||
<TextCard title="Lilac" description="$lilac-200" color="lilac" />
|
||||
<TextCard title="Yellow" description="$yellow-100" color="yellow" />
|
||||
<TextCard title="Blue" description="$blue-100" color="blue" />
|
||||
```
|
||||
|
||||
### Disabled Card
|
||||
|
||||
```tsx
|
||||
<TextCard
|
||||
title="Disabled Card"
|
||||
description="This card is disabled and cannot be interacted with."
|
||||
color="neutral-light"
|
||||
disabled
|
||||
/>
|
||||
```
|
||||
|
||||
### Within CardsTwoColumn Pattern
|
||||
|
||||
```tsx
|
||||
import { CardsTwoColumn } from 'shared/patterns/CardsTwoColumn';
|
||||
|
||||
<CardsTwoColumn
|
||||
title="Section Title"
|
||||
description="Section description text."
|
||||
cards={[
|
||||
{ title: "Card 1", description: "Description 1", color: "lilac" },
|
||||
{ title: "Card 2", description: "Description 2", color: "neutral-light" },
|
||||
{ title: "Card 3", description: "Description 3", color: "neutral-dark" },
|
||||
{ title: "Card 4", description: "Description 4", color: "green" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
- `TextCard.tsx` - Component implementation
|
||||
- `TextCard.scss` - Styles with color variants and responsive breakpoints
|
||||
- `index.ts` - Barrel exports
|
||||
- `TextCard.md` - This documentation
|
||||
|
||||
## Related Components
|
||||
|
||||
- **CardsTwoColumn**: Pattern that uses TextCard in a 2×2 grid layout
|
||||
|
||||
## Design References
|
||||
|
||||
- **Figma Design**: [Section Cards - Two Column](https://www.figma.com/design/MP5gjNp7yPBnKBKleb8LRL/Section-Cards---Two-Column)
|
||||
- **Component Location**: `shared/components/TextCard/`
|
||||
|
||||
@@ -1,389 +0,0 @@
|
||||
// BDS TextCard Component Styles
|
||||
// Brand Design System - Card with title and description
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-text-card - Base card container
|
||||
// .bds-text-card--green - Green variant
|
||||
// .bds-text-card--neutral-light - Neutral light variant
|
||||
// .bds-text-card--neutral-dark - Neutral dark variant
|
||||
// .bds-text-card--lilac - Lilac variant
|
||||
// .bds-text-card--yellow - Yellow variant
|
||||
// .bds-text-card--blue - Blue variant
|
||||
// .bds-text-card__overlay - Hover gradient overlay (window shade animation)
|
||||
// .bds-text-card__title - Card title (heading-lg)
|
||||
// .bds-text-card__description - Card description (body-l)
|
||||
//
|
||||
// Color states from Figma (Light Mode):
|
||||
// - Green: Default $green-200, Hover $green-300, Pressed $green-400
|
||||
// - NeutralLight: Default $gray-200, Hover $gray-300, Pressed $gray-400
|
||||
// - NeutralDark: Default $gray-300, Hover $gray-400, Pressed $gray-500
|
||||
// - Lilac: Default $lilac-200, Hover $lilac-300, Pressed $lilac-400
|
||||
// - Yellow: Default $yellow-100, Hover $yellow-200, Pressed $yellow-300
|
||||
// - Blue: Default $blue-100, Hover $blue-200, Pressed $blue-300
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens from Figma
|
||||
// =============================================================================
|
||||
|
||||
// Card internal padding
|
||||
$bds-text-card-padding-mobile: 16px;
|
||||
$bds-text-card-padding-tablet: 20px;
|
||||
$bds-text-card-padding-desktop: 24px;
|
||||
|
||||
// Card heights (fixed per breakpoint)
|
||||
$bds-text-card-height-mobile: 274px;
|
||||
$bds-text-card-height-tablet: 309px;
|
||||
$bds-text-card-height-desktop: 340px;
|
||||
|
||||
// Card description max-width (from Figma)
|
||||
$bds-text-card-description-max-width: 478px;
|
||||
|
||||
// Colors - Light Mode (from Figma)
|
||||
$bds-text-color: $black; // #141414 - Neutral black
|
||||
|
||||
// =============================================================================
|
||||
// TextCard Component
|
||||
// =============================================================================
|
||||
|
||||
.bds-text-card {
|
||||
// Use shared window shade animation base
|
||||
@include bds-window-shade-base;
|
||||
|
||||
// Layout
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
text-decoration: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
// Mobile dimensions and padding
|
||||
height: $bds-text-card-height-mobile;
|
||||
padding: $bds-text-card-padding-mobile;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
height: $bds-text-card-height-tablet;
|
||||
padding: $bds-text-card-padding-tablet;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
height: $bds-text-card-height-desktop;
|
||||
padding: $bds-text-card-padding-desktop;
|
||||
}
|
||||
|
||||
// Interaction
|
||||
cursor: pointer;
|
||||
|
||||
// Focus styles - Light Mode
|
||||
@include bds-focus-styles($black);
|
||||
|
||||
// Hover state for linked cards
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Overlay (Window Shade Animation)
|
||||
// =============================================================================
|
||||
|
||||
.bds-text-card__overlay {
|
||||
@include bds-window-shade-overlay;
|
||||
}
|
||||
|
||||
// Hover state: reveal overlay
|
||||
.bds-text-card:hover .bds-text-card__overlay {
|
||||
@include bds-window-shade-revealed;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Color Variants - Light Mode
|
||||
// =============================================================================
|
||||
|
||||
// Green Variant
|
||||
// Default: $green-200, Hover: $green-300, Pressed: $green-400
|
||||
.bds-text-card--green {
|
||||
background-color: $green-200;
|
||||
|
||||
// Preserve background on focus (overrides light theme transparent rule)
|
||||
&:focus {
|
||||
background-color: $green-200;
|
||||
}
|
||||
|
||||
.bds-text-card__overlay {
|
||||
background-color: $green-300;
|
||||
}
|
||||
|
||||
&:active {
|
||||
.bds-text-card__overlay {
|
||||
background-color: $green-400;
|
||||
@include bds-window-shade-revealed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Neutral Light Variant (Light Mode)
|
||||
// Default: $gray-200, Hover: $gray-300, Focus: $gray-300, Pressed: $gray-400
|
||||
.bds-text-card--neutral-light {
|
||||
background-color: $gray-200;
|
||||
|
||||
// Focus uses hover color ($gray-300)
|
||||
&:focus {
|
||||
background-color: $gray-300;
|
||||
}
|
||||
|
||||
.bds-text-card__overlay {
|
||||
background-color: $gray-300;
|
||||
}
|
||||
|
||||
&:active {
|
||||
.bds-text-card__overlay {
|
||||
background-color: $gray-400;
|
||||
@include bds-window-shade-revealed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Neutral Dark Variant (Light Mode)
|
||||
// Default: $gray-300, Hover: $gray-200, Focus: $gray-200, Pressed: $gray-400
|
||||
.bds-text-card--neutral-dark {
|
||||
background-color: $gray-300;
|
||||
|
||||
// Focus uses hover color ($gray-200)
|
||||
&:focus {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
|
||||
.bds-text-card__overlay {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
|
||||
&:active {
|
||||
.bds-text-card__overlay {
|
||||
background-color: $gray-400;
|
||||
@include bds-window-shade-revealed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lilac Variant
|
||||
// Default: $lilac-200, Hover: $lilac-300, Pressed: $lilac-400
|
||||
.bds-text-card--lilac {
|
||||
background-color: $lilac-200;
|
||||
|
||||
// Preserve background on focus (overrides light theme transparent rule)
|
||||
&:focus {
|
||||
background-color: $lilac-200;
|
||||
}
|
||||
|
||||
.bds-text-card__overlay {
|
||||
background-color: $lilac-300;
|
||||
}
|
||||
|
||||
&:active {
|
||||
.bds-text-card__overlay {
|
||||
background-color: $lilac-400;
|
||||
@include bds-window-shade-revealed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Yellow Variant
|
||||
// Default: $yellow-100, Hover: $yellow-200, Pressed: $yellow-300
|
||||
.bds-text-card--yellow {
|
||||
background-color: $yellow-100;
|
||||
|
||||
// Preserve background on focus (overrides light theme transparent rule)
|
||||
&:focus {
|
||||
background-color: $yellow-100;
|
||||
}
|
||||
|
||||
.bds-text-card__overlay {
|
||||
background-color: $yellow-200;
|
||||
}
|
||||
|
||||
&:active {
|
||||
.bds-text-card__overlay {
|
||||
background-color: $yellow-300;
|
||||
@include bds-window-shade-revealed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Blue Variant
|
||||
// Default: $blue-100, Hover: $blue-200, Pressed: $blue-300
|
||||
.bds-text-card--blue {
|
||||
background-color: $blue-100;
|
||||
|
||||
// Preserve background on focus (overrides light theme transparent rule)
|
||||
&:focus {
|
||||
background-color: $blue-100;
|
||||
}
|
||||
|
||||
.bds-text-card__overlay {
|
||||
background-color: $blue-200;
|
||||
}
|
||||
|
||||
&:active {
|
||||
.bds-text-card__overlay {
|
||||
background-color: $blue-300;
|
||||
@include bds-window-shade-revealed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Card Title
|
||||
// =============================================================================
|
||||
|
||||
.bds-text-card__title {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
color: $bds-text-color;
|
||||
// Typography handled by .h-lg class from _font.scss
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Card Description
|
||||
// =============================================================================
|
||||
|
||||
.bds-text-card__description {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
color: $bds-text-color;
|
||||
max-width: $bds-text-card-description-max-width;
|
||||
// Typography handled by .body-l class from _font.scss
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Disabled State
|
||||
// =============================================================================
|
||||
|
||||
.bds-text-card--disabled {
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Light Mode Overrides
|
||||
// =============================================================================
|
||||
// Override the light theme rule: a:not(.bds-link):not(.btn):focus { background-color: transparent }
|
||||
// This rule has higher specificity, so we need html.light scoped rules
|
||||
|
||||
html.light {
|
||||
a.bds-text-card.bds-text-card--green:focus {
|
||||
background-color: $green-200;
|
||||
}
|
||||
a.bds-text-card.bds-text-card--neutral-light:focus {
|
||||
background-color: $gray-300;
|
||||
}
|
||||
a.bds-text-card.bds-text-card--neutral-dark:focus {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
a.bds-text-card.bds-text-card--lilac:focus {
|
||||
background-color: $lilac-200;
|
||||
}
|
||||
a.bds-text-card.bds-text-card--yellow:focus {
|
||||
background-color: $yellow-100;
|
||||
}
|
||||
a.bds-text-card.bds-text-card--blue:focus {
|
||||
background-color: $blue-100;
|
||||
}
|
||||
|
||||
// Disabled state in light mode: $gray-100 background, $gray-500 text
|
||||
.bds-text-card--disabled {
|
||||
background-color: $gray-100 !important;
|
||||
|
||||
.bds-text-card__title,
|
||||
.bds-text-card__description {
|
||||
color: $gray-500;
|
||||
}
|
||||
|
||||
.bds-text-card__overlay {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dark Mode Styles
|
||||
// =============================================================================
|
||||
// In dark mode:
|
||||
// - Focus border changes from black to white
|
||||
// - Text color remains black (cards have light-colored backgrounds)
|
||||
// - Neutral-light dark mode: Default $gray-300, Hover $gray-200, Focus $gray-200, Pressed $gray-400
|
||||
// - Neutral-dark dark mode: Default $gray-400, Hover $gray-300, Focus $gray-300, Pressed $gray-500
|
||||
|
||||
html.dark {
|
||||
.bds-text-card {
|
||||
// Focus styles - Dark Mode (white border)
|
||||
&:focus-visible {
|
||||
outline-color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
// Neutral Light in dark mode
|
||||
// Default: $gray-300, Hover: $gray-200, Focus: $gray-200, Pressed: $gray-400
|
||||
.bds-text-card--neutral-light {
|
||||
background-color: $gray-300;
|
||||
|
||||
&:focus {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
|
||||
.bds-text-card__overlay {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
|
||||
&:active {
|
||||
.bds-text-card__overlay {
|
||||
background-color: $gray-400;
|
||||
@include bds-window-shade-revealed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Neutral Dark in dark mode
|
||||
// Default: $gray-400, Hover: $gray-300, Focus: $gray-300, Pressed: $gray-500
|
||||
.bds-text-card--neutral-dark {
|
||||
background-color: $gray-400;
|
||||
|
||||
&:focus {
|
||||
background-color: $gray-300;
|
||||
}
|
||||
|
||||
.bds-text-card__overlay {
|
||||
background-color: $gray-300;
|
||||
}
|
||||
|
||||
&:active {
|
||||
.bds-text-card__overlay {
|
||||
background-color: $gray-500;
|
||||
@include bds-window-shade-revealed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Focus overrides for dark mode (to override light theme rules)
|
||||
a.bds-text-card.bds-text-card--neutral-light:focus {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
|
||||
a.bds-text-card.bds-text-card--neutral-dark:focus {
|
||||
background-color: $gray-300;
|
||||
}
|
||||
|
||||
// Disabled state in dark mode: $gray-500 background with 30% opacity
|
||||
.bds-text-card--disabled {
|
||||
background-color: rgba($gray-500, 0.3) !important;
|
||||
|
||||
.bds-text-card__overlay {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
/**
|
||||
* Color variants for the TextCard component
|
||||
* Maps to Figma design tokens (Light Mode):
|
||||
* - green: Default $green-200, Hover $green-300, Pressed $green-400
|
||||
* - neutral-light: Default $gray-200, Hover $gray-300, Pressed $gray-400
|
||||
* - neutral-dark: Default $gray-300, Hover $gray-400, Pressed $gray-500
|
||||
* - lilac: Default $lilac-200, Hover $lilac-300, Pressed $lilac-400
|
||||
* - yellow: Default $yellow-100, Hover $yellow-200, Pressed $yellow-300
|
||||
* - blue: Default $blue-100, Hover $blue-200, Pressed $blue-300
|
||||
*/
|
||||
export type TextCardColor = 'green' | 'neutral-light' | 'neutral-dark' | 'lilac' | 'yellow' | 'blue';
|
||||
|
||||
export interface TextCardProps extends Omit<React.ComponentPropsWithoutRef<'article'>, 'title'> {
|
||||
/** Card title text (heading-lg typography) */
|
||||
title: React.ReactNode;
|
||||
/** Card description text (body-l typography) */
|
||||
description?: React.ReactNode;
|
||||
/** Optional link URL - makes the card clickable */
|
||||
href?: string;
|
||||
/** Background color variant */
|
||||
color?: TextCardColor;
|
||||
/** Whether the card is disabled */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* TextCard Component
|
||||
*
|
||||
* A card component with a title at the top and description at the bottom.
|
||||
* Used within the CardsTwoColumn pattern to display content in a 2x2 grid.
|
||||
*
|
||||
* Features:
|
||||
* - 6 color variants: green, neutral-light, neutral-dark, lilac, yellow, blue
|
||||
* - Responsive typography and spacing
|
||||
* - Title uses heading-lg typography (Tobias Light)
|
||||
* - Description uses body-l typography (Booton Light)
|
||||
*
|
||||
* Responsive behavior:
|
||||
* - Desktop: 604px × 340px, 24px padding
|
||||
* - Tablet: Full width × 309px, 20px padding
|
||||
* - Mobile: Full width × 274px, 16px padding
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <TextCard
|
||||
* title="Institutions"
|
||||
* description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products..."
|
||||
* href="/institutions"
|
||||
* color="green"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const TextCard = React.forwardRef<HTMLElement, TextCardProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
href,
|
||||
color = 'neutral-light',
|
||||
disabled = false,
|
||||
className,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const rootClasses = clsx(
|
||||
'bds-text-card',
|
||||
`bds-text-card--${color}`,
|
||||
disabled && 'bds-text-card--disabled',
|
||||
className
|
||||
);
|
||||
|
||||
const CardWrapper = href ? 'a' : 'article';
|
||||
const wrapperProps = href
|
||||
? { href, className: rootClasses, ref: ref as React.Ref<HTMLAnchorElement> }
|
||||
: { className: rootClasses, ref };
|
||||
|
||||
return (
|
||||
<CardWrapper {...(wrapperProps as any)} {...rest}>
|
||||
{/* Overlay for window shade animation */}
|
||||
<div className="bds-text-card__overlay" aria-hidden="true" />
|
||||
|
||||
{/* Title at top */}
|
||||
<h3 className="bds-text-card__title h-lg">{title}</h3>
|
||||
|
||||
{/* Description at bottom */}
|
||||
{description && (
|
||||
<p className="bds-text-card__description body-l">{description}</p>
|
||||
)}
|
||||
</CardWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TextCard.displayName = 'TextCard';
|
||||
|
||||
export default TextCard;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export { TextCard, type TextCardProps, type TextCardColor } from './TextCard';
|
||||
export { default } from './TextCard';
|
||||
|
||||
@@ -12,13 +12,26 @@
|
||||
// .bds-tile-logo__overlay - Hover gradient overlay (window shade animation)
|
||||
// .bds-tile-logo__image - Logo image element
|
||||
|
||||
@import '../../../styles/breakpoints';
|
||||
|
||||
// Grid gutter (matching PageGrid)
|
||||
$bds-grid-gutter: 8px;
|
||||
|
||||
// Focus border colors (component-specific, dark mode default)
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
|
||||
// Focus border colors
|
||||
$bds-tile-logo-focus-border-light: $black;
|
||||
$bds-tile-logo-focus-border-dark: $white;
|
||||
|
||||
// Focus border width
|
||||
$bds-tile-logo-focus-border-width: 2px;
|
||||
|
||||
// Animation (matching CardOffgrid)
|
||||
$bds-tile-logo-transition-duration: 200ms;
|
||||
$bds-tile-logo-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Shape Tokens - Square (1:1 aspect ratio)
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -61,14 +74,14 @@ $bds-tile-logo-rect-padding-lg: 32px 64px; // LG: vertical 32px, horizontal 64px
|
||||
// Interaction
|
||||
cursor: pointer;
|
||||
|
||||
// Transitions (using shared animation tokens)
|
||||
// Transitions
|
||||
transition:
|
||||
background-color $bds-transition-duration $bds-transition-timing,
|
||||
opacity $bds-transition-duration $bds-transition-timing;
|
||||
background-color $bds-tile-logo-transition-duration $bds-tile-logo-transition-timing,
|
||||
opacity $bds-tile-logo-transition-duration $bds-tile-logo-transition-timing;
|
||||
|
||||
// Focus styles - Dark Mode (default)
|
||||
&:focus {
|
||||
outline: $bds-focus-border-width solid $bds-tile-logo-focus-border-dark;
|
||||
outline: $bds-tile-logo-focus-border-width solid $bds-tile-logo-focus-border-dark;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
@@ -77,7 +90,7 @@ $bds-tile-logo-rect-padding-lg: 32px 64px; // LG: vertical 32px, horizontal 64px
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: $bds-focus-border-width solid $bds-tile-logo-focus-border-dark;
|
||||
outline: $bds-tile-logo-focus-border-width solid $bds-tile-logo-focus-border-dark;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
@@ -132,14 +145,21 @@ $bds-tile-logo-rect-padding-lg: 32px 64px; // LG: vertical 32px, horizontal 64px
|
||||
// Hover out: shade falls from top to bottom (hides)
|
||||
|
||||
.bds-tile-logo__overlay {
|
||||
// Use shared window shade animation mixin
|
||||
@include bds-window-shade-overlay;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
|
||||
// Default: hidden (shade is "rolled up" at bottom, top is 100% clipped)
|
||||
// When transitioning TO this state, the top inset increases = shade falls down
|
||||
clip-path: inset(100% 0 0 0);
|
||||
transition: clip-path $bds-tile-logo-transition-duration $bds-tile-logo-transition-timing;
|
||||
}
|
||||
|
||||
// Hovered state: shade fully raised (visible)
|
||||
// When transitioning TO this state, the top inset decreases = shade rises up
|
||||
.bds-tile-logo--hovered .bds-tile-logo__overlay {
|
||||
@include bds-window-shade-revealed;
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
// BDS ButtonGroup Component Styles
|
||||
// Brand Design System - Responsive button group pattern
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-button-group - Base component
|
||||
// .bds-button-group--gap-none - No gap between buttons on tablet+ (0px)
|
||||
// .bds-button-group--gap-small - Small gap between buttons on tablet+ (8px)
|
||||
// .bds-button-group--block - Block layout for 3+ buttons (all tertiary)
|
||||
|
||||
// =============================================================================
|
||||
// Base Component Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-button-group {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
@extend .flex-wrap;
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
|
||||
// Tablet breakpoint - horizontal layout
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Gap Modifiers
|
||||
// =============================================================================
|
||||
|
||||
.bds-button-group--gap-none {
|
||||
// Tablet breakpoint - no gap
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-button-group--gap-small {
|
||||
// Tablet breakpoint - keep 8px gap
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Block Layout Modifier (3+ buttons)
|
||||
// =============================================================================
|
||||
|
||||
.bds-button-group--block {
|
||||
// Override default flex layout - force column layout on all screen sizes
|
||||
flex-direction: column !important;
|
||||
gap: 16px !important;
|
||||
|
||||
// All buttons should be full width in block layout
|
||||
.bds-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '../../components/Button/Button';
|
||||
|
||||
export interface ButtonConfig {
|
||||
/** Button text label */
|
||||
label: string;
|
||||
/** URL to navigate to - renders button as a link */
|
||||
href?: string;
|
||||
/** Force the color to remain constant regardless of theme mode */
|
||||
forceColor?: boolean;
|
||||
/** Click handler - matches Button component's onClick signature */
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export interface ButtonGroupValidationResult {
|
||||
/** The validated and potentially trimmed list of buttons */
|
||||
buttons: ButtonConfig[];
|
||||
/** Whether the button list is valid and should render */
|
||||
isValid: boolean;
|
||||
/** True if there are valid buttons to render (convenience flag) */
|
||||
hasButtons: boolean;
|
||||
/** Any warnings generated during validation */
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and processes a ButtonConfig array for ButtonGroup.
|
||||
*
|
||||
* Performs the following validations:
|
||||
* - Applies maxButtons limit if specified
|
||||
* - Checks for empty button arrays
|
||||
* - Validates individual button configs (label required, href or onClick recommended)
|
||||
* - Automatically logs warnings in development mode
|
||||
*
|
||||
* @param buttons - Array of button configurations (can be undefined)
|
||||
* @param maxButtons - Optional maximum number of buttons to render
|
||||
* @param autoLogWarnings - Whether to automatically log warnings in development mode (default: true)
|
||||
* @returns Validation result with processed buttons, validity flag, hasButtons flag, and warnings
|
||||
*
|
||||
* @example
|
||||
* // Basic usage with auto-logging
|
||||
* const validation = validateButtonGroup(buttons, 2);
|
||||
* if (validation.hasButtons) {
|
||||
* <ButtonGroup buttons={validation.buttons} />
|
||||
* }
|
||||
*
|
||||
* @example
|
||||
* // Disable auto-logging
|
||||
* const validation = validateButtonGroup(buttons, 2, false);
|
||||
* // Handle warnings manually
|
||||
* validation.warnings.forEach(w => customLogger(w));
|
||||
*/
|
||||
export function validateButtonGroup(
|
||||
buttons: ButtonConfig[] | undefined,
|
||||
maxButtons?: number,
|
||||
autoLogWarnings: boolean = true
|
||||
): ButtonGroupValidationResult {
|
||||
// Handle undefined/null buttons
|
||||
if (!buttons || buttons.length === 0) {
|
||||
return {
|
||||
buttons: [],
|
||||
isValid: false,
|
||||
hasButtons: false,
|
||||
warnings: []
|
||||
};
|
||||
}
|
||||
const warnings: string[] = [];
|
||||
let buttonList = [...buttons];
|
||||
|
||||
// Validate individual button configs
|
||||
buttonList.forEach((button, index) => {
|
||||
if (!button.label || button.label.trim() === '') {
|
||||
warnings.push(
|
||||
`[ButtonGroup] Button at index ${index} is missing a label. This button may not render correctly.`
|
||||
);
|
||||
}
|
||||
if (!button.href && !button.onClick) {
|
||||
warnings.push(
|
||||
`[ButtonGroup] Button "${button.label || `at index ${index}`}" has no href or onClick. Consider adding an action.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Apply maxButtons limit if specified
|
||||
if (maxButtons !== undefined && maxButtons > 0 && buttons.length > maxButtons) {
|
||||
warnings.push(
|
||||
`[ButtonGroup] ${buttons.length} buttons were passed but maxButtons is set to ${maxButtons}. ` +
|
||||
`Only the first ${maxButtons} button(s) will be rendered.`
|
||||
);
|
||||
buttonList = buttonList.slice(0, maxButtons);
|
||||
}
|
||||
|
||||
// Check for empty array
|
||||
if (buttonList.length === 0) {
|
||||
warnings.push(
|
||||
`[ButtonGroup] No buttons to render. ` +
|
||||
`Either an empty buttons array was passed or all buttons were removed by maxButtons limit.`
|
||||
);
|
||||
|
||||
// Auto-log warnings in development mode
|
||||
if (autoLogWarnings && process.env.NODE_ENV === 'development' && warnings.length > 0) {
|
||||
warnings.forEach(warning => console.warn(warning));
|
||||
}
|
||||
|
||||
return { buttons: [], isValid: false, hasButtons: false, warnings };
|
||||
}
|
||||
|
||||
// Auto-log warnings in development mode
|
||||
if (autoLogWarnings && process.env.NODE_ENV === 'development' && warnings.length > 0) {
|
||||
warnings.forEach(warning => console.warn(warning));
|
||||
}
|
||||
|
||||
const hasButtons = buttonList.length > 0;
|
||||
return { buttons: buttonList, isValid: true, hasButtons, warnings };
|
||||
}
|
||||
|
||||
export interface ButtonGroupProps {
|
||||
/** Array of button configurations
|
||||
* - 1 button: renders with singleButtonVariant (default: primary)
|
||||
* - 2 buttons: first as primary, second as tertiary
|
||||
* - 3+ buttons: all tertiary in block layout
|
||||
*/
|
||||
buttons: ButtonConfig[];
|
||||
/** Button color theme */
|
||||
color?: 'green' | 'black';
|
||||
/** Whether to force the color to remain constant regardless of theme mode */
|
||||
forceColor?: boolean;
|
||||
/** Gap between buttons on tablet+ (0px or 4px) */
|
||||
gap?: 'none' | 'small';
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
/** Override variant for single button (default: 'primary', can be 'secondary') */
|
||||
singleButtonVariant?: 'primary' | 'secondary';
|
||||
/** Maximum number of buttons to render. If more buttons are passed, only the first N will be rendered. */
|
||||
maxButtons?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* ButtonGroup Component
|
||||
*
|
||||
* A responsive button group container that displays buttons with adaptive layout:
|
||||
* - 1 button: Renders with singleButtonVariant (default: primary, can be secondary)
|
||||
* - 2 buttons: First as primary, second as tertiary (responsive layout)
|
||||
* - 3+ buttons: All tertiary in block layout
|
||||
*
|
||||
* @example
|
||||
* // Single button
|
||||
* <ButtonGroup
|
||||
* buttons={[{ label: "Get Started", href: "/start" }]}
|
||||
* color="green"
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Two buttons (primary + tertiary)
|
||||
* <ButtonGroup
|
||||
* buttons={[
|
||||
* { label: "Get Started", href: "/start" },
|
||||
* { label: "Learn More", href: "/learn" }
|
||||
* ]}
|
||||
* color="green"
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Three or more buttons (all tertiary, block layout)
|
||||
* <ButtonGroup
|
||||
* buttons={[
|
||||
* { label: "Option 1", href: "/option1" },
|
||||
* { label: "Option 2", href: "/option2" },
|
||||
* { label: "Option 3", href: "/option3" }
|
||||
* ]}
|
||||
* color="green"
|
||||
* />
|
||||
*/
|
||||
export const ButtonGroup: React.FC<ButtonGroupProps> = ({
|
||||
buttons,
|
||||
color = 'green',
|
||||
forceColor = false,
|
||||
gap = 'small',
|
||||
className = '',
|
||||
singleButtonVariant = 'primary',
|
||||
maxButtons,
|
||||
}) => {
|
||||
// Validate and process buttons
|
||||
const validation = validateButtonGroup(buttons, maxButtons);
|
||||
|
||||
// Log warnings in development mode
|
||||
if (process.env.NODE_ENV === 'development' && validation.warnings.length > 0) {
|
||||
validation.warnings.forEach(warning => console.warn(warning));
|
||||
}
|
||||
|
||||
// Don't render if validation failed
|
||||
if (!validation.isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const buttonList = validation.buttons;
|
||||
|
||||
const isMultiButton = buttonList.length >= 3;
|
||||
|
||||
const classNames = clsx(
|
||||
'bds-button-group',
|
||||
`bds-button-group--gap-${gap}`,
|
||||
{
|
||||
'bds-button-group--block': isMultiButton,
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
// Render 3+ buttons: all tertiary in block layout
|
||||
if (isMultiButton) {
|
||||
return (
|
||||
<div className={classNames}>
|
||||
{buttonList.map((button, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="tertiary"
|
||||
color={color}
|
||||
forceColor={forceColor}
|
||||
href={button.href}
|
||||
onClick={button.onClick}
|
||||
forceNoPadding
|
||||
>
|
||||
{button.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render 1-2 buttons
|
||||
// Single button: use singleButtonVariant (default: primary, can be secondary)
|
||||
// Two buttons: first as primary, second as tertiary
|
||||
const firstButtonVariant = buttonList.length === 1 ? singleButtonVariant : 'primary';
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
{buttonList[0] && (
|
||||
<Button
|
||||
variant={firstButtonVariant}
|
||||
color={color}
|
||||
forceColor={forceColor}
|
||||
href={buttonList[0].href}
|
||||
onClick={buttonList[0].onClick}
|
||||
>
|
||||
{buttonList[0].label}
|
||||
</Button>
|
||||
)}
|
||||
{buttonList[1] && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
color={color}
|
||||
forceColor={forceColor}
|
||||
href={buttonList[1].href}
|
||||
onClick={buttonList[1].onClick}
|
||||
>
|
||||
{buttonList[1].label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonGroup;
|
||||
@@ -1,109 +0,0 @@
|
||||
# ButtonGroup Component
|
||||
|
||||
A responsive button group container that automatically assigns button variants based on the number of buttons passed. Stacks vertically on mobile and horizontally on tablet+.
|
||||
|
||||
## Features
|
||||
|
||||
- **Auto-Variant Assignment**: Automatically assigns Primary/Tertiary/Secondary variants based on button count
|
||||
- **Responsive Layout**: Vertical stack on mobile, horizontal row on tablet+ (for 1-2 buttons)
|
||||
- **Block Layout**: 3+ buttons render as all tertiary in a vertical block layout
|
||||
- **Customizable Spacing**: Control gap between buttons on tablet+ (none or small)
|
||||
- **Theme Support**: Green or black color themes
|
||||
- **Max Buttons Limit**: Optionally limit the number of buttons rendered
|
||||
|
||||
## Button Behavior
|
||||
|
||||
The component automatically determines button variants based on count:
|
||||
|
||||
| Count | Behavior |
|
||||
|-------|----------|
|
||||
| 1 button | Renders as Primary (or Secondary with `singleButtonVariant="secondary"`) |
|
||||
| 2 buttons | First as Primary, second as Tertiary (responsive layout) |
|
||||
| 3+ buttons | All as Tertiary in block layout (vertical on all screen sizes) |
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { ButtonGroup } from 'shared/patterns/ButtonGroup';
|
||||
|
||||
// Single button (Primary by default)
|
||||
<ButtonGroup
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "/start" }
|
||||
]}
|
||||
color="green"
|
||||
/>
|
||||
|
||||
// Single button as Secondary
|
||||
<ButtonGroup
|
||||
buttons={[
|
||||
{ label: "Learn More", href: "/learn" }
|
||||
]}
|
||||
singleButtonVariant="secondary"
|
||||
color="green"
|
||||
/>
|
||||
|
||||
// Two buttons (auto: Primary + Tertiary)
|
||||
<ButtonGroup
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "/start" },
|
||||
{ label: "Learn More", href: "/learn" }
|
||||
]}
|
||||
color="green"
|
||||
/>
|
||||
|
||||
// Three or more buttons (auto: all Tertiary, block layout)
|
||||
<ButtonGroup
|
||||
buttons={[
|
||||
{ label: "Documentation", href: "/docs" },
|
||||
{ label: "API Reference", href: "/api" },
|
||||
{ label: "Tutorials", href: "/tutorials" }
|
||||
]}
|
||||
color="black"
|
||||
/>
|
||||
|
||||
// Limit to 2 buttons even if more are passed
|
||||
<ButtonGroup
|
||||
buttons={[
|
||||
{ label: "First", href: "/first" },
|
||||
{ label: "Second", href: "/second" },
|
||||
{ label: "Third (not rendered)", href: "/third" }
|
||||
]}
|
||||
maxButtons={2}
|
||||
color="green"
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `buttons` | `ButtonConfig[]` | *required* | Array of button configurations |
|
||||
| `color` | `'green' \| 'black'` | `'green'` | Button color theme |
|
||||
| `forceColor` | `boolean` | `false` | Force color to remain constant across light/dark modes |
|
||||
| `gap` | `'none' \| 'small'` | `'small'` | Gap between buttons on tablet+ (0px or 4px) |
|
||||
| `singleButtonVariant` | `'primary' \| 'secondary'` | `'primary'` | Variant for single button |
|
||||
| `maxButtons` | `number` | - | Maximum number of buttons to render |
|
||||
| `className` | `string` | `''` | Additional CSS classes |
|
||||
|
||||
### ButtonConfig
|
||||
|
||||
```tsx
|
||||
interface ButtonConfig {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
forceColor?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
- **Mobile (<768px)**: Buttons stack vertically with 8px gap, aligned to start
|
||||
- **Tablet+ (≥768px)**: Buttons align horizontally, centered, with configurable gap (0px or 4px)
|
||||
|
||||
## CSS Classes
|
||||
|
||||
- `.bds-button-group` - Base component
|
||||
- `.bds-button-group--gap-none` - No gap on tablet+ (0px)
|
||||
- `.bds-button-group--gap-small` - Small gap on tablet+ (4px)
|
||||
@@ -1,2 +0,0 @@
|
||||
export { ButtonGroup, validateButtonGroup } from './ButtonGroup';
|
||||
export type { ButtonGroupProps, ButtonConfig, ButtonGroupValidationResult } from './ButtonGroup';
|
||||
@@ -15,8 +15,7 @@
|
||||
// .bds-callout-media-banner__text - Text content container
|
||||
// .bds-callout-media-banner__heading - Heading element
|
||||
// .bds-callout-media-banner__subheading - Subheading element
|
||||
//
|
||||
// Note: Button layout is handled by the ButtonGroup component
|
||||
// .bds-callout-media-banner__actions - Button container
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
@@ -192,8 +191,21 @@ $bds-cmb-image-overlay: rgba(0, 0, 0, 0.3);
|
||||
|
||||
// =============================================================================
|
||||
// Action Buttons
|
||||
// =============================================================================
|
||||
// Note: Button layout is now handled by the ButtonGroup component
|
||||
// ==================================================================== c=========
|
||||
|
||||
.bds-callout-media-banner__actions {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
@extend .align-items-start;
|
||||
@extend .flex-wrap;
|
||||
gap: 8px;
|
||||
// Tablet breakpoint
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Color Variants (only applied when NO backgroundImage is provided)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '../../components/Button/Button';
|
||||
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
|
||||
export interface CalloutMediaBannerProps {
|
||||
/** Color variant - determines background color (ignored if backgroundImage is provided) */
|
||||
@@ -14,8 +14,18 @@ export interface CalloutMediaBannerProps {
|
||||
heading?: string;
|
||||
/** Subheading/description text */
|
||||
subheading: string;
|
||||
/** Button configurations (1-2 buttons supported) */
|
||||
buttons?: ButtonConfig[];
|
||||
/** Primary button configuration */
|
||||
primaryButton?: {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
};
|
||||
/** Tertiary button configuration */
|
||||
tertiaryButton?: {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
};
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
@@ -32,21 +42,19 @@ export interface CalloutMediaBannerProps {
|
||||
* variant="green"
|
||||
* heading="The Compliant Ledger Protocol"
|
||||
* subheading="A decentralized public Layer 1 blockchain..."
|
||||
* buttons={[
|
||||
* { label: "Get Started", href: "/docs" },
|
||||
* { label: "Learn More", href: "/about" }
|
||||
* ]}
|
||||
* primaryButton={{ label: "Get Started", href: "/docs" }}
|
||||
* tertiaryButton={{ label: "Learn More", href: "/about" }}
|
||||
* />
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // With background image (white text - default)
|
||||
* <CalloutMediaBanner
|
||||
* backgroundImage="/images/hero-bg.jpg"
|
||||
* heading="Build on XRPL"
|
||||
* subheading="Start building your next project"
|
||||
* buttons={[{ label: "Start Building", onClick: handleClick }]}
|
||||
* primaryButton={{ label: "Start Building", onClick: handleClick }}
|
||||
* />
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // With background image and black text (fixed across light/dark modes)
|
||||
* <CalloutMediaBanner
|
||||
@@ -54,7 +62,7 @@ export interface CalloutMediaBannerProps {
|
||||
* textColor="black"
|
||||
* heading="Build on XRPL"
|
||||
* subheading="Start building your next project"
|
||||
* buttons={[{ label: "Start Building", onClick: handleClick }]}
|
||||
* primaryButton={{ label: "Start Building", onClick: handleClick }}
|
||||
* />
|
||||
*/
|
||||
export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
|
||||
@@ -63,13 +71,13 @@ export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
|
||||
textColor = 'white',
|
||||
heading,
|
||||
subheading,
|
||||
buttons,
|
||||
primaryButton,
|
||||
tertiaryButton,
|
||||
className = '',
|
||||
}) => {
|
||||
// Validate buttons if provided (max 2 buttons supported)
|
||||
const buttonValidation = validateButtonGroup(buttons, 2);
|
||||
const hasButtons = buttonValidation.hasButtons;
|
||||
|
||||
// Check if there are any buttons
|
||||
const hasButtons = !!(primaryButton || tertiaryButton);
|
||||
|
||||
// Check if we should center content: no buttons OR (no heading but has buttons)
|
||||
const shouldCenter = !hasButtons || (!heading && hasButtons);
|
||||
|
||||
@@ -108,12 +116,29 @@ export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
{hasButtons && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color={buttonColor}
|
||||
gap="none"
|
||||
/>
|
||||
{(primaryButton || tertiaryButton) && (
|
||||
<div className="bds-callout-media-banner__actions">
|
||||
{primaryButton && (
|
||||
<Button
|
||||
variant="primary"
|
||||
color={buttonColor}
|
||||
href={primaryButton.href}
|
||||
onClick={primaryButton?.onClick as (() => void) | undefined}
|
||||
>
|
||||
{primaryButton.label}
|
||||
</Button>
|
||||
)}
|
||||
{tertiaryButton && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
color={buttonColor}
|
||||
href={tertiaryButton.href}
|
||||
onClick={tertiaryButton?.onClick as (() => void) | undefined}
|
||||
>
|
||||
{tertiaryButton.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PageGridCol>
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
# CardStats Pattern
|
||||
|
||||
A section pattern that displays a heading, optional description, and a responsive grid of `CardStat` components. Designed for showcasing key statistics and metrics on landing pages.
|
||||
|
||||
## Features
|
||||
|
||||
- Responsive grid layout (2 columns mobile/tablet, 3 columns desktop)
|
||||
- Heading with `heading-md` typography (Tobias Light)
|
||||
- Optional description with `body-l` typography (Booton Light)
|
||||
- Proper spacing using `PageGrid` for container and alignment
|
||||
- Full dark mode support
|
||||
- Reuses the `CardStat` component for consistent styling
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { CardStats } from 'shared/patterns/CardStats';
|
||||
|
||||
<CardStats
|
||||
heading="Blockchain Trusted at Scale"
|
||||
description="Streamline development and build powerful RWA tokenization solutions with XRP Ledger's comprehensive developer toolset."
|
||||
cards={[
|
||||
{
|
||||
statistic: "12",
|
||||
superscript: "+",
|
||||
label: "Continuous uptime years",
|
||||
variant: "lilac",
|
||||
primaryButton: { label: "Learn More", href: "/docs" }
|
||||
},
|
||||
{
|
||||
statistic: "6M",
|
||||
superscript: "2",
|
||||
label: "Active wallets",
|
||||
variant: "light-gray",
|
||||
primaryButton: { label: "Explore", href: "/wallets" }
|
||||
},
|
||||
{
|
||||
statistic: "$1T",
|
||||
superscript: "+",
|
||||
label: "Value transferred",
|
||||
variant: "green",
|
||||
primaryButton: { label: "View Stats", href: "/stats" }
|
||||
},
|
||||
// ... more cards
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `heading` | `React.ReactNode` | Yes | Section heading text |
|
||||
| `description` | `React.ReactNode` | No | Optional section description text |
|
||||
| `cards` | `CardStatProps[]` | Yes | Array of CardStat configurations |
|
||||
| `className` | `string` | No | Additional CSS classes |
|
||||
|
||||
### CardStat Configuration
|
||||
|
||||
Each card in the `cards` array accepts the following props:
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `statistic` | `string` | Yes | The main statistic to display (e.g., "6M", "$1T") |
|
||||
| `superscript` | `'+' \| '*' \| '1'-'9' \| '0'` | No | Superscript text for the statistic |
|
||||
| `label` | `string` | Yes | Descriptive label for the statistic |
|
||||
| `variant` | `'lilac' \| 'green' \| 'light-gray' \| 'dark-gray'` | No | Background color variant (default: 'lilac') |
|
||||
| `primaryButton` | `{ label: string, href?: string, onClick?: () => void }` | No | Primary CTA button |
|
||||
| `secondaryButton` | `{ label: string, href?: string, onClick?: () => void }` | No | Secondary CTA button |
|
||||
|
||||
## Color Variants
|
||||
|
||||
| Variant | Color | Hex | Use Case |
|
||||
|---------|-------|-----|----------|
|
||||
| `lilac` | Lilac 300 | #C0A7FF | User metrics, community stats |
|
||||
| `green` | Green 300 | #21E46B | Financial metrics, growth indicators |
|
||||
| `light-gray` | Gray 200 | #E6EAF0 | Technical stats, reliability metrics |
|
||||
| `dark-gray` | Gray 300 | #CAD4DF | Neutral metrics, secondary info |
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
Uses breakpoints from `styles/_breakpoints.scss`:
|
||||
|
||||
| Breakpoint | Width | Columns | Card Gap |
|
||||
|------------|-------|---------|----------|
|
||||
| xs (Mobile) | 0 - 575px | 1 | 8px |
|
||||
| md (Tablet) | 576px - 991px | 2 | 8px |
|
||||
| lg (Desktop) | 992px+ | 3 | 8px |
|
||||
|
||||
## Examples
|
||||
|
||||
### With Description
|
||||
|
||||
```tsx
|
||||
<CardStats
|
||||
heading="Blockchain Trusted at Scale"
|
||||
description="Streamline development and build powerful RWA tokenization solutions."
|
||||
cards={statsCards}
|
||||
/>
|
||||
```
|
||||
|
||||
### Without Description (Heading Only)
|
||||
|
||||
```tsx
|
||||
<CardStats
|
||||
heading="XRPL Network Statistics"
|
||||
cards={statsCards}
|
||||
/>
|
||||
```
|
||||
|
||||
### Mixed Card Variants
|
||||
|
||||
```tsx
|
||||
<CardStats
|
||||
heading="Why Build on XRPL?"
|
||||
cards={[
|
||||
{ statistic: "12", superscript: "+", label: "Uptime years", variant: "lilac" },
|
||||
{ statistic: "6M", label: "Active wallets", variant: "green" },
|
||||
{ statistic: "3-5s", label: "Transaction finality", variant: "light-gray" },
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## CSS Classes
|
||||
|
||||
| Class | Description |
|
||||
|-------|-------------|
|
||||
| `.bds-card-stats` | Base section container |
|
||||
| `.bds-card-stats__header` | Header wrapper for heading and description |
|
||||
| `.bds-card-stats__heading` | Section heading (uses `.h-md`) |
|
||||
| `.bds-card-stats__description` | Section description (uses `.body-l`) |
|
||||
| `.bds-card-stats__cards` | Cards grid container |
|
||||
| `.bds-card-stats__card-wrapper` | Individual card wrapper |
|
||||
|
||||
## Design References
|
||||
|
||||
- **Figma Design**: [Section Cards - Stats](https://www.figma.com/design/drnQQXnK9Q67MTPPKQsY9l/Section-Cards---Stats?node-id=32051-2839&m=dev)
|
||||
- **Showcase Page**: `/about/card-stats-showcase`
|
||||
- **Pattern Location**: `shared/patterns/CardStats/`
|
||||
- **Component Used**: `shared/components/CardStat/`
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Uses semantic HTML with `<section>` and proper heading hierarchy
|
||||
- CardStat buttons include proper focus states
|
||||
- Color contrast meets WCAG AA requirements
|
||||
- Responsive layout ensures readability on all devices
|
||||
|
||||
## Version History
|
||||
|
||||
- Initial implementation: January 2026
|
||||
- Figma design alignment with 4 color variants
|
||||
- Responsive grid layout with PageGrid integration
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
// BDS CardStats Pattern Styles
|
||||
// Brand Design System - Section with heading, description, and grid of CardStat components
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-card-stats - Base section container
|
||||
// .bds-card-stats__header - Header wrapper for heading and description
|
||||
// .bds-card-stats__heading - Section heading (uses .h-md)
|
||||
// .bds-card-stats__description - Section description (uses .body-l)
|
||||
// .bds-card-stats__cards - Cards grid container
|
||||
// .bds-card-stats__card-wrapper - Individual card wrapper
|
||||
//
|
||||
// Design tokens from Figma:
|
||||
// Light Mode:
|
||||
// - Background: White (#FFFFFF)
|
||||
// - Heading: Neutral Black (#141414) → $black
|
||||
// - Description: Neutral Black (#141414) → $black
|
||||
//
|
||||
// Dark Mode:
|
||||
// - Background: transparent (inherits page background)
|
||||
// - Heading: Neutral White (#FFFFFF) → $white
|
||||
// - Description: Neutral White (#FFFFFF) → $white
|
||||
//
|
||||
// - Header content max-width: 808px (approximately 8 columns at desktop)
|
||||
// - Gap between heading and description: 16px
|
||||
// - Gap between cards: 8px (matches $bds-grid-gutter)
|
||||
|
||||
|
||||
// Color tokens - Light Mode (from Figma: node 32051-2839)
|
||||
$bds-card-stats-heading-light: $black; // --neutral/black (#141414)
|
||||
$bds-card-stats-description-light: $black; // --neutral/black (#141414)
|
||||
|
||||
// Color tokens - Dark Mode (from Figma: node 32051-2524)
|
||||
$bds-card-stats-bg-dark: transparent; // Inherits page background
|
||||
$bds-card-stats-heading-dark: $white; // --neutral/white (#FFFFFF)
|
||||
$bds-card-stats-description-dark: $white; // --neutral/white (#FFFFFF)
|
||||
|
||||
// Spacing - Header gap (between heading and description)
|
||||
$bds-card-stats-header-gap-base: 8px; // Base: 8px
|
||||
$bds-card-stats-header-gap-lg: 16px; // Desktop: 16px
|
||||
|
||||
// Spacing - Section gap (between header and cards)
|
||||
$bds-card-stats-section-gap-sm: 24px; // Mobile: 24px
|
||||
$bds-card-stats-section-gap-md: 32px; // Tablet: 32px
|
||||
$bds-card-stats-section-gap-lg: 40px; // Desktop: 40px
|
||||
|
||||
// Spacing - Section padding
|
||||
$bds-card-stats-padding-y-sm: 24px; // Mobile: 24px
|
||||
$bds-card-stats-padding-y-md: 32px; // Tablet: 32px
|
||||
$bds-card-stats-padding-y-lg: 40px; // Desktop: 40px
|
||||
|
||||
// =============================================================================
|
||||
// Base Section Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-card-stats {
|
||||
// Vertical padding
|
||||
padding-top: $bds-card-stats-padding-y-sm;
|
||||
padding-bottom: $bds-card-stats-padding-y-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: $bds-card-stats-padding-y-md;
|
||||
padding-bottom: $bds-card-stats-padding-y-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: $bds-card-stats-padding-y-lg;
|
||||
padding-bottom: $bds-card-stats-padding-y-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-card-stats__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-card-stats-header-gap-base;
|
||||
margin-bottom: $bds-card-stats-section-gap-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-bottom: $bds-card-stats-section-gap-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-card-stats-header-gap-lg;
|
||||
margin-bottom: $bds-card-stats-section-gap-lg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Dark Mode Styles
|
||||
// =============================================================================
|
||||
|
||||
html.dark {
|
||||
.bds-card-stats__heading,
|
||||
.bds-card-stats__description {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { CardStat, CardStatProps } from '../../components/CardStat';
|
||||
import { PageGrid } from '../../components/PageGrid/page-grid';
|
||||
|
||||
/**
|
||||
* Configuration for a single stat card in the CardStats pattern
|
||||
*/
|
||||
export type CardStatsCardConfig = CardStatProps;
|
||||
|
||||
/**
|
||||
* Props for the CardStats pattern component
|
||||
*/
|
||||
export interface CardStatsProps extends React.ComponentPropsWithoutRef<'section'> {
|
||||
/** Section heading text */
|
||||
heading: React.ReactNode;
|
||||
/** Optional section description text */
|
||||
description?: React.ReactNode;
|
||||
/** Array of CardStat configurations */
|
||||
cards: CardStatsCardConfig[];
|
||||
}
|
||||
|
||||
/**
|
||||
* CardStats Pattern Component
|
||||
*
|
||||
* A section pattern that displays a heading, optional description, and a responsive
|
||||
* grid of CardStat components. Designed for showcasing key statistics and metrics.
|
||||
*
|
||||
* Features:
|
||||
* - Responsive grid layout (2 columns mobile/tablet, 3 columns desktop)
|
||||
* - Heading with `heading-md` typography (Tobias Light)
|
||||
* - Optional description with `body-l` typography (Booton Light)
|
||||
* - Proper spacing using PageGrid for container and alignment
|
||||
* - Full dark mode support
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <CardStats
|
||||
* heading="Blockchain Trusted at Scale"
|
||||
* description="Streamline development and build powerful RWA tokenization solutions."
|
||||
* cards={[
|
||||
* {
|
||||
* statistic: "12",
|
||||
* superscript: "+",
|
||||
* label: "Continuous uptime years",
|
||||
* variant: "lilac",
|
||||
* primaryButton: { label: "Learn More", href: "/docs" }
|
||||
* },
|
||||
* {
|
||||
* statistic: "6M",
|
||||
* superscript: "2",
|
||||
* label: "Active wallets",
|
||||
* variant: "light-gray"
|
||||
* },
|
||||
* // ... more cards
|
||||
* ]}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const CardStats = React.forwardRef<HTMLElement, CardStatsProps>(
|
||||
(props, ref) => {
|
||||
const { heading, description, cards, className, ...rest } = props;
|
||||
|
||||
// Early return for empty cards array
|
||||
if (cards.length === 0) {
|
||||
console.warn('CardStats: No cards provided');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageGrid ref={ref as React.Ref<HTMLDivElement>}
|
||||
className={clsx('bds-card-stats', className)}
|
||||
{...rest}
|
||||
>
|
||||
<PageGrid.Row>
|
||||
<PageGrid.Col span={{ base: 4, md: 6, lg: 8 }}>
|
||||
{/* Header section */}
|
||||
<div className="bds-card-stats__header">
|
||||
<h2 className="mb-0 h-md">{heading}</h2>
|
||||
{description && (
|
||||
<p className="bmb-0 body-l">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
<PageGrid.Row>
|
||||
{cards.map((cardConfig, index) => (
|
||||
<CardStat
|
||||
key={index}
|
||||
{...cardConfig}
|
||||
span={cardConfig.span ?? { base: 4, md: 4, lg: 4 }}
|
||||
/>
|
||||
))}
|
||||
</PageGrid.Row>
|
||||
</PageGrid>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardStats.displayName = 'CardStats';
|
||||
|
||||
export default CardStats;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export { CardStats, type CardStatsProps, type CardStatsCardConfig } from './CardStats';
|
||||
export { default } from './CardStats';
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { CardImage, CardImageProps } from '../../components/CardImage';
|
||||
import { PageGrid } from '../../components/PageGrid/page-grid';
|
||||
import { getCardKey, isEnvironment } from '../../utils';
|
||||
|
||||
/**
|
||||
* Configuration for a single card in the CardsFeatured pattern
|
||||
@@ -21,6 +20,16 @@ export interface CardsFeaturedProps extends React.ComponentPropsWithoutRef<'sect
|
||||
cards: readonly CardsFeaturedCardConfig[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a stable key for a card based on its properties.
|
||||
* Falls back to index if no stable identifier is available.
|
||||
*/
|
||||
const getCardKey = (card: CardsFeaturedCardConfig, index: number): string | number => {
|
||||
if (card.href) return card.href;
|
||||
if (card.title) return `${card.title}-${index}`;
|
||||
return index;
|
||||
};
|
||||
|
||||
/**
|
||||
* CardsFeatured Pattern Component
|
||||
*
|
||||
@@ -59,9 +68,7 @@ export const CardsFeatured = React.forwardRef<HTMLElement, CardsFeaturedProps>(
|
||||
|
||||
// Early return for empty cards array
|
||||
if (cards.length === 0) {
|
||||
if (isEnvironment('development')) {
|
||||
console.warn('CardsFeatured: No cards provided');
|
||||
}
|
||||
console.warn('CardsFeatured: No cards provided');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -94,7 +101,7 @@ export const CardsFeatured = React.forwardRef<HTMLElement, CardsFeaturedProps>(
|
||||
<div className="bds-cards-featured__cards">
|
||||
{cards.map((card, index) => (
|
||||
<div
|
||||
key={getCardKey(card.href || card.title, index, "card-featured")}
|
||||
key={getCardKey(card, index)}
|
||||
className="bds-cards-featured__card-wrapper"
|
||||
>
|
||||
<CardImage {...card} fullBleed />
|
||||
|
||||
141
shared/patterns/CardsIconGrid/CardsIconGrid.md
Normal file
141
shared/patterns/CardsIconGrid/CardsIconGrid.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# CardsIconGrid Pattern
|
||||
|
||||
A section pattern that displays a heading, optional description, and a responsive grid of `CardIcon` components. Follows the "CardIconGrid" design from Figma.
|
||||
|
||||
## Features
|
||||
|
||||
- Responsive grid layout (1 column mobile, 2 tablet, 3 desktop)
|
||||
- Heading with `heading-md` typography (Tobias Light)
|
||||
- Optional description with `body-l` typography (Booton Light)
|
||||
- Proper spacing using `PageGrid` for container and alignment
|
||||
- Full dark mode support
|
||||
- Uses the existing `CardIcon` component for cards
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { CardsIconGrid } from 'shared/patterns/CardsIconGrid';
|
||||
|
||||
<CardsIconGrid
|
||||
heading="Unlock new business models with embedded payments"
|
||||
description="Streamline development and build powerful RWA tokenization solutions with XRP Ledger's comprehensive developer toolset."
|
||||
cards={[
|
||||
{
|
||||
icon: "/icons/wallet.svg",
|
||||
label: "Digital Wallets",
|
||||
href: "/docs/wallets",
|
||||
variant: "green"
|
||||
},
|
||||
{
|
||||
icon: "/icons/payments.svg",
|
||||
label: "B2B Payment Rails",
|
||||
href: "/docs/payments",
|
||||
variant: "green"
|
||||
},
|
||||
{
|
||||
icon: "/icons/compliance.svg",
|
||||
label: "Compliance-First Payments",
|
||||
href: "/docs/compliance",
|
||||
variant: "green"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `heading` | `React.ReactNode` | Yes | Section heading text |
|
||||
| `description` | `React.ReactNode` | No | Section description text (optional) |
|
||||
| `cards` | `CardsIconGridCardConfig[]` | Yes | Array of card configurations (uses `CardIconProps`) |
|
||||
| `className` | `string` | No | Additional CSS class names |
|
||||
|
||||
### CardsIconGridCardConfig
|
||||
|
||||
Each card in the `cards` array accepts all props from `CardIconProps`:
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `icon` | `string` | Yes | Icon image source (URL or path) |
|
||||
| `iconAlt` | `string` | No | Alt text for the icon image |
|
||||
| `label` | `string` | Yes | Card label text |
|
||||
| `variant` | `'neutral' \| 'green'` | No | Color variant (default: 'neutral') |
|
||||
| `href` | `string` | No | Link destination - renders as `<a>` |
|
||||
| `onClick` | `() => void` | No | Click handler - renders as `<button>` |
|
||||
| `disabled` | `boolean` | No | Disabled state |
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
| Breakpoint | Grid Columns | Vertical Padding |
|
||||
|------------|--------------|------------------|
|
||||
| Mobile (< 768px) | 1 column | 48px |
|
||||
| Tablet (768px - 1199px) | 2 columns | 64px |
|
||||
| Desktop (≥ 1200px) | 3 columns | 80px |
|
||||
|
||||
## Design Tokens
|
||||
|
||||
### Colors
|
||||
|
||||
| Mode | Element | Color |
|
||||
|------|---------|-------|
|
||||
| Light | Heading | `$black` (#141414) |
|
||||
| Light | Description | `$black` (#141414) |
|
||||
| Dark | Heading | `$white` (#FFFFFF) |
|
||||
| Dark | Description | `$white` (#FFFFFF) |
|
||||
|
||||
### Spacing
|
||||
|
||||
- Header gap (heading to description): 8px mobile/tablet, 16px desktop
|
||||
- Section gap (header to cards): 24px mobile, 32px tablet, 40px desktop
|
||||
- Cards column gap: 24px mobile, 8px tablet/desktop
|
||||
- Cards row gap: 24px mobile, 32px tablet, 40px desktop
|
||||
|
||||
## CSS Classes
|
||||
|
||||
| Class | Description |
|
||||
|-------|-------------|
|
||||
| `.bds-cards-icon-grid` | Base section container |
|
||||
| `.bds-cards-icon-grid__header` | Header wrapper for heading and description |
|
||||
| `.bds-cards-icon-grid__heading` | Section heading (uses `.h-md`) |
|
||||
| `.bds-cards-icon-grid__description` | Section description (uses `.body-l`) |
|
||||
| `.bds-cards-icon-grid__cards` | Cards grid container |
|
||||
| `.bds-cards-icon-grid__card-wrapper` | Individual card wrapper |
|
||||
|
||||
## Card Variants
|
||||
|
||||
The pattern supports both CardIcon variants:
|
||||
|
||||
### Green Variant
|
||||
```tsx
|
||||
cards={[
|
||||
{ icon: "/icons/wallet.svg", label: "Digital Wallets", href: "/wallets", variant: "green" }
|
||||
]}
|
||||
```
|
||||
|
||||
### Neutral Variant
|
||||
```tsx
|
||||
cards={[
|
||||
{ icon: "/icons/docs.svg", label: "Documentation", href: "/docs", variant: "neutral" }
|
||||
]}
|
||||
```
|
||||
|
||||
## Without Description
|
||||
|
||||
The description prop is optional:
|
||||
|
||||
```tsx
|
||||
<CardsIconGrid
|
||||
heading="Funding & Support Programs"
|
||||
cards={[...]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Figma Reference
|
||||
|
||||
- Design: [Section Cards - Icon Grid](https://www.figma.com/design/Ojj6UpFBw3HMb0QqRaKxAU/Section-Cards---Icon?node-id=30071-3082&m=dev)
|
||||
|
||||
## Showcase
|
||||
|
||||
View the pattern showcase at: `/about/cards-icon-grid-showcase`
|
||||
|
||||
180
shared/patterns/CardsIconGrid/CardsIconGrid.scss
Normal file
180
shared/patterns/CardsIconGrid/CardsIconGrid.scss
Normal file
@@ -0,0 +1,180 @@
|
||||
// BDS CardsIconGrid Pattern Styles
|
||||
// Brand Design System - Section with heading, optional description, and grid of CardIcon components
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-cards-icon-grid - Base section container
|
||||
// .bds-cards-icon-grid__header - Header wrapper for heading and description
|
||||
// .bds-cards-icon-grid__heading - Section heading (uses .h-md)
|
||||
// .bds-cards-icon-grid__description - Section description (uses .body-l)
|
||||
// .bds-cards-icon-grid__cards - Cards grid container
|
||||
// .bds-cards-icon-grid__card-wrapper - Individual card wrapper
|
||||
//
|
||||
// Design tokens from Figma:
|
||||
// Light Mode:
|
||||
// - Heading: Neutral Black (#141414) → $black
|
||||
// - Description: Neutral Black (#141414) → $black
|
||||
//
|
||||
// Dark Mode:
|
||||
// - Heading: Neutral White (#FFFFFF) → $white
|
||||
// - Description: Neutral White (#FFFFFF) → $white
|
||||
//
|
||||
// - Header content max-width: 808px (approximately 8 columns at desktop)
|
||||
// - Gap between heading and description: 8px mobile/tablet, 16px desktop
|
||||
// - Gap between cards: 8px (matches $bds-grid-gutter)
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens (from Figma)
|
||||
// =============================================================================
|
||||
|
||||
$bds-grid-gutter: 8px;
|
||||
|
||||
// Spacing - Header gap (between heading and description)
|
||||
$bds-cards-icon-grid-header-gap-sm: 8px; // Mobile: 8px
|
||||
$bds-cards-icon-grid-header-gap-md: 8px; // Tablet: 8px
|
||||
$bds-cards-icon-grid-header-gap-lg: 16px; // Desktop: 16px
|
||||
|
||||
// Spacing - Section gap (between header and cards)
|
||||
$bds-cards-icon-grid-section-gap-sm: 24px; // Mobile
|
||||
$bds-cards-icon-grid-section-gap-md: 32px; // Tablet
|
||||
$bds-cards-icon-grid-section-gap-lg: 40px; // Desktop
|
||||
|
||||
// Spacing - Cards gap
|
||||
$bds-cards-icon-grid-cards-gap-sm: 24px; // Mobile: 24px vertical stack
|
||||
$bds-cards-icon-grid-cards-gap-md: 8px; // Tablet: 8px
|
||||
$bds-cards-icon-grid-cards-gap-lg: 8px; // Desktop: 8px
|
||||
|
||||
// Spacing - Row gap (between rows of cards)
|
||||
$bds-cards-icon-grid-row-gap-sm: 24px; // Mobile
|
||||
$bds-cards-icon-grid-row-gap-md: 32px; // Tablet
|
||||
$bds-cards-icon-grid-row-gap-lg: 40px; // Desktop
|
||||
|
||||
// Spacing - Section padding (vertical)
|
||||
$bds-cards-icon-grid-padding-y-sm: 48px; // Mobile
|
||||
$bds-cards-icon-grid-padding-y-md: 64px; // Tablet
|
||||
$bds-cards-icon-grid-padding-y-lg: 80px; // Desktop
|
||||
|
||||
// Colors - Light Mode (default)
|
||||
$bds-cards-icon-grid-heading-color: $black; // #141414 - Neutral black
|
||||
$bds-cards-icon-grid-description-color: $black; // #141414 - Neutral black
|
||||
|
||||
// Colors - Dark Mode
|
||||
$bds-cards-icon-grid-heading-color-dark: $white; // #FFFFFF - Neutral white
|
||||
$bds-cards-icon-grid-description-color-dark: $white; // #FFFFFF - Neutral white
|
||||
|
||||
// =============================================================================
|
||||
// Section Container
|
||||
// =============================================================================
|
||||
|
||||
.bds-cards-icon-grid {
|
||||
width: 100%;
|
||||
padding-top: $bds-cards-icon-grid-padding-y-sm;
|
||||
padding-bottom: $bds-cards-icon-grid-padding-y-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: $bds-cards-icon-grid-padding-y-md;
|
||||
padding-bottom: $bds-cards-icon-grid-padding-y-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: $bds-cards-icon-grid-padding-y-lg;
|
||||
padding-bottom: $bds-cards-icon-grid-padding-y-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Section
|
||||
// =============================================================================
|
||||
|
||||
.bds-cards-icon-grid__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-cards-icon-grid-header-gap-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-cards-icon-grid-header-gap-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-cards-icon-grid-header-gap-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-cards-icon-grid__heading {
|
||||
margin: 0;
|
||||
// Typography handled by .h-md class from _font.scss
|
||||
}
|
||||
|
||||
.bds-cards-icon-grid__description {
|
||||
margin: 0;
|
||||
// Typography handled by .body-l class from _font.scss
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Cards Grid
|
||||
// =============================================================================
|
||||
|
||||
.bds-cards-icon-grid__cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: $bds-cards-icon-grid-cards-gap-sm;
|
||||
width: 100%;
|
||||
margin-top: $bds-cards-icon-grid-section-gap-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
column-gap: $bds-cards-icon-grid-cards-gap-md;
|
||||
row-gap: $bds-cards-icon-grid-row-gap-md;
|
||||
margin-top: $bds-cards-icon-grid-section-gap-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
column-gap: $bds-cards-icon-grid-cards-gap-lg;
|
||||
row-gap: $bds-cards-icon-grid-row-gap-lg;
|
||||
margin-top: $bds-cards-icon-grid-section-gap-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-cards-icon-grid__card-wrapper {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
|
||||
// Ensure CardIcon fills the wrapper
|
||||
.bds-card-icon {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Light Mode Styles
|
||||
// =============================================================================
|
||||
|
||||
html.light {
|
||||
.bds-cards-icon-grid {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.bds-cards-icon-grid__heading {
|
||||
color: $bds-cards-icon-grid-heading-color;
|
||||
}
|
||||
|
||||
.bds-cards-icon-grid__description {
|
||||
color: $bds-cards-icon-grid-description-color;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dark Mode Styles
|
||||
// =============================================================================
|
||||
|
||||
html.dark {
|
||||
.bds-cards-icon-grid__heading {
|
||||
color: $bds-cards-icon-grid-heading-color-dark;
|
||||
}
|
||||
|
||||
.bds-cards-icon-grid__description {
|
||||
color: $bds-cards-icon-grid-description-color-dark;
|
||||
}
|
||||
}
|
||||
|
||||
121
shared/patterns/CardsIconGrid/CardsIconGrid.tsx
Normal file
121
shared/patterns/CardsIconGrid/CardsIconGrid.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { CardIcon, CardIconProps } from '../../components/CardIcon';
|
||||
import { PageGrid } from '../../components/PageGrid/page-grid';
|
||||
|
||||
/**
|
||||
* Configuration for a single card in the CardsIconGrid pattern
|
||||
*/
|
||||
export type CardsIconGridCardConfig = CardIconProps;
|
||||
|
||||
/**
|
||||
* Props for the CardsIconGrid pattern component
|
||||
*/
|
||||
export interface CardsIconGridProps extends React.ComponentPropsWithoutRef<'section'> {
|
||||
/** Section heading text */
|
||||
heading: React.ReactNode;
|
||||
/** Section description text (optional) */
|
||||
description?: React.ReactNode;
|
||||
/** Array of card configurations (uses CardIconProps) */
|
||||
cards: readonly CardsIconGridCardConfig[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique key for a card based on its props
|
||||
*/
|
||||
const getCardKey = (card: CardsIconGridCardConfig, index: number): string => {
|
||||
if (card.href) return `card-${card.href}-${index}`;
|
||||
if (card.label) return `card-${card.label.toString().slice(0, 20)}-${index}`;
|
||||
return `card-${index}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* CardsIconGrid Pattern Component
|
||||
*
|
||||
* A section pattern that displays a heading, optional description, and a responsive grid
|
||||
* of CardIcon components. Follows the "CardIconGrid" pattern from Figma.
|
||||
*
|
||||
* Features:
|
||||
* - Responsive grid layout (1 column mobile, 2 tablet, 3 desktop)
|
||||
* - Heading with `heading-md` typography (Tobias Light)
|
||||
* - Optional description with `body-l` typography (Booton Light)
|
||||
* - Proper spacing using PageGrid for container and alignment
|
||||
* - Full dark mode support
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <CardsIconGrid
|
||||
* heading="Unlock new business models with embedded payments"
|
||||
* description="Streamline development and build powerful solutions."
|
||||
* cards={[
|
||||
* {
|
||||
* icon: "/icons/wallet.svg",
|
||||
* label: "Digital Wallets",
|
||||
* href: "/docs/wallets",
|
||||
* variant: "green"
|
||||
* },
|
||||
* {
|
||||
* icon: "/icons/payments.svg",
|
||||
* label: "B2B Payment Rails",
|
||||
* href: "/docs/payments",
|
||||
* variant: "green"
|
||||
* }
|
||||
* ]}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const CardsIconGrid = React.forwardRef<HTMLElement, CardsIconGridProps>(
|
||||
function CardsIconGrid(
|
||||
{ className, heading, description, cards, ...rest },
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<section
|
||||
ref={ref}
|
||||
className={clsx('bds-cards-icon-grid', className)}
|
||||
{...rest}
|
||||
>
|
||||
<PageGrid>
|
||||
{/* Header content row */}
|
||||
<PageGrid.Row>
|
||||
<PageGrid.Col
|
||||
span={{
|
||||
base: 'fill',
|
||||
md: 6,
|
||||
lg: 8,
|
||||
}}
|
||||
>
|
||||
<div className="bds-cards-icon-grid__header">
|
||||
<h2 className="bds-cards-icon-grid__heading h-md">{heading}</h2>
|
||||
{description && (
|
||||
<p className="bds-cards-icon-grid__description body-l">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
|
||||
{/* Cards grid row */}
|
||||
<PageGrid.Row>
|
||||
<PageGrid.Col span="fill">
|
||||
<div className="bds-cards-icon-grid__cards">
|
||||
{cards.map((card, index) => (
|
||||
<div
|
||||
key={getCardKey(card, index)}
|
||||
className="bds-cards-icon-grid__card-wrapper"
|
||||
>
|
||||
<CardIcon {...card} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
</PageGrid>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardsIconGrid.displayName = 'CardsIconGrid';
|
||||
|
||||
export default CardsIconGrid;
|
||||
|
||||
3
shared/patterns/CardsIconGrid/index.ts
Normal file
3
shared/patterns/CardsIconGrid/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { CardsIconGrid, type CardsIconGridProps, type CardsIconGridCardConfig } from './CardsIconGrid';
|
||||
export { default } from './CardsIconGrid';
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
// BDS CardsTwoColumn Pattern Styles
|
||||
// Brand Design System - Section with header and 2x2 card grid
|
||||
// Uses PageGrid for responsive layout
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-cards-two-column - Base section container
|
||||
// .bds-cards-two-column__container - PageGrid container with vertical padding
|
||||
// .bds-cards-two-column__header - Header row (title + description)
|
||||
// .bds-cards-two-column__header-left - Left column (title)
|
||||
// .bds-cards-two-column__header-right - Right column (description)
|
||||
// .bds-cards-two-column__title - Section title (heading-md)
|
||||
// .bds-cards-two-column__description - Section description (body-l, muted)
|
||||
// .bds-cards-two-column__cards - Cards row (2x2 on desktop)
|
||||
//
|
||||
// Note: TextCard styles are in shared/components/TextCard/TextCard.scss
|
||||
// Note: PageGrid handles horizontal padding and column widths
|
||||
//
|
||||
// Design tokens from Figma (Section Cards - Two Column):
|
||||
//
|
||||
// Breakpoints:
|
||||
// - Mobile: < 576px (min-width: 240px, max-width: 575px)
|
||||
// - Tablet: 576px - 991px (min-width: 576px, max-width: 991px)
|
||||
// - Desktop: ≥ 992px (min-width: 992px, max-width: 1280px)
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens from Figma
|
||||
// =============================================================================
|
||||
|
||||
// Section vertical padding (horizontal handled by PageGrid)
|
||||
$bds-section-padding-y-mobile: 24px;
|
||||
$bds-section-padding-y-tablet: 32px;
|
||||
$bds-section-padding-y-desktop: 40px;
|
||||
|
||||
// Gap between header row and cards row
|
||||
$bds-section-row-gap-mobile: 24px;
|
||||
$bds-section-row-gap-tablet: 32px;
|
||||
$bds-section-row-gap-desktop: 40px;
|
||||
|
||||
// Card height for header alignment (desktop only)
|
||||
$bds-text-card-height-desktop: 340px;
|
||||
|
||||
// Colors - Light Mode (from Figma)
|
||||
$bds-text-color: $black; // #141414 - Neutral black
|
||||
$bds-text-color-muted: $gray-500; // #72777E - Neutral/500 for description
|
||||
|
||||
// =============================================================================
|
||||
// Section Container
|
||||
// =============================================================================
|
||||
|
||||
.bds-cards-two-column {
|
||||
width: 100%;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PageGrid Container Override
|
||||
// =============================================================================
|
||||
|
||||
.bds-cards-two-column__container {
|
||||
// Add vertical padding (PageGrid handles horizontal)
|
||||
padding-top: $bds-section-padding-y-mobile;
|
||||
padding-bottom: $bds-section-padding-y-mobile;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: $bds-section-padding-y-tablet;
|
||||
padding-bottom: $bds-section-padding-y-tablet;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: $bds-section-padding-y-desktop;
|
||||
padding-bottom: $bds-section-padding-y-desktop;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Row
|
||||
// =============================================================================
|
||||
|
||||
.bds-cards-two-column__header {
|
||||
// Add margin-bottom for gap between header and cards rows
|
||||
margin-bottom: $bds-section-row-gap-mobile;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-bottom: $bds-section-row-gap-tablet;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-bottom: $bds-section-row-gap-desktop;
|
||||
// Desktop: align items to match card height
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-cards-two-column__header-left {
|
||||
@include media-breakpoint-up(lg) {
|
||||
// Desktop: match card height for alignment
|
||||
min-height: $bds-text-card-height-desktop;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-cards-two-column__header-right {
|
||||
@include media-breakpoint-up(lg) {
|
||||
// Desktop: match card height, align description to bottom
|
||||
min-height: $bds-text-card-height-desktop;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-cards-two-column__title {
|
||||
margin: 0;
|
||||
color: $bds-text-color;
|
||||
// Typography handled by .h-md class from _font.scss
|
||||
}
|
||||
|
||||
.bds-cards-two-column__description {
|
||||
color: $bds-text-color-muted;
|
||||
// Typography handled by .body-l class from _font.scss
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
& + p {
|
||||
margin-top: 16px; // Paragraph spacing from Figma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Cards Row
|
||||
// =============================================================================
|
||||
|
||||
.bds-cards-two-column__cards {
|
||||
// PageGrid.Row handles the flex layout and gap
|
||||
// TextCards fill their column width automatically
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Light Mode Styles
|
||||
// =============================================================================
|
||||
|
||||
html.light {
|
||||
.bds-cards-two-column {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.bds-cards-two-column__title {
|
||||
color: $bds-text-color;
|
||||
}
|
||||
|
||||
.bds-cards-two-column__description {
|
||||
color: $bds-text-color-muted;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dark Mode Styles
|
||||
// =============================================================================
|
||||
// Dark mode color mappings from Figma (node 33054:969):
|
||||
// - Section background: Neutral/black (#141414) → $black
|
||||
// - Section title: Neutral/white (#FFFFFF) → $white
|
||||
// - Section description: Neutral/white (#FFFFFF) → $white
|
||||
|
||||
html.dark {
|
||||
.bds-cards-two-column {
|
||||
background-color: $black;
|
||||
}
|
||||
|
||||
// Section header text colors inverted for dark mode
|
||||
.bds-cards-two-column__title {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.bds-cards-two-column__description {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { TextCard, TextCardProps } from 'shared/components/TextCard';
|
||||
import { PageGrid } from 'shared/components/PageGrid';
|
||||
|
||||
/**
|
||||
* Configuration for a card in the CardsTwoColumn pattern
|
||||
*/
|
||||
export type CardsTwoColumnCardConfig = TextCardProps;
|
||||
|
||||
/**
|
||||
* Props for the CardsTwoColumn pattern component
|
||||
*/
|
||||
export interface CardsTwoColumnProps extends Omit<React.ComponentPropsWithoutRef<'section'>, 'title'> {
|
||||
/** Section title (heading-md typography) */
|
||||
title: React.ReactNode;
|
||||
/** Section description (body-l typography, muted color). Can be string or ReactNode */
|
||||
description?: React.ReactNode;
|
||||
/** Secondary description paragraph (body-l typography, muted color). Can be string or ReactNode */
|
||||
secondaryDescription?: React.ReactNode;
|
||||
/** Array of 4 card configurations for the 2x2 grid */
|
||||
cards: readonly [CardsTwoColumnCardConfig, CardsTwoColumnCardConfig, CardsTwoColumnCardConfig, CardsTwoColumnCardConfig];
|
||||
}
|
||||
|
||||
/**
|
||||
* CardsTwoColumn Pattern Component
|
||||
*
|
||||
* A section pattern that displays a header with title/description and a 2x2 grid
|
||||
* of TextCard components. Uses PageGrid for responsive layout.
|
||||
*
|
||||
* Structure:
|
||||
* - Header: Title (left) + Description (right) on desktop, stacked on tablet/mobile
|
||||
* - Cards: 2x2 grid on desktop, single column stacked on tablet/mobile
|
||||
*
|
||||
* Responsive behavior:
|
||||
* - Desktop (≥992px):
|
||||
* - Header: Title left (6 cols), description right (6 cols)
|
||||
* - Cards: 2x2 grid (6 cols each)
|
||||
* - Section padding: 40px vertical, 32px horizontal
|
||||
* - Gap between header and cards: 40px
|
||||
* - Gap between cards: 8px
|
||||
*
|
||||
* - Tablet (576-991px):
|
||||
* - Header: Stacked (title above description, full width)
|
||||
* - Cards: Single column, stacked vertically
|
||||
* - Section padding: 32px vertical, 24px horizontal
|
||||
* - Gap between header and cards: 32px
|
||||
* - Gap between cards: 8px
|
||||
*
|
||||
* - Mobile (<576px):
|
||||
* - Header: Stacked (title above description, full width)
|
||||
* - Cards: Single column, stacked vertically
|
||||
* - Section padding: 24px vertical, 16px horizontal
|
||||
* - Gap between header and cards: 24px
|
||||
* - Gap between cards: 8px
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <CardsTwoColumn
|
||||
* title="The Future of Finance is Already Onchain"
|
||||
* description="XRP Ledger isn't about bold predictions. It's about delivering value now."
|
||||
* secondaryDescription="On XRPL, you're not waiting for the future. You're building it."
|
||||
* cards={[
|
||||
* { title: "Institutions", description: "Banks, asset managers...", color: "lilac" },
|
||||
* { title: "Developers", description: "Build decentralized...", color: "neutral-light" },
|
||||
* { title: "Enterprise", description: "Scale your business...", color: "neutral-dark" },
|
||||
* { title: "Community", description: "Join the global...", color: "green" }
|
||||
* ]}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const CardsTwoColumn = React.forwardRef<HTMLElement, CardsTwoColumnProps>(
|
||||
(props, ref) => {
|
||||
const { title, description, secondaryDescription, cards, className, ...rest } = props;
|
||||
|
||||
if (cards.length !== 4) {
|
||||
console.warn('CardsTwoColumn: Exactly 4 cards are required');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={ref}
|
||||
className={clsx('bds-cards-two-column', className)}
|
||||
{...rest}
|
||||
>
|
||||
<PageGrid className="bds-cards-two-column__container">
|
||||
{/* Header Row */}
|
||||
<PageGrid.Row className="bds-cards-two-column__header">
|
||||
<PageGrid.Col span={{ md: 8, lg: 6 }} className="bds-cards-two-column__header-left">
|
||||
<h2 className="bds-cards-two-column__title h-md">{title}</h2>
|
||||
</PageGrid.Col>
|
||||
{(description || secondaryDescription) && (
|
||||
<PageGrid.Col span={{ md: 8, lg: 6 }} className="bds-cards-two-column__header-right bds-cards-two-column__description body-l">
|
||||
{description && <p>{description}</p>}
|
||||
{secondaryDescription && <p>{secondaryDescription}</p>}
|
||||
</PageGrid.Col>
|
||||
)}
|
||||
</PageGrid.Row>
|
||||
|
||||
{/* Cards Row - 2x2 on desktop, stacked on tablet/mobile */}
|
||||
<PageGrid.Row className="bds-cards-two-column__cards">
|
||||
{cards.map((card, index) => (
|
||||
<PageGrid.Col key={index} span={{ md: 8, lg: 6 }}>
|
||||
<TextCard {...card} />
|
||||
</PageGrid.Col>
|
||||
))}
|
||||
</PageGrid.Row>
|
||||
</PageGrid>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardsTwoColumn.displayName = 'CardsTwoColumn';
|
||||
|
||||
export default CardsTwoColumn;
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
# CardsTwoColumn Pattern
|
||||
|
||||
A section pattern featuring a header with title and description, plus a 2×2 grid of TextCard components. Designed for showcasing multiple related content areas with visual variety through 6 color variants.
|
||||
|
||||
## Features
|
||||
|
||||
- **Header Section**: Title (heading-md) with optional description (body-l, muted)
|
||||
- **4-Card Grid**: 2×2 layout on desktop, single column stacked on tablet/mobile
|
||||
- **6 Color Variants**: Green, neutral-light, neutral-dark, lilac, yellow, and blue
|
||||
- **Interactive States**: Hover (window shade animation), focus, and pressed states
|
||||
- **Disabled State**: Cards can be disabled with appropriate styling for light/dark modes
|
||||
- **Responsive Design**: Adapts layout and spacing across all breakpoints
|
||||
- **Dark Mode Support**: Full dark mode styling via `html.dark`
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { CardsTwoColumn } from 'shared/patterns/CardsTwoColumn';
|
||||
|
||||
<CardsTwoColumn
|
||||
title="The Future of Finance is Already Onchain"
|
||||
description="XRP Ledger isn't about bold predictions. It's about delivering value now."
|
||||
secondaryDescription="On XRPL, you're not waiting for the future. You're building it."
|
||||
cards={[
|
||||
{ title: "Institutions", description: "Banks, asset managers...", color: "lilac" },
|
||||
{ title: "Developers", description: "Build decentralized...", color: "neutral-light" },
|
||||
{ title: "Enterprise", description: "Scale your business...", color: "neutral-dark" },
|
||||
{ title: "Community", description: "Join the global...", color: "green" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `title` | `ReactNode` | *required* | Section title (heading-md typography) |
|
||||
| `description` | `ReactNode` | - | Section description (body-l, muted color) |
|
||||
| `secondaryDescription` | `ReactNode` | - | Additional description paragraph |
|
||||
| `cards` | `[TextCardProps, TextCardProps, TextCardProps, TextCardProps]` | *required* | Array of exactly 4 card configurations |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
|
||||
### TextCardProps
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `title` | `ReactNode` | *required* | Card title (heading-lg typography) |
|
||||
| `description` | `ReactNode` | - | Card description (body-l typography) |
|
||||
| `href` | `string` | - | Optional link URL (makes card clickable) |
|
||||
| `color` | `'green' \| 'neutral-light' \| 'neutral-dark' \| 'lilac' \| 'yellow' \| 'blue'` | `'neutral-light'` | Background color variant |
|
||||
| `disabled` | `boolean` | `false` | Whether the card is disabled |
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
### Desktop (≥992px)
|
||||
- Header: Two-column layout (title left, description right)
|
||||
- Cards: 2×2 grid with 8px gap
|
||||
- Section padding: 40px vertical, 32px horizontal
|
||||
- Gap between header and cards: 40px
|
||||
- Card height: 340px, padding: 24px
|
||||
|
||||
### Tablet (576-991px)
|
||||
- Header: Stacked (title above description)
|
||||
- Cards: Single column, stacked vertically with 8px gap
|
||||
- Section padding: 32px vertical, 24px horizontal
|
||||
- Gap between header and cards: 32px
|
||||
- Card height: 309px, padding: 20px
|
||||
|
||||
### Mobile (<576px)
|
||||
- Header: Stacked (title above description)
|
||||
- Cards: Single column, stacked vertically with 8px gap
|
||||
- Section padding: 24px vertical, 16px horizontal
|
||||
- Gap between header and cards: 24px
|
||||
- Card height: 274px, padding: 16px
|
||||
|
||||
## Color Variants & States
|
||||
|
||||
Each color variant has four interactive states with a "window shade" hover animation.
|
||||
|
||||
### Light Mode
|
||||
|
||||
| Variant | Default | Hover | Focus | Pressed |
|
||||
|---------|---------|-------|-------|---------|
|
||||
| `green` | `$green-200` (#70EE97) | `$green-300` (#21E46B) | `$green-300` (#21E46B) | `$green-400` (#0DAA3E) |
|
||||
| `neutral-light` | `$gray-200` (#E6EAF0) | `$gray-300` (#CAD4DF) | `$gray-300` (#CAD4DF) | `$gray-400` (#8A919A) |
|
||||
| `neutral-dark` | `$gray-300` (#CAD4DF) | `$gray-200` (#E6EAF0) | `$gray-200` (#E6EAF0) | `$gray-400` (#8A919A) |
|
||||
| `lilac` | `$lilac-200` (#D9CAFF) | `$lilac-300` (#C0A7FF) | `$lilac-300` (#C0A7FF) | `$lilac-400` (#7649E3) |
|
||||
| `yellow` | `$yellow-100` (#F3F1EB) | `$yellow-200` (#E6F1A7) | `$yellow-200` (#E6F1A7) | `$yellow-300` (#DBF15E) |
|
||||
| `blue` | `$blue-100` (#EDF4FF) | `$blue-200` (#93BFF1) | `$blue-200` (#93BFF1) | `$blue-300` (#428CFF) |
|
||||
|
||||
### Dark Mode
|
||||
|
||||
| Variant | Default | Hover | Focus | Pressed |
|
||||
|---------|---------|-------|-------|---------|
|
||||
| `green` | `$green-200` (#70EE97) | `$green-300` (#21E46B) | `$green-300` (#21E46B) | `$green-400` (#0DAA3E) |
|
||||
| `neutral-light` | `$gray-300` (#CAD4DF) | `$gray-200` (#E6EAF0) | `$gray-200` (#E6EAF0) | `$gray-400` (#8A919A) |
|
||||
| `neutral-dark` | `$gray-400` (#8A919A) | `$gray-300` (#CAD4DF) | `$gray-300` (#CAD4DF) | `$gray-500` (#72777E) |
|
||||
| `lilac` | `$lilac-200` (#D9CAFF) | `$lilac-300` (#C0A7FF) | `$lilac-300` (#C0A7FF) | `$lilac-400` (#7649E3) |
|
||||
| `yellow` | `$yellow-100` (#F3F1EB) | `$yellow-200` (#E6F1A7) | `$yellow-200` (#E6F1A7) | `$yellow-300` (#DBF15E) |
|
||||
| `blue` | `$blue-100` (#EDF4FF) | `$blue-200` (#93BFF1) | `$blue-200` (#93BFF1) | `$blue-300` (#428CFF) |
|
||||
|
||||
### Disabled State
|
||||
|
||||
| Mode | Background | Text |
|
||||
|------|------------|------|
|
||||
| Light | `$gray-100` (#F0F3F7) | `$gray-500` (#72777E) |
|
||||
| Dark | `rgba($gray-500, 0.3)` | Default text color |
|
||||
|
||||
## CSS Classes
|
||||
|
||||
```
|
||||
.bds-cards-two-column // Section container
|
||||
.bds-cards-two-column__container // Inner container with max-width
|
||||
.bds-cards-two-column__header // Header section
|
||||
.bds-cards-two-column__header-left // Left side (title)
|
||||
.bds-cards-two-column__header-right // Right side (description)
|
||||
.bds-cards-two-column__title // Section title
|
||||
.bds-cards-two-column__description // Section description
|
||||
.bds-cards-two-column__cards // Cards grid container
|
||||
```
|
||||
|
||||
## Typography Tokens
|
||||
|
||||
- **Section Title**: Uses `h-md` (heading-md, Tobias Light)
|
||||
- Desktop: 40px / 46px line-height
|
||||
- Tablet: 36px / 45px line-height
|
||||
- Mobile: 32px / 40px line-height
|
||||
|
||||
- **Section Description**: Uses `body-l` (Booton Light), color: `$gray-500`
|
||||
- All breakpoints: 18px / 26.1px line-height
|
||||
|
||||
- **Card Title**: Uses `h-lg` (heading-lg, Tobias Light)
|
||||
- Desktop: 48px / 52.8px line-height
|
||||
- Tablet: 42px / 46.2px line-height
|
||||
- Mobile: 36px / 39.6px line-height
|
||||
|
||||
- **Card Description**: Uses `body-l` (Booton Light)
|
||||
- All breakpoints: 18px / 26.1px line-height
|
||||
|
||||
## Files
|
||||
|
||||
- `CardsTwoColumn.tsx` - Main pattern component
|
||||
- `CardsTwoColumn.scss` - Styles with responsive breakpoints
|
||||
- `index.ts` - Barrel exports
|
||||
- `README.md` - This documentation
|
||||
|
||||
## Related Components
|
||||
|
||||
- **TextCard**: Core component for individual cards within the grid (`shared/components/TextCard`)
|
||||
- **PageGrid**: Can be used alongside for additional layout needs
|
||||
|
||||
## Design References
|
||||
|
||||
- **Figma Design**: [Section Cards - Two Column](https://www.figma.com/design/MP5gjNp7yPBnKBKleb8LRL/Section-Cards---Two-Column)
|
||||
- **Showcase Page**: `/about/cards-two-column-showcase`
|
||||
- **Component Location**: `shared/patterns/CardsTwoColumn/`
|
||||
|
||||
## Version History
|
||||
|
||||
- **January 2026**: Initial implementation
|
||||
- Header section with title and description
|
||||
- 2×2 card grid with 6 color variants (green, neutral-light, neutral-dark, lilac, yellow, blue)
|
||||
- Window shade hover animation
|
||||
- Full responsive design
|
||||
- Dark mode support with correct color mappings for neutral-light and neutral-dark
|
||||
- Disabled state support for light and dark modes
|
||||
@@ -1,4 +0,0 @@
|
||||
export { CardsTwoColumn, type CardsTwoColumnProps, type CardsTwoColumnCardConfig } from './CardsTwoColumn';
|
||||
export { TextCard, type TextCardProps, type TextCardColor } from 'shared/components/TextCard';
|
||||
export { default } from './CardsTwoColumn';
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
# CarouselCardList Pattern
|
||||
|
||||
A horizontal scrolling carousel pattern that displays `CardOffgrid` components with navigation buttons. Features responsive sizing, smooth scrolling, and full dark/light mode theming support.
|
||||
|
||||
## Features
|
||||
|
||||
- Horizontal scrolling card carousel with navigation buttons
|
||||
- Automatic button enable/disable based on scroll position
|
||||
- Two color variants: `neutral` and `green` (inherited by cards and buttons)
|
||||
- Responsive card sizing across mobile, tablet, and desktop breakpoints
|
||||
- Full dark mode and light mode support
|
||||
- Heading and description constrained to page grid
|
||||
- Hidden scrollbar with smooth scroll behavior
|
||||
- Keyboard navigation and accessibility support
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { CarouselCardList } from 'shared/patterns/CarouselCardList';
|
||||
|
||||
<CarouselCardList
|
||||
variant="neutral"
|
||||
heading="Why Build on the XRP Ledger"
|
||||
description="Discover the unique features that make XRPL ideal for your project."
|
||||
cards={[
|
||||
{
|
||||
icon: <TokenIcon />,
|
||||
title: "Native\nTokenization",
|
||||
description: "Issue and manage digital assets directly on the ledger.",
|
||||
href: "/docs/tokenization",
|
||||
},
|
||||
{
|
||||
icon: <WalletIcon />,
|
||||
title: "Low Cost\nTransactions",
|
||||
description: "Transaction costs are a fraction of a cent.",
|
||||
href: "/docs/fees",
|
||||
},
|
||||
// ... more cards
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
### CarouselCardListProps
|
||||
|
||||
| Prop | Type | Required | Default | Description |
|
||||
|------|------|----------|---------|-------------|
|
||||
| `variant` | `'neutral' \| 'green'` | No | `'neutral'` | Color variant for cards and navigation buttons |
|
||||
| `heading` | `ReactNode` | Yes | - | Section heading text |
|
||||
| `description` | `ReactNode` | Yes | - | Section description text |
|
||||
| `cards` | `CarouselCardConfig[]` | Yes | - | Array of card configurations |
|
||||
| `className` | `string` | No | - | Additional CSS classes for the section |
|
||||
|
||||
### CarouselCardConfig
|
||||
|
||||
Each card in the `cards` array accepts the following properties (same as `CardOffgridProps`, excluding `variant`):
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `icon` | `ReactNode \| string` | Yes | Icon component or image URL |
|
||||
| `title` | `string` | Yes | Card title (use `\n` for line breaks) |
|
||||
| `description` | `string` | Yes | Card description text |
|
||||
| `href` | `string` | No | Link destination URL |
|
||||
| `onClick` | `() => void` | No | Click handler function |
|
||||
| `disabled` | `boolean` | No | Disabled state |
|
||||
| `className` | `string` | No | Additional CSS classes |
|
||||
|
||||
## Variants
|
||||
|
||||
### Neutral Variant
|
||||
|
||||
The default variant using gray color palette. Best for general purpose content sections.
|
||||
|
||||
```tsx
|
||||
<CarouselCardList
|
||||
variant="neutral"
|
||||
heading="Platform Features"
|
||||
description="Explore what makes our platform unique."
|
||||
cards={cards}
|
||||
/>
|
||||
```
|
||||
|
||||
### Green Variant
|
||||
|
||||
Uses the brand green color palette. Best for featured or highlighted sections.
|
||||
|
||||
```tsx
|
||||
<CarouselCardList
|
||||
variant="green"
|
||||
heading="Enterprise Solutions"
|
||||
description="Purpose-built for institutional adoption."
|
||||
cards={cards}
|
||||
/>
|
||||
```
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
| Breakpoint | Card Width | Card Height | Card Padding | Button Size |
|
||||
|------------|------------|-------------|--------------|-------------|
|
||||
| Mobile (< 576px) | 343px | 400px | 16px | 37px |
|
||||
| Tablet (576px - 991px) | 356px | 440px | 20px | 37px |
|
||||
| Desktop (≥ 992px) | 400px | 480px | 24px | 40px |
|
||||
|
||||
### Spacing Tokens
|
||||
|
||||
| Token | Mobile | Tablet | Desktop |
|
||||
|-------|--------|--------|---------|
|
||||
| Header gap | 8px | 8px | 16px |
|
||||
| Section gap | 24px | 32px | 40px |
|
||||
| Cards gap | 8px | 8px | 8px |
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Navigation buttons have descriptive `aria-label` attributes ("Previous cards", "Next cards")
|
||||
- Carousel track has `role="region"` with `aria-label="Card carousel"`
|
||||
- Keyboard navigation supported via Tab and arrow keys
|
||||
- Focus ring visible on keyboard navigation (uses `focus-visible`)
|
||||
- Disabled buttons properly convey state via `disabled` attribute and visual styling
|
||||
- Cards support keyboard interaction (Tab, Enter, Space)
|
||||
|
||||
## Design Tokens
|
||||
|
||||
### Colors (Dark Mode - Default)
|
||||
|
||||
**Neutral Variant:**
|
||||
- Button Enabled: `$gray-300` (#CAD4DF)
|
||||
- Button Hover: `$gray-400` (#8A919A)
|
||||
- Button Disabled: `$gray-500` @ 50% opacity
|
||||
|
||||
**Green Variant:**
|
||||
- Button Enabled: `$green-300` (#21E46B)
|
||||
- Button Hover: `$green-200` (#70EE97)
|
||||
- Button Disabled: `$green-100` (#ACFFC5)
|
||||
|
||||
### Colors (Light Mode - `html.light`)
|
||||
|
||||
**Neutral Variant:**
|
||||
- Button Enabled: `$gray-300` (#CAD4DF)
|
||||
- Button Hover: `$gray-400` (#8A919A)
|
||||
- Button Disabled: `$gray-100` (#F0F3F7)
|
||||
|
||||
**Green Variant:**
|
||||
- Button Enabled: `$green-300` (#21E46B)
|
||||
- Button Hover: `$green-200` (#70EE97)
|
||||
- Button Disabled: `$green-100` (#ACFFC5)
|
||||
|
||||
## CSS Classes
|
||||
|
||||
| Class | Description |
|
||||
|-------|-------------|
|
||||
| `.bds-carousel-card-list` | Base section container |
|
||||
| `.bds-carousel-card-list--neutral` | Neutral color variant |
|
||||
| `.bds-carousel-card-list--green` | Green color variant |
|
||||
| `.bds-carousel-card-list__header` | Header wrapper (title, subtitle, nav) |
|
||||
| `.bds-carousel-card-list__header-content` | Title and subtitle wrapper |
|
||||
| `.bds-carousel-card-list__heading` | Section heading (uses `.h-md`) |
|
||||
| `.bds-carousel-card-list__description` | Section description (uses `.body-l`) |
|
||||
| `.bds-carousel-card-list__nav` | Navigation buttons wrapper |
|
||||
| `.bds-carousel-card-list__button` | Navigation button |
|
||||
| `.bds-carousel-card-list__button--prev` | Previous button modifier |
|
||||
| `.bds-carousel-card-list__button--disabled` | Disabled button modifier |
|
||||
| `.bds-carousel-card-list__track-wrapper` | Scroll container wrapper |
|
||||
| `.bds-carousel-card-list__track` | Horizontal scroll track |
|
||||
| `.bds-carousel-card-list__card` | Individual card wrapper |
|
||||
|
||||
## Showcase
|
||||
|
||||
View the pattern showcase at: `/about/carousel-card-list-showcase`
|
||||
|
||||
## Design References
|
||||
|
||||
- **Main Design:** [Section Carousel - Card List (Figma)](https://www.figma.com/design/w0CVv1c40nWDRD27mLiMWS/Section-Carousel---Card-List?node-id=15055-3730&m=dev)
|
||||
- **Button States:** [Carousel Button States (Figma)](https://www.figma.com/design/w0CVv1c40nWDRD27mLiMWS/Section-Carousel---Card-List?node-id=15055-1033&m=dev)
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
// BDS CarouselCardList Pattern Styles
|
||||
// Brand Design System - Horizontal scrolling carousel with CardOffgrid components
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-carousel-card-list - Base section container
|
||||
// .bds-carousel-card-list--neutral - Neutral color variant
|
||||
// .bds-carousel-card-list--green - Green color variant
|
||||
// .bds-carousel-card-list__header - Header wrapper (title, subtitle, nav)
|
||||
// .bds-carousel-card-list__header-content - Title and subtitle wrapper
|
||||
// .bds-carousel-card-list__heading - Section heading (uses .h-md)
|
||||
// .bds-carousel-card-list__description - Section description (uses .body-l)
|
||||
// .bds-carousel-card-list__nav - Navigation buttons wrapper
|
||||
// .bds-carousel-card-list__track-wrapper - Scroll container wrapper
|
||||
// .bds-carousel-card-list__track - Horizontal scroll track
|
||||
// .bds-carousel-card-list__card - Individual card wrapper
|
||||
//
|
||||
// Note: Navigation button styles are in shared/components/CarouselButton/CarouselButton.scss
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens (from Figma)
|
||||
// =============================================================================
|
||||
|
||||
$bds-grid-gutter: 8px;
|
||||
|
||||
// Grid padding (matches PageGrid container padding)
|
||||
$bds-carousel-grid-padding-sm: 18px; // Mobile
|
||||
$bds-carousel-grid-padding-md: 24px; // Tablet
|
||||
$bds-carousel-grid-padding-lg: 32px; // Desktop (lg+)
|
||||
$bds-carousel-grid-max-width: 1280px; // Max container width (per _breakpoints.scss $xl)
|
||||
|
||||
// Spacing - Header gap (between heading and description)
|
||||
$bds-carousel-header-gap-sm: 8px; // Mobile
|
||||
$bds-carousel-header-gap-md: 8px; // Tablet
|
||||
$bds-carousel-header-gap-lg: 16px; // Desktop
|
||||
|
||||
// Spacing - Section gap (between header content and buttons row on mobile)
|
||||
$bds-carousel-section-gap-sm: 24px; // Mobile
|
||||
$bds-carousel-section-gap-md: 32px; // Tablet
|
||||
$bds-carousel-section-gap-lg: 40px; // Desktop
|
||||
|
||||
// Spacing - Gap between header and cards
|
||||
$bds-carousel-cards-gap-sm: 24px; // Mobile
|
||||
$bds-carousel-cards-gap-md: 32px; // Tablet
|
||||
$bds-carousel-cards-gap-lg: 40px; // Desktop
|
||||
|
||||
// Button gap (button styles are in shared/components/CarouselButton)
|
||||
$bds-carousel-button-gap: 8px;
|
||||
|
||||
// Card dimensions per breakpoint
|
||||
$bds-carousel-card-width-sm: 343px; // Mobile
|
||||
$bds-carousel-card-height-sm: 400px;
|
||||
$bds-carousel-card-width-md: 356px; // Tablet
|
||||
$bds-carousel-card-height-md: 440px;
|
||||
$bds-carousel-card-width-lg: 400px; // Desktop
|
||||
$bds-carousel-card-height-lg: 480px;
|
||||
|
||||
// Card padding per breakpoint
|
||||
$bds-carousel-card-padding-sm: 16px;
|
||||
$bds-carousel-card-padding-md: 20px;
|
||||
$bds-carousel-card-padding-lg: 24px;
|
||||
|
||||
// Transition
|
||||
$bds-carousel-transition: 200ms cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
|
||||
// =============================================================================
|
||||
// Section Container
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-card-list {
|
||||
width: 100%;
|
||||
// Constrain to max-width at xl breakpoint (per _breakpoints.scss)
|
||||
@include media-breakpoint-up(xl) {
|
||||
max-width: $bds-carousel-grid-max-width;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
// Allow focus rings to be visible (no overflow:hidden)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Section
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-card-list__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-carousel-section-gap-sm;
|
||||
// Apply same padding as track to align header with cards
|
||||
padding-left: $bds-carousel-grid-padding-sm;
|
||||
padding-right: $bds-carousel-grid-padding-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-carousel-section-gap-md;
|
||||
padding-left: $bds-carousel-grid-padding-md;
|
||||
padding-right: $bds-carousel-grid-padding-md;
|
||||
}
|
||||
|
||||
// Row layout only at desktop (lg and up)
|
||||
@include media-breakpoint-up(lg) {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: $bds-carousel-section-gap-lg;
|
||||
padding-left: $bds-carousel-grid-padding-lg;
|
||||
padding-right: $bds-carousel-grid-padding-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-carousel-card-list__header-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-carousel-header-gap-sm;
|
||||
// Full width on mobile and tablet
|
||||
max-width: 100%;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-carousel-header-gap-md;
|
||||
}
|
||||
|
||||
// Constrain heading/description to grid (8 columns at desktop)
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-carousel-header-gap-lg;
|
||||
max-width: 808px; // Desktop: 8 columns
|
||||
}
|
||||
}
|
||||
|
||||
.bds-carousel-card-list__heading {
|
||||
margin: 0;
|
||||
// Typography handled by .h-md class from _font.scss
|
||||
}
|
||||
|
||||
.bds-carousel-card-list__description {
|
||||
margin: 0;
|
||||
// Typography handled by .body-l class from _font.scss
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Navigation Buttons Container
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-card-list__nav {
|
||||
display: flex;
|
||||
gap: $bds-carousel-button-gap;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
// Add padding to allow focus ring to be visible without clipping
|
||||
padding: 4px;
|
||||
margin: -4px;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Scroll Track
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-card-list__track-wrapper {
|
||||
margin-top: $bds-carousel-cards-gap-sm;
|
||||
overflow: visible;
|
||||
// Add left padding here so it's OUTSIDE the scrollable area
|
||||
padding-left: $bds-carousel-grid-padding-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-top: $bds-carousel-cards-gap-md;
|
||||
padding-left: $bds-carousel-grid-padding-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-top: $bds-carousel-cards-gap-lg;
|
||||
padding-left: $bds-carousel-grid-padding-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-carousel-card-list__track {
|
||||
display: flex;
|
||||
gap: $bds-grid-gutter;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
scroll-snap-type: x mandatory;
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
// Hide scrollbar but keep functionality
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Vertical padding to prevent focus ring clipping
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
margin-top: -4px;
|
||||
margin-bottom: -4px;
|
||||
|
||||
// Focus outline for keyboard navigation
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $white;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Card Wrapper
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-card-list__card {
|
||||
flex-shrink: 0;
|
||||
scroll-snap-align: start;
|
||||
|
||||
// Override CardOffgrid dimensions for carousel
|
||||
.bds-card-offgrid {
|
||||
width: $bds-carousel-card-width-sm;
|
||||
height: $bds-carousel-card-height-sm;
|
||||
padding: $bds-carousel-card-padding-sm;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
width: $bds-carousel-card-width-md;
|
||||
height: $bds-carousel-card-height-md;
|
||||
padding: $bds-carousel-card-padding-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
width: $bds-carousel-card-width-lg;
|
||||
height: $bds-carousel-card-height-lg;
|
||||
padding: $bds-carousel-card-padding-lg;
|
||||
}
|
||||
|
||||
// Fix: Prevent unwanted hover styles from parent styles
|
||||
// No text underline on hover
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// Ensure title and description never have underline
|
||||
.bds-card-offgrid__title,
|
||||
.bds-card-offgrid__description {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure icon does not change color on hover
|
||||
.bds-card-offgrid__icon-container {
|
||||
// Icon color is inherited from card text color, which CardOffgrid manages
|
||||
// No additional color changes should happen on hover
|
||||
> * {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DARK MODE (Default) - Section Text Colors
|
||||
// =============================================================================
|
||||
|
||||
// Section text colors - Dark Mode (applies to both neutral and green card variants)
|
||||
.bds-carousel-card-list--neutral,
|
||||
.bds-carousel-card-list--green {
|
||||
.bds-carousel-card-list__heading,
|
||||
.bds-carousel-card-list__description {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LIGHT MODE (html.light) - Color Variants
|
||||
// =============================================================================
|
||||
|
||||
html.light {
|
||||
.bds-carousel-card-list__track {
|
||||
&:focus-visible {
|
||||
outline-color: $gray-900;
|
||||
}
|
||||
}
|
||||
|
||||
// Section text colors - Light Mode
|
||||
.bds-carousel-card-list--neutral,
|
||||
.bds-carousel-card-list--green {
|
||||
.bds-carousel-card-list__heading,
|
||||
.bds-carousel-card-list__description {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
import React, { useRef, useState, useCallback, useEffect } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { CardOffgrid, CardOffgridProps } from '../../components/CardOffgrid';
|
||||
import { CarouselButton } from '../../components/CarouselButton';
|
||||
import type { ButtonProps } from '../../components/Button';
|
||||
|
||||
/**
|
||||
* Configuration for a single card in the CarouselCardList pattern
|
||||
* Extends CardOffgridProps but removes variant (controlled by carousel)
|
||||
*/
|
||||
export type CarouselCardConfig = Omit<CardOffgridProps, 'variant'>;
|
||||
|
||||
/** BEM class name for card elements */
|
||||
const CARD_CLASS_NAME = 'bds-carousel-card-list__card';
|
||||
|
||||
/**
|
||||
* Props for the CarouselCardList pattern component
|
||||
*/
|
||||
export interface CarouselCardListProps extends React.ComponentPropsWithoutRef<'section'> {
|
||||
/** Color variant of the cards */
|
||||
variant?: 'neutral' | 'green';
|
||||
/** Color variant of the navigation buttons (independent of card color). Defaults to 'neutral'. Derived from Button color prop. */
|
||||
buttonVariant?: ButtonProps['color'] | 'neutral';
|
||||
/** Section heading text */
|
||||
heading: React.ReactNode;
|
||||
/** Section description text */
|
||||
description: React.ReactNode;
|
||||
/** Array of card configurations */
|
||||
cards: readonly CarouselCardConfig[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a stable key for a card based on its properties.
|
||||
*/
|
||||
const getCardKey = (card: CarouselCardConfig, index: number): string | number => {
|
||||
if (card.href) return card.href;
|
||||
if (card.title) return `${card.title}-${index}`;
|
||||
return index;
|
||||
};
|
||||
|
||||
/**
|
||||
* CarouselCardList Pattern Component
|
||||
*
|
||||
* A horizontal scrolling carousel that displays CardOffgrid components.
|
||||
* Features navigation buttons that scroll cards in/out of view.
|
||||
* The navigation button colors can be set independently of the card colors
|
||||
* using the `buttonVariant` prop.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <CarouselCardList
|
||||
* variant="neutral"
|
||||
* buttonVariant="green"
|
||||
* heading="Why Choose Our Platform"
|
||||
* description="Discover the benefits of our solution."
|
||||
* cards={[
|
||||
* { icon: <Icon />, title: "Feature 1", description: "..." },
|
||||
* { icon: <Icon />, title: "Feature 2", description: "..." },
|
||||
* ]}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const CarouselCardList = React.forwardRef<HTMLElement, CarouselCardListProps>(
|
||||
(props, ref) => {
|
||||
const { variant = 'neutral', buttonVariant = 'neutral', heading, description, cards, className, ...rest } = props;
|
||||
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [canScrollPrev, setCanScrollPrev] = useState(false);
|
||||
const [canScrollNext, setCanScrollNext] = useState(true);
|
||||
|
||||
// Check scroll position and update button states
|
||||
const updateScrollButtons = useCallback(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const { scrollLeft, scrollWidth, clientWidth } = container;
|
||||
setCanScrollPrev(scrollLeft > 0);
|
||||
setCanScrollNext(scrollLeft + clientWidth < scrollWidth - 1);
|
||||
}, []);
|
||||
|
||||
// Initialize and listen for scroll events
|
||||
useEffect(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
updateScrollButtons();
|
||||
container.addEventListener('scroll', updateScrollButtons, { passive: true });
|
||||
window.addEventListener('resize', updateScrollButtons);
|
||||
|
||||
return () => {
|
||||
container.removeEventListener('scroll', updateScrollButtons);
|
||||
window.removeEventListener('resize', updateScrollButtons);
|
||||
};
|
||||
}, [updateScrollButtons, cards.length]);
|
||||
|
||||
// Scroll by one card width
|
||||
const scroll = useCallback((direction: 'prev' | 'next') => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
// Get the first card to determine scroll amount
|
||||
const card = container.querySelector(`.${CARD_CLASS_NAME}`) as HTMLElement;
|
||||
if (!card) return;
|
||||
|
||||
const cardWidth = card.offsetWidth;
|
||||
const gap = 8; // 8px gap between cards
|
||||
const scrollAmount = cardWidth + gap;
|
||||
|
||||
container.scrollBy({
|
||||
left: direction === 'next' ? scrollAmount : -scrollAmount,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Early return for empty cards
|
||||
if (cards.length === 0) {
|
||||
console.warn('CarouselCardList: No cards provided');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={ref}
|
||||
className={clsx('bds-carousel-card-list', `bds-carousel-card-list--${variant}`, className)}
|
||||
{...rest}
|
||||
>
|
||||
{/* Header with title, description, and navigation buttons */}
|
||||
<div className="bds-carousel-card-list__header">
|
||||
<div className="bds-carousel-card-list__header-content">
|
||||
<h2 className="bds-carousel-card-list__heading h-md">{heading}</h2>
|
||||
<p className="bds-carousel-card-list__description body-l">{description}</p>
|
||||
</div>
|
||||
<div className="bds-carousel-card-list__nav">
|
||||
{(['prev', 'next'] as const).map((direction) => (
|
||||
<CarouselButton
|
||||
key={direction}
|
||||
direction={direction}
|
||||
variant={buttonVariant}
|
||||
disabled={direction === 'prev' ? !canScrollPrev : !canScrollNext}
|
||||
onClick={() => scroll(direction)}
|
||||
aria-label={direction === 'prev' ? 'Previous cards' : 'Next cards'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards scroll container - full bleed */}
|
||||
<div className="bds-carousel-card-list__track-wrapper">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="bds-carousel-card-list__track"
|
||||
role="region"
|
||||
aria-label="Card carousel"
|
||||
tabIndex={0}
|
||||
>
|
||||
{cards.map((card, index) => (
|
||||
<div key={getCardKey(card, index)} className={CARD_CLASS_NAME}>
|
||||
<CardOffgrid {...card} variant={variant} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CarouselCardList.displayName = 'CarouselCardList';
|
||||
|
||||
export default CarouselCardList;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export { CarouselCardList, type CarouselCardListProps, type CarouselCardConfig } from './CarouselCardList';
|
||||
export { default } from './CarouselCardList';
|
||||
|
||||
@@ -1,437 +0,0 @@
|
||||
// BDS CarouselFeatured Pattern Styles
|
||||
// Brand Design System - Featured image carousel with two-column layout
|
||||
//
|
||||
// Layout:
|
||||
// - Desktop (lg+): Two-column layout - Image LEFT (50%), Content RIGHT (50%)
|
||||
// - Tablet/Mobile: Single column - Content TOP, Image BOTTOM
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-carousel-featured - Base section container
|
||||
// .bds-carousel-featured__media-col - Image/media column wrapper
|
||||
// .bds-carousel-featured__content-col - Content column wrapper
|
||||
// .bds-carousel-featured__content - Content column
|
||||
// .bds-carousel-featured__header - Header row (heading + nav)
|
||||
// .bds-carousel-featured__heading - Section heading
|
||||
// .bds-carousel-featured__nav - Navigation buttons wrapper
|
||||
// .bds-carousel-featured__bottom - Bottom section (features + CTA)
|
||||
// .bds-carousel-featured__features - Feature list container
|
||||
// .bds-carousel-featured__feature - Individual feature item
|
||||
// .bds-carousel-featured__feature-title - Feature title
|
||||
// .bds-carousel-featured__feature-description - Feature description
|
||||
// .bds-carousel-featured__cta - CTA section (buttons + mobile nav)
|
||||
// .bds-carousel-featured__buttons - Button group wrapper
|
||||
// .bds-carousel-featured__slides - Slides container
|
||||
// .bds-carousel-featured__slide-track - Sliding track
|
||||
// .bds-carousel-featured__slide - Individual slide
|
||||
// .bds-carousel-featured__slide--active - Active slide modifier
|
||||
// .bds-carousel-featured__image - Slide image
|
||||
//
|
||||
// Note: This file is imported within xrpl.scss after Bootstrap and project
|
||||
// variables are loaded, so $grid-breakpoints, colors, and mixins are available.
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens (from Figma)
|
||||
// =============================================================================
|
||||
|
||||
// Spacing
|
||||
$bds-carousel-featured-padding-sm: 24px 16px;
|
||||
$bds-carousel-featured-padding-md: 32px 24px;
|
||||
$bds-carousel-featured-padding-lg: 40px 32px;
|
||||
|
||||
// Content gap between image and content columns
|
||||
$bds-carousel-featured-column-gap: 8px;
|
||||
|
||||
// Transition
|
||||
$bds-carousel-featured-transition: 400ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
// =============================================================================
|
||||
// Color Variant Configuration Map
|
||||
// =============================================================================
|
||||
|
||||
// Define all background variants with their color properties (Dark Mode)
|
||||
$bds-carousel-featured-variants: (
|
||||
'grey': (
|
||||
'bg-color': $gray-300,
|
||||
'text-color': $black,
|
||||
'divider-color': $black,
|
||||
'button-variant': 'black',
|
||||
'button-bg': $black,
|
||||
'button-color': $white,
|
||||
'button-hover': $gray-500,
|
||||
'button-active': $black
|
||||
),
|
||||
'neutral': (
|
||||
'bg-color': $black,
|
||||
'text-color': $white,
|
||||
'divider-color': $white,
|
||||
'button-variant': 'green',
|
||||
'button-bg': $green-300,
|
||||
'button-color': $black,
|
||||
'button-hover': $green-200,
|
||||
'button-active': $green-300
|
||||
),
|
||||
'yellow': (
|
||||
'bg-color': $yellow-100,
|
||||
'text-color': $black,
|
||||
'divider-color': $black,
|
||||
'button-variant': 'black',
|
||||
'button-bg': $black,
|
||||
'button-color': $white,
|
||||
'button-hover': $gray-500,
|
||||
'button-active': $black
|
||||
)
|
||||
);
|
||||
|
||||
// Define light mode variant overrides
|
||||
$bds-carousel-featured-variants-light: (
|
||||
'grey': (
|
||||
'bg-color': $gray-200,
|
||||
'text-color': $black,
|
||||
'divider-color': $black
|
||||
),
|
||||
'neutral': (
|
||||
'bg-color': $white,
|
||||
'text-color': $black,
|
||||
'divider-color': $black
|
||||
),
|
||||
'yellow': (
|
||||
'bg-color': $yellow-100,
|
||||
'text-color': $black,
|
||||
'divider-color': $black
|
||||
)
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// Mixins: Apply Background Variant Styles
|
||||
// =============================================================================
|
||||
|
||||
// Full variant mixin (for dark mode with button styles)
|
||||
@mixin carousel-featured-variant($variant-name, $config) {
|
||||
&--bg-#{$variant-name} {
|
||||
background-color: map-get($config, 'bg-color');
|
||||
|
||||
// Text colors
|
||||
.bds-carousel-featured__heading,
|
||||
.bds-carousel-featured__feature-title,
|
||||
.bds-carousel-featured__feature-description {
|
||||
color: map-get($config, 'text-color');
|
||||
}
|
||||
|
||||
// Divider color
|
||||
.bds-divider {
|
||||
background-color: map-get($config, 'divider-color');
|
||||
}
|
||||
|
||||
// Carousel nav buttons - enabled states only
|
||||
// Disabled states are handled by CarouselButton component styles
|
||||
.bds-carousel-button--#{map-get($config, 'button-variant')} {
|
||||
background-color: map-get($config, 'button-bg');
|
||||
color: map-get($config, 'button-color');
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: map-get($config, 'button-hover');
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: map-get($config, 'button-active');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Light mode variant mixin (only colors, no button states)
|
||||
@mixin carousel-featured-variant-light($variant-name, $config) {
|
||||
.bds-carousel-featured--bg-#{$variant-name} {
|
||||
background-color: map-get($config, 'bg-color');
|
||||
|
||||
.bds-carousel-featured__heading,
|
||||
.bds-carousel-featured__feature-title,
|
||||
.bds-carousel-featured__feature-description {
|
||||
color: map-get($config, 'text-color');
|
||||
}
|
||||
|
||||
.bds-divider {
|
||||
background-color: map-get($config, 'divider-color');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Base Container Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-featured {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
// Default background - dark mode default (grey variant)
|
||||
background-color: $gray-300;
|
||||
|
||||
// Mobile (default)
|
||||
padding: $bds-carousel-featured-padding-sm;
|
||||
|
||||
// Tablet
|
||||
@include media-breakpoint-up(md) {
|
||||
padding: $bds-carousel-featured-padding-md;
|
||||
}
|
||||
|
||||
// Desktop
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding: $bds-carousel-featured-padding-lg;
|
||||
}
|
||||
|
||||
// Max width constraint
|
||||
@include media-breakpoint-up(xl) {
|
||||
max-width: 1280px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Background Color Variants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Dark Mode (default) - Generate all variant styles using the mixin
|
||||
@include bds-theme-mode(dark) {
|
||||
@each $variant-name, $config in $bds-carousel-featured-variants {
|
||||
@include carousel-featured-variant($variant-name, $config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Content Column
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-featured__content-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
// Stretch to match image height
|
||||
align-self: stretch;
|
||||
// Add 8px padding-left to create 16px total gap (8px row gap + 8px padding)
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-carousel-featured__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 0; // Use space-between instead
|
||||
min-height: 500px; // Mobile min height
|
||||
justify-content: space-between; // Header at top, features+CTA at bottom
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
min-height: 440px; // Tablet min height
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
flex: 1;
|
||||
min-height: auto; // Reset min-height on desktop
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Section (Heading + Nav)
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-featured__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bds-carousel-featured__heading {
|
||||
margin: 0;
|
||||
// Dark mode default: light text on dark background
|
||||
color: $white;
|
||||
max-width: 392px;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Bottom Section (Features + CTA grouped together)
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-featured__bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px; // Mobile
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: 32px; // Tablet
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: 40px; // Desktop
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Navigation Buttons
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-featured__nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
|
||||
// Desktop nav (in header row)
|
||||
&--desktop {
|
||||
display: none;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile/Tablet nav (in CTA row)
|
||||
&--mobile {
|
||||
display: flex;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Feature List
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-featured__features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bds-carousel-featured__feature {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
|
||||
// Spacing between description and next divider
|
||||
// Mobile/Tablet: 16px, Desktop: 24px
|
||||
&:not(:first-child) {
|
||||
padding-top: 16px;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bds-carousel-featured__feature-title {
|
||||
margin: 0;
|
||||
// Dark mode default: light text on dark background
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.bds-carousel-featured__feature-description {
|
||||
margin: 0;
|
||||
// Dark mode default: muted light text on dark background
|
||||
color: $gray-400;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CTA Section (Buttons + Mobile Nav)
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-featured__cta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
gap: 16px;
|
||||
|
||||
// Tablet+: no wrap needed
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-wrap: nowrap;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
// Desktop: nav is hidden, so buttons just align left
|
||||
@include media-breakpoint-up(lg) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Slides Container
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-featured__slides {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bds-carousel-featured__slide-track {
|
||||
display: flex;
|
||||
transition: transform $bds-carousel-featured-transition;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Individual Slides
|
||||
// =============================================================================
|
||||
|
||||
.bds-carousel-featured__slide {
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
// Mobile: 343/193 aspect ratio
|
||||
aspect-ratio: 343 / 193;
|
||||
|
||||
// Tablet: 16/9 aspect ratio
|
||||
@include media-breakpoint-up(md) {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
// Desktop: Square aspect ratio (604x604)
|
||||
@include media-breakpoint-up(lg) {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-carousel-featured__image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LIGHT MODE (html.light) - Color Overrides
|
||||
// =============================================================================
|
||||
|
||||
html.light {
|
||||
// Default (no variant class) - Light mode: gray-200 background
|
||||
.bds-carousel-featured {
|
||||
background-color: $gray-200;
|
||||
|
||||
.bds-carousel-featured__heading,
|
||||
.bds-carousel-featured__feature-title,
|
||||
.bds-carousel-featured__feature-description {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.bds-divider {
|
||||
background-color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate all light mode variant overrides using the mixin
|
||||
@each $variant-name, $config in $bds-carousel-featured-variants-light {
|
||||
@include carousel-featured-variant-light($variant-name, $config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { CarouselButton } from '../../components/CarouselButton';
|
||||
import { Divider } from '../../components/Divider';
|
||||
import { PageGrid, PageGridRow, PageGridCol } from '../../components/PageGrid';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
|
||||
/**
|
||||
* Props for a single slide in the CarouselFeatured component
|
||||
*/
|
||||
export interface CarouselSlide {
|
||||
/** Unique identifier for the slide */
|
||||
id: string | number;
|
||||
/** Image source URL */
|
||||
imageSrc: string;
|
||||
/** Alt text for the image */
|
||||
imageAlt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for a feature list item
|
||||
*/
|
||||
export interface CarouselFeatureItem {
|
||||
/** Feature title */
|
||||
title: string;
|
||||
/** Feature description */
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Background color options for CarouselFeatured
|
||||
* Each variant adapts to light/dark mode:
|
||||
* - 'grey': Light mode: gray-200 (#E6EAF0), Dark mode: gray-300 (#CAD4DF)
|
||||
* - 'neutral': Light mode: white (#FFF), Dark mode: black (#141414)
|
||||
* - 'yellow': Light mode: yellow-100 (#F3F1EB), Dark mode: yellow-100 (#F3F1EB)
|
||||
*/
|
||||
export type CarouselFeaturedBackground = 'grey' | 'neutral' | 'yellow';
|
||||
|
||||
/**
|
||||
* Props for the CarouselFeatured pattern component
|
||||
*/
|
||||
export interface CarouselFeaturedProps extends React.ComponentPropsWithoutRef<'section'> {
|
||||
/** Array of slides to display */
|
||||
slides: readonly CarouselSlide[];
|
||||
/** Heading text displayed at the top of the content area */
|
||||
heading: string;
|
||||
/** Array of feature items to display in the list */
|
||||
features: readonly CarouselFeatureItem[];
|
||||
/** Button configurations (1-2 buttons supported) */
|
||||
buttons?: ButtonConfig[];
|
||||
/** Background color variant. Defaults to 'grey'. */
|
||||
background?: CarouselFeaturedBackground;
|
||||
}
|
||||
|
||||
/**
|
||||
* CarouselFeatured Pattern Component
|
||||
*
|
||||
* A featured image carousel with two-column layout on desktop (image left, content right)
|
||||
* and single-column layout on tablet/mobile (content top, image bottom).
|
||||
* Features a heading, feature list with dividers, and optional buttons.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <CarouselFeatured
|
||||
* heading="Powered by Developers"
|
||||
* features={[
|
||||
* { title: "Easy-to-Integrate APIs", description: "Build with common languages..." },
|
||||
* { title: "Full Lifecycle Support", description: "From dev tools to deployment..." },
|
||||
* ]}
|
||||
* buttons={[
|
||||
* { label: "Get Started", href: "/docs" },
|
||||
* { label: "Learn More", href: "/about" }
|
||||
* ]}
|
||||
* slides={[
|
||||
* { id: 1, imageSrc: '/image1.jpg', imageAlt: 'Slide 1' },
|
||||
* ]}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const CarouselFeatured = React.forwardRef<HTMLElement, CarouselFeaturedProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
slides,
|
||||
heading,
|
||||
features,
|
||||
buttons,
|
||||
background = 'grey',
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
const canGoPrev = currentIndex > 0;
|
||||
const canGoNext = currentIndex < slides.length - 1;
|
||||
|
||||
// Validate buttons if provided (max 2 buttons supported)
|
||||
const buttonValidation = validateButtonGroup(buttons, 2);
|
||||
const hasButtons = buttonValidation.hasButtons;
|
||||
|
||||
const goToPrev = useCallback(() => {
|
||||
if (canGoPrev) {
|
||||
setCurrentIndex((prev) => prev - 1);
|
||||
}
|
||||
}, [canGoPrev]);
|
||||
|
||||
const goToNext = useCallback(() => {
|
||||
if (canGoNext) {
|
||||
setCurrentIndex((prev) => prev + 1);
|
||||
}
|
||||
}, [canGoNext]);
|
||||
|
||||
// Early return for empty slides
|
||||
if (slides.length === 0) {
|
||||
console.warn('CarouselFeatured: No slides provided');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine carousel nav button variant based on background
|
||||
// grey/yellow → black (always), neutral → green (always)
|
||||
const buttonVariant = background === 'neutral' ? 'green' : 'black';
|
||||
|
||||
return (
|
||||
<PageGrid
|
||||
ref={ref as React.Ref<HTMLDivElement>}
|
||||
className={clsx(
|
||||
'bds-carousel-featured',
|
||||
`bds-carousel-featured--bg-${background}`,
|
||||
className
|
||||
)}
|
||||
aria-roledescription="carousel"
|
||||
aria-label={heading}
|
||||
{...rest}>
|
||||
<PageGridRow>
|
||||
{/* Content Column - Right on desktop, top on mobile */}
|
||||
<PageGridCol
|
||||
span={{ base: 4, md: 8, lg: 6 }}
|
||||
className="bds-carousel-featured__content-col order-1 order-lg-2"
|
||||
>
|
||||
<div className="bds-carousel-featured__content">
|
||||
{/* Header row with heading and nav buttons */}
|
||||
<div className="bds-carousel-featured__header">
|
||||
<h2 className="bds-carousel-featured__heading h-md">{heading}</h2>
|
||||
<div className={clsx(
|
||||
'bds-carousel-featured__nav',
|
||||
'bds-carousel-featured__nav--desktop',
|
||||
slides.length === 1 && 'd-none'
|
||||
)}>
|
||||
{(['prev', 'next'] as const).map((direction) => (
|
||||
<CarouselButton
|
||||
key={direction}
|
||||
direction={direction}
|
||||
variant={buttonVariant}
|
||||
disabled={direction === 'prev' ? !canGoPrev : !canGoNext}
|
||||
onClick={direction === 'prev' ? goToPrev : goToNext}
|
||||
aria-label={direction === 'prev' ? 'Previous slide' : 'Next slide'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom section: features + CTA grouped together */}
|
||||
<div className="bds-carousel-featured__bottom">
|
||||
{/* Feature list with dividers */}
|
||||
<div className="bds-carousel-featured__features">
|
||||
{features.map((feature, index) => (
|
||||
<div key={index} className="bds-carousel-featured__feature">
|
||||
<Divider color="base" weight="regular" />
|
||||
<p className="bds-carousel-featured__feature-title body-r">{feature.title}</p>
|
||||
<p className="bds-carousel-featured__feature-description label-l">{feature.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA section with buttons and mobile nav */}
|
||||
<div className="bds-carousel-featured__cta">
|
||||
{/* Buttons wrapper - groups primary and tertiary together */}
|
||||
{hasButtons && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color="black"
|
||||
forceColor={background !== 'neutral'}
|
||||
className="bds-carousel-featured__buttons"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mobile/Tablet nav buttons */}
|
||||
<div className={clsx(
|
||||
'bds-carousel-featured__nav',
|
||||
'bds-carousel-featured__nav--mobile',
|
||||
slides.length === 1 && 'd-none'
|
||||
)}>
|
||||
{(['prev', 'next'] as const).map((direction) => (
|
||||
<CarouselButton
|
||||
key={direction}
|
||||
direction={direction}
|
||||
variant={buttonVariant}
|
||||
disabled={direction === 'prev' ? !canGoPrev : !canGoNext}
|
||||
onClick={direction === 'prev' ? goToPrev : goToNext}
|
||||
aria-label={direction === 'prev' ? 'Previous slide' : 'Next slide'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
|
||||
{/* Image/Media Column - Left on desktop, bottom on mobile */}
|
||||
<PageGridCol
|
||||
span={{ base: 4, md: 8, lg: 6 }}
|
||||
className="bds-carousel-featured__media-col order-2 order-lg-1"
|
||||
>
|
||||
<div
|
||||
className="bds-carousel-featured__slides"
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
aria-label={`Slide ${currentIndex + 1} of ${slides.length}`}
|
||||
>
|
||||
<div
|
||||
className="bds-carousel-featured__slide-track"
|
||||
style={{ transform: `translateX(-${currentIndex * 100}%)` }}
|
||||
>
|
||||
{slides.map((slide, index) => (
|
||||
<div
|
||||
key={slide.id}
|
||||
className={clsx(
|
||||
'bds-carousel-featured__slide',
|
||||
{ 'bds-carousel-featured__slide--active': index === currentIndex }
|
||||
)}
|
||||
aria-hidden={index !== currentIndex}
|
||||
>
|
||||
<img
|
||||
src={slide.imageSrc}
|
||||
alt={slide.imageAlt}
|
||||
className="bds-carousel-featured__image"
|
||||
loading={index === 0 ? 'eager' : 'lazy'}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
|
||||
{/* Render any additional children */}
|
||||
{children}
|
||||
</PageGrid>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CarouselFeatured.displayName = 'CarouselFeatured';
|
||||
|
||||
export default CarouselFeatured;
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
# CarouselFeatured
|
||||
|
||||
A featured image carousel pattern with a two-column layout on desktop (image left, content right) and single-column layout on tablet/mobile (content top, image bottom). Features a heading, feature list with dividers, optional buttons, and navigation controls.
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { CarouselFeatured } from '@/shared/patterns/CarouselFeatured';
|
||||
|
||||
<CarouselFeatured
|
||||
heading="Powered by Developers"
|
||||
features={[
|
||||
{ title: "Easy-to-Integrate APIs", description: "Build with common languages..." },
|
||||
{ title: "Full Lifecycle Support", description: "From dev tools to deployment..." },
|
||||
]}
|
||||
buttons={[
|
||||
{ label: "Get Started", href: "/docs" },
|
||||
{ label: "Learn More", href: "/about" }
|
||||
]}
|
||||
slides={[
|
||||
{ id: 1, imageSrc: '/image1.jpg', imageAlt: 'Slide 1' },
|
||||
{ id: 2, imageSrc: '/image2.jpg', imageAlt: 'Slide 2' },
|
||||
]}
|
||||
background="grey"
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
### Required Props
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `heading` | `string` | Heading text displayed at the top of the content area |
|
||||
| `features` | `CarouselFeatureItem[]` | Array of feature items with title and description |
|
||||
| `slides` | `CarouselSlide[]` | Array of slides to display in the carousel |
|
||||
|
||||
### Optional Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `buttons` | `ButtonConfig[]` | `undefined` | Array of button configurations (1-2 buttons supported, uses ButtonGroup) |
|
||||
| `background` | `'grey' \| 'neutral' \| 'yellow'` | `'grey'` | Background color variant |
|
||||
|
||||
## Type Definitions
|
||||
|
||||
### CarouselSlide
|
||||
|
||||
```tsx
|
||||
interface CarouselSlide {
|
||||
id: string | number; // Unique identifier for the slide
|
||||
imageSrc: string; // Image source URL
|
||||
imageAlt: string; // Alt text for the image
|
||||
}
|
||||
```
|
||||
|
||||
### CarouselFeatureItem
|
||||
|
||||
```tsx
|
||||
interface CarouselFeatureItem {
|
||||
title: string; // Feature title
|
||||
description: string; // Feature description
|
||||
}
|
||||
```
|
||||
|
||||
### ButtonConfig
|
||||
|
||||
```tsx
|
||||
interface ButtonConfig {
|
||||
label: string; // Button text
|
||||
href?: string; // Optional link URL
|
||||
onClick?: () => void; // Optional click handler
|
||||
forceColor?: boolean; // Force button color override
|
||||
}
|
||||
```
|
||||
|
||||
## Background Variants
|
||||
|
||||
The component supports three background variants that adapt to light/dark mode:
|
||||
|
||||
- **`grey`** (default): Light mode: gray-200 (#E6EAF0), Dark mode: gray-300 (#CAD4DF)
|
||||
- **`neutral`**: Light mode: white (#FFF), Dark mode: black (#141414)
|
||||
- **`yellow`**: Light mode: yellow-100 (#F3F1EB), Dark mode: yellow-100 (#F3F1EB)
|
||||
|
||||
## Features
|
||||
|
||||
- **Responsive Layout**: Two-column on desktop (lg+), single-column on mobile/tablet
|
||||
- **Image Carousel**: Navigate through multiple slides with prev/next buttons
|
||||
- **Auto-hide Navigation**: Navigation buttons automatically hide when only one slide is present
|
||||
- **Feature List**: Display multiple features with dividers
|
||||
- **Button Group**: Supports 1-2 buttons with validation
|
||||
- **Background Variants**: Three color options with light/dark mode support
|
||||
- **Accessibility**: Proper ARIA labels for navigation buttons
|
||||
|
||||
## Layout Behavior
|
||||
|
||||
### Desktop (lg+)
|
||||
- Image column on the left (6 columns)
|
||||
- Content column on the right (6 columns)
|
||||
- Navigation buttons in header (desktop variant)
|
||||
|
||||
### Tablet/Mobile
|
||||
- Content section at the top
|
||||
- Image section at the bottom
|
||||
- Navigation buttons in CTA section (mobile variant)
|
||||
|
||||
## Examples
|
||||
|
||||
See the [showcase page](../../../about/carousel-featured-showcase.page.tsx) for live examples with different configurations.
|
||||
|
||||
## Notes
|
||||
|
||||
- Navigation buttons are automatically hidden when `slides.length === 1`
|
||||
- Buttons are validated using `validateButtonGroup` with a maximum of 2 buttons
|
||||
- Button colors are automatically adjusted based on the background variant
|
||||
- The component uses `ButtonGroup` pattern for consistent button styling
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export {
|
||||
CarouselFeatured,
|
||||
type CarouselFeaturedProps,
|
||||
type CarouselFeaturedBackground,
|
||||
type CarouselSlide,
|
||||
type CarouselFeatureItem
|
||||
} from './CarouselFeatured';
|
||||
export { default } from './CarouselFeatured';
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
// =============================================================================
|
||||
// FeatureSingleTopic Pattern
|
||||
// =============================================================================
|
||||
// A feature section pattern with single topic layout for title and media.
|
||||
// Supports variants (default, accentSurface).
|
||||
// Orientation (left, right) is handled via Bootstrap utility classes in TSX.
|
||||
// Based on Figma: 1280px desktop design with 706px image + content area
|
||||
//
|
||||
// Note: Buttons are rendered using the ButtonGroup component.
|
||||
// =============================================================================
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
|
||||
// Background colors from _colors.scss
|
||||
$bds-single-topic-bg: $white; // #FFFFFF (Neutral-white)
|
||||
$bds-single-topic-title-bg: $gray-200; // #E6EAF0 (Neutral-200) for accentSurface variant
|
||||
|
||||
// Text colors from _colors.scss
|
||||
$bds-single-topic-title-color: $black; // #141414 (Neutral-black)
|
||||
$bds-single-topic-description-color: $gray-500; // #72777E (Neutral-500)
|
||||
|
||||
// Spacing - Desktop (≥992px) - based on Figma 1280px design
|
||||
$bds-single-topic-desktop-py: 40px; // Vertical padding from Figma
|
||||
$bds-single-topic-desktop-content-pl: 8px; // Content left padding
|
||||
$bds-single-topic-desktop-description-gap: 40px; // Gap between description and ButtonGroup
|
||||
$bds-single-topic-desktop-title-padding: 16px; // Title section padding for accentSurface
|
||||
$bds-single-topic-desktop-height: 565px; // Fixed height from Figma design
|
||||
|
||||
// Spacing - Tablet (576px - 991px)
|
||||
$bds-single-topic-tablet-py: 32px;
|
||||
$bds-single-topic-tablet-content-gap: 32px; // Gap between image and content on tablet
|
||||
$bds-single-topic-tablet-content-min-height: 320px; // Min height for content on tablet
|
||||
$bds-single-topic-tablet-title-description-gap: 80px; // Gap between accent/title and description on tablet
|
||||
|
||||
// Spacing - Mobile (<576px)
|
||||
$bds-single-topic-mobile-py: 24px;
|
||||
$bds-single-topic-mobile-content-gap: 24px; // Gap between image and content on mobile
|
||||
$bds-single-topic-mobile-content-min-height: 280px; // Min height for content on mobile
|
||||
$bds-single-topic-mobile-title-description-gap: 40px; // Gap between accent/title and description on mobile
|
||||
|
||||
// =============================================================================
|
||||
// Base Styles
|
||||
// =============================================================================
|
||||
.bds-feature-single-topic {
|
||||
width: 100%;
|
||||
background-color: $bds-single-topic-bg;
|
||||
|
||||
// Container - uses PageGrid with vertical padding
|
||||
&__container {
|
||||
padding-top: $bds-single-topic-mobile-py;
|
||||
padding-bottom: $bds-single-topic-mobile-py;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: $bds-single-topic-tablet-py;
|
||||
padding-bottom: $bds-single-topic-tablet-py;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: $bds-single-topic-desktop-py;
|
||||
padding-bottom: $bds-single-topic-desktop-py;
|
||||
}
|
||||
}
|
||||
|
||||
// Row - align items stretch so columns match height
|
||||
// Use row-gap for spacing between image and content on mobile/tablet
|
||||
&__row {
|
||||
align-items: stretch;
|
||||
row-gap: $bds-single-topic-mobile-content-gap;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
row-gap: $bds-single-topic-tablet-content-gap;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
row-gap: 0;
|
||||
// Fixed height from Figma design
|
||||
height: $bds-single-topic-desktop-height;
|
||||
}
|
||||
}
|
||||
|
||||
// Media column
|
||||
&__media-col {
|
||||
@include media-breakpoint-up(lg) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Content column - flex container with left padding on desktop
|
||||
&__content-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-left: $bds-single-topic-desktop-content-pl;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Media container
|
||||
&__media {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Media image - responsive aspect ratios per Figma
|
||||
&__media-img {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
// Mobile: 343/193 aspect ratio
|
||||
aspect-ratio: 343 / 193;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
// Tablet: 16/9 aspect ratio
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
// Desktop: 701/561 aspect ratio (fills the 565px height)
|
||||
aspect-ratio: 701 / 561;
|
||||
height: $bds-single-topic-desktop-height;
|
||||
}
|
||||
}
|
||||
|
||||
// Content wrapper - uses space-between to push title to top, description/CTA to bottom
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
// Gap between accent/title section and description section
|
||||
gap: $bds-single-topic-mobile-title-description-gap; // 40px on mobile
|
||||
// Min height on mobile to prevent squished content
|
||||
min-height: $bds-single-topic-mobile-content-min-height;
|
||||
justify-content: space-between;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-single-topic-tablet-title-description-gap; // 80px on tablet
|
||||
min-height: $bds-single-topic-tablet-content-min-height;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
min-height: auto; // Desktop uses fixed height from row
|
||||
gap: 0; // space-between handles the gap on desktop
|
||||
}
|
||||
}
|
||||
|
||||
// Title section - at the top
|
||||
&__title-section {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// Title - Heading MD from styles/_font.scss
|
||||
// Font: Tobias (secondary/monospace), Size: 40px, Weight: 300, Line-height: 46px, Letter-spacing: -1px
|
||||
&__title {
|
||||
@include type(heading-md);
|
||||
color: $bds-single-topic-title-color;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Description section - at the bottom, contains description + ButtonGroup
|
||||
&__description-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-single-topic-mobile-content-gap;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-single-topic-desktop-description-gap;
|
||||
}
|
||||
}
|
||||
|
||||
// Description - Label L from styles/_font.scss
|
||||
// Font: Booton (primary/sans-serif), Size: 16px, Weight: 300 (light), Line-height: 23.2px
|
||||
&__description {
|
||||
@include type(label-l);
|
||||
color: $bds-single-topic-description-color;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Variant Modifiers
|
||||
// =============================================================================
|
||||
|
||||
// Default variant - no background on title section
|
||||
.bds-feature-single-topic--default {
|
||||
.bds-feature-single-topic__title-section {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// AccentSurface variant - gray background on title section
|
||||
.bds-feature-single-topic--accentSurface {
|
||||
.bds-feature-single-topic__title-section {
|
||||
background-color: $bds-single-topic-title-bg;
|
||||
padding: $bds-single-topic-desktop-title-padding;
|
||||
// Mobile min-height
|
||||
min-height: 160px;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
// Tablet min-height
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
// Desktop min-height
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dark Mode Theme Overrides
|
||||
// =============================================================================
|
||||
|
||||
// Dark mode design tokens from Figma
|
||||
$bds-single-topic-dark-bg: $black; // #141414 (Neutral/black)
|
||||
$bds-single-topic-dark-title-bg: $gray-300; // #CAD4DF (Neutral/300default) for accentSurface variant
|
||||
$bds-single-topic-dark-title-color: $black; // #141414 - title stays black on light background
|
||||
$bds-single-topic-dark-description-color: $white; // #FFFFFF - description is white in dark mode
|
||||
|
||||
html.dark {
|
||||
.bds-feature-single-topic {
|
||||
background-color: $bds-single-topic-dark-bg;
|
||||
|
||||
&__title {
|
||||
color: $white; // White title on dark background for default variant
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: $bds-single-topic-dark-description-color;
|
||||
}
|
||||
}
|
||||
|
||||
// Default variant in dark mode - title is white on dark background
|
||||
.bds-feature-single-topic--default {
|
||||
.bds-feature-single-topic__title-section {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.bds-feature-single-topic__title {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
// AccentSurface variant in dark mode - title section has light background
|
||||
.bds-feature-single-topic--accentSurface {
|
||||
.bds-feature-single-topic__title-section {
|
||||
background-color: $bds-single-topic-dark-title-bg;
|
||||
}
|
||||
|
||||
// Title stays black on the light gray background
|
||||
.bds-feature-single-topic__title {
|
||||
color: $bds-single-topic-dark-title-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid } from '../../components/PageGrid/page-grid';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
|
||||
export interface FeatureSingleTopicProps {
|
||||
/** Background variant for the title section
|
||||
* - 'default': No background on title section
|
||||
* - 'accentSurface': Gray background (#E6EAF0) on title section
|
||||
*/
|
||||
variant?: 'default' | 'accentSurface';
|
||||
/** Content arrangement - controls position of image relative to content
|
||||
* - 'left': Image on left, content on right
|
||||
* - 'right': Image on right, content on left
|
||||
*/
|
||||
orientation?: 'left' | 'right';
|
||||
/** Feature title text (heading-md typography) */
|
||||
title: string;
|
||||
/** Feature description text (label-l typography) */
|
||||
description?: string;
|
||||
/** Array of links (1-5 links supported)
|
||||
* - 1 link: renders as primary or secondary button (based on singleButtonVariant)
|
||||
* - 2 links: renders as primary + tertiary buttons side by side
|
||||
* - 3+ links: all tertiary buttons stacked
|
||||
*/
|
||||
buttons?: ButtonConfig[];
|
||||
/** Button variant for single button configuration
|
||||
* - 'primary': Primary button (default)
|
||||
* - 'secondary': Secondary button
|
||||
*/
|
||||
singleButtonVariant?: 'primary' | 'secondary';
|
||||
/** Feature media (image) configuration */
|
||||
media: {
|
||||
src: string;
|
||||
alt: string;
|
||||
};
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* FeatureSingleTopic Pattern
|
||||
*
|
||||
* A feature section pattern that pairs a title/description with a media element
|
||||
* in a two-column layout. Supports two variants: default (no title background)
|
||||
* and accentSurface (gray background on title section).
|
||||
*
|
||||
* Layout based on Figma 1280px design:
|
||||
* - Desktop: Side-by-side with image 7 columns, content 5 columns
|
||||
* - Mobile/Tablet: Stacked layout (full width)
|
||||
*/
|
||||
export const FeatureSingleTopic: React.FC<FeatureSingleTopicProps> = ({
|
||||
variant = 'default',
|
||||
orientation = 'left',
|
||||
title,
|
||||
description,
|
||||
buttons = [],
|
||||
singleButtonVariant = 'primary',
|
||||
media,
|
||||
className,
|
||||
}) => {
|
||||
// Validate buttons if provided (max 5 buttons supported)
|
||||
const buttonValidation = validateButtonGroup(buttons, 5);
|
||||
const hasButtons = buttonValidation.hasButtons;
|
||||
|
||||
// Button color is always green for this component
|
||||
const buttonColor = 'green';
|
||||
const forceColor = false;
|
||||
|
||||
// Build root class names
|
||||
const rootClasses = clsx(
|
||||
'bds-feature-single-topic',
|
||||
`bds-feature-single-topic--${variant}`,
|
||||
className
|
||||
);
|
||||
|
||||
// Build row class names - column-reverse on mobile/tablet for both orientations
|
||||
const rowClasses = clsx(
|
||||
'bds-feature-single-topic__row',
|
||||
'flex-column-reverse flex-lg-row' // Content above image on mobile, side-by-side on desktop
|
||||
);
|
||||
|
||||
|
||||
// Render content section (title at top, description/CTA at bottom)
|
||||
const renderContent = () => (
|
||||
<div className="bds-feature-single-topic__content">
|
||||
<div className="bds-feature-single-topic__title-section">
|
||||
<h2 className="bds-feature-single-topic__title">{title}</h2>
|
||||
</div>
|
||||
<div className="bds-feature-single-topic__description-section">
|
||||
{description && (
|
||||
<p className="bds-feature-single-topic__description">{description}</p>
|
||||
)}
|
||||
{hasButtons && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color={buttonColor}
|
||||
forceColor={forceColor}
|
||||
singleButtonVariant={singleButtonVariant}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Render media section
|
||||
const renderMedia = () => (
|
||||
<div className="bds-feature-single-topic__media">
|
||||
<img
|
||||
src={media.src}
|
||||
alt={media.alt}
|
||||
className="bds-feature-single-topic__media-img"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className={rootClasses}>
|
||||
<PageGrid className="bds-feature-single-topic__container" containerType="standard">
|
||||
<PageGrid.Row className={rowClasses}>
|
||||
<PageGrid.Col
|
||||
span={{ base: 4, md: 8, lg: 7 }}
|
||||
className={clsx(
|
||||
'bds-feature-single-topic__media-col',
|
||||
orientation === 'left' ? 'order-lg-1' : 'order-lg-2'
|
||||
)}
|
||||
>
|
||||
{renderMedia()}
|
||||
</PageGrid.Col>
|
||||
<PageGrid.Col
|
||||
span={{ base: 4, md: 8, lg: 5 }}
|
||||
className={clsx(
|
||||
'bds-feature-single-topic__content-col',
|
||||
orientation === 'left' ? 'order-lg-2' : 'order-lg-1'
|
||||
)}
|
||||
>
|
||||
{renderContent()}
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
</PageGrid>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureSingleTopic;
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
# FeatureSingleTopic Pattern
|
||||
|
||||
A feature section pattern that pairs a title/description with a media element in a two-column layout. Supports two variants (default, accentSurface) and two orientations (left, right).
|
||||
|
||||
## Features
|
||||
|
||||
- Responsive two-column layout (image + content) that stacks on smaller screens
|
||||
- Two background variants: default (no background) and accentSurface (gray title background)
|
||||
- Two orientations: left (image left) and right (image right)
|
||||
- Flexible button layout supporting 1-5 links with automatic variant assignment
|
||||
- Responsive image aspect ratios per Figma design
|
||||
- Full dark mode support
|
||||
- Uses PageGrid for consistent spacing
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```tsx
|
||||
import { FeatureSingleTopic } from 'shared/patterns/FeatureSingleTopic';
|
||||
|
||||
<FeatureSingleTopic
|
||||
variant="default"
|
||||
orientation="left"
|
||||
title="Developer Spotlight"
|
||||
description="Are you building a peer-to-peer payments solution?"
|
||||
media={{
|
||||
src: "/img/feature-image.png",
|
||||
alt: "Feature image"
|
||||
}}
|
||||
links={[
|
||||
{ label: "Get Started", href: "/start" },
|
||||
{ label: "Learn More", href: "/learn" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `variant` | `'default' \| 'accentSurface'` | `'default'` | Background variant for title section |
|
||||
| `orientation` | `'left' \| 'right'` | `'left'` | Image position relative to content |
|
||||
| `title` | `string` | *required* | Feature title (heading-md typography) |
|
||||
| `description` | `string` | - | Feature description (label-l typography) |
|
||||
| `buttons` | `ButtonConfig[]` | `[]` | Array of button configurations (1-5 supported) |
|
||||
| `singleButtonVariant` | `'primary' \| 'secondary'` | `'primary'` | Button variant for single button configuration |
|
||||
| `media` | `{ src: string; alt: string }` | *required* | Image configuration |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
|
||||
### ButtonConfig
|
||||
|
||||
```tsx
|
||||
interface ButtonConfig {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
forceColor?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Button configurations are handled by the `ButtonGroup` component. See [ButtonGroup documentation](../ButtonGroup/README.md) for more details.
|
||||
|
||||
## Button Behavior
|
||||
|
||||
The component automatically determines button variants based on count:
|
||||
|
||||
| Count | Layout |
|
||||
|-------|--------|
|
||||
| 1 button | Primary or Secondary button (configurable via `singleButtonVariant` prop) |
|
||||
| 2 buttons | Primary + Tertiary side by side |
|
||||
| 3-5 buttons | All Tertiary buttons stacked |
|
||||
|
||||
**Note:** The component supports a maximum of 5 buttons. Additional buttons beyond 5 will trigger a validation warning in development mode and will be ignored. On mobile, the first two buttons (Primary + Tertiary) remain side by side.
|
||||
|
||||
## Variants
|
||||
|
||||
### Default
|
||||
No background on the title section. Clean, minimal look.
|
||||
|
||||
```tsx
|
||||
<FeatureSingleTopic variant="default" ... />
|
||||
```
|
||||
|
||||
### AccentSurface
|
||||
Gray background (#E6EAF0 light / #CAD4DF dark) on the title section.
|
||||
|
||||
```tsx
|
||||
<FeatureSingleTopic variant="accentSurface" ... />
|
||||
```
|
||||
|
||||
## Orientation
|
||||
|
||||
### Left (default)
|
||||
Image on left, content on right on desktop.
|
||||
|
||||
### Right
|
||||
Image on right, content on left on desktop.
|
||||
|
||||
**Note:** On mobile/tablet, content always appears above image regardless of orientation.
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
### Desktop (≥992px)
|
||||
- Side-by-side layout: 7-column image, 5-column content
|
||||
- Fixed height: 565px
|
||||
- Image aspect ratio: 701/561
|
||||
|
||||
### Tablet (768px - 991px)
|
||||
- Stacked layout with 32px gap between sections
|
||||
- Image aspect ratio: 16/9
|
||||
- Content min-height: 320px
|
||||
|
||||
### Mobile (<768px)
|
||||
- Stacked layout with 24px gap between sections
|
||||
- Image aspect ratio: 343/193
|
||||
- Content min-height: 280px
|
||||
|
||||
## CSS Classes
|
||||
|
||||
```
|
||||
.bds-feature-single-topic // Section container
|
||||
.bds-feature-single-topic--default // Default variant modifier
|
||||
.bds-feature-single-topic--accentSurface // AccentSurface variant modifier
|
||||
.bds-feature-single-topic__container // PageGrid container
|
||||
.bds-feature-single-topic__row // PageGrid row (uses flex-column-reverse flex-lg-row)
|
||||
.bds-feature-single-topic__media-col // Media column (uses order-lg-1 or order-lg-2)
|
||||
.bds-feature-single-topic__content-col // Content column (uses order-lg-1 or order-lg-2)
|
||||
.bds-feature-single-topic__media // Media wrapper
|
||||
.bds-feature-single-topic__media-img // Image element
|
||||
.bds-feature-single-topic__content // Content wrapper
|
||||
.bds-feature-single-topic__title-section // Title section
|
||||
.bds-feature-single-topic__title // Title element
|
||||
.bds-feature-single-topic__description-section // Description + buttons wrapper
|
||||
.bds-feature-single-topic__description // Description element
|
||||
```
|
||||
|
||||
**Note:**
|
||||
- Orientation logic is handled via Bootstrap utility classes (`order-lg-1`, `order-lg-2`) applied dynamically in TSX
|
||||
- Buttons are rendered by the `ButtonGroup` component with its own class structure
|
||||
- Mobile/tablet layout uses `flex-column-reverse` to show content above image
|
||||
- Desktop layout uses `flex-lg-row` for side-by-side display
|
||||
|
||||
## Typography Tokens
|
||||
|
||||
- **Title**: Uses `heading-md` type token (Tobias Light font)
|
||||
- Desktop: 40px / 46px line-height / -1px letter-spacing
|
||||
|
||||
- **Description**: Uses `label-l` type token (Booton Light font)
|
||||
- Desktop: 16px / 23.2px line-height
|
||||
|
||||
## Dark Mode
|
||||
|
||||
Full dark mode support with `html.dark` selector:
|
||||
|
||||
- **Section background**: #141414 (black)
|
||||
- **Title (default variant)**: #FFFFFF (white)
|
||||
- **Title (accentSurface)**: #141414 (black) on #CAD4DF background
|
||||
- **Description**: #FFFFFF (white)
|
||||
|
||||
## Files
|
||||
|
||||
- `FeatureSingleTopic.tsx` - Main pattern component
|
||||
- `FeatureSingleTopic.scss` - Styles with responsive breakpoints
|
||||
- `index.ts` - Barrel exports
|
||||
- `README.md` - This documentation
|
||||
|
||||
## Design References
|
||||
|
||||
- **Figma Design**: [Section Feature - Single Topic](https://www.figma.com/design/sg6T5EptbN0V2olfCSHzcx/Section-Feature---Single-Topic?node-id=18030-2250&m=dev)
|
||||
- **Showcase Page**: `/about/feature-single-topic-showcase`
|
||||
- **Component Location**: `shared/patterns/FeatureSingleTopic/`
|
||||
|
||||
## Related Components
|
||||
|
||||
- **Button**: Used for CTA buttons
|
||||
- **PageGrid**: Used for responsive grid layout
|
||||
|
||||
## Version History
|
||||
|
||||
- **February 2026**: Initial implementation
|
||||
- Two variants (default, accentSurface)
|
||||
- Two orientations (left, right)
|
||||
- Responsive image aspect ratios
|
||||
- 1-5 link support with automatic button variant assignment
|
||||
- Full dark mode support
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export { FeatureSingleTopic, type FeatureSingleTopicProps } from './FeatureSingleTopic';
|
||||
export { default } from './FeatureSingleTopic';
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
# FeatureTwoColumn Pattern
|
||||
|
||||
A feature section pattern that pairs editorial content with a media element in a two-column layout. Designed for showcasing features, products, or use cases with flexible button configurations based on the number of links.
|
||||
|
||||
## Overview
|
||||
|
||||
FeatureTwoColumn supports four color theme variants (neutral, lilac, yellow, green) and adapts responsively across desktop, tablet, and mobile breakpoints. The button rendering automatically adjusts based on the number of links provided.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Highlighting a specific feature, product, or use case
|
||||
- Presenting content with supporting visual media
|
||||
- Creating visual variety with alternating left/right arrangements
|
||||
- When 1-5 action links are needed with appropriate button hierarchy
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `color` | `'neutral' \| 'lilac' \| 'yellow' \| 'green'` | `'neutral'` | Background color theme variant |
|
||||
| `arrange` | `'left' \| 'right'` | `'left'` | Controls whether content appears on the left or right side |
|
||||
| `title` | `string` | *required* | Feature title text (heading-md typography) |
|
||||
| `description` | `string` | *required* | Feature description text (body-l typography) |
|
||||
| `links` | `FeatureTwoColumnLink[]` | *required* | Array of 1-5 links (see button behavior below) |
|
||||
| `media` | `{ src: string; alt: string }` | *required* | Feature media (image) configuration |
|
||||
| `className` | `string` | - | Additional CSS classes |
|
||||
|
||||
### FeatureTwoColumnLink
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `label` | `string` | Link label text |
|
||||
| `href` | `string` | Link URL |
|
||||
|
||||
## Button Behavior
|
||||
|
||||
The component automatically renders buttons based on the number of links provided:
|
||||
|
||||
| Link Count | Button Configuration |
|
||||
|------------|---------------------|
|
||||
| 1 link | Secondary button |
|
||||
| 2 links | Primary button + Tertiary button |
|
||||
| 3-5 links | Primary + Tertiary (row), Secondary, then Tertiary links |
|
||||
|
||||
## Variants
|
||||
|
||||
### Color Themes
|
||||
|
||||
- **Neutral**: White background (dark mode: black)
|
||||
- **Lilac**: Light purple background ($lilac-100, dark mode: $lilac-500)
|
||||
- **Yellow**: Light yellow background ($yellow-100, dark mode: $yellow-500)
|
||||
- **Green**: Light green background ($green-100, dark mode: $green-500)
|
||||
|
||||
### Arrangement
|
||||
|
||||
- **Left** (default): Content on the left, media on the right
|
||||
- **Right**: Content on the right, media on the left
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```tsx
|
||||
import { FeatureTwoColumn } from '@/shared/patterns/FeatureTwoColumn';
|
||||
|
||||
function MyPage() {
|
||||
return (
|
||||
<FeatureTwoColumn
|
||||
color="lilac"
|
||||
arrange="left"
|
||||
title="Institutions"
|
||||
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
|
||||
links={[
|
||||
{ label: "Get Started", href: "/docs" },
|
||||
{ label: "Learn More", href: "/about" }
|
||||
]}
|
||||
media={{ src: "/img/institutions.png", alt: "Institutions illustration" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Single Link (Secondary Button)
|
||||
|
||||
```tsx
|
||||
<FeatureTwoColumn
|
||||
color="green"
|
||||
arrange="right"
|
||||
title="Developers"
|
||||
description="Build powerful applications on XRPL with comprehensive documentation and tools."
|
||||
links={[{ label: "View Documentation", href: "/docs" }]}
|
||||
media={{ src: "/img/dev.png", alt: "Developer tools" }}
|
||||
/>
|
||||
```
|
||||
|
||||
### Two Links (Primary + Tertiary)
|
||||
|
||||
```tsx
|
||||
<FeatureTwoColumn
|
||||
color="yellow"
|
||||
arrange="left"
|
||||
title="Enterprise Solutions"
|
||||
description="Scale your business with blockchain technology."
|
||||
links={[
|
||||
{ label: "Contact Sales", href: "/contact" },
|
||||
{ label: "Learn More", href: "/enterprise" }
|
||||
]}
|
||||
media={{ src: "/img/enterprise.png", alt: "Enterprise" }}
|
||||
/>
|
||||
```
|
||||
|
||||
### Multiple Links (3-5 Tertiary)
|
||||
|
||||
```tsx
|
||||
<FeatureTwoColumn
|
||||
color="neutral"
|
||||
arrange="left"
|
||||
title="Explore XRPL"
|
||||
description="Discover all the ways to interact with the XRP Ledger."
|
||||
links={[
|
||||
{ label: "Documentation", href: "/docs" },
|
||||
{ label: "Tutorials", href: "/tutorials" },
|
||||
{ label: "API Reference", href: "/api" },
|
||||
{ label: "Community", href: "/community" },
|
||||
{ label: "GitHub", href: "/github" }
|
||||
]}
|
||||
media={{ src: "/img/explore.png", alt: "Explore XRPL" }}
|
||||
/>
|
||||
```
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
### Desktop (≥992px)
|
||||
- Side-by-side layout with content and media columns (6/12 columns each)
|
||||
- Media uses 1:1 (square) aspect ratio
|
||||
- Vertical padding: 96px
|
||||
- Text gap (title to description): 16px
|
||||
- CTA gap (between buttons): 25px
|
||||
|
||||
### Tablet (576px - 991px)
|
||||
- Stacked layout (content above media, 8/8 columns = full width)
|
||||
- Media uses 16:9 aspect ratio
|
||||
- Vertical padding: 80px
|
||||
- Text gap: 8px
|
||||
- Content to CTA gap: 32px
|
||||
- CTA gap: 16px
|
||||
|
||||
### Mobile (<576px)
|
||||
- Stacked layout (content above media, 4/4 columns = full width)
|
||||
- Media uses 1:1 aspect ratio
|
||||
- Vertical padding: 64px
|
||||
- Text gap: 8px
|
||||
- Content to CTA gap: 24px
|
||||
- CTA gap: 16px
|
||||
|
||||
## Anatomy
|
||||
|
||||
```
|
||||
FeatureTwoColumn
|
||||
├── PageGrid Container (responsive padding)
|
||||
│ └── PageGrid.Row (flex layout)
|
||||
│ ├── PageGrid.Col (content column, 6/12 on desktop)
|
||||
│ │ └── Content
|
||||
│ │ ├── TextGroup
|
||||
│ │ │ ├── Title (h2, heading-md)
|
||||
│ │ │ └── Description (p, body-l)
|
||||
│ │ └── CTA (button configuration varies by link count)
|
||||
│ │ ├── 1 link: Secondary Button
|
||||
│ │ ├── 2 links: Primary + Tertiary Buttons
|
||||
│ │ └── 3-5 links: Primary + Tertiary (row), Secondary, Tertiary list
|
||||
│ └── PageGrid.Col (media column, 6/12 on desktop)
|
||||
│ └── Media
|
||||
│ └── Image (object-fit: cover)
|
||||
```
|
||||
|
||||
## CSS Classes
|
||||
|
||||
| Class | Description |
|
||||
|-------|-------------|
|
||||
| `.bds-feature-two-column` | Root element |
|
||||
| `.bds-feature-two-column--neutral` | Neutral color theme |
|
||||
| `.bds-feature-two-column--lilac` | Lilac color theme |
|
||||
| `.bds-feature-two-column--yellow` | Yellow color theme |
|
||||
| `.bds-feature-two-column--green` | Green color theme |
|
||||
| `.bds-feature-two-column--left` | Content left arrangement |
|
||||
| `.bds-feature-two-column--right` | Content right arrangement |
|
||||
| `.bds-feature-two-column__container` | PageGrid container |
|
||||
| `.bds-feature-two-column__row` | PageGrid.Row wrapper |
|
||||
| `.bds-feature-two-column__content-col` | Content column wrapper |
|
||||
| `.bds-feature-two-column__content` | Content container |
|
||||
| `.bds-feature-two-column__text-group` | Title + description container |
|
||||
| `.bds-feature-two-column__title` | Title heading |
|
||||
| `.bds-feature-two-column__description` | Description text |
|
||||
| `.bds-feature-two-column__cta` | CTA buttons container |
|
||||
| `.bds-feature-two-column__cta--single` | Single button variant |
|
||||
| `.bds-feature-two-column__cta--double` | Two button variant |
|
||||
| `.bds-feature-two-column__cta--multiple` | Multiple buttons variant |
|
||||
| `.bds-feature-two-column__media-col` | Media column wrapper |
|
||||
| `.bds-feature-two-column__media` | Media container |
|
||||
| `.bds-feature-two-column__media-img` | Media image |
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Uses semantic `<section>` element for the pattern container
|
||||
- Title uses `<h2>` heading for proper document structure
|
||||
- Media requires `alt` text for screen readers
|
||||
- Buttons inherit accessible labels from the Button component
|
||||
- Color contrast ratios meet WCAG 2.1 AA standards
|
||||
|
||||
## Design References
|
||||
|
||||
- **Figma Design**: [Pattern - Feature - Two Column](https://www.figma.com/design/3tmqxMrEvOVvpYhgOCxv2D/Pattern-Feature---Two-Column?node-id=20017-3501&m=dev)
|
||||
- **Component Location**: `shared/patterns/FeatureTwoColumn/`
|
||||
- **Color Tokens**: `styles/_colors.scss`
|
||||
- **Typography**: `styles/_font.scss`
|
||||
|
||||
@@ -1,358 +0,0 @@
|
||||
// FeatureTwoColumn Pattern Styles
|
||||
// =============================================================================
|
||||
// A feature section pattern with two-column layout for content and media.
|
||||
// Supports color themes (neutral, lilac, yellow, green) and arrangement variants.
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
|
||||
// Color variants map - centralizes all variant configurations
|
||||
// Structure: variant-name: (light-bg, dark-bg)
|
||||
$bds-feature-variants: (
|
||||
'neutral': (
|
||||
light-bg: $gray-100, // #F0F3F7 (Neutral-100)
|
||||
dark-bg: $gray-200 // #E6EAF0 (Neutral-200)
|
||||
),
|
||||
'lilac': (
|
||||
light-bg: $lilac-200, // #D9CAFF (Primary-Lilac-200)
|
||||
dark-bg: $lilac-200 // #D9CAFF (Primary-Lilac-200)
|
||||
),
|
||||
'yellow': (
|
||||
light-bg: $yellow-100, // #F3F1EB (Secondary-Yellow-100)
|
||||
dark-bg: $yellow-100 // #F3F1EB (Secondary-Yellow-100)
|
||||
),
|
||||
'green': (
|
||||
light-bg: $green-300, // #21E46B (Primary-Green-300default)
|
||||
dark-bg: $green-300 // #21E46B (Primary-Green-300default)
|
||||
)
|
||||
);
|
||||
|
||||
// Text colors - same for light and dark modes
|
||||
// From styles/_colors.scss: $black = #141414 (Neutral-black)
|
||||
$bds-feature-title-color: $black; // #141414 (Neutral-black)
|
||||
$bds-feature-title-color-dark: $black; // #141414 (same in dark mode)
|
||||
$bds-feature-description-color: $black; // #141414 (Neutral-black)
|
||||
$bds-feature-description-color-dark: $black; // #141414 (same in dark mode)
|
||||
|
||||
// Spacing - Desktop (≥992px) - based on Figma 1280px design
|
||||
$bds-feature-desktop-py: 96px;
|
||||
$bds-feature-desktop-text-gap: 16px;
|
||||
$bds-feature-desktop-cta-gap-col: 0; // Gap between button rows
|
||||
$bds-feature-desktop-content-gap: 0; // Gap between text-group and cta (space-between handles this)
|
||||
|
||||
// Spacing - Tablet (576px - 991px) - based on Figma 768px design
|
||||
$bds-feature-tablet-py: 80px;
|
||||
$bds-feature-tablet-pl: 115px; // Left padding from Figma
|
||||
$bds-feature-tablet-pr: 107px; // Right padding from Figma
|
||||
$bds-feature-tablet-text-gap: 8px;
|
||||
$bds-feature-tablet-cta-gap-row: 16px; // Gap between buttons in row from Figma
|
||||
$bds-feature-tablet-cta-gap-col: 0;
|
||||
$bds-feature-tablet-content-gap: 32px;
|
||||
|
||||
// Spacing - Mobile (<576px) - based on Figma 375px design
|
||||
$bds-feature-mobile-py: 64px;
|
||||
$bds-feature-mobile-px: 16px;
|
||||
$bds-feature-mobile-text-gap: 8px;
|
||||
$bds-feature-mobile-cta-gap: 16px; // Gap between stacked buttons from Figma
|
||||
$bds-feature-mobile-content-gap: 24px;
|
||||
|
||||
// Grid gutter - consistent with PageGrid
|
||||
$bds-grid-gutter: 8px;
|
||||
|
||||
// =============================================================================
|
||||
// Base Styles
|
||||
// =============================================================================
|
||||
.bds-feature-two-column {
|
||||
width: 100%;
|
||||
|
||||
// Extra large screens (>1280px): constrain component width and center
|
||||
// Background color stays within 1280px, parent section background shows beyond
|
||||
@include media-breakpoint-up(lg) {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
// Desktop layout - hidden on mobile/tablet, shown on desktop
|
||||
&__desktop-layout {
|
||||
display: none;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: stretch; // Both columns match height
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile layout - shown on mobile/tablet, hidden on desktop
|
||||
&__mobile-layout {
|
||||
display: block;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Container - uses PageGrid with wide variant (for mobile layout)
|
||||
// Override all PageGrid padding - content columns have their own padding
|
||||
&__container {
|
||||
padding: 0 !important; // Override PageGrid default padding at all breakpoints
|
||||
}
|
||||
|
||||
// Row - uses PageGrid.Row (for mobile layout)
|
||||
&__row {
|
||||
gap: 0 !important; // No gap between content and media sections, override PageGrid
|
||||
}
|
||||
|
||||
// Content column wrapper
|
||||
&__content-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// Mobile: vertical padding, no horizontal padding (grid handles positioning)
|
||||
padding: $bds-feature-mobile-py 0;
|
||||
|
||||
// Tablet: vertical padding, no horizontal padding (grid handles positioning)
|
||||
@include media-breakpoint-up(md) {
|
||||
padding: $bds-feature-tablet-py 0;
|
||||
}
|
||||
|
||||
// Desktop: 50% width, vertical padding (grid handles horizontal positioning)
|
||||
@include media-breakpoint-up(lg) {
|
||||
width: 50%;
|
||||
padding: $bds-feature-desktop-py 0;
|
||||
// Match height of media column (which has aspect-ratio 1/1)
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Content grid - positions content within column using grid system
|
||||
// Desktop: 6-column grid within the content half, content spans cols 2-5 (offset-1, span-4)
|
||||
// Tablet: 8-column grid, content spans cols 2-7 (offset-1, span-6)
|
||||
// Mobile: 4-column grid, content spans cols 1-4 (no offset, span-4)
|
||||
&__content-grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// Mobile: 4 columns, content full width
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: $bds-grid-gutter;
|
||||
padding: 0 16px; // Mobile edge padding
|
||||
|
||||
// Tablet: 8 columns
|
||||
@include media-breakpoint-up(md) {
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
padding: 0 24px; // Tablet edge padding
|
||||
}
|
||||
|
||||
// Desktop: 6 columns (within the 50% content half)
|
||||
@include media-breakpoint-up(lg) {
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
padding: 0 32px; // Desktop edge padding
|
||||
}
|
||||
}
|
||||
|
||||
// Content wrapper - positioned within the grid
|
||||
&__content-wrapper {
|
||||
// Mobile: span all 4 columns (cols 1-4)
|
||||
grid-column: 1 / -1;
|
||||
|
||||
// Tablet: start at col 2, span 6 columns (cols 2-7)
|
||||
@include media-breakpoint-up(md) {
|
||||
grid-column: 2 / span 6;
|
||||
}
|
||||
|
||||
// Desktop: start at col 2, span 4 columns (cols 2-5)
|
||||
@include media-breakpoint-up(lg) {
|
||||
grid-column: 2 / span 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Media column wrapper - desktop layout (background image)
|
||||
&__media-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// Desktop: 50% width with background image, square aspect ratio
|
||||
@include media-breakpoint-up(lg) {
|
||||
width: 50%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile media column
|
||||
&__media-col--mobile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// Content
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
gap: $bds-feature-mobile-content-gap;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-feature-tablet-content-gap;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-feature-desktop-content-gap;
|
||||
}
|
||||
}
|
||||
|
||||
// Content with multiple links
|
||||
// Mobile: 24px gap between text-group and button-group
|
||||
// Tablet: 32px gap between text-group and button-group
|
||||
// Desktop: space-between with no gap (auto distribution between text-group and button-group)
|
||||
&__content--multiple {
|
||||
// Mobile: no gap since we only have 2 items (text-group and button-group)
|
||||
// The 24px gap is handled via button-group margin or flex gap
|
||||
gap: 24px;
|
||||
justify-content: flex-start;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
// Secondary button should not stretch - keep intrinsic width
|
||||
>.bds-btn--secondary {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
// Text group - title + description
|
||||
&__text-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-feature-mobile-text-gap;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-feature-tablet-text-gap;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-feature-desktop-text-gap;
|
||||
}
|
||||
}
|
||||
|
||||
// Title - Heading MD from styles/_font.scss
|
||||
// Font: Tobias (secondary), Size: 40px, Weight: 300, Line-height: 46px, Letter-spacing: -1px
|
||||
&__title {
|
||||
@include type(heading-md);
|
||||
color: $bds-feature-title-color; // #141414 (Neutral-black) - same in light/dark
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Description - Body L from styles/_font.scss
|
||||
// Font: Booton (primary), Size: 18px, Weight: 300, Line-height: 26.1px, Letter-spacing: -0.5px
|
||||
&__description {
|
||||
@include type(body-l);
|
||||
color: $bds-feature-description-color; // #141414 (Neutral-black) - same in light/dark
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Media container
|
||||
&__media {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
// Mobile - 1:1 aspect ratio
|
||||
aspect-ratio: 1 / 1;
|
||||
|
||||
// Tablet - 16:9 aspect ratio
|
||||
@include media-breakpoint-up(md) {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
// Desktop - 1:1 aspect ratio (square)
|
||||
@include media-breakpoint-up(lg) {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Media image
|
||||
&__media-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Color Theme Modifiers
|
||||
// =============================================================================
|
||||
// Generated from $bds-feature-variants map
|
||||
|
||||
@each $variant-name, $variant-colors in $bds-feature-variants {
|
||||
.bds-feature-two-column--#{$variant-name} {
|
||||
background-color: map-get($variant-colors, light-bg);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dark Mode Theme Overrides
|
||||
// =============================================================================
|
||||
|
||||
html.dark {
|
||||
.bds-feature-two-column {
|
||||
&__title {
|
||||
color: $bds-feature-title-color-dark;
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: $bds-feature-description-color-dark;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate dark mode styles for each variant
|
||||
@each $variant-name, $variant-colors in $bds-feature-variants {
|
||||
.bds-feature-two-column--#{$variant-name} {
|
||||
background-color: map-get($variant-colors, dark-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Layout Modifiers (Arrange)
|
||||
// =============================================================================
|
||||
|
||||
// Right arrangement - content on right, media on left (media first on mobile/tablet)
|
||||
// Use flex-direction to swap columns on all screen sizes
|
||||
.bds-feature-two-column--right {
|
||||
|
||||
// Mobile/Tablet layout - reverse the column order to show media first
|
||||
.bds-feature-two-column__row {
|
||||
flex-direction: column-reverse !important;
|
||||
}
|
||||
|
||||
// Desktop layout - reverse the flex direction for side-by-side
|
||||
.bds-feature-two-column__desktop-layout {
|
||||
@include media-breakpoint-up(lg) {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
// Desktop content wrapper - mirror the grid positioning for right layout
|
||||
// Content should start from col 2 and span 4 cols (same as left, grid handles positioning)
|
||||
.bds-feature-two-column__content-wrapper {
|
||||
@include media-breakpoint-up(lg) {
|
||||
// Same positioning as left layout - grid alignment handles visual positioning
|
||||
grid-column: 2 / span 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid } from '../../components/PageGrid/page-grid';
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
|
||||
export interface FeatureTwoColumnLink {
|
||||
/** Link label text */
|
||||
label: string;
|
||||
/** Link URL */
|
||||
href: string;
|
||||
}
|
||||
|
||||
export interface FeatureTwoColumnProps {
|
||||
/** Color theme variant */
|
||||
color?: 'neutral' | 'lilac' | 'yellow' | 'green';
|
||||
/** Content arrangement - left places content on left side, right places content on right side */
|
||||
arrange?: 'left' | 'right';
|
||||
/** Feature title text (heading-md typography) */
|
||||
title: string;
|
||||
/** Feature description text (body-l typography) */
|
||||
description: string;
|
||||
/** Array of links (1-5 links supported)
|
||||
* - 1 link: renders as secondary button
|
||||
* - 2 links: renders as primary + tertiary buttons
|
||||
* - 3-5 links: renders all as tertiary buttons
|
||||
*/
|
||||
links: FeatureTwoColumnLink[];
|
||||
/** Feature media (image) configuration */
|
||||
media: {
|
||||
src: string;
|
||||
alt: string;
|
||||
};
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* FeatureTwoColumn Pattern
|
||||
*
|
||||
* A feature section pattern that pairs editorial content with a media element
|
||||
* in a two-column layout. Designed for showcasing features, products, or use cases.
|
||||
*
|
||||
* Uses the PageGrid component system for responsive layout:
|
||||
* - Mobile: Stacked layout (content above media)
|
||||
* - Tablet: Stacked layout (content above media)
|
||||
* - Desktop: Side-by-side (6/12 columns each)
|
||||
*
|
||||
* Button behavior based on link count:
|
||||
* - 1 link: Secondary button
|
||||
* - 2 links: Primary button (first) + Tertiary button (second)
|
||||
* - 3-5 links: All tertiary buttons (first is filled, rest are text-only)
|
||||
*/
|
||||
export const FeatureTwoColumn: React.FC<FeatureTwoColumnProps> = ({
|
||||
color = 'neutral',
|
||||
arrange = 'left',
|
||||
title,
|
||||
description,
|
||||
links = [],
|
||||
media,
|
||||
className,
|
||||
}) => {
|
||||
// Determine button color based on background
|
||||
// Rule: Black buttons must be used for all backgrounds (including neutral)
|
||||
const buttonColor = 'black';
|
||||
const forceColor = true;
|
||||
|
||||
// Convert links to ButtonConfig format
|
||||
const buttonConfigs: ButtonConfig[] = links.map(link => ({
|
||||
label: link.label,
|
||||
href: link.href,
|
||||
forceColor: forceColor,
|
||||
}));
|
||||
|
||||
// Validate buttons (FeatureTwoColumn supports 1-5 links per design spec)
|
||||
const buttonValidation = validateButtonGroup(buttonConfigs, 5);
|
||||
const hasButtons = buttonValidation.hasButtons;
|
||||
|
||||
// Build root class names
|
||||
const rootClasses = clsx(
|
||||
'bds-feature-two-column',
|
||||
`bds-feature-two-column--${color}`,
|
||||
`bds-feature-two-column--${arrange}`,
|
||||
className
|
||||
);
|
||||
|
||||
// Render content section with ButtonGroup
|
||||
const renderContent = () => {
|
||||
// Determine content class based on validated button count
|
||||
const contentClass = clsx(
|
||||
'bds-feature-two-column__content',
|
||||
{
|
||||
'bds-feature-two-column__content--multiple': hasButtons && buttonValidation.buttons.length >= 3,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={contentClass}>
|
||||
<div className="bds-feature-two-column__text-group">
|
||||
<h2 className="bds-feature-two-column__title">{title}</h2>
|
||||
<p className="bds-feature-two-column__description">{description}</p>
|
||||
</div>
|
||||
{hasButtons && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color={buttonColor}
|
||||
forceColor={forceColor}
|
||||
singleButtonVariant="secondary"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Render media section (for mobile/tablet stacked layout)
|
||||
const renderMedia = () => (
|
||||
<div className="bds-feature-two-column__media">
|
||||
<img
|
||||
src={media.src}
|
||||
alt={media.alt}
|
||||
className="bds-feature-two-column__media-img"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className={rootClasses}>
|
||||
{/* Desktop layout - simple two-column flex with background image */}
|
||||
<div className="bds-feature-two-column__desktop-layout">
|
||||
<div className="bds-feature-two-column__content-col">
|
||||
<div className="bds-feature-two-column__content-grid">
|
||||
<div className="bds-feature-two-column__content-wrapper">
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="bds-feature-two-column__media-col"
|
||||
style={{ backgroundImage: `url(${media.src})` }}
|
||||
role="img"
|
||||
aria-label={media.alt}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mobile/Tablet layout - stacked with PageGrid */}
|
||||
<div className="bds-feature-two-column__mobile-layout">
|
||||
<PageGrid className="bds-feature-two-column__container" containerType="wide">
|
||||
<PageGrid.Row className="bds-feature-two-column__row">
|
||||
<PageGrid.Col
|
||||
span={{ base: 4, md: 8 }}
|
||||
className="bds-feature-two-column__content-col"
|
||||
>
|
||||
<div className="bds-feature-two-column__content-grid">
|
||||
<div className="bds-feature-two-column__content-wrapper">
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
</PageGrid.Col>
|
||||
<PageGrid.Col
|
||||
span={{ base: 4, md: 8 }}
|
||||
className="bds-feature-two-column__media-col--mobile"
|
||||
>
|
||||
{renderMedia()}
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
</PageGrid>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureTwoColumn;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export { FeatureTwoColumn, type FeatureTwoColumnProps, type FeatureTwoColumnLink } from './FeatureTwoColumn';
|
||||
export { default } from './FeatureTwoColumn';
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import React, { forwardRef, useCallback } from "react";
|
||||
import clsx from "clsx";
|
||||
import { PageGrid } from "shared/components/PageGrid/page-grid";
|
||||
import { ButtonGroup, ButtonConfig, validateButtonGroup } from "shared/patterns/ButtonGroup/ButtonGroup";
|
||||
import { isEmpty, isEnvironment } from "shared/utils";
|
||||
import {
|
||||
DesignConstrainedCallToActionsProps,
|
||||
DesignConstrainedVideoProps,
|
||||
} from "shared/utils/types";
|
||||
|
||||
export interface FeaturedVideoHeroProps
|
||||
extends
|
||||
React.ComponentPropsWithoutRef<"header">,
|
||||
DesignConstrainedCallToActionsProps {
|
||||
headline: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
videoElement: DesignConstrainedVideoProps;
|
||||
}
|
||||
const FeaturedVideoHero = forwardRef<HTMLElement, FeaturedVideoHeroProps>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
headline,
|
||||
subtitle,
|
||||
videoElement,
|
||||
callsToAction,
|
||||
className,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const validateProps = useCallback<() => boolean>(() => {
|
||||
const requiredProps = { headline, videoElement } as const;
|
||||
let isValid = true;
|
||||
|
||||
for (const [key, value] of Object.entries(requiredProps)) {
|
||||
if (isEmpty(value)) {
|
||||
if (isEnvironment(["development", "test"])) {
|
||||
console.warn(`${key} is required for FeaturedVideoHero`);
|
||||
}
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
return isValid;
|
||||
}, [headline, videoElement]);
|
||||
|
||||
if (!validateProps()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert callsToAction to ButtonConfig format for ButtonGroup
|
||||
const buttonConfigs: ButtonConfig[] = (callsToAction ?? [])
|
||||
.filter((cta) => !isEmpty(cta))
|
||||
.map((cta) => ({
|
||||
label: typeof cta?.children === 'string' ? cta.children : '',
|
||||
href: cta?.href,
|
||||
onClick: cta?.onClick,
|
||||
forceColor: true,
|
||||
}));
|
||||
|
||||
// Validate buttons (max 2 CTAs supported)
|
||||
const buttonValidation = validateButtonGroup(
|
||||
buttonConfigs,
|
||||
2,
|
||||
isEnvironment(["development", "test"]) // Only log warnings in dev/test
|
||||
);
|
||||
const hasCallsToAction = buttonValidation.hasButtons;
|
||||
|
||||
return (
|
||||
<header
|
||||
ref={ref}
|
||||
className={clsx("bds-featured-video-hero", className)}
|
||||
{...rest}
|
||||
>
|
||||
<PageGrid>
|
||||
<PageGrid.Row>
|
||||
<PageGrid.Col span={{ base: 4, md: 8, lg: 6 }}>
|
||||
<div className="bds-featured-video-hero__content">
|
||||
<h1 className="mb-0 h-md">
|
||||
{headline}
|
||||
</h1>
|
||||
|
||||
<div className="bds-featured-video-hero__bottom-group">
|
||||
{subtitle && (
|
||||
<PageGrid.Row className="bds-featured-video-hero__subtitle body-l">
|
||||
<PageGrid.Col
|
||||
span={{ base: "fill", md: 6, lg: 10 }}
|
||||
className="bds-featured-video-hero__subtitle-col"
|
||||
>
|
||||
{subtitle}
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
)}
|
||||
{hasCallsToAction && (
|
||||
<ButtonGroup
|
||||
buttons={buttonValidation.buttons}
|
||||
color="green"
|
||||
forceColor
|
||||
gap="small"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageGrid.Col>
|
||||
<PageGrid.Col
|
||||
span={{ base: 4, md: 8, lg: 6 }}
|
||||
>
|
||||
<div className="bds-featured-video-hero__video-container">
|
||||
<video
|
||||
{...videoElement}
|
||||
className="bds-featured-video-hero__video"
|
||||
/>
|
||||
</div>
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
</PageGrid>
|
||||
</header>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FeaturedVideoHero.displayName = "FeaturedVideoHero";
|
||||
|
||||
export default FeaturedVideoHero;
|
||||
@@ -1,162 +0,0 @@
|
||||
# FeaturedVideoHero Pattern
|
||||
|
||||
A page-level hero pattern featuring a headline, optional subtitle, call-to-action buttons, and a featured video. The video uses native HTML `<video>` props and is displayed in a responsive two-column layout with content on the left and video on the right.
|
||||
|
||||
## Overview
|
||||
|
||||
The FeaturedVideoHero component provides a structured hero section with:
|
||||
|
||||
- Responsive two-column layout (content left, video right) that stacks on smaller screens
|
||||
- Required headline and video; optional subtitle and call-to-action buttons
|
||||
- Design-constrained CTAs: primary and optional secondary, with variant and color set by the component
|
||||
- Development-time validation: returns `null` when required props are missing and logs warnings in development/test
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```tsx
|
||||
import { FeaturedVideoHero } from "shared/patterns/FeaturedVideoHero";
|
||||
|
||||
function MyPage() {
|
||||
return (
|
||||
<FeaturedVideoHero
|
||||
headline="Build on XRPL"
|
||||
subtitle={
|
||||
<p>
|
||||
Issue, manage, and trade real-world assets without needing to build
|
||||
smart contracts.
|
||||
</p>
|
||||
}
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
| --------------- | --------------------------------- | -------- | ---------------------------------------------------------------------------- |
|
||||
| `headline` | `React.ReactNode` | Yes | Hero headline text (h-md typography) |
|
||||
| `subtitle` | `React.ReactNode` | No | Hero subtitle content |
|
||||
| `callsToAction` | `DesignConstrainedCallsToActions` | No | Array with primary CTA and optional secondary CTA. Omit to hide CTA section. |
|
||||
| `videoElement` | `DesignConstrainedVideoProps` | Yes | Native `<video>` element props (e.g. `src`, `autoPlay`, `loop`, `muted`) |
|
||||
| `className` | `string` | No | Additional CSS classes for the header element |
|
||||
| `...rest` | `HTMLHeaderElement` attributes | No | Any other HTML header attributes |
|
||||
|
||||
### Calls to Action
|
||||
|
||||
The `callsToAction` prop is optional. When provided, at least one non-empty CTA is required to show the CTA section. The component uses design-constrained Button props; `variant` and `color` are set automatically:
|
||||
|
||||
- **Primary CTA**: `variant="primary"`, `color="green"`, `forceColor={true}`
|
||||
- **Secondary CTA**: `variant="tertiary"`, `color="green"`, `forceColor={true}`
|
||||
|
||||
All other Button props are supported (e.g., `children`, `href`, `onClick`). Do not pass `variant` or `color` in the CTA objects.
|
||||
|
||||
### Video Element
|
||||
|
||||
`videoElement` accepts native HTML video element props. Required and commonly used props:
|
||||
|
||||
- `src` (required) – Video URL
|
||||
- `autoPlay`, `loop`, `muted`, `playsInline` – Typical for background/hero autoplay
|
||||
- `controls`, `preload`, `poster` – Optional; use for user-controlled playback
|
||||
|
||||
The video is rendered with `object-fit: cover` and a 16:9 aspect ratio container.
|
||||
|
||||
## Examples
|
||||
|
||||
### With primary and secondary CTAs
|
||||
|
||||
```tsx
|
||||
<FeaturedVideoHero
|
||||
headline="Real-world asset tokenization"
|
||||
subtitle="Learn how to issue crypto tokens and build tokenization solutions."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" },
|
||||
{ children: "Learn More", href: "/about" },
|
||||
]}
|
||||
videoElement={{
|
||||
src: "/video/tokenization.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Without subtitle
|
||||
|
||||
```tsx
|
||||
<FeaturedVideoHero
|
||||
headline="Headline Only"
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### With video controls
|
||||
|
||||
```tsx
|
||||
<FeaturedVideoHero
|
||||
headline="Watch and Learn"
|
||||
subtitle="Explore our video tutorials and guides."
|
||||
callsToAction={[{ children: "Watch Tutorials", href: "/tutorials" }]}
|
||||
videoElement={{
|
||||
src: "/video/intro.mp4",
|
||||
autoPlay: false,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true,
|
||||
controls: true,
|
||||
preload: "metadata",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
- **Required props**: `headline`, `videoElement`. If either is missing or empty, the component returns `null` and (in development/test) logs a console warning.
|
||||
- **Optional props**: `subtitle`, `callsToAction`. Omit `callsToAction` or pass an array with no renderable CTAs to hide the CTA section.
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
- **Mobile / small screens**: Content and video stack vertically; video appears below the content block with top margin.
|
||||
- **Large (lg+)**: Two-column layout: content (5 cols) on the left, video (6 cols, offset 1) on the right. Video container uses 16:9 aspect ratio and `object-fit: cover`.
|
||||
|
||||
## CSS Classes
|
||||
|
||||
- `bds-featured-video-hero` – Root header element
|
||||
- `bds-featured-video-hero__content` – Content column (headline, subtitle, CTAs)
|
||||
- `bds-featured-video-hero__title` – Headline (`h1`)
|
||||
- `bds-featured-video-hero__subtitle` – Subtitle row
|
||||
- `bds-featured-video-hero__subtitle-col` – Subtitle column
|
||||
- `bds-featured-video-hero__cta-buttons` – CTA buttons wrapper
|
||||
- `bds-featured-video-hero__video-container` – Video wrapper (16:9)
|
||||
- `bds-featured-video-hero__video` – Video element
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Video format**: Use MP4 with H.264 for broad compatibility; keep file sizes reasonable for fast loading.
|
||||
2. **Autoplay**: Use `muted` and `playsInline` with `autoPlay` for reliable autoplay on mobile.
|
||||
3. **CTAs**: Keep CTA text concise and action-oriented; primary CTA should be the main action.
|
||||
4. **Headlines**: Keep headlines concise; use the subtitle for additional context.
|
||||
5. **Accessibility**: Provide an `aria-label` (or other accessible name) on the video when it conveys meaningful content.
|
||||
|
||||
## Showcase
|
||||
|
||||
An interactive showcase with more examples and prop documentation is available at:
|
||||
|
||||
- **Showcase page**: `/about/featured-video-hero-showcase.page.tsx`
|
||||
@@ -1,88 +0,0 @@
|
||||
.bds-featured-video-hero{
|
||||
padding: 24px 0;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
@include bds-theme-mode(light) {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
@include bds-theme-mode(dark) {
|
||||
background-color: $black;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
height: 100%;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__video-container {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
margin-top: 16px;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&__video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__bottom-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
import React, { forwardRef, memo, useEffect } from "react";
|
||||
import clsx from "clsx";
|
||||
import { PageGrid } from "shared/components/PageGrid/page-grid";
|
||||
import { Button } from "shared/components/Button/Button";
|
||||
import {
|
||||
isEmpty,
|
||||
DesignConstrainedButtonProps,
|
||||
isEnvironment,
|
||||
} from "shared/utils";
|
||||
import {
|
||||
DesignConstrainedImageProps,
|
||||
DesignConstrainedVideoProps,
|
||||
} from "shared/utils/types";
|
||||
|
||||
/**
|
||||
* Image media type - extends native img element props
|
||||
*/
|
||||
type ImageMediaProps = {
|
||||
type: "image";
|
||||
} & DesignConstrainedImageProps;
|
||||
|
||||
/**
|
||||
* Video media type - extends native video element props
|
||||
*/
|
||||
type VideoMediaProps = {
|
||||
type: "video";
|
||||
} & DesignConstrainedVideoProps;
|
||||
|
||||
/**
|
||||
* Custom element media type - allows passing any React element
|
||||
* The element will be wrapped in a container with the required aspect ratio
|
||||
*/
|
||||
type CustomMediaProps = {
|
||||
type: "custom";
|
||||
element: React.ReactElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Discriminated union of all supported media types.
|
||||
* Each type allows extending native React element props while ensuring
|
||||
* the media container maintains the 9:16 aspect ratio and object-fit: cover.
|
||||
*/
|
||||
export type HeaderHeroMedia =
|
||||
| ImageMediaProps
|
||||
| VideoMediaProps
|
||||
| CustomMediaProps;
|
||||
|
||||
export interface HeaderHeroPrimaryMediaProps extends React.ComponentPropsWithoutRef<"header"> {
|
||||
/** Hero title text (display-md typography) */
|
||||
headline: React.ReactNode;
|
||||
/** Hero subtitle text (subhead-sm-l typography) */
|
||||
subtitle: React.ReactNode;
|
||||
callsToAction: [DesignConstrainedButtonProps, DesignConstrainedButtonProps?];
|
||||
/** Media element - supports image, video, or custom React element */
|
||||
media: HeaderHeroMedia;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the appropriate media element based on the media type.
|
||||
* All media is wrapped in a container with 9:16 aspect ratio and object-fit: cover.
|
||||
*/
|
||||
const MediaRenderer: React.FC<{ media: HeaderHeroMedia }> = memo(
|
||||
({ media }) => {
|
||||
const mediaContainerClassName =
|
||||
"bds-header-hero-primary-media__media-container";
|
||||
const mediaElementClassName =
|
||||
"bds-header-hero-primary-media__media-element";
|
||||
|
||||
switch (media.type) {
|
||||
case "image": {
|
||||
const { type, ...imgProps } = media;
|
||||
return (
|
||||
<div className={mediaContainerClassName}>
|
||||
<img {...imgProps} className={mediaElementClassName} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case "video": {
|
||||
// alt here is being used as a aria label value
|
||||
const { type, alt, ...videoProps } = media;
|
||||
return (
|
||||
<div className={mediaContainerClassName}>
|
||||
<video
|
||||
{...videoProps}
|
||||
className={mediaElementClassName}
|
||||
aria-label={alt}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case "custom": {
|
||||
const { element } = media;
|
||||
return (
|
||||
<div className={mediaContainerClassName}>
|
||||
<div className={mediaElementClassName}>{element}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const HeaderHeroPrimaryMedia = forwardRef<
|
||||
HTMLElement,
|
||||
HeaderHeroPrimaryMediaProps
|
||||
>((props, ref) => {
|
||||
const { headline, subtitle, callsToAction, media, className, ...restProps } =
|
||||
props;
|
||||
|
||||
const [primaryCta, secondaryCta] = callsToAction;
|
||||
|
||||
// Headline is critical - exit early if missing
|
||||
if (!headline) {
|
||||
if (isEnvironment("development")) {
|
||||
console.error("Headline is required for HeaderHeroPrimaryMedia");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate other props and log warnings for missing optional/required fields
|
||||
// Note: These props log warnings but don't prevent rendering
|
||||
useEffect(() => {
|
||||
if (!isEnvironment(["development", "test"])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propsToValidate = {
|
||||
subtitle,
|
||||
callsToAction,
|
||||
media,
|
||||
};
|
||||
|
||||
Object.entries(propsToValidate).forEach(([key, value]) => {
|
||||
if (isEmpty(value)) {
|
||||
console.warn(`${key} is required for HeaderHeroPrimaryMedia`);
|
||||
}
|
||||
});
|
||||
}, [subtitle, callsToAction, media]);
|
||||
|
||||
return (
|
||||
<header
|
||||
className={clsx("bds-header-hero-primary-media", className)}
|
||||
ref={ref}
|
||||
{...restProps}
|
||||
>
|
||||
<PageGrid>
|
||||
<PageGrid.Row>
|
||||
<PageGrid.Col
|
||||
span={{ base: 12, md: 6, lg: 5 }}
|
||||
className="bds-header-hero-primary-media__headline-container"
|
||||
>
|
||||
<h1 className="bds-header-hero-primary-media__headline display-md">
|
||||
<span>{headline}</span>
|
||||
</h1>
|
||||
</PageGrid.Col>
|
||||
<PageGrid.Col offset={{ base: 0, lg: 1 }} span={{ base: 12, lg: 5 }}>
|
||||
<div className="bds-header-hero-primary-media__cta-container">
|
||||
{!isEmpty(subtitle) && (
|
||||
<div className="bds-header-hero-primary-media__subtitle body-l">
|
||||
{subtitle}
|
||||
</div>
|
||||
)}
|
||||
{(!isEmpty(primaryCta) || !isEmpty(secondaryCta)) && (
|
||||
<div className="bds-header-hero-primary-media__cta-buttons">
|
||||
{!isEmpty(primaryCta) && (
|
||||
<Button
|
||||
{...primaryCta!}
|
||||
variant="primary"
|
||||
color="green"
|
||||
showIcon={true}
|
||||
/>
|
||||
)}
|
||||
{!isEmpty(secondaryCta) && (
|
||||
<Button
|
||||
{...secondaryCta!}
|
||||
className={clsx(
|
||||
"bds-header-hero-primary-media__cta-button-tertiary",
|
||||
secondaryCta?.className,
|
||||
)}
|
||||
variant="tertiary"
|
||||
color="green"
|
||||
showIcon={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
{/* Media */}
|
||||
{!isEmpty(media) && (
|
||||
<PageGrid.Row>
|
||||
<PageGrid.Col span={12}>
|
||||
<MediaRenderer media={media} />
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
)}
|
||||
</PageGrid>
|
||||
</header>
|
||||
);
|
||||
});
|
||||
|
||||
export default HeaderHeroPrimaryMedia;
|
||||
@@ -1,213 +0,0 @@
|
||||
# HeaderHeroPrimaryMedia Pattern
|
||||
|
||||
A page-level hero pattern featuring a headline, subtitle, call-to-action buttons, and a primary media element. Supports images, videos, or custom React elements with enforced aspect ratios and object-fit constraints.
|
||||
|
||||
## Overview
|
||||
|
||||
The HeaderHeroPrimaryMedia component provides a structured hero section with:
|
||||
|
||||
- Responsive headline and subtitle layout
|
||||
- Primary and optional secondary call-to-action buttons
|
||||
- Media element (image, video, or custom) with responsive aspect ratios
|
||||
- Development-time validation warnings
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```tsx
|
||||
import HeaderHeroPrimaryMedia from "shared/patterns/HeaderHeroPrimaryMedia/HeaderHeroPrimaryMedia";
|
||||
|
||||
function MyPage() {
|
||||
return (
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Build on XRPL"
|
||||
subtitle="Start developing today with our comprehensive developer tools."
|
||||
callsToAction={[{ children: "Get Started", href: "/docs" }]}
|
||||
media={{
|
||||
type: "image",
|
||||
src: "/img/hero.png",
|
||||
alt: "XRPL Development",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
| --------------- | ------------------------------ | -------- | ------------------------------------------------------------ |
|
||||
| `headline` | `React.ReactNode` | Yes | Hero headline text (display-md typography) |
|
||||
| `subtitle` | `React.ReactNode` | Yes | Hero subtitle text (label-l typography) |
|
||||
| `callsToAction` | `[ButtonProps, ButtonProps?]` | Yes | Array with primary CTA (required) and optional secondary CTA |
|
||||
| `media` | `HeaderHeroMedia` | Yes | Media element (image, video, or custom) |
|
||||
| `className` | `string` | No | Additional CSS classes for the header element |
|
||||
| `...rest` | `HTMLHeaderElement attributes` | No | Any other HTML header attributes |
|
||||
|
||||
### Calls to Action
|
||||
|
||||
The `callsToAction` prop accepts Button component props, but `variant` and `color` are automatically set:
|
||||
|
||||
- **Primary CTA**: `variant="primary"`, `color="green"`
|
||||
- **Secondary CTA**: `variant="tertiary"`, `color="green"`
|
||||
|
||||
All other Button props are supported (e.g., `children`, `href`, `onClick`, etc.).
|
||||
|
||||
## Media Types
|
||||
|
||||
The `media` prop accepts a discriminated union of three types:
|
||||
|
||||
### Image Media
|
||||
|
||||
```tsx
|
||||
media={{
|
||||
type: "image",
|
||||
src: string, // Required
|
||||
alt: string, // Required
|
||||
// ... all native <img> props except className and style
|
||||
}}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
media={{
|
||||
type: "image",
|
||||
src: "/img/hero.png",
|
||||
alt: "Hero image",
|
||||
loading: "lazy",
|
||||
decoding: "async"
|
||||
}}
|
||||
```
|
||||
|
||||
### Video Media
|
||||
|
||||
```tsx
|
||||
media={{
|
||||
type: "video",
|
||||
src: string, // Required
|
||||
alt?: string, // Optional but recommended
|
||||
// ... all native <video> props except className and style
|
||||
}}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
media={{
|
||||
type: "video",
|
||||
src: "/video/intro.mp4",
|
||||
alt: "Introduction video",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
playsInline: true
|
||||
}}
|
||||
```
|
||||
|
||||
### Custom Element Media
|
||||
|
||||
```tsx
|
||||
media={{
|
||||
type: "custom",
|
||||
element: React.ReactElement // Required
|
||||
}}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
media={{
|
||||
type: "custom",
|
||||
element: <MyAnimationComponent />
|
||||
}}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### With Secondary CTA
|
||||
|
||||
```tsx
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Real-world asset tokenization"
|
||||
subtitle="Learn how to issue crypto tokens and build solutions."
|
||||
callsToAction={[
|
||||
{ children: "Get Started", href: "/docs" },
|
||||
{ children: "Learn More", href: "/about" },
|
||||
]}
|
||||
media={{
|
||||
type: "image",
|
||||
src: "/img/tokenization.png",
|
||||
alt: "Tokenization",
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Video Media
|
||||
|
||||
```tsx
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Watch and Learn"
|
||||
subtitle="Explore our video tutorials."
|
||||
callsToAction={[{ children: "Watch Tutorials", href: "/tutorials" }]}
|
||||
media={{
|
||||
type: "video",
|
||||
src: "/video/intro.mp4",
|
||||
alt: "Introduction video",
|
||||
autoPlay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Custom Element
|
||||
|
||||
```tsx
|
||||
<HeaderHeroPrimaryMedia
|
||||
headline="Interactive Experience"
|
||||
subtitle="Engage with custom media."
|
||||
callsToAction={[{ children: "Explore", href: "/interactive" }]}
|
||||
media={{
|
||||
type: "custom",
|
||||
element: <MyAnimationComponent />,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
## Design Constraints
|
||||
|
||||
The component enforces specific design requirements:
|
||||
|
||||
- **Aspect Ratios**: Media maintains responsive aspect ratios:
|
||||
- Base: `16:9`
|
||||
- Medium (md+): `2:1`
|
||||
- Large (lg+): `3:1`
|
||||
- **Object Fit**: All media uses `object-fit: cover` to fill the container
|
||||
- **Type Safety**: TypeScript discriminated unions ensure type-safe media selection
|
||||
|
||||
## Validation
|
||||
|
||||
The component includes development-time validation that logs warnings to the console when required props are missing:
|
||||
|
||||
- Missing `headline`: Component returns `null` (error logged)
|
||||
- Missing `subtitle`, `callsToAction`, or `media`: Warning logged, component still renders
|
||||
|
||||
## CSS Classes
|
||||
|
||||
The component generates the following CSS classes:
|
||||
|
||||
- `bds-header-hero-primary-media` - Root header element
|
||||
- `bds-header-hero-primary-media__headline` - Headline container
|
||||
- `bds-header-hero-primary-media__subtitle` - Subtitle element
|
||||
- `bds-header-hero-primary-media__cta-container` - CTA container
|
||||
- `bds-header-hero-primary-media__cta-buttons` - CTA buttons wrapper
|
||||
- `bds-header-hero-primary-media__media-container` - Media container
|
||||
- `bds-header-hero-primary-media__media-element` - Media element
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Media Selection**: Choose media that works well with the responsive aspect ratios (16:9 base, 2:1 md+, 3:1 lg+)
|
||||
2. **Alt Text**: Always provide meaningful alt text for images and videos
|
||||
3. **Performance**: Use `loading="lazy"` for images below the fold
|
||||
4. **CTAs**: Keep CTA text concise and action-oriented
|
||||
5. **Headlines**: Keep headlines concise and impactful
|
||||
@@ -1,130 +0,0 @@
|
||||
.bds-header-hero-primary-media {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: 32px;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: 170px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
@include bds-theme-mode(light) {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
@include bds-theme-mode(dark) {
|
||||
background-color: $black;
|
||||
}
|
||||
|
||||
&__headline-container {
|
||||
margin-bottom: 32px; // this margin is also default with the class - however, to avoid regressive changes we are reinforcing here
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
&__headline {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
|
||||
* {
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@include bds-theme-mode(light) {
|
||||
color: $gray-500
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&__cta-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
justify-content: flex-end;
|
||||
min-height: 100%;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
&__cta-buttons {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
& .bds-btn--tertiary {
|
||||
padding: 0; // Design requires this button in this use-case to be overwritten with no padding
|
||||
|
||||
&:hover,
|
||||
&:focus-visible,
|
||||
&:focus {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__media-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9; // Design req uirement: 16/9 aspect ratio
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin-top: 24px;
|
||||
height: auto;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-top: 32px;
|
||||
aspect-ratio: 2 / 1;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-top: 40px;
|
||||
aspect-ratio: 3 / 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__media-element {
|
||||
// Styles are applied inline to ensure object-fit: cover
|
||||
// This ensures the media covers the entire container area
|
||||
// while maintaining the aspect ratio constraint
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
// BDS LinkSmallGrid Pattern Styles
|
||||
// Brand Design System - Link grid section pattern with heading and responsive tile grid
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-link-small-grid - Base section container
|
||||
// .bds-link-small-grid--gray - Gray color variant
|
||||
// .bds-link-small-grid--lilac - Lilac color variant
|
||||
// .bds-link-small-grid__header - Header container (heading + description)
|
||||
// .bds-link-small-grid__heading - Heading text
|
||||
// .bds-link-small-grid__description - Description text
|
||||
|
||||
@import '../../../styles/breakpoints';
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
|
||||
// Spacing tokens
|
||||
$bds-link-small-grid-spacing-base: 24px; // Vertical section padding
|
||||
$bds-link-small-grid-spacing-md: 32px; // Vertical section padding
|
||||
$bds-link-small-grid-spacing-lg: 40px; // Vertical section padding
|
||||
|
||||
// Typography tokens (using existing typography classes)
|
||||
// - Heading: h-md class (handled in component)
|
||||
// - Description: body-l class (handled in component)
|
||||
|
||||
// =============================================================================
|
||||
// Base Section Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-link-small-grid {
|
||||
// Section spacing
|
||||
padding-top: $bds-link-small-grid-spacing-base;
|
||||
padding-bottom: $bds-link-small-grid-spacing-base;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-top: $bds-link-small-grid-spacing-md;
|
||||
padding-bottom: $bds-link-small-grid-spacing-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding-top: $bds-link-small-grid-spacing-lg;
|
||||
padding-bottom: $bds-link-small-grid-spacing-lg;
|
||||
}
|
||||
|
||||
// Background color - default to transparent
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Section
|
||||
// =============================================================================
|
||||
|
||||
.bds-link-small-grid__header {
|
||||
gap: 8px;
|
||||
margin-bottom: $bds-link-small-grid-spacing-base;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: 16px;
|
||||
margin-bottom: $bds-link-small-grid-spacing-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: 16px;
|
||||
margin-bottom: $bds-link-small-grid-spacing-lg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
|
||||
import { TileLink, TileLinkProps } from '../TileLinks/TileLink';
|
||||
import { calculateTileOffset } from 'shared/utils/helpers';
|
||||
|
||||
export interface LinkItem extends Omit<TileLinkProps, 'variant'> {}
|
||||
|
||||
export interface LinkSmallGridProps {
|
||||
/** Color variant - determines tile background color */
|
||||
variant?: 'gray' | 'lilac';
|
||||
/** Heading text (required) */
|
||||
heading: string;
|
||||
/** Optional description text */
|
||||
description?: string;
|
||||
/** Array of link items to display in the grid */
|
||||
links: LinkItem[];
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* LinkSmallGrid Component
|
||||
*
|
||||
* A responsive grid section pattern for displaying navigational links using TileLink components.
|
||||
* Features a heading, optional description, and a grid of clickable tiles with 2 color variants
|
||||
* and full light/dark mode support.
|
||||
*
|
||||
* Grid Layout (12-column grid system):
|
||||
* - Base (< 576px): 1 tile per row (each tile spans 4 of 4 columns = full width)
|
||||
* - MD (576px - 991px): 2 tiles per row (each tile spans 4 of 8 columns = 50% width)
|
||||
* - LG (≥ 992px): 4 tiles per row (each tile spans 3 of 12 columns = 25% width)
|
||||
*
|
||||
* Right-Alignment Logic (applied when < 10 total tiles):
|
||||
* The first tile of each row gets an offset to right-align the grid at LG breakpoint only:
|
||||
* - LG: 1 tile = offset 9, 2 tiles = offset 6, 3 tiles = offset 3, 4 tiles = offset 0
|
||||
* - 10+ tiles: no offset (left-aligned grid)
|
||||
* - MD and Base: no offset applied
|
||||
*
|
||||
* Each tile uses the TileLink component which features:
|
||||
* - Window shade hover animation
|
||||
* - Arrow icon with animation
|
||||
* - Responsive sizing (64px height at all breakpoints)
|
||||
* - Support for both links (href) and buttons (onClick)
|
||||
* - Gray and Lilac color variants
|
||||
*
|
||||
* @example
|
||||
* // Basic usage with gray variant
|
||||
* <LinkSmallGrid
|
||||
* variant="gray"
|
||||
* heading="Quick Links"
|
||||
* description="Navigate to key sections"
|
||||
* links={[
|
||||
* { label: "Documentation", href: "/docs" },
|
||||
* { label: "Tutorials", href: "/tutorials" }
|
||||
* ]}
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Lilac variant with click handlers
|
||||
* <LinkSmallGrid
|
||||
* variant="lilac"
|
||||
* heading="Get Started"
|
||||
* links={[
|
||||
* { label: "Quick Start", onClick: () => navigate('/start') },
|
||||
* { label: "Examples", href: "/examples" }
|
||||
* ]}
|
||||
* />
|
||||
*/
|
||||
export const LinkSmallGrid: React.FC<LinkSmallGridProps> = ({
|
||||
variant = 'gray',
|
||||
heading,
|
||||
description,
|
||||
links,
|
||||
className,
|
||||
}) => {
|
||||
// Build class names using BEM with bds namespace
|
||||
const classNames = clsx(
|
||||
'bds-link-small-grid',
|
||||
`bds-link-small-grid--${variant}`,
|
||||
className
|
||||
);
|
||||
|
||||
// Memoize offset calculations - only recalculate when links array changes
|
||||
const linkOffsets = useMemo(() => {
|
||||
const total = links.length;
|
||||
return links.map((_, index) => calculateTileOffset(index, total));
|
||||
}, [links]);
|
||||
|
||||
return (
|
||||
<section className={classNames}>
|
||||
<PageGrid>
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 6, lg: 8 }}>
|
||||
{/* Header Section */}
|
||||
<div className="bds-link-small-grid__header">
|
||||
<h2 className="bds-link-small-grid__heading h-md">{heading}</h2>
|
||||
{description && (
|
||||
<p className="body-l mb-0">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
{links.map((link, index) => {
|
||||
const offset = linkOffsets[index];
|
||||
const hasOffset = offset.lg > 0;
|
||||
// Use href or label as key, fallback to index
|
||||
const key = link.href || link.label || index;
|
||||
return (
|
||||
<PageGridCol
|
||||
key={key}
|
||||
span={{ base: 4, md: 4, lg: 3 }}
|
||||
offset={hasOffset ? { lg: offset.lg } : undefined}
|
||||
>
|
||||
<TileLink
|
||||
variant={variant}
|
||||
{...link}
|
||||
/>
|
||||
</PageGridCol>
|
||||
);
|
||||
})}
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkSmallGrid;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user