mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2026-01-24 00:25:20 +00:00
Compare commits
8 Commits
section/ca
...
go/primary
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a2ff6e69f | ||
|
|
6bce7efae0 | ||
|
|
99d3442bef | ||
|
|
e66a877868 | ||
|
|
b085502a4d | ||
|
|
f7c80a5c04 | ||
|
|
1e61c71c94 | ||
|
|
bf88924d3d |
@@ -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 /%}
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
|
||||
import { CardStats } 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 = [
|
||||
{
|
||||
statistic: "12",
|
||||
superscript: "+" as const,
|
||||
label: "Continuous uptime years",
|
||||
variant: "lilac" as const,
|
||||
primaryButton: { label: "Learn More", href: "#uptime" },
|
||||
},
|
||||
{
|
||||
statistic: "6M",
|
||||
superscript: "2" as const,
|
||||
label: "Active wallets",
|
||||
variant: "light-gray" as const,
|
||||
primaryButton: { label: "Explore", href: "#wallets" },
|
||||
},
|
||||
{
|
||||
statistic: "$1T",
|
||||
superscript: "+" as const,
|
||||
label: "Value transferred",
|
||||
variant: "green" as const,
|
||||
primaryButton: { label: "View Stats", href: "#value" },
|
||||
},
|
||||
{
|
||||
statistic: "3-5s",
|
||||
label: "Transaction finality",
|
||||
variant: "green" as const,
|
||||
primaryButton: { label: "Learn More", href: "#speed" },
|
||||
},
|
||||
{
|
||||
statistic: "70",
|
||||
superscript: "+" as const,
|
||||
label: "Ecosystem partners",
|
||||
variant: "dark-gray" as const,
|
||||
primaryButton: { label: "Meet Partners", href: "#partners" },
|
||||
},
|
||||
{
|
||||
statistic: "100K",
|
||||
superscript: "+" as const,
|
||||
label: "Developer community",
|
||||
variant: "lilac" as const,
|
||||
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 */}
|
||||
<section>
|
||||
<CardStats
|
||||
heading="Blockchain Trusted at Scale"
|
||||
description="Streamline development and build powerful RWA tokenization solutions with XRP Ledger's comprehensive developer toolset."
|
||||
cards={sampleCards}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Heading Only - No Description */}
|
||||
<section>
|
||||
<CardStats
|
||||
heading="XRPL Network Statistics"
|
||||
cards={sampleCards.slice(0, 3)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 4 Cards Example */}
|
||||
<section>
|
||||
<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)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<section>
|
||||
<CardStats
|
||||
heading="Get Started with XRPL"
|
||||
description="Explore the XRP Ledger ecosystem with comprehensive documentation and developer resources."
|
||||
cards={[
|
||||
{
|
||||
statistic: "12",
|
||||
superscript: "+" as const,
|
||||
label: "Continuous uptime years",
|
||||
variant: "lilac" as const,
|
||||
primaryButton: { label: "Learn More", href: "#learn" },
|
||||
secondaryButton: { label: "View Docs", href: "#docs" },
|
||||
},
|
||||
{
|
||||
statistic: "6M",
|
||||
superscript: "+" as const,
|
||||
label: "Active wallets",
|
||||
variant: "green" as const,
|
||||
primaryButton: { label: "Get Started", href: "#start" },
|
||||
secondaryButton: { label: "Explore", href: "#explore" },
|
||||
},
|
||||
{
|
||||
statistic: "$1T",
|
||||
superscript: "+" as const,
|
||||
label: "Value transferred",
|
||||
variant: "light-gray" as const,
|
||||
primaryButton: { label: "View Stats", href: "#stats" },
|
||||
secondaryButton: { label: "Learn More", href: "#about" },
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
769
about/header-hero-primary-media-showcase.page.tsx
Normal file
769
about/header-hero-primary-media-showcase.page.tsx
Normal file
@@ -0,0 +1,769 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
530
about/logo-square-grid-showcase.page.tsx
Normal file
530
about/logo-square-grid-showcase.page.tsx
Normal file
File diff suppressed because one or more lines are too long
@@ -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.
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
|
||||
// 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
|
||||
$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;
|
||||
@@ -44,16 +44,16 @@ $bds-card-stat-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
|
||||
|
||||
// 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;
|
||||
|
||||
|
||||
// Content wrapper
|
||||
&__content {
|
||||
display: flex;
|
||||
@@ -62,43 +62,35 @@ $bds-card-stat-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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;
|
||||
@@ -138,12 +130,11 @@ $bds-card-stat-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
|
||||
|
||||
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
|
||||
@@ -158,12 +149,11 @@ html.dark {
|
||||
.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;
|
||||
|
||||
@@ -13,8 +13,8 @@ interface ButtonConfig {
|
||||
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
|
||||
@@ -72,43 +72,52 @@ 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 (
|
||||
<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>
|
||||
)}
|
||||
|
||||
43
shared/patterns/ButtonGroup/ButtonGroup.scss
Normal file
43
shared/patterns/ButtonGroup/ButtonGroup.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
// 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)
|
||||
|
||||
// =============================================================================
|
||||
// Base Component Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-button-group {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
@extend .align-items-start;
|
||||
@extend .flex-wrap;
|
||||
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;
|
||||
}
|
||||
}
|
||||
90
shared/patterns/ButtonGroup/ButtonGroup.tsx
Normal file
90
shared/patterns/ButtonGroup/ButtonGroup.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '../../components/Button/Button';
|
||||
|
||||
export interface ButtonConfig {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
export interface ButtonGroupProps {
|
||||
/** Primary button configuration */
|
||||
primaryButton?: ButtonConfig;
|
||||
/** Tertiary button configuration */
|
||||
tertiaryButton?: ButtonConfig;
|
||||
/** Button color theme */
|
||||
color?: 'green' | 'black';
|
||||
/** Gap between buttons on tablet+ (0px or 8px) */
|
||||
gap?: 'none' | 'small';
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ButtonGroup Component
|
||||
*
|
||||
* A responsive button group container that displays primary and/or tertiary buttons.
|
||||
* Stacks vertically on mobile and horizontally on tablet+.
|
||||
*
|
||||
* @example
|
||||
* // Basic usage with both buttons
|
||||
* <ButtonGroup
|
||||
* primaryButton={{ label: "Get Started", href: "/start" }}
|
||||
* tertiaryButton={{ label: "Learn More", href: "/learn" }}
|
||||
* color="green"
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // With custom gap
|
||||
* <ButtonGroup
|
||||
* primaryButton={{ label: "Action", onClick: handleClick }}
|
||||
* color="black"
|
||||
* gap="small"
|
||||
* />
|
||||
*/
|
||||
export const ButtonGroup: React.FC<ButtonGroupProps> = ({
|
||||
primaryButton,
|
||||
tertiaryButton,
|
||||
color = 'green',
|
||||
gap = 'small',
|
||||
className = '',
|
||||
}) => {
|
||||
// Don't render if no buttons are provided
|
||||
if (!primaryButton && !tertiaryButton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const classNames = clsx(
|
||||
'bds-button-group',
|
||||
`bds-button-group--gap-${gap}`,
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames}>
|
||||
{primaryButton && (
|
||||
<Button
|
||||
variant="primary"
|
||||
color={color}
|
||||
href={primaryButton.href}
|
||||
onClick={primaryButton?.onClick as (() => void) | undefined}
|
||||
>
|
||||
{primaryButton.label}
|
||||
</Button>
|
||||
)}
|
||||
{tertiaryButton && (
|
||||
<Button
|
||||
variant="tertiary"
|
||||
color={color}
|
||||
href={tertiaryButton.href}
|
||||
onClick={tertiaryButton?.onClick as (() => void) | undefined}
|
||||
>
|
||||
{tertiaryButton.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonGroup;
|
||||
68
shared/patterns/ButtonGroup/README.md
Normal file
68
shared/patterns/ButtonGroup/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# ButtonGroup Component
|
||||
|
||||
A responsive button group container that displays primary and/or tertiary buttons. Stacks vertically on mobile and horizontally on tablet+.
|
||||
|
||||
## Features
|
||||
|
||||
- **Responsive Layout**: Vertical stack on mobile, horizontal row on tablet+
|
||||
- **Flexible Configuration**: Support for primary, tertiary, or both buttons
|
||||
- **Customizable Spacing**: Control gap between buttons on tablet+ (none or small)
|
||||
- **Theme Support**: Green or black color themes
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import { ButtonGroup } from 'shared/patterns/ButtonGroup';
|
||||
|
||||
// Basic usage with both buttons
|
||||
<ButtonGroup
|
||||
primaryButton={{ label: "Get Started", href: "/start" }}
|
||||
tertiaryButton={{ label: "Learn More", href: "/learn" }}
|
||||
color="green"
|
||||
/>
|
||||
|
||||
// With no gap on tablet+
|
||||
<ButtonGroup
|
||||
primaryButton={{ label: "Action", onClick: handleClick }}
|
||||
color="black"
|
||||
gap="none"
|
||||
/>
|
||||
|
||||
// With small gap on tablet+ (4px - default)
|
||||
<ButtonGroup
|
||||
primaryButton={{ label: "Primary Action", href: "/action" }}
|
||||
tertiaryButton={{ label: "Secondary", href: "/secondary" }}
|
||||
gap="small"
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `primaryButton` | `ButtonConfig` | - | Primary button configuration |
|
||||
| `tertiaryButton` | `ButtonConfig` | - | Tertiary button configuration |
|
||||
| `color` | `'green' \| 'black'` | `'green'` | Button color theme |
|
||||
| `gap` | `'none' \| 'small'` | `'small'` | Gap between buttons on tablet+ (0px or 4px) |
|
||||
| `className` | `string` | `''` | Additional CSS classes |
|
||||
|
||||
### ButtonConfig
|
||||
|
||||
```tsx
|
||||
interface ButtonConfig {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
```
|
||||
|
||||
## 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)
|
||||
2
shared/patterns/ButtonGroup/index.ts
Normal file
2
shared/patterns/ButtonGroup/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { ButtonGroup } from './ButtonGroup';
|
||||
export type { ButtonGroupProps, ButtonConfig } from './ButtonGroup';
|
||||
@@ -15,7 +15,8 @@
|
||||
// .bds-callout-media-banner__text - Text content container
|
||||
// .bds-callout-media-banner__heading - Heading element
|
||||
// .bds-callout-media-banner__subheading - Subheading element
|
||||
// .bds-callout-media-banner__actions - Button container
|
||||
//
|
||||
// Note: Button layout is handled by the ButtonGroup component
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
@@ -191,21 +192,8 @@ $bds-cmb-image-overlay: rgba(0, 0, 0, 0.3);
|
||||
|
||||
// =============================================================================
|
||||
// Action Buttons
|
||||
// ==================================================================== 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;
|
||||
}
|
||||
}
|
||||
// =============================================================================
|
||||
// Note: Button layout is now handled by the ButtonGroup component
|
||||
|
||||
// =============================================================================
|
||||
// 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 } from '../ButtonGroup/ButtonGroup';
|
||||
|
||||
export interface CalloutMediaBannerProps {
|
||||
/** Color variant - determines background color (ignored if backgroundImage is provided) */
|
||||
@@ -116,30 +116,12 @@ export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
{(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>
|
||||
)}
|
||||
<ButtonGroup
|
||||
primaryButton={primaryButton}
|
||||
tertiaryButton={tertiaryButton}
|
||||
color={buttonColor}
|
||||
gap="none"
|
||||
/>
|
||||
</div>
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
|
||||
@@ -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,176 +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)
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens (from Figma)
|
||||
// =============================================================================
|
||||
|
||||
$bds-grid-gutter: 8px;
|
||||
|
||||
// Color tokens - Light Mode (from Figma: node 32051-2839)
|
||||
$bds-card-stats-bg-light: $white; // --neutral/white (#FFFFFF)
|
||||
$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-sm: 8px; // Mobile: 8px
|
||||
$bds-card-stats-header-gap-md: 8px; // Tablet: 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 {
|
||||
// Layout
|
||||
display: block;
|
||||
width: 100%;
|
||||
background-color: $bds-card-stats-bg-light;
|
||||
|
||||
// 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-sm;
|
||||
max-width: 808px;
|
||||
|
||||
// Responsive header gap
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-card-stats-header-gap-md;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-card-stats-header-gap-lg;
|
||||
}
|
||||
|
||||
// Margin below header (gap between header and cards)
|
||||
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) {
|
||||
margin-bottom: $bds-card-stats-section-gap-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-card-stats__heading {
|
||||
color: $black;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bds-card-stats__description {
|
||||
color: $black;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Cards Grid Styles
|
||||
// Breakpoints from _breakpoints.scss:
|
||||
// - xs: 0 (mobile - 1 column)
|
||||
// - sm/md: 576px (tablet - 2 columns)
|
||||
// - lg: 992px (desktop - 3 columns)
|
||||
// - xl: 1280px, xxl: 1512px
|
||||
// =============================================================================
|
||||
|
||||
.bds-card-stats__cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $bds-grid-gutter;
|
||||
width: 100%;
|
||||
|
||||
// Tablet and above: switch to flex-wrap row layout
|
||||
@include media-breakpoint-up(md) {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-card-stats__card-wrapper {
|
||||
// Mobile (base): 1 column - full width
|
||||
flex: 0 0 100%;
|
||||
min-width: 0;
|
||||
|
||||
// Tablet (md - 576px): 2 columns
|
||||
@include media-breakpoint-up(md) {
|
||||
flex: 0 0 calc(50% - #{$bds-grid-gutter / 2});
|
||||
}
|
||||
|
||||
// Desktop (lg - 992px): 3 columns
|
||||
@include media-breakpoint-up(lg) {
|
||||
flex: 0 0 calc(33.333% - #{$bds-grid-gutter * 2 / 3});
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dark Mode Styles
|
||||
// =============================================================================
|
||||
|
||||
html.dark {
|
||||
.bds-card-stats {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.bds-card-stats__heading,
|
||||
.bds-card-stats__description {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,111 +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: readonly 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 (
|
||||
<section
|
||||
ref={ref}
|
||||
className={clsx('bds-card-stats', className)}
|
||||
{...rest}
|
||||
>
|
||||
<PageGrid>
|
||||
<PageGrid.Row>
|
||||
<PageGrid.Col span={{ base: 4, md: 8, lg: 12 }}>
|
||||
{/* Header section */}
|
||||
<div className="bds-card-stats__header">
|
||||
<h2 className="bds-card-stats__heading h-md">{heading}</h2>
|
||||
{description && (
|
||||
<p className="bds-card-stats__description body-l">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cards grid */}
|
||||
<div className="bds-card-stats__cards">
|
||||
{cards.map((cardConfig, index) => (
|
||||
<div
|
||||
key={`card-stat-${index}`}
|
||||
className="bds-card-stats__card-wrapper"
|
||||
>
|
||||
<CardStat {...cardConfig} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PageGrid.Col>
|
||||
</PageGrid.Row>
|
||||
</PageGrid>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardStats.displayName = 'CardStats';
|
||||
|
||||
export default CardStats;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export { CardStats, type CardStatsProps, type CardStatsCardConfig } from './CardStats';
|
||||
export { default } from './CardStats';
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import React, { forwardRef, memo, useEffect } from "react";
|
||||
import clsx from "clsx";
|
||||
import { PageGrid } from "shared/components/PageGrid/page-grid";
|
||||
import { Button, ButtonProps } from "shared/components/Button/Button";
|
||||
|
||||
const isEmpty = (val: unknown): boolean => {
|
||||
if (val === null || val === undefined) return true;
|
||||
if (typeof val === "string") return val.trim().length === 0;
|
||||
if (Array.isArray(val)) return val.length === 0;
|
||||
return !Boolean(val);
|
||||
};
|
||||
|
||||
type DesignContrainedButtonProps = Omit<ButtonProps, "variant" | "color">;
|
||||
|
||||
/**
|
||||
* Base props that all media elements must have to ensure proper styling.
|
||||
* These props are automatically applied to maintain the 9:16 aspect ratio
|
||||
* and object-fit: cover behavior.
|
||||
*/
|
||||
type MediaStyleProps = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
/**
|
||||
* Image media type - extends native img element props
|
||||
*/
|
||||
type ImageMediaProps = {
|
||||
type: "image";
|
||||
} & Omit<
|
||||
React.ComponentPropsWithoutRef<"img">,
|
||||
keyof MediaStyleProps | "src" | "alt"
|
||||
> & {
|
||||
src: string; // Required for image media
|
||||
alt: string; // Required for image media
|
||||
};
|
||||
|
||||
/**
|
||||
* Video media type - extends native video element props
|
||||
*/
|
||||
type VideoMediaProps = {
|
||||
type: "video";
|
||||
} & Omit<
|
||||
React.ComponentPropsWithoutRef<"video">,
|
||||
keyof MediaStyleProps | "src"
|
||||
> & {
|
||||
src: string; // Required for video media
|
||||
alt?: string; // Optional for video, but recommended for accessibility
|
||||
};
|
||||
|
||||
/**
|
||||
* 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: [DesignContrainedButtonProps, DesignContrainedButtonProps?];
|
||||
/** 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": {
|
||||
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) {
|
||||
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(() => {
|
||||
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">
|
||||
<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;
|
||||
213
shared/patterns/HeaderHeroPrimaryMedia/README.md
Normal file
213
shared/patterns/HeaderHeroPrimaryMedia/README.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# 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
|
||||
@@ -0,0 +1,130 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
115
shared/patterns/LogoSquareGrid/LogoSquareGrid.scss
Normal file
115
shared/patterns/LogoSquareGrid/LogoSquareGrid.scss
Normal file
@@ -0,0 +1,115 @@
|
||||
// BDS LogoSquareGrid Component Styles
|
||||
// Brand Design System - Logo grid pattern with optional header section
|
||||
//
|
||||
// Naming Convention: BEM with 'bds' namespace
|
||||
// .bds-logo-square-grid - Base component
|
||||
// .bds-logo-square-grid--gray - Gray variant (maps to TileLogo 'neutral')
|
||||
// .bds-logo-square-grid--green - Green variant (maps to TileLogo 'green')
|
||||
// .bds-logo-square-grid__header - Header section container
|
||||
// .bds-logo-square-grid__text - Text content container
|
||||
// .bds-logo-square-grid__heading - Heading element
|
||||
// .bds-logo-square-grid__description - Description element
|
||||
//
|
||||
// Note: Individual logo tiles are rendered using the TileLogo component
|
||||
// Note: Button layout is handled by the ButtonGroup component
|
||||
|
||||
// =============================================================================
|
||||
// Design Tokens
|
||||
// =============================================================================
|
||||
// Note: Color variants are now handled by the TileLogo component
|
||||
// LogoSquareGrid 'gray' maps to TileLogo 'neutral'
|
||||
// LogoSquareGrid 'green' maps to TileLogo 'green'
|
||||
|
||||
// Spacing tokens - responsive
|
||||
// Mobile (<768px)
|
||||
$bds-lsg-header-gap-mobile: 24px;
|
||||
$bds-lsg-text-gap-mobile: 8px;
|
||||
|
||||
// Tablet (768px-1023px)
|
||||
$bds-lsg-header-gap-tablet: 32px;
|
||||
|
||||
// Desktop (≥1024px)
|
||||
$bds-lsg-header-gap-desktop: 40px;
|
||||
$bds-lsg-text-gap-desktop: 16px;
|
||||
|
||||
// =============================================================================
|
||||
// Base Component Styles
|
||||
// =============================================================================
|
||||
|
||||
.bds-logo-square-grid {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
@extend .w-100;
|
||||
|
||||
// Mobile-first gap
|
||||
gap: $bds-lsg-header-gap-mobile;
|
||||
|
||||
// Tablet breakpoint
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-lsg-header-gap-tablet;
|
||||
}
|
||||
|
||||
// Desktop breakpoint
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-lsg-header-gap-desktop;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Header Section
|
||||
// =============================================================================
|
||||
|
||||
.bds-logo-square-grid__header {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
// Mobile-first gap
|
||||
gap: $bds-lsg-header-gap-mobile;
|
||||
|
||||
// Tablet breakpoint
|
||||
@include media-breakpoint-up(md) {
|
||||
gap: $bds-lsg-header-gap-tablet;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
// Desktop breakpoint
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-lsg-header-gap-desktop;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Text Content
|
||||
// =============================================================================
|
||||
|
||||
.bds-logo-square-grid__text {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
|
||||
// Mobile-first gap
|
||||
gap: $bds-lsg-text-gap-mobile;
|
||||
|
||||
// Desktop breakpoint
|
||||
@include media-breakpoint-up(lg) {
|
||||
gap: $bds-lsg-text-gap-desktop;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Action Buttons
|
||||
// =============================================================================
|
||||
// Note: Button layout is now handled by the ButtonGroup component
|
||||
|
||||
// =============================================================================
|
||||
// Logo Grid Row
|
||||
// =============================================================================
|
||||
// Note: Grid layout is now handled by PageGridRow/PageGridCol
|
||||
// Each tile uses PageGridCol with span={{ base: 2, lg: 3 }}
|
||||
// This gives us 2 columns on mobile (2/4) and 4 columns on desktop (3/12)
|
||||
// Tile rendering and styling is handled by the TileLogo component
|
||||
140
shared/patterns/LogoSquareGrid/LogoSquareGrid.tsx
Normal file
140
shared/patterns/LogoSquareGrid/LogoSquareGrid.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
|
||||
import { TileLogo } from '../../components/TileLogo/TileLogo';
|
||||
import { ButtonGroup } from '../ButtonGroup/ButtonGroup';
|
||||
|
||||
export interface LogoItem {
|
||||
/** Logo image source URL */
|
||||
src: string;
|
||||
/** Alt text for the logo image */
|
||||
alt: string;
|
||||
/** Optional link URL - makes the logo clickable */
|
||||
href?: string;
|
||||
/** Optional click handler - makes the logo a button */
|
||||
onClick?: () => void;
|
||||
/** Disabled state */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface LogoSquareGridProps {
|
||||
/** Color variant - determines background color */
|
||||
variant?: 'gray' | 'green';
|
||||
/** Optional heading text */
|
||||
heading?: string;
|
||||
/** Optional description text */
|
||||
description?: string;
|
||||
/** 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;
|
||||
};
|
||||
/** Array of logo items to display in the grid */
|
||||
logos: LogoItem[];
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* LogoSquareGrid Component
|
||||
*
|
||||
* A responsive grid pattern for displaying company/partner logos with an optional header section.
|
||||
* Features square tiles arranged in a responsive grid with 2 color variants and dark mode support.
|
||||
*
|
||||
* @example
|
||||
* // Basic usage with gray variant
|
||||
* <LogoSquareGrid
|
||||
* variant="gray"
|
||||
* heading="Developer tools & APIs"
|
||||
* description="Streamline development with comprehensive tools."
|
||||
* logos={[
|
||||
* { src: "/logos/company1.svg", alt: "Company 1" },
|
||||
* { src: "/logos/company2.svg", alt: "Company 2" }
|
||||
* ]}
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // With buttons and clickable logos
|
||||
* <LogoSquareGrid
|
||||
* variant="green"
|
||||
* heading="Our Partners"
|
||||
* description="Leading companies building on XRPL."
|
||||
* primaryButton={{ label: "View All Partners", href: "/partners" }}
|
||||
* tertiaryButton={{ label: "Become a Partner", href: "/partner-program" }}
|
||||
* logos={[
|
||||
* { src: "/logos/partner1.svg", alt: "Partner 1", href: "https://partner1.com" }
|
||||
* ]}
|
||||
* />
|
||||
*/
|
||||
export const LogoSquareGrid: React.FC<LogoSquareGridProps> = ({
|
||||
variant = 'gray',
|
||||
heading,
|
||||
description,
|
||||
primaryButton,
|
||||
tertiaryButton,
|
||||
logos,
|
||||
className = '',
|
||||
}) => {
|
||||
// Build class names using BEM with bds namespace
|
||||
const classNames = clsx(
|
||||
'bds-logo-square-grid',
|
||||
`bds-logo-square-grid--${variant}`,
|
||||
className
|
||||
);
|
||||
|
||||
// Determine if we should show the header section
|
||||
const hasHeader = !!(heading || description || primaryButton || tertiaryButton);
|
||||
|
||||
return (
|
||||
<PageGrid className="">
|
||||
<PageGridRow>
|
||||
<PageGridCol span={{ base: 4, md: 6, lg: 8 }}>
|
||||
{/* Header Section */}
|
||||
{hasHeader && (
|
||||
<div className="bds-logo-square-grid__header">
|
||||
{/* Text Content */}
|
||||
{(heading || description) && (
|
||||
<div className="bds-logo-square-grid__text">
|
||||
{heading && <h4 className="h-md mb-0">{heading}</h4>}
|
||||
{description && <p className="body-l mb-0">{description}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
<ButtonGroup
|
||||
primaryButton={primaryButton}
|
||||
tertiaryButton={tertiaryButton}
|
||||
color="green"
|
||||
gap="small"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PageGridCol>
|
||||
</PageGridRow>
|
||||
<PageGridRow>
|
||||
{logos.map((logo, index) => (
|
||||
<PageGridCol key={index} span={{ base: 2, lg: 3 }}>
|
||||
<TileLogo
|
||||
shape="square"
|
||||
variant={variant === 'gray' ? 'neutral' : 'green'}
|
||||
logo={logo.src}
|
||||
alt={logo.alt}
|
||||
href={logo.href}
|
||||
onClick={logo.onClick}
|
||||
disabled={logo.disabled}
|
||||
/>
|
||||
</PageGridCol>
|
||||
))}
|
||||
</PageGridRow>
|
||||
</PageGrid>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogoSquareGrid;
|
||||
405
shared/patterns/LogoSquareGrid/README.md
Normal file
405
shared/patterns/LogoSquareGrid/README.md
Normal file
@@ -0,0 +1,405 @@
|
||||
# LogoSquareGrid Component
|
||||
|
||||
A responsive grid pattern for displaying company/partner logos with an optional header section. Built on top of the TileLogo component, featuring square tiles arranged in a responsive grid with 2 color variants and full dark mode support.
|
||||
|
||||
## Features
|
||||
|
||||
- **2 Color Variants**: Gray and Green backgrounds
|
||||
- **Responsive Grid**: Automatically adapts from 2 columns (mobile) to 4 columns (tablet/desktop)
|
||||
- **Optional Header**: Includes heading, description, and action buttons
|
||||
- **Clickable Logos**: Support for optional links on individual logos
|
||||
- **Dark Mode Support**: Full light and dark mode compatibility
|
||||
- **Square Tiles**: Maintains perfect square aspect ratio at all breakpoints
|
||||
- **Grid Integration**: Built-in PageGrid wrapper with standard container support
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
The component automatically adapts its grid layout based on viewport width:
|
||||
|
||||
| Breakpoint | Columns | Gap | Tile Size |
|
||||
|------------|---------|-----|-----------|
|
||||
| Mobile (< 768px) | 2 | 8px | ~183px |
|
||||
| Tablet (768px - 1023px) | 4 | 8px | ~178px |
|
||||
| Desktop (≥ 1024px) | 4 | 8px | ~298px |
|
||||
|
||||
## Color Variants
|
||||
|
||||
The LogoSquareGrid pattern uses two color variants that map directly to TileLogo component variants:
|
||||
|
||||
| LogoSquareGrid Variant | TileLogo Variant | Description |
|
||||
|------------------------|------------------|-------------|
|
||||
| `gray` | `neutral` | Subtle, professional appearance for general partner showcases |
|
||||
| `green` | `green` | Highlights featured or primary partners |
|
||||
|
||||
Colors are managed by the TileLogo component and automatically adapt between light and dark modes with proper hover states and animations.
|
||||
|
||||
## Props API
|
||||
|
||||
```typescript
|
||||
interface LogoItem {
|
||||
/** Logo image source URL */
|
||||
src: string;
|
||||
/** Alt text for the logo image */
|
||||
alt: string;
|
||||
/** Optional link URL - makes the logo clickable */
|
||||
href?: string;
|
||||
/** Optional click handler - makes the logo a button */
|
||||
onClick?: () => void;
|
||||
/** Disabled state */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface LogoSquareGridProps {
|
||||
/** Color variant - determines background color */
|
||||
variant?: 'gray' | 'green';
|
||||
/** Optional heading text */
|
||||
heading?: string;
|
||||
/** Optional description text */
|
||||
description?: string;
|
||||
/** Primary button configuration */
|
||||
primaryButton?: {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
/** Tertiary button configuration */
|
||||
tertiaryButton?: {
|
||||
label: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
/** Array of logo items to display in the grid */
|
||||
logos: LogoItem[];
|
||||
/** Additional CSS classes */
|
||||
className?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Default Values
|
||||
|
||||
- `variant`: `'gray'`
|
||||
- `heading`: `undefined`
|
||||
- `description`: `undefined`
|
||||
- `primaryButton`: `undefined`
|
||||
- `tertiaryButton`: `undefined`
|
||||
- `className`: `''`
|
||||
|
||||
### Required Props
|
||||
|
||||
- `logos`: Array of logo items (required)
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage with Gray Variant
|
||||
|
||||
```tsx
|
||||
import { LogoSquareGrid } from 'shared/patterns/LogoSquareGrid';
|
||||
|
||||
<LogoSquareGrid
|
||||
variant="gray"
|
||||
logos={[
|
||||
{ src: "/img/logos/company1.svg", alt: "Company 1" },
|
||||
{ src: "/img/logos/company2.svg", alt: "Company 2" },
|
||||
{ src: "/img/logos/company3.svg", alt: "Company 3" },
|
||||
{ src: "/img/logos/company4.svg", alt: "Company 4" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### With Header Section
|
||||
|
||||
```tsx
|
||||
<LogoSquareGrid
|
||||
variant="green"
|
||||
heading="Developer tools & APIs"
|
||||
description="Streamline development and build powerful RWA tokenization solutions with XRP Ledger's comprehensive developer toolset."
|
||||
primaryButton={{ label: "View Documentation", href: "/docs" }}
|
||||
tertiaryButton={{ label: "Explore Tools", href: "/tools" }}
|
||||
logos={[
|
||||
{ src: "/img/logos/tool1.svg", alt: "Tool 1" },
|
||||
{ src: "/img/logos/tool2.svg", alt: "Tool 2" },
|
||||
{ src: "/img/logos/tool3.svg", alt: "Tool 3" },
|
||||
{ src: "/img/logos/tool4.svg", alt: "Tool 4" },
|
||||
{ src: "/img/logos/tool5.svg", alt: "Tool 5" },
|
||||
{ src: "/img/logos/tool6.svg", alt: "Tool 6" },
|
||||
{ src: "/img/logos/tool7.svg", alt: "Tool 7" },
|
||||
{ src: "/img/logos/tool8.svg", alt: "Tool 8" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### With Clickable Logos
|
||||
|
||||
```tsx
|
||||
<LogoSquareGrid
|
||||
variant="gray"
|
||||
heading="Our Partners"
|
||||
description="Leading companies building on XRPL."
|
||||
logos={[
|
||||
{
|
||||
src: "/img/logos/partner1.svg",
|
||||
alt: "Partner 1",
|
||||
href: "https://partner1.com"
|
||||
},
|
||||
{
|
||||
src: "/img/logos/partner2.svg",
|
||||
alt: "Partner 2",
|
||||
href: "https://partner2.com"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### With Button Handlers
|
||||
|
||||
```tsx
|
||||
<LogoSquareGrid
|
||||
variant="green"
|
||||
heading="Interactive Partners"
|
||||
description="Click any logo to learn more."
|
||||
logos={[
|
||||
{
|
||||
src: "/img/logos/partner1.svg",
|
||||
alt: "Partner 1",
|
||||
onClick: () => openModal('partner1')
|
||||
},
|
||||
{
|
||||
src: "/img/logos/partner2.svg",
|
||||
alt: "Partner 2",
|
||||
onClick: () => openModal('partner2')
|
||||
}
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### With Disabled State
|
||||
|
||||
```tsx
|
||||
<LogoSquareGrid
|
||||
variant="gray"
|
||||
heading="Coming Soon"
|
||||
description="New partners joining the ecosystem."
|
||||
logos={[
|
||||
{
|
||||
src: "/img/logos/partner1.svg",
|
||||
alt: "Partner 1",
|
||||
href: "/partners/partner1"
|
||||
},
|
||||
{
|
||||
src: "/img/logos/coming-soon.svg",
|
||||
alt: "Coming Soon",
|
||||
disabled: true
|
||||
}
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### Without Header (Logo Grid Only)
|
||||
|
||||
```tsx
|
||||
<LogoSquareGrid
|
||||
variant="gray"
|
||||
logos={[
|
||||
{ src: "/img/logos/sponsor1.svg", alt: "Sponsor 1" },
|
||||
{ src: "/img/logos/sponsor2.svg", alt: "Sponsor 2" },
|
||||
{ src: "/img/logos/sponsor3.svg", alt: "Sponsor 3" },
|
||||
{ src: "/img/logos/sponsor4.svg", alt: "Sponsor 4" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### With Single Button
|
||||
|
||||
```tsx
|
||||
<LogoSquareGrid
|
||||
variant="green"
|
||||
heading="Featured Integrations"
|
||||
description="Connect with leading platforms and services."
|
||||
primaryButton={{ label: "See All Integrations", href: "/integrations" }}
|
||||
logos={[
|
||||
{ src: "/img/logos/integration1.svg", alt: "Integration 1" },
|
||||
{ src: "/img/logos/integration2.svg", alt: "Integration 2" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### With Click Handler
|
||||
|
||||
```tsx
|
||||
<LogoSquareGrid
|
||||
variant="gray"
|
||||
heading="Developer Resources"
|
||||
description="Access comprehensive tools and libraries."
|
||||
primaryButton={{
|
||||
label: "Get Started",
|
||||
onClick: () => console.log('Primary clicked')
|
||||
}}
|
||||
tertiaryButton={{
|
||||
label: "Learn More",
|
||||
href: "/learn"
|
||||
}}
|
||||
logos={[
|
||||
{ src: "/img/logos/resource1.svg", alt: "Resource 1" }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Important Implementation Details
|
||||
|
||||
### Logo Image Requirements
|
||||
|
||||
For best results, logo images should:
|
||||
- Be SVG format for crisp scaling
|
||||
- Have transparent backgrounds
|
||||
- Be reasonably sized (width: 120-200px recommended)
|
||||
- Use monochrome or simple color schemes
|
||||
- Have consistent visual weight across all logos
|
||||
|
||||
### Grid Behavior
|
||||
|
||||
- The grid uses PageGridCol components for responsive layout
|
||||
- Each tile uses `span={{ base: 2, lg: 3 }}` (2 cols on mobile out of 4, 3 cols on desktop out of 12)
|
||||
- Tiles maintain a 1:1 aspect ratio using `aspect-ratio: 1`
|
||||
- Gaps between tiles are handled by PageGrid's built-in gutter system
|
||||
- Grid automatically wraps to new rows as needed
|
||||
|
||||
### Clickable Logo Behavior
|
||||
|
||||
Logo tiles leverage the TileLogo component's interactive capabilities:
|
||||
- **With `href` property**: Renders as a link (`<a>` tag) with window shade hover animation
|
||||
- **With `onClick` property**: Renders as a button with the same interactive states
|
||||
- **With `disabled` property**: Prevents interaction and applies disabled styling
|
||||
- **Interactive states**: Default, Hover, Focused, Pressed, and Disabled
|
||||
- **Animation**: Window shade effect that wipes from bottom to top on hover
|
||||
- All tiles automatically maintain focus states for keyboard accessibility
|
||||
|
||||
### Header Section Logic
|
||||
|
||||
The header section only renders if at least one of the following is provided:
|
||||
- `heading`
|
||||
- `description`
|
||||
- `primaryButton`
|
||||
- `tertiaryButton`
|
||||
|
||||
### Button Styling
|
||||
|
||||
- Both primary and tertiary buttons use green color scheme
|
||||
- Buttons stack vertically on mobile, horizontal on tablet+
|
||||
- Button spacing: 8px gap on mobile, 4px gap on tablet+
|
||||
- Button layout is handled by the shared ButtonGroup component
|
||||
|
||||
## Styling
|
||||
|
||||
### BEM Class Structure
|
||||
|
||||
```scss
|
||||
.bds-logo-square-grid // Base component
|
||||
.bds-logo-square-grid--gray // Gray variant (maps to TileLogo 'neutral')
|
||||
.bds-logo-square-grid--green // Green variant (maps to TileLogo 'green')
|
||||
.bds-logo-square-grid__header // Header section container
|
||||
.bds-logo-square-grid__text // Text content container
|
||||
.bds-logo-square-grid__heading // Heading element
|
||||
.bds-logo-square-grid__description // Description element
|
||||
```
|
||||
|
||||
**Note**: Individual logo tiles are rendered using the TileLogo component with its own BEM structure (`bds-tile-logo`). Grid layout is handled by PageGridRow and PageGridCol components. Button layout is handled by the ButtonGroup component (`bds-button-group`).
|
||||
|
||||
### Typography Tokens
|
||||
|
||||
- **Heading**: Uses `heading-md` type token (Tobias Light font)
|
||||
- Desktop: 40px / 46px line-height / -1px letter-spacing
|
||||
- Tablet: 36px / 45px line-height / -0.5px letter-spacing
|
||||
- Mobile: 32px / 40px line-height / 0px letter-spacing
|
||||
|
||||
- **Description**: Uses `body-l` type token (Booton Light font)
|
||||
- Desktop: 18px / 26.1px line-height / -0.5px letter-spacing
|
||||
- Tablet: 18px / 26.1px line-height / -0.5px letter-spacing
|
||||
- Mobile: 18px / 26.1px line-height / -0.5px letter-spacing
|
||||
|
||||
### Color Tokens
|
||||
|
||||
All colors are sourced from `styles/_colors.scss`:
|
||||
|
||||
```scss
|
||||
// Tile backgrounds
|
||||
$gray-200 // Gray variant (light mode)
|
||||
$gray-700 // Gray variant (dark mode)
|
||||
$green-200 // Green variant (light mode)
|
||||
$green-300 // Green variant (dark mode)
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Semantic HTML structure with proper heading hierarchy
|
||||
- All logos include descriptive alt text
|
||||
- Clickable logos have proper link semantics
|
||||
- Keyboard navigation support with visible focus states
|
||||
- ARIA labels provided through Button component
|
||||
- Color contrast meets WCAG AA standards in all variants
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use Each Variant
|
||||
|
||||
- **Gray**: General-purpose logo grids, subtle integration
|
||||
- **Green**: Featured partnerships, brand-focused sections
|
||||
|
||||
### Content Guidelines
|
||||
|
||||
- **Heading**: Keep concise (1 line preferred), use sentence case
|
||||
- **Description**: Provide context (2-3 lines max), complete sentences
|
||||
- **Logo Count**: Aim for multiples of 4 for visual balance on desktop
|
||||
- **Alt Text**: Use company/product names, not generic "logo"
|
||||
|
||||
### Logo Preparation
|
||||
|
||||
1. **Consistent Sizing**: Ensure all logos have similar visual weight
|
||||
2. **Format**: Use SVG for scalability and crisp rendering
|
||||
3. **Background**: Transparent backgrounds work best
|
||||
4. **Color**: Consider providing light/dark variants if needed
|
||||
5. **Padding**: Include minimal internal padding in the SVG itself
|
||||
|
||||
### Performance
|
||||
|
||||
- Use optimized SVG files (run through SVGO or similar)
|
||||
- Consider lazy loading for grids with many logos
|
||||
- Provide appropriate alt text for all images
|
||||
- Use `width` and `height` attributes on img tags when possible
|
||||
|
||||
### Technical Implementation
|
||||
|
||||
- **Grid System**: Uses PageGridCol with `span={{ base: 2, lg: 3 }}` for responsive layout (2 cols mobile, 4 cols desktop)
|
||||
- **Tile Rendering**: Leverages TileLogo component for all logo tiles
|
||||
- **Variant Mapping**: LogoSquareGrid 'gray' → TileLogo 'neutral', LogoSquareGrid 'green' → TileLogo 'green'
|
||||
- **Interactive States**: TileLogo handles href (links), onClick (buttons), and disabled states
|
||||
- **Aspect Ratio**: Square tiles maintained by TileLogo with CSS `aspect-ratio: 1`
|
||||
- **Animations**: Window shade hover effect managed by TileLogo component
|
||||
- **Button Layout**: Uses shared ButtonGroup component with `gap="small"` (4px on tablet+)
|
||||
|
||||
## Files
|
||||
|
||||
- `LogoSquareGrid.tsx` - Component implementation
|
||||
- `LogoSquareGrid.scss` - Styles with color variants and responsive breakpoints
|
||||
- `index.ts` - Barrel exports
|
||||
- `README.md` - This documentation
|
||||
|
||||
## Related Components
|
||||
|
||||
- **TileLogo**: Core component used to render individual logo tiles with interactive states
|
||||
- **ButtonGroup**: Shared pattern used for responsive button layout in the header
|
||||
- **PageGrid**: Used internally for responsive grid structure and standard container support
|
||||
|
||||
## Design References
|
||||
|
||||
- **Figma Design**: [Pattern Logo - Square Grid](https://www.figma.com/design/ThBcoYLNKsBGw3r9g1L6Z8/Pattern-Logo---Square-Grid?node-id=1-2)
|
||||
- **Showcase Page**: `/about/logo-square-grid-showcase.page.tsx`
|
||||
- **Component Location**: `shared/patterns/LogoSquareGrid/`
|
||||
|
||||
## Version History
|
||||
|
||||
- **January 2026**: Initial implementation
|
||||
- Figma design alignment with 2 color variants
|
||||
- Responsive grid with 2/4 column layout
|
||||
- Optional header section with buttons
|
||||
- Clickable logo support
|
||||
- Refactored to use shared ButtonGroup component for button layout
|
||||
2
shared/patterns/LogoSquareGrid/index.ts
Normal file
2
shared/patterns/LogoSquareGrid/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { LogoSquareGrid } from './LogoSquareGrid';
|
||||
export type { LogoSquareGridProps, LogoItem } from './LogoSquareGrid';
|
||||
@@ -5332,7 +5332,7 @@ textarea.form-control-lg {
|
||||
display: table-cell !important;
|
||||
}
|
||||
|
||||
.d-flex, .bds-callout-media-banner__actions, .bds-callout-media-banner__text, .bds-callout-media-banner__content, .bds-callout-media-banner > [class*=bds-grid__col], .bds-callout-media-banner {
|
||||
.d-flex, .bds-logo-square-grid__text, .bds-logo-square-grid__header, .bds-logo-square-grid, .bds-callout-media-banner__text, .bds-callout-media-banner__content, .bds-callout-media-banner > [class*=bds-grid__col], .bds-callout-media-banner, .bds-button-group {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
@@ -5646,7 +5646,7 @@ textarea.form-control-lg {
|
||||
width: 75% !important;
|
||||
}
|
||||
|
||||
.w-100, .bds-callout-media-banner__content, .bds-callout-media-banner {
|
||||
.w-100, .bds-logo-square-grid, .bds-callout-media-banner__content, .bds-callout-media-banner {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
@@ -5706,7 +5706,7 @@ textarea.form-control-lg {
|
||||
flex-direction: row !important;
|
||||
}
|
||||
|
||||
.flex-column, .bds-callout-media-banner__actions, .bds-callout-media-banner__text, .bds-callout-media-banner__content, .bds-callout-media-banner > [class*=bds-grid__col] {
|
||||
.flex-column, .bds-logo-square-grid__text, .bds-logo-square-grid__header, .bds-logo-square-grid, .bds-callout-media-banner__text, .bds-callout-media-banner__content, .bds-callout-media-banner > [class*=bds-grid__col], .bds-button-group {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
@@ -5734,7 +5734,7 @@ textarea.form-control-lg {
|
||||
flex-shrink: 1 !important;
|
||||
}
|
||||
|
||||
.flex-wrap, .bds-callout-media-banner__actions {
|
||||
.flex-wrap, .bds-button-group {
|
||||
flex-wrap: wrap !important;
|
||||
}
|
||||
|
||||
@@ -5770,7 +5770,7 @@ textarea.form-control-lg {
|
||||
justify-content: space-evenly !important;
|
||||
}
|
||||
|
||||
.align-items-start, .bds-callout-media-banner__actions {
|
||||
.align-items-start, .bds-button-group {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
@@ -11295,7 +11295,7 @@ aside .active-parent > a {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.w-100, .bds-callout-media-banner__content, .bds-callout-media-banner {
|
||||
.w-100, .bds-logo-square-grid, .bds-callout-media-banner__content, .bds-callout-media-banner {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -13313,11 +13313,6 @@ html.dark .bds-btn--tertiary:not(.bds-btn--black):disabled, html.dark .bds-btn--
|
||||
top: -0.4em;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
.bds-card-stat__statistic sup.bds-card-stat__superscript--numeric {
|
||||
font-size: 0.5em;
|
||||
top: -0.75em;
|
||||
font-weight: 400;
|
||||
}
|
||||
.bds-card-stat__buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -19572,6 +19567,120 @@ html.light .bds-card-icon--disabled.bds-card-icon--green .bds-card-icon__icon-im
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
|
||||
.bds-header-hero-primary-media {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-header-hero-primary-media {
|
||||
padding-top: 32px;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-header-hero-primary-media {
|
||||
padding-top: 170px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
}
|
||||
html.light .bds-header-hero-primary-media {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
html.dark .bds-header-hero-primary-media {
|
||||
background-color: #141414;
|
||||
}
|
||||
.bds-header-hero-primary-media__headline-container {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-header-hero-primary-media__headline-container {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
.bds-header-hero-primary-media__headline {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 100%;
|
||||
}
|
||||
.bds-header-hero-primary-media__headline * {
|
||||
max-width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
.bds-header-hero-primary-media__subtitle {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html.light .bds-header-hero-primary-media__subtitle {
|
||||
color: #72777E;
|
||||
}
|
||||
.bds-header-hero-primary-media__cta-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
justify-content: flex-end;
|
||||
min-height: 100%;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-header-hero-primary-media__cta-container {
|
||||
gap: 40px;
|
||||
}
|
||||
}
|
||||
.bds-header-hero-primary-media__cta-buttons {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-header-hero-primary-media__cta-buttons {
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-header-hero-primary-media__cta-buttons {
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
.bds-header-hero-primary-media__cta-buttons .bds-btn--tertiary {
|
||||
padding: 0;
|
||||
}
|
||||
.bds-header-hero-primary-media__cta-buttons .bds-btn--tertiary:hover, .bds-header-hero-primary-media__cta-buttons .bds-btn--tertiary:focus-visible, .bds-header-hero-primary-media__cta-buttons .bds-btn--tertiary:focus {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.bds-header-hero-primary-media__media-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 16/9;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin-top: 24px;
|
||||
height: auto;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-header-hero-primary-media__media-container {
|
||||
margin-top: 32px;
|
||||
aspect-ratio: 2/1;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-header-hero-primary-media__media-container {
|
||||
margin-top: 40px;
|
||||
aspect-ratio: 3/1;
|
||||
}
|
||||
}
|
||||
.bds-header-hero-primary-media__media-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.bds-hero-split-media {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -19865,6 +19974,301 @@ html.light .bds-hero-split-media--accent .bds-hero-split-media__subtitle {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.bds-button-group {
|
||||
gap: 8px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-button-group {
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.bds-button-group--gap-none {
|
||||
gap: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.bds-button-group--gap-small {
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-callout-media-banner {
|
||||
box-sizing: border-box;
|
||||
min-height: 280px;
|
||||
}
|
||||
.bds-callout-media-banner {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
padding: 16px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-callout-media-banner {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-callout-media-banner {
|
||||
padding: 40px 32px;
|
||||
min-height: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-callout-media-banner__content {
|
||||
gap: 48px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-callout-media-banner__content {
|
||||
gap: 64px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.bds-callout-media-banner__content {
|
||||
gap: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-callout-media-banner--centered .bds-callout-media-banner__content {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.bds-callout-media-banner__text {
|
||||
flex-direction: column;
|
||||
color: var(--bds-cmb-text-color, #232021);
|
||||
gap: 16px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-callout-media-banner__text {
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.bds-callout-media-banner__text {
|
||||
gap: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-callout-media-banner__heading {
|
||||
font-family: "Tobias", "Noto Serif", monospace;
|
||||
font-weight: 300;
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
letter-spacing: 0px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-callout-media-banner__heading {
|
||||
font-size: 36px;
|
||||
line-height: 45px;
|
||||
letter-spacing: -0.5px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-callout-media-banner__heading {
|
||||
font-size: 40px;
|
||||
line-height: 46px;
|
||||
letter-spacing: -1px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
.bds-callout-media-banner__heading {
|
||||
margin: 0;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.bds-callout-media-banner__subheading {
|
||||
font-family: "Booton", "Noto Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
letter-spacing: -1px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-callout-media-banner__subheading {
|
||||
font-size: 28px;
|
||||
line-height: 35px;
|
||||
letter-spacing: -0.75px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-callout-media-banner__subheading {
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
letter-spacing: -0.5px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
.bds-callout-media-banner__subheading {
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.bds-callout-media-banner--default {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.bds-callout-media-banner--light-gray {
|
||||
background-color: #E6EAF0;
|
||||
}
|
||||
|
||||
.bds-callout-media-banner--lilac {
|
||||
background-color: #C0A7FF;
|
||||
}
|
||||
|
||||
.bds-callout-media-banner--green {
|
||||
background-color: #70EE97;
|
||||
}
|
||||
|
||||
.bds-callout-media-banner--gray {
|
||||
background-color: #CAD4DF;
|
||||
}
|
||||
|
||||
.bds-callout-media-banner--image {
|
||||
background-color: transparent;
|
||||
}
|
||||
.bds-callout-media-banner--image::before {
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.2) 50%, rgba(0, 0, 0, 0.1) 100%);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bds-callout-media-banner--image-text-black::before {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.1) 100%);
|
||||
}
|
||||
.bds-callout-media-banner--image-text-black .bds-callout-media-banner__text {
|
||||
color: #141414 !important;
|
||||
}
|
||||
|
||||
html.dark .bds-callout-media-banner--default {
|
||||
background-color: #232325;
|
||||
}
|
||||
html.dark .bds-callout-media-banner--default .bds-callout-media-banner__text {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
html.dark .bds-callout-media-banner--light-gray {
|
||||
background-color: #343437;
|
||||
}
|
||||
html.dark .bds-callout-media-banner--light-gray .bds-callout-media-banner__text {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
html.dark .bds-callout-media-banner--lilac {
|
||||
background-color: #7649E3;
|
||||
}
|
||||
html.dark .bds-callout-media-banner--lilac .bds-callout-media-banner__text {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
html.dark .bds-callout-media-banner--green {
|
||||
background-color: #21E46B;
|
||||
}
|
||||
html.dark .bds-callout-media-banner--green .bds-callout-media-banner__text {
|
||||
color: #141414;
|
||||
}
|
||||
html.dark .bds-callout-media-banner--gray {
|
||||
background-color: #454549;
|
||||
}
|
||||
html.dark .bds-callout-media-banner--gray .bds-callout-media-banner__text {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
html.dark .bds-callout-media-banner--image .bds-callout-media-banner__text {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
html.dark .bds-callout-media-banner--image-text-black .bds-callout-media-banner__text {
|
||||
color: #141414 !important;
|
||||
}
|
||||
|
||||
html.light .bds-callout-media-banner--default {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
html.light .bds-callout-media-banner--default .bds-callout-media-banner__text {
|
||||
color: #141414;
|
||||
}
|
||||
html.light .bds-callout-media-banner--light-gray {
|
||||
background-color: #E6EAF0;
|
||||
}
|
||||
html.light .bds-callout-media-banner--light-gray .bds-callout-media-banner__text {
|
||||
color: #141414;
|
||||
}
|
||||
html.light .bds-callout-media-banner--lilac {
|
||||
background-color: #C0A7FF;
|
||||
}
|
||||
html.light .bds-callout-media-banner--lilac .bds-callout-media-banner__text {
|
||||
color: #141414;
|
||||
}
|
||||
html.light .bds-callout-media-banner--green {
|
||||
background-color: #70EE97;
|
||||
}
|
||||
html.light .bds-callout-media-banner--green .bds-callout-media-banner__text {
|
||||
color: #141414;
|
||||
}
|
||||
html.light .bds-callout-media-banner--gray {
|
||||
background-color: #CAD4DF;
|
||||
}
|
||||
html.light .bds-callout-media-banner--gray .bds-callout-media-banner__text {
|
||||
color: #141414;
|
||||
}
|
||||
html.light .bds-callout-media-banner--image .bds-callout-media-banner__text {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
html.light .bds-callout-media-banner--image-text-black .bds-callout-media-banner__text {
|
||||
color: #141414 !important;
|
||||
}
|
||||
|
||||
.bds-logo-square-grid {
|
||||
gap: 24px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-logo-square-grid {
|
||||
gap: 32px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-logo-square-grid {
|
||||
gap: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-logo-square-grid__header {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 24px;
|
||||
gap: 24px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-logo-square-grid__header {
|
||||
gap: 32px;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-logo-square-grid__header {
|
||||
gap: 40px;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-logo-square-grid__text {
|
||||
gap: 8px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-logo-square-grid__text {
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-cards-featured {
|
||||
width: 100%;
|
||||
padding-top: 48px;
|
||||
@@ -20072,17 +20476,6 @@ html.dark .bds-cards-featured__description {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.bds-callout-media-banner__actions {
|
||||
gap: 8px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-callout-media-banner__actions {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-callout-media-banner--default {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
@@ -20199,102 +20592,6 @@ html.light .bds-callout-media-banner--image-text-black .bds-callout-media-banner
|
||||
color: #141414 !important;
|
||||
}
|
||||
|
||||
.bds-card-stats {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background-color: #FFFFFF;
|
||||
padding-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-card-stats {
|
||||
padding-top: 32px;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-card-stats {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-card-stats__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-width: 808px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-card-stats__header {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-card-stats__header {
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
.bds-card-stats__header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-card-stats__header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-card-stats__header {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-card-stats__heading {
|
||||
color: #141414;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bds-card-stats__description {
|
||||
color: #141414;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bds-card-stats__cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-card-stats__cards {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.bds-card-stats__card-wrapper {
|
||||
flex: 0 0 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.bds-card-stats__card-wrapper {
|
||||
flex: 0 0 calc(50% - 4px);
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.bds-card-stats__card-wrapper {
|
||||
flex: 0 0 calc(33.333% - 5.3333333333px);
|
||||
}
|
||||
}
|
||||
|
||||
html.dark .bds-card-stats {
|
||||
background-color: transparent;
|
||||
}
|
||||
html.dark .bds-card-stats__heading,
|
||||
html.dark .bds-card-stats__description {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.bds-feature-two-column__button-group .bds-btn--tertiary {
|
||||
padding-top: 0px !important;
|
||||
padding-bottom: 0px !important;
|
||||
|
||||
@@ -98,10 +98,13 @@ $line-height-base: 1.5;
|
||||
@import "../shared/components/TileLogo/TileLogo.scss";
|
||||
@import "../shared/components/CardIcon/CardIcon.scss";
|
||||
@import "../shared/components/SmallTilesSection/_small-tiles-section.scss";
|
||||
@import "../shared/patterns/HeaderHeroPrimaryMedia/_header-hero-primary-media.scss";
|
||||
@import "../shared/patterns/HeaderHeroSplitMedia/HeaderHeroSplitMedia.scss";
|
||||
@import "../shared/patterns/ButtonGroup/ButtonGroup.scss";
|
||||
@import "../shared/patterns/CalloutMediaBanner/CalloutMediaBanner.scss";
|
||||
@import "../shared/patterns/LogoSquareGrid/LogoSquareGrid.scss";
|
||||
@import "../shared/patterns/CardsFeatured/CardsFeatured.scss";
|
||||
@import "../shared/patterns/CalloutMediaBanner/CalloutMediaBanner.scss";
|
||||
@import "../shared/patterns/CardStats/CardStats.scss";
|
||||
@import "../shared/patterns/FeatureTwoColumn/FeatureTwoColumn.scss";
|
||||
@import "_code-tabs.scss";
|
||||
@import "_code-walkthrough.scss";
|
||||
|
||||
Reference in New Issue
Block a user