Compare commits

..

8 Commits

Author SHA1 Message Date
gabriel-ortiz
8a2ff6e69f Update padding for CTA buttons in HeaderHeroPrimaryMedia component
- Added !important to padding for hover and focus states of tertiary buttons to ensure consistent styling across interactions.
2026-01-22 18:08:30 -08:00
gabriel-ortiz
6bce7efae0 Merge pull request #3450 from XRPLF/go/xrpl-brand-update/PrimaryHero
Add HeaderHeroPrimaryMedia component and styles
2026-01-22 14:15:03 -08:00
Calvin
99d3442bef Merge pull request #3451 from XRPLF/pattern/logo-square-grid
Pattern/Logo Square Grid
2026-01-21 15:51:58 -08:00
Calvin Jhunjhuwala
e66a877868 merging master 2026-01-21 15:20:22 -08:00
gabriel-ortiz
b085502a4d Refactor HeaderHeroPrimaryMedia styles and component structure
- Updated SCSS styles for improved layout and responsiveness, including adjustments to padding and margin for various breakpoints.
- Enhanced the HeaderHeroPrimaryMedia component by refining the media rendering logic and updating prop types for better type safety.
- Changed subtitle class from 'label-l' to 'body-l' for consistent styling.
- Ensured proper handling of empty values in the isEmpty utility function.
2026-01-20 19:54:31 -08:00
Calvin Jhunjhuwala
f7c80a5c04 fixing size on tablet for text 2026-01-19 15:49:22 -08:00
Calvin Jhunjhuwala
1e61c71c94 logo square grid ready for review 2026-01-17 17:52:46 -08:00
gabriel-ortiz
bf88924d3d Add HeaderHeroPrimaryMedia component and styles
- Introduced HeaderHeroPrimaryMedia component for a responsive hero section featuring a headline, subtitle, call-to-action buttons, and media elements (images, videos, or custom React components).
- Implemented associated SCSS styles to ensure proper layout and design constraints, including aspect ratios and object-fit properties.
- Added README documentation detailing usage, props, and best practices for the component.
- Included validation for required props with console warnings for missing fields.
2026-01-16 17:47:10 -08:00
41 changed files with 3254 additions and 974 deletions

View File

@@ -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のマネーロンダリング、詐欺、詐欺、不正使用を追跡し、最小限に抑えるための勧告リストを維持しています。取引所やその他のサービス・プロバイダは、金融犯罪を防止し対応するためにこのサービスを利用することができます。
## セキュリティ上の懸念

View File

@@ -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つだけ(または少数)使用するだけで、ユーザの支払いを区別することができます。

View File

@@ -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" /%}

View File

@@ -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 %}

View File

@@ -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プロトコル-コンセンサスと検証)に関するドキュメントをご覧ください)。

View File

@@ -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:
- ブロックチェーン
---

View File

@@ -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もの準備金が必要になります。
| 準備金の種類 | 準備金の額 |
|:--------------------|--------:|

View File

@@ -72,7 +72,7 @@ labels:
### `NFTokenOffer`の準備金
`NFTokenOffer`オブジェクトは、オファーを出すアカウントに1つ分の準備金の増額を要求します。執筆時点では、準備金の増分は{% $env.PUBLIC_OWNER_RESERVE %}です。この準備金は、オファーをキャンセルすることで取り戻すことができます。
`NFTokenOffer`オブジェクトは、オファーを出すアカウントに1つ分の準備金の増額を要求します。執筆時点では、準備金の増分は2XRPです。この準備金は、オファーをキャンセルすることで取り戻すことができます。
### `NFTokenOfferID`のフォーマット

View File

@@ -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トランザクション][]と同じ特別なトランザクションコストです。
## エラーケース

View File

@@ -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が必要となります。必要額は、リストの署名者の数に応じて増加します。

View File

@@ -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)をご覧ください。

View File

@@ -48,9 +48,9 @@ NFTをオークション形式で販売することができます。[NFTオー
### 準備金要件
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。NFTokenページは1632個のNFTを保管することができます。
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、2XRPの準備金が必要です。NFTokenページは1632個のNFTを保管することができます。
`NFTokenOffer`オブジェクトは、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。
`NFTokenOffer`オブジェクトは、2XRPの準備金が必要です。
`NFTokenOffer`を作成したり、NFTを売却したりする際には、些細な送金手数料およそ6000ドロップ、または0.006 XRPが発生します。大量に販売する場合、こうした少額の手数料はすぐにかさみますので、ビジネスのコストとして考慮する必要があります。

View File

@@ -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」は1632個の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ページは1632個のNFTを保管することができます。
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、2XRPの準備金が必要です。NFTokenページは1632個のNFTを保管することができます。
`NFTokenOffer`オブジェクトは、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。
`NFTokenOffer`オブジェクトは、2XRPの準備金が必要です。
`NFTokenOffer`を作成したり、NFTを売却したりする際には、些細な送金手数料およそ6000ドロップ、または0.006 XRPが発生します。大量に販売する場合、こうした少額の手数料はすぐにかさみますので、ビジネスのコストとして考慮する必要があります。

View File

@@ -44,9 +44,9 @@ NFTをオークション形式で販売することができます。[NFTオー
### 準備金要件
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。NFTokenページは1632個のNFTを保管することができます。
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、2XRPの準備金が必要です。NFTokenページは1632個のNFTを保管することができます。
`NFTokenOffer`オブジェクトは、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。
`NFTokenOffer`オブジェクトは、2XRPの準備金が必要です。
`NFTokenOffer`を作成したり、NFTを売却したりする際には、些細な送金手数料およそ6000ドロップ、または0.006 XRPが発生します。大量に販売する場合、こうした少額の手数料はすぐにかさみますので、ビジネスのコストとして考慮する必要があります。

View File

@@ -6,7 +6,7 @@ metadata:
---
# リソース
XRP Ledgerの理解や開発ためのリソース。
XRP Ledgerの理解や開発ためのリソース。Other resources to help understand the XRPL and develop on it.
{% child-pages /%}

View File

@@ -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>
);
}

View 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>&lt;header&gt;</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>&lt;img&gt;</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>&lt;video&gt;</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>
);
}

File diff suppressed because one or more lines are too long

View File

@@ -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:

View File

@@ -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.

View File

@@ -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;

View File

@@ -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>
)}

View 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;
}
}

View 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;

View 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)

View File

@@ -0,0 +1,2 @@
export { ButtonGroup } from './ButtonGroup';
export type { ButtonGroupProps, ButtonConfig } from './ButtonGroup';

View File

@@ -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)

View File

@@ -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>

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -1,3 +0,0 @@
export { CardStats, type CardStatsProps, type CardStatsCardConfig } from './CardStats';
export { default } from './CardStats';

View File

@@ -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;

View 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

View File

@@ -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;
}
}

View 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

View 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;

View 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

View File

@@ -0,0 +1,2 @@
export { LogoSquareGrid } from './LogoSquareGrid';
export type { LogoSquareGridProps, LogoItem } from './LogoSquareGrid';

View File

@@ -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;

View File

@@ -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";