Compare commits

..

95 Commits

Author SHA1 Message Date
Calvin Jhunjhuwala
acb2476d7d tweaks to pattern and section 2026-02-07 10:55:48 -08:00
Calvin Jhunjhuwala
f5c38ffe77 cleaning up key 2026-02-06 23:51:37 -08:00
Calvin Jhunjhuwala
ee6a32d159 adding TileLinks pattern, adding LinkSmallGrid 2026-02-06 23:37:57 -08:00
Calvin
e1d18bd621 Merge pull request #3480 from XRPLF/section/feature-single-topic
Add FeatureSingleTopic pattern component with associated styles
2026-02-05 09:59:59 -08:00
Calvin Jhunjhuwala
a85dc47781 fix button utils, remove separate file, enhance within main BUttonGroup file 2026-02-05 09:48:40 -08:00
Calvin Jhunjhuwala
eecd14d763 adding button group utils for validation, cleaning up styling for FeatureSingleTopic 2026-02-04 16:20:03 -08:00
Calvin Jhunjhuwala
42282a2012 Merge branch 'xrpl-brand-update-2026' of github.com:XRPLF/xrpl-dev-portal into section/feature-single-topic 2026-02-04 14:57:33 -08:00
akcodez
6442318205 Enhance FeatureSingleTopic component with button variant support and responsive behavior updates
- Updated the FeatureSingleTopic component to allow configurable button variants (primary or secondary) based on the `singleButtonVariant` prop.
- Improved mobile and tablet responsiveness by ensuring content always appears above the image, regardless of orientation.
- Refined button rendering logic for better clarity in the README and component documentation.
- Adjusted SCSS styles for improved spacing and alignment across different screen sizes.
2026-02-04 11:29:56 -08:00
Calvin
1ee76bfbea Merge pull request #3482 from XRPLF/quick-fix-020426
fix back to html.light, will consolidate later
2026-02-04 11:24:33 -08:00
Calvin Jhunjhuwala
d558b7474d fix back to html.light, will consolidate later 2026-02-04 11:22:59 -08:00
Calvin
da49b0a154 Merge pull request #3477 from XRPLF/section/carousel-feature-image
Add CarouselButton component and integrate into CarouselCardList and …
2026-02-04 11:19:14 -08:00
Calvin Jhunjhuwala
01931bd177 stylesheet clean up 2026-02-04 11:14:42 -08:00
Calvin Jhunjhuwala
c2ef761b01 cleaning up the files, removing dead css, leveraging the grid more 2026-02-03 16:26:47 -08:00
akcodez
d6ce246420 Add FeatureSingleTopic pattern component with associated styles
- Introduced the FeatureSingleTopic component, enhancing layout flexibility with responsive design.
- Added SCSS styles for various screen sizes, including media queries for improved spacing and alignment.
- Implemented dark mode styles for better accessibility and visual consistency.
2026-02-03 13:29:24 -08:00
akcodez
33c6315510 Refactor CarouselCardList and CarouselFeatured to streamline button rendering logic using a map function for navigation buttons, enhancing code maintainability and consistency across components. 2026-02-03 10:40:08 -08:00
akcodez
af0b8cd40a Refactor CarouselFeatured to use ButtonGroup for button management, simplifying button rendering logic and improving code maintainability. 2026-02-03 10:38:45 -08:00
akcodez
95c4ffaa1b merge buttongroup in 2026-02-03 10:29:25 -08:00
Calvin
08c5572f16 Merge pull request #3475 from XRPLF/go/feat/FeaturedVideoSection
Add FeaturedVideoHero pattern component with showcase and styles
2026-02-03 10:25:12 -08:00
Calvin Jhunjhuwala
b49bc02dd2 fixing button and button groups for feature 2 column 2026-02-02 17:03:04 -08:00
Calvin Jhunjhuwala
65a61c5e47 fixing spacing and alignments 2026-02-02 16:20:50 -08:00
Calvin Jhunjhuwala
237ddc3c74 cleaning up styles, aligning text to bottom for section 2026-02-02 15:57:20 -08:00
Calvin
dd6cfd34fe Merge pull request #3474 from XRPLF/pattern/button-group
Add ButtonGroup pattern component
2026-02-02 14:30:26 -08:00
akcodez
607f8bdf07 Add CarouselButton component and integrate into CarouselCardList and CarouselFeatured showcases
- Introduced CarouselButton component for navigation with multiple color variants (neutral, green, black).
- Enhanced CarouselCardListShowcase to include a visual showcase of CarouselButton in various states.
- Created CarouselFeatured component showcasing featured images with navigation and responsive behavior.
- Updated styles for CarouselButton and integrated it into existing patterns for improved navigation experience.
2026-02-02 13:56:00 -08:00
Calvin Jhunjhuwala
7b601da3a0 updating to add more warnings/dev feedback 2026-02-02 13:20:12 -08:00
Aria Keshmiri
dc85b8c241 Merge pull request #3472 from XRPLF/navbar/updates
Update navigation constants and submenu data for improved routing
2026-02-02 08:59:34 -08:00
akcodez
0a25b1b9c0 hide scroll on open mobile menu 2026-02-02 08:59:13 -08:00
gabriel-ortiz
6021b458e6 Add FeaturedVideoHero pattern component with showcase and styles
- Introduced the FeaturedVideoHero component featuring a responsive layout with a headline, optional subtitle, call-to-action buttons, and a video element.
- Added associated SCSS styles for the component to ensure proper spacing and theming.
- Created a showcase page to demonstrate the usage of the FeaturedVideoHero pattern.
- Updated types to include design constraints for buttons and video elements.
- Implemented validation for required props to enhance development experience.
2026-01-30 13:47:38 -08:00
Calvin Jhunjhuwala
e5f3bf75e3 minor add for maxnumber 2026-01-30 10:30:53 -08:00
Calvin Jhunjhuwala
7dd32d63da working button group, updated examples 2026-01-29 16:50:32 -08:00
akcodez
7376dce9ef Update navigation constants and submenu data for improved routing
- Changed hrefs for main navigation items to reflect new paths.
- Updated submenu data for 'Develop' and 'Use Cases' sections with new links.
- Adjusted community submenu links for better resource access.
- Refined network submenu data to include relevant resources and insights.
2026-01-29 10:21:57 -08:00
Aria Keshmiri
ce9012f26f Merge pull request #3465 from XRPLF/section/cards-two-column
Add CardsTwoColumn pattern styles and integrate into XRPL styles
2026-01-29 09:51:13 -08:00
akcodez
9042a60b28 Refactor TextCard component structure and styles
- Removed unnecessary wrapper elements for header and footer in TextCard, simplifying the markup.
- Updated class names in SCSS for better clarity and consistency.
- Enhanced CardsTwoColumnProps documentation to specify that description and secondaryDescription can be either string or ReactNode.
- Cleaned up CSS by removing redundant vendor prefixes for better maintainability.
2026-01-29 09:50:59 -08:00
akcodez
9ff586b172 merge main branch in 2026-01-29 09:46:07 -08:00
Aria Keshmiri
a082d9030c Merge pull request #3457 from XRPLF/section/card-stats
Add Section Card Stat + slight optimizations to card stat component
2026-01-28 15:02:03 -08:00
akcodez
9814a24dcf merge 2026-01-28 15:01:49 -08:00
Aria Keshmiri
05c36beae2 Merge pull request #3454 from XRPLF/section/carousel-card-list
Add CarouselCardList pattern component and showcase
2026-01-28 15:00:03 -08:00
akcodez
41e01b51bd Refactor CarouselCardList component to improve code clarity and maintainability
- Introduced a constant for the BEM class name to enhance readability.
- Updated buttonVariant prop type to derive from ButtonProps for consistency.
- Replaced hardcoded class name with the new constant in the component's render method.
2026-01-28 14:59:46 -08:00
akcodez
4c2b2d487c merg 2026-01-28 14:58:33 -08:00
gabriel-ortiz
dec76b1f71 Merge pull request #3467 from XRPLF/go/feat/cardStandardSection
Add StandardCardGroupSection pattern component and related styles
2026-01-28 14:53:41 -08:00
gabriel-ortiz
b26b185c04 Merge branch 'xrpl-brand-update-2026' into go/feat/cardStandardSection 2026-01-28 14:52:55 -08:00
akcodez
fb7707c6b6 Update TextCard and CardsTwoColumn documentation to reflect new features 2026-01-28 14:48:51 -08:00
akcodez
ee80283265 Add disabled state to TextCard component and update showcase
- Implemented a disabled state for the TextCard component, enhancing accessibility and user experience.
- Updated the CardsTwoColumnShowcase to include examples of disabled TextCards in both light and dark modes.
- Refined styles for the disabled state to ensure visual clarity and consistency across themes.
- Enhanced documentation to reflect the new disabled functionality in the TextCard component.
2026-01-28 14:39:53 -08:00
gabriel-ortiz
a3119f9fc0 Fix typo in DesignContrainedButtonProps to DesignConstrainedButtonProps across multiple components and utility files for consistency. 2026-01-28 14:36:46 -08:00
akcodez
5c87e7e1cb Enhance TextCard component and update CardsTwoColumn pattern
- Introduced the TextCard component with 6 color variants and interactive states, including hover and pressed effects.
- Updated CardsTwoColumn pattern to utilize the new TextCard component, showcasing all 6 color variants in a responsive layout.
- Improved documentation for both TextCard and CardsTwoColumn, detailing usage, props, and responsive behavior.
- Refactored styles to ensure consistency and maintainability across components.
2026-01-28 13:53:03 -08:00
Calvin
9084d37db3 Merge pull request #3468 from XRPLF/qa-card-stats-section
CardStats Component Refactoring and Grid Integration
2026-01-28 12:43:04 -08:00
Calvin
bf7bd6fbd7 Merge pull request #3460 from XRPLF/pattern/logo-rectangle-grid
Pattern/logo rectangle grid
2026-01-28 09:22:37 -08:00
Calvin Jhunjhuwala
50df631f8a merging master 2026-01-27 21:54:28 -08:00
Calvin Jhunjhuwala
0d779b4d47 code clean up, ready for additional review 2026-01-27 21:50:04 -08:00
Calvin Jhunjhuwala
071e940e6b merging main branch 2026-01-27 18:06:41 -08:00
Calvin Jhunjhuwala
9fe2a7377f cleaning up code, consolidating and attaching the CardStat to the grid, less css 2026-01-27 17:09:59 -08:00
gabriel-ortiz
e40ea50259 Update error handling and clean up imports in StandardCardGroupSection and helpers
- Changed console warning to error for better visibility when no cards are provided in StandardCardGroupSection.
- Simplified imports in helpers by removing unused React types to enhance code clarity.
2026-01-27 14:36:05 -08:00
gabriel-ortiz
41e3e82984 Refactor StandardCard component and utility functions
- Replaced the hasChildren utility function with isEmpty for better clarity in child element checks.
- Updated SCSS styles for the StandardCard buttons to maintain consistent alignment and spacing across breakpoints.
- Removed the deprecated hasChildren function from the helpers module to streamline utility functions.
2026-01-27 14:14:37 -08:00
gabriel-ortiz
1ba6c9753d Add StandardCardGroupSection pattern component and related styles
- Introduced the StandardCardGroupSection component, which displays a headline, description, and a responsive grid of StandardCard components.
- Implemented SCSS styles for the StandardCardGroupSection, ensuring consistent spacing and dark mode support.
- Added utility functions for key generation and environment checks to enhance component functionality.
- Created a README file detailing usage, props, and best practices for the StandardCardGroupSection.
- Included new StandardCard component with customizable variants and button handling.
2026-01-27 14:05:48 -08:00
gabriel-ortiz
0fe57025c6 Merge pull request #3462 from XRPLF/go/primaryHeaderTertiaryHotfix
Update padding for CTA buttons in HeaderHeroPrimaryMedia component
2026-01-26 13:21:50 -08:00
akcodez
e43ce195a4 rm dead code 2026-01-26 13:16:09 -08:00
akcodez
c192ccec70 Add CardsTwoColumn pattern styles and integrate into XRPL styles
- Introduced new styles for the CardsTwoColumn pattern, including responsive layouts and various card designs.
- Updated the xrpl.scss file to import the new CardsTwoColumn styles, ensuring they are included in the overall styling framework.
2026-01-26 13:14:52 -08:00
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 Jhunjhuwala
df1ab88ef7 cleaning up duplicate, cleaning up button group, useMemo for rectangle grid 2026-01-21 18:41:19 -08:00
Calvin Jhunjhuwala
8f931a2a4c working ofset for large and medium 2026-01-21 17:06:57 -08:00
Calvin Jhunjhuwala
be46c362cf merging master 2026-01-21 16:02:20 -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
Calvin Jhunjhuwala
b9410305ef merging master, fixing merge conflicts 2026-01-21 15:19:30 -08:00
akcodez
d5e7fceb21 add forceCOlor 2026-01-21 12:31:36 -08:00
akcodez
7be7ad4806 Merge remote-tracking branch 'origin' into section/card-stats 2026-01-21 12:29:19 -08:00
Aria Keshmiri
7498f9820c Merge pull request #3446 from XRPLF/pattern/feature-two-column
Pattern/feature two column
2026-01-21 12:29:04 -08:00
Calvin Jhunjhuwala
9d4ed9a477 updates to logo rectangle, need to work on the offsetting 2026-01-21 11:46:18 -08:00
akcodez
7a6aab6493 design changes 2026-01-21 11:20:09 -08:00
akcodez
a992f0ddf3 Refactor CardStat component buttons and add CardStats styles
- Simplified button rendering in CardStat component by consolidating href and onClick handling.
- Introduced new styles for CardStats, including responsive design for various screen sizes and dark mode support.
- Updated SCSS imports to include CardStats styles.
2026-01-21 10:27:17 -08:00
akcodez
87c3c6ef19 fix 2026-01-21 09:58:17 -08:00
akcodez
8c5f6f79c1 update 2026-01-21 09:56:23 -08:00
akcodez
a6fde81c36 Refactor FeatureTwoColumn styles and improve button key handling
- Centralized background color variants for light and dark modes using a map structure in SCSS.
- Updated the button rendering logic to use a unique key based on link properties, enhancing performance and preventing potential key collisions.
- Improved code readability and maintainability by reducing redundancy in background color definitions.
2026-01-21 09:55:53 -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
4da20f1ac1 correct layout, working on right align next 2026-01-20 17:50:08 -08:00
Rome Reginelli
3853484deb Merge pull request #3429 from nabe3m/fix/ja-typos-and-translation-issues
[JA] Fix correct typos and translation issues in Japanese docs
2026-01-20 15:02:11 -08:00
Rome Reginelli
c9a560441f Merge pull request #3428 from nabe3m/fix/ja-reserve-constants
[JA] Fix Replace hardcoded reserve values with constants
2026-01-20 14:14:53 -08:00
Rome Reginelli
a789936ad2 Merge pull request #3427 from nabe3m/docs/add-nftoken-lsfmutable-flag
Add lsfMutable flag documentation to NFToken
2026-01-20 14:14:03 -08:00
akcodez
ef200ee737 Add CarouselCardList pattern component and showcase
- Introduced the CarouselCardList component for a horizontal scrolling card carousel with navigation buttons.
- Implemented responsive design for mobile, tablet, and desktop views.
- Added SCSS styles for light and dark mode support.
- Created a dedicated showcase page demonstrating the CarouselCardList features and usage examples.
- Included comprehensive documentation for props and variants.
2026-01-20 12:41:58 -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
Rome Reginelli
5455108464 Merge pull request #3447 from XRPLF/rr-update-docker-instructions
Update Docker instructions based on testing
2026-01-15 16:58:06 -08:00
akcodez
3873ae0085 design QA and changes with Design team 2026-01-15 11:23:24 -08:00
Rome Reginelli
2b1216012e Update Docker instructions based on testing
I tried out this tutorial today and managed to complete it successfully, but I have some suggestions to help with some hiccups I encountered along the way.

Minor edit re: each node steps for validators.txt

Fix private network fixes
2026-01-14 15:39:10 -08:00
akcodez
f62c99b387 fix tablet 2026-01-14 14:07:59 -08:00
akcodez
7383bf8044 add back bundle 2026-01-14 14:05:49 -08:00
akcodez
e12b1bf8dc complete pattern and showcase 2026-01-14 14:04:58 -08:00
akcodez
0b52e5f747 Add FeatureTwoColumn pattern and button enhancements
- Introduced the FeatureTwoColumn pattern for showcasing features in a two-column layout, supporting multiple color themes and responsive design.
- Implemented button behavior based on the number of links, with configurations for 1 to 5 links.
- Added `forceColor` prop to Button component to maintain button color across light and dark themes, particularly useful for colored backgrounds.
- Updated styles and documentation for both the FeatureTwoColumn pattern and Button component to reflect new features and usage guidelines.
2026-01-14 14:04:17 -08:00
nabe3m
8f853ffb0b fix: add spaces 2026-01-11 12:02:37 +09:00
nabe3m
ba7a756e39 fix: flag value 2026-01-11 11:57:44 +09:00
nabe3m
24ba1687f9 fix: correct typos and translation issues in Japanese docs 2025-12-23 02:18:23 +09:00
nabe3m
e4aa7010d9 Fix replace hardcoded base reserve with dynamic variable 2025-12-23 01:14:24 +09:00
nabe3m
4eeb2d2d49 fix(ja): Replace hardcoded reserve values with constants 2025-12-20 20:57:39 +09:00
nabe3m
e4cdb7ccea docs: add lsfMutable flag documentation to NFToken
Add documentation for the lsfMutable flag (0x00000010) which allows
updating the URI field of an NFToken using the NFTokenModify transaction.
2025-12-20 11:42:09 +09:00
132 changed files with 18892 additions and 4941 deletions

View File

@@ -124,9 +124,9 @@ XRP Ledgerは、スパム対策として、需要に基づいて[トランザク
XRP Ledgerネットワークはオープンネットワークであり、すべての取引はオープンに公開されています。
Rippleは Ledgerネットワーク全体のAMLフラグを監視・報告し、該当する疑わしい活動をFinCENに報告することにコミットしています。
Rippleは XRP 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)(現在2XRP)に相当する特別な[トランザクションコスト][]を支払う必要があります。
- トランザクションの送信時、少なくとも1つ分の[所有者準備金](reserves.md)(現在{% $env.PUBLIC_OWNER_RESERVE %})に相当する特別な[トランザクションコスト][]を支払う必要があります。
## 削除コスト
{% admonition type="warning" name="注意" %}アカウントの削除要件を満たしていないためにトランザクションが失敗した場合でも、[AccountDeleteトランザクション][]のトランザクションコストは、トランザクションが検証済みレジャーに含まれる場合常に発生します。アカウントを削除できなかった場合に高いトランザクションコストを支払う可能性を減らすには、AccountDeleteトランザクションを送信するときに`fail_hard`オプションを使用してください。{% /admonition %}
ビットコインや他の多くの暗号通貨とは異なり、XRP Ledgerの公開レジャーチェーンのそれぞれの新しいレジャーバージョンは、レジャーの完全な状態を含んでおり、新しいアカウントが増えるごとにサイズが増加します。そのため、必要な場合を除き、新しいXRP Ledgerアカウントを作成すべきではありません。アカウントを削除することで、アカウントの10XRPの[準備金](reserves.md)の一部を回復することができますが、そのためには少なくとも2XRPを破棄する必要があります。
ビットコインや他の多くの暗号通貨とは異なり、XRP Ledgerの公開レジャーチェーンのそれぞれの新しいレジャーバージョンは、レジャーの完全な状態を含んでおり、新しいアカウントが増えるごとにサイズが増加します。そのため、必要な場合を除き、新しいXRP Ledgerアカウントを作成すべきではありません。アカウントを削除することで、アカウントの{% $env.PUBLIC_BASE_RESERVE %}の[準備金](reserves.md)の一部を回復することができますが、そのためには少なくとも{% $env.PUBLIC_OWNER_RESERVE %}を破棄する必要があります。
取引所など、多くのユーザのために価値の送受信を行う組織は、[**送信元タグ**と**宛先タグ**](../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がアカウントの最低準備金現時点では10XRP以下である場合は、このアカウントを送金先に指定できます。これにより、アカウントがトランザクションを送信することも、XRPを受領することもできずに操作不可能な状態になるのを防ぎます。この場合、アカウントの所有者の準備金は関係ありません。
- アカウントのXRP残高がアカウントの最低[必要準備金](reserves.md)以下で、XRP PaymentのAmountがアカウントの最低準備金現時点では{% $env.PUBLIC_BASE_RESERVE %}以下である場合は、このアカウントを送金先に指定できます。これにより、アカウントがトランザクションを送信することも、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)(現在は10XRPを支払う必要があります。この金額のXRPは無期限に使用できなくなります。一方で、一般的な取引所では通常、顧客のXRPはすべて、共有されたいくつかのXRP Ledgerアカウントに保有されているため、顧客はその取引所で個々のアカウントの準備金を支払う必要はありません。引き出す前に、XRP Ledgerに直接アカウントを保有することが、金額に見合う価値があるかどうかを検討してください。{% /admonition %}
{% admonition type="warning" name="注意" %}自身のXRP Ledgerアドレスで初めてXRPを受け取る場合は[アカウントの準備金](reserves.md)(現在は{% $env.PUBLIC_BASE_RESERVE %}を支払う必要があります。この金額の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枚につき2XRPを確保する必要があります。 (このXRPは、チケットを使用した後に再び使用可能になります一度に多くのチケットを作成すると、このコストはすぐに膨れ上がります。{% /admonition %}
{% admonition type="warning" name="注意" %}チケットは1枚ごとに[所有者準備金](reserves.md#所有者準備金)としてカウントされますので、チケット1枚につき{% $env.PUBLIC_OWNER_RESERVE %}を確保する必要があります。 (この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枚につき2XRPを確保する必要があります。このXRPは、チケットを使用した後、再び使用することができます。
- 各チケットは[所有者準備金](reserves.md#所有者準備金)にカウントされるため、まだ使用していないチケット1枚につき{% $env.PUBLIC_OWNER_RESERVE %}を確保する必要があります。この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: Learn how the XRP Ledger Consensus Protocol is protected against various problems and attacks that may occur in a decentralized financial system. #TODO: translate
description: 分散型金融システムで発生する可能性のあるさまざまな問題や攻撃から、XRP Ledgerコンセンサスプロトコルがどのように保護されているかを学びます。
labels:
- ブロックチェーン
---

View File

@@ -12,11 +12,11 @@ NFTをミントし、保有し、販売するためには、XRPを準備金と
## 基本準備金
アカウントでは、基本準備金(現在10XRPを用意する必要があります。基本準備金のXRPの金額は変更される可能性があります。[基本準備金と所有者準備金](../../accounts/reserves.md#基本準備金と所有者準備金)をご覧ください。
アカウントでは、基本準備金(現在{% $env.PUBLIC_BASE_RESERVE %}を用意する必要があります。基本準備金のXRPの金額は変更される可能性があります。[基本準備金と所有者準備金](../../accounts/reserves.md#基本準備金と所有者準備金)をご覧ください。
## 所有者準備金
XRP Ledgerで所有する各オブジェクトには、現在2XRPの所有者準備金が必要とされています。これは、ユーザが不必要なデータで台帳にスパムをかけることを抑制し、不要になったデータを削除することを促すためのものです。所有者準備金の額は変更される可能性があります。[基本準備金と所有者準備金](../../accounts/reserves.md#基本準備金と所有者準備金)をご覧ください。
XRP Ledgerで所有する各オブジェクトには、現在{% $env.PUBLIC_OWNER_RESERVE %}の所有者準備金が必要とされています。これは、ユーザが不必要なデータで台帳にスパムをかけることを抑制し、不要になったデータを削除することを促すためのものです。所有者準備金の額は変更される可能性があります。[基本準備金と所有者準備金](../../accounts/reserves.md#基本準備金と所有者準備金)をご覧ください。
NFTの場合、 _オブジェクト_ はそれぞれのNFTを指すのではなく、アカウントが所有する`NFTokenPage`オブジェクトを指します。`NFTokenPage`オブジェクトは最大32個のNFTを格納することができます。
@@ -38,7 +38,7 @@ NFTの保有枚数や保有ページ数によって、所有者準備金の総
## `NFTokenOffer`の準備金
`NFTokenOffer`オブジェクトは、オファーを出すアカウントに対して準備金の1つの増加を必要とします。この記事の執筆時点では、準備金の増分は2XRPです。準備金は、オファーをキャンセルすることで取り戻すことができます。また、オファーが受け入れられると、XRP Ledgerからオファーが削除され、準備金は取り戻されます。
`NFTokenOffer`オブジェクトは、オファーを出すアカウントに対して準備金の1つの増加を必要とします。この記事の執筆時点では、準備金の増分は{% $env.PUBLIC_OWNER_RESERVE %}です。準備金は、オファーをキャンセルすることで取り戻すことができます。また、オファーが受け入れられると、XRP Ledgerからオファーが削除され、準備金は取り戻されます。
## Practical Considerations
@@ -55,7 +55,7 @@ NFTをミントし、保有し、売買のオファーをする場合、必要
{% admonition type="info" name="注記" %}準備金要件ではありませんが、ミントと売却のプロセスにおけるトランザクションの些細な手数料通常12drops、または.000012XRPを負担するために、少なくとも必要準備金より1XRPより多く用意しておきくべきです。{% /admonition %}
仮に200個のNFTをミントし、それぞれに「NFTokenSellOffer」を作成すると、436XRPもの準備金が必要になります。
仮に200個のNFTをミントし、それぞれに「NFTokenSellOffer」を作成すると、43.6XRPもの準備金が必要になります。
| 準備金の種類 | 準備金の額 |
|:--------------------|--------:|

View File

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

View File

@@ -48,7 +48,7 @@ AMMを表す[AMMエントリ][]と[特殊なAccountRootエントリ](../../ledge
## 特殊なトランザクションコスト
各AMMインスタンスはAccountRootレジャーエントリ、AMMレジャーエントリ、プール内の各トークンのトラストラインを含むため、AMMCreateトランザクションは台帳スパムを抑止するために通常よりもはるかに高い[トランザクションコスト][]を必要とします。標準的な最低0.00001XRPの代わりに、AMMCreateは少なくとも所有者準備金の増分(現在は2XRP)を破棄しなければなりません。これは[AccountDeleteトランザクション][]と同じ特別なトランザクションコストです。
各AMMインスタンスはAccountRootレジャーエントリ、AMMレジャーエントリ、プール内の各トークンのトラストラインを含むため、AMMCreateトランザクションは台帳スパムを抑止するために通常よりもはるかに高い[トランザクションコスト][]を必要とします。標準的な最低0.00001XRPの代わりに、AMMCreateは少なくとも所有者準備金の増分(現在は{% $env.PUBLIC_OWNER_RESERVE %})を破棄しなければなりません。これは[AccountDeleteトランザクション][]と同じ特別なトランザクションコストです。
## エラーケース

View File

@@ -17,7 +17,7 @@ labels:
- トランザクションを送信するための十分なXRPが供給されていて、新しい署名者リストの[必要準備金](../../../concepts/accounts/reserves.md)を満たしている資金供給のあるXRP Ledger[アドレス](../../../concepts/accounts/index.md)が必要です。
- [MultiSignReserve Amendment][]が有効な場合、マルチシグを使用するには、使用する署名と署名者の数に関わらず、アカウントの準備金として2 XRPが必要です。MultiSignReserve Amendmentは**2019年4月7日**以降、本番環境のXRP Ledgerで有効になっています。)
- [MultiSignReserve Amendment][]が有効な場合、マルチシグを使用するには、使用する署名と署名者の数に関わらず、アカウントの準備金として{% $env.PUBLIC_OWNER_RESERVE %}が必要です。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のプロトコルは、他のより長い、あるいはより複雑な経路のトレードを自動的に見つけて競うことはしません。(可能な限り最善の経路を見つけることは、計算集約型な問題のカテゴリとして知られています。)したがって、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)などのコストを差し引いた利益を得ることができます。
**循環支払い**を利用して、マルチアセットトレードを完了し利益を得ることができます。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)などのコストを差し引いた利益を得ることができます。
資産の価格が異なる複数の取引所(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ページには、2XRPの準備金が必要です。NFTokenページは1632個のNFTを保管することができます。
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。NFTokenページは1632個のNFTを保管することができます。
`NFTokenOffer`オブジェクトは、2XRPの準備金が必要です。
`NFTokenOffer`オブジェクトは、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。
`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つにつき2XRPの準備金が必要です。[NFT準備金](../../concepts/tokens/nfts/reserve-requirements.md)をご覧ください。
ミント済みのNFTは、`NFTokenPage`に記録されます。アカウント上の`NFTokenPage`1つにつき{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。[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ページには、2XRPの準備金が必要です。NFTokenページは1632個のNFTを保管することができます。
販売用のNFTをミントする際には、XRPの準備金が必要となります。各NFTokenページには、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。NFTokenページは1632個のNFTを保管することができます。
`NFTokenOffer`オブジェクトは、2XRPの準備金が必要です。
`NFTokenOffer`オブジェクトは、{% $env.PUBLIC_OWNER_RESERVE %}の準備金が必要です。
`NFTokenOffer`を作成したり、NFTを売却したりする際には、些細な送金手数料およそ6000ドロップ、または0.006 XRPが発生します。大量に販売する場合、こうした少額の手数料はすぐにかさみますので、ビジネスのコストとして考慮する必要があります。

View File

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

View File

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

View File

@@ -38,11 +38,9 @@ export { default as communityIcon } from "../../../../static/img/navbar/communit
export { default as insightsIcon } from "../../../../static/img/navbar/insights.svg";
export { default as resourcesIcon } from "../../../../static/img/navbar/resources.svg";
// Network submenu pattern images
export { default as resourcesPurplePattern } from "../../../../static/img/navbar/resources-purple.svg";
export { default as insightsGreenPattern } from "../../../../static/img/navbar/insights-green.svg";
export { default as darkInsightsGreenPattern } from "../../../../static/img/navbar/dark-insights-green.svg";
export { default as darkLilacPattern } from "../../../../static/img/navbar/dark-lilac.svg";
// Network submenu pattern images (used for both light and dark mode)
export { default as resourcesIconPattern } from "../../../../static/img/navbar/resources-icon.svg";
export { default as insightsIconPattern } from "../../../../static/img/navbar/insights-icon.svg";
// Wallet icon mapping for dynamic icon lookup
import greenWallet from "../../../../static/img/navbar/green-wallet.svg";

View File

@@ -10,10 +10,10 @@ export const alertBanner = {
// Main navigation items
export const navItems: NavItem[] = [
{ label: "Develop", labelTranslationKey: "navbar.develop", href: "/docs", hasSubmenu: true },
{ label: "Use Cases", labelTranslationKey: "navbar.usecases", href: "/about/uses", hasSubmenu: true },
{ label: "Develop", labelTranslationKey: "navbar.develop", href: "/develop", hasSubmenu: true },
{ label: "Use Cases", labelTranslationKey: "navbar.usecases", href: "/use-cases", hasSubmenu: true },
{ label: "Community", labelTranslationKey: "navbar.community", href: "/community", hasSubmenu: true },
{ label: "Network", labelTranslationKey: "navbar.network", href: "/docs/concepts/networks-and-servers", hasSubmenu: true },
{ label: "Network", labelTranslationKey: "navbar.network", href: "/resources", hasSubmenu: true },
];
// Develop submenu data structure
@@ -22,9 +22,9 @@ export const developSubmenuData: {
right: SubmenuItemWithChildren[];
} = {
left: [
{ label: "Developer's Home", href: "/docs", icon: "dev_home" },
{ label: "Learn", href: "/docs/tutorials", icon: "learn" },
{ label: "Code Samples", href: "/_code-samples", icon: "code_samples" },
{ label: "Developer's Home", href: "/develop", icon: "dev_home" },
{ label: "Learn", href: "https://learn.xrpl.org", icon: "learn" },
{ label: "Code Samples", href: "/resources/code-samples", icon: "code_samples" },
],
right: [
{
@@ -32,21 +32,21 @@ export const developSubmenuData: {
href: "/docs",
icon: "docs",
children: [
{ label: "API Reference", href: "/docs/references" },
{ label: "API Reference", href: "/references" },
{ label: "Tutorials", href: "/docs/tutorials" },
{ label: "Concepts", href: "/docs/concepts" },
{ label: "Concepts", href: "/concepts" },
{ label: "Infrastructure", href: "/docs/infrastructure" },
],
},
{
label: "Client Libraries",
href: "/docs/references/client-libraries",
href: "#",
icon: "client_lib",
children: [
{ label: "JavaScript", href: "/docs/references/xrpljs" },
{ label: "Python", href: "/docs/references/xrpl-py" },
{ label: "PHP", href: "/docs/references/xrpl-php" },
{ label: "Go", href: "/docs/references/xrpl-go" },
{ label: "JavaScript", href: "#" },
{ label: "Python", href: "#" },
{ label: "PHP", href: "#" },
{ label: "Go", href: "#" },
],
},
],
@@ -60,44 +60,44 @@ export const useCasesSubmenuData: {
left: [
{
label: "Payments",
href: "/about/uses/payments",
href: "/use-cases/payments",
icon: "payments",
children: [
{ label: "Direct XRP Payments", href: "/about/uses/direct-xrp-payments" },
{ label: "Cross-currency Payments", href: "/about/uses/cross-currency-payments" },
{ label: "Escrow", href: "/about/uses/escrow" },
{ label: "Checks", href: "/about/uses/checks" },
{ label: "Direct XRP Payments", href: "/use-cases/payments/direct-xrp-payments" },
{ label: "Cross-currency Payments", href: "/use-cases/payments/cross-currency-payments" },
{ label: "Escrow", href: "/use-cases/payments/escrow" },
{ label: "Checks", href: "/use-cases/payments/checks" },
],
},
{
label: "Tokenization",
href: "/about/uses/tokenization",
href: "/use-cases/tokenization",
icon: "tokenization",
children: [
{ label: "Stablecoin", href: "/about/uses/stablecoin" },
{ label: "NFT", href: "/about/uses/nft" },
{ label: "Stablecoin", href: "/use-cases/tokenization/stablecoin" },
{ label: "NFT", href: "/use-cases/tokenization/nft" },
],
},
],
right: [
{
label: "Credit",
href: "/about/uses/credit",
href: "/use-cases/credit",
icon: "credit",
children: [
{ label: "Lending", href: "/about/uses/lending" },
{ label: "Collateralization", href: "/about/uses/collateralization" },
{ label: "Sustainability", href: "/about/uses/sustainability" },
{ label: "Lending", href: "/use-cases/credit/lending" },
{ label: "Collateralization", href: "/use-cases/credit/collateralization" },
{ label: "Sustainability", href: "/use-cases/credit/sustainability" },
],
},
{
label: "Trading",
href: "/about/uses/trading",
href: "/use-cases/trading",
icon: "trading",
children: [
{ label: "DEX", href: "/about/uses/dex" },
{ label: "Permissioned Trading", href: "/about/uses/permissioned-trading" },
{ label: "AMM", href: "/about/uses/amm" },
{ label: "DEX", href: "/use-cases/trading/dex" },
{ label: "Permissioned Trading", href: "/use-cases/trading/permissioned-trading" },
{ label: "AMM", href: "/use-cases/trading/amm" },
],
},
],
@@ -115,10 +115,7 @@ export const communitySubmenuData: {
icon: "community",
children: [
{ label: "Events", href: "/community/events" },
{ label: "News", href: "/blog", active: true },
{ label: "Blog", href: "/blog" },
{ label: "Marketplace", href: "/community/marketplace" },
{ label: "Partner Connect", href: "/community/partner-connect" },
],
},
{ label: "Funding", href: "/community/developer-funding", icon: "code_samples" },
@@ -126,15 +123,14 @@ export const communitySubmenuData: {
right: [
{
label: "Contribute",
href: "/resources/contribute-documentation",
href: "/community/contribute",
icon: "client_lib",
children: [
{ label: "Ecosystem Map", href: "/community/ecosystem-map" },
{ label: "Bug Bounty", href: "/community/bug-bounty" },
{ label: "Research", href: "/community/research" },
{ label: "Bug Bounty", href: "/blog/2020/rippled-1.5.0#bug-bounties-and-responsible-disclosures" },
{ label: "Research", href: "https://xls.xrpl.org/" },
],
},
{ label: "Creators", href: "/community/ambassadors", icon: "learn" },
{ label: "Ecosystem Map", href: "/about/uses", icon: "learn" },
],
};
@@ -142,23 +138,21 @@ export const communitySubmenuData: {
export const networkSubmenuData: NetworkSubmenuSection[] = [
{
label: "Resources",
href: "/docs/concepts/networks-and-servers",
href: "/resources",
icon: "resources",
children: [
{ label: "Validators", href: "/docs/concepts/networks-and-servers/validators" },
{ label: "Governance", href: "/docs/concepts/networks-and-servers/governance", active: true },
{ label: "XRPL Roadmap", href: "/docs/concepts/networks-and-servers/xrpl-roadmap" },
{ label: "About", href: "/about/history" },
{ label: "XRPL Brand Kit", href: "/community/brand-kit" },
],
patternColor: 'lilac',
},
{
label: "Insights",
href: "/docs/concepts/networks-and-servers/insights",
href: "/insights",
icon: "insights",
children: [
{ label: "Explorer", href: "https://livenet.xrpl.org" },
{ label: "Data Dashboard", href: "/docs/concepts/networks-and-servers/data-dashboard" },
{ label: "Amendment Voting Status", href: "/docs/concepts/networks-and-servers/amendments" },
{ label: "Amendment Voting Status", href: "https://xrpl.org/resources/known-amendments" },
],
patternColor: 'green',
},

View File

@@ -2,7 +2,7 @@ import * as React from "react";
import { useThemeHooks } from "@redocly/theme/core/hooks";
import { SubmenuSection } from "./SubmenuSection";
import { ArrowIcon } from "../icons";
import { walletIcons, resourcesPurplePattern, insightsGreenPattern, darkInsightsGreenPattern, darkLilacPattern } from "../constants/icons";
import { walletIcons, resourcesIconPattern, insightsIconPattern } from "../constants/icons";
import { developSubmenuData, useCasesSubmenuData, communitySubmenuData, networkSubmenuData } from "../constants/navigation";
import type { SubmenuItem, SubmenuItemWithChildren, NetworkSubmenuSection } from "../types";
@@ -161,13 +161,10 @@ export function Submenu({ variant, isActive, isClosing, onClose }: SubmenuProps)
);
}
/** Network submenu with theme-aware pattern images */
/** Network submenu with pattern images (same for light and dark mode) */
function NetworkSubmenuContent({ isActive, isClosing, onClose }: { isActive: boolean; isClosing: boolean; onClose?: () => void }) {
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
// Start with null to indicate "not yet determined" - avoids hydration mismatch
// by ensuring server and client both render the same initial state
const [isDarkMode, setIsDarkMode] = React.useState<boolean | null>(null);
// Handle keyboard events for accessibility
const handleKeyDown = React.useCallback((event: KeyboardEvent) => {
@@ -223,21 +220,11 @@ function NetworkSubmenuContent({ isActive, isClosing, onClose }: { isActive: boo
}
}, [isActive, handleKeyDown]);
React.useEffect(() => {
const checkTheme = () => {
setIsDarkMode(document.documentElement.classList.contains('dark'));
};
checkTheme();
const observer = new MutationObserver(checkTheme);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
return () => observer.disconnect();
}, []);
// Default to light mode patterns until client-side detection runs
const patternImages = React.useMemo(() => ({
lilac: isDarkMode === true ? darkLilacPattern : resourcesPurplePattern,
green: isDarkMode === true ? darkInsightsGreenPattern : insightsGreenPattern,
}), [isDarkMode]);
// Use same pattern images for both light and dark mode
const patternImages = {
lilac: resourcesIconPattern,
green: insightsIconPattern,
};
const classNames = [
'bds-submenu',

View File

@@ -40,8 +40,10 @@ export default function CalloutMediaBannerShowcase() {
variant="green"
heading="The Compliant Ledger Protocol"
subheading="A decentralized public Layer 1 blockchain for creating, transferring, and exchanging digital assets with a focus on compliance."
primaryButton={{ label: "Get Started", onClick: () => handleClick('responsive-demo-primary') }}
tertiaryButton={{ label: "Learn More", onClick: () => handleClick('responsive-demo-tertiary') }}
buttons={[
{ label: "Get Started", onClick: () => handleClick('responsive-demo-primary') },
{ label: "Learn More", onClick: () => handleClick('responsive-demo-tertiary') }
]}
/>
{/* Responsive Behavior */}
@@ -122,7 +124,9 @@ export default function CalloutMediaBannerShowcase() {
variant="default"
heading="Build on XRPL"
subheading="Start building your next decentralized application on the XRP Ledger."
primaryButton={{ label: "Start Building", href: "#start" }}
buttons={[
{ label: "Start Building", href: "#start" }
]}
/>
</div>
@@ -143,8 +147,10 @@ export default function CalloutMediaBannerShowcase() {
variant="light-gray"
heading="Developer Resources"
subheading="Access comprehensive documentation, tutorials, and code samples."
primaryButton={{ label: "View Docs", href: "#docs" }}
tertiaryButton={{ label: "Browse Tutorials", href: "#tutorials" }}
buttons={[
{ label: "View Docs", href: "#docs" },
{ label: "Browse Tutorials", href: "#tutorials" }
]}
/>
</div>
@@ -165,7 +171,9 @@ export default function CalloutMediaBannerShowcase() {
variant="lilac"
heading="New Feature Release"
subheading="Discover the latest enhancements and capabilities added to the XRP Ledger."
primaryButton={{ label: "Learn More", href: "#features" }}
buttons={[
{ label: "Learn More", href: "#features" }
]}
/>
</div>
@@ -186,8 +194,10 @@ export default function CalloutMediaBannerShowcase() {
variant="green"
heading="The Compliant Ledger Protocol"
subheading="A decentralized public Layer 1 blockchain for creating, transferring, and exchanging digital assets with a focus on compliance."
primaryButton={{ label: "Get Started", href: "#get-started" }}
tertiaryButton={{ label: "Learn More", href: "#learn" }}
buttons={[
{ label: "Get Started", href: "#get-started" },
{ label: "Learn More", href: "#learn" }
]}
/>
</div>
@@ -208,8 +218,10 @@ export default function CalloutMediaBannerShowcase() {
variant="gray"
heading="Join the Community"
subheading="Connect with developers building on XRPL."
primaryButton={{ label: "Join Discord", href: "#discord" }}
tertiaryButton={{ label: "View Events", href: "#events" }}
buttons={[
{ label: "Join Discord", href: "#discord" },
{ label: "View Events", href: "#events" }
]}
/>
</div>
@@ -255,7 +267,9 @@ export default function CalloutMediaBannerShowcase() {
<CalloutMediaBanner
backgroundImage={sampleBackgroundImage}
subheading="A decentralized public Layer 1 blockchain for creating, transferring, and exchanging digital assets with a focus on compliance."
primaryButton={{ label: "Start Building", onClick: () => handleClick('image-white-primary') }}
buttons={[
{ label: "Start Building", onClick: () => handleClick('image-white-primary') }
]}
/>
</div>
@@ -277,8 +291,10 @@ export default function CalloutMediaBannerShowcase() {
textColor="black"
heading="Build the Future of Finance"
subheading="Create powerful decentralized applications with XRPL's fast, efficient, and sustainable blockchain technology."
primaryButton={{ label: "Start Building", onClick: () => handleClick('image-black-primary') }}
tertiaryButton={{ label: "Explore Features", onClick: () => handleClick('image-black-tertiary') }}
buttons={[
{ label: "Start Building", onClick: () => handleClick('image-black-primary') },
{ label: "Explore Features", onClick: () => handleClick('image-black-tertiary') }
]}
/>
</div>
@@ -321,8 +337,10 @@ export default function CalloutMediaBannerShowcase() {
variant="default"
heading="Complete Feature Set"
subheading="Access all the tools you need to build on XRPL."
primaryButton={{ label: "Get Started", href: "#start" }}
tertiaryButton={{ label: "Learn More", href: "#learn" }}
buttons={[
{ label: "Get Started", href: "#start" },
{ label: "Learn More", href: "#learn" }
]}
/>
</div>
@@ -343,7 +361,9 @@ export default function CalloutMediaBannerShowcase() {
variant="light-gray"
heading="Simple Call-to-Action"
subheading="Focus user attention on a single primary action."
primaryButton={{ label: "Take Action", href: "#action" }}
buttons={[
{ label: "Take Action", href: "#action" }
]}
/>
</div>
@@ -383,8 +403,10 @@ export default function CalloutMediaBannerShowcase() {
<CalloutMediaBanner
variant="green"
subheading="Important information or announcement without requiring user action."
primaryButton={{ label: "Take Action", href: "#action" }}
tertiaryButton={{ label: "Learn More", href: "#learn" }}
buttons={[
{ label: "Take Action", href: "#action" },
{ label: "Learn More", href: "#learn" }
]}
/>
</div>
@@ -564,20 +586,12 @@ export default function CalloutMediaBannerShowcase() {
<div style={{ flex: '1 1 0', minWidth: 0 }}>Subheading/description text</div>
</div>
{/* primaryButton */}
{/* buttons */}
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>primaryButton</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>{`{ label, href?, onClick? }`}</code></div>
<div style={{ width: '140px', flexShrink: 0 }}><code>buttons</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>{`Array<{ label, href?, onClick?, forceColor? }>`}</code></div>
<div style={{ width: '120px', flexShrink: 0 }}><code>undefined</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Primary button configuration</div>
</div>
{/* tertiaryButton */}
<div className="d-flex flex-row py-3" style={{ gap: '1rem', borderBottom: '1px solid var(--bs-border-color, #dee2e6)' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>tertiaryButton</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>{`{ label, href?, onClick? }`}</code></div>
<div style={{ width: '120px', flexShrink: 0 }}><code>undefined</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Tertiary button configuration</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Button configurations (1-2 buttons supported)</div>
</div>
{/* className */}

View File

@@ -0,0 +1,279 @@
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
import { CardStats, CardStatsCardConfig } from "shared/patterns/CardStats";
import { Divider } from "shared/components/Divider";
export const frontmatter = {
seo: {
title: 'CardStats Pattern Showcase',
description: "A comprehensive showcase of the CardStats pattern component demonstrating different configurations and color variants in the XRPL.org Design System.",
}
};
// Sample cards data matching Figma design (node 32051:2839)
const sampleCards: CardStatsCardConfig[] = [
{
statistic: "12",
superscript: "+",
label: "Continuous uptime years",
variant: "lilac",
primaryButton: { label: "Learn More", href: "#uptime" },
},
{
statistic: "6M",
superscript: "2",
label: "Active wallets",
variant: "light-gray",
primaryButton: { label: "Explore", href: "#wallets" },
},
{
statistic: "$1T",
superscript: "+",
label: "Value transferred",
variant: "green",
primaryButton: { label: "View Stats", href: "#value" },
},
{
statistic: "3-5s",
label: "Transaction finality",
variant: "green",
primaryButton: { label: "Learn More", href: "#speed" },
},
{
statistic: "70",
superscript: "+",
label: "Ecosystem partners",
variant: "dark-gray",
primaryButton: { label: "Meet Partners", href: "#partners" },
},
{
statistic: "100K",
superscript: "+",
label: "Developer community",
variant: "lilac",
primaryButton: { label: "Join Us", href: "#community" },
},
];
export default function CardStatsShowcase() {
return (
<div className="landing">
<div className="overflow-hidden">
{/* Hero Section */}
<section className="py-26 text-center">
<div className="col-lg-8 mx-auto">
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
<h1 className="mb-4">CardStats Pattern</h1>
<p className="longform">
A section pattern that displays a heading, optional description, and a responsive
grid of CardStat components. Designed for showcasing key statistics and metrics.
</p>
</div>
</section>
{/* Design Tokens Info */}
<PageGrid className="py-10">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Design Specifications</h2>
<div className="d-flex flex-wrap gap-6">
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Typography</h6>
<ul className="mb-0">
<li><strong>Heading:</strong> heading-md (Tobias Light)</li>
<li><strong>Description:</strong> body-l (Booton Light)</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Grid Layout</h6>
<ul className="mb-0">
<li><strong>Mobile:</strong> 2 columns</li>
<li><strong>Tablet:</strong> 2 columns</li>
<li><strong>Desktop:</strong> 3 columns</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Color Variants</h6>
<ul className="mb-0">
<li><strong>Lilac:</strong> #C0A7FF</li>
<li><strong>Green:</strong> #21E46B</li>
<li><strong>Light Gray:</strong> #E6EAF0</li>
<li><strong>Dark Gray:</strong> #CAD4DF</li>
</ul>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<Divider />
{/* Full Example - 6 Cards with Heading and Description */}
<CardStats
heading="Blockchain Trusted at Scale"
description="Streamline development and build powerful RWA tokenization solutions with XRP Ledger's comprehensive developer toolset."
cards={sampleCards}
/>
<Divider />
{/* Heading Only - No Description */}
<CardStats
heading="XRPL Network Statistics"
cards={[
{
statistic: "12",
superscript: "+",
label: "Continuous uptime years",
variant: "lilac",
primaryButton: { label: "Learn More", href: "#uptime" },
span: { base: 4, md: 4, lg: 6 },
},
{
statistic: "6M",
superscript: "2",
label: "Active wallets",
variant: "light-gray",
primaryButton: { label: "Explore", href: "#wallets" },
span: { base: 4, md: 4, lg: 6 },
},
{
statistic: "$1T",
superscript: "+",
label: "Value transferred",
variant: "green",
primaryButton: { label: "View Stats", href: "#value" },
span: { base: 4, md: 8, lg: 12 },
}]}
/>
<Divider />
{/* 4 Cards Example */}
<CardStats
heading="Why Build on XRPL?"
description="The XRP Ledger provides enterprise-grade infrastructure for building the future of finance."
cards={sampleCards.slice(0, 4)}
/>
<Divider />
{/* Two Buttons Example */}
<PageGrid className="py-10">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-4">Two Button Cards</h2>
<p className="mb-8">Cards can include both primary and secondary buttons for multiple CTAs.</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CardStats
heading="Get Started with XRPL"
description="Explore the XRP Ledger ecosystem with comprehensive documentation and developer resources."
cards={[
{
statistic: "12",
superscript: "+",
label: "Continuous uptime years",
variant: "lilac",
primaryButton: { label: "Learn More", href: "#learn" },
secondaryButton: { label: "View Docs", href: "#docs" },
},
{
statistic: "6M",
superscript: "+",
label: "Active wallets",
variant: "green",
primaryButton: { label: "Get Started", href: "#start" },
secondaryButton: { label: "Explore", href: "#explore" },
},
{
statistic: "$1T",
superscript: "+",
label: "Value transferred",
variant: "light-gray",
primaryButton: { label: "View Stats", href: "#stats" },
secondaryButton: { label: "Learn More", href: "#about" },
},
]}
/>
<Divider />
{/* Code Examples */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-8">Code Examples</h2>
<h5 className="mb-4">Basic Usage</h5>
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`import { CardStats } from 'shared/patterns/CardStats';
<CardStats
heading="Blockchain Trusted at Scale"
description="Optional description text here."
cards={[
{
statistic: "12",
superscript: "+",
label: "Continuous uptime years",
variant: "lilac",
primaryButton: { label: "Learn More", href: "/docs" }
},
{
statistic: "6M",
label: "Active wallets",
variant: "green"
},
// ... more cards
]}
/>`}</pre>
</div>
<h5 className="mb-4">Without Description</h5>
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`<CardStats
heading="XRPL Network Statistics"
cards={statsCards}
/>`}</pre>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<Divider />
{/* Design References */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Design References</h2>
<div className="d-flex flex-column gap-3">
<div>
<strong>Figma Design:</strong>{' '}
<a href="https://www.figma.com/design/drnQQXnK9Q67MTPPKQsY9l/Section-Cards---Stats?node-id=32051-2839&m=dev" target="_blank" rel="noopener noreferrer">
Section Cards - Stats (Figma)
</a>
</div>
<div>
<strong>Pattern Location:</strong>{' '}
<code>shared/patterns/CardStats/</code>
</div>
<div>
<strong>Component Used:</strong>{' '}
<code>shared/components/CardStat/</code>
</div>
<div>
<strong>Color Tokens:</strong>{' '}
<code>styles/_colors.scss</code>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
</div>
</div>
);
}

View File

@@ -1,257 +0,0 @@
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
import { CardsIconGrid } from "shared/patterns/CardsIconGrid";
import { Divider } from "shared/components/Divider";
export const frontmatter = {
seo: {
title: 'CardsIconGrid Pattern Showcase',
description: "A comprehensive showcase of the CardsIconGrid pattern component demonstrating light and dark mode variations in the XRPL.org Design System.",
}
};
// Sample icon SVG for demonstration
const SAMPLE_ICON = "/img/icons/card-icon-placeholder.svg";
// Sample cards data - Green variant
const greenCards = [
{
icon: SAMPLE_ICON,
iconAlt: "Digital Wallets icon",
label: "Digital Wallets",
href: "#wallets",
variant: "green" as const,
},
{
icon: SAMPLE_ICON,
iconAlt: "B2B Payment Rails icon",
label: "B2B Payment Rails",
href: "#payments",
variant: "green" as const,
},
{
icon: SAMPLE_ICON,
iconAlt: "Compliance-First Payments icon",
label: "Compliance-First Payments",
href: "#compliance",
variant: "green" as const,
},
{
icon: SAMPLE_ICON,
iconAlt: "Merchant Settlement icon",
label: "Merchant Settlement",
href: "#settlement",
variant: "green" as const,
},
{
icon: SAMPLE_ICON,
iconAlt: "Cross-Border Payments icon",
label: "Cross-Border Payments",
href: "#cross-border",
variant: "green" as const,
},
{
icon: SAMPLE_ICON,
iconAlt: "Treasury Management icon",
label: "Treasury Management",
href: "#treasury",
variant: "green" as const,
},
];
// Sample cards data - Neutral variant
const neutralCards = [
{
icon: SAMPLE_ICON,
iconAlt: "Documentation icon",
label: "Documentation",
href: "#docs",
variant: "neutral" as const,
},
{
icon: SAMPLE_ICON,
iconAlt: "Tutorials icon",
label: "Tutorials",
href: "#tutorials",
variant: "neutral" as const,
},
{
icon: SAMPLE_ICON,
iconAlt: "API Reference icon",
label: "API Reference",
href: "#api",
variant: "neutral" as const,
},
];
export default function CardsIconGridShowcase() {
return (
<div className="landing">
<div className="overflow-hidden">
{/* Hero Section */}
<section className="py-26 text-center">
<div className="col-lg-8 mx-auto">
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
<h1 className="mb-4">CardsIconGrid Pattern</h1>
<p className="longform">
A section pattern that displays a heading, optional description, and a responsive grid
of CardIcon components. Follows the "CardIconGrid" design from Figma.
</p>
</div>
</section>
{/* Design Tokens Reference */}
<PageGrid className="py-10">
<PageGridRow>
<PageGridCol span={{ base: 4, md: 8, lg: 12 }}>
<h2 className="h4 mb-6">Design Specifications</h2>
<div className="d-flex flex-wrap gap-8">
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Typography</h6>
<ul className="mb-0">
<li><strong>Heading:</strong> heading-md (Tobias Light)</li>
<li><strong>Description:</strong> body-l (Booton Light)</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Grid Layout</h6>
<ul className="mb-0">
<li><strong>Mobile:</strong> 1 column</li>
<li><strong>Tablet:</strong> 2 columns</li>
<li><strong>Desktop:</strong> 3 columns</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Colors</h6>
<ul className="mb-0">
<li><strong>Light Mode:</strong> $black (#141414)</li>
<li><strong>Dark Mode:</strong> $white (#FFFFFF)</li>
</ul>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<Divider />
{/* 6 Cards Example - Green Variant */}
<section>
<CardsIconGrid
heading="Unlock new business models with embedded payments"
description="Streamline development and build powerful RWA tokenization solutions with XRP Ledger's comprehensive developer toolset."
cards={greenCards}
/>
</section>
<Divider />
{/* 3 Cards Example - Neutral Variant */}
<section>
<CardsIconGrid
heading="Developer Resources"
description="Everything you need to start building on the XRP Ledger."
cards={neutralCards}
/>
</section>
<Divider />
{/* Without Description */}
<PageGrid className="py-10">
<PageGridRow>
<PageGridCol span={{ base: 4, md: 8, lg: 12 }}>
<h2 className="h4 mb-4">Without Description</h2>
<p className="mb-0">
The description prop is optional. When omitted, only the heading appears above the cards.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<section>
<CardsIconGrid
heading="Funding & Support Programs"
cards={greenCards.slice(0, 3)}
/>
</section>
<Divider />
{/* Code Examples */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={{ base: 4, md: 8, lg: 10 }}>
<h2 className="h4 mb-6">Code Examples</h2>
<h5 className="mb-4">Basic Usage</h5>
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#1a1a1a', fontFamily: 'monospace', fontSize: '14px' }}>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#f8f8f2' }}>{`import { CardsIconGrid } from 'shared/patterns/CardsIconGrid';
<CardsIconGrid
heading="Unlock new business models"
description="Build powerful solutions with XRPL."
cards={[
{
icon: "/icons/wallet.svg",
label: "Digital Wallets",
href: "/docs/wallets",
variant: "green"
},
{
icon: "/icons/payments.svg",
label: "B2B Payment Rails",
href: "/docs/payments",
variant: "green"
},
{
icon: "/icons/compliance.svg",
label: "Compliance-First Payments",
href: "/docs/compliance",
variant: "green"
}
]}
/>`}</pre>
</div>
<h5 className="mb-4">Without Description</h5>
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#1a1a1a', fontFamily: 'monospace', fontSize: '14px' }}>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#f8f8f2' }}>{`<CardsIconGrid
heading="Developer Resources"
cards={[
{ icon: "/icons/docs.svg", label: "Documentation", href: "/docs", variant: "neutral" },
{ icon: "/icons/tutorials.svg", label: "Tutorials", href: "/tutorials", variant: "neutral" },
{ icon: "/icons/api.svg", label: "API Reference", href: "/api", variant: "neutral" }
]}
/>`}</pre>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<Divider />
{/* Design References */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={{ base: 4, md: 8, lg: 12 }}>
<h2 className="h4 mb-6">Design References</h2>
<div className="d-flex flex-column gap-3">
<div>
<strong>Figma:</strong>{' '}
<a href="https://www.figma.com/design/Ojj6UpFBw3HMb0QqRaKxAU/Section-Cards---Icon?node-id=30071-3082&m=dev" target="_blank" rel="noopener noreferrer">
Section Cards - Icon Grid
</a>
</div>
<div>
<strong>Documentation:</strong>{' '}
<code>shared/patterns/CardsIconGrid/CardsIconGrid.md</code>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
</div>
</div>
);
}

View File

@@ -0,0 +1,291 @@
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
import { CardsTwoColumn } from "shared/patterns/CardsTwoColumn";
import { TextCard } from "shared/patterns/CardsTwoColumn";
import { Divider } from "shared/components/Divider";
export const frontmatter = {
seo: {
title: 'CardsTwoColumn Pattern Showcase',
description: "A comprehensive showcase of the CardsTwoColumn pattern component demonstrating different color combinations and arrangements in the XRPL.org Design System.",
}
};
export default function CardsTwoColumnShowcase() {
return (
<div className="landing">
<div className="overflow-hidden">
{/* Hero Section */}
<section className="py-26 text-center">
<div className="col-lg-8 mx-auto">
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
<h1 className="mb-4">CardsTwoColumn Pattern</h1>
<p className="longform">
A section pattern with a header (title + description) and a 2x2 grid of TextCard components.
Features 4 color variants and responsive behavior across all breakpoints.
</p>
</div>
</section>
{/* Design Specifications */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Design Specifications</h2>
<div className="d-flex flex-row gap-6 mb-6" style={{ flexWrap: 'wrap' }}>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Section Typography</h6>
<ul className="mb-0">
<li><strong>Title:</strong> heading-md (Tobias Light)</li>
<li><strong>Description:</strong> body-l (Booton Light, muted)</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Card Typography</h6>
<ul className="mb-0">
<li><strong>Title:</strong> heading-lg (Tobias Light)</li>
<li><strong>Description:</strong> body-l (Booton Light)</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Card Heights</h6>
<ul className="mb-0">
<li><strong>Desktop:</strong> 340px</li>
<li><strong>Tablet:</strong> 309px</li>
<li><strong>Mobile:</strong> 274px</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Section Padding</h6>
<ul className="mb-0">
<li><strong>Desktop:</strong> 40px vertical, 32px horizontal</li>
<li><strong>Tablet:</strong> 32px vertical, 24px horizontal</li>
<li><strong>Mobile:</strong> 24px vertical, 16px horizontal</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Gap Between Header & Cards</h6>
<ul className="mb-0">
<li><strong>Desktop:</strong> 40px</li>
<li><strong>Tablet:</strong> 32px</li>
<li><strong>Mobile:</strong> 24px</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Grid Layout</h6>
<ul className="mb-0">
<li><strong>Desktop:</strong> 2×2 grid (8px gap)</li>
<li><strong>Tablet:</strong> 1 column stacked (8px gap)</li>
<li><strong>Mobile:</strong> 1 column stacked (8px gap)</li>
</ul>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<Divider />
{/* Full Pattern Example */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Full Pattern Example</h2>
<p className="mb-6">
The CardsTwoColumn pattern includes a header section (title + description) and a 2×2 grid of TextCards.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CardsTwoColumn
title="The Future of Finance is Already Onchain"
description="XRP Ledger isn't about bold predictions. It's about delivering value now. Institutions, developers, and enterprises are already building on XRPL."
secondaryDescription="On XRPL, you're not waiting for the future. You're building it."
cards={[
{
title: "Institutions",
description: "Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility.",
href: "#institutions",
color: "lilac"
},
{
title: "Developers",
description: "Build decentralized applications with comprehensive documentation, tutorials, and developer tools.",
href: "#developers",
color: "neutral-light"
},
{
title: "Enterprise",
description: "Scale your business with enterprise-grade blockchain solutions and dedicated support.",
href: "#enterprise",
color: "neutral-dark"
},
{
title: "Community",
description: "Join the global community of XRPL developers, validators, and enthusiasts.",
href: "#community",
color: "green"
}
]}
/>
<Divider />
{/* Color Variants Section */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">All 6 Color Variants</h2>
<p className="mb-6">
TextCard supports 6 color variants with hover and pressed states. Hover over cards to see the window shade animation.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* All Color Variants - Standalone TextCards */}
<PageGrid className="pb-26">
<PageGridRow>
<PageGridCol span={12}>
<div className="d-flex flex-wrap gap-3">
<TextCard
title="Green"
description="Default: $green-200 → Hover: $green-300 → Pressed: $green-400"
color="green"
style={{ flex: '1 1 300px', minWidth: 280 }}
/>
<TextCard
title="Neutral Light"
description="Default: $gray-200 → Hover: $gray-300 → Pressed: $gray-400"
color="neutral-light"
style={{ flex: '1 1 300px', minWidth: 280 }}
/>
<TextCard
title="Neutral Dark"
description="Default: $gray-300 → Hover: $gray-400 → Pressed: $gray-500"
color="neutral-dark"
style={{ flex: '1 1 300px', minWidth: 280 }}
/>
<TextCard
title="Lilac"
description="Default: $lilac-200 → Hover: $lilac-300 → Pressed: $lilac-400"
color="lilac"
style={{ flex: '1 1 300px', minWidth: 280 }}
/>
<TextCard
title="Yellow"
description="Default: $yellow-100 → Hover: $yellow-200 → Pressed: $yellow-300"
color="yellow"
style={{ flex: '1 1 300px', minWidth: 280 }}
/>
<TextCard
title="Blue"
description="Default: $blue-100 → Hover: $blue-200 → Pressed: $blue-300"
color="blue"
style={{ flex: '1 1 300px', minWidth: 280 }}
/>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<Divider />
{/* Mixed Colors in Pattern */}
<CardsTwoColumn
title="All 6 Colors in Pattern"
description="The CardsTwoColumn pattern accepts exactly 4 cards. Here we show various color combinations including the new blue variant."
cards={[
{ title: "Lilac", description: "Primary accent color for highlights.", color: "lilac" },
{ title: "Blue", description: "Secondary accent for cool tones.", color: "blue" },
{ title: "Green", description: "Brand color for positive actions.", color: "green" },
{ title: "Yellow", description: "Secondary accent for warm tones.", color: "yellow" }
]}
/>
<Divider />
{/* Alternative Color Combo */}
<CardsTwoColumn
title="Alternative Color Arrangement"
description="Different colors can be used to create visual hierarchy and distinguish between content types."
cards={[
{ title: "Documentation", description: "Comprehensive guides and API references.", color: "neutral-dark" },
{ title: "Tutorials", description: "Step-by-step learning resources.", color: "green" },
{ title: "Use Cases", description: "Real-world applications and success stories.", color: "yellow" },
{ title: "Resources", description: "Tools and libraries for development.", color: "lilac" }
]}
/>
<Divider />
{/* Disabled State Section */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Disabled State</h2>
<p className="mb-6">
TextCards can be disabled. In light mode, disabled cards have a $gray-100 background with $gray-500 text.
In dark mode, disabled cards have a $gray-500 background with 30% opacity. Toggle dark mode to see the difference.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<PageGrid className="pb-26">
<PageGridRow>
<PageGridCol span={12}>
<div className="d-flex flex-wrap gap-3">
<TextCard
title="Disabled Green"
description="This card is disabled and cannot be interacted with."
color="green"
disabled
style={{ flex: '1 1 300px', minWidth: 280 }}
/>
<TextCard
title="Disabled Neutral Light"
description="This card is disabled and cannot be interacted with."
color="neutral-light"
disabled
style={{ flex: '1 1 300px', minWidth: 280 }}
/>
<TextCard
title="Disabled Neutral Dark"
description="This card is disabled and cannot be interacted with."
color="neutral-dark"
disabled
style={{ flex: '1 1 300px', minWidth: 280 }}
/>
<TextCard
title="Disabled Lilac"
description="This card is disabled and cannot be interacted with."
color="lilac"
disabled
style={{ flex: '1 1 300px', minWidth: 280 }}
/>
<TextCard
title="Disabled Yellow"
description="This card is disabled and cannot be interacted with."
color="yellow"
disabled
style={{ flex: '1 1 300px', minWidth: 280 }}
/>
<TextCard
title="Disabled Blue"
description="This card is disabled and cannot be interacted with."
color="blue"
disabled
style={{ flex: '1 1 300px', minWidth: 280 }}
/>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<Divider />
</div>
</div>
);
}

View File

@@ -1,537 +0,0 @@
import * as React from 'react';
import { CardStat } from 'shared/components/CardStat';
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
export const frontmatter = {
seo: {
title: 'CardStat Component Showcase',
description: 'Interactive showcase of the Brand Design System CardStat component with all variants and configurations.',
},
};
export default function CardStatShowcase() {
const [clickCount, setClickCount] = React.useState<Record<string, number>>({});
const handleClick = (id: string) => {
setClickCount((prev) => ({ ...prev, [id]: (prev[id] || 0) + 1 }));
};
return (
<div className="landing">
{/* Hero Section */}
<PageGrid className="py-26">
<div className="d-flex flex-column-reverse col-lg-8 mx-auto">
<h1 className="mb-0">CardStat Component</h1>
<h6 className="eyebrow mb-3">Brand Design System</h6>
</div>
<p className="col-lg-8 mx-auto mt-10">
A statistics card component following the XRPL Brand Design System. This showcase demonstrates
all color variants, button configurations, and responsive behavior using PageGrid.
</p>
</PageGrid>
{/* Basic Usage */}
<PageGrid className="py-26">
<PageGridRow>
<div className="d-flex flex-column-reverse w-100">
<h2 className="h4 mb-8">Basic Usage</h2>
<h6 className="eyebrow mb-3">Simple Statistics</h6>
</div>
<p className="mb-8">
CardStat components display prominent statistics with descriptive labels. They adapt responsively
and can be used without buttons for purely informational displays.
</p>
</PageGridRow>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
<CardStat
statistic="6 Million"
superscript="2"
label="Active wallets"
variant="lilac"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
<CardStat
statistic="$1 Trillion"
superscript="*"
label="Value moved"
variant="green"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
<CardStat
statistic="12"
superscript="+"
label="Continuous uptime years"
variant="light-gray"
/>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Color Variants */}
<PageGrid className="py-26">
<PageGridRow>
<div className="d-flex flex-column-reverse w-100">
<h2 className="h4 mb-8">Color Variants</h2>
<h6 className="eyebrow mb-3">Visual Themes</h6>
</div>
<p className="mb-8">
Four color variants are available to match different types of statistics and visual contexts.
</p>
</PageGridRow>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<CardStat
statistic="6M"
superscript="+"
label="Active wallets"
variant="lilac"
/>
<p className="mt-4 text-muted"><strong>Lilac</strong> - User metrics, community stats</p>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<CardStat
statistic="$1T"
superscript="+"
label="Value moved"
variant="green"
/>
<p className="mt-4 text-muted"><strong>Green</strong> - Financial metrics, growth</p>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<CardStat
statistic="12"
superscript="+"
label="Uptime years"
variant="light-gray"
/>
<p className="mt-4 text-muted"><strong>Light Gray</strong> - Technical stats, reliability</p>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<CardStat
statistic="70+"
label="Partners"
variant="dark-gray"
/>
<p className="mt-4 text-muted"><strong>Dark Gray</strong> - Neutral metrics, secondary info</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* With Single Button */}
<PageGrid className="py-26">
<PageGridRow>
<div className="d-flex flex-column-reverse w-full">
<h2 className="h4 mb-8">With Primary Button</h2>
<h6 className="eyebrow mb-3">Single CTA</h6>
</div>
<p className="mb-8">
Add a primary button for a main call-to-action. Buttons use the black variant for proper
contrast on colored backgrounds.
</p>
</PageGridRow>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 4, lg: 6 }}>
<CardStat
statistic="6 Million"
superscript="+"
label="Active wallets"
variant="lilac"
primaryButton={{
label: "Explore",
onClick: () => handleClick('explore-1')
}}
/>
{clickCount['explore-1'] > 0 && (
<p className="mt-4 text-muted">Clicked {clickCount['explore-1']} time{clickCount['explore-1'] !== 1 ? 's' : ''}</p>
)}
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 6 }}>
<CardStat
statistic="$1 Trillion"
superscript="+"
label="Value moved"
variant="green"
primaryButton={{
label: "Learn More",
onClick: () => handleClick('learn-1')
}}
/>
{clickCount['learn-1'] > 0 && (
<p className="mt-4 text-muted">Clicked {clickCount['learn-1']} time{clickCount['learn-1'] !== 1 ? 's' : ''}</p>
)}
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 12 }}>
<CardStat
statistic="12"
superscript="+"
label="Continuous uptime years"
variant="light-gray"
primaryButton={{
label: "View Details",
onClick: () => handleClick('view-1')
}}
/>
{clickCount['view-1'] > 0 && (
<p className="mt-4 text-muted">Clicked {clickCount['view-1']} time{clickCount['view-1'] !== 1 ? 's' : ''}</p>
)}
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* With Two Buttons */}
<PageGrid className="py-26">
<PageGridRow>
<div className="d-flex flex-column-reverse w-full">
<h2 className="h4 mb-8">With Two Buttons</h2>
<h6 className="eyebrow mb-3">Multiple CTAs</h6>
</div>
<p className="mb-8">
Include both primary and secondary buttons for multiple action options. Buttons wrap responsively
and maintain consistent spacing.
</p>
</PageGridRow>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
<CardStat
statistic="6 Million"
superscript="+"
label="Active wallets"
variant="lilac"
primaryButton={{
label: "Learn More",
onClick: () => handleClick('primary-1')
}}
secondaryButton={{
label: "Get Started",
onClick: () => handleClick('secondary-1')
}}
/>
{(clickCount['primary-1'] > 0 || clickCount['secondary-1'] > 0) && (
<p className="mt-4 text-muted">
Primary: {clickCount['primary-1'] || 0}, Secondary: {clickCount['secondary-1'] || 0}
</p>
)}
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
<CardStat
statistic="$1 Trillion"
superscript="+"
label="Value moved"
variant="green"
primaryButton={{
label: "Explore",
onClick: () => handleClick('primary-2')
}}
secondaryButton={{
label: "View Stats",
onClick: () => handleClick('secondary-2')
}}
/>
{(clickCount['primary-2'] > 0 || clickCount['secondary-2'] > 0) && (
<p className="mt-4 text-muted">
Primary: {clickCount['primary-2'] || 0}, Secondary: {clickCount['secondary-2'] || 0}
</p>
)}
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
<CardStat
statistic="12"
superscript="+"
label="Continuous uptime years"
variant="light-gray"
primaryButton={{
label: "Read More",
onClick: () => handleClick('primary-3')
}}
secondaryButton={{
label: "Try It",
onClick: () => handleClick('secondary-3')
}}
/>
{(clickCount['primary-3'] > 0 || clickCount['secondary-3'] > 0) && (
<p className="mt-4 text-muted">
Primary: {clickCount['primary-3'] || 0}, Secondary: {clickCount['secondary-3'] || 0}
</p>
)}
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Responsive Behavior */}
<PageGrid className="py-26">
<PageGridRow>
<div className="d-flex flex-column-reverse w-full">
<h2 className="h4 mb-8">Responsive Layout</h2>
<h6 className="eyebrow mb-3">Adaptive Grid</h6>
</div>
<p className="mb-8">
Cards adapt to different screen sizes. On mobile (base), cards stack vertically. On tablet (md),
they can be arranged in 2 columns. On desktop (lg+), up to 3-4 columns are supported.
</p>
</PageGridRow>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<CardStat
statistic="1M"
superscript="+"
label="Transactions daily"
variant="lilac"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<CardStat
statistic="150"
superscript="+"
label="Countries"
variant="green"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<CardStat
statistic="99.9"
superscript="%"
label="Uptime"
variant="light-gray"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<CardStat
statistic="24/7"
label="Support"
variant="dark-gray"
/>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Mixed Configurations */}
<PageGrid className="py-26">
<PageGridRow>
<div className="d-flex flex-column-reverse w-100">
<h2 className="h4 mb-8">Mixed Configurations</h2>
<h6 className="eyebrow mb-3">Flexible Usage</h6>
</div>
<p className="mb-8">
Mix and match cards with different button configurations in the same layout.
</p>
</PageGridRow>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
<CardStat
statistic="6 Million"
superscript="+"
label="Active wallets"
variant="lilac"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
<CardStat
statistic="$1 Trillion"
superscript="+"
label="Value moved"
variant="green"
primaryButton={{
label: "Learn More",
onClick: () => handleClick('mixed-1')
}}
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
<CardStat
statistic="12"
superscript="+"
label="Continuous uptime years"
variant="light-gray"
primaryButton={{
label: "Explore",
onClick: () => handleClick('mixed-2')
}}
secondaryButton={{
label: "Get Started",
onClick: () => handleClick('mixed-3')
}}
/>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Wide Layout */}
<PageGrid className="py-26">
<PageGridRow>
<div className="d-flex flex-column-reverse w-100">
<h2 className="h4 mb-8">Wide Card Layout</h2>
<h6 className="eyebrow mb-3">Larger Spans</h6>
</div>
<p className="mb-8">
Cards can span multiple columns for wider layouts on larger screens.
</p>
</PageGridRow>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 8, lg: 6 }}>
<CardStat
statistic="6 Million"
superscript="+"
label="Active wallets using XRPL"
variant="lilac"
primaryButton={{
label: "Explore Wallets",
onClick: () => handleClick('wide-1')
}}
secondaryButton={{
label: "Get Started",
onClick: () => handleClick('wide-2')
}}
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 8, lg: 6 }}>
<CardStat
statistic="$1 Trillion"
superscript="+"
label="Total value moved on the network"
variant="green"
primaryButton={{
label: "View Statistics",
onClick: () => handleClick('wide-3')
}}
secondaryButton={{
label: "Learn More",
onClick: () => handleClick('wide-4')
}}
/>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Usage Guidelines */}
<PageGrid className="py-26">
<PageGridRow>
<div className="d-flex flex-column-reverse w-100">
<h2 className="h4 mb-8">Usage Guidelines</h2>
<h6 className="eyebrow mb-3">Best Practices</h6>
</div>
<div className="col-lg-8 mx-auto w-100">
<h5 className="mb-4">When to Use</h5>
<ul className="mb-8">
<li><strong>Key metrics</strong> - Highlight important numbers prominently</li>
<li><strong>Dashboard sections</strong> - Create stat-focused areas on landing pages</li>
<li><strong>About pages</strong> - Showcase company or product statistics</li>
<li><strong>Feature sections</strong> - Emphasize quantitative benefits</li>
</ul>
<h5 className="mb-4">Color Variant Selection</h5>
<ul className="mb-8">
<li><strong>Lilac</strong> - User-focused statistics, community metrics</li>
<li><strong>Green</strong> - Financial metrics, growth indicators</li>
<li><strong>Light Gray</strong> - Technical statistics, reliability metrics</li>
<li><strong>Dark Gray</strong> - Neutral or secondary information</li>
</ul>
<h5 className="mb-4">Button Configuration</h5>
<ul className="mb-8">
<li><strong>No buttons</strong> - For purely informational displays</li>
<li><strong>Single button</strong> - For one clear call-to-action</li>
<li><strong>Two buttons</strong> - For multiple action options</li>
</ul>
<h5 className="mb-4">Tips</h5>
<ul>
<li>Keep statistics concise using abbreviations (M, K, T, +)</li>
<li>Use descriptive labels that clearly explain the metric</li>
<li>Choose colors that match the type of statistic</li>
<li>Test on all breakpoints to ensure proper responsive behavior</li>
<li>Limit buttons to essential actions</li>
</ul>
</div>
</PageGridRow>
</PageGrid>
{/* Implementation Examples */}
<PageGrid className="py-26">
<PageGridRow>
<div className="col-lg-10 mx-auto d-flex flex-column-reverse">
<h2 className="h4 mb-8">Code Examples</h2>
<h6 className="eyebrow mb-3">Implementation</h6>
</div>
<div className="col-lg-10 mx-auto">
<h5 className="mb-4">Basic Card</h5>
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`<CardStat
statistic="6 Million"
superscript="+"
label="Active wallets"
variant="lilac"
/>`}</pre>
</div>
<h5 className="mb-4">With Primary Button</h5>
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`<CardStat
statistic="$1 Trillion"
superscript="+"
label="Value moved"
variant="green"
primaryButton={{
label: "Learn More",
href: "/about"
}}
/>`}</pre>
</div>
<h5 className="mb-4">With Two Buttons</h5>
<div className="p-4 mb-8 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`<CardStat
statistic="12"
superscript="+"
label="Continuous uptime years"
variant="light-gray"
primaryButton={{
label: "Learn More",
onClick: handleLearnMore
}}
secondaryButton={{
label: "Get Started",
href: "/start"
}}
/>`}</pre>
</div>
<h5 className="mb-4">In PageGrid Layout</h5>
<div className="p-4 br-4" style={{ backgroundColor: '#f5f5f7', fontFamily: 'monospace', fontSize: '14px' }}>
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', color: '#000' }}>{`<PageGrid>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
<CardStat
statistic="6 Million"
superscript="+"
label="Active wallets"
variant="lilac"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
<CardStat
statistic="$1 Trillion"
superscript="+"
label="Value moved"
variant="green"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
<CardStat
statistic="12"
superscript="+"
label="Uptime years"
variant="light-gray"
/>
</PageGridCol>
</PageGridRow>
</PageGrid>`}</pre>
</div>
</div>
</ PageGridRow>
</ PageGrid>
</div>
);
}

View File

@@ -0,0 +1,686 @@
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
import { CarouselCardList } from "shared/patterns/CarouselCardList";
import { CarouselButton } from "shared/components/CarouselButton";
import { Divider } from "shared/components/Divider";
export const frontmatter = {
seo: {
title: 'CarouselCardList Pattern Showcase',
description: "A comprehensive showcase of the CarouselCardList pattern component demonstrating horizontal scrolling, navigation buttons, and color variants in the XRPL.org Design System.",
}
};
// Sample icon components for demonstration
const TokenIcon = () => (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="34" cy="34" r="20" stroke="currentColor" strokeWidth="2" fill="none"/>
<path d="M34 22V46M26 34H42" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
const WalletIcon = () => (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="14" y="20" width="40" height="28" rx="4" stroke="currentColor" strokeWidth="2" fill="none"/>
<circle cx="46" cy="34" r="4" fill="currentColor"/>
<path d="M14 28H54" stroke="currentColor" strokeWidth="2"/>
</svg>
);
const ChartIcon = () => (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 50L26 38L34 46L54 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M46 18H54V26" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const ShieldIcon = () => (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M34 12L52 20V32C52 44 44 52 34 56C24 52 16 44 16 32V20L34 12Z" stroke="currentColor" strokeWidth="2" fill="none"/>
<path d="M26 34L32 40L42 28" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
const GlobeIcon = () => (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="34" cy="34" r="20" stroke="currentColor" strokeWidth="2" fill="none"/>
<ellipse cx="34" cy="34" rx="10" ry="20" stroke="currentColor" strokeWidth="2" fill="none"/>
<path d="M14 34H54" stroke="currentColor" strokeWidth="2"/>
</svg>
);
const CodeIcon = () => (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26 24L14 34L26 44" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M42 24L54 34L42 44" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M38 18L30 50" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
// Sample cards data for neutral variant
const neutralCards = [
{
icon: <TokenIcon />,
title: "Native\nTokenization",
description: "Issue and manage digital assets directly on the ledger without smart contracts.",
href: "#tokenization",
},
{
icon: <WalletIcon />,
title: "Low Cost\nTransactions",
description: "Transaction costs are a fraction of a cent, making microtransactions viable.",
href: "#low-cost",
},
{
icon: <ChartIcon />,
title: "Built-in\nDEX",
description: "Trade any token for any other token using the native decentralized exchange.",
href: "#dex",
},
{
icon: <ShieldIcon />,
title: "Enterprise\nSecurity",
description: "Multi-signature support and advanced key management for institutional needs.",
href: "#security",
},
{
icon: <GlobeIcon />,
title: "Global\nReach",
description: "Connect to a worldwide network of validators in seconds, not minutes.",
href: "#global",
},
{
icon: <CodeIcon />,
title: "Developer\nFriendly",
description: "Comprehensive SDKs and APIs for JavaScript, Python, Java, and more.",
href: "#developer",
},
];
// Sample cards data for green variant
const greenCards = [
{
icon: <TokenIcon />,
title: "Stablecoin\nIssuance",
description: "Create and manage compliant stablecoins with built-in freeze and clawback capabilities.",
href: "#stablecoin",
},
{
icon: <WalletIcon />,
title: "Institutional\nCustody",
description: "Multi-signature accounts and escrow features for enterprise-grade custody solutions.",
href: "#custody",
},
{
icon: <ChartIcon />,
title: "Real-Time\nSettlement",
description: "Transactions settle in 3-5 seconds with finality, enabling real-time payments.",
href: "#settlement",
},
{
icon: <ShieldIcon />,
title: "Regulatory\nCompliance",
description: "Built-in features for AML/KYC compliance and regulatory reporting requirements.",
href: "#compliance",
},
{
icon: <GlobeIcon />,
title: "Cross-Border\nPayments",
description: "Seamless international transfers without correspondent banking delays.",
href: "#cross-border",
},
];
export default function CarouselCardListShowcase() {
return (
<div className="landing">
<div className="overflow-hidden">
{/* Hero Section */}
<section className="py-26 text-center">
<div className="col-lg-8 mx-auto">
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
<h1 className="mb-4">CarouselCardList Pattern</h1>
<p className="longform">
A horizontal scrolling carousel that displays CardOffgrid components with navigation buttons.
Supports neutral and green color variants, responsive sizing, and dark/light mode theming.
</p>
</div>
</section>
{/* Feature Overview */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Features</h2>
<div className="d-flex flex-row gap-6" style={{ flexWrap: 'wrap' }}>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Layout</h6>
<ul className="mb-0">
<li>Horizontal scrolling cards</li>
<li>Navigation buttons (prev/next)</li>
<li>Title constrained to grid</li>
<li>Hidden scrollbar</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Responsive Behavior</h6>
<ul className="mb-0">
<li><strong>Mobile:</strong> 343×400px cards</li>
<li><strong>Tablet:</strong> 356×440px cards</li>
<li><strong>Desktop:</strong> 400×480px cards</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Button States</h6>
<ul className="mb-0">
<li>Enabled / Disabled</li>
<li>Hover / Active states</li>
<li>Focus ring for keyboard nav</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Theming</h6>
<ul className="mb-0">
<li>Dark mode (default)</li>
<li>Light mode (<code>html.light</code>)</li>
<li>Neutral &amp; Green card variants</li>
<li>Independent button colors (neutral, green, black)</li>
</ul>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Neutral Cards + Neutral Buttons (Default) */}
<section className="py-10">
<PageGrid className="mb-6">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-3">Neutral Cards + Neutral Buttons (Default)</h2>
<p className="mb-0">
<code>variant="neutral"</code> - Gray cards with matching gray navigation buttons.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CarouselCardList
variant="neutral"
buttonVariant="neutral"
heading="Why Build on the XRP Ledger"
description="Discover the unique features that make XRPL the ideal blockchain for building tokenization, payments, and DeFi applications."
cards={neutralCards}
/>
</section>
{/* Neutral Cards + Green Buttons */}
<section className="py-10">
<PageGrid className="mb-6">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-3">Neutral Cards + Green Buttons</h2>
<p className="mb-0">
<code>variant="neutral" buttonVariant="green"</code> - Gray cards with green navigation buttons.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CarouselCardList
variant="neutral"
buttonVariant="green"
heading="Platform Features"
description="Gray cards paired with vibrant green buttons for emphasis."
cards={neutralCards}
/>
</section>
{/* Neutral Cards + Black Buttons */}
<section className="py-10">
<PageGrid className="mb-6">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-3">Neutral Cards + Black Buttons</h2>
<p className="mb-0">
<code>variant="neutral" buttonVariant="black"</code> - Gray cards with black navigation buttons.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CarouselCardList
variant="neutral"
buttonVariant="black"
heading="Developer Tools"
description="Gray cards paired with black buttons for high contrast."
cards={neutralCards}
/>
</section>
{/* Green Cards + Green Buttons */}
<section className="py-10">
<PageGrid className="mb-6">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-3">Green Cards + Green Buttons</h2>
<p className="mb-0">
<code>variant="green" buttonVariant="green"</code> - Green cards with matching green navigation buttons.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CarouselCardList
variant="green"
buttonVariant="green"
heading="Enterprise Solutions"
description="Purpose-built features for institutional adoption with cohesive green theming."
cards={greenCards}
/>
</section>
{/* Green Cards + Black Buttons */}
<section className="py-10">
<PageGrid className="mb-6">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-3">Green Cards + Black Buttons</h2>
<p className="mb-0">
<code>variant="green" buttonVariant="black"</code> - Green cards with black navigation buttons.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CarouselCardList
variant="green"
buttonVariant="black"
heading="Cross-Border Payments"
description="Green cards with contrasting black buttons for visual interest."
cards={greenCards}
/>
</section>
{/* Navigation Buttons */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Navigation Button Specifications</h2>
<div className="d-flex flex-row gap-6 mb-6" style={{ flexWrap: 'wrap' }}>
<div style={{ flex: '1 1 220px' }}>
<h6 className="mb-3">Dimensions</h6>
<ul className="mb-0">
<li><strong>Desktop:</strong> 40px × 40px</li>
<li><strong>Tablet/Mobile:</strong> 37px × 37px</li>
<li><strong>Gap:</strong> 8px between buttons</li>
</ul>
</div>
<div style={{ flex: '1 1 220px' }}>
<h6 className="mb-3">Neutral Colors (Dark Mode)</h6>
<ul className="mb-0">
<li><strong>Enabled:</strong> $gray-500 (#72777E)</li>
<li><strong>Hover:</strong> $gray-400 (#8A919A)</li>
<li><strong>Disabled:</strong> $gray-500 @ 50%</li>
</ul>
</div>
<div style={{ flex: '1 1 220px' }}>
<h6 className="mb-3">Green Colors (Dark Mode)</h6>
<ul className="mb-0">
<li><strong>Enabled:</strong> $green-300 (#21E46B)</li>
<li><strong>Hover:</strong> $green-200 (#70EE97)</li>
<li><strong>Disabled:</strong> $green-500 @ 50%</li>
</ul>
</div>
<div style={{ flex: '1 1 220px' }}>
<h6 className="mb-3">Black Colors (Dark Mode)</h6>
<ul className="mb-0">
<li><strong>Enabled:</strong> $black (#141414)</li>
<li><strong>Hover:</strong> $gray-500 (#72777E)</li>
<li><strong>Disabled:</strong> $black @ 50%</li>
</ul>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Button Visual Showcase */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Button Visual Showcase</h2>
<p className="mb-6">Interactive carousel buttons showing all variants and states.</p>
{/* Grey/Neutral Buttons */}
<div className="mb-8">
<h5 className="mb-4">Grey (Neutral) Buttons</h5>
<div className="d-flex flex-row gap-4 align-items-center mb-3" style={{ padding: '24px', backgroundColor: 'var(--bs-body-bg)', borderRadius: '8px' }}>
<div className="text-center">
<CarouselButton direction="prev" variant="neutral" aria-label="Previous" />
<div className="mt-2" style={{ fontSize: '12px' }}>Enabled</div>
</div>
<div className="text-center">
<CarouselButton direction="next" variant="neutral" aria-label="Next" />
<div className="mt-2" style={{ fontSize: '12px' }}>Enabled</div>
</div>
<div className="text-center">
<CarouselButton direction="prev" variant="neutral" disabled aria-label="Previous disabled" />
<div className="mt-2" style={{ fontSize: '12px' }}>Disabled</div>
</div>
<div className="text-center">
<CarouselButton direction="next" variant="neutral" disabled aria-label="Next disabled" />
<div className="mt-2" style={{ fontSize: '12px' }}>Disabled</div>
</div>
</div>
</div>
{/* Green Buttons */}
<div className="mb-8">
<h5 className="mb-4">Green Buttons</h5>
<div className="d-flex flex-row gap-4 align-items-center mb-3" style={{ padding: '24px', backgroundColor: 'var(--bs-body-bg)', borderRadius: '8px' }}>
<div className="text-center">
<CarouselButton direction="prev" variant="green" aria-label="Previous" />
<div className="mt-2" style={{ fontSize: '12px' }}>Enabled</div>
</div>
<div className="text-center">
<CarouselButton direction="next" variant="green" aria-label="Next" />
<div className="mt-2" style={{ fontSize: '12px' }}>Enabled</div>
</div>
<div className="text-center">
<CarouselButton direction="prev" variant="green" disabled aria-label="Previous disabled" />
<div className="mt-2" style={{ fontSize: '12px' }}>Disabled</div>
</div>
<div className="text-center">
<CarouselButton direction="next" variant="green" disabled aria-label="Next disabled" />
<div className="mt-2" style={{ fontSize: '12px' }}>Disabled</div>
</div>
</div>
</div>
{/* Black Buttons */}
<div className="mb-8">
<h5 className="mb-4">Black Buttons</h5>
<div className="d-flex flex-row gap-4 align-items-center mb-3" style={{ padding: '24px', backgroundColor: 'var(--bs-body-bg)', borderRadius: '8px' }}>
<div className="text-center">
<CarouselButton direction="prev" variant="black" aria-label="Previous" />
<div className="mt-2" style={{ fontSize: '12px' }}>Enabled</div>
</div>
<div className="text-center">
<CarouselButton direction="next" variant="black" aria-label="Next" />
<div className="mt-2" style={{ fontSize: '12px' }}>Enabled</div>
</div>
<div className="text-center">
<CarouselButton direction="prev" variant="black" disabled aria-label="Previous disabled" />
<div className="mt-2" style={{ fontSize: '12px' }}>Disabled</div>
</div>
<div className="text-center">
<CarouselButton direction="next" variant="black" disabled aria-label="Next disabled" />
<div className="mt-2" style={{ fontSize: '12px' }}>Disabled</div>
</div>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Spacing Tokens */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Spacing Tokens</h2>
<div className="mb-6">
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
<div style={{ width: '200px', flexShrink: 0 }}><strong>Token</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Mobile</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Tablet</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Desktop</strong></div>
</div>
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '200px', flexShrink: 0 }}>Header Gap</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>8px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>8px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>16px</code></div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '200px', flexShrink: 0 }}>Section Gap</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>24px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>32px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>40px</code></div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '200px', flexShrink: 0 }}>Cards Gap</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>8px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>8px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>8px</code></div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '200px', flexShrink: 0 }}>Card Dimensions</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>343×400px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>356×440px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>400×480px</code></div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '200px', flexShrink: 0 }}>Card Padding</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>16px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>20px</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>24px</code></div>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* API Reference */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Component API</h2>
<h5 className="mb-4">CarouselCardListProps</h5>
<div className="mb-8">
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
<div style={{ width: '140px', flexShrink: 0 }}><strong>Prop</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Type</strong></div>
<div style={{ width: '100px', flexShrink: 0 }}><strong>Default</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Description</strong></div>
</div>
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>variant</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>'neutral' | 'green'</code></div>
<div style={{ width: '100px', flexShrink: 0 }}><code>'neutral'</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Color variant for cards</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>buttonVariant</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>'neutral' | 'green' | 'black'</code></div>
<div style={{ width: '100px', flexShrink: 0 }}><code>'neutral'</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Color variant for navigation buttons (independent of cards)</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>heading</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>ReactNode</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Section heading text</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>description</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>ReactNode</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Section description text</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>cards</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>CarouselCardConfig[]</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Array of card configurations</div>
</div>
</div>
<h5 className="mb-4">CarouselCardConfig</h5>
<p className="mb-4">Each card in the <code>cards</code> array accepts the following properties (same as CardOffgrid, without variant):</p>
<div className="mb-6">
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
<div style={{ width: '120px', flexShrink: 0 }}><strong>Prop</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Type</strong></div>
<div style={{ width: '100px', flexShrink: 0 }}><strong>Required</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Description</strong></div>
</div>
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>icon</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>ReactNode | string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Icon component or image URL</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>title</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Card title (use \n for line breaks)</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>description</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Card description text</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>href</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>No</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Link destination URL</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>onClick</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>() =&gt; void</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>No</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Click handler function</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>disabled</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>boolean</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>No</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Disabled state</div>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Usage Example */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Usage Example</h2>
<div className="card p-4">
<pre className="mb-0" style={{ backgroundColor: 'var(--bs-gray-800)', padding: '1rem', borderRadius: '4px', overflow: 'auto' }}>
{`import { CarouselCardList } from 'shared/patterns/CarouselCardList';
// Basic usage - button color matches card color by default
<CarouselCardList
variant="neutral"
heading="Why Build on the XRP Ledger"
description="Discover the unique features that make XRPL ideal for your project."
cards={[
{
icon: <TokenIcon />,
title: "Native\\nTokenization",
description: "Issue and manage digital assets directly on the ledger.",
href: "/docs/tokenization",
},
// ... more cards
]}
/>
// With independent button color
<CarouselCardList
variant="neutral"
buttonVariant="black" // Button color independent of card color
heading="Developer Tools"
description="Gray cards with black navigation buttons."
cards={cards}
/>`}
</pre>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Design References */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Design References</h2>
<div className="d-flex flex-column gap-3">
<div>
<strong>Main Carousel Design:</strong>{' '}
<a href="https://www.figma.com/design/w0CVv1c40nWDRD27mLiMWS/Section-Carousel---Card-List?node-id=15055-3730&m=dev" target="_blank" rel="noopener noreferrer">
Section Carousel - Card List (Figma)
</a>
</div>
<div>
<strong>Button States:</strong>{' '}
<a href="https://www.figma.com/design/w0CVv1c40nWDRD27mLiMWS/Section-Carousel---Card-List?node-id=15055-1033&m=dev" target="_blank" rel="noopener noreferrer">
Carousel Button States (Figma)
</a>
</div>
<div>
<strong>Component Location:</strong>{' '}
<code>shared/patterns/CarouselCardList/</code>
</div>
<div>
<strong>Documentation:</strong>{' '}
<code>shared/patterns/CarouselCardList/CarouselCardList.md</code>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
</div>
</div>
);
}

View File

@@ -0,0 +1,365 @@
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
import { CarouselFeatured, type CarouselSlide, type CarouselFeatureItem } from "shared/patterns/CarouselFeatured";
import { Divider } from "shared/components/Divider";
export const frontmatter = {
seo: {
title: 'CarouselFeatured Pattern Showcase',
description: "A comprehensive showcase of the CarouselFeatured pattern component demonstrating featured image carousels with navigation, background variants, and responsive behavior in the XRPL.org Design System.",
}
};
// Sample image URL for demonstration
const SAMPLE_IMAGE = "/img/demo-bg.png";
// Sample slides data
const sampleSlides: CarouselSlide[] = [
{
id: 1,
imageSrc: SAMPLE_IMAGE,
imageAlt: "Featured slide 1 - XRPL Overview",
},
{
id: 2,
imageSrc: SAMPLE_IMAGE,
imageAlt: "Featured slide 2 - Developer Tools",
},
{
id: 3,
imageSrc: SAMPLE_IMAGE,
imageAlt: "Featured slide 3 - Enterprise Solutions",
},
];
// Sample features data (matching Figma design)
const sampleFeatures: CarouselFeatureItem[] = [
{
title: "Easy-to-Integrate APIs",
description: "Build with common languages and skip complex smart contract development",
},
{
title: "Full Lifecycle Support",
description: "From dev tools and testnets to deployment and growth-stage",
},
{
title: "Enterprise-Grade Security",
description: "Battle-tested infrastructure with 12+ years of continuous uptime",
},
];
export default function CarouselFeaturedShowcase() {
return (
<div className="landing">
<div className="overflow-hidden">
{/* Hero Section */}
<section className="py-26 text-center">
<div className="col-lg-8 mx-auto">
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
<h1 className="mb-4">CarouselFeatured Pattern</h1>
<p className="longform">
A featured image carousel with two-column layout on desktop (image left, content right)
and single-column layout on tablet/mobile (content top, image bottom).
</p>
</div>
</section>
{/* Feature Overview */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Features</h2>
<div className="d-flex flex-row gap-6" style={{ flexWrap: 'wrap' }}>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Layout</h6>
<ul className="mb-0">
<li>Two-column layout on desktop</li>
<li>Image left, content right</li>
<li>Feature list with dividers</li>
<li>Primary + tertiary buttons</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Background Colors</h6>
<ul className="mb-0">
<li><code>gray-200</code> (#E6EAF0) - default</li>
<li><code>gray-300</code> (#CAD4DF) - neutral</li>
<li><code>black</code> (#141414) - dark</li>
<li><code>yellow-100</code> (#F3F1EB) - warm</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Content</h6>
<ul className="mb-0">
<li>Heading (h-md typography)</li>
<li>Feature list items</li>
<li>Primary button (black pill)</li>
<li>Tertiary link (optional)</li>
</ul>
</div>
<div style={{ flex: '1 1 250px' }}>
<h6 className="mb-3">Responsive</h6>
<ul className="mb-0">
<li><strong>Mobile:</strong> Single column, content top</li>
<li><strong>Tablet:</strong> Single column, content top</li>
<li><strong>Desktop:</strong> Two columns, image left</li>
</ul>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<Divider weight="strong" color="gray" />
{/* Default: gray-200 background */}
<section className="py-10">
<PageGrid className="mb-6">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-3">Grey Background</h2>
<p className="mb-0">
<code>background="grey"</code> - Light neutral background, the default option.
Light mode: gray-200 (#E6EAF0), Dark mode: gray-300 (#CAD4DF).
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CarouselFeatured
background="grey"
heading="Powered by Developers"
features={sampleFeatures}
buttons={[
{ label: "Get Started", href: "#get-started" },
{ label: "Learn More", href: "#learn-more" }
]}
slides={sampleSlides.slice(0,1)}
/>
</section>
<Divider weight="strong" color="gray" />
{/* neutral background */}
<section className="py-10">
<PageGrid className="mb-6">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-3">Neutral Background</h2>
<p className="mb-0">
<code>background="neutral"</code> - High contrast neutral background.
Light mode: white (#FFF), Dark mode: black (#141414).
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CarouselFeatured
background="neutral"
heading="Platform Updates"
features={sampleFeatures}
buttons={[
{ label: "View Updates", href: "#updates" },
{ label: "See All", href: "#all" }
]}
slides={sampleSlides}
/>
</section>
<Divider weight="strong" color="gray" />
{/* yellow background */}
<section className="py-10">
<PageGrid className="mb-6">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-3">Yellow Background</h2>
<p className="mb-0">
<code>background="yellow"</code> - Warm secondary background color.
Same in both modes: yellow-100 (#F3F1EB).
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CarouselFeatured
background="yellow"
heading="Community Highlights"
features={sampleFeatures}
buttons={[
{ label: "Join Community", href: "#community" },
{ label: "Learn More", href: "#learn" }
]}
slides={sampleSlides}
/>
</section>
<Divider weight="strong" color="gray" />
{/* Single button example */}
<section className="py-10">
<PageGrid className="mb-6">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-3">Single Button (Same Line on Mobile)</h2>
<p className="mb-0">
When only one button is provided, the button and carousel navigation
stay on the same line on mobile instead of stacking.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<CarouselFeatured
background="grey"
heading="Single Button Example"
features={sampleFeatures}
buttons={[
{ label: "Get Started", href: "#get-started" }
]}
slides={sampleSlides}
/>
</section>
<Divider weight="strong" color="gray" />
{/* API Reference */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Component API</h2>
<h5 className="mb-4">CarouselFeaturedProps</h5>
<div className="mb-8">
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
<div style={{ width: '140px', flexShrink: 0 }}><strong>Prop</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Type</strong></div>
<div style={{ width: '100px', flexShrink: 0 }}><strong>Default</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Description</strong></div>
</div>
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>heading</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Section heading text</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>features</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>CarouselFeatureItem[]</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Array of feature items with title and description</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>buttons</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>ButtonConfig[]</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>optional</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Array of button configurations (1-2 buttons supported, uses ButtonGroup)</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>slides</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>CarouselSlide[]</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>required</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Array of slide configurations</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '140px', flexShrink: 0 }}><code>background</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>'grey' | 'neutral' | 'yellow'</code></div>
<div style={{ width: '100px', flexShrink: 0 }}><code>'grey'</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Background color variant (adapts to light/dark mode)</div>
</div>
</div>
<h5 className="mb-4">CarouselSlide</h5>
<div className="mb-6">
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
<div style={{ width: '120px', flexShrink: 0 }}><strong>Prop</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Type</strong></div>
<div style={{ width: '100px', flexShrink: 0 }}><strong>Required</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Description</strong></div>
</div>
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>id</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string | number</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Unique identifier for the slide</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>imageSrc</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Image source URL</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>imageAlt</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Alt text for the image</div>
</div>
</div>
<h5 className="mb-4">CarouselFeatureItem</h5>
<div className="mb-6">
<div className="d-flex flex-row mb-3 pb-2" style={{ gap: '1rem', borderBottom: '2px solid var(--bs-border-color, #dee2e6)' }}>
<div style={{ width: '120px', flexShrink: 0 }}><strong>Prop</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Type</strong></div>
<div style={{ width: '100px', flexShrink: 0 }}><strong>Required</strong></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><strong>Description</strong></div>
</div>
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>title</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Feature title text</div>
</div>
<Divider weight="thin" color="gray" />
<div className="d-flex flex-row py-3" style={{ gap: '1rem' }}>
<div style={{ width: '120px', flexShrink: 0 }}><code>description</code></div>
<div style={{ flex: '1 1 0', minWidth: 0 }}><code>string</code></div>
<div style={{ width: '100px', flexShrink: 0 }}>Yes</div>
<div style={{ flex: '1 1 0', minWidth: 0 }}>Feature description text</div>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Design References */}
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Design References</h2>
<div className="d-flex flex-column gap-3">
<div>
<strong>Figma:</strong>{' '}
<a href="https://www.figma.com/design/OO2UYKTmDZ7PJIekfaCGAg/Section-Carousel---Feature-Image?node-id=19075-4106" target="_blank" rel="noopener noreferrer">
Section Carousel - Feature Image
</a>
</div>
<div>
<strong>Component Location:</strong>{' '}
<code>shared/patterns/CarouselFeatured/</code>
</div>
<div>
<strong>Shared Button Component:</strong>{' '}
<code>shared/components/CarouselButton/</code>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
</div>
</div>
);
}

View File

@@ -0,0 +1,413 @@
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
import { FeatureSingleTopic } from 'shared/patterns/FeatureSingleTopic';
export const frontmatter = {
seo: {
title: 'FeatureSingleTopic Pattern Showcase',
description: 'Interactive showcase of the FeatureSingleTopic pattern with all variants, orientations, and button configurations.',
},
};
export default function FeatureSingleTopicShowcase() {
// Placeholder image
const placeholderImage = '/img/demo-bg.png';
return (
<div className="landing">
<div className="overflow-hidden">
{/* Hero Section */}
<section className="my-5 text-center">
<div className="col-lg-8 mx-auto">
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
<h1 className="mb-4">FeatureSingleTopic Pattern</h1>
<p className="longform">
A feature section pattern that pairs a title and description with a media element
in a two-column layout. Supports two variants (default and accentSurface) and
left/right orientation for flexible content positioning.
</p>
</div>
</section>
{/* Variant Section */}
<PageGrid className="my-5">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Variants</h2>
<p className="mb-4">
The component supports two variants that control the title section background:
</p>
<ul className="mb-6">
<li><strong>default:</strong> No background on title section</li>
<li><strong>accentSurface:</strong> Gray background (#E6EAF0) on title section</li>
</ul>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Default Variant */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>Default Variant</strong> - <code>variant="default"</code>
<br />
<small className="text-muted">No background on title section. Clean, minimal look.</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureSingleTopic
variant="default"
orientation="left"
title="Developer Spotlight"
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
media={{ src: placeholderImage, alt: "Feature illustration" }}
buttons={[
{ label: "Get Started", href: "#start" },
{ label: "Learn More", href: "#learn" }
]}
/>
</div>
{/* AccentSurface Variant */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>AccentSurface Variant</strong> - <code>variant="accentSurface"</code>
<br />
<small className="text-muted">
Gray background (<code>$gray-200</code> / #E6EAF0) on title section.
</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureSingleTopic
variant="accentSurface"
orientation="left"
title="Developer Spotlight"
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
media={{ src: placeholderImage, alt: "Feature illustration" }}
buttons={[
{ label: "Get Started", href: "#start" },
{ label: "Learn More", href: "#learn" }
]}
/>
</div>
{/* Orientation Section */}
<PageGrid className="my-5">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Orientation Variants</h2>
<p className="mb-6">
Control image/content position with the <code>orientation</code> prop.
Use alternating orientations for visual variety on pages with multiple sections.
</p>
<div className="mb-4 p-3" style={{ backgroundColor: '#f0f3f7', borderRadius: '8px' }}>
<strong>📱 Responsive Behavior:</strong>
<ul className="mb-0 mt-2">
<li><code>orientation="left"</code>: Image left, content right on desktop</li>
<li><code>orientation="right"</code>: Image right, content left on desktop</li>
<li><strong>Mobile/Tablet:</strong> Content always appears above image regardless of orientation</li>
</ul>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Orientation Left */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>Orientation Left (Default)</strong> - <code>orientation="left"</code>
<br />
<small className="text-muted">
Desktop: Image left, content right | Mobile/Tablet: Content above image
</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureSingleTopic
variant="default"
orientation="left"
title="Image on Left"
description="This layout places the image on the left side and content on the right on desktop screens."
media={{ src: placeholderImage, alt: "Left orientation" }}
buttons={[
{ label: "Primary Action", href: "#primary" },
{ label: "Secondary", href: "#secondary" }
]}
/>
</div>
{/* Orientation Right */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>Orientation Right</strong> - <code>orientation="right"</code>
<br />
<small className="text-muted">
Desktop: Image right, content left | Mobile/Tablet: Content above image
</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureSingleTopic
variant="accentSurface"
orientation="right"
title="Image on Right"
description="This layout places the image on the right side and content on the left on desktop screens."
media={{ src: placeholderImage, alt: "Right orientation" }}
buttons={[
{ label: "Primary Action", href: "#primary" },
{ label: "Secondary", href: "#secondary" }
]}
/>
</div>
{/* Button Behavior Section */}
<PageGrid className="my-5">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Button Behavior</h2>
<p className="mb-4">
The component automatically adjusts button rendering based on the number of links provided:
</p>
<ul className="mb-6">
<li><strong>1 link:</strong> Primary or Secondary button (configurable via <code>singleButtonVariant</code> prop)</li>
<li><strong>2 links:</strong> Primary + Tertiary buttons side by side</li>
<li><strong>3+ links:</strong> All Tertiary buttons stacked</li>
</ul>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* 1 Link - Primary */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>ex: 1 button</strong> - Primary Button (default)
<br />
<small className="text-muted">Single action rendered as a primary (filled) button.</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureSingleTopic
variant="default"
orientation="left"
title="Developer Spotlight"
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
media={{ src: placeholderImage, alt: "Single button" }}
buttons={[
{ label: "Primary Link", href: "#start" }
]}
/>
</div>
{/* 1 Link - Secondary */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>ex: 1 button</strong> - Secondary Button
<br />
<small className="text-muted">Single action rendered as a secondary (outlined) button using <code>singleButtonVariant="secondary"</code>.</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureSingleTopic
variant="default"
orientation="left"
title="Developer Spotlight"
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
media={{ src: placeholderImage, alt: "Single button secondary" }}
singleButtonVariant="secondary"
buttons={[
{ label: "Secondary Link", href: "#start" }
]}
/>
</div>
{/* 2 Links */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>ex: 2 button</strong> - Primary + Tertiary Side by Side
<br />
<small className="text-muted">Primary and tertiary buttons displayed side by side on all breakpoints.</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureSingleTopic
variant="default"
orientation="left"
title="Developer Spotlight"
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
media={{ src: placeholderImage, alt: "Two buttons" }}
buttons={[
{ label: "Primary Link", href: "#primary" },
{ label: "Tertiary Link", href: "#tertiary" }
]}
/>
</div>
{/* 5 Links */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>ex: 5 button</strong> - All Tertiary Stacked
<br />
<small className="text-muted">3+ links render as all tertiary buttons stacked vertically.</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureSingleTopic
variant="accentSurface"
orientation="left"
title="Developer Spotlight"
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
media={{ src: placeholderImage, alt: "Multiple buttons" }}
buttons={[
{ label: "Tertiary Link", href: "#link1" },
{ label: "Tertiary Link", href: "#link2" },
{ label: "Tertiary Link", href: "#link3" },
{ label: "Tertiary Link", href: "#link4" },
{ label: "Tertiary Link", href: "#link5" }
]}
/>
</div>
{/* 3 Links */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>ex: 3 button</strong> - All Tertiary Stacked
<br />
<small className="text-muted">3+ links render as all tertiary buttons stacked vertically.</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureSingleTopic
variant="default"
orientation="left"
title="Developer Spotlight"
description="Are you building a peer-to-peer payments solution, integrating stablecoins, or exploring RLUSD on the XRP Ledger?"
media={{ src: placeholderImage, alt: "Three buttons" }}
buttons={[
{ label: "Tertiary Link", href: "#link1" },
{ label: "Tertiary Link", href: "#link2" },
{ label: "Tertiary Link", href: "#link3" }
]}
/>
</div>
{/* Alternating Pattern Example */}
<PageGrid className="my-5">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Alternating Pattern</h2>
<p className="mb-6">
Use alternating orientations and variants to create visual rhythm on feature-heavy pages.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureSingleTopic
variant="default"
orientation="left"
title="First Feature"
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products."
buttons={[{ label: "Learn More", href: "#learn" }]}
media={{ src: placeholderImage, alt: "First feature" }}
/>
<FeatureSingleTopic
variant="accentSurface"
orientation="right"
title="Second Feature"
description="Build powerful applications on XRPL with comprehensive documentation and tools."
buttons={[
{ label: "Get Started", href: "#start" },
{ label: "Documentation", href: "#docs" }
]}
media={{ src: placeholderImage, alt: "Second feature" }}
/>
<FeatureSingleTopic
variant="default"
orientation="left"
title="Third Feature"
description="Scale your business with blockchain technology and enterprise-grade solutions."
buttons={[
{ label: "Contact Sales", href: "#contact" },
{ label: "View Plans", href: "#plans" }
]}
media={{ src: placeholderImage, alt: "Third feature" }}
/>
{/* Design References */}
<PageGrid className="my-5">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Design References</h2>
<div className="d-flex flex-column gap-3">
<div>
<strong>Figma Design (Default):</strong>{' '}
<a href="https://www.figma.com/design/sg6T5EptbN0V2olfCSHzcx/Section-Feature---Single-Topic?node-id=18030-2250&m=dev" target="_blank" rel="noopener noreferrer">
Section Feature - Single Topic (Default Variant)
</a>
</div>
<div>
<strong>Figma Design (AccentSurface):</strong>{' '}
<a href="https://www.figma.com/design/sg6T5EptbN0V2olfCSHzcx/Section-Feature---Single-Topic?node-id=18030-2251&m=dev" target="_blank" rel="noopener noreferrer">
Section Feature - Single Topic (AccentSurface Variant)
</a>
</div>
<div>
<strong>Component Location:</strong>{' '}
<code>shared/patterns/FeatureSingleTopic/</code>
</div>
<div>
<strong>Color Tokens:</strong>{' '}
<code>styles/_colors.scss</code>
</div>
<div>
<strong>Typography:</strong>{' '}
<code>styles/_font.scss</code>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
</div>
</div>
);
}

View File

@@ -0,0 +1,427 @@
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
import { FeatureTwoColumn } from 'shared/patterns/FeatureTwoColumn';
export const frontmatter = {
seo: {
title: 'FeatureTwoColumn Pattern Showcase',
description: 'Interactive showcase of the FeatureTwoColumn pattern with all color variants, arrangements, and button configurations.',
},
};
export default function FeatureTwoColumnShowcase() {
// Placeholder image
const placeholderImage = '/img/demo-bg.png';
return (
<div className="landing">
<div className="overflow-hidden">
{/* Hero Section */}
<section className="my-5 text-center">
<div className="col-lg-8 mx-auto">
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
<h1 className="mb-4">FeatureTwoColumn Pattern</h1>
<p className="longform">
A feature section pattern that pairs editorial content with a media element
in a two-column layout. Supports four color themes, left/right arrangements,
and automatic button configuration based on link count.
</p>
</div>
</section>
{/* Button Behavior Section */}
<PageGrid className="my-5">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Button Behavior</h2>
<p className="mb-4">
The component uses the ButtonGroup pattern which automatically adjusts button rendering based on the number of links provided:
</p>
<ul className="mb-6">
<li><strong>1 link:</strong> Secondary button</li>
<li><strong>2 links:</strong> Primary + Tertiary buttons (responsive layout)</li>
<li><strong>3+ links:</strong> All Tertiary buttons in block layout (vertical on all screen sizes)</li>
</ul>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* 1 Link - Secondary Button */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>1 Link</strong> - Secondary Button
<br />
<small className="text-muted">Single action rendered as a secondary (outline) button.</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureTwoColumn
color="lilac"
arrange="left"
title="Institutions"
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
links={[
{ label: "Secondary Link", href: "#link1" }
]}
media={{ src: placeholderImage, alt: "Feature illustration" }}
/>
</div>
{/* 2 Links - Primary + Tertiary */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>2 Links</strong> - Primary + Tertiary Buttons
<br />
<small className="text-muted">Primary action with a secondary tertiary link.</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureTwoColumn
color="neutral"
arrange="left"
title="Institutions"
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
links={[
{ label: "Primary Link", href: "#link1" },
{ label: "Tertiary Link", href: "#link2" }
]}
media={{ src: placeholderImage, alt: "Feature illustration" }}
/>
</div>
{/* 5 Links - Multiple Tertiary */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>5 Links</strong> - Multiple Links Configuration
<br />
<small className="text-muted">Primary + Tertiary in first row, Secondary below, remaining as Tertiary list.</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureTwoColumn
color="neutral"
arrange="left"
title="Institutions"
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
links={[
{ label: "Primary Link", href: "#link1" },
{ label: "Tertiary Link", href: "#link2" },
{ label: "Secondary Link", href: "#link3" },
{ label: "Tertiary Link", href: "#link4" },
{ label: "Tertiary Link", href: "#link5" }
]}
media={{ src: placeholderImage, alt: "Feature illustration" }}
/>
</div>
{/* Color Variants Section */}
<PageGrid className="my-5">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Color Variants</h2>
<p className="mb-6">
Four color themes available: neutral, lilac, yellow, and green.
Each adapts automatically for light and dark modes.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Neutral Variant */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>Neutral</strong> - <code>color="neutral"</code>
<br />
<small className="text-muted">
Light: <code>$gray-100</code> (#F0F3F7) | Dark: <code>$gray-200</code> (#E6EAF0)
<br />
From <code>styles/_colors.scss</code>
</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureTwoColumn
color="neutral"
arrange="left"
title="Institutions"
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
links={[
{ label: "Get Started", href: "#start" },
{ label: "Learn More", href: "#learn" }
]}
media={{ src: placeholderImage, alt: "Neutral theme" }}
/>
</div>
{/* Lilac Variant */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>Lilac</strong> - <code>color="lilac"</code>
<br />
<small className="text-muted">
Light: <code>$lilac-200</code> (#D9CAFF) | Dark: <code>$lilac-200</code> (#D9CAFF)
<br />
From <code>styles/_colors.scss</code>
</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureTwoColumn
color="lilac"
arrange="left"
title="Institutions"
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
links={[
{ label: "Get Started", href: "#start" },
{ label: "Learn More", href: "#learn" }
]}
media={{ src: placeholderImage, alt: "Lilac theme" }}
/>
</div>
{/* Yellow Variant */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>Yellow</strong> - <code>color="yellow"</code>
<br />
<small className="text-muted">
Light: <code>$yellow-100</code> (#F3F1EB) | Dark: <code>$yellow-100</code> (#F3F1EB)
<br />
From <code>styles/_colors.scss</code>
</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureTwoColumn
color="yellow"
arrange="left"
title="Institutions"
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
links={[
{ label: "Get Started", href: "#start" },
{ label: "Learn More", href: "#learn" }
]}
media={{ src: placeholderImage, alt: "Yellow theme" }}
/>
</div>
{/* Green Variant */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>Green</strong> - <code>color="green"</code>
<br />
<small className="text-muted">
Light: <code>$green-300</code> (#21E46B) | Dark: <code>$green-300</code> (#21E46B)
<br />
From <code>styles/_colors.scss</code>
</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureTwoColumn
color="green"
arrange="left"
title="Institutions"
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
links={[
{ label: "Get Started", href: "#start" },
{ label: "Learn More", href: "#learn" }
]}
media={{ src: placeholderImage, alt: "Green theme" }}
/>
</div>
{/* Arrangement Section */}
<PageGrid className="my-5">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Arrangement Variants</h2>
<p className="mb-6">
Control content position with the <code>arrange</code> prop.
Use alternating arrangements for visual variety on pages with multiple sections.
</p>
<div className="mb-4 p-3" style={{ backgroundColor: '#f0f3f7', borderRadius: '8px' }}>
<strong>📱 Responsive Behavior:</strong>
<ul className="mb-0 mt-2">
<li><code>arrange="left"</code>: Content above media on mobile/tablet, content left on desktop</li>
<li><code>arrange="right"</code>: Media above content on mobile/tablet, content right on desktop</li>
</ul>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
{/* Arrange Left */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>Arrange Left (Default)</strong> - <code>arrange="left"</code>
<br />
<small className="text-muted">
Desktop: Content left, media right | Mobile/Tablet: Content above media
</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureTwoColumn
color="lilac"
arrange="left"
title="Content Left"
description="This content appears on the left side of the layout on desktop, and above the media on mobile/tablet. This is the default arrangement."
links={[
{ label: "Primary", href: "#primary" },
{ label: "Learn More", href: "#secondary" }
]}
media={{ src: placeholderImage, alt: "Left arrangement" }}
/>
</div>
{/* Arrange Right */}
<div className="mb-5">
<PageGrid>
<PageGridRow>
<PageGridCol span={12}>
<div className="mb-3">
<strong>Arrange Right</strong> - <code>arrange="right"</code>
<br />
<small className="text-muted">
Desktop: Content right, media left | Mobile/Tablet: Media above content
</small>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureTwoColumn
color="yellow"
arrange="right"
title="Content Right"
description="This content appears on the right side on desktop, and below the media on mobile/tablet. The media-first approach works well for visual hierarchy."
links={[
{ label: "Primary", href: "#primary" },
{ label: "Learn More", href: "#secondary" }
]}
media={{ src: placeholderImage, alt: "Right arrangement" }}
/>
</div>
{/* Alternating Pattern Example */}
<PageGrid className="my-5">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Alternating Pattern</h2>
<p className="mb-6">
Use alternating arrangements and colors to create visual rhythm on feature-heavy pages.
</p>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeatureTwoColumn
color="neutral"
arrange="left"
title="First Feature"
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products."
links={[{ label: "Learn More", href: "#learn" }]}
media={{ src: placeholderImage, alt: "First feature" }}
/>
<FeatureTwoColumn
color="lilac"
arrange="right"
title="Second Feature"
description="Build powerful applications on XRPL with comprehensive documentation and tools."
links={[
{ label: "Get Started", href: "#start" },
{ label: "Documentation", href: "#docs" }
]}
media={{ src: placeholderImage, alt: "Second feature" }}
/>
<FeatureTwoColumn
color="yellow"
arrange="left"
title="Third Feature"
description="Scale your business with blockchain technology and enterprise-grade solutions."
links={[
{ label: "Contact Sales", href: "#contact" },
{ label: "View Plans", href: "#plans" }
]}
media={{ src: placeholderImage, alt: "Third feature" }}
/>
<FeatureTwoColumn
color="green"
arrange="right"
title="Fourth Feature"
description="Join thousands of developers building the future of finance on XRPL."
links={[
{ label: "Start Building", href: "#build" },
{ label: "Tutorials", href: "#tutorials" },
{ label: "API Reference", href: "#api" }
]}
media={{ src: placeholderImage, alt: "Fourth feature" }}
/>
{/* Design References */}
<PageGrid className="my-5">
<PageGridRow>
<PageGridCol span={12}>
<h2 className="h4 mb-6">Design References</h2>
<div className="d-flex flex-column gap-3">
<div>
<strong>Figma Design:</strong>{' '}
<a href="https://www.figma.com/design/3tmqxMrEvOVvpYhgOCxv2D/Pattern-Feature---Two-Column?node-id=20017-3501&m=dev" target="_blank" rel="noopener noreferrer">
Pattern - Feature - Two Column (Figma)
</a>
</div>
<div>
<strong>Component Location:</strong>{' '}
<code>shared/patterns/FeatureTwoColumn/</code>
</div>
<div>
<strong>Color Tokens:</strong>{' '}
<code>styles/_colors.scss</code>
</div>
<div>
<strong>Typography:</strong>{' '}
<code>styles/_font.scss</code>
</div>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
</div>
</div>
);
}

View File

@@ -0,0 +1,495 @@
import * as React from "react";
import {
PageGrid,
PageGridRow,
PageGridCol,
} from "shared/components/PageGrid/page-grid";
import FeaturedVideoHero from "shared/patterns/FeaturedVideoHero/FeaturedVideoHero";
export const frontmatter = {
seo: {
title: "FeaturedVideoHero Pattern Showcase",
description:
"Interactive showcase of the FeaturedVideoHero pattern with video hero, CTAs, and responsive behavior.",
},
};
const DemoCaption = ({
title,
description,
code,
}: {
title: string;
description?: string;
code?: string;
}) => (
<div className="mb-6">
<h3 className="h4 mb-4">{title}</h3>
{description && <p className="mb-6">{description}</p>}
{code && (
<div
className="mb-6 p-4 bg-light br-4 text-black"
style={{
fontFamily: "monospace",
fontSize: "14px",
overflow: "auto",
}}
>
<pre style={{ margin: 0, whiteSpace: "pre-wrap", color: "#000" }}>
{code}
</pre>
</div>
)}
</div>
);
const placeholderVideo =
"https://cdn.sanity.io/files/ior4a5y3/production/6e2fcba46e3f045a5570c86fd5d20d5ba93d6aad.mp4";
export default function FeaturedVideoHeroShowcase() {
const videoRef = React.useRef<HTMLVideoElement>(null);
React.useEffect(() => {
if (videoRef.current) {
console.log("FeaturedVideoHero video element:", videoRef.current);
}
}, []);
return (
<div className="landing">
<PageGrid className="py-26">
<PageGridRow>
<PageGridCol>
<div className="text-center mb-26">
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
<h1 className="h2 mb-4">FeaturedVideoHero Pattern</h1>
<p className="longform">
A page-level hero pattern featuring a headline, optional
subtitle, call-to-action buttons, and a featured video. The
video uses native HTML video element props and is displayed in a
responsive two-column layout with content on the left and video
on the right.
</p>
</div>
</PageGridCol>
</PageGridRow>
{/* Basic Usage */}
<PageGridRow>
<PageGridCol>
<DemoCaption
title="Basic Usage with Video"
description="The simplest implementation with a headline, subtitle, primary CTA, and video. This example assigns a ref to the video element and logs it to the console on mount."
code={`const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (videoRef.current) {
console.log("FeaturedVideoHero video element:", videoRef.current);
}
}, []);
<FeaturedVideoHero
headline="Build on XRPL"
subtitle={
<>
<p>Issue, manage, and trade real-world assets without needing to build smart contracts.</p>
<p>XRP Ledger's built-in functionality and compliance-enabling features allow asset tokenization without additional layers of complexity.</p>
</>
}
callsToAction={[
{ children: "Get Started", href: "/docs" }
]}
videoElement={{
ref: videoRef,
src: "/video/intro.mp4",
autoPlay: true,
loop: true,
muted: true,
playsInline: true
}}
/>`}
/>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeaturedVideoHero
headline="Build on XRPL"
subtitle={
<>
<p>
Issue, manage, and trade real-world assets without needing to
build smart contracts.
</p>
<p>
XRP Ledger's built-in functionality and compliance-enabling
features allow asset tokenization without additional layers of
complexity.
</p>
</>
}
callsToAction={[{ children: "Get Started", href: "/docs" }]}
videoElement={{
ref: videoRef,
src: placeholderVideo,
autoPlay: true,
loop: true,
muted: true,
playsInline: true,
}}
/>
<PageGrid className="py-26">
{/* Primary + Secondary CTA */}
<PageGridRow>
<PageGridCol>
<DemoCaption
title="Primary and Secondary CTAs"
description="Include both primary and secondary call-to-action buttons."
code={`<FeaturedVideoHero
headline="Real-world asset tokenization"
subtitle="Learn how to issue crypto tokens and build tokenization solutions."
callsToAction={[
{ children: "Get Started", href: "/docs" },
{ children: "Learn More", href: "/about" }
]}
videoElement={{
src: "/video/tokenization.mp4",
autoPlay: true,
loop: true,
muted: true,
playsInline: true
}}
/>`}
/>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeaturedVideoHero
headline="Real-world asset tokenization"
subtitle="Learn how to issue crypto tokens and build tokenization solutions."
callsToAction={[
{ children: "Get Started", href: "/docs" },
{ children: "Learn More", href: "/about" },
]}
videoElement={{
src: placeholderVideo,
autoPlay: true,
loop: true,
muted: true,
playsInline: true,
}}
/>
<PageGrid className="py-26">
{/* Video with extended props */}
<PageGridRow>
<PageGridCol>
<DemoCaption
title="Extended Video Props"
description="Use native video element props for controls, preload, poster, etc."
code={`<FeaturedVideoHero
headline="Watch and Learn"
subtitle="Explore our video tutorials and guides."
callsToAction={[
{ children: "Watch Tutorials", href: "/tutorials" }
]}
videoElement={{
src: "/video/intro.mp4",
autoPlay: true,
loop: true,
muted: true,
playsInline: true,
controls: true,
preload: "metadata"
}}
/>`}
/>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeaturedVideoHero
headline="Watch and Learn"
subtitle="Explore our video tutorials and guides."
callsToAction={[{ children: "Watch Tutorials", href: "/tutorials" }]}
videoElement={{
src: placeholderVideo,
autoPlay: false,
loop: true,
muted: true,
playsInline: true,
controls: true,
preload: "metadata",
}}
/>
<PageGrid className="py-26">
{/* Validation Examples */}
<PageGridRow>
<PageGridCol>
<div className="mb-26">
<h2 className="h3 mb-6">Validation Examples</h2>
<p className="mb-6">
The component includes development-time validation that logs
warnings to the console when required props (headline,
videoElement) are missing. The component will return null and
not render when validation fails. The callsToAction prop is
optional; when provided, at least one non-empty CTA is needed to
show the CTA section.
</p>
</div>
</PageGridCol>
</PageGridRow>
{/* Primary CTA Only */}
<PageGridRow>
<PageGridCol>
<DemoCaption
title="Primary CTA Only (No Secondary)"
description="The secondary CTA is optional. When omitted, only the primary CTA button is displayed."
code={`<FeaturedVideoHero
headline="Single Call to Action"
subtitle="Focus on one primary action for better conversion."
callsToAction={[
{ children: "Get Started", href: "/docs" }
]}
videoElement={{
src: "/video/intro.mp4",
autoPlay: true,
loop: true,
muted: true,
playsInline: true
}}
/>`}
/>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeaturedVideoHero
headline="Single Call to Action"
subtitle="Focus on one primary action for better conversion."
callsToAction={[{ children: "Get Started", href: "/docs" }]}
videoElement={{
src: placeholderVideo,
autoPlay: true,
loop: true,
muted: true,
playsInline: true,
}}
/>
<PageGrid className="py-26">
{/* Without subtitle */}
<PageGridRow>
<PageGridCol>
<DemoCaption
title="Without Subtitle"
description="Subtitle is optional. The component renders without a subtitle section when omitted."
code={`<FeaturedVideoHero
headline="Headline Only"
callsToAction={[
{ children: "Get Started", href: "/docs" }
]}
videoElement={{
src: "/video/intro.mp4",
autoPlay: true,
loop: true,
muted: true,
playsInline: true
}}
/>`}
/>
</PageGridCol>
</PageGridRow>
</PageGrid>
<FeaturedVideoHero
headline="Headline Only"
callsToAction={[{ children: "Get Started", href: "/docs" }]}
videoElement={{
src: placeholderVideo,
autoPlay: true,
loop: true,
muted: true,
playsInline: true,
}}
/>
<PageGrid className="py-26">
{/* Props Documentation */}
<PageGridRow>
<PageGridCol>
<div className="mb-26">
<h2 className="h3 mb-6">Props Documentation</h2>
<h4 className="h5 mb-4">FeaturedVideoHeroProps</h4>
<div className="mb-6">
<ul>
<li>
<code>headline</code> (required) -{" "}
<code>React.ReactNode</code> - Hero headline text
</li>
<li>
<code>subtitle</code> (optional) -{" "}
<code>React.ReactNode</code> - Hero subtitle text
</li>
<li>
<code>callsToAction</code> (optional) -{" "}
<code>DesignConstrainedCallsToActions</code> - Array with
primary CTA and optional secondary CTA. Omit or pass
empty/non-rendering CTAs to hide the CTA section.
</li>
<li>
<code>videoElement</code> (required) - Native{" "}
<code>&lt;video&gt;</code> element props (e.g. src,
autoPlay, loop, muted, playsInline, controls, preload,
poster)
</li>
<li>
<code>className</code> (optional) - <code>string</code> -
Additional CSS classes for the header element
</li>
<li>
All standard HTML <code>&lt;header&gt;</code> attributes are
supported
</li>
</ul>
</div>
<h4 className="h5 mb-4">Button Props (callsToAction)</h4>
<p className="mb-4">
The <code>callsToAction</code> prop accepts design-constrained
Button props; <code>variant</code> and <code>color</code> are
set by the component:
</p>
<ul>
<li>
Primary CTA: <code>variant="primary"</code>,{" "}
<code>color="green"</code>
</li>
<li>
Secondary CTA: <code>variant="tertiary"</code>,{" "}
<code>color="green"</code>
</li>
<li>
All other Button props are supported (e.g.,{" "}
<code>children</code>, <code>href</code>, <code>onClick</code>
, etc.)
</li>
</ul>
</div>
</PageGridCol>
</PageGridRow>
{/* Code Examples */}
<PageGridRow>
<PageGridCol>
<div className="mb-26">
<h2 className="h3 mb-6">Code Examples</h2>
<h4 className="h5 mb-4">Import</h4>
<div
className="p-4 bg-light br-4 mb-6"
style={{ fontFamily: "monospace", fontSize: "14px" }}
>
<pre style={{ margin: 0, color: "#000" }}>
{`import { FeaturedVideoHero } from "shared/patterns/FeaturedVideoHero";`}
</pre>
</div>
<h4 className="h5 mb-4">Basic Example</h4>
<div
className="p-4 bg-light br-4 mb-6"
style={{
fontFamily: "monospace",
fontSize: "14px",
overflow: "auto",
backgroundColor: "#1e1e1e",
color: "#d4d4d4",
}}
>
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
{`<FeaturedVideoHero
headline="Build on XRPL"
subtitle={
<>
<p>Issue, manage, and trade real-world assets without needing to build smart contracts.</p>
<p>XRP Ledger's built-in functionality and compliance-enabling features allow asset tokenization without additional layers of complexity.</p>
</>
}
callsToAction={[
{ children: "Get Started", href: "/docs" }
]}
videoElement={{
src: "/video/intro.mp4",
autoPlay: true,
loop: true,
muted: true,
playsInline: true
}}
/>`}
</pre>
</div>
<h4 className="h5 mb-4">With Secondary CTA</h4>
<div
className="p-4 bg-light br-4 mb-6"
style={{
fontFamily: "monospace",
fontSize: "14px",
overflow: "auto",
backgroundColor: "#1e1e1e",
color: "#d4d4d4",
}}
>
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
{`<FeaturedVideoHero
headline="Real-world asset tokenization"
subtitle="Learn how to issue crypto tokens and build solutions."
callsToAction={[
{ children: "Get Started", href: "/docs" },
{ children: "Learn More", href: "/about" }
]}
videoElement={{
src: "/video/tokenization.mp4",
autoPlay: true,
loop: true,
muted: true,
playsInline: true
}}
/>`}
</pre>
</div>
</div>
</PageGridCol>
</PageGridRow>
{/* Best Practices */}
<PageGridRow>
<PageGridCol>
<div className="mb-26">
<h2 className="h3 mb-6">Best Practices</h2>
<ul>
<li>
<strong>Video format:</strong> Use MP4 with H.264 for broad
compatibility. Keep file sizes reasonable for fast loading.
</li>
<li>
<strong>Autoplay:</strong> Use <code>muted</code> and{" "}
<code>playsInline</code> with <code>autoPlay</code> for
reliable autoplay on mobile.
</li>
<li>
<strong>CTAs:</strong> Keep CTA text concise and
action-oriented. Primary CTA should be the main action.
</li>
<li>
<strong>Headlines:</strong> Keep headlines concise and
impactful. Use the subtitle for additional context.
</li>
<li>
<strong>Responsive design:</strong> The layout stacks on
smaller screens; test across breakpoints to ensure video and
content display correctly.
</li>
</ul>
</div>
</PageGridCol>
</PageGridRow>
</PageGrid>
</div>
);
}

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

View File

@@ -0,0 +1,148 @@
import * as React from "react";
import { LinkSmallGrid } from "shared/patterns/LinkSmallGrid";
import { Divider } from "shared/components/Divider";
export const frontmatter = {
seo: {
title: 'LinkSmallGrid Component Showcase',
description: "A comprehensive showcase of the LinkSmallGrid pattern component with responsive grid layouts, color variants, and usage examples.",
}
};
export default function LinkSmallGridShowcase() {
const handleClick = (message: string) => {
console.log(`Link clicked: ${message}`);
};
// Sample links for demonstrations
const sampleLinks = [
{ label: "Documentation", href: "/docs" },
{ label: "Tutorials", href: "/tutorials" },
{ label: "API Reference", href: "/api" },
{ label: "Examples", href: "/examples" },
{ label: "Best Practices", href: "/best-practices" },
{ label: "Tools", href: "/tools" },
{ label: "Resources", href: "/resources" },
{ label: "Community", href: "/community" },
];
return (
<div className="landing">
<div className="overflow-hidden">
{/* Hero Section */}
<section className="py-26 text-center">
<div className="col-lg-8 mx-auto">
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
<h1 className="mb-4">LinkSmallGrid Pattern</h1>
<p className="longform">
A responsive grid section pattern for displaying navigational links using TileLink components.
Features a heading, optional description, and a grid of clickable tiles with 2 color variants
and full light/dark mode support.
</p>
</div>
</section>
<Divider color="gray" />
{/* Full Example - Gray Variant */}
<LinkSmallGrid
variant="gray"
heading="Quick Links"
description="Navigate to key sections of our documentation and resources."
links={sampleLinks}
/>
<Divider color="gray" />
{/* Full Example - Lilac Variant */}
<LinkSmallGrid
variant="lilac"
heading="Get Started"
description="Explore tutorials and guides to begin your journey with XRPL."
links={sampleLinks.slice(0, 4)}
/>
<Divider color="gray" />
{/* Gray Variant - Heading Only */}
<LinkSmallGrid
variant="gray"
heading="Developer Resources"
links={sampleLinks.slice(0, 6)}
/>
<Divider color="gray" />
{/* Lilac Variant - With Click Handlers */}
<LinkSmallGrid
variant="lilac"
heading="Interactive Examples"
description="Click any tile to see the onClick handler in action (check console)."
links={[
{ label: "Example 1", onClick: () => handleClick('Example 1') },
{ label: "Example 2", onClick: () => handleClick('Example 2') },
{ label: "Example 3", onClick: () => handleClick('Example 3') },
{ label: "Example 4", onClick: () => handleClick('Example 4') },
]}
/>
<Divider color="gray" />
{/* Different Link Counts */}
<section className=" py-26">
<div className="d-flex flex-column-reverse mb-10">
<h2 className="h4 mb-8">Different Link Counts</h2>
<h6 className="eyebrow mb-3">Responsive Behavior</h6>
</div>
<div className="mb-10">
<h6 className="mb-4">2 Links</h6>
<LinkSmallGrid
variant="gray"
heading="Featured Sections"
links={sampleLinks.slice(0, 2)}
/>
</div>
<div className="mb-10">
<h6 className="mb-4">3 Links</h6>
<LinkSmallGrid
variant="lilac"
heading="Core Topics"
links={sampleLinks.slice(0, 3)}
/>
</div>
<div className="mb-10">
<h6 className="mb-4">5 Links</h6>
<LinkSmallGrid
variant="gray"
heading="Learning Paths"
description="Choose a path to start learning."
links={sampleLinks.slice(0, 5)}
/>
</div>
<div className="mb-10">
<h6 className="mb-4">12 Links</h6>
<LinkSmallGrid
variant="lilac"
heading="Complete Navigation"
description="Full grid with multiple rows."
links={[
...sampleLinks,
{ label: "Blog", href: "/blog" },
{ label: "Events", href: "/events" },
{ label: "Newsletter", href: "/newsletter" },
{ label: "Support", href: "/support" },
]}
/>
</div>
</section>
<Divider color="gray" />
</div>
</div>
);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@ import {
PageGridRow,
PageGridCol,
} from "shared/components/PageGrid/page-grid";
import { SmallTilesSection } from "shared/components/SmallTilesSection/SmallTilesSection";
import { SmallTilesSection } from "shared/patterns/SmallTilesSection/SmallTilesSection";
export const frontmatter = {
seo: {

View File

@@ -0,0 +1,559 @@
import * as React from "react";
import {
PageGrid,
PageGridRow,
PageGridCol,
} from "shared/components/PageGrid/page-grid";
import StandardCardGroupSection, {
type StandardCardPropsWithoutVariant,
} from "shared/patterns/StandardCardGroupSection/StandardCardGroupSection";
export const frontmatter = {
seo: {
title: "StandardCardGroupSection Pattern Showcase",
description:
"Interactive showcase of the StandardCardGroupSection pattern with all variants, responsive behavior, and composition examples.",
},
};
// Demo component for code examples
const CodeDemo = ({
title,
description,
code,
children,
}: {
title: string;
description?: string;
code?: string;
children?: React.ReactNode;
}) => (
<div className="mb-26">
<h3 className="h4 mb-4">{title}</h3>
{description && <p className="mb-6">{description}</p>}
{code && (
<div
className="mb-6 p-4 bg-light br-4 text-black"
style={{
fontFamily: "monospace",
fontSize: "14px",
overflow: "auto",
}}
>
<pre style={{ margin: 0, whiteSpace: "pre-wrap", color: "#000" }}>
{code}
</pre>
</div>
)}
{children && (
<div
style={{
border: "1px dashed #ccc",
padding: "16px",
backgroundColor: "#f9f9f9",
borderRadius: "8px",
}}
>
{children}
</div>
)}
</div>
);
// Module-level card data to avoid recreating on each render
const BASIC_CARDS: readonly StandardCardPropsWithoutVariant[] = [
{
headline: "Feature 1",
children: "Description of feature 1",
callsToAction: [{ children: "Learn More", href: "/feature1" }],
},
{
headline: "Feature 2",
children: "Description of feature 2",
callsToAction: [{ children: "Learn More", href: "/feature2" }],
},
];
const GREEN_VARIANT_CARDS: readonly StandardCardPropsWithoutVariant[] = [
{
headline: "Developer Tools",
children: "Comprehensive APIs and SDKs for building on XRPL",
callsToAction: [{ children: "Get Started", href: "/docs" }],
},
{
headline: "Payment Solutions",
children: "Fast, low-cost global payment infrastructure",
callsToAction: [{ children: "Learn More", href: "/payments" }],
},
{
headline: "Tokenization",
children: "Issue and manage digital assets on XRPL",
callsToAction: [{ children: "Explore", href: "/tokens" }],
},
{
headline: "DeFi Protocols",
children: "Decentralized finance applications and liquidity pools",
callsToAction: [{ children: "Discover", href: "/defi" }],
},
{
headline: "NFT Marketplace",
children: "Create, trade, and manage non-fungible tokens",
callsToAction: [{ children: "View Marketplace", href: "/nfts" }],
},
{
headline: "Enterprise Solutions",
children: "Scalable blockchain infrastructure for businesses",
callsToAction: [{ children: "Contact Sales", href: "/enterprise" }],
},
];
const NEUTRAL_VARIANT_CARDS: readonly StandardCardPropsWithoutVariant[] = [
{
headline: "Documentation",
children: "Comprehensive guides and API references",
callsToAction: [{ children: "View Docs", href: "/docs" }],
},
{
headline: "Tutorials",
children: "Step-by-step guides and examples",
callsToAction: [{ children: "Browse Tutorials", href: "/tutorials" }],
},
];
const YELLOW_VARIANT_CARDS: readonly StandardCardPropsWithoutVariant[] = [
{
headline: "New Features",
children: "Latest updates and enhancements to the platform",
callsToAction: [{ children: "See What's New", href: "/features" }],
},
{
headline: "Special Offers",
children: "Exclusive deals and promotions for early adopters",
callsToAction: [{ children: "View Offers", href: "/offers" }],
},
{
headline: "Community Events",
children: "Join upcoming workshops, webinars, and meetups",
callsToAction: [{ children: "Browse Events", href: "/events" }],
},
];
const BLUE_VARIANT_CARDS: readonly StandardCardPropsWithoutVariant[] = [
{
headline: "Cross-Border Payments",
children: "Send money globally in seconds",
callsToAction: [{ children: "Learn More", href: "/payments" }],
},
{
headline: "NFT Marketplaces",
children: "Create and trade digital collectibles",
callsToAction: [{ children: "Explore", href: "/nfts" }],
},
{
headline: "Central Bank Digital Currencies",
children: "CBDC infrastructure and solutions",
callsToAction: [{ children: "Read More", href: "/cbdc" }],
},
];
const SECONDARY_CTA_CARDS: readonly StandardCardPropsWithoutVariant[] = [
{
headline: "Enterprise Solutions",
children: "Scalable infrastructure for large organizations",
callsToAction: [
{ children: "Contact Sales", href: "/contact" },
{ children: "View Case Studies", href: "/cases" },
],
},
{
headline: "Developer Platform",
children: "Tools and APIs for building on XRPL",
callsToAction: [
{ children: "Get Started", href: "/start" },
{ children: "View Docs", href: "/docs" },
],
},
];
const SINGLE_CTA_CARDS: readonly StandardCardPropsWithoutVariant[] = [
{
headline: "Documentation",
children: "Complete API reference and guides",
callsToAction: [{ children: "View Docs", href: "/docs" }],
},
{
headline: "Community",
children: "Join developers and builders",
callsToAction: [{ children: "Join Now", href: "/community" }],
},
{
headline: "Blog",
children: "Latest news and updates",
callsToAction: [{ children: "Read Blog", href: "/blog" }],
},
];
export default function StandardCardGroupSectionShowcase() {
return (
<div className="landing">
<PageGridRow>
<PageGridCol>
<div className="text-center mb-26">
<h6 className="eyebrow mb-3">Pattern Showcase</h6>
<h1 className="h2 mb-4">StandardCardGroupSection Pattern</h1>
<p className="longform">
A section pattern that displays a headline, description, and a
responsive grid of StandardCard components. All cards share a
uniform variant determined by the section, ensuring visual
consistency across the group.
</p>
</div>
</PageGridCol>
</PageGridRow>
{/* Basic Usage */}
<PageGridRow>
<PageGridCol>
<CodeDemo
title="Basic Usage"
description="The simplest implementation with a headline, description, variant, and array of cards."
code={`<StandardCardGroupSection
headline="Our Features"
description="Explore what we offer"
variant="neutral"
cards={[
{
headline: "Feature 1",
children: "Description of feature 1",
callsToAction: [
{ children: "Learn More", href: "/feature1" }
]
},
{
headline: "Feature 2",
children: "Description of feature 2",
callsToAction: [
{ children: "Learn More", href: "/feature2" }
]
}
]}
/>`}
>
<StandardCardGroupSection
headline="Our Features"
description="Explore what we offer"
variant="neutral"
cards={BASIC_CARDS}
/>
</CodeDemo>
</PageGridCol>
</PageGridRow>
{/* Variant: Green */}
<PageGridRow>
<PageGridCol>
<CodeDemo
title="Green Variant"
description="Using the green variant for brand-focused content."
code={`<StandardCardGroupSection
headline="XRPL Solutions"
description="Powerful tools and services built on XRPL"
variant="green"
cards={[...]}
/>`}
/>
</PageGridCol>
</PageGridRow>
<StandardCardGroupSection
headline="XRPL Solutions"
description="Powerful tools and services built on XRPL"
variant="green"
cards={GREEN_VARIANT_CARDS}
/>
{/* Variant: Light Gray */}
<PageGridRow>
<PageGridCol>
<CodeDemo
title="Neutral Variant"
description="Using the neutral variant for subtle, neutral content."
code={`<StandardCardGroupSection
headline="Resources"
description="Everything you need to get started"
variant="neutral"
cards={[...]}
/>`}
/>
</PageGridCol>
</PageGridRow>
<StandardCardGroupSection
headline="Resources"
description="Everything you need to get started"
variant="neutral"
cards={NEUTRAL_VARIANT_CARDS}
/>
{/* Variant: Yellow */}
<PageGridRow>
<PageGridCol>
<CodeDemo
title="Yellow Variant"
description="Using the yellow variant for attention-grabbing, high-energy content."
code={`<StandardCardGroupSection
headline="Featured Highlights"
description="Discover our most exciting features and opportunities"
variant="yellow"
cards={[...]}
/>`}
/>
</PageGridCol>
</PageGridRow>
<StandardCardGroupSection
headline="Featured Highlights"
description="Discover our most exciting features and opportunities"
variant="yellow"
cards={YELLOW_VARIANT_CARDS}
/>
{/* Variant: Blue */}
<PageGridRow>
<PageGridCol>
<CodeDemo
title="Blue Variant"
description="Using the blue variant for secondary content sections."
code={`<StandardCardGroupSection
headline="Use Cases"
description="Real-world applications of XRPL technology"
variant="blue"
cards={[...]}
/>`}
/>
</PageGridCol>
</PageGridRow>
<StandardCardGroupSection
headline="Use Cases"
description="Real-world applications of XRPL technology"
variant="blue"
cards={BLUE_VARIANT_CARDS}
/>
{/* With Secondary CTA */}
<PageGridRow>
<PageGridCol>
<CodeDemo
title="Cards with Secondary CTA"
description="Cards can include both primary and secondary call-to-action buttons."
code={`<StandardCardGroupSection
headline="Services"
description="Comprehensive solutions for your needs"
variant="neutral"
cards={[
{
headline: "Service 1",
children: "Description",
callsToAction: [
{ children: "Get Started", href: "/start" },
{ children: "Learn More", href: "/learn" }
]
}
]}
/>`}
/>
</PageGridCol>
</PageGridRow>
<StandardCardGroupSection
headline="Services"
description="Comprehensive solutions for your needs"
variant="neutral"
cards={SECONDARY_CTA_CARDS}
/>
{/* Single CTA Only */}
<PageGridRow>
<PageGridCol>
<CodeDemo
title="Single CTA Only"
description="Cards can have just a primary call-to-action button."
code={`<StandardCardGroupSection
headline="Quick Links"
description="Fast access to key resources"
variant="green"
cards={[
{
headline: "Link 1",
children: "Description",
callsToAction: [
{ children: "Visit", href: "/link1" }
]
}
]}
/>`}
/>
</PageGridCol>
</PageGridRow>
<StandardCardGroupSection
headline="Quick Links"
description="Fast access to key resources"
variant="green"
cards={SINGLE_CTA_CARDS}
/>
{/* Responsive Behavior */}
<PageGridRow>
<PageGridCol>
<div className="mb-26">
<h2 className="h3 mb-6">Responsive Behavior</h2>
<p className="mb-6">
The StandardCardGroupSection automatically adapts its layout based
on screen size:
</p>
<ul className="mb-6">
<li>
<strong>Mobile (base):</strong> 1 column - cards stack
vertically
</li>
<li>
<strong>Tablet (md):</strong> 3 columns - cards display in a
3-column grid
</li>
<li>
<strong>Desktop (lg):</strong> 3 columns - cards display in a
3-column grid
</li>
</ul>
<p className="mb-6">
Resize your browser window to see the responsive behavior in
action.
</p>
</div>
</PageGridCol>
</PageGridRow>
{/* Code Examples */}
<PageGridRow>
<PageGridCol>
<div className="mb-26">
<h2 className="h3 mb-6">Code Examples</h2>
<h4 className="h5 mb-4">Import</h4>
<div
className="p-4 bg-light br-4 mb-6"
style={{ fontFamily: "monospace", fontSize: "14px" }}
>
<pre style={{ margin: 0, color: "#000" }}>
{`import StandardCardGroupSection from "shared/patterns/StandardCardGroupSection/StandardCardGroupSection";`}
</pre>
</div>
<h4 className="h5 mb-4">Basic Example</h4>
<div
className="p-4 bg-light br-4 mb-6"
style={{
fontFamily: "monospace",
fontSize: "14px",
overflow: "auto",
backgroundColor: "#1e1e1e",
color: "#d4d4d4",
}}
>
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
{`<StandardCardGroupSection
headline="Our Features"
description="Explore what we offer"
variant="neutral"
cards={[
{
headline: "Feature 1",
children: "Description of feature 1",
callsToAction: [
{ children: "Learn More", href: "/feature1" }
]
},
{
headline: "Feature 2",
children: "Description of feature 2",
callsToAction: [
{ children: "Learn More", href: "/feature2" }
]
}
]}
/>`}
</pre>
</div>
<h4 className="h5 mb-4">With Secondary CTA</h4>
<div
className="p-4 bg-light br-4 mb-6"
style={{
fontFamily: "monospace",
fontSize: "14px",
overflow: "auto",
backgroundColor: "#1e1e1e",
color: "#d4d4d4",
}}
>
<pre style={{ margin: 0, whiteSpace: "pre-wrap" }}>
{`<StandardCardGroupSection
headline="Services"
description="Comprehensive solutions"
variant="green"
cards={[
{
headline: "Service 1",
children: "Description",
callsToAction: [
{ children: "Get Started", href: "/start" },
{ children: "Learn More", href: "/learn" }
]
}
]}
/>`}
</pre>
</div>
</div>
</PageGridCol>
</PageGridRow>
{/* Best Practices */}
<PageGridRow>
<PageGridCol>
<div className="mb-26">
<h2 className="h3 mb-6">Best Practices</h2>
<ul>
<li>
<strong>Variant Consistency:</strong> All cards in a section
share the same variant. This ensures visual consistency and
prevents individual cards from having different variants.
</li>
<li>
<strong>Card Count:</strong> Aim for multiples of 3 for best
visual balance on desktop (3, 6, 9 cards).
</li>
<li>
<strong>Headlines:</strong> Keep card headlines concise and
impactful (1-2 lines preferred).
</li>
<li>
<strong>Descriptions:</strong> Provide clear, actionable
descriptions (2-3 lines max).
</li>
<li>
<strong>CTAs:</strong> Use action-oriented button text ("Get
Started" not "Click Here").
</li>
<li>
<strong>Section Headline:</strong> Make it descriptive and
specific to help users understand the card group's purpose.
</li>
<li>
<strong>Accessibility:</strong> The component includes ARIA
roles and labels for screen readers. Ensure card headlines are
descriptive.
</li>
</ul>
</div>
</PageGridCol>
</PageGridRow>
</div>
);
}

View File

@@ -0,0 +1,507 @@
import * as React from "react";
import { PageGrid, PageGridRow, PageGridCol } from "shared/components/PageGrid/page-grid";
import { TileLink } from "shared/patterns/TileLinks";
import { Divider } from "shared/components/Divider";
export const frontmatter = {
seo: {
title: 'TileLink Component Showcase',
description: "A comprehensive showcase of all TileLink component variants, states, and responsive behavior in the XRPL.org Design System.",
}
};
export default function TileLinkShowcase() {
const handleClick = (message: string) => {
console.log(`TileLink clicked: ${message}`);
};
return (
<div className="landing">
<div className="overflow-hidden">
{/* Hero Section */}
<section className="py-26 text-center">
<div className="col-lg-8 mx-auto">
<h6 className="eyebrow mb-3">Component Showcase</h6>
<h1 className="mb-4">TileLink Component</h1>
<p className="longform">
A clickable tile component for link grids, featuring text content with an arrow icon.
Supports gray and lilac color variants with full light/dark mode theming.
</p>
</div>
</section>
<Divider color="gray" />
{/* Gray Variant Section */}
<section className="container-new py-10">
<div className="d-flex flex-column-reverse">
<h2 className="h4 mb-8">Gray Variant</h2>
<h6 className="eyebrow mb-3">Color Variants</h6>
</div>
<p className="mb-6 text-muted">
The gray variant uses neutral gray tones. In light mode, it displays with gray-200 background.
In dark mode, it uses gray-500 with white text.
</p>
</section>
<PageGrid>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink
variant="gray"
label="Documentation"
href="/docs"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink
variant="gray"
label="Get Started"
href="/get-started"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink
variant="gray"
label="Tutorials"
onClick={() => handleClick('Tutorials')}
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink
variant="gray"
label="API Reference"
href="/api"
/>
</PageGridCol>
</PageGridRow>
</PageGrid>
<Divider variant="gray" />
{/* Lilac Variant Section */}
<section className="container-new py-26">
<div className="d-flex flex-column-reverse">
<h2 className="h4 mb-8">Lilac Variant</h2>
<h6 className="eyebrow mb-3">Color Variants</h6>
</div>
<p className="mb-6 text-muted">
The lilac variant uses purple/lilac tones. In light mode, it displays with lilac-300 background.
In dark mode, it uses lilac-400 with white text.
</p>
<PageGrid>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink
variant="lilac"
label="Community"
href="/community"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink
variant="lilac"
label="Events"
href="/events"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink
variant="lilac"
label="Blog"
onClick={() => handleClick('Blog')}
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink
variant="lilac"
label="Newsletter"
href="/newsletter"
/>
</PageGridCol>
</PageGridRow>
</PageGrid>
</section>
<Divider variant="gray" />
{/* Mixed Variants Section */}
<section className="container-new py-26">
<div className="d-flex flex-column-reverse">
<h2 className="h4 mb-8">Mixed Variants</h2>
<h6 className="eyebrow mb-3">Combinations</h6>
</div>
<p className="mb-6 text-muted">
Gray and lilac variants can be mixed in the same grid for visual variety.
</p>
<PageGrid>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink
variant="gray"
label="Introduction"
href="/intro"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink
variant="lilac"
label="Quick Start"
href="/quick-start"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink
variant="gray"
label="Concepts"
href="/concepts"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink
variant="lilac"
label="Advanced Topics"
href="/advanced"
/>
</PageGridCol>
</PageGridRow>
</PageGrid>
</section>
<Divider variant="gray" />
{/* Interactive States Section */}
<section className="container-new py-26">
<div className="d-flex flex-column-reverse">
<h2 className="h4 mb-8">Interactive States</h2>
<h6 className="eyebrow mb-3">States</h6>
</div>
<p className="mb-6 text-muted">
TileLink supports multiple interaction states: default, hover, focus, pressed, and disabled.
Hover over the tiles to see the window shade animation.
</p>
<div className="mb-8">
<h6 className="mb-4">Gray Variant States</h6>
<PageGrid>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<div className="mb-2">
<small className="text-muted">Default / Hover / Pressed</small>
</div>
<TileLink
variant="gray"
label="Interactive Link"
href="/link"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<div className="mb-2">
<small className="text-muted">Button with onClick</small>
</div>
<TileLink
variant="gray"
label="Click Handler"
onClick={() => handleClick('Gray button clicked')}
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<div className="mb-2">
<small className="text-muted">Disabled State</small>
</div>
<TileLink
variant="gray"
label="Coming Soon"
disabled
/>
</PageGridCol>
</PageGridRow>
</PageGrid>
</div>
<div className="mb-8">
<h6 className="mb-4">Lilac Variant States</h6>
<PageGrid>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<div className="mb-2">
<small className="text-muted">Default / Hover / Pressed</small>
</div>
<TileLink
variant="lilac"
label="Interactive Link"
href="/link"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<div className="mb-2">
<small className="text-muted">Button with onClick</small>
</div>
<TileLink
variant="lilac"
label="Click Handler"
onClick={() => handleClick('Lilac button clicked')}
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<div className="mb-2">
<small className="text-muted">Disabled State</small>
</div>
<TileLink
variant="lilac"
label="Coming Soon"
disabled
/>
</PageGridCol>
</PageGridRow>
</PageGrid>
</div>
</section>
<Divider variant="gray" />
{/* Responsive Behavior Section */}
<section className="container-new py-26">
<div className="d-flex flex-column-reverse">
<h2 className="h4 mb-8">Responsive Behavior</h2>
<h6 className="eyebrow mb-3">Layout</h6>
</div>
<p className="mb-6 text-muted">
TileLink adapts to different screen sizes. Resize your browser to see the responsive behavior:
</p>
<ul className="mb-6 text-muted">
<li><strong>Mobile (&lt; 576px):</strong> 1 column, 80px height, 12px padding, 16px font</li>
<li><strong>Tablet (576px - 991px):</strong> 2 columns, 88px height, 16px padding, 16px font</li>
<li><strong>Desktop ( 992px):</strong> 4 columns, 96px height, 20px padding, 18px font</li>
</ul>
<PageGrid>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="gray" label="Responsive Tile 1" href="#1" />
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="lilac" label="Responsive Tile 2" href="#2" />
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="gray" label="Responsive Tile 3" href="#3" />
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="lilac" label="Responsive Tile 4" href="#4" />
</PageGridCol>
</PageGridRow>
</PageGrid>
</section>
<Divider variant="gray" />
{/* Large Grid Example */}
<section className="container-new py-26">
<div className="d-flex flex-column-reverse">
<h2 className="h4 mb-8">Large Grid Example</h2>
<h6 className="eyebrow mb-3">Real-World Usage</h6>
</div>
<p className="mb-6 text-muted">
Example of a larger grid with multiple rows, demonstrating how TileLink works in a typical section layout.
</p>
<PageGrid>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="gray" label="Getting Started" href="/start" />
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="lilac" label="Core Concepts" href="/concepts" />
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="gray" label="Tutorials" href="/tutorials" />
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="lilac" label="API Reference" href="/api" />
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="gray" label="Examples" href="/examples" />
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="lilac" label="Best Practices" href="/best-practices" />
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="gray" label="Tools" href="/tools" />
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="lilac" label="Resources" href="/resources" />
</PageGridCol>
</PageGridRow>
</PageGrid>
</section>
<Divider variant="gray" />
{/* Code Examples Section */}
<section className="container-new py-26">
<div className="d-flex flex-column-reverse">
<h2 className="h4 mb-8">Code Examples</h2>
<h6 className="eyebrow mb-3">Implementation</h6>
</div>
<div className="mb-8">
<h6 className="mb-4">Basic Usage</h6>
<div className="p-6-sm p-10-until-sm br-8" style={{ backgroundColor: '#1e1e1e', color: '#d4d4d4' }}>
<pre style={{ margin: 0, overflow: 'auto' }}>
<code>{`import { TileLink } from 'shared/patterns/TileLinks';
// Gray variant with link
<TileLink
variant="gray"
label="Documentation"
href="/docs"
/>
// Lilac variant with click handler
<TileLink
variant="lilac"
label="Get Started"
onClick={() => navigate('/start')}
/>
// Disabled state
<TileLink
variant="gray"
label="Coming Soon"
disabled
/>`}</code>
</pre>
</div>
</div>
<div className="mb-8">
<h6 className="mb-4">Grid Layout with PageGrid</h6>
<div className="p-6-sm p-10-until-sm br-8" style={{ backgroundColor: '#1e1e1e', color: '#d4d4d4' }}>
<pre style={{ margin: 0, overflow: 'auto' }}>
<code>{`import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
import { TileLink } from 'shared/patterns/TileLinks';
<PageGrid>
<PageGrid.Row>
{/* Mobile: 1 column, Tablet: 2 columns, Desktop: 4 columns */}
<PageGrid.Col span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="gray" label="Link 1" href="/link1" />
</PageGrid.Col>
<PageGrid.Col span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="lilac" label="Link 2" href="/link2" />
</PageGrid.Col>
<PageGrid.Col span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="gray" label="Link 3" href="/link3" />
</PageGrid.Col>
<PageGrid.Col span={{ base: 4, md: 4, lg: 3 }}>
<TileLink variant="lilac" label="Link 4" href="/link4" />
</PageGrid.Col>
</PageGrid.Row>
</PageGrid>`}</code>
</pre>
</div>
</div>
<div className="mb-8">
<h6 className="mb-4">Props API</h6>
<div className="p-6-sm p-10-until-sm br-8" style={{ backgroundColor: '#1e1e1e', color: '#d4d4d4' }}>
<pre style={{ margin: 0, overflow: 'auto' }}>
<code>{`interface TileLinkProps {
/** Color variant: 'gray' (default) or 'lilac' */
variant?: 'gray' | 'lilac';
/** Link text/label */
label: string;
/** Link destination - renders as <a> */
href?: string;
/** Click handler - renders as <button> */
onClick?: () => void;
/** Disabled state - prevents interaction */
disabled?: boolean;
/** Additional CSS classes */
className?: string;
}`}</code>
</pre>
</div>
</div>
</section>
<Divider variant="gray" />
{/* Features Section */}
<section className="container-new py-26">
<div className="d-flex flex-column-reverse">
<h2 className="h4 mb-8">Features</h2>
<h6 className="eyebrow mb-3">Component Capabilities</h6>
</div>
<div className="row">
<div className="col-md-6 mb-6">
<h6 className="mb-3">🎨 Color Variants</h6>
<ul className="text-muted">
<li>Gray variant with neutral tones</li>
<li>Lilac variant with purple/lilac tones</li>
<li>Full light and dark mode support</li>
</ul>
</div>
<div className="col-md-6 mb-6">
<h6 className="mb-3"> Animations</h6>
<ul className="text-muted">
<li>Window shade hover effect (bottom-to-top)</li>
<li>Arrow animation on hover</li>
<li>Smooth transitions (200ms cubic-bezier)</li>
</ul>
</div>
<div className="col-md-6 mb-6">
<h6 className="mb-3">📱 Responsive Design</h6>
<ul className="text-muted">
<li>Mobile: 80px height, 12px padding</li>
<li>Tablet: 88px height, 16px padding</li>
<li>Desktop: 96px height, 20px padding</li>
</ul>
</div>
<div className="col-md-6 mb-6">
<h6 className="mb-3"> Accessibility</h6>
<ul className="text-muted">
<li>Proper ARIA labels and roles</li>
<li>Keyboard navigation support</li>
<li>Focus states with visible outlines</li>
<li>Disabled state handling</li>
</ul>
</div>
<div className="col-md-6 mb-6">
<h6 className="mb-3">🔗 Flexible Rendering</h6>
<ul className="text-muted">
<li>Renders as &lt;a&gt; tag when href is provided</li>
<li>Renders as &lt;button&gt; for onClick handlers</li>
<li>Supports disabled state for both</li>
</ul>
</div>
<div className="col-md-6 mb-6">
<h6 className="mb-3">🎯 Grid Integration</h6>
<ul className="text-muted">
<li>Designed to work with PageGrid system</li>
<li>Responsive column spans</li>
<li>Consistent 8px gap between tiles</li>
</ul>
</div>
</div>
</section>
</div>
</div>
);
}

View File

@@ -1,9 +1,7 @@
---
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
---
@@ -261,17 +259,25 @@ 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.
For each node, follow these steps:
Follow these steps to add validator configuration files to each validator:
1. Create a `validators.txt` file in the configuration directory.
1. Create a `validators.txt` file.
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
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/
```
## Start the Network
@@ -285,7 +291,6 @@ 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
@@ -338,9 +343,15 @@ 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. 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`).
1. Open a terminal in the validator_1 container:
2. Run the `rippled server_info` command to check the state of the validator:
```
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:
```
rippled server_info | grep server_state
@@ -354,7 +365,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 %}
3. Verify the number of peers connected to the validator.
4. Verify the number of peers connected to the validator.
```
rippled server_info | grep peers
@@ -366,10 +377,10 @@ Now that the private ledger network is up, you need to verify that **each** vali
"peers" : 2
```
4. Run the following command to check the genesis account information:
5. Run the following command to check the genesis account information:
```
rippled account_info rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh validated strict
rippled account_info rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh validated
```
Sample Output:
@@ -396,7 +407,9 @@ Now that the private ledger network is up, you need to verify that **each** vali
}
```
5. To leave the Docker container shell, enter `exit` in the terminal.
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.
### Perform a test transaction
@@ -439,7 +452,7 @@ Perform a **test** transaction to ensure you can send money to an account.
```
docker exec -it validator_1 \
rippled account_info r9wRwVgL2vWVnKhTPdtxva5vdH7FNw1zPs validated strict
rippled account_info r9wRwVgL2vWVnKhTPdtxva5vdH7FNw1zPs validated
```
Sample Output:

View File

@@ -57,6 +57,7 @@ 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

@@ -23,6 +23,12 @@ interface ButtonProps {
variant?: 'primary' | 'secondary' | 'tertiary';
/** Color theme - green (default) or black */
color?: 'green' | 'black';
/**
* Force the color to remain constant regardless of theme mode.
* When true, the button color will not change between light/dark modes.
* Use this for buttons on colored backgrounds where black should stay black.
*/
forceColor?: boolean;
/** Button content/label */
children: React.ReactNode;
/** Click handler */
@@ -48,6 +54,7 @@ interface ButtonProps {
- `variant`: `'primary'`
- `color`: `'green'`
- `forceColor`: `false`
- `disabled`: `false`
- `type`: `'button'`
- `className`: `''`
@@ -132,6 +139,37 @@ The black theme provides an alternative color scheme:
</Button>
```
### Force Color (Theme-Independent)
By default, black buttons automatically switch to green in dark mode for better visibility. However, when placing buttons on colored backgrounds (e.g., lilac, yellow, green), you may want black buttons to remain black regardless of theme mode.
Use the `forceColor` prop to prevent automatic color switching:
**Usage:**
```tsx
{/* Black button that stays black in both light and dark modes */}
<Button variant="primary" color="black" forceColor onClick={handleClick}>
Always Black
</Button>
{/* Useful for colored backgrounds like in FeatureTwoColumn pattern */}
<FeatureTwoColumn color="lilac">
<Button variant="primary" color="black" forceColor href="/get-started">
Get Started
</Button>
<Button variant="tertiary" color="black" forceColor href="/learn-more">
Learn More
</Button>
</FeatureTwoColumn>
```
**When to use `forceColor`:**
- Buttons on colored backgrounds (lilac, yellow, green variants)
- When you need consistent button colors regardless of user's theme preference
- Pattern components like `FeatureTwoColumn` where black text is required for readability
**Note:** The `forceColor` prop only affects the color behavior; all other button functionality (hover animations, focus states, etc.) remains the same.
## Link Buttons
The Button component can render as an anchor element for navigation by passing the `href` prop. When `href` is provided, the button is wrapped in a Redocly `Link` component for proper routing support within the application.

View File

@@ -553,41 +553,41 @@ html.dark {
stroke: $green-300;
}
// Hover state
// Hover state - use !important to override light mode global styles
&:hover:not(:disabled):not(.bds-btn--disabled) {
color: $green-200;
color: $green-200 !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $green-200;
stroke: $green-200;
color: $green-200 !important;
stroke: $green-200 !important;
}
}
// Focus state
// Focus state - use !important to override light mode global styles
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
color: $green-200;
color: $green-200 !important;
outline: $bds-btn-focus-border-width solid $white;
outline-offset: 2px;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $green-200;
stroke: $green-200;
color: $green-200 !important;
stroke: $green-200 !important;
}
}
// Active state
// Active state - use !important to override light mode global styles
&:active:not(:disabled):not(.bds-btn--disabled) {
color: $green-300;
color: $green-300 !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $green-300;
stroke: $green-300;
color: $green-300 !important;
stroke: $green-300 !important;
}
}
@@ -1109,6 +1109,315 @@ html.dark {
}
}
// =============================================================================
// Force Color Modifier
// =============================================================================
// When .bds-btn--force-color is applied, the button color remains constant
// regardless of theme mode. This is used for buttons on colored backgrounds
// where black should stay black even in dark mode.
//
// Usage: <Button color="black" forceColor />
// =============================================================================
.bds-btn--force-color {
// Black buttons with force-color should maintain black styling in dark mode
&.bds-btn--black {
// Primary black - force black background
&.bds-btn--primary {
color: $bds-btn-primary-black-text !important;
background-color: $bds-btn-primary-black-bg !important;
&::before {
background-color: $bds-btn-primary-black-bg-hover !important;
}
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-primary-black-text !important;
stroke: $bds-btn-primary-black-text !important;
}
&:hover:not(:disabled):not(.bds-btn--disabled),
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
&:active:not(:disabled):not(.bds-btn--disabled) {
color: $bds-btn-primary-black-text !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-primary-black-text !important;
stroke: $bds-btn-primary-black-text !important;
}
}
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
outline-color: $bds-btn-neutral-black !important;
}
}
// Secondary black - force black text/border
&.bds-btn--secondary {
color: $bds-btn-secondary-black-text !important;
border-color: $bds-btn-secondary-black-border !important;
&::before {
background-color: $bds-btn-secondary-black-bg-hover !important;
}
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-secondary-black-text !important;
stroke: $bds-btn-secondary-black-text !important;
}
&:hover:not(:disabled):not(.bds-btn--disabled),
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
&:active:not(:disabled):not(.bds-btn--disabled) {
color: $bds-btn-secondary-black-text !important;
border-color: $bds-btn-secondary-black-border !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-secondary-black-text !important;
stroke: $bds-btn-secondary-black-text !important;
}
}
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
outline-color: $bds-btn-neutral-black !important;
}
}
// Tertiary black - force black text
&.bds-btn--tertiary {
color: $bds-btn-tertiary-black-text !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-tertiary-black-text !important;
stroke: $bds-btn-tertiary-black-text !important;
}
&:hover:not(:disabled):not(.bds-btn--disabled),
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
&:active:not(:disabled):not(.bds-btn--disabled) {
color: $bds-btn-tertiary-black-text !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-tertiary-black-text !important;
stroke: $bds-btn-tertiary-black-text !important;
}
}
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
outline-color: $bds-btn-tertiary-black-focus-outline !important;
}
}
}
}
// =============================================================================
// Force Color Modifier - Dark Mode Overrides
// =============================================================================
// These overrides must have higher specificity than the dark mode color swaps
// to ensure black buttons stay black on colored backgrounds in dark mode.
// =============================================================================
html.dark {
// Primary black with force-color - override dark mode green swap
.bds-btn.bds-btn.bds-btn--primary.bds-btn--primary.bds-btn--black.bds-btn--force-color {
color: $bds-btn-primary-black-text !important;
background-color: $bds-btn-primary-black-bg !important;
&::before {
background-color: $bds-btn-primary-black-bg-hover !important;
}
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-primary-black-text !important;
stroke: $bds-btn-primary-black-text !important;
}
&:hover:not(:disabled):not(.bds-btn--disabled),
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
&:active:not(:disabled):not(.bds-btn--disabled) {
color: $bds-btn-primary-black-text !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-primary-black-text !important;
stroke: $bds-btn-primary-black-text !important;
}
}
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
outline-color: $white !important; // White outline for visibility on dark backgrounds
}
}
// Secondary black with force-color - override dark mode green swap
.bds-btn.bds-btn.bds-btn--secondary.bds-btn--secondary.bds-btn--black.bds-btn--force-color {
color: $bds-btn-secondary-black-text !important;
border-color: $bds-btn-secondary-black-border !important;
background-color: transparent !important;
&::before {
background-color: $bds-btn-secondary-black-bg-hover !important;
}
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-secondary-black-text !important;
stroke: $bds-btn-secondary-black-text !important;
}
&:hover:not(:disabled):not(.bds-btn--disabled),
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
&:active:not(:disabled):not(.bds-btn--disabled) {
color: $bds-btn-secondary-black-text !important;
border-color: $bds-btn-secondary-black-border !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-secondary-black-text !important;
stroke: $bds-btn-secondary-black-text !important;
}
}
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
outline-color: $white !important; // White outline for visibility on dark backgrounds
}
}
// Tertiary black with force-color - override dark mode green swap
.bds-btn.bds-btn.bds-btn--tertiary.bds-btn--tertiary.bds-btn--black.bds-btn--force-color {
color: $bds-btn-tertiary-black-text !important;
background-color: transparent !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-tertiary-black-text !important;
stroke: $bds-btn-tertiary-black-text !important;
}
&:hover:not(:disabled):not(.bds-btn--disabled),
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
&:active:not(:disabled):not(.bds-btn--disabled) {
color: $bds-btn-tertiary-black-text !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-tertiary-black-text !important;
stroke: $bds-btn-tertiary-black-text !important;
}
}
&:focus-visible:not(:disabled):not(.bds-btn--disabled) {
outline-color: $white !important; // White outline for visibility on dark backgrounds
}
}
// Anchor link buttons with force-color - dark mode overrides
a.bds-btn.bds-btn--force-color {
&.bds-btn--primary.bds-btn--black {
color: $bds-btn-primary-black-text !important;
background-color: $bds-btn-primary-black-bg !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-primary-black-text !important;
stroke: $bds-btn-primary-black-text !important;
}
&:hover,
&:focus,
&:focus-visible,
&:active,
&:visited {
color: $bds-btn-primary-black-text !important;
background-color: $bds-btn-primary-black-bg !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-primary-black-text !important;
stroke: $bds-btn-primary-black-text !important;
}
}
}
&.bds-btn--secondary.bds-btn--black {
color: $bds-btn-secondary-black-text !important;
border-color: $bds-btn-secondary-black-border !important;
background-color: transparent !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-secondary-black-text !important;
stroke: $bds-btn-secondary-black-text !important;
}
&:hover,
&:focus,
&:focus-visible,
&:active,
&:visited {
color: $bds-btn-secondary-black-text !important;
border-color: $bds-btn-secondary-black-border !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-secondary-black-text !important;
stroke: $bds-btn-secondary-black-text !important;
}
}
}
&.bds-btn--tertiary.bds-btn--black {
color: $bds-btn-tertiary-black-text !important;
background-color: transparent !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-tertiary-black-text !important;
stroke: $bds-btn-tertiary-black-text !important;
}
&:hover,
&:focus,
&:focus-visible,
&:active,
&:visited {
color: $bds-btn-tertiary-black-text !important;
.bds-btn__icon,
.bds-btn__icon-line,
.bds-btn__icon-chevron {
color: $bds-btn-tertiary-black-text !important;
stroke: $bds-btn-tertiary-black-text !important;
}
}
}
}
}
// =============================================================================
// Tertiary Variant
// =============================================================================
@@ -1591,4 +1900,34 @@ html.dark {
}
}
}
}
// =============================================================================
// No Padding Modifier
// =============================================================================
// When .bds-btn--no-padding is applied, removes all padding and left-aligns content.
// Useful for tertiary buttons in block layouts where left alignment is needed.
// =============================================================================
.bds-btn--no-padding {
padding: 0 !important;
justify-content: flex-start !important;
// Override all state paddings
&:hover:not(:disabled):not(.bds-btn--disabled),
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
&:active:not(:disabled):not(.bds-btn--disabled) {
padding: 0 !important;
}
// Responsive overrides
@include media-breakpoint-down(xl) {
padding: 0 !important;
&:hover:not(:disabled):not(.bds-btn--disabled),
&:focus-visible:not(:disabled):not(.bds-btn--disabled),
&:active:not(:disabled):not(.bds-btn--disabled) {
padding: 0 !important;
}
}
}

View File

@@ -7,6 +7,12 @@ export interface ButtonProps {
variant?: 'primary' | 'secondary' | 'tertiary';
/** Color theme - green (default) or black */
color?: 'green' | 'black';
/**
* Force the color to remain constant regardless of theme mode.
* When true, the button color will not change between light/dark modes.
* Use this for buttons on colored backgrounds where black should stay black.
*/
forceColor?: boolean;
/** Button content/label */
children: React.ReactNode;
/** Click handler */
@@ -25,6 +31,12 @@ export interface ButtonProps {
href?: string;
/** Link target - only applies when href is provided */
target?: '_self' | '_blank';
/**
* Force no padding and left-align text.
* When true, removes all padding and aligns content to the left.
* Useful for tertiary buttons in block layouts where left alignment is needed.
*/
forceNoPadding?: boolean;
}
/**
@@ -99,6 +111,7 @@ const getTextFromChildren = (children: React.ReactNode): string => {
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
color = 'green',
forceColor = false,
children,
onClick,
disabled = false,
@@ -108,6 +121,7 @@ export const Button: React.FC<ButtonProps> = ({
ariaLabel,
href,
target = '_self',
forceNoPadding = false,
}) => {
// Hide icon when disabled (per design spec)
const shouldShowIcon = showIcon && !disabled;
@@ -123,6 +137,8 @@ export const Button: React.FC<ButtonProps> = ({
{
'bds-btn--disabled': disabled,
'bds-btn--no-icon': !shouldShowIcon,
'bds-btn--force-color': forceColor,
'bds-btn--no-padding': forceNoPadding,
},
className
);

View File

@@ -18,14 +18,17 @@
// Design Tokens
// =============================================================================
// 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
// Color variant map: (variant-name: (light-mode-bg, dark-mode-bg))
// null for dark-mode-bg means no change in dark mode
$bds-card-stat-variants: (
'lilac': ($lilac-300, null),
'green': ($green-300, null),
'light-gray': (#E6EAF0, #CAD4DF),
'dark-gray': (#CAD4DF, #8A919A)
);
// Text colors
$bds-card-stat-text: $black; // Neutral black
$bds-card-stat-text: $black; // Neutral black
// Spacing
$bds-card-stat-gap: 8px;
@@ -44,53 +47,56 @@ $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
height: 100%;
justify-content: space-between;
flex: 1;
padding: 8px;
gap: 4px;
// Visual - default to lilac
background-color: nth(map-get($bds-card-stat-variants, 'lilac'), 1);
// Typography
color: $bds-card-stat-text;
// Transitions
transition: transform $bds-card-stat-transition-duration $bds-card-stat-transition-timing;
// Content wrapper
&__content {
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
padding: 8px;
gap: 4px;
// Tablet (md) breakpoint
@include media-breakpoint-up(md) {
padding: 12px;
}
// Desktop (lg+) breakpoint
@include media-breakpoint-up(lg) {
padding: 16px;
}
// 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;
@@ -101,43 +107,21 @@ $bds-card-stat-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
}
// =============================================================================
// Color Variants
// Color Variants (generated from map)
// =============================================================================
// Lilac variant (default)
.bds-card-stat--lilac {
background-color: $bds-card-stat-lilac-bg;
}
@each $variant, $colors in $bds-card-stat-variants {
$light-bg: nth($colors, 1);
$dark-bg: nth($colors, 2);
// Green variant
.bds-card-stat--green {
background-color: $bds-card-stat-green-bg;
}
.bds-card-stat--#{$variant} {
background-color: $light-bg;
}
// Light gray variant
.bds-card-stat--light-gray {
background-color: $bds-card-stat-light-gray-bg;
}
// Dark gray variant
.bds-card-stat--dark-gray {
background-color: $bds-card-stat-dark-gray-bg;
}
// =============================================================================
// Dark Mode Styles
// =============================================================================
html.dark {
.bds-card-stat {
// Light gray variant gets dark-gray background in dark mode
&--light-gray {
background-color: $bds-card-stat-dark-gray-bg; // Darker gray for dark mode
}
// Dark gray variant gets darker gray background in dark mode
&--dark-gray {
background-color: #8A919A; // Darker gray for dark mode
// Dark mode override (only if dark-mode color is defined)
@if $dark-bg != null {
html.dark .bds-card-stat--#{$variant} {
background-color: $dark-bg;
}
}
}
@@ -149,11 +133,12 @@ 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

@@ -1,5 +1,7 @@
import React from 'react';
import { Button } from '../Button';
import { PageGridCol } from '../PageGrid/page-grid';
import type { PageGridBreakpoint } from '../PageGrid/page-grid';
interface ButtonConfig {
/** Button label text */
@@ -10,11 +12,14 @@ interface ButtonConfig {
href?: string;
}
/** Responsive span configuration for PageGridCol */
type SpanConfig = Partial<Record<PageGridBreakpoint, number>>;
export interface CardStatProps {
/** The main statistic to display (e.g., "6 Million+") */
statistic: string;
/** Superscript text for the statistic */
superscript?: '*' | '+' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '0';
/** Superscript text for the statistic (symbols like '*', '+' or numeric strings like '1', '12') */
superscript?: string;
/** Descriptive label for the statistic */
label: string;
/** Background color variant
@@ -28,6 +33,8 @@ export interface CardStatProps {
primaryButton?: ButtonConfig;
/** Secondary button configuration */
secondaryButton?: ButtonConfig;
/** Grid column span configuration - defaults to { base: 4, md: 4, lg: 4 } */
span?: SpanConfig;
/** Additional CSS classes */
className?: string;
}
@@ -52,6 +59,9 @@ export interface CardStatProps {
* primaryButton={{ label: "Learn More", href: "/docs" }}
* />
*/
/** Default span configuration */
const DEFAULT_SPAN: SpanConfig = { base: 4, md: 4, lg: 4 };
export const CardStat: React.FC<CardStatProps> = ({
statistic,
superscript,
@@ -59,6 +69,7 @@ export const CardStat: React.FC<CardStatProps> = ({
variant = 'lilac',
primaryButton,
secondaryButton,
span = DEFAULT_SPAN,
className = '',
}) => {
// Build class names using BEM with bds namespace
@@ -72,57 +83,48 @@ 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">
<PageGridCol span={span}>
<div className={classNames}>
{/* Text section */}
<div className="bds-card-stat__text">
<div className="bds-card-stat__statistic">
{statistic}{superscript && <sup>{superscript}</sup>}</div>
{statistic}{superscript && <sup className={isNumericSuperscript ? 'bds-card-stat__superscript--numeric' : ''}>{superscript}</sup>}</div>
<div className="body-r">{label}</div>
</div>
{/* Buttons section */}
{/* Buttons section */}
{hasButtons && (
<div className="bds-card-stat__buttons">
{primaryButton && (
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>
)
<Button
forceColor
variant="primary"
color="black"
href={primaryButton.href}
onClick={primaryButton.onClick}
>
{primaryButton.label}
</Button>
)}
{secondaryButton && (
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>
)
<Button
forceColor
variant="secondary"
color="black"
href={secondaryButton.href}
onClick={secondaryButton.onClick}
>
{secondaryButton.label}
</Button>
)}
</div>
)}
</div>
</div>
</PageGridCol>
);
};

View File

@@ -0,0 +1,212 @@
// BDS CarouselButton Component Styles
// Brand Design System - Circular navigation button for carousels
//
// Naming Convention: BEM with 'bds' namespace
// .bds-carousel-button - Base button
// .bds-carousel-button--prev - Previous/left direction
// .bds-carousel-button--next - Next/right direction
// .bds-carousel-button--neutral - Neutral/gray color variant
// .bds-carousel-button--green - Green color variant
// .bds-carousel-button--black - Black/white color variant
// .bds-carousel-button--disabled - Disabled state modifier
// .bds-carousel-button__arrow-icon - Arrow icon element
//
// Note: This file is imported within xrpl.scss after Bootstrap and project
// variables are loaded, so $grid-breakpoints, colors, and mixins are available.
// =============================================================================
// Design Tokens
// =============================================================================
// Button dimensions
$bds-carousel-button-size-sm: 37px; // Mobile/Tablet
$bds-carousel-button-size-lg: 40px; // Desktop
// Transition
$bds-carousel-button-transition: 200ms cubic-bezier(0.98, 0.12, 0.12, 0.98);
// =============================================================================
// Color Variant Configuration Maps
// =============================================================================
// Dark Mode color variants
$bds-carousel-button-variants-dark: (
'green': (
'bg': $green-300,
'color': $black,
'hover': $green-400,
'active': $green-300,
'disabled-bg': $green-500,
'disabled-color': #F0F3F7,
'disabled-opacity': 0.5
),
'neutral': (
'bg': $gray-300,
'color': $black,
'hover': $gray-400,
'active': $gray-300,
'disabled-bg': $gray-500,
'disabled-color': $gray-300,
'disabled-opacity': 0.5
),
'black': (
'bg': $white,
'color': $black,
'hover': $gray-300,
'active': $white,
'disabled-bg': $gray-500,
'disabled-color': null,
'disabled-opacity': 0.5
)
);
// Light Mode color variants
$bds-carousel-button-variants-light: (
'green': (
'bg': $green-300,
'color': $black,
'hover': $green-200,
'active': $green-300,
'disabled-bg': $green-100,
'disabled-color': $gray-300,
'disabled-opacity': 1
),
'neutral': (
'bg': $gray-300,
'color': $black,
'hover': $gray-200,
'active': $gray-300,
'disabled-bg': $gray-100,
'disabled-color': $gray-300,
'disabled-opacity': 1
),
'black': (
'bg': $black,
'color': $white,
'hover': $gray-500,
'active': $black,
'disabled-bg': #F0F3F7,
'disabled-color': $gray-300,
'disabled-opacity': 1
)
);
// =============================================================================
// Mixin: Apply Color Variant Styles
// =============================================================================
@mixin carousel-button-variant($variant-name, $config) {
.bds-carousel-button--#{$variant-name} {
background-color: map-get($config, 'bg');
color: map-get($config, 'color');
&:hover:not(:disabled) {
background-color: map-get($config, 'hover');
}
&:active:not(:disabled) {
background-color: map-get($config, 'active');
}
&.bds-carousel-button--disabled,
&:disabled {
background-color: map-get($config, 'disabled-bg') !important;
@if map-get($config, 'disabled-color') {
color: map-get($config, 'disabled-color') !important;
}
opacity: map-get($config, 'disabled-opacity') !important;
cursor: not-allowed !important;
}
}
}
// =============================================================================
// Base Button Styles
// =============================================================================
.bds-carousel-button {
// Reset button styles
appearance: none;
border: none;
background: none;
padding: 0;
margin: 0;
cursor: pointer;
// Layout
display: flex;
align-items: center;
justify-content: center;
width: $bds-carousel-button-size-sm;
height: $bds-carousel-button-size-sm;
// Transition
transition: background-color $bds-carousel-button-transition,
opacity $bds-carousel-button-transition;
@include media-breakpoint-up(lg) {
width: $bds-carousel-button-size-lg;
height: $bds-carousel-button-size-lg;
}
// Focus styles
&:focus {
outline: 2px solid $white;
outline-offset: 2px;
}
&:focus:not(:focus-visible) {
outline: none;
}
&:focus-visible {
outline: 2px solid $white;
outline-offset: 2px;
}
}
// =============================================================================
// Arrow Icon
// =============================================================================
.bds-carousel-button__arrow-icon {
width: 18px;
height: 16px;
@include media-breakpoint-down(lg) {
width: 18px;
height: 15px;
}
}
// =============================================================================
// DARK MODE (Default) - Color Variants
// =============================================================================
// Generate all dark mode variant styles using the mixin
@each $variant-name, $config in $bds-carousel-button-variants-dark {
@include carousel-button-variant($variant-name, $config);
}
// =============================================================================
// LIGHT MODE (html.light) - Color Variants
// =============================================================================
html.light {
// Focus styles - Light Mode
.bds-carousel-button {
&:focus {
outline-color: $gray-900;
}
&:focus-visible {
outline-color: $gray-900;
}
}
// Generate all light mode variant overrides using the mixin
@each $variant-name, $config in $bds-carousel-button-variants-light {
@include carousel-button-variant($variant-name, $config);
}
}

View File

@@ -0,0 +1,107 @@
import React from 'react';
import clsx from 'clsx';
/**
* Props for the CarouselButton component
*/
export interface CarouselButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Arrow direction */
direction: 'prev' | 'next';
/** Color variant */
variant: 'neutral' | 'green' | 'black';
}
/**
* CarouselButton Component
*
* A circular navigation button for carousel components.
* Displays left/right arrow icons and supports multiple color variants.
*
* @example
* ```tsx
* <CarouselButton
* direction="prev"
* variant="neutral"
* onClick={() => scroll('prev')}
* disabled={!canScrollPrev}
* aria-label="Previous items"
* />
* ```
*/
export const CarouselButton: React.FC<CarouselButtonProps> = ({
direction,
variant,
disabled,
className,
...buttonProps
}) => {
return (
<button
type="button"
className={clsx(
'bds-carousel-button',
`bds-carousel-button--${direction}`,
`bds-carousel-button--${variant}`,
{ 'bds-carousel-button--disabled': disabled },
className
)}
disabled={disabled}
{...buttonProps}
>
{direction === 'prev' ? <CarouselArrowIconLeft /> : <CarouselArrowIconRight />}
</button>
);
};
CarouselButton.displayName = 'CarouselButton';
/**
* SVG Arrow Icon for carousel navigation - Right arrow
*/
export const CarouselArrowIconRight: React.FC = () => (
<svg
className="bds-carousel-button__arrow-icon"
width="18"
height="16"
viewBox="0 0 18 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M9.33387 1.33461L15.9999 8.00058L9.33387 14.6666M15.9982 7.99893L-0.000149269 7.99893"
stroke="currentColor"
strokeWidth="1.5"
strokeMiterlimit="10"
/>
</svg>
);
CarouselArrowIconRight.displayName = 'CarouselArrowIconRight';
/**
* SVG Arrow Icon for carousel navigation - Left arrow
*/
export const CarouselArrowIconLeft: React.FC = () => (
<svg
className="bds-carousel-button__arrow-icon"
width="18"
height="15"
viewBox="0 0 18 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M7.72667 0.530285L1.0607 7.19626L7.72667 13.8622M1.06235 7.19461L17.0607 7.19461"
stroke="currentColor"
strokeWidth="1.5"
strokeMiterlimit="10"
/>
</svg>
);
CarouselArrowIconLeft.displayName = 'CarouselArrowIconLeft';
export default CarouselButton;

View File

@@ -0,0 +1,8 @@
export {
CarouselButton,
CarouselArrowIconLeft,
CarouselArrowIconRight,
type CarouselButtonProps
} from './CarouselButton';
export { default } from './CarouselButton';

View File

@@ -17,6 +17,21 @@ $bds-grid-gutter: 8px;
width: calc(((100% - (#{$bds-grid-gutter} * (#{$columns} - 1))) / #{$columns}) * #{$size} + (#{$bds-grid-gutter} * (#{$size} - 1)));
}
// Custom mixin that accounts for gap spacing in offset calculations
@mixin bds-make-col-offset($size, $columns) {
// Calculate margin-left accounting for gap spacing
// Formula: (width per column * offset) + (gap * offset)
// This accounts for both the column widths AND the gaps between them
// Total available width: 100% - (gap * total gaps in grid)
// Width per column: available width / columns
// For offset of $size: (width per column * size) + (gap * size)
@if $size == 0 {
margin-left: 0;
} @else {
margin-left: calc(((100% - (#{$bds-grid-gutter} * (#{$columns} - 1))) / #{$columns}) * #{$size} + (#{$bds-grid-gutter} * #{$size}));
}
}
@mixin bds-grid-generate-cols($columns, $suffix: null) {
@for $i from 1 through $columns {
$selector: if($suffix, ".bds-grid__col-#{$suffix}-#{$i}", ".bds-grid__col-#{$i}");
@@ -49,7 +64,7 @@ $bds-grid-gutter: 8px;
$selector: if($suffix, ".bds-grid__offset-#{$suffix}-#{$i}", ".bds-grid__offset-#{$i}");
#{$selector} {
@include make-col-offset($i, $columns);
@include bds-make-col-offset($i, $columns);
}
}
}

View File

@@ -4,7 +4,7 @@ import clsx from "clsx";
type PageGridElementProps = React.HTMLAttributes<HTMLDivElement>;
// Define the standard PageGrid breakpoints
type PageGridBreakpoint = "base" | "sm" | "md" | "lg" | "xl";
export type PageGridBreakpoint = "base" | "sm" | "md" | "lg" | "xl";
// Define the ResponsiveValue type using Partial<Record> for breakpoints
type ResponsiveValue<T> = T | Partial<Record<PageGridBreakpoint, T>>;

View File

@@ -0,0 +1,83 @@
.bds-standard-card {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 16px;
width: 100%;
height: 100%; // Stretch to fill parent column (ensures equal heights in grid rows)
// aspect-ratio sets a preferred ratio but allows growth if content requires more space
// The card will be at least this tall, but can grow taller if needed
aspect-ratio: 4/3;
gap: 16px;
@include media-breakpoint-up(md) {
aspect-ratio: 1;
padding: 20px;
}
@include media-breakpoint-up(lg) {
padding: 24px;
}
&#{&}--neutral {
background-color: $gray-200;
}
&#{&}--green {
background-color: $green-300;
}
&#{&}--yellow {
background-color: $yellow-100;
}
&#{&}--blue {
background-color: $blue-100;
}
&__content {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
color: $bds-btn-neutral-black;
@include media-breakpoint-up(lg) {
gap: 16px;
}
}
&__headline {
@include wordbreak('break-word');
color: $bds-btn-neutral-black;
margin: 0;
&:before{
display: none;
}
}
&__description {
margin: 0;
}
&__buttons {
display: flex;
flex-direction: column;
width: 100%;
flex-wrap: wrap;
align-items: flex-start;
@include media-breakpoint-up(lg) {
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
}
}

View File

@@ -0,0 +1,104 @@
import React, { forwardRef } from "react";
import clsx from "clsx";
import Button from "../Button/Button";
import {
DesignConstrainedButtonProps,
isEnvironment,
isEmpty,
} from "../../utils";
import { DesignConstrainedCallToActionsProps } from "shared/utils/types";
/**
* Available background color variants for StandardCard:
* - 'neutral': Default neutral background
* - 'green': XRPL brand green background
* - 'yellow': Yellow background
* - 'blue': Blue background
*/
export type StandardCardVariant = "neutral" | "green" | "yellow" | "blue";
export interface StandardCardProps
extends
React.ComponentPropsWithoutRef<"article">,
DesignConstrainedCallToActionsProps {
headline: React.ReactNode;
/** Background color variant */
variant: StandardCardVariant;
children?: React.ReactNode;
}
/**
* StandardCard props without the variant prop.
* Used by StandardCardGroupSection to ensure uniform variant across all cards.
*/
export type StandardCardPropsWithoutVariant = Omit<
StandardCardProps,
"variant"
>;
const StandardCard = forwardRef<HTMLElement, StandardCardProps>(
(props, ref) => {
const {
headline,
variant = "neutral",
callsToAction,
className,
children,
...rest
} = props;
const [primaryButton, secondaryButton] = callsToAction;
const hasButtons = callsToAction.some((button) => !isEmpty(button));
if (!headline) {
if (isEnvironment("development")) {
console.warn("Headline is required for StandardCard");
}
return null;
}
return (
<article
ref={ref}
className={clsx(
"bds-standard-card",
`bds-standard-card--${variant}`,
className,
)}
{...rest}
>
<div className="bds-standard-card__content">
<h2 className="bds-standard-card__headline sh-md-r">{headline}</h2>
{!isEmpty(children) && (
<div className="bds-standard-card__description body-l">
{children}
</div>
)}
</div>
{hasButtons && (
<div className="bds-standard-card__buttons">
{primaryButton && (
<Button
{...primaryButton}
variant="primary"
color="black"
forceColor={true}
/>
)}
{secondaryButton && (
<Button
{...secondaryButton}
variant="tertiary"
color="black"
forceColor={true}
/>
)}
</div>
)}
</article>
);
},
);
export default StandardCard;

View File

@@ -0,0 +1,183 @@
# TextCard Component
A card component with a title at the top and description at the bottom. Used within the CardsTwoColumn pattern to display content in a 2×2 grid, but can also be used independently.
## Features
- **6 Color Variants**: Green, neutral-light, neutral-dark, lilac, yellow, and blue backgrounds
- **Interactive States**: Default, hover (window shade animation), focus, and pressed states
- **Responsive Design**: Adapts height and padding across breakpoints
- **Optional Link**: Can be made clickable with an `href` prop
- **Flexible Content**: Title and description support ReactNode
## Usage
```tsx
import { TextCard } from 'shared/components/TextCard';
// Basic usage
<TextCard
title="Institutions"
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products."
color="green"
/>
// As a clickable card
<TextCard
title="Developers"
description="Build decentralized applications with comprehensive documentation."
href="/developers"
color="lilac"
/>
```
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `title` | `ReactNode` | *required* | Card title (heading-lg typography) |
| `description` | `ReactNode` | - | Card description (body-l typography) |
| `href` | `string` | - | Optional link URL (makes card clickable) |
| `color` | `TextCardColor` | `'neutral-light'` | Background color variant |
| `disabled` | `boolean` | `false` | Whether the card is disabled |
| `className` | `string` | - | Additional CSS classes |
### TextCardColor
```tsx
type TextCardColor = 'green' | 'neutral-light' | 'neutral-dark' | 'lilac' | 'yellow' | 'blue';
```
## Responsive Behavior
| Breakpoint | Height | Padding |
|------------|--------|---------|
| Desktop (≥992px) | 340px | 24px |
| Tablet (576-991px) | 309px | 20px |
| Mobile (<576px) | 274px | 16px |
## Color Variants & States
Each color variant has four interactive states with a "window shade" hover animation.
### Light Mode
| Variant | Default | Hover | Focus | Pressed |
|---------|---------|-------|-------|---------|
| `green` | `$green-200` (#70EE97) | `$green-300` (#21E46B) | `$green-300` (#21E46B) | `$green-400` (#0DAA3E) |
| `neutral-light` | `$gray-200` (#E6EAF0) | `$gray-300` (#CAD4DF) | `$gray-300` (#CAD4DF) | `$gray-400` (#8A919A) |
| `neutral-dark` | `$gray-300` (#CAD4DF) | `$gray-200` (#E6EAF0) | `$gray-200` (#E6EAF0) | `$gray-400` (#8A919A) |
| `lilac` | `$lilac-200` (#D9CAFF) | `$lilac-300` (#C0A7FF) | `$lilac-300` (#C0A7FF) | `$lilac-400` (#7649E3) |
| `yellow` | `$yellow-100` (#F3F1EB) | `$yellow-200` (#E6F1A7) | `$yellow-200` (#E6F1A7) | `$yellow-300` (#DBF15E) |
| `blue` | `$blue-100` (#EDF4FF) | `$blue-200` (#93BFF1) | `$blue-200` (#93BFF1) | `$blue-300` (#428CFF) |
### Dark Mode
| Variant | Default | Hover | Focus | Pressed |
|---------|---------|-------|-------|---------|
| `green` | `$green-200` (#70EE97) | `$green-300` (#21E46B) | `$green-300` (#21E46B) | `$green-400` (#0DAA3E) |
| `neutral-light` | `$gray-300` (#CAD4DF) | `$gray-200` (#E6EAF0) | `$gray-200` (#E6EAF0) | `$gray-400` (#8A919A) |
| `neutral-dark` | `$gray-400` (#8A919A) | `$gray-300` (#CAD4DF) | `$gray-300` (#CAD4DF) | `$gray-500` (#72777E) |
| `lilac` | `$lilac-200` (#D9CAFF) | `$lilac-300` (#C0A7FF) | `$lilac-300` (#C0A7FF) | `$lilac-400` (#7649E3) |
| `yellow` | `$yellow-100` (#F3F1EB) | `$yellow-200` (#E6F1A7) | `$yellow-200` (#E6F1A7) | `$yellow-300` (#DBF15E) |
| `blue` | `$blue-100` (#EDF4FF) | `$blue-200` (#93BFF1) | `$blue-200` (#93BFF1) | `$blue-300` (#428CFF) |
### Disabled State
| Mode | Background | Text |
|------|------------|------|
| Light | `$gray-100` (#F0F3F7) | `$gray-500` (#72777E) |
| Dark | `rgba($gray-500, 0.3)` | Default text color |
### Focus Outline
| Mode | Focus Outline |
|------|---------------|
| Light Mode | 2px solid black outline with 2px offset |
| Dark Mode | 2px solid white outline with 2px offset |
## CSS Classes
```
.bds-text-card // Base card container
.bds-text-card--green // Green variant
.bds-text-card--neutral-light // Neutral light variant
.bds-text-card--neutral-dark // Neutral dark variant
.bds-text-card--lilac // Lilac variant
.bds-text-card--yellow // Yellow variant
.bds-text-card--blue // Blue variant
.bds-text-card__overlay // Hover animation overlay
.bds-text-card__header // Title container
.bds-text-card__title // Title element
.bds-text-card__footer // Description container
.bds-text-card__description // Description element
```
## Typography
- **Title**: Uses `h-lg` class (heading-lg, Tobias Light font)
- Desktop: 48px / 52.8px line-height
- Tablet: 42px / 46.2px line-height
- Mobile: 36px / 39.6px line-height
- **Description**: Uses `body-l` class (Booton Light font)
- All breakpoints: 18px / 26.1px line-height
- Max width: 478px (from Figma)
## Examples
### All Color Variants
```tsx
<TextCard title="Green" description="$green-200" color="green" />
<TextCard title="Neutral Light" description="$gray-200" color="neutral-light" />
<TextCard title="Neutral Dark" description="$gray-300" color="neutral-dark" />
<TextCard title="Lilac" description="$lilac-200" color="lilac" />
<TextCard title="Yellow" description="$yellow-100" color="yellow" />
<TextCard title="Blue" description="$blue-100" color="blue" />
```
### Disabled Card
```tsx
<TextCard
title="Disabled Card"
description="This card is disabled and cannot be interacted with."
color="neutral-light"
disabled
/>
```
### Within CardsTwoColumn Pattern
```tsx
import { CardsTwoColumn } from 'shared/patterns/CardsTwoColumn';
<CardsTwoColumn
title="Section Title"
description="Section description text."
cards={[
{ title: "Card 1", description: "Description 1", color: "lilac" },
{ title: "Card 2", description: "Description 2", color: "neutral-light" },
{ title: "Card 3", description: "Description 3", color: "neutral-dark" },
{ title: "Card 4", description: "Description 4", color: "green" }
]}
/>
```
## Files
- `TextCard.tsx` - Component implementation
- `TextCard.scss` - Styles with color variants and responsive breakpoints
- `index.ts` - Barrel exports
- `TextCard.md` - This documentation
## Related Components
- **CardsTwoColumn**: Pattern that uses TextCard in a 2×2 grid layout
## Design References
- **Figma Design**: [Section Cards - Two Column](https://www.figma.com/design/MP5gjNp7yPBnKBKleb8LRL/Section-Cards---Two-Column)
- **Component Location**: `shared/components/TextCard/`

View File

@@ -0,0 +1,389 @@
// BDS TextCard Component Styles
// Brand Design System - Card with title and description
//
// Naming Convention: BEM with 'bds' namespace
// .bds-text-card - Base card container
// .bds-text-card--green - Green variant
// .bds-text-card--neutral-light - Neutral light variant
// .bds-text-card--neutral-dark - Neutral dark variant
// .bds-text-card--lilac - Lilac variant
// .bds-text-card--yellow - Yellow variant
// .bds-text-card--blue - Blue variant
// .bds-text-card__overlay - Hover gradient overlay (window shade animation)
// .bds-text-card__title - Card title (heading-lg)
// .bds-text-card__description - Card description (body-l)
//
// Color states from Figma (Light Mode):
// - Green: Default $green-200, Hover $green-300, Pressed $green-400
// - NeutralLight: Default $gray-200, Hover $gray-300, Pressed $gray-400
// - NeutralDark: Default $gray-300, Hover $gray-400, Pressed $gray-500
// - Lilac: Default $lilac-200, Hover $lilac-300, Pressed $lilac-400
// - Yellow: Default $yellow-100, Hover $yellow-200, Pressed $yellow-300
// - Blue: Default $blue-100, Hover $blue-200, Pressed $blue-300
// =============================================================================
// Design Tokens from Figma
// =============================================================================
// Card internal padding
$bds-text-card-padding-mobile: 16px;
$bds-text-card-padding-tablet: 20px;
$bds-text-card-padding-desktop: 24px;
// Card heights (fixed per breakpoint)
$bds-text-card-height-mobile: 274px;
$bds-text-card-height-tablet: 309px;
$bds-text-card-height-desktop: 340px;
// Card description max-width (from Figma)
$bds-text-card-description-max-width: 478px;
// Colors - Light Mode (from Figma)
$bds-text-color: $black; // #141414 - Neutral black
// =============================================================================
// TextCard Component
// =============================================================================
.bds-text-card {
// Use shared window shade animation base
@include bds-window-shade-base;
// Layout
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
text-decoration: none;
box-sizing: border-box;
// Mobile dimensions and padding
height: $bds-text-card-height-mobile;
padding: $bds-text-card-padding-mobile;
@include media-breakpoint-up(md) {
height: $bds-text-card-height-tablet;
padding: $bds-text-card-padding-tablet;
}
@include media-breakpoint-up(lg) {
height: $bds-text-card-height-desktop;
padding: $bds-text-card-padding-desktop;
}
// Interaction
cursor: pointer;
// Focus styles - Light Mode
@include bds-focus-styles($black);
// Hover state for linked cards
&:hover {
text-decoration: none;
}
}
// =============================================================================
// Overlay (Window Shade Animation)
// =============================================================================
.bds-text-card__overlay {
@include bds-window-shade-overlay;
}
// Hover state: reveal overlay
.bds-text-card:hover .bds-text-card__overlay {
@include bds-window-shade-revealed;
}
// =============================================================================
// Color Variants - Light Mode
// =============================================================================
// Green Variant
// Default: $green-200, Hover: $green-300, Pressed: $green-400
.bds-text-card--green {
background-color: $green-200;
// Preserve background on focus (overrides light theme transparent rule)
&:focus {
background-color: $green-200;
}
.bds-text-card__overlay {
background-color: $green-300;
}
&:active {
.bds-text-card__overlay {
background-color: $green-400;
@include bds-window-shade-revealed;
}
}
}
// Neutral Light Variant (Light Mode)
// Default: $gray-200, Hover: $gray-300, Focus: $gray-300, Pressed: $gray-400
.bds-text-card--neutral-light {
background-color: $gray-200;
// Focus uses hover color ($gray-300)
&:focus {
background-color: $gray-300;
}
.bds-text-card__overlay {
background-color: $gray-300;
}
&:active {
.bds-text-card__overlay {
background-color: $gray-400;
@include bds-window-shade-revealed;
}
}
}
// Neutral Dark Variant (Light Mode)
// Default: $gray-300, Hover: $gray-200, Focus: $gray-200, Pressed: $gray-400
.bds-text-card--neutral-dark {
background-color: $gray-300;
// Focus uses hover color ($gray-200)
&:focus {
background-color: $gray-200;
}
.bds-text-card__overlay {
background-color: $gray-200;
}
&:active {
.bds-text-card__overlay {
background-color: $gray-400;
@include bds-window-shade-revealed;
}
}
}
// Lilac Variant
// Default: $lilac-200, Hover: $lilac-300, Pressed: $lilac-400
.bds-text-card--lilac {
background-color: $lilac-200;
// Preserve background on focus (overrides light theme transparent rule)
&:focus {
background-color: $lilac-200;
}
.bds-text-card__overlay {
background-color: $lilac-300;
}
&:active {
.bds-text-card__overlay {
background-color: $lilac-400;
@include bds-window-shade-revealed;
}
}
}
// Yellow Variant
// Default: $yellow-100, Hover: $yellow-200, Pressed: $yellow-300
.bds-text-card--yellow {
background-color: $yellow-100;
// Preserve background on focus (overrides light theme transparent rule)
&:focus {
background-color: $yellow-100;
}
.bds-text-card__overlay {
background-color: $yellow-200;
}
&:active {
.bds-text-card__overlay {
background-color: $yellow-300;
@include bds-window-shade-revealed;
}
}
}
// Blue Variant
// Default: $blue-100, Hover: $blue-200, Pressed: $blue-300
.bds-text-card--blue {
background-color: $blue-100;
// Preserve background on focus (overrides light theme transparent rule)
&:focus {
background-color: $blue-100;
}
.bds-text-card__overlay {
background-color: $blue-200;
}
&:active {
.bds-text-card__overlay {
background-color: $blue-300;
@include bds-window-shade-revealed;
}
}
}
// =============================================================================
// Card Title
// =============================================================================
.bds-text-card__title {
width: 100%;
position: relative;
z-index: 1;
margin: 0;
color: $bds-text-color;
// Typography handled by .h-lg class from _font.scss
}
// =============================================================================
// Card Description
// =============================================================================
.bds-text-card__description {
width: 100%;
position: relative;
z-index: 1;
margin: 0;
color: $bds-text-color;
max-width: $bds-text-card-description-max-width;
// Typography handled by .body-l class from _font.scss
}
// =============================================================================
// Disabled State
// =============================================================================
.bds-text-card--disabled {
pointer-events: none;
cursor: not-allowed;
}
// =============================================================================
// Light Mode Overrides
// =============================================================================
// Override the light theme rule: a:not(.bds-link):not(.btn):focus { background-color: transparent }
// This rule has higher specificity, so we need html.light scoped rules
html.light {
a.bds-text-card.bds-text-card--green:focus {
background-color: $green-200;
}
a.bds-text-card.bds-text-card--neutral-light:focus {
background-color: $gray-300;
}
a.bds-text-card.bds-text-card--neutral-dark:focus {
background-color: $gray-200;
}
a.bds-text-card.bds-text-card--lilac:focus {
background-color: $lilac-200;
}
a.bds-text-card.bds-text-card--yellow:focus {
background-color: $yellow-100;
}
a.bds-text-card.bds-text-card--blue:focus {
background-color: $blue-100;
}
// Disabled state in light mode: $gray-100 background, $gray-500 text
.bds-text-card--disabled {
background-color: $gray-100 !important;
.bds-text-card__title,
.bds-text-card__description {
color: $gray-500;
}
.bds-text-card__overlay {
display: none;
}
}
}
// =============================================================================
// Dark Mode Styles
// =============================================================================
// In dark mode:
// - Focus border changes from black to white
// - Text color remains black (cards have light-colored backgrounds)
// - Neutral-light dark mode: Default $gray-300, Hover $gray-200, Focus $gray-200, Pressed $gray-400
// - Neutral-dark dark mode: Default $gray-400, Hover $gray-300, Focus $gray-300, Pressed $gray-500
html.dark {
.bds-text-card {
// Focus styles - Dark Mode (white border)
&:focus-visible {
outline-color: $white;
}
}
// Neutral Light in dark mode
// Default: $gray-300, Hover: $gray-200, Focus: $gray-200, Pressed: $gray-400
.bds-text-card--neutral-light {
background-color: $gray-300;
&:focus {
background-color: $gray-200;
}
.bds-text-card__overlay {
background-color: $gray-200;
}
&:active {
.bds-text-card__overlay {
background-color: $gray-400;
@include bds-window-shade-revealed;
}
}
}
// Neutral Dark in dark mode
// Default: $gray-400, Hover: $gray-300, Focus: $gray-300, Pressed: $gray-500
.bds-text-card--neutral-dark {
background-color: $gray-400;
&:focus {
background-color: $gray-300;
}
.bds-text-card__overlay {
background-color: $gray-300;
}
&:active {
.bds-text-card__overlay {
background-color: $gray-500;
@include bds-window-shade-revealed;
}
}
}
// Focus overrides for dark mode (to override light theme rules)
a.bds-text-card.bds-text-card--neutral-light:focus {
background-color: $gray-200;
}
a.bds-text-card.bds-text-card--neutral-dark:focus {
background-color: $gray-300;
}
// Disabled state in dark mode: $gray-500 background with 30% opacity
.bds-text-card--disabled {
background-color: rgba($gray-500, 0.3) !important;
.bds-text-card__overlay {
display: none;
}
}
}

View File

@@ -0,0 +1,100 @@
import React from 'react';
import clsx from 'clsx';
/**
* Color variants for the TextCard component
* Maps to Figma design tokens (Light Mode):
* - green: Default $green-200, Hover $green-300, Pressed $green-400
* - neutral-light: Default $gray-200, Hover $gray-300, Pressed $gray-400
* - neutral-dark: Default $gray-300, Hover $gray-400, Pressed $gray-500
* - lilac: Default $lilac-200, Hover $lilac-300, Pressed $lilac-400
* - yellow: Default $yellow-100, Hover $yellow-200, Pressed $yellow-300
* - blue: Default $blue-100, Hover $blue-200, Pressed $blue-300
*/
export type TextCardColor = 'green' | 'neutral-light' | 'neutral-dark' | 'lilac' | 'yellow' | 'blue';
export interface TextCardProps extends Omit<React.ComponentPropsWithoutRef<'article'>, 'title'> {
/** Card title text (heading-lg typography) */
title: React.ReactNode;
/** Card description text (body-l typography) */
description?: React.ReactNode;
/** Optional link URL - makes the card clickable */
href?: string;
/** Background color variant */
color?: TextCardColor;
/** Whether the card is disabled */
disabled?: boolean;
}
/**
* TextCard Component
*
* A card component with a title at the top and description at the bottom.
* Used within the CardsTwoColumn pattern to display content in a 2x2 grid.
*
* Features:
* - 6 color variants: green, neutral-light, neutral-dark, lilac, yellow, blue
* - Responsive typography and spacing
* - Title uses heading-lg typography (Tobias Light)
* - Description uses body-l typography (Booton Light)
*
* Responsive behavior:
* - Desktop: 604px × 340px, 24px padding
* - Tablet: Full width × 309px, 20px padding
* - Mobile: Full width × 274px, 16px padding
*
* @example
* ```tsx
* <TextCard
* title="Institutions"
* description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products..."
* href="/institutions"
* color="green"
* />
* ```
*/
export const TextCard = React.forwardRef<HTMLElement, TextCardProps>(
(props, ref) => {
const {
title,
description,
href,
color = 'neutral-light',
disabled = false,
className,
...rest
} = props;
const rootClasses = clsx(
'bds-text-card',
`bds-text-card--${color}`,
disabled && 'bds-text-card--disabled',
className
);
const CardWrapper = href ? 'a' : 'article';
const wrapperProps = href
? { href, className: rootClasses, ref: ref as React.Ref<HTMLAnchorElement> }
: { className: rootClasses, ref };
return (
<CardWrapper {...(wrapperProps as any)} {...rest}>
{/* Overlay for window shade animation */}
<div className="bds-text-card__overlay" aria-hidden="true" />
{/* Title at top */}
<h3 className="bds-text-card__title h-lg">{title}</h3>
{/* Description at bottom */}
{description && (
<p className="bds-text-card__description body-l">{description}</p>
)}
</CardWrapper>
);
}
);
TextCard.displayName = 'TextCard';
export default TextCard;

View File

@@ -0,0 +1,3 @@
export { TextCard, type TextCardProps, type TextCardColor } from './TextCard';
export { default } from './TextCard';

View File

@@ -12,26 +12,13 @@
// .bds-tile-logo__overlay - Hover gradient overlay (window shade animation)
// .bds-tile-logo__image - Logo image element
@import '../../../styles/breakpoints';
// Grid gutter (matching PageGrid)
$bds-grid-gutter: 8px;
// =============================================================================
// Design Tokens
// =============================================================================
// Focus border colors
// Focus border colors (component-specific, dark mode default)
$bds-tile-logo-focus-border-light: $black;
$bds-tile-logo-focus-border-dark: $white;
// Focus border width
$bds-tile-logo-focus-border-width: 2px;
// Animation (matching CardOffgrid)
$bds-tile-logo-transition-duration: 200ms;
$bds-tile-logo-transition-timing: cubic-bezier(0.98, 0.12, 0.12, 0.98);
// -----------------------------------------------------------------------------
// Shape Tokens - Square (1:1 aspect ratio)
// -----------------------------------------------------------------------------
@@ -74,14 +61,14 @@ $bds-tile-logo-rect-padding-lg: 32px 64px; // LG: vertical 32px, horizontal 64px
// Interaction
cursor: pointer;
// Transitions
// Transitions (using shared animation tokens)
transition:
background-color $bds-tile-logo-transition-duration $bds-tile-logo-transition-timing,
opacity $bds-tile-logo-transition-duration $bds-tile-logo-transition-timing;
background-color $bds-transition-duration $bds-transition-timing,
opacity $bds-transition-duration $bds-transition-timing;
// Focus styles - Dark Mode (default)
&:focus {
outline: $bds-tile-logo-focus-border-width solid $bds-tile-logo-focus-border-dark;
outline: $bds-focus-border-width solid $bds-tile-logo-focus-border-dark;
outline-offset: 1px;
}
@@ -90,7 +77,7 @@ $bds-tile-logo-rect-padding-lg: 32px 64px; // LG: vertical 32px, horizontal 64px
}
&:focus-visible {
outline: $bds-tile-logo-focus-border-width solid $bds-tile-logo-focus-border-dark;
outline: $bds-focus-border-width solid $bds-tile-logo-focus-border-dark;
outline-offset: 2px;
}
}
@@ -145,21 +132,14 @@ $bds-tile-logo-rect-padding-lg: 32px 64px; // LG: vertical 32px, horizontal 64px
// Hover out: shade falls from top to bottom (hides)
.bds-tile-logo__overlay {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
// Default: hidden (shade is "rolled up" at bottom, top is 100% clipped)
// When transitioning TO this state, the top inset increases = shade falls down
clip-path: inset(100% 0 0 0);
transition: clip-path $bds-tile-logo-transition-duration $bds-tile-logo-transition-timing;
// Use shared window shade animation mixin
@include bds-window-shade-overlay;
}
// Hovered state: shade fully raised (visible)
// When transitioning TO this state, the top inset decreases = shade rises up
.bds-tile-logo--hovered .bds-tile-logo__overlay {
clip-path: inset(0 0 0 0);
@include bds-window-shade-revealed;
}
// =============================================================================

View File

@@ -0,0 +1,59 @@
// BDS ButtonGroup Component Styles
// Brand Design System - Responsive button group pattern
//
// Naming Convention: BEM with 'bds' namespace
// .bds-button-group - Base component
// .bds-button-group--gap-none - No gap between buttons on tablet+ (0px)
// .bds-button-group--gap-small - Small gap between buttons on tablet+ (8px)
// .bds-button-group--block - Block layout for 3+ buttons (all tertiary)
// =============================================================================
// Base Component Styles
// =============================================================================
.bds-button-group {
@extend .d-flex;
@extend .flex-column;
@extend .flex-wrap;
align-items: start;
gap: 8px;
// Tablet breakpoint - horizontal layout
@include media-breakpoint-up(md) {
flex-direction: row !important;
align-items: center;
}
}
// =============================================================================
// Gap Modifiers
// =============================================================================
.bds-button-group--gap-none {
// Tablet breakpoint - no gap
@include media-breakpoint-up(md) {
gap: 0px;
}
}
.bds-button-group--gap-small {
// Tablet breakpoint - keep 8px gap
@include media-breakpoint-up(md) {
gap: 4px;
}
}
// =============================================================================
// Block Layout Modifier (3+ buttons)
// =============================================================================
.bds-button-group--block {
// Override default flex layout - force column layout on all screen sizes
flex-direction: column !important;
gap: 16px !important;
// All buttons should be full width in block layout
.bds-btn {
width: 100%;
}
}

View File

@@ -0,0 +1,264 @@
import React from 'react';
import clsx from 'clsx';
import { Button } from '../../components/Button/Button';
export interface ButtonConfig {
/** Button text label */
label: string;
/** URL to navigate to - renders button as a link */
href?: string;
/** Force the color to remain constant regardless of theme mode */
forceColor?: boolean;
/** Click handler - matches Button component's onClick signature */
onClick?: () => void;
}
export interface ButtonGroupValidationResult {
/** The validated and potentially trimmed list of buttons */
buttons: ButtonConfig[];
/** Whether the button list is valid and should render */
isValid: boolean;
/** True if there are valid buttons to render (convenience flag) */
hasButtons: boolean;
/** Any warnings generated during validation */
warnings: string[];
}
/**
* Validates and processes a ButtonConfig array for ButtonGroup.
*
* Performs the following validations:
* - Applies maxButtons limit if specified
* - Checks for empty button arrays
* - Validates individual button configs (label required, href or onClick recommended)
* - Automatically logs warnings in development mode
*
* @param buttons - Array of button configurations (can be undefined)
* @param maxButtons - Optional maximum number of buttons to render
* @param autoLogWarnings - Whether to automatically log warnings in development mode (default: true)
* @returns Validation result with processed buttons, validity flag, hasButtons flag, and warnings
*
* @example
* // Basic usage with auto-logging
* const validation = validateButtonGroup(buttons, 2);
* if (validation.hasButtons) {
* <ButtonGroup buttons={validation.buttons} />
* }
*
* @example
* // Disable auto-logging
* const validation = validateButtonGroup(buttons, 2, false);
* // Handle warnings manually
* validation.warnings.forEach(w => customLogger(w));
*/
export function validateButtonGroup(
buttons: ButtonConfig[] | undefined,
maxButtons?: number,
autoLogWarnings: boolean = true
): ButtonGroupValidationResult {
// Handle undefined/null buttons
if (!buttons || buttons.length === 0) {
return {
buttons: [],
isValid: false,
hasButtons: false,
warnings: []
};
}
const warnings: string[] = [];
let buttonList = [...buttons];
// Validate individual button configs
buttonList.forEach((button, index) => {
if (!button.label || button.label.trim() === '') {
warnings.push(
`[ButtonGroup] Button at index ${index} is missing a label. This button may not render correctly.`
);
}
if (!button.href && !button.onClick) {
warnings.push(
`[ButtonGroup] Button "${button.label || `at index ${index}`}" has no href or onClick. Consider adding an action.`
);
}
});
// Apply maxButtons limit if specified
if (maxButtons !== undefined && maxButtons > 0 && buttons.length > maxButtons) {
warnings.push(
`[ButtonGroup] ${buttons.length} buttons were passed but maxButtons is set to ${maxButtons}. ` +
`Only the first ${maxButtons} button(s) will be rendered.`
);
buttonList = buttonList.slice(0, maxButtons);
}
// Check for empty array
if (buttonList.length === 0) {
warnings.push(
`[ButtonGroup] No buttons to render. ` +
`Either an empty buttons array was passed or all buttons were removed by maxButtons limit.`
);
// Auto-log warnings in development mode
if (autoLogWarnings && process.env.NODE_ENV === 'development' && warnings.length > 0) {
warnings.forEach(warning => console.warn(warning));
}
return { buttons: [], isValid: false, hasButtons: false, warnings };
}
// Auto-log warnings in development mode
if (autoLogWarnings && process.env.NODE_ENV === 'development' && warnings.length > 0) {
warnings.forEach(warning => console.warn(warning));
}
const hasButtons = buttonList.length > 0;
return { buttons: buttonList, isValid: true, hasButtons, warnings };
}
export interface ButtonGroupProps {
/** Array of button configurations
* - 1 button: renders with singleButtonVariant (default: primary)
* - 2 buttons: first as primary, second as tertiary
* - 3+ buttons: all tertiary in block layout
*/
buttons: ButtonConfig[];
/** Button color theme */
color?: 'green' | 'black';
/** Whether to force the color to remain constant regardless of theme mode */
forceColor?: boolean;
/** Gap between buttons on tablet+ (0px or 4px) */
gap?: 'none' | 'small';
/** Additional CSS classes */
className?: string;
/** Override variant for single button (default: 'primary', can be 'secondary') */
singleButtonVariant?: 'primary' | 'secondary';
/** Maximum number of buttons to render. If more buttons are passed, only the first N will be rendered. */
maxButtons?: number;
}
/**
* ButtonGroup Component
*
* A responsive button group container that displays buttons with adaptive layout:
* - 1 button: Renders with singleButtonVariant (default: primary, can be secondary)
* - 2 buttons: First as primary, second as tertiary (responsive layout)
* - 3+ buttons: All tertiary in block layout
*
* @example
* // Single button
* <ButtonGroup
* buttons={[{ label: "Get Started", href: "/start" }]}
* color="green"
* />
*
* @example
* // Two buttons (primary + tertiary)
* <ButtonGroup
* buttons={[
* { label: "Get Started", href: "/start" },
* { label: "Learn More", href: "/learn" }
* ]}
* color="green"
* />
*
* @example
* // Three or more buttons (all tertiary, block layout)
* <ButtonGroup
* buttons={[
* { label: "Option 1", href: "/option1" },
* { label: "Option 2", href: "/option2" },
* { label: "Option 3", href: "/option3" }
* ]}
* color="green"
* />
*/
export const ButtonGroup: React.FC<ButtonGroupProps> = ({
buttons,
color = 'green',
forceColor = false,
gap = 'small',
className = '',
singleButtonVariant = 'primary',
maxButtons,
}) => {
// Validate and process buttons
const validation = validateButtonGroup(buttons, maxButtons);
// Log warnings in development mode
if (process.env.NODE_ENV === 'development' && validation.warnings.length > 0) {
validation.warnings.forEach(warning => console.warn(warning));
}
// Don't render if validation failed
if (!validation.isValid) {
return null;
}
const buttonList = validation.buttons;
const isMultiButton = buttonList.length >= 3;
const classNames = clsx(
'bds-button-group',
`bds-button-group--gap-${gap}`,
{
'bds-button-group--block': isMultiButton,
},
className
);
// Render 3+ buttons: all tertiary in block layout
if (isMultiButton) {
return (
<div className={classNames}>
{buttonList.map((button, index) => (
<Button
key={index}
variant="tertiary"
color={color}
forceColor={forceColor}
href={button.href}
onClick={button.onClick}
forceNoPadding
>
{button.label}
</Button>
))}
</div>
);
}
// Render 1-2 buttons
// Single button: use singleButtonVariant (default: primary, can be secondary)
// Two buttons: first as primary, second as tertiary
const firstButtonVariant = buttonList.length === 1 ? singleButtonVariant : 'primary';
return (
<div className={classNames}>
{buttonList[0] && (
<Button
variant={firstButtonVariant}
color={color}
forceColor={forceColor}
href={buttonList[0].href}
onClick={buttonList[0].onClick}
>
{buttonList[0].label}
</Button>
)}
{buttonList[1] && (
<Button
variant="tertiary"
color={color}
forceColor={forceColor}
href={buttonList[1].href}
onClick={buttonList[1].onClick}
>
{buttonList[1].label}
</Button>
)}
</div>
);
};
export default ButtonGroup;

View File

@@ -0,0 +1,109 @@
# ButtonGroup Component
A responsive button group container that automatically assigns button variants based on the number of buttons passed. Stacks vertically on mobile and horizontally on tablet+.
## Features
- **Auto-Variant Assignment**: Automatically assigns Primary/Tertiary/Secondary variants based on button count
- **Responsive Layout**: Vertical stack on mobile, horizontal row on tablet+ (for 1-2 buttons)
- **Block Layout**: 3+ buttons render as all tertiary in a vertical block layout
- **Customizable Spacing**: Control gap between buttons on tablet+ (none or small)
- **Theme Support**: Green or black color themes
- **Max Buttons Limit**: Optionally limit the number of buttons rendered
## Button Behavior
The component automatically determines button variants based on count:
| Count | Behavior |
|-------|----------|
| 1 button | Renders as Primary (or Secondary with `singleButtonVariant="secondary"`) |
| 2 buttons | First as Primary, second as Tertiary (responsive layout) |
| 3+ buttons | All as Tertiary in block layout (vertical on all screen sizes) |
## Usage
```tsx
import { ButtonGroup } from 'shared/patterns/ButtonGroup';
// Single button (Primary by default)
<ButtonGroup
buttons={[
{ label: "Get Started", href: "/start" }
]}
color="green"
/>
// Single button as Secondary
<ButtonGroup
buttons={[
{ label: "Learn More", href: "/learn" }
]}
singleButtonVariant="secondary"
color="green"
/>
// Two buttons (auto: Primary + Tertiary)
<ButtonGroup
buttons={[
{ label: "Get Started", href: "/start" },
{ label: "Learn More", href: "/learn" }
]}
color="green"
/>
// Three or more buttons (auto: all Tertiary, block layout)
<ButtonGroup
buttons={[
{ label: "Documentation", href: "/docs" },
{ label: "API Reference", href: "/api" },
{ label: "Tutorials", href: "/tutorials" }
]}
color="black"
/>
// Limit to 2 buttons even if more are passed
<ButtonGroup
buttons={[
{ label: "First", href: "/first" },
{ label: "Second", href: "/second" },
{ label: "Third (not rendered)", href: "/third" }
]}
maxButtons={2}
color="green"
/>
```
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `buttons` | `ButtonConfig[]` | *required* | Array of button configurations |
| `color` | `'green' \| 'black'` | `'green'` | Button color theme |
| `forceColor` | `boolean` | `false` | Force color to remain constant across light/dark modes |
| `gap` | `'none' \| 'small'` | `'small'` | Gap between buttons on tablet+ (0px or 4px) |
| `singleButtonVariant` | `'primary' \| 'secondary'` | `'primary'` | Variant for single button |
| `maxButtons` | `number` | - | Maximum number of buttons to render |
| `className` | `string` | `''` | Additional CSS classes |
### ButtonConfig
```tsx
interface ButtonConfig {
label: string;
href?: string;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
forceColor?: boolean;
}
```
## Responsive Behavior
- **Mobile (<768px)**: Buttons stack vertically with 8px gap, aligned to start
- **Tablet+ (≥768px)**: Buttons align horizontally, centered, with configurable gap (0px or 4px)
## CSS Classes
- `.bds-button-group` - Base component
- `.bds-button-group--gap-none` - No gap on tablet+ (0px)
- `.bds-button-group--gap-small` - Small gap on tablet+ (4px)

View File

@@ -0,0 +1,2 @@
export { ButtonGroup, validateButtonGroup } from './ButtonGroup';
export type { ButtonGroupProps, ButtonConfig, ButtonGroupValidationResult } 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, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
export interface CalloutMediaBannerProps {
/** Color variant - determines background color (ignored if backgroundImage is provided) */
@@ -14,18 +14,8 @@ export interface CalloutMediaBannerProps {
heading?: string;
/** Subheading/description text */
subheading: 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;
};
/** Button configurations (1-2 buttons supported) */
buttons?: ButtonConfig[];
/** Additional CSS classes */
className?: string;
}
@@ -42,19 +32,21 @@ export interface CalloutMediaBannerProps {
* variant="green"
* heading="The Compliant Ledger Protocol"
* subheading="A decentralized public Layer 1 blockchain..."
* primaryButton={{ label: "Get Started", href: "/docs" }}
* tertiaryButton={{ label: "Learn More", href: "/about" }}
* buttons={[
* { label: "Get Started", href: "/docs" },
* { label: "Learn More", href: "/about" }
* ]}
* />
*
*
* @example
* // With background image (white text - default)
* <CalloutMediaBanner
* backgroundImage="/images/hero-bg.jpg"
* heading="Build on XRPL"
* subheading="Start building your next project"
* primaryButton={{ label: "Start Building", onClick: handleClick }}
* buttons={[{ label: "Start Building", onClick: handleClick }]}
* />
*
*
* @example
* // With background image and black text (fixed across light/dark modes)
* <CalloutMediaBanner
@@ -62,7 +54,7 @@ export interface CalloutMediaBannerProps {
* textColor="black"
* heading="Build on XRPL"
* subheading="Start building your next project"
* primaryButton={{ label: "Start Building", onClick: handleClick }}
* buttons={[{ label: "Start Building", onClick: handleClick }]}
* />
*/
export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
@@ -71,13 +63,13 @@ export const CalloutMediaBanner: React.FC<CalloutMediaBannerProps> = ({
textColor = 'white',
heading,
subheading,
primaryButton,
tertiaryButton,
buttons,
className = '',
}) => {
// Check if there are any buttons
const hasButtons = !!(primaryButton || tertiaryButton);
// Validate buttons if provided (max 2 buttons supported)
const buttonValidation = validateButtonGroup(buttons, 2);
const hasButtons = buttonValidation.hasButtons;
// Check if we should center content: no buttons OR (no heading but has buttons)
const shouldCenter = !hasButtons || (!heading && hasButtons);
@@ -116,29 +108,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>
{hasButtons && (
<ButtonGroup
buttons={buttonValidation.buttons}
color={buttonColor}
gap="none"
/>
)}
</div>
</PageGridCol>

View File

@@ -0,0 +1,154 @@
# 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

@@ -0,0 +1,103 @@
// BDS CardStats Pattern Styles
// Brand Design System - Section with heading, description, and grid of CardStat components
//
// Naming Convention: BEM with 'bds' namespace
// .bds-card-stats - Base section container
// .bds-card-stats__header - Header wrapper for heading and description
// .bds-card-stats__heading - Section heading (uses .h-md)
// .bds-card-stats__description - Section description (uses .body-l)
// .bds-card-stats__cards - Cards grid container
// .bds-card-stats__card-wrapper - Individual card wrapper
//
// Design tokens from Figma:
// Light Mode:
// - Background: White (#FFFFFF)
// - Heading: Neutral Black (#141414) → $black
// - Description: Neutral Black (#141414) → $black
//
// Dark Mode:
// - Background: transparent (inherits page background)
// - Heading: Neutral White (#FFFFFF) → $white
// - Description: Neutral White (#FFFFFF) → $white
//
// - Header content max-width: 808px (approximately 8 columns at desktop)
// - Gap between heading and description: 16px
// - Gap between cards: 8px (matches $bds-grid-gutter)
// Color tokens - Light Mode (from Figma: node 32051-2839)
$bds-card-stats-heading-light: $black; // --neutral/black (#141414)
$bds-card-stats-description-light: $black; // --neutral/black (#141414)
// Color tokens - Dark Mode (from Figma: node 32051-2524)
$bds-card-stats-bg-dark: transparent; // Inherits page background
$bds-card-stats-heading-dark: $white; // --neutral/white (#FFFFFF)
$bds-card-stats-description-dark: $white; // --neutral/white (#FFFFFF)
// Spacing - Header gap (between heading and description)
$bds-card-stats-header-gap-base: 8px; // Base: 8px
$bds-card-stats-header-gap-lg: 16px; // Desktop: 16px
// Spacing - Section gap (between header and cards)
$bds-card-stats-section-gap-sm: 24px; // Mobile: 24px
$bds-card-stats-section-gap-md: 32px; // Tablet: 32px
$bds-card-stats-section-gap-lg: 40px; // Desktop: 40px
// Spacing - Section padding
$bds-card-stats-padding-y-sm: 24px; // Mobile: 24px
$bds-card-stats-padding-y-md: 32px; // Tablet: 32px
$bds-card-stats-padding-y-lg: 40px; // Desktop: 40px
// =============================================================================
// Base Section Styles
// =============================================================================
.bds-card-stats {
// Vertical padding
padding-top: $bds-card-stats-padding-y-sm;
padding-bottom: $bds-card-stats-padding-y-sm;
@include media-breakpoint-up(md) {
padding-top: $bds-card-stats-padding-y-md;
padding-bottom: $bds-card-stats-padding-y-md;
}
@include media-breakpoint-up(lg) {
padding-top: $bds-card-stats-padding-y-lg;
padding-bottom: $bds-card-stats-padding-y-lg;
}
}
// =============================================================================
// Header Styles
// =============================================================================
.bds-card-stats__header {
display: flex;
flex-direction: column;
gap: $bds-card-stats-header-gap-base;
margin-bottom: $bds-card-stats-section-gap-sm;
@include media-breakpoint-up(md) {
margin-bottom: $bds-card-stats-section-gap-md;
}
@include media-breakpoint-up(lg) {
gap: $bds-card-stats-header-gap-lg;
margin-bottom: $bds-card-stats-section-gap-lg;
}
}
// =============================================================================
// Dark Mode Styles
// =============================================================================
html.dark {
.bds-card-stats__heading,
.bds-card-stats__description {
color: $white;
}
}

View File

@@ -0,0 +1,105 @@
import React from 'react';
import clsx from 'clsx';
import { CardStat, CardStatProps } from '../../components/CardStat';
import { PageGrid } from '../../components/PageGrid/page-grid';
/**
* Configuration for a single stat card in the CardStats pattern
*/
export type CardStatsCardConfig = CardStatProps;
/**
* Props for the CardStats pattern component
*/
export interface CardStatsProps extends React.ComponentPropsWithoutRef<'section'> {
/** Section heading text */
heading: React.ReactNode;
/** Optional section description text */
description?: React.ReactNode;
/** Array of CardStat configurations */
cards: CardStatsCardConfig[];
}
/**
* CardStats Pattern Component
*
* A section pattern that displays a heading, optional description, and a responsive
* grid of CardStat components. Designed for showcasing key statistics and metrics.
*
* Features:
* - Responsive grid layout (2 columns mobile/tablet, 3 columns desktop)
* - Heading with `heading-md` typography (Tobias Light)
* - Optional description with `body-l` typography (Booton Light)
* - Proper spacing using PageGrid for container and alignment
* - Full dark mode support
*
* @example
* ```tsx
* <CardStats
* heading="Blockchain Trusted at Scale"
* description="Streamline development and build powerful RWA tokenization solutions."
* cards={[
* {
* statistic: "12",
* superscript: "+",
* label: "Continuous uptime years",
* variant: "lilac",
* primaryButton: { label: "Learn More", href: "/docs" }
* },
* {
* statistic: "6M",
* superscript: "2",
* label: "Active wallets",
* variant: "light-gray"
* },
* // ... more cards
* ]}
* />
* ```
*/
export const CardStats = React.forwardRef<HTMLElement, CardStatsProps>(
(props, ref) => {
const { heading, description, cards, className, ...rest } = props;
// Early return for empty cards array
if (cards.length === 0) {
console.warn('CardStats: No cards provided');
return null;
}
return (
<PageGrid ref={ref as React.Ref<HTMLDivElement>}
className={clsx('bds-card-stats', className)}
{...rest}
>
<PageGrid.Row>
<PageGrid.Col span={{ base: 4, md: 6, lg: 8 }}>
{/* Header section */}
<div className="bds-card-stats__header">
<h2 className="mb-0 h-md">{heading}</h2>
{description && (
<p className="bmb-0 body-l">
{description}
</p>
)}
</div>
</PageGrid.Col>
</PageGrid.Row>
<PageGrid.Row>
{cards.map((cardConfig, index) => (
<CardStat
key={index}
{...cardConfig}
span={cardConfig.span ?? { base: 4, md: 4, lg: 4 }}
/>
))}
</PageGrid.Row>
</PageGrid>
);
}
);
CardStats.displayName = 'CardStats';
export default CardStats;

View File

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

View File

@@ -2,6 +2,7 @@ import React from 'react';
import clsx from 'clsx';
import { CardImage, CardImageProps } from '../../components/CardImage';
import { PageGrid } from '../../components/PageGrid/page-grid';
import { getCardKey, isEnvironment } from '../../utils';
/**
* Configuration for a single card in the CardsFeatured pattern
@@ -20,16 +21,6 @@ export interface CardsFeaturedProps extends React.ComponentPropsWithoutRef<'sect
cards: readonly CardsFeaturedCardConfig[];
}
/**
* Generates a stable key for a card based on its properties.
* Falls back to index if no stable identifier is available.
*/
const getCardKey = (card: CardsFeaturedCardConfig, index: number): string | number => {
if (card.href) return card.href;
if (card.title) return `${card.title}-${index}`;
return index;
};
/**
* CardsFeatured Pattern Component
*
@@ -68,7 +59,9 @@ export const CardsFeatured = React.forwardRef<HTMLElement, CardsFeaturedProps>(
// Early return for empty cards array
if (cards.length === 0) {
console.warn('CardsFeatured: No cards provided');
if (isEnvironment('development')) {
console.warn('CardsFeatured: No cards provided');
}
return null;
}
@@ -101,7 +94,7 @@ export const CardsFeatured = React.forwardRef<HTMLElement, CardsFeaturedProps>(
<div className="bds-cards-featured__cards">
{cards.map((card, index) => (
<div
key={getCardKey(card, index)}
key={getCardKey(card.href || card.title, index, "card-featured")}
className="bds-cards-featured__card-wrapper"
>
<CardImage {...card} fullBleed />

View File

@@ -1,141 +0,0 @@
# CardsIconGrid Pattern
A section pattern that displays a heading, optional description, and a responsive grid of `CardIcon` components. Follows the "CardIconGrid" design from Figma.
## Features
- Responsive grid layout (1 column mobile, 2 tablet, 3 desktop)
- Heading with `heading-md` typography (Tobias Light)
- Optional description with `body-l` typography (Booton Light)
- Proper spacing using `PageGrid` for container and alignment
- Full dark mode support
- Uses the existing `CardIcon` component for cards
## Usage
```tsx
import { CardsIconGrid } from 'shared/patterns/CardsIconGrid';
<CardsIconGrid
heading="Unlock new business models with embedded payments"
description="Streamline development and build powerful RWA tokenization solutions with XRP Ledger's comprehensive developer toolset."
cards={[
{
icon: "/icons/wallet.svg",
label: "Digital Wallets",
href: "/docs/wallets",
variant: "green"
},
{
icon: "/icons/payments.svg",
label: "B2B Payment Rails",
href: "/docs/payments",
variant: "green"
},
{
icon: "/icons/compliance.svg",
label: "Compliance-First Payments",
href: "/docs/compliance",
variant: "green"
}
]}
/>
```
## Props
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `heading` | `React.ReactNode` | Yes | Section heading text |
| `description` | `React.ReactNode` | No | Section description text (optional) |
| `cards` | `CardsIconGridCardConfig[]` | Yes | Array of card configurations (uses `CardIconProps`) |
| `className` | `string` | No | Additional CSS class names |
### CardsIconGridCardConfig
Each card in the `cards` array accepts all props from `CardIconProps`:
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `icon` | `string` | Yes | Icon image source (URL or path) |
| `iconAlt` | `string` | No | Alt text for the icon image |
| `label` | `string` | Yes | Card label text |
| `variant` | `'neutral' \| 'green'` | No | Color variant (default: 'neutral') |
| `href` | `string` | No | Link destination - renders as `<a>` |
| `onClick` | `() => void` | No | Click handler - renders as `<button>` |
| `disabled` | `boolean` | No | Disabled state |
## Responsive Behavior
| Breakpoint | Grid Columns | Vertical Padding |
|------------|--------------|------------------|
| Mobile (< 768px) | 1 column | 48px |
| Tablet (768px - 1199px) | 2 columns | 64px |
| Desktop (≥ 1200px) | 3 columns | 80px |
## Design Tokens
### Colors
| Mode | Element | Color |
|------|---------|-------|
| Light | Heading | `$black` (#141414) |
| Light | Description | `$black` (#141414) |
| Dark | Heading | `$white` (#FFFFFF) |
| Dark | Description | `$white` (#FFFFFF) |
### Spacing
- Header gap (heading to description): 8px mobile/tablet, 16px desktop
- Section gap (header to cards): 24px mobile, 32px tablet, 40px desktop
- Cards column gap: 24px mobile, 8px tablet/desktop
- Cards row gap: 24px mobile, 32px tablet, 40px desktop
## CSS Classes
| Class | Description |
|-------|-------------|
| `.bds-cards-icon-grid` | Base section container |
| `.bds-cards-icon-grid__header` | Header wrapper for heading and description |
| `.bds-cards-icon-grid__heading` | Section heading (uses `.h-md`) |
| `.bds-cards-icon-grid__description` | Section description (uses `.body-l`) |
| `.bds-cards-icon-grid__cards` | Cards grid container |
| `.bds-cards-icon-grid__card-wrapper` | Individual card wrapper |
## Card Variants
The pattern supports both CardIcon variants:
### Green Variant
```tsx
cards={[
{ icon: "/icons/wallet.svg", label: "Digital Wallets", href: "/wallets", variant: "green" }
]}
```
### Neutral Variant
```tsx
cards={[
{ icon: "/icons/docs.svg", label: "Documentation", href: "/docs", variant: "neutral" }
]}
```
## Without Description
The description prop is optional:
```tsx
<CardsIconGrid
heading="Funding & Support Programs"
cards={[...]}
/>
```
## Figma Reference
- Design: [Section Cards - Icon Grid](https://www.figma.com/design/Ojj6UpFBw3HMb0QqRaKxAU/Section-Cards---Icon?node-id=30071-3082&m=dev)
## Showcase
View the pattern showcase at: `/about/cards-icon-grid-showcase`

View File

@@ -1,180 +0,0 @@
// BDS CardsIconGrid Pattern Styles
// Brand Design System - Section with heading, optional description, and grid of CardIcon components
//
// Naming Convention: BEM with 'bds' namespace
// .bds-cards-icon-grid - Base section container
// .bds-cards-icon-grid__header - Header wrapper for heading and description
// .bds-cards-icon-grid__heading - Section heading (uses .h-md)
// .bds-cards-icon-grid__description - Section description (uses .body-l)
// .bds-cards-icon-grid__cards - Cards grid container
// .bds-cards-icon-grid__card-wrapper - Individual card wrapper
//
// Design tokens from Figma:
// Light Mode:
// - Heading: Neutral Black (#141414) → $black
// - Description: Neutral Black (#141414) → $black
//
// Dark Mode:
// - Heading: Neutral White (#FFFFFF) → $white
// - Description: Neutral White (#FFFFFF) → $white
//
// - Header content max-width: 808px (approximately 8 columns at desktop)
// - Gap between heading and description: 8px mobile/tablet, 16px desktop
// - Gap between cards: 8px (matches $bds-grid-gutter)
// =============================================================================
// Design Tokens (from Figma)
// =============================================================================
$bds-grid-gutter: 8px;
// Spacing - Header gap (between heading and description)
$bds-cards-icon-grid-header-gap-sm: 8px; // Mobile: 8px
$bds-cards-icon-grid-header-gap-md: 8px; // Tablet: 8px
$bds-cards-icon-grid-header-gap-lg: 16px; // Desktop: 16px
// Spacing - Section gap (between header and cards)
$bds-cards-icon-grid-section-gap-sm: 24px; // Mobile
$bds-cards-icon-grid-section-gap-md: 32px; // Tablet
$bds-cards-icon-grid-section-gap-lg: 40px; // Desktop
// Spacing - Cards gap
$bds-cards-icon-grid-cards-gap-sm: 24px; // Mobile: 24px vertical stack
$bds-cards-icon-grid-cards-gap-md: 8px; // Tablet: 8px
$bds-cards-icon-grid-cards-gap-lg: 8px; // Desktop: 8px
// Spacing - Row gap (between rows of cards)
$bds-cards-icon-grid-row-gap-sm: 24px; // Mobile
$bds-cards-icon-grid-row-gap-md: 32px; // Tablet
$bds-cards-icon-grid-row-gap-lg: 40px; // Desktop
// Spacing - Section padding (vertical)
$bds-cards-icon-grid-padding-y-sm: 48px; // Mobile
$bds-cards-icon-grid-padding-y-md: 64px; // Tablet
$bds-cards-icon-grid-padding-y-lg: 80px; // Desktop
// Colors - Light Mode (default)
$bds-cards-icon-grid-heading-color: $black; // #141414 - Neutral black
$bds-cards-icon-grid-description-color: $black; // #141414 - Neutral black
// Colors - Dark Mode
$bds-cards-icon-grid-heading-color-dark: $white; // #FFFFFF - Neutral white
$bds-cards-icon-grid-description-color-dark: $white; // #FFFFFF - Neutral white
// =============================================================================
// Section Container
// =============================================================================
.bds-cards-icon-grid {
width: 100%;
padding-top: $bds-cards-icon-grid-padding-y-sm;
padding-bottom: $bds-cards-icon-grid-padding-y-sm;
@include media-breakpoint-up(md) {
padding-top: $bds-cards-icon-grid-padding-y-md;
padding-bottom: $bds-cards-icon-grid-padding-y-md;
}
@include media-breakpoint-up(lg) {
padding-top: $bds-cards-icon-grid-padding-y-lg;
padding-bottom: $bds-cards-icon-grid-padding-y-lg;
}
}
// =============================================================================
// Header Section
// =============================================================================
.bds-cards-icon-grid__header {
display: flex;
flex-direction: column;
gap: $bds-cards-icon-grid-header-gap-sm;
@include media-breakpoint-up(md) {
gap: $bds-cards-icon-grid-header-gap-md;
}
@include media-breakpoint-up(lg) {
gap: $bds-cards-icon-grid-header-gap-lg;
}
}
.bds-cards-icon-grid__heading {
margin: 0;
// Typography handled by .h-md class from _font.scss
}
.bds-cards-icon-grid__description {
margin: 0;
// Typography handled by .body-l class from _font.scss
}
// =============================================================================
// Cards Grid
// =============================================================================
.bds-cards-icon-grid__cards {
display: grid;
grid-template-columns: 1fr;
gap: $bds-cards-icon-grid-cards-gap-sm;
width: 100%;
margin-top: $bds-cards-icon-grid-section-gap-sm;
@include media-breakpoint-up(md) {
grid-template-columns: repeat(2, 1fr);
column-gap: $bds-cards-icon-grid-cards-gap-md;
row-gap: $bds-cards-icon-grid-row-gap-md;
margin-top: $bds-cards-icon-grid-section-gap-md;
}
@include media-breakpoint-up(lg) {
grid-template-columns: repeat(3, 1fr);
column-gap: $bds-cards-icon-grid-cards-gap-lg;
row-gap: $bds-cards-icon-grid-row-gap-lg;
margin-top: $bds-cards-icon-grid-section-gap-lg;
}
}
.bds-cards-icon-grid__card-wrapper {
display: flex;
min-width: 0;
width: 100%;
// Ensure CardIcon fills the wrapper
.bds-card-icon {
width: 100%;
}
}
// =============================================================================
// Light Mode Styles
// =============================================================================
html.light {
.bds-cards-icon-grid {
background-color: $white;
}
.bds-cards-icon-grid__heading {
color: $bds-cards-icon-grid-heading-color;
}
.bds-cards-icon-grid__description {
color: $bds-cards-icon-grid-description-color;
}
}
// =============================================================================
// Dark Mode Styles
// =============================================================================
html.dark {
.bds-cards-icon-grid__heading {
color: $bds-cards-icon-grid-heading-color-dark;
}
.bds-cards-icon-grid__description {
color: $bds-cards-icon-grid-description-color-dark;
}
}

View File

@@ -1,121 +0,0 @@
import React from 'react';
import clsx from 'clsx';
import { CardIcon, CardIconProps } from '../../components/CardIcon';
import { PageGrid } from '../../components/PageGrid/page-grid';
/**
* Configuration for a single card in the CardsIconGrid pattern
*/
export type CardsIconGridCardConfig = CardIconProps;
/**
* Props for the CardsIconGrid pattern component
*/
export interface CardsIconGridProps extends React.ComponentPropsWithoutRef<'section'> {
/** Section heading text */
heading: React.ReactNode;
/** Section description text (optional) */
description?: React.ReactNode;
/** Array of card configurations (uses CardIconProps) */
cards: readonly CardsIconGridCardConfig[];
}
/**
* Generate a unique key for a card based on its props
*/
const getCardKey = (card: CardsIconGridCardConfig, index: number): string => {
if (card.href) return `card-${card.href}-${index}`;
if (card.label) return `card-${card.label.toString().slice(0, 20)}-${index}`;
return `card-${index}`;
};
/**
* CardsIconGrid Pattern Component
*
* A section pattern that displays a heading, optional description, and a responsive grid
* of CardIcon components. Follows the "CardIconGrid" pattern from Figma.
*
* Features:
* - Responsive grid layout (1 column mobile, 2 tablet, 3 desktop)
* - Heading with `heading-md` typography (Tobias Light)
* - Optional description with `body-l` typography (Booton Light)
* - Proper spacing using PageGrid for container and alignment
* - Full dark mode support
*
* @example
* ```tsx
* <CardsIconGrid
* heading="Unlock new business models with embedded payments"
* description="Streamline development and build powerful solutions."
* cards={[
* {
* icon: "/icons/wallet.svg",
* label: "Digital Wallets",
* href: "/docs/wallets",
* variant: "green"
* },
* {
* icon: "/icons/payments.svg",
* label: "B2B Payment Rails",
* href: "/docs/payments",
* variant: "green"
* }
* ]}
* />
* ```
*/
export const CardsIconGrid = React.forwardRef<HTMLElement, CardsIconGridProps>(
function CardsIconGrid(
{ className, heading, description, cards, ...rest },
ref
) {
return (
<section
ref={ref}
className={clsx('bds-cards-icon-grid', className)}
{...rest}
>
<PageGrid>
{/* Header content row */}
<PageGrid.Row>
<PageGrid.Col
span={{
base: 'fill',
md: 6,
lg: 8,
}}
>
<div className="bds-cards-icon-grid__header">
<h2 className="bds-cards-icon-grid__heading h-md">{heading}</h2>
{description && (
<p className="bds-cards-icon-grid__description body-l">{description}</p>
)}
</div>
</PageGrid.Col>
</PageGrid.Row>
{/* Cards grid row */}
<PageGrid.Row>
<PageGrid.Col span="fill">
<div className="bds-cards-icon-grid__cards">
{cards.map((card, index) => (
<div
key={getCardKey(card, index)}
className="bds-cards-icon-grid__card-wrapper"
>
<CardIcon {...card} />
</div>
))}
</div>
</PageGrid.Col>
</PageGrid.Row>
</PageGrid>
</section>
);
}
);
CardsIconGrid.displayName = 'CardsIconGrid';
export default CardsIconGrid;

View File

@@ -1,3 +0,0 @@
export { CardsIconGrid, type CardsIconGridProps, type CardsIconGridCardConfig } from './CardsIconGrid';
export { default } from './CardsIconGrid';

View File

@@ -0,0 +1,180 @@
// BDS CardsTwoColumn Pattern Styles
// Brand Design System - Section with header and 2x2 card grid
// Uses PageGrid for responsive layout
//
// Naming Convention: BEM with 'bds' namespace
// .bds-cards-two-column - Base section container
// .bds-cards-two-column__container - PageGrid container with vertical padding
// .bds-cards-two-column__header - Header row (title + description)
// .bds-cards-two-column__header-left - Left column (title)
// .bds-cards-two-column__header-right - Right column (description)
// .bds-cards-two-column__title - Section title (heading-md)
// .bds-cards-two-column__description - Section description (body-l, muted)
// .bds-cards-two-column__cards - Cards row (2x2 on desktop)
//
// Note: TextCard styles are in shared/components/TextCard/TextCard.scss
// Note: PageGrid handles horizontal padding and column widths
//
// Design tokens from Figma (Section Cards - Two Column):
//
// Breakpoints:
// - Mobile: < 576px (min-width: 240px, max-width: 575px)
// - Tablet: 576px - 991px (min-width: 576px, max-width: 991px)
// - Desktop: ≥ 992px (min-width: 992px, max-width: 1280px)
// =============================================================================
// Design Tokens from Figma
// =============================================================================
// Section vertical padding (horizontal handled by PageGrid)
$bds-section-padding-y-mobile: 24px;
$bds-section-padding-y-tablet: 32px;
$bds-section-padding-y-desktop: 40px;
// Gap between header row and cards row
$bds-section-row-gap-mobile: 24px;
$bds-section-row-gap-tablet: 32px;
$bds-section-row-gap-desktop: 40px;
// Card height for header alignment (desktop only)
$bds-text-card-height-desktop: 340px;
// Colors - Light Mode (from Figma)
$bds-text-color: $black; // #141414 - Neutral black
$bds-text-color-muted: $gray-500; // #72777E - Neutral/500 for description
// =============================================================================
// Section Container
// =============================================================================
.bds-cards-two-column {
width: 100%;
background-color: $white;
}
// =============================================================================
// PageGrid Container Override
// =============================================================================
.bds-cards-two-column__container {
// Add vertical padding (PageGrid handles horizontal)
padding-top: $bds-section-padding-y-mobile;
padding-bottom: $bds-section-padding-y-mobile;
@include media-breakpoint-up(md) {
padding-top: $bds-section-padding-y-tablet;
padding-bottom: $bds-section-padding-y-tablet;
}
@include media-breakpoint-up(lg) {
padding-top: $bds-section-padding-y-desktop;
padding-bottom: $bds-section-padding-y-desktop;
}
}
// =============================================================================
// Header Row
// =============================================================================
.bds-cards-two-column__header {
// Add margin-bottom for gap between header and cards rows
margin-bottom: $bds-section-row-gap-mobile;
@include media-breakpoint-up(md) {
margin-bottom: $bds-section-row-gap-tablet;
}
@include media-breakpoint-up(lg) {
margin-bottom: $bds-section-row-gap-desktop;
// Desktop: align items to match card height
align-items: stretch;
}
}
.bds-cards-two-column__header-left {
@include media-breakpoint-up(lg) {
// Desktop: match card height for alignment
min-height: $bds-text-card-height-desktop;
display: flex;
flex-direction: column;
}
}
.bds-cards-two-column__header-right {
@include media-breakpoint-up(lg) {
// Desktop: match card height, align description to bottom
min-height: $bds-text-card-height-desktop;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
}
.bds-cards-two-column__title {
margin: 0;
color: $bds-text-color;
// Typography handled by .h-md class from _font.scss
}
.bds-cards-two-column__description {
color: $bds-text-color-muted;
// Typography handled by .body-l class from _font.scss
p {
margin: 0;
& + p {
margin-top: 16px; // Paragraph spacing from Figma
}
}
}
// =============================================================================
// Cards Row
// =============================================================================
.bds-cards-two-column__cards {
// PageGrid.Row handles the flex layout and gap
// TextCards fill their column width automatically
}
// =============================================================================
// Light Mode Styles
// =============================================================================
html.light {
.bds-cards-two-column {
background-color: $white;
}
.bds-cards-two-column__title {
color: $bds-text-color;
}
.bds-cards-two-column__description {
color: $bds-text-color-muted;
}
}
// =============================================================================
// Dark Mode Styles
// =============================================================================
// Dark mode color mappings from Figma (node 33054:969):
// - Section background: Neutral/black (#141414) → $black
// - Section title: Neutral/white (#FFFFFF) → $white
// - Section description: Neutral/white (#FFFFFF) → $white
html.dark {
.bds-cards-two-column {
background-color: $black;
}
// Section header text colors inverted for dark mode
.bds-cards-two-column__title {
color: $white;
}
.bds-cards-two-column__description {
color: $white;
}
}

View File

@@ -0,0 +1,118 @@
import React from 'react';
import clsx from 'clsx';
import { TextCard, TextCardProps } from 'shared/components/TextCard';
import { PageGrid } from 'shared/components/PageGrid';
/**
* Configuration for a card in the CardsTwoColumn pattern
*/
export type CardsTwoColumnCardConfig = TextCardProps;
/**
* Props for the CardsTwoColumn pattern component
*/
export interface CardsTwoColumnProps extends Omit<React.ComponentPropsWithoutRef<'section'>, 'title'> {
/** Section title (heading-md typography) */
title: React.ReactNode;
/** Section description (body-l typography, muted color). Can be string or ReactNode */
description?: React.ReactNode;
/** Secondary description paragraph (body-l typography, muted color). Can be string or ReactNode */
secondaryDescription?: React.ReactNode;
/** Array of 4 card configurations for the 2x2 grid */
cards: readonly [CardsTwoColumnCardConfig, CardsTwoColumnCardConfig, CardsTwoColumnCardConfig, CardsTwoColumnCardConfig];
}
/**
* CardsTwoColumn Pattern Component
*
* A section pattern that displays a header with title/description and a 2x2 grid
* of TextCard components. Uses PageGrid for responsive layout.
*
* Structure:
* - Header: Title (left) + Description (right) on desktop, stacked on tablet/mobile
* - Cards: 2x2 grid on desktop, single column stacked on tablet/mobile
*
* Responsive behavior:
* - Desktop (≥992px):
* - Header: Title left (6 cols), description right (6 cols)
* - Cards: 2x2 grid (6 cols each)
* - Section padding: 40px vertical, 32px horizontal
* - Gap between header and cards: 40px
* - Gap between cards: 8px
*
* - Tablet (576-991px):
* - Header: Stacked (title above description, full width)
* - Cards: Single column, stacked vertically
* - Section padding: 32px vertical, 24px horizontal
* - Gap between header and cards: 32px
* - Gap between cards: 8px
*
* - Mobile (<576px):
* - Header: Stacked (title above description, full width)
* - Cards: Single column, stacked vertically
* - Section padding: 24px vertical, 16px horizontal
* - Gap between header and cards: 24px
* - Gap between cards: 8px
*
* @example
* ```tsx
* <CardsTwoColumn
* title="The Future of Finance is Already Onchain"
* description="XRP Ledger isn't about bold predictions. It's about delivering value now."
* secondaryDescription="On XRPL, you're not waiting for the future. You're building it."
* cards={[
* { title: "Institutions", description: "Banks, asset managers...", color: "lilac" },
* { title: "Developers", description: "Build decentralized...", color: "neutral-light" },
* { title: "Enterprise", description: "Scale your business...", color: "neutral-dark" },
* { title: "Community", description: "Join the global...", color: "green" }
* ]}
* />
* ```
*/
export const CardsTwoColumn = React.forwardRef<HTMLElement, CardsTwoColumnProps>(
(props, ref) => {
const { title, description, secondaryDescription, cards, className, ...rest } = props;
if (cards.length !== 4) {
console.warn('CardsTwoColumn: Exactly 4 cards are required');
return null;
}
return (
<section
ref={ref}
className={clsx('bds-cards-two-column', className)}
{...rest}
>
<PageGrid className="bds-cards-two-column__container">
{/* Header Row */}
<PageGrid.Row className="bds-cards-two-column__header">
<PageGrid.Col span={{ md: 8, lg: 6 }} className="bds-cards-two-column__header-left">
<h2 className="bds-cards-two-column__title h-md">{title}</h2>
</PageGrid.Col>
{(description || secondaryDescription) && (
<PageGrid.Col span={{ md: 8, lg: 6 }} className="bds-cards-two-column__header-right bds-cards-two-column__description body-l">
{description && <p>{description}</p>}
{secondaryDescription && <p>{secondaryDescription}</p>}
</PageGrid.Col>
)}
</PageGrid.Row>
{/* Cards Row - 2x2 on desktop, stacked on tablet/mobile */}
<PageGrid.Row className="bds-cards-two-column__cards">
{cards.map((card, index) => (
<PageGrid.Col key={index} span={{ md: 8, lg: 6 }}>
<TextCard {...card} />
</PageGrid.Col>
))}
</PageGrid.Row>
</PageGrid>
</section>
);
}
);
CardsTwoColumn.displayName = 'CardsTwoColumn';
export default CardsTwoColumn;

View File

@@ -0,0 +1,166 @@
# CardsTwoColumn Pattern
A section pattern featuring a header with title and description, plus a 2×2 grid of TextCard components. Designed for showcasing multiple related content areas with visual variety through 6 color variants.
## Features
- **Header Section**: Title (heading-md) with optional description (body-l, muted)
- **4-Card Grid**: 2×2 layout on desktop, single column stacked on tablet/mobile
- **6 Color Variants**: Green, neutral-light, neutral-dark, lilac, yellow, and blue
- **Interactive States**: Hover (window shade animation), focus, and pressed states
- **Disabled State**: Cards can be disabled with appropriate styling for light/dark modes
- **Responsive Design**: Adapts layout and spacing across all breakpoints
- **Dark Mode Support**: Full dark mode styling via `html.dark`
## Usage
```tsx
import { CardsTwoColumn } from 'shared/patterns/CardsTwoColumn';
<CardsTwoColumn
title="The Future of Finance is Already Onchain"
description="XRP Ledger isn't about bold predictions. It's about delivering value now."
secondaryDescription="On XRPL, you're not waiting for the future. You're building it."
cards={[
{ title: "Institutions", description: "Banks, asset managers...", color: "lilac" },
{ title: "Developers", description: "Build decentralized...", color: "neutral-light" },
{ title: "Enterprise", description: "Scale your business...", color: "neutral-dark" },
{ title: "Community", description: "Join the global...", color: "green" }
]}
/>
```
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `title` | `ReactNode` | *required* | Section title (heading-md typography) |
| `description` | `ReactNode` | - | Section description (body-l, muted color) |
| `secondaryDescription` | `ReactNode` | - | Additional description paragraph |
| `cards` | `[TextCardProps, TextCardProps, TextCardProps, TextCardProps]` | *required* | Array of exactly 4 card configurations |
| `className` | `string` | - | Additional CSS classes |
### TextCardProps
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `title` | `ReactNode` | *required* | Card title (heading-lg typography) |
| `description` | `ReactNode` | - | Card description (body-l typography) |
| `href` | `string` | - | Optional link URL (makes card clickable) |
| `color` | `'green' \| 'neutral-light' \| 'neutral-dark' \| 'lilac' \| 'yellow' \| 'blue'` | `'neutral-light'` | Background color variant |
| `disabled` | `boolean` | `false` | Whether the card is disabled |
## Responsive Behavior
### Desktop (≥992px)
- Header: Two-column layout (title left, description right)
- Cards: 2×2 grid with 8px gap
- Section padding: 40px vertical, 32px horizontal
- Gap between header and cards: 40px
- Card height: 340px, padding: 24px
### Tablet (576-991px)
- Header: Stacked (title above description)
- Cards: Single column, stacked vertically with 8px gap
- Section padding: 32px vertical, 24px horizontal
- Gap between header and cards: 32px
- Card height: 309px, padding: 20px
### Mobile (<576px)
- Header: Stacked (title above description)
- Cards: Single column, stacked vertically with 8px gap
- Section padding: 24px vertical, 16px horizontal
- Gap between header and cards: 24px
- Card height: 274px, padding: 16px
## Color Variants & States
Each color variant has four interactive states with a "window shade" hover animation.
### Light Mode
| Variant | Default | Hover | Focus | Pressed |
|---------|---------|-------|-------|---------|
| `green` | `$green-200` (#70EE97) | `$green-300` (#21E46B) | `$green-300` (#21E46B) | `$green-400` (#0DAA3E) |
| `neutral-light` | `$gray-200` (#E6EAF0) | `$gray-300` (#CAD4DF) | `$gray-300` (#CAD4DF) | `$gray-400` (#8A919A) |
| `neutral-dark` | `$gray-300` (#CAD4DF) | `$gray-200` (#E6EAF0) | `$gray-200` (#E6EAF0) | `$gray-400` (#8A919A) |
| `lilac` | `$lilac-200` (#D9CAFF) | `$lilac-300` (#C0A7FF) | `$lilac-300` (#C0A7FF) | `$lilac-400` (#7649E3) |
| `yellow` | `$yellow-100` (#F3F1EB) | `$yellow-200` (#E6F1A7) | `$yellow-200` (#E6F1A7) | `$yellow-300` (#DBF15E) |
| `blue` | `$blue-100` (#EDF4FF) | `$blue-200` (#93BFF1) | `$blue-200` (#93BFF1) | `$blue-300` (#428CFF) |
### Dark Mode
| Variant | Default | Hover | Focus | Pressed |
|---------|---------|-------|-------|---------|
| `green` | `$green-200` (#70EE97) | `$green-300` (#21E46B) | `$green-300` (#21E46B) | `$green-400` (#0DAA3E) |
| `neutral-light` | `$gray-300` (#CAD4DF) | `$gray-200` (#E6EAF0) | `$gray-200` (#E6EAF0) | `$gray-400` (#8A919A) |
| `neutral-dark` | `$gray-400` (#8A919A) | `$gray-300` (#CAD4DF) | `$gray-300` (#CAD4DF) | `$gray-500` (#72777E) |
| `lilac` | `$lilac-200` (#D9CAFF) | `$lilac-300` (#C0A7FF) | `$lilac-300` (#C0A7FF) | `$lilac-400` (#7649E3) |
| `yellow` | `$yellow-100` (#F3F1EB) | `$yellow-200` (#E6F1A7) | `$yellow-200` (#E6F1A7) | `$yellow-300` (#DBF15E) |
| `blue` | `$blue-100` (#EDF4FF) | `$blue-200` (#93BFF1) | `$blue-200` (#93BFF1) | `$blue-300` (#428CFF) |
### Disabled State
| Mode | Background | Text |
|------|------------|------|
| Light | `$gray-100` (#F0F3F7) | `$gray-500` (#72777E) |
| Dark | `rgba($gray-500, 0.3)` | Default text color |
## CSS Classes
```
.bds-cards-two-column // Section container
.bds-cards-two-column__container // Inner container with max-width
.bds-cards-two-column__header // Header section
.bds-cards-two-column__header-left // Left side (title)
.bds-cards-two-column__header-right // Right side (description)
.bds-cards-two-column__title // Section title
.bds-cards-two-column__description // Section description
.bds-cards-two-column__cards // Cards grid container
```
## Typography Tokens
- **Section Title**: Uses `h-md` (heading-md, Tobias Light)
- Desktop: 40px / 46px line-height
- Tablet: 36px / 45px line-height
- Mobile: 32px / 40px line-height
- **Section Description**: Uses `body-l` (Booton Light), color: `$gray-500`
- All breakpoints: 18px / 26.1px line-height
- **Card Title**: Uses `h-lg` (heading-lg, Tobias Light)
- Desktop: 48px / 52.8px line-height
- Tablet: 42px / 46.2px line-height
- Mobile: 36px / 39.6px line-height
- **Card Description**: Uses `body-l` (Booton Light)
- All breakpoints: 18px / 26.1px line-height
## Files
- `CardsTwoColumn.tsx` - Main pattern component
- `CardsTwoColumn.scss` - Styles with responsive breakpoints
- `index.ts` - Barrel exports
- `README.md` - This documentation
## Related Components
- **TextCard**: Core component for individual cards within the grid (`shared/components/TextCard`)
- **PageGrid**: Can be used alongside for additional layout needs
## Design References
- **Figma Design**: [Section Cards - Two Column](https://www.figma.com/design/MP5gjNp7yPBnKBKleb8LRL/Section-Cards---Two-Column)
- **Showcase Page**: `/about/cards-two-column-showcase`
- **Component Location**: `shared/patterns/CardsTwoColumn/`
## Version History
- **January 2026**: Initial implementation
- Header section with title and description
- 2×2 card grid with 6 color variants (green, neutral-light, neutral-dark, lilac, yellow, blue)
- Window shade hover animation
- Full responsive design
- Dark mode support with correct color mappings for neutral-light and neutral-dark
- Disabled state support for light and dark modes

View File

@@ -0,0 +1,4 @@
export { CardsTwoColumn, type CardsTwoColumnProps, type CardsTwoColumnCardConfig } from './CardsTwoColumn';
export { TextCard, type TextCardProps, type TextCardColor } from 'shared/components/TextCard';
export { default } from './CardsTwoColumn';

View File

@@ -0,0 +1,175 @@
# CarouselCardList Pattern
A horizontal scrolling carousel pattern that displays `CardOffgrid` components with navigation buttons. Features responsive sizing, smooth scrolling, and full dark/light mode theming support.
## Features
- Horizontal scrolling card carousel with navigation buttons
- Automatic button enable/disable based on scroll position
- Two color variants: `neutral` and `green` (inherited by cards and buttons)
- Responsive card sizing across mobile, tablet, and desktop breakpoints
- Full dark mode and light mode support
- Heading and description constrained to page grid
- Hidden scrollbar with smooth scroll behavior
- Keyboard navigation and accessibility support
## Usage
```tsx
import { CarouselCardList } from 'shared/patterns/CarouselCardList';
<CarouselCardList
variant="neutral"
heading="Why Build on the XRP Ledger"
description="Discover the unique features that make XRPL ideal for your project."
cards={[
{
icon: <TokenIcon />,
title: "Native\nTokenization",
description: "Issue and manage digital assets directly on the ledger.",
href: "/docs/tokenization",
},
{
icon: <WalletIcon />,
title: "Low Cost\nTransactions",
description: "Transaction costs are a fraction of a cent.",
href: "/docs/fees",
},
// ... more cards
]}
/>
```
## Props
### CarouselCardListProps
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `variant` | `'neutral' \| 'green'` | No | `'neutral'` | Color variant for cards and navigation buttons |
| `heading` | `ReactNode` | Yes | - | Section heading text |
| `description` | `ReactNode` | Yes | - | Section description text |
| `cards` | `CarouselCardConfig[]` | Yes | - | Array of card configurations |
| `className` | `string` | No | - | Additional CSS classes for the section |
### CarouselCardConfig
Each card in the `cards` array accepts the following properties (same as `CardOffgridProps`, excluding `variant`):
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `icon` | `ReactNode \| string` | Yes | Icon component or image URL |
| `title` | `string` | Yes | Card title (use `\n` for line breaks) |
| `description` | `string` | Yes | Card description text |
| `href` | `string` | No | Link destination URL |
| `onClick` | `() => void` | No | Click handler function |
| `disabled` | `boolean` | No | Disabled state |
| `className` | `string` | No | Additional CSS classes |
## Variants
### Neutral Variant
The default variant using gray color palette. Best for general purpose content sections.
```tsx
<CarouselCardList
variant="neutral"
heading="Platform Features"
description="Explore what makes our platform unique."
cards={cards}
/>
```
### Green Variant
Uses the brand green color palette. Best for featured or highlighted sections.
```tsx
<CarouselCardList
variant="green"
heading="Enterprise Solutions"
description="Purpose-built for institutional adoption."
cards={cards}
/>
```
## Responsive Behavior
| Breakpoint | Card Width | Card Height | Card Padding | Button Size |
|------------|------------|-------------|--------------|-------------|
| Mobile (< 576px) | 343px | 400px | 16px | 37px |
| Tablet (576px - 991px) | 356px | 440px | 20px | 37px |
| Desktop (≥ 992px) | 400px | 480px | 24px | 40px |
### Spacing Tokens
| Token | Mobile | Tablet | Desktop |
|-------|--------|--------|---------|
| Header gap | 8px | 8px | 16px |
| Section gap | 24px | 32px | 40px |
| Cards gap | 8px | 8px | 8px |
## Accessibility
- Navigation buttons have descriptive `aria-label` attributes ("Previous cards", "Next cards")
- Carousel track has `role="region"` with `aria-label="Card carousel"`
- Keyboard navigation supported via Tab and arrow keys
- Focus ring visible on keyboard navigation (uses `focus-visible`)
- Disabled buttons properly convey state via `disabled` attribute and visual styling
- Cards support keyboard interaction (Tab, Enter, Space)
## Design Tokens
### Colors (Dark Mode - Default)
**Neutral Variant:**
- Button Enabled: `$gray-300` (#CAD4DF)
- Button Hover: `$gray-400` (#8A919A)
- Button Disabled: `$gray-500` @ 50% opacity
**Green Variant:**
- Button Enabled: `$green-300` (#21E46B)
- Button Hover: `$green-200` (#70EE97)
- Button Disabled: `$green-100` (#ACFFC5)
### Colors (Light Mode - `html.light`)
**Neutral Variant:**
- Button Enabled: `$gray-300` (#CAD4DF)
- Button Hover: `$gray-400` (#8A919A)
- Button Disabled: `$gray-100` (#F0F3F7)
**Green Variant:**
- Button Enabled: `$green-300` (#21E46B)
- Button Hover: `$green-200` (#70EE97)
- Button Disabled: `$green-100` (#ACFFC5)
## CSS Classes
| Class | Description |
|-------|-------------|
| `.bds-carousel-card-list` | Base section container |
| `.bds-carousel-card-list--neutral` | Neutral color variant |
| `.bds-carousel-card-list--green` | Green color variant |
| `.bds-carousel-card-list__header` | Header wrapper (title, subtitle, nav) |
| `.bds-carousel-card-list__header-content` | Title and subtitle wrapper |
| `.bds-carousel-card-list__heading` | Section heading (uses `.h-md`) |
| `.bds-carousel-card-list__description` | Section description (uses `.body-l`) |
| `.bds-carousel-card-list__nav` | Navigation buttons wrapper |
| `.bds-carousel-card-list__button` | Navigation button |
| `.bds-carousel-card-list__button--prev` | Previous button modifier |
| `.bds-carousel-card-list__button--disabled` | Disabled button modifier |
| `.bds-carousel-card-list__track-wrapper` | Scroll container wrapper |
| `.bds-carousel-card-list__track` | Horizontal scroll track |
| `.bds-carousel-card-list__card` | Individual card wrapper |
## Showcase
View the pattern showcase at: `/about/carousel-card-list-showcase`
## Design References
- **Main Design:** [Section Carousel - Card List (Figma)](https://www.figma.com/design/w0CVv1c40nWDRD27mLiMWS/Section-Carousel---Card-List?node-id=15055-3730&m=dev)
- **Button States:** [Carousel Button States (Figma)](https://www.figma.com/design/w0CVv1c40nWDRD27mLiMWS/Section-Carousel---Card-List?node-id=15055-1033&m=dev)

View File

@@ -0,0 +1,292 @@
// BDS CarouselCardList Pattern Styles
// Brand Design System - Horizontal scrolling carousel with CardOffgrid components
//
// Naming Convention: BEM with 'bds' namespace
// .bds-carousel-card-list - Base section container
// .bds-carousel-card-list--neutral - Neutral color variant
// .bds-carousel-card-list--green - Green color variant
// .bds-carousel-card-list__header - Header wrapper (title, subtitle, nav)
// .bds-carousel-card-list__header-content - Title and subtitle wrapper
// .bds-carousel-card-list__heading - Section heading (uses .h-md)
// .bds-carousel-card-list__description - Section description (uses .body-l)
// .bds-carousel-card-list__nav - Navigation buttons wrapper
// .bds-carousel-card-list__track-wrapper - Scroll container wrapper
// .bds-carousel-card-list__track - Horizontal scroll track
// .bds-carousel-card-list__card - Individual card wrapper
//
// Note: Navigation button styles are in shared/components/CarouselButton/CarouselButton.scss
// =============================================================================
// Design Tokens (from Figma)
// =============================================================================
$bds-grid-gutter: 8px;
// Grid padding (matches PageGrid container padding)
$bds-carousel-grid-padding-sm: 18px; // Mobile
$bds-carousel-grid-padding-md: 24px; // Tablet
$bds-carousel-grid-padding-lg: 32px; // Desktop (lg+)
$bds-carousel-grid-max-width: 1280px; // Max container width (per _breakpoints.scss $xl)
// Spacing - Header gap (between heading and description)
$bds-carousel-header-gap-sm: 8px; // Mobile
$bds-carousel-header-gap-md: 8px; // Tablet
$bds-carousel-header-gap-lg: 16px; // Desktop
// Spacing - Section gap (between header content and buttons row on mobile)
$bds-carousel-section-gap-sm: 24px; // Mobile
$bds-carousel-section-gap-md: 32px; // Tablet
$bds-carousel-section-gap-lg: 40px; // Desktop
// Spacing - Gap between header and cards
$bds-carousel-cards-gap-sm: 24px; // Mobile
$bds-carousel-cards-gap-md: 32px; // Tablet
$bds-carousel-cards-gap-lg: 40px; // Desktop
// Button gap (button styles are in shared/components/CarouselButton)
$bds-carousel-button-gap: 8px;
// Card dimensions per breakpoint
$bds-carousel-card-width-sm: 343px; // Mobile
$bds-carousel-card-height-sm: 400px;
$bds-carousel-card-width-md: 356px; // Tablet
$bds-carousel-card-height-md: 440px;
$bds-carousel-card-width-lg: 400px; // Desktop
$bds-carousel-card-height-lg: 480px;
// Card padding per breakpoint
$bds-carousel-card-padding-sm: 16px;
$bds-carousel-card-padding-md: 20px;
$bds-carousel-card-padding-lg: 24px;
// Transition
$bds-carousel-transition: 200ms cubic-bezier(0.98, 0.12, 0.12, 0.98);
// =============================================================================
// Section Container
// =============================================================================
.bds-carousel-card-list {
width: 100%;
// Constrain to max-width at xl breakpoint (per _breakpoints.scss)
@include media-breakpoint-up(xl) {
max-width: $bds-carousel-grid-max-width;
margin-left: auto;
margin-right: auto;
}
// Allow focus rings to be visible (no overflow:hidden)
}
// =============================================================================
// Header Section
// =============================================================================
.bds-carousel-card-list__header {
display: flex;
flex-direction: column;
gap: $bds-carousel-section-gap-sm;
// Apply same padding as track to align header with cards
padding-left: $bds-carousel-grid-padding-sm;
padding-right: $bds-carousel-grid-padding-sm;
@include media-breakpoint-up(md) {
gap: $bds-carousel-section-gap-md;
padding-left: $bds-carousel-grid-padding-md;
padding-right: $bds-carousel-grid-padding-md;
}
// Row layout only at desktop (lg and up)
@include media-breakpoint-up(lg) {
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
gap: $bds-carousel-section-gap-lg;
padding-left: $bds-carousel-grid-padding-lg;
padding-right: $bds-carousel-grid-padding-lg;
}
}
.bds-carousel-card-list__header-content {
display: flex;
flex-direction: column;
gap: $bds-carousel-header-gap-sm;
// Full width on mobile and tablet
max-width: 100%;
@include media-breakpoint-up(md) {
gap: $bds-carousel-header-gap-md;
}
// Constrain heading/description to grid (8 columns at desktop)
@include media-breakpoint-up(lg) {
gap: $bds-carousel-header-gap-lg;
max-width: 808px; // Desktop: 8 columns
}
}
.bds-carousel-card-list__heading {
margin: 0;
// Typography handled by .h-md class from _font.scss
}
.bds-carousel-card-list__description {
margin: 0;
// Typography handled by .body-l class from _font.scss
}
// =============================================================================
// Navigation Buttons Container
// =============================================================================
.bds-carousel-card-list__nav {
display: flex;
gap: $bds-carousel-button-gap;
justify-content: flex-end;
flex-shrink: 0;
// Add padding to allow focus ring to be visible without clipping
padding: 4px;
margin: -4px;
}
// =============================================================================
// Scroll Track
// =============================================================================
.bds-carousel-card-list__track-wrapper {
margin-top: $bds-carousel-cards-gap-sm;
overflow: visible;
// Add left padding here so it's OUTSIDE the scrollable area
padding-left: $bds-carousel-grid-padding-sm;
@include media-breakpoint-up(md) {
margin-top: $bds-carousel-cards-gap-md;
padding-left: $bds-carousel-grid-padding-md;
}
@include media-breakpoint-up(lg) {
margin-top: $bds-carousel-cards-gap-lg;
padding-left: $bds-carousel-grid-padding-lg;
}
}
.bds-carousel-card-list__track {
display: flex;
gap: $bds-grid-gutter;
overflow-x: auto;
overflow-y: visible;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
// Hide scrollbar but keep functionality
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
// Vertical padding to prevent focus ring clipping
padding-top: 4px;
padding-bottom: 4px;
margin-top: -4px;
margin-bottom: -4px;
// Focus outline for keyboard navigation
&:focus {
outline: none;
}
&:focus-visible {
outline: 2px solid $white;
outline-offset: 4px;
}
}
// =============================================================================
// Card Wrapper
// =============================================================================
.bds-carousel-card-list__card {
flex-shrink: 0;
scroll-snap-align: start;
// Override CardOffgrid dimensions for carousel
.bds-card-offgrid {
width: $bds-carousel-card-width-sm;
height: $bds-carousel-card-height-sm;
padding: $bds-carousel-card-padding-sm;
@include media-breakpoint-up(md) {
width: $bds-carousel-card-width-md;
height: $bds-carousel-card-height-md;
padding: $bds-carousel-card-padding-md;
}
@include media-breakpoint-up(lg) {
width: $bds-carousel-card-width-lg;
height: $bds-carousel-card-height-lg;
padding: $bds-carousel-card-padding-lg;
}
// Fix: Prevent unwanted hover styles from parent styles
// No text underline on hover
&:hover {
text-decoration: none;
}
// Ensure title and description never have underline
.bds-card-offgrid__title,
.bds-card-offgrid__description {
text-decoration: none;
&:hover {
text-decoration: none;
}
}
// Ensure icon does not change color on hover
.bds-card-offgrid__icon-container {
// Icon color is inherited from card text color, which CardOffgrid manages
// No additional color changes should happen on hover
> * {
transition: none;
}
}
}
}
// =============================================================================
// DARK MODE (Default) - Section Text Colors
// =============================================================================
// Section text colors - Dark Mode (applies to both neutral and green card variants)
.bds-carousel-card-list--neutral,
.bds-carousel-card-list--green {
.bds-carousel-card-list__heading,
.bds-carousel-card-list__description {
color: $white;
}
}
// =============================================================================
// LIGHT MODE (html.light) - Color Variants
// =============================================================================
html.light {
.bds-carousel-card-list__track {
&:focus-visible {
outline-color: $gray-900;
}
}
// Section text colors - Light Mode
.bds-carousel-card-list--neutral,
.bds-carousel-card-list--green {
.bds-carousel-card-list__heading,
.bds-carousel-card-list__description {
color: $black;
}
}
}

View File

@@ -0,0 +1,171 @@
import React, { useRef, useState, useCallback, useEffect } from 'react';
import clsx from 'clsx';
import { CardOffgrid, CardOffgridProps } from '../../components/CardOffgrid';
import { CarouselButton } from '../../components/CarouselButton';
import type { ButtonProps } from '../../components/Button';
/**
* Configuration for a single card in the CarouselCardList pattern
* Extends CardOffgridProps but removes variant (controlled by carousel)
*/
export type CarouselCardConfig = Omit<CardOffgridProps, 'variant'>;
/** BEM class name for card elements */
const CARD_CLASS_NAME = 'bds-carousel-card-list__card';
/**
* Props for the CarouselCardList pattern component
*/
export interface CarouselCardListProps extends React.ComponentPropsWithoutRef<'section'> {
/** Color variant of the cards */
variant?: 'neutral' | 'green';
/** Color variant of the navigation buttons (independent of card color). Defaults to 'neutral'. Derived from Button color prop. */
buttonVariant?: ButtonProps['color'] | 'neutral';
/** Section heading text */
heading: React.ReactNode;
/** Section description text */
description: React.ReactNode;
/** Array of card configurations */
cards: readonly CarouselCardConfig[];
}
/**
* Generates a stable key for a card based on its properties.
*/
const getCardKey = (card: CarouselCardConfig, index: number): string | number => {
if (card.href) return card.href;
if (card.title) return `${card.title}-${index}`;
return index;
};
/**
* CarouselCardList Pattern Component
*
* A horizontal scrolling carousel that displays CardOffgrid components.
* Features navigation buttons that scroll cards in/out of view.
* The navigation button colors can be set independently of the card colors
* using the `buttonVariant` prop.
*
* @example
* ```tsx
* <CarouselCardList
* variant="neutral"
* buttonVariant="green"
* heading="Why Choose Our Platform"
* description="Discover the benefits of our solution."
* cards={[
* { icon: <Icon />, title: "Feature 1", description: "..." },
* { icon: <Icon />, title: "Feature 2", description: "..." },
* ]}
* />
* ```
*/
export const CarouselCardList = React.forwardRef<HTMLElement, CarouselCardListProps>(
(props, ref) => {
const { variant = 'neutral', buttonVariant = 'neutral', heading, description, cards, className, ...rest } = props;
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [canScrollPrev, setCanScrollPrev] = useState(false);
const [canScrollNext, setCanScrollNext] = useState(true);
// Check scroll position and update button states
const updateScrollButtons = useCallback(() => {
const container = scrollContainerRef.current;
if (!container) return;
const { scrollLeft, scrollWidth, clientWidth } = container;
setCanScrollPrev(scrollLeft > 0);
setCanScrollNext(scrollLeft + clientWidth < scrollWidth - 1);
}, []);
// Initialize and listen for scroll events
useEffect(() => {
const container = scrollContainerRef.current;
if (!container) return;
updateScrollButtons();
container.addEventListener('scroll', updateScrollButtons, { passive: true });
window.addEventListener('resize', updateScrollButtons);
return () => {
container.removeEventListener('scroll', updateScrollButtons);
window.removeEventListener('resize', updateScrollButtons);
};
}, [updateScrollButtons, cards.length]);
// Scroll by one card width
const scroll = useCallback((direction: 'prev' | 'next') => {
const container = scrollContainerRef.current;
if (!container) return;
// Get the first card to determine scroll amount
const card = container.querySelector(`.${CARD_CLASS_NAME}`) as HTMLElement;
if (!card) return;
const cardWidth = card.offsetWidth;
const gap = 8; // 8px gap between cards
const scrollAmount = cardWidth + gap;
container.scrollBy({
left: direction === 'next' ? scrollAmount : -scrollAmount,
behavior: 'smooth',
});
}, []);
// Early return for empty cards
if (cards.length === 0) {
console.warn('CarouselCardList: No cards provided');
return null;
}
return (
<section
ref={ref}
className={clsx('bds-carousel-card-list', `bds-carousel-card-list--${variant}`, className)}
{...rest}
>
{/* Header with title, description, and navigation buttons */}
<div className="bds-carousel-card-list__header">
<div className="bds-carousel-card-list__header-content">
<h2 className="bds-carousel-card-list__heading h-md">{heading}</h2>
<p className="bds-carousel-card-list__description body-l">{description}</p>
</div>
<div className="bds-carousel-card-list__nav">
{(['prev', 'next'] as const).map((direction) => (
<CarouselButton
key={direction}
direction={direction}
variant={buttonVariant}
disabled={direction === 'prev' ? !canScrollPrev : !canScrollNext}
onClick={() => scroll(direction)}
aria-label={direction === 'prev' ? 'Previous cards' : 'Next cards'}
/>
))}
</div>
</div>
{/* Cards scroll container - full bleed */}
<div className="bds-carousel-card-list__track-wrapper">
<div
ref={scrollContainerRef}
className="bds-carousel-card-list__track"
role="region"
aria-label="Card carousel"
tabIndex={0}
>
{cards.map((card, index) => (
<div key={getCardKey(card, index)} className={CARD_CLASS_NAME}>
<CardOffgrid {...card} variant={variant} />
</div>
))}
</div>
</div>
</section>
);
}
);
CarouselCardList.displayName = 'CarouselCardList';
export default CarouselCardList;

View File

@@ -0,0 +1,3 @@
export { CarouselCardList, type CarouselCardListProps, type CarouselCardConfig } from './CarouselCardList';
export { default } from './CarouselCardList';

View File

@@ -0,0 +1,437 @@
// BDS CarouselFeatured Pattern Styles
// Brand Design System - Featured image carousel with two-column layout
//
// Layout:
// - Desktop (lg+): Two-column layout - Image LEFT (50%), Content RIGHT (50%)
// - Tablet/Mobile: Single column - Content TOP, Image BOTTOM
//
// Naming Convention: BEM with 'bds' namespace
// .bds-carousel-featured - Base section container
// .bds-carousel-featured__media-col - Image/media column wrapper
// .bds-carousel-featured__content-col - Content column wrapper
// .bds-carousel-featured__content - Content column
// .bds-carousel-featured__header - Header row (heading + nav)
// .bds-carousel-featured__heading - Section heading
// .bds-carousel-featured__nav - Navigation buttons wrapper
// .bds-carousel-featured__bottom - Bottom section (features + CTA)
// .bds-carousel-featured__features - Feature list container
// .bds-carousel-featured__feature - Individual feature item
// .bds-carousel-featured__feature-title - Feature title
// .bds-carousel-featured__feature-description - Feature description
// .bds-carousel-featured__cta - CTA section (buttons + mobile nav)
// .bds-carousel-featured__buttons - Button group wrapper
// .bds-carousel-featured__slides - Slides container
// .bds-carousel-featured__slide-track - Sliding track
// .bds-carousel-featured__slide - Individual slide
// .bds-carousel-featured__slide--active - Active slide modifier
// .bds-carousel-featured__image - Slide image
//
// Note: This file is imported within xrpl.scss after Bootstrap and project
// variables are loaded, so $grid-breakpoints, colors, and mixins are available.
// =============================================================================
// Design Tokens (from Figma)
// =============================================================================
// Spacing
$bds-carousel-featured-padding-sm: 24px 16px;
$bds-carousel-featured-padding-md: 32px 24px;
$bds-carousel-featured-padding-lg: 40px 32px;
// Content gap between image and content columns
$bds-carousel-featured-column-gap: 8px;
// Transition
$bds-carousel-featured-transition: 400ms cubic-bezier(0.4, 0, 0.2, 1);
// =============================================================================
// Color Variant Configuration Map
// =============================================================================
// Define all background variants with their color properties (Dark Mode)
$bds-carousel-featured-variants: (
'grey': (
'bg-color': $gray-300,
'text-color': $black,
'divider-color': $black,
'button-variant': 'black',
'button-bg': $black,
'button-color': $white,
'button-hover': $gray-500,
'button-active': $black
),
'neutral': (
'bg-color': $black,
'text-color': $white,
'divider-color': $white,
'button-variant': 'green',
'button-bg': $green-300,
'button-color': $black,
'button-hover': $green-200,
'button-active': $green-300
),
'yellow': (
'bg-color': $yellow-100,
'text-color': $black,
'divider-color': $black,
'button-variant': 'black',
'button-bg': $black,
'button-color': $white,
'button-hover': $gray-500,
'button-active': $black
)
);
// Define light mode variant overrides
$bds-carousel-featured-variants-light: (
'grey': (
'bg-color': $gray-200,
'text-color': $black,
'divider-color': $black
),
'neutral': (
'bg-color': $white,
'text-color': $black,
'divider-color': $black
),
'yellow': (
'bg-color': $yellow-100,
'text-color': $black,
'divider-color': $black
)
);
// =============================================================================
// Mixins: Apply Background Variant Styles
// =============================================================================
// Full variant mixin (for dark mode with button styles)
@mixin carousel-featured-variant($variant-name, $config) {
&--bg-#{$variant-name} {
background-color: map-get($config, 'bg-color');
// Text colors
.bds-carousel-featured__heading,
.bds-carousel-featured__feature-title,
.bds-carousel-featured__feature-description {
color: map-get($config, 'text-color');
}
// Divider color
.bds-divider {
background-color: map-get($config, 'divider-color');
}
// Carousel nav buttons - enabled states only
// Disabled states are handled by CarouselButton component styles
.bds-carousel-button--#{map-get($config, 'button-variant')} {
background-color: map-get($config, 'button-bg');
color: map-get($config, 'button-color');
&:hover:not(:disabled) {
background-color: map-get($config, 'button-hover');
}
&:active:not(:disabled) {
background-color: map-get($config, 'button-active');
}
}
}
}
// Light mode variant mixin (only colors, no button states)
@mixin carousel-featured-variant-light($variant-name, $config) {
.bds-carousel-featured--bg-#{$variant-name} {
background-color: map-get($config, 'bg-color');
.bds-carousel-featured__heading,
.bds-carousel-featured__feature-title,
.bds-carousel-featured__feature-description {
color: map-get($config, 'text-color');
}
.bds-divider {
background-color: map-get($config, 'divider-color');
}
}
}
// =============================================================================
// Base Container Styles
// =============================================================================
.bds-carousel-featured {
width: 100%;
overflow: hidden;
// Default background - dark mode default (grey variant)
background-color: $gray-300;
// Mobile (default)
padding: $bds-carousel-featured-padding-sm;
// Tablet
@include media-breakpoint-up(md) {
padding: $bds-carousel-featured-padding-md;
}
// Desktop
@include media-breakpoint-up(lg) {
padding: $bds-carousel-featured-padding-lg;
}
// Max width constraint
@include media-breakpoint-up(xl) {
max-width: 1280px;
margin-left: auto;
margin-right: auto;
}
// ---------------------------------------------------------------------------
// Background Color Variants
// ---------------------------------------------------------------------------
// Dark Mode (default) - Generate all variant styles using the mixin
@include bds-theme-mode(dark) {
@each $variant-name, $config in $bds-carousel-featured-variants {
@include carousel-featured-variant($variant-name, $config);
}
}
}
// =============================================================================
// Content Column
// =============================================================================
.bds-carousel-featured__content-col {
display: flex;
flex-direction: column;
@include media-breakpoint-up(lg) {
// Stretch to match image height
align-self: stretch;
// Add 8px padding-left to create 16px total gap (8px row gap + 8px padding)
padding-left: 8px;
}
}
.bds-carousel-featured__content {
display: flex;
flex-direction: column;
width: 100%;
gap: 0; // Use space-between instead
min-height: 500px; // Mobile min height
justify-content: space-between; // Header at top, features+CTA at bottom
@include media-breakpoint-up(md) {
min-height: 440px; // Tablet min height
}
@include media-breakpoint-up(lg) {
flex: 1;
min-height: auto; // Reset min-height on desktop
}
}
// =============================================================================
// Header Section (Heading + Nav)
// =============================================================================
.bds-carousel-featured__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
width: 100%;
}
.bds-carousel-featured__heading {
margin: 0;
// Dark mode default: light text on dark background
color: $white;
max-width: 392px;
}
// =============================================================================
// Bottom Section (Features + CTA grouped together)
// =============================================================================
.bds-carousel-featured__bottom {
display: flex;
flex-direction: column;
gap: 24px; // Mobile
@include media-breakpoint-up(md) {
gap: 32px; // Tablet
}
@include media-breakpoint-up(lg) {
gap: 40px; // Desktop
}
}
// =============================================================================
// Navigation Buttons
// =============================================================================
.bds-carousel-featured__nav {
display: flex;
gap: 8px;
flex-shrink: 0;
// Desktop nav (in header row)
&--desktop {
display: none;
@include media-breakpoint-up(lg) {
display: flex;
}
}
// Mobile/Tablet nav (in CTA row)
&--mobile {
display: flex;
@include media-breakpoint-up(lg) {
display: none;
}
}
}
// =============================================================================
// Feature List
// =============================================================================
.bds-carousel-featured__features {
display: flex;
flex-direction: column;
width: 100%;
}
.bds-carousel-featured__feature {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
// Spacing between description and next divider
// Mobile/Tablet: 16px, Desktop: 24px
&:not(:first-child) {
padding-top: 16px;
@include media-breakpoint-up(lg) {
padding-top: 24px;
}
}
}
.bds-carousel-featured__feature-title {
margin: 0;
// Dark mode default: light text on dark background
color: $white;
}
.bds-carousel-featured__feature-description {
margin: 0;
// Dark mode default: muted light text on dark background
color: $gray-400;
}
// =============================================================================
// CTA Section (Buttons + Mobile Nav)
// =============================================================================
.bds-carousel-featured__cta {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-end;
width: 100%;
gap: 16px;
// Tablet+: no wrap needed
@include media-breakpoint-up(md) {
flex-wrap: nowrap;
gap: 0;
}
// Desktop: nav is hidden, so buttons just align left
@include media-breakpoint-up(lg) {
justify-content: flex-start;
}
}
// =============================================================================
// Slides Container
// =============================================================================
.bds-carousel-featured__slides {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
}
.bds-carousel-featured__slide-track {
display: flex;
transition: transform $bds-carousel-featured-transition;
will-change: transform;
}
// =============================================================================
// Individual Slides
// =============================================================================
.bds-carousel-featured__slide {
flex: 0 0 100%;
width: 100%;
position: relative;
// Mobile: 343/193 aspect ratio
aspect-ratio: 343 / 193;
// Tablet: 16/9 aspect ratio
@include media-breakpoint-up(md) {
aspect-ratio: 16 / 9;
}
// Desktop: Square aspect ratio (604x604)
@include media-breakpoint-up(lg) {
aspect-ratio: 1 / 1;
}
}
.bds-carousel-featured__image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
pointer-events: none;
}
// =============================================================================
// LIGHT MODE (html.light) - Color Overrides
// =============================================================================
html.light {
// Default (no variant class) - Light mode: gray-200 background
.bds-carousel-featured {
background-color: $gray-200;
.bds-carousel-featured__heading,
.bds-carousel-featured__feature-title,
.bds-carousel-featured__feature-description {
color: $black;
}
.bds-divider {
background-color: $black;
}
}
// Generate all light mode variant overrides using the mixin
@each $variant-name, $config in $bds-carousel-featured-variants-light {
@include carousel-featured-variant-light($variant-name, $config);
}
}

View File

@@ -0,0 +1,258 @@
import React, { useState, useCallback } from 'react';
import clsx from 'clsx';
import { CarouselButton } from '../../components/CarouselButton';
import { Divider } from '../../components/Divider';
import { PageGrid, PageGridRow, PageGridCol } from '../../components/PageGrid';
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
/**
* Props for a single slide in the CarouselFeatured component
*/
export interface CarouselSlide {
/** Unique identifier for the slide */
id: string | number;
/** Image source URL */
imageSrc: string;
/** Alt text for the image */
imageAlt: string;
}
/**
* Props for a feature list item
*/
export interface CarouselFeatureItem {
/** Feature title */
title: string;
/** Feature description */
description: string;
}
/**
* Background color options for CarouselFeatured
* Each variant adapts to light/dark mode:
* - 'grey': Light mode: gray-200 (#E6EAF0), Dark mode: gray-300 (#CAD4DF)
* - 'neutral': Light mode: white (#FFF), Dark mode: black (#141414)
* - 'yellow': Light mode: yellow-100 (#F3F1EB), Dark mode: yellow-100 (#F3F1EB)
*/
export type CarouselFeaturedBackground = 'grey' | 'neutral' | 'yellow';
/**
* Props for the CarouselFeatured pattern component
*/
export interface CarouselFeaturedProps extends React.ComponentPropsWithoutRef<'section'> {
/** Array of slides to display */
slides: readonly CarouselSlide[];
/** Heading text displayed at the top of the content area */
heading: string;
/** Array of feature items to display in the list */
features: readonly CarouselFeatureItem[];
/** Button configurations (1-2 buttons supported) */
buttons?: ButtonConfig[];
/** Background color variant. Defaults to 'grey'. */
background?: CarouselFeaturedBackground;
}
/**
* CarouselFeatured Pattern Component
*
* A featured image carousel with two-column layout on desktop (image left, content right)
* and single-column layout on tablet/mobile (content top, image bottom).
* Features a heading, feature list with dividers, and optional buttons.
*
* @example
* ```tsx
* <CarouselFeatured
* heading="Powered by Developers"
* features={[
* { title: "Easy-to-Integrate APIs", description: "Build with common languages..." },
* { title: "Full Lifecycle Support", description: "From dev tools to deployment..." },
* ]}
* buttons={[
* { label: "Get Started", href: "/docs" },
* { label: "Learn More", href: "/about" }
* ]}
* slides={[
* { id: 1, imageSrc: '/image1.jpg', imageAlt: 'Slide 1' },
* ]}
* />
* ```
*/
export const CarouselFeatured = React.forwardRef<HTMLElement, CarouselFeaturedProps>(
(props, ref) => {
const {
slides,
heading,
features,
buttons,
background = 'grey',
className,
children,
...rest
} = props;
const [currentIndex, setCurrentIndex] = useState(0);
const canGoPrev = currentIndex > 0;
const canGoNext = currentIndex < slides.length - 1;
// Validate buttons if provided (max 2 buttons supported)
const buttonValidation = validateButtonGroup(buttons, 2);
const hasButtons = buttonValidation.hasButtons;
const goToPrev = useCallback(() => {
if (canGoPrev) {
setCurrentIndex((prev) => prev - 1);
}
}, [canGoPrev]);
const goToNext = useCallback(() => {
if (canGoNext) {
setCurrentIndex((prev) => prev + 1);
}
}, [canGoNext]);
// Early return for empty slides
if (slides.length === 0) {
console.warn('CarouselFeatured: No slides provided');
return null;
}
// Determine carousel nav button variant based on background
// grey/yellow → black (always), neutral → green (always)
const buttonVariant = background === 'neutral' ? 'green' : 'black';
return (
<PageGrid
ref={ref as React.Ref<HTMLDivElement>}
className={clsx(
'bds-carousel-featured',
`bds-carousel-featured--bg-${background}`,
className
)}
aria-roledescription="carousel"
aria-label={heading}
{...rest}>
<PageGridRow>
{/* Content Column - Right on desktop, top on mobile */}
<PageGridCol
span={{ base: 4, md: 8, lg: 6 }}
className="bds-carousel-featured__content-col order-1 order-lg-2"
>
<div className="bds-carousel-featured__content">
{/* Header row with heading and nav buttons */}
<div className="bds-carousel-featured__header">
<h2 className="bds-carousel-featured__heading h-md">{heading}</h2>
<div className={clsx(
'bds-carousel-featured__nav',
'bds-carousel-featured__nav--desktop',
slides.length === 1 && 'd-none'
)}>
{(['prev', 'next'] as const).map((direction) => (
<CarouselButton
key={direction}
direction={direction}
variant={buttonVariant}
disabled={direction === 'prev' ? !canGoPrev : !canGoNext}
onClick={direction === 'prev' ? goToPrev : goToNext}
aria-label={direction === 'prev' ? 'Previous slide' : 'Next slide'}
/>
))}
</div>
</div>
{/* Bottom section: features + CTA grouped together */}
<div className="bds-carousel-featured__bottom">
{/* Feature list with dividers */}
<div className="bds-carousel-featured__features">
{features.map((feature, index) => (
<div key={index} className="bds-carousel-featured__feature">
<Divider color="base" weight="regular" />
<p className="bds-carousel-featured__feature-title body-r">{feature.title}</p>
<p className="bds-carousel-featured__feature-description label-l">{feature.description}</p>
</div>
))}
</div>
{/* CTA section with buttons and mobile nav */}
<div className="bds-carousel-featured__cta">
{/* Buttons wrapper - groups primary and tertiary together */}
{hasButtons && (
<ButtonGroup
buttons={buttonValidation.buttons}
color="black"
forceColor={background !== 'neutral'}
className="bds-carousel-featured__buttons"
/>
)}
{/* Mobile/Tablet nav buttons */}
<div className={clsx(
'bds-carousel-featured__nav',
'bds-carousel-featured__nav--mobile',
slides.length === 1 && 'd-none'
)}>
{(['prev', 'next'] as const).map((direction) => (
<CarouselButton
key={direction}
direction={direction}
variant={buttonVariant}
disabled={direction === 'prev' ? !canGoPrev : !canGoNext}
onClick={direction === 'prev' ? goToPrev : goToNext}
aria-label={direction === 'prev' ? 'Previous slide' : 'Next slide'}
/>
))}
</div>
</div>
</div>
</div>
</PageGridCol>
{/* Image/Media Column - Left on desktop, bottom on mobile */}
<PageGridCol
span={{ base: 4, md: 8, lg: 6 }}
className="bds-carousel-featured__media-col order-2 order-lg-1"
>
<div
className="bds-carousel-featured__slides"
role="group"
aria-roledescription="slide"
aria-label={`Slide ${currentIndex + 1} of ${slides.length}`}
>
<div
className="bds-carousel-featured__slide-track"
style={{ transform: `translateX(-${currentIndex * 100}%)` }}
>
{slides.map((slide, index) => (
<div
key={slide.id}
className={clsx(
'bds-carousel-featured__slide',
{ 'bds-carousel-featured__slide--active': index === currentIndex }
)}
aria-hidden={index !== currentIndex}
>
<img
src={slide.imageSrc}
alt={slide.imageAlt}
className="bds-carousel-featured__image"
loading={index === 0 ? 'eager' : 'lazy'}
/>
</div>
))}
</div>
</div>
</PageGridCol>
</PageGridRow>
{/* Render any additional children */}
{children}
</PageGrid>
);
}
);
CarouselFeatured.displayName = 'CarouselFeatured';
export default CarouselFeatured;

View File

@@ -0,0 +1,117 @@
# CarouselFeatured
A featured image carousel pattern with a two-column layout on desktop (image left, content right) and single-column layout on tablet/mobile (content top, image bottom). Features a heading, feature list with dividers, optional buttons, and navigation controls.
## Usage
```tsx
import { CarouselFeatured } from '@/shared/patterns/CarouselFeatured';
<CarouselFeatured
heading="Powered by Developers"
features={[
{ title: "Easy-to-Integrate APIs", description: "Build with common languages..." },
{ title: "Full Lifecycle Support", description: "From dev tools to deployment..." },
]}
buttons={[
{ label: "Get Started", href: "/docs" },
{ label: "Learn More", href: "/about" }
]}
slides={[
{ id: 1, imageSrc: '/image1.jpg', imageAlt: 'Slide 1' },
{ id: 2, imageSrc: '/image2.jpg', imageAlt: 'Slide 2' },
]}
background="grey"
/>
```
## Props
### Required Props
| Prop | Type | Description |
|------|------|-------------|
| `heading` | `string` | Heading text displayed at the top of the content area |
| `features` | `CarouselFeatureItem[]` | Array of feature items with title and description |
| `slides` | `CarouselSlide[]` | Array of slides to display in the carousel |
### Optional Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `buttons` | `ButtonConfig[]` | `undefined` | Array of button configurations (1-2 buttons supported, uses ButtonGroup) |
| `background` | `'grey' \| 'neutral' \| 'yellow'` | `'grey'` | Background color variant |
## Type Definitions
### CarouselSlide
```tsx
interface CarouselSlide {
id: string | number; // Unique identifier for the slide
imageSrc: string; // Image source URL
imageAlt: string; // Alt text for the image
}
```
### CarouselFeatureItem
```tsx
interface CarouselFeatureItem {
title: string; // Feature title
description: string; // Feature description
}
```
### ButtonConfig
```tsx
interface ButtonConfig {
label: string; // Button text
href?: string; // Optional link URL
onClick?: () => void; // Optional click handler
forceColor?: boolean; // Force button color override
}
```
## Background Variants
The component supports three background variants that adapt to light/dark mode:
- **`grey`** (default): Light mode: gray-200 (#E6EAF0), Dark mode: gray-300 (#CAD4DF)
- **`neutral`**: Light mode: white (#FFF), Dark mode: black (#141414)
- **`yellow`**: Light mode: yellow-100 (#F3F1EB), Dark mode: yellow-100 (#F3F1EB)
## Features
- **Responsive Layout**: Two-column on desktop (lg+), single-column on mobile/tablet
- **Image Carousel**: Navigate through multiple slides with prev/next buttons
- **Auto-hide Navigation**: Navigation buttons automatically hide when only one slide is present
- **Feature List**: Display multiple features with dividers
- **Button Group**: Supports 1-2 buttons with validation
- **Background Variants**: Three color options with light/dark mode support
- **Accessibility**: Proper ARIA labels for navigation buttons
## Layout Behavior
### Desktop (lg+)
- Image column on the left (6 columns)
- Content column on the right (6 columns)
- Navigation buttons in header (desktop variant)
### Tablet/Mobile
- Content section at the top
- Image section at the bottom
- Navigation buttons in CTA section (mobile variant)
## Examples
See the [showcase page](../../../about/carousel-featured-showcase.page.tsx) for live examples with different configurations.
## Notes
- Navigation buttons are automatically hidden when `slides.length === 1`
- Buttons are validated using `validateButtonGroup` with a maximum of 2 buttons
- Button colors are automatically adjusted based on the background variant
- The component uses `ButtonGroup` pattern for consistent button styling

View File

@@ -0,0 +1,9 @@
export {
CarouselFeatured,
type CarouselFeaturedProps,
type CarouselFeaturedBackground,
type CarouselSlide,
type CarouselFeatureItem
} from './CarouselFeatured';
export { default } from './CarouselFeatured';

View File

@@ -0,0 +1,260 @@
// =============================================================================
// FeatureSingleTopic Pattern
// =============================================================================
// A feature section pattern with single topic layout for title and media.
// Supports variants (default, accentSurface).
// Orientation (left, right) is handled via Bootstrap utility classes in TSX.
// Based on Figma: 1280px desktop design with 706px image + content area
//
// Note: Buttons are rendered using the ButtonGroup component.
// =============================================================================
// =============================================================================
// Design Tokens
// =============================================================================
// Background colors from _colors.scss
$bds-single-topic-bg: $white; // #FFFFFF (Neutral-white)
$bds-single-topic-title-bg: $gray-200; // #E6EAF0 (Neutral-200) for accentSurface variant
// Text colors from _colors.scss
$bds-single-topic-title-color: $black; // #141414 (Neutral-black)
$bds-single-topic-description-color: $gray-500; // #72777E (Neutral-500)
// Spacing - Desktop (≥992px) - based on Figma 1280px design
$bds-single-topic-desktop-py: 40px; // Vertical padding from Figma
$bds-single-topic-desktop-content-pl: 8px; // Content left padding
$bds-single-topic-desktop-description-gap: 40px; // Gap between description and ButtonGroup
$bds-single-topic-desktop-title-padding: 16px; // Title section padding for accentSurface
$bds-single-topic-desktop-height: 565px; // Fixed height from Figma design
// Spacing - Tablet (576px - 991px)
$bds-single-topic-tablet-py: 32px;
$bds-single-topic-tablet-content-gap: 32px; // Gap between image and content on tablet
$bds-single-topic-tablet-content-min-height: 320px; // Min height for content on tablet
$bds-single-topic-tablet-title-description-gap: 80px; // Gap between accent/title and description on tablet
// Spacing - Mobile (<576px)
$bds-single-topic-mobile-py: 24px;
$bds-single-topic-mobile-content-gap: 24px; // Gap between image and content on mobile
$bds-single-topic-mobile-content-min-height: 280px; // Min height for content on mobile
$bds-single-topic-mobile-title-description-gap: 40px; // Gap between accent/title and description on mobile
// =============================================================================
// Base Styles
// =============================================================================
.bds-feature-single-topic {
width: 100%;
background-color: $bds-single-topic-bg;
// Container - uses PageGrid with vertical padding
&__container {
padding-top: $bds-single-topic-mobile-py;
padding-bottom: $bds-single-topic-mobile-py;
@include media-breakpoint-up(md) {
padding-top: $bds-single-topic-tablet-py;
padding-bottom: $bds-single-topic-tablet-py;
}
@include media-breakpoint-up(lg) {
padding-top: $bds-single-topic-desktop-py;
padding-bottom: $bds-single-topic-desktop-py;
}
}
// Row - align items stretch so columns match height
// Use row-gap for spacing between image and content on mobile/tablet
&__row {
align-items: stretch;
row-gap: $bds-single-topic-mobile-content-gap;
@include media-breakpoint-up(md) {
row-gap: $bds-single-topic-tablet-content-gap;
}
@include media-breakpoint-up(lg) {
row-gap: 0;
// Fixed height from Figma design
height: $bds-single-topic-desktop-height;
}
}
// Media column
&__media-col {
@include media-breakpoint-up(lg) {
height: 100%;
}
}
// Content column - flex container with left padding on desktop
&__content-col {
display: flex;
flex-direction: column;
@include media-breakpoint-up(lg) {
padding-left: $bds-single-topic-desktop-content-pl;
height: 100%;
}
}
// Media container
&__media {
width: 100%;
overflow: hidden;
}
// Media image - responsive aspect ratios per Figma
&__media-img {
width: 100%;
object-fit: cover;
object-position: center;
// Mobile: 343/193 aspect ratio
aspect-ratio: 343 / 193;
@include media-breakpoint-up(md) {
// Tablet: 16/9 aspect ratio
aspect-ratio: 16 / 9;
}
@include media-breakpoint-up(lg) {
// Desktop: 701/561 aspect ratio (fills the 565px height)
aspect-ratio: 701 / 561;
height: $bds-single-topic-desktop-height;
}
}
// Content wrapper - uses space-between to push title to top, description/CTA to bottom
&__content {
display: flex;
flex-direction: column;
height: 100%;
// Gap between accent/title section and description section
gap: $bds-single-topic-mobile-title-description-gap; // 40px on mobile
// Min height on mobile to prevent squished content
min-height: $bds-single-topic-mobile-content-min-height;
justify-content: space-between;
@include media-breakpoint-up(md) {
gap: $bds-single-topic-tablet-title-description-gap; // 80px on tablet
min-height: $bds-single-topic-tablet-content-min-height;
}
@include media-breakpoint-up(lg) {
min-height: auto; // Desktop uses fixed height from row
gap: 0; // space-between handles the gap on desktop
}
}
// Title section - at the top
&__title-section {
flex-shrink: 0;
}
// Title - Heading MD from styles/_font.scss
// Font: Tobias (secondary/monospace), Size: 40px, Weight: 300, Line-height: 46px, Letter-spacing: -1px
&__title {
@include type(heading-md);
color: $bds-single-topic-title-color;
margin: 0;
}
// Description section - at the bottom, contains description + ButtonGroup
&__description-section {
display: flex;
flex-direction: column;
gap: $bds-single-topic-mobile-content-gap;
@include media-breakpoint-up(lg) {
gap: $bds-single-topic-desktop-description-gap;
}
}
// Description - Label L from styles/_font.scss
// Font: Booton (primary/sans-serif), Size: 16px, Weight: 300 (light), Line-height: 23.2px
&__description {
@include type(label-l);
color: $bds-single-topic-description-color;
margin: 0;
}
}
// =============================================================================
// Variant Modifiers
// =============================================================================
// Default variant - no background on title section
.bds-feature-single-topic--default {
.bds-feature-single-topic__title-section {
background-color: transparent;
padding: 0;
}
}
// AccentSurface variant - gray background on title section
.bds-feature-single-topic--accentSurface {
.bds-feature-single-topic__title-section {
background-color: $bds-single-topic-title-bg;
padding: $bds-single-topic-desktop-title-padding;
// Mobile min-height
min-height: 160px;
@include media-breakpoint-up(md) {
// Tablet min-height
min-height: 200px;
}
@include media-breakpoint-up(lg) {
// Desktop min-height
min-height: 200px;
}
}
}
// =============================================================================
// Dark Mode Theme Overrides
// =============================================================================
// Dark mode design tokens from Figma
$bds-single-topic-dark-bg: $black; // #141414 (Neutral/black)
$bds-single-topic-dark-title-bg: $gray-300; // #CAD4DF (Neutral/300default) for accentSurface variant
$bds-single-topic-dark-title-color: $black; // #141414 - title stays black on light background
$bds-single-topic-dark-description-color: $white; // #FFFFFF - description is white in dark mode
html.dark {
.bds-feature-single-topic {
background-color: $bds-single-topic-dark-bg;
&__title {
color: $white; // White title on dark background for default variant
}
&__description {
color: $bds-single-topic-dark-description-color;
}
}
// Default variant in dark mode - title is white on dark background
.bds-feature-single-topic--default {
.bds-feature-single-topic__title-section {
background-color: transparent;
}
.bds-feature-single-topic__title {
color: $white;
}
}
// AccentSurface variant in dark mode - title section has light background
.bds-feature-single-topic--accentSurface {
.bds-feature-single-topic__title-section {
background-color: $bds-single-topic-dark-title-bg;
}
// Title stays black on the light gray background
.bds-feature-single-topic__title {
color: $bds-single-topic-dark-title-color;
}
}
}

View File

@@ -0,0 +1,146 @@
import React from 'react';
import clsx from 'clsx';
import { PageGrid } from '../../components/PageGrid/page-grid';
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
export interface FeatureSingleTopicProps {
/** Background variant for the title section
* - 'default': No background on title section
* - 'accentSurface': Gray background (#E6EAF0) on title section
*/
variant?: 'default' | 'accentSurface';
/** Content arrangement - controls position of image relative to content
* - 'left': Image on left, content on right
* - 'right': Image on right, content on left
*/
orientation?: 'left' | 'right';
/** Feature title text (heading-md typography) */
title: string;
/** Feature description text (label-l typography) */
description?: string;
/** Array of links (1-5 links supported)
* - 1 link: renders as primary or secondary button (based on singleButtonVariant)
* - 2 links: renders as primary + tertiary buttons side by side
* - 3+ links: all tertiary buttons stacked
*/
buttons?: ButtonConfig[];
/** Button variant for single button configuration
* - 'primary': Primary button (default)
* - 'secondary': Secondary button
*/
singleButtonVariant?: 'primary' | 'secondary';
/** Feature media (image) configuration */
media: {
src: string;
alt: string;
};
/** Additional CSS classes */
className?: string;
}
/**
* FeatureSingleTopic Pattern
*
* A feature section pattern that pairs a title/description with a media element
* in a two-column layout. Supports two variants: default (no title background)
* and accentSurface (gray background on title section).
*
* Layout based on Figma 1280px design:
* - Desktop: Side-by-side with image 7 columns, content 5 columns
* - Mobile/Tablet: Stacked layout (full width)
*/
export const FeatureSingleTopic: React.FC<FeatureSingleTopicProps> = ({
variant = 'default',
orientation = 'left',
title,
description,
buttons = [],
singleButtonVariant = 'primary',
media,
className,
}) => {
// Validate buttons if provided (max 5 buttons supported)
const buttonValidation = validateButtonGroup(buttons, 5);
const hasButtons = buttonValidation.hasButtons;
// Button color is always green for this component
const buttonColor = 'green';
const forceColor = false;
// Build root class names
const rootClasses = clsx(
'bds-feature-single-topic',
`bds-feature-single-topic--${variant}`,
className
);
// Build row class names - column-reverse on mobile/tablet for both orientations
const rowClasses = clsx(
'bds-feature-single-topic__row',
'flex-column-reverse flex-lg-row' // Content above image on mobile, side-by-side on desktop
);
// Render content section (title at top, description/CTA at bottom)
const renderContent = () => (
<div className="bds-feature-single-topic__content">
<div className="bds-feature-single-topic__title-section">
<h2 className="bds-feature-single-topic__title">{title}</h2>
</div>
<div className="bds-feature-single-topic__description-section">
{description && (
<p className="bds-feature-single-topic__description">{description}</p>
)}
{hasButtons && (
<ButtonGroup
buttons={buttonValidation.buttons}
color={buttonColor}
forceColor={forceColor}
singleButtonVariant={singleButtonVariant}
/>
)}
</div>
</div>
);
// Render media section
const renderMedia = () => (
<div className="bds-feature-single-topic__media">
<img
src={media.src}
alt={media.alt}
className="bds-feature-single-topic__media-img"
/>
</div>
);
return (
<section className={rootClasses}>
<PageGrid className="bds-feature-single-topic__container" containerType="standard">
<PageGrid.Row className={rowClasses}>
<PageGrid.Col
span={{ base: 4, md: 8, lg: 7 }}
className={clsx(
'bds-feature-single-topic__media-col',
orientation === 'left' ? 'order-lg-1' : 'order-lg-2'
)}
>
{renderMedia()}
</PageGrid.Col>
<PageGrid.Col
span={{ base: 4, md: 8, lg: 5 }}
className={clsx(
'bds-feature-single-topic__content-col',
orientation === 'left' ? 'order-lg-2' : 'order-lg-1'
)}
>
{renderContent()}
</PageGrid.Col>
</PageGrid.Row>
</PageGrid>
</section>
);
};
export default FeatureSingleTopic;

View File

@@ -0,0 +1,185 @@
# FeatureSingleTopic Pattern
A feature section pattern that pairs a title/description with a media element in a two-column layout. Supports two variants (default, accentSurface) and two orientations (left, right).
## Features
- Responsive two-column layout (image + content) that stacks on smaller screens
- Two background variants: default (no background) and accentSurface (gray title background)
- Two orientations: left (image left) and right (image right)
- Flexible button layout supporting 1-5 links with automatic variant assignment
- Responsive image aspect ratios per Figma design
- Full dark mode support
- Uses PageGrid for consistent spacing
## Basic Usage
```tsx
import { FeatureSingleTopic } from 'shared/patterns/FeatureSingleTopic';
<FeatureSingleTopic
variant="default"
orientation="left"
title="Developer Spotlight"
description="Are you building a peer-to-peer payments solution?"
media={{
src: "/img/feature-image.png",
alt: "Feature image"
}}
links={[
{ label: "Get Started", href: "/start" },
{ label: "Learn More", href: "/learn" }
]}
/>
```
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `variant` | `'default' \| 'accentSurface'` | `'default'` | Background variant for title section |
| `orientation` | `'left' \| 'right'` | `'left'` | Image position relative to content |
| `title` | `string` | *required* | Feature title (heading-md typography) |
| `description` | `string` | - | Feature description (label-l typography) |
| `buttons` | `ButtonConfig[]` | `[]` | Array of button configurations (1-5 supported) |
| `singleButtonVariant` | `'primary' \| 'secondary'` | `'primary'` | Button variant for single button configuration |
| `media` | `{ src: string; alt: string }` | *required* | Image configuration |
| `className` | `string` | - | Additional CSS classes |
### ButtonConfig
```tsx
interface ButtonConfig {
label: string;
href?: string;
onClick?: () => void;
forceColor?: boolean;
}
```
**Note:** Button configurations are handled by the `ButtonGroup` component. See [ButtonGroup documentation](../ButtonGroup/README.md) for more details.
## Button Behavior
The component automatically determines button variants based on count:
| Count | Layout |
|-------|--------|
| 1 button | Primary or Secondary button (configurable via `singleButtonVariant` prop) |
| 2 buttons | Primary + Tertiary side by side |
| 3-5 buttons | All Tertiary buttons stacked |
**Note:** The component supports a maximum of 5 buttons. Additional buttons beyond 5 will trigger a validation warning in development mode and will be ignored. On mobile, the first two buttons (Primary + Tertiary) remain side by side.
## Variants
### Default
No background on the title section. Clean, minimal look.
```tsx
<FeatureSingleTopic variant="default" ... />
```
### AccentSurface
Gray background (#E6EAF0 light / #CAD4DF dark) on the title section.
```tsx
<FeatureSingleTopic variant="accentSurface" ... />
```
## Orientation
### Left (default)
Image on left, content on right on desktop.
### Right
Image on right, content on left on desktop.
**Note:** On mobile/tablet, content always appears above image regardless of orientation.
## Responsive Behavior
### Desktop (≥992px)
- Side-by-side layout: 7-column image, 5-column content
- Fixed height: 565px
- Image aspect ratio: 701/561
### Tablet (768px - 991px)
- Stacked layout with 32px gap between sections
- Image aspect ratio: 16/9
- Content min-height: 320px
### Mobile (<768px)
- Stacked layout with 24px gap between sections
- Image aspect ratio: 343/193
- Content min-height: 280px
## CSS Classes
```
.bds-feature-single-topic // Section container
.bds-feature-single-topic--default // Default variant modifier
.bds-feature-single-topic--accentSurface // AccentSurface variant modifier
.bds-feature-single-topic__container // PageGrid container
.bds-feature-single-topic__row // PageGrid row (uses flex-column-reverse flex-lg-row)
.bds-feature-single-topic__media-col // Media column (uses order-lg-1 or order-lg-2)
.bds-feature-single-topic__content-col // Content column (uses order-lg-1 or order-lg-2)
.bds-feature-single-topic__media // Media wrapper
.bds-feature-single-topic__media-img // Image element
.bds-feature-single-topic__content // Content wrapper
.bds-feature-single-topic__title-section // Title section
.bds-feature-single-topic__title // Title element
.bds-feature-single-topic__description-section // Description + buttons wrapper
.bds-feature-single-topic__description // Description element
```
**Note:**
- Orientation logic is handled via Bootstrap utility classes (`order-lg-1`, `order-lg-2`) applied dynamically in TSX
- Buttons are rendered by the `ButtonGroup` component with its own class structure
- Mobile/tablet layout uses `flex-column-reverse` to show content above image
- Desktop layout uses `flex-lg-row` for side-by-side display
## Typography Tokens
- **Title**: Uses `heading-md` type token (Tobias Light font)
- Desktop: 40px / 46px line-height / -1px letter-spacing
- **Description**: Uses `label-l` type token (Booton Light font)
- Desktop: 16px / 23.2px line-height
## Dark Mode
Full dark mode support with `html.dark` selector:
- **Section background**: #141414 (black)
- **Title (default variant)**: #FFFFFF (white)
- **Title (accentSurface)**: #141414 (black) on #CAD4DF background
- **Description**: #FFFFFF (white)
## Files
- `FeatureSingleTopic.tsx` - Main pattern component
- `FeatureSingleTopic.scss` - Styles with responsive breakpoints
- `index.ts` - Barrel exports
- `README.md` - This documentation
## Design References
- **Figma Design**: [Section Feature - Single Topic](https://www.figma.com/design/sg6T5EptbN0V2olfCSHzcx/Section-Feature---Single-Topic?node-id=18030-2250&m=dev)
- **Showcase Page**: `/about/feature-single-topic-showcase`
- **Component Location**: `shared/patterns/FeatureSingleTopic/`
## Related Components
- **Button**: Used for CTA buttons
- **PageGrid**: Used for responsive grid layout
## Version History
- **February 2026**: Initial implementation
- Two variants (default, accentSurface)
- Two orientations (left, right)
- Responsive image aspect ratios
- 1-5 link support with automatic button variant assignment
- Full dark mode support

View File

@@ -0,0 +1,3 @@
export { FeatureSingleTopic, type FeatureSingleTopicProps } from './FeatureSingleTopic';
export { default } from './FeatureSingleTopic';

View File

@@ -0,0 +1,216 @@
# FeatureTwoColumn Pattern
A feature section pattern that pairs editorial content with a media element in a two-column layout. Designed for showcasing features, products, or use cases with flexible button configurations based on the number of links.
## Overview
FeatureTwoColumn supports four color theme variants (neutral, lilac, yellow, green) and adapts responsively across desktop, tablet, and mobile breakpoints. The button rendering automatically adjusts based on the number of links provided.
## When to Use
- Highlighting a specific feature, product, or use case
- Presenting content with supporting visual media
- Creating visual variety with alternating left/right arrangements
- When 1-5 action links are needed with appropriate button hierarchy
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `color` | `'neutral' \| 'lilac' \| 'yellow' \| 'green'` | `'neutral'` | Background color theme variant |
| `arrange` | `'left' \| 'right'` | `'left'` | Controls whether content appears on the left or right side |
| `title` | `string` | *required* | Feature title text (heading-md typography) |
| `description` | `string` | *required* | Feature description text (body-l typography) |
| `links` | `FeatureTwoColumnLink[]` | *required* | Array of 1-5 links (see button behavior below) |
| `media` | `{ src: string; alt: string }` | *required* | Feature media (image) configuration |
| `className` | `string` | - | Additional CSS classes |
### FeatureTwoColumnLink
| Property | Type | Description |
|----------|------|-------------|
| `label` | `string` | Link label text |
| `href` | `string` | Link URL |
## Button Behavior
The component automatically renders buttons based on the number of links provided:
| Link Count | Button Configuration |
|------------|---------------------|
| 1 link | Secondary button |
| 2 links | Primary button + Tertiary button |
| 3-5 links | Primary + Tertiary (row), Secondary, then Tertiary links |
## Variants
### Color Themes
- **Neutral**: White background (dark mode: black)
- **Lilac**: Light purple background ($lilac-100, dark mode: $lilac-500)
- **Yellow**: Light yellow background ($yellow-100, dark mode: $yellow-500)
- **Green**: Light green background ($green-100, dark mode: $green-500)
### Arrangement
- **Left** (default): Content on the left, media on the right
- **Right**: Content on the right, media on the left
## Basic Usage
```tsx
import { FeatureTwoColumn } from '@/shared/patterns/FeatureTwoColumn';
function MyPage() {
return (
<FeatureTwoColumn
color="lilac"
arrange="left"
title="Institutions"
description="Banks, asset managers, PSPs, and fintechs use XRPL to build financial products and DeFi solutions efficiently and with more flexibility."
links={[
{ label: "Get Started", href: "/docs" },
{ label: "Learn More", href: "/about" }
]}
media={{ src: "/img/institutions.png", alt: "Institutions illustration" }}
/>
);
}
```
## Examples
### Single Link (Secondary Button)
```tsx
<FeatureTwoColumn
color="green"
arrange="right"
title="Developers"
description="Build powerful applications on XRPL with comprehensive documentation and tools."
links={[{ label: "View Documentation", href: "/docs" }]}
media={{ src: "/img/dev.png", alt: "Developer tools" }}
/>
```
### Two Links (Primary + Tertiary)
```tsx
<FeatureTwoColumn
color="yellow"
arrange="left"
title="Enterprise Solutions"
description="Scale your business with blockchain technology."
links={[
{ label: "Contact Sales", href: "/contact" },
{ label: "Learn More", href: "/enterprise" }
]}
media={{ src: "/img/enterprise.png", alt: "Enterprise" }}
/>
```
### Multiple Links (3-5 Tertiary)
```tsx
<FeatureTwoColumn
color="neutral"
arrange="left"
title="Explore XRPL"
description="Discover all the ways to interact with the XRP Ledger."
links={[
{ label: "Documentation", href: "/docs" },
{ label: "Tutorials", href: "/tutorials" },
{ label: "API Reference", href: "/api" },
{ label: "Community", href: "/community" },
{ label: "GitHub", href: "/github" }
]}
media={{ src: "/img/explore.png", alt: "Explore XRPL" }}
/>
```
## Responsive Behavior
### Desktop (≥992px)
- Side-by-side layout with content and media columns (6/12 columns each)
- Media uses 1:1 (square) aspect ratio
- Vertical padding: 96px
- Text gap (title to description): 16px
- CTA gap (between buttons): 25px
### Tablet (576px - 991px)
- Stacked layout (content above media, 8/8 columns = full width)
- Media uses 16:9 aspect ratio
- Vertical padding: 80px
- Text gap: 8px
- Content to CTA gap: 32px
- CTA gap: 16px
### Mobile (<576px)
- Stacked layout (content above media, 4/4 columns = full width)
- Media uses 1:1 aspect ratio
- Vertical padding: 64px
- Text gap: 8px
- Content to CTA gap: 24px
- CTA gap: 16px
## Anatomy
```
FeatureTwoColumn
├── PageGrid Container (responsive padding)
│ └── PageGrid.Row (flex layout)
│ ├── PageGrid.Col (content column, 6/12 on desktop)
│ │ └── Content
│ │ ├── TextGroup
│ │ │ ├── Title (h2, heading-md)
│ │ │ └── Description (p, body-l)
│ │ └── CTA (button configuration varies by link count)
│ │ ├── 1 link: Secondary Button
│ │ ├── 2 links: Primary + Tertiary Buttons
│ │ └── 3-5 links: Primary + Tertiary (row), Secondary, Tertiary list
│ └── PageGrid.Col (media column, 6/12 on desktop)
│ └── Media
│ └── Image (object-fit: cover)
```
## CSS Classes
| Class | Description |
|-------|-------------|
| `.bds-feature-two-column` | Root element |
| `.bds-feature-two-column--neutral` | Neutral color theme |
| `.bds-feature-two-column--lilac` | Lilac color theme |
| `.bds-feature-two-column--yellow` | Yellow color theme |
| `.bds-feature-two-column--green` | Green color theme |
| `.bds-feature-two-column--left` | Content left arrangement |
| `.bds-feature-two-column--right` | Content right arrangement |
| `.bds-feature-two-column__container` | PageGrid container |
| `.bds-feature-two-column__row` | PageGrid.Row wrapper |
| `.bds-feature-two-column__content-col` | Content column wrapper |
| `.bds-feature-two-column__content` | Content container |
| `.bds-feature-two-column__text-group` | Title + description container |
| `.bds-feature-two-column__title` | Title heading |
| `.bds-feature-two-column__description` | Description text |
| `.bds-feature-two-column__cta` | CTA buttons container |
| `.bds-feature-two-column__cta--single` | Single button variant |
| `.bds-feature-two-column__cta--double` | Two button variant |
| `.bds-feature-two-column__cta--multiple` | Multiple buttons variant |
| `.bds-feature-two-column__media-col` | Media column wrapper |
| `.bds-feature-two-column__media` | Media container |
| `.bds-feature-two-column__media-img` | Media image |
## Accessibility
- Uses semantic `<section>` element for the pattern container
- Title uses `<h2>` heading for proper document structure
- Media requires `alt` text for screen readers
- Buttons inherit accessible labels from the Button component
- Color contrast ratios meet WCAG 2.1 AA standards
## Design References
- **Figma Design**: [Pattern - Feature - Two Column](https://www.figma.com/design/3tmqxMrEvOVvpYhgOCxv2D/Pattern-Feature---Two-Column?node-id=20017-3501&m=dev)
- **Component Location**: `shared/patterns/FeatureTwoColumn/`
- **Color Tokens**: `styles/_colors.scss`
- **Typography**: `styles/_font.scss`

View File

@@ -0,0 +1,358 @@
// FeatureTwoColumn Pattern Styles
// =============================================================================
// A feature section pattern with two-column layout for content and media.
// Supports color themes (neutral, lilac, yellow, green) and arrangement variants.
// =============================================================================
// Design Tokens
// =============================================================================
// Color variants map - centralizes all variant configurations
// Structure: variant-name: (light-bg, dark-bg)
$bds-feature-variants: (
'neutral': (
light-bg: $gray-100, // #F0F3F7 (Neutral-100)
dark-bg: $gray-200 // #E6EAF0 (Neutral-200)
),
'lilac': (
light-bg: $lilac-200, // #D9CAFF (Primary-Lilac-200)
dark-bg: $lilac-200 // #D9CAFF (Primary-Lilac-200)
),
'yellow': (
light-bg: $yellow-100, // #F3F1EB (Secondary-Yellow-100)
dark-bg: $yellow-100 // #F3F1EB (Secondary-Yellow-100)
),
'green': (
light-bg: $green-300, // #21E46B (Primary-Green-300default)
dark-bg: $green-300 // #21E46B (Primary-Green-300default)
)
);
// Text colors - same for light and dark modes
// From styles/_colors.scss: $black = #141414 (Neutral-black)
$bds-feature-title-color: $black; // #141414 (Neutral-black)
$bds-feature-title-color-dark: $black; // #141414 (same in dark mode)
$bds-feature-description-color: $black; // #141414 (Neutral-black)
$bds-feature-description-color-dark: $black; // #141414 (same in dark mode)
// Spacing - Desktop (≥992px) - based on Figma 1280px design
$bds-feature-desktop-py: 96px;
$bds-feature-desktop-text-gap: 16px;
$bds-feature-desktop-cta-gap-col: 0; // Gap between button rows
$bds-feature-desktop-content-gap: 0; // Gap between text-group and cta (space-between handles this)
// Spacing - Tablet (576px - 991px) - based on Figma 768px design
$bds-feature-tablet-py: 80px;
$bds-feature-tablet-pl: 115px; // Left padding from Figma
$bds-feature-tablet-pr: 107px; // Right padding from Figma
$bds-feature-tablet-text-gap: 8px;
$bds-feature-tablet-cta-gap-row: 16px; // Gap between buttons in row from Figma
$bds-feature-tablet-cta-gap-col: 0;
$bds-feature-tablet-content-gap: 32px;
// Spacing - Mobile (<576px) - based on Figma 375px design
$bds-feature-mobile-py: 64px;
$bds-feature-mobile-px: 16px;
$bds-feature-mobile-text-gap: 8px;
$bds-feature-mobile-cta-gap: 16px; // Gap between stacked buttons from Figma
$bds-feature-mobile-content-gap: 24px;
// Grid gutter - consistent with PageGrid
$bds-grid-gutter: 8px;
// =============================================================================
// Base Styles
// =============================================================================
.bds-feature-two-column {
width: 100%;
// Extra large screens (>1280px): constrain component width and center
// Background color stays within 1280px, parent section background shows beyond
@include media-breakpoint-up(lg) {
max-width: 1280px;
margin: 0 auto;
}
// Desktop layout - hidden on mobile/tablet, shown on desktop
&__desktop-layout {
display: none;
@include media-breakpoint-up(lg) {
display: flex;
width: 100%;
align-items: stretch; // Both columns match height
}
}
// Mobile layout - shown on mobile/tablet, hidden on desktop
&__mobile-layout {
display: block;
@include media-breakpoint-up(lg) {
display: none;
}
}
// Container - uses PageGrid with wide variant (for mobile layout)
// Override all PageGrid padding - content columns have their own padding
&__container {
padding: 0 !important; // Override PageGrid default padding at all breakpoints
}
// Row - uses PageGrid.Row (for mobile layout)
&__row {
gap: 0 !important; // No gap between content and media sections, override PageGrid
}
// Content column wrapper
&__content-col {
display: flex;
flex-direction: column;
// Mobile: vertical padding, no horizontal padding (grid handles positioning)
padding: $bds-feature-mobile-py 0;
// Tablet: vertical padding, no horizontal padding (grid handles positioning)
@include media-breakpoint-up(md) {
padding: $bds-feature-tablet-py 0;
}
// Desktop: 50% width, vertical padding (grid handles horizontal positioning)
@include media-breakpoint-up(lg) {
width: 50%;
padding: $bds-feature-desktop-py 0;
// Match height of media column (which has aspect-ratio 1/1)
aspect-ratio: 1 / 1;
}
}
// Content grid - positions content within column using grid system
// Desktop: 6-column grid within the content half, content spans cols 2-5 (offset-1, span-4)
// Tablet: 8-column grid, content spans cols 2-7 (offset-1, span-6)
// Mobile: 4-column grid, content spans cols 1-4 (no offset, span-4)
&__content-grid {
display: grid;
width: 100%;
height: 100%;
// Mobile: 4 columns, content full width
grid-template-columns: repeat(4, 1fr);
gap: $bds-grid-gutter;
padding: 0 16px; // Mobile edge padding
// Tablet: 8 columns
@include media-breakpoint-up(md) {
grid-template-columns: repeat(8, 1fr);
padding: 0 24px; // Tablet edge padding
}
// Desktop: 6 columns (within the 50% content half)
@include media-breakpoint-up(lg) {
grid-template-columns: repeat(6, 1fr);
padding: 0 32px; // Desktop edge padding
}
}
// Content wrapper - positioned within the grid
&__content-wrapper {
// Mobile: span all 4 columns (cols 1-4)
grid-column: 1 / -1;
// Tablet: start at col 2, span 6 columns (cols 2-7)
@include media-breakpoint-up(md) {
grid-column: 2 / span 6;
}
// Desktop: start at col 2, span 4 columns (cols 2-5)
@include media-breakpoint-up(lg) {
grid-column: 2 / span 4;
}
}
// Media column wrapper - desktop layout (background image)
&__media-col {
display: flex;
flex-direction: column;
// Desktop: 50% width with background image, square aspect ratio
@include media-breakpoint-up(lg) {
width: 50%;
aspect-ratio: 1 / 1;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
}
// Mobile media column
&__media-col--mobile {
display: flex;
flex-direction: column;
}
// Content
&__content {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
height: 100%;
gap: $bds-feature-mobile-content-gap;
@include media-breakpoint-up(md) {
gap: $bds-feature-tablet-content-gap;
}
@include media-breakpoint-up(lg) {
gap: $bds-feature-desktop-content-gap;
}
}
// Content with multiple links
// Mobile: 24px gap between text-group and button-group
// Tablet: 32px gap between text-group and button-group
// Desktop: space-between with no gap (auto distribution between text-group and button-group)
&__content--multiple {
// Mobile: no gap since we only have 2 items (text-group and button-group)
// The 24px gap is handled via button-group margin or flex gap
gap: 24px;
justify-content: flex-start;
@include media-breakpoint-up(md) {
gap: 0;
}
@include media-breakpoint-up(lg) {
justify-content: space-between;
}
// Secondary button should not stretch - keep intrinsic width
>.bds-btn--secondary {
align-self: flex-start;
}
}
// Text group - title + description
&__text-group {
display: flex;
flex-direction: column;
gap: $bds-feature-mobile-text-gap;
@include media-breakpoint-up(md) {
gap: $bds-feature-tablet-text-gap;
}
@include media-breakpoint-up(lg) {
gap: $bds-feature-desktop-text-gap;
}
}
// Title - Heading MD from styles/_font.scss
// Font: Tobias (secondary), Size: 40px, Weight: 300, Line-height: 46px, Letter-spacing: -1px
&__title {
@include type(heading-md);
color: $bds-feature-title-color; // #141414 (Neutral-black) - same in light/dark
margin: 0;
}
// Description - Body L from styles/_font.scss
// Font: Booton (primary), Size: 18px, Weight: 300, Line-height: 26.1px, Letter-spacing: -0.5px
&__description {
@include type(body-l);
color: $bds-feature-description-color; // #141414 (Neutral-black) - same in light/dark
margin: 0;
}
// Media container
&__media {
width: 100%;
overflow: hidden;
// Mobile - 1:1 aspect ratio
aspect-ratio: 1 / 1;
// Tablet - 16:9 aspect ratio
@include media-breakpoint-up(md) {
aspect-ratio: 16 / 9;
}
// Desktop - 1:1 aspect ratio (square)
@include media-breakpoint-up(lg) {
aspect-ratio: 1 / 1;
}
}
// Media image
&__media-img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
}
// =============================================================================
// Color Theme Modifiers
// =============================================================================
// Generated from $bds-feature-variants map
@each $variant-name, $variant-colors in $bds-feature-variants {
.bds-feature-two-column--#{$variant-name} {
background-color: map-get($variant-colors, light-bg);
}
}
// =============================================================================
// Dark Mode Theme Overrides
// =============================================================================
html.dark {
.bds-feature-two-column {
&__title {
color: $bds-feature-title-color-dark;
}
&__description {
color: $bds-feature-description-color-dark;
}
}
// Generate dark mode styles for each variant
@each $variant-name, $variant-colors in $bds-feature-variants {
.bds-feature-two-column--#{$variant-name} {
background-color: map-get($variant-colors, dark-bg);
}
}
}
// =============================================================================
// Layout Modifiers (Arrange)
// =============================================================================
// Right arrangement - content on right, media on left (media first on mobile/tablet)
// Use flex-direction to swap columns on all screen sizes
.bds-feature-two-column--right {
// Mobile/Tablet layout - reverse the column order to show media first
.bds-feature-two-column__row {
flex-direction: column-reverse !important;
}
// Desktop layout - reverse the flex direction for side-by-side
.bds-feature-two-column__desktop-layout {
@include media-breakpoint-up(lg) {
flex-direction: row-reverse;
}
}
// Desktop content wrapper - mirror the grid positioning for right layout
// Content should start from col 2 and span 4 cols (same as left, grid handles positioning)
.bds-feature-two-column__content-wrapper {
@include media-breakpoint-up(lg) {
// Same positioning as left layout - grid alignment handles visual positioning
grid-column: 2 / span 4;
}
}
}

View File

@@ -0,0 +1,172 @@
import React from 'react';
import clsx from 'clsx';
import { PageGrid } from '../../components/PageGrid/page-grid';
import { ButtonGroup, ButtonConfig, validateButtonGroup } from '../ButtonGroup/ButtonGroup';
export interface FeatureTwoColumnLink {
/** Link label text */
label: string;
/** Link URL */
href: string;
}
export interface FeatureTwoColumnProps {
/** Color theme variant */
color?: 'neutral' | 'lilac' | 'yellow' | 'green';
/** Content arrangement - left places content on left side, right places content on right side */
arrange?: 'left' | 'right';
/** Feature title text (heading-md typography) */
title: string;
/** Feature description text (body-l typography) */
description: string;
/** Array of links (1-5 links supported)
* - 1 link: renders as secondary button
* - 2 links: renders as primary + tertiary buttons
* - 3-5 links: renders all as tertiary buttons
*/
links: FeatureTwoColumnLink[];
/** Feature media (image) configuration */
media: {
src: string;
alt: string;
};
/** Additional CSS classes */
className?: string;
}
/**
* FeatureTwoColumn Pattern
*
* A feature section pattern that pairs editorial content with a media element
* in a two-column layout. Designed for showcasing features, products, or use cases.
*
* Uses the PageGrid component system for responsive layout:
* - Mobile: Stacked layout (content above media)
* - Tablet: Stacked layout (content above media)
* - Desktop: Side-by-side (6/12 columns each)
*
* Button behavior based on link count:
* - 1 link: Secondary button
* - 2 links: Primary button (first) + Tertiary button (second)
* - 3-5 links: All tertiary buttons (first is filled, rest are text-only)
*/
export const FeatureTwoColumn: React.FC<FeatureTwoColumnProps> = ({
color = 'neutral',
arrange = 'left',
title,
description,
links = [],
media,
className,
}) => {
// Determine button color based on background
// Rule: Black buttons must be used for all backgrounds (including neutral)
const buttonColor = 'black';
const forceColor = true;
// Convert links to ButtonConfig format
const buttonConfigs: ButtonConfig[] = links.map(link => ({
label: link.label,
href: link.href,
forceColor: forceColor,
}));
// Validate buttons (FeatureTwoColumn supports 1-5 links per design spec)
const buttonValidation = validateButtonGroup(buttonConfigs, 5);
const hasButtons = buttonValidation.hasButtons;
// Build root class names
const rootClasses = clsx(
'bds-feature-two-column',
`bds-feature-two-column--${color}`,
`bds-feature-two-column--${arrange}`,
className
);
// Render content section with ButtonGroup
const renderContent = () => {
// Determine content class based on validated button count
const contentClass = clsx(
'bds-feature-two-column__content',
{
'bds-feature-two-column__content--multiple': hasButtons && buttonValidation.buttons.length >= 3,
}
);
return (
<div className={contentClass}>
<div className="bds-feature-two-column__text-group">
<h2 className="bds-feature-two-column__title">{title}</h2>
<p className="bds-feature-two-column__description">{description}</p>
</div>
{hasButtons && (
<ButtonGroup
buttons={buttonValidation.buttons}
color={buttonColor}
forceColor={forceColor}
singleButtonVariant="secondary"
/>
)}
</div>
);
};
// Render media section (for mobile/tablet stacked layout)
const renderMedia = () => (
<div className="bds-feature-two-column__media">
<img
src={media.src}
alt={media.alt}
className="bds-feature-two-column__media-img"
/>
</div>
);
return (
<section className={rootClasses}>
{/* Desktop layout - simple two-column flex with background image */}
<div className="bds-feature-two-column__desktop-layout">
<div className="bds-feature-two-column__content-col">
<div className="bds-feature-two-column__content-grid">
<div className="bds-feature-two-column__content-wrapper">
{renderContent()}
</div>
</div>
</div>
<div
className="bds-feature-two-column__media-col"
style={{ backgroundImage: `url(${media.src})` }}
role="img"
aria-label={media.alt}
/>
</div>
{/* Mobile/Tablet layout - stacked with PageGrid */}
<div className="bds-feature-two-column__mobile-layout">
<PageGrid className="bds-feature-two-column__container" containerType="wide">
<PageGrid.Row className="bds-feature-two-column__row">
<PageGrid.Col
span={{ base: 4, md: 8 }}
className="bds-feature-two-column__content-col"
>
<div className="bds-feature-two-column__content-grid">
<div className="bds-feature-two-column__content-wrapper">
{renderContent()}
</div>
</div>
</PageGrid.Col>
<PageGrid.Col
span={{ base: 4, md: 8 }}
className="bds-feature-two-column__media-col--mobile"
>
{renderMedia()}
</PageGrid.Col>
</PageGrid.Row>
</PageGrid>
</div>
</section>
);
};
export default FeatureTwoColumn;

View File

@@ -0,0 +1,3 @@
export { FeatureTwoColumn, type FeatureTwoColumnProps, type FeatureTwoColumnLink } from './FeatureTwoColumn';
export { default } from './FeatureTwoColumn';

View File

@@ -0,0 +1,122 @@
import React, { forwardRef, useCallback } from "react";
import clsx from "clsx";
import { PageGrid } from "shared/components/PageGrid/page-grid";
import { ButtonGroup, ButtonConfig, validateButtonGroup } from "shared/patterns/ButtonGroup/ButtonGroup";
import { isEmpty, isEnvironment } from "shared/utils";
import {
DesignConstrainedCallToActionsProps,
DesignConstrainedVideoProps,
} from "shared/utils/types";
export interface FeaturedVideoHeroProps
extends
React.ComponentPropsWithoutRef<"header">,
DesignConstrainedCallToActionsProps {
headline: React.ReactNode;
subtitle?: React.ReactNode;
videoElement: DesignConstrainedVideoProps;
}
const FeaturedVideoHero = forwardRef<HTMLElement, FeaturedVideoHeroProps>(
(props, ref) => {
const {
headline,
subtitle,
videoElement,
callsToAction,
className,
...rest
} = props;
const validateProps = useCallback<() => boolean>(() => {
const requiredProps = { headline, videoElement } as const;
let isValid = true;
for (const [key, value] of Object.entries(requiredProps)) {
if (isEmpty(value)) {
if (isEnvironment(["development", "test"])) {
console.warn(`${key} is required for FeaturedVideoHero`);
}
isValid = false;
}
}
return isValid;
}, [headline, videoElement]);
if (!validateProps()) {
return null;
}
// Convert callsToAction to ButtonConfig format for ButtonGroup
const buttonConfigs: ButtonConfig[] = (callsToAction ?? [])
.filter((cta) => !isEmpty(cta))
.map((cta) => ({
label: typeof cta?.children === 'string' ? cta.children : '',
href: cta?.href,
onClick: cta?.onClick,
forceColor: true,
}));
// Validate buttons (max 2 CTAs supported)
const buttonValidation = validateButtonGroup(
buttonConfigs,
2,
isEnvironment(["development", "test"]) // Only log warnings in dev/test
);
const hasCallsToAction = buttonValidation.hasButtons;
return (
<header
ref={ref}
className={clsx("bds-featured-video-hero", className)}
{...rest}
>
<PageGrid>
<PageGrid.Row>
<PageGrid.Col span={{ base: 4, md: 8, lg: 6 }}>
<div className="bds-featured-video-hero__content">
<h1 className="mb-0 h-md">
{headline}
</h1>
<div className="bds-featured-video-hero__bottom-group">
{subtitle && (
<PageGrid.Row className="bds-featured-video-hero__subtitle body-l">
<PageGrid.Col
span={{ base: "fill", md: 6, lg: 10 }}
className="bds-featured-video-hero__subtitle-col"
>
{subtitle}
</PageGrid.Col>
</PageGrid.Row>
)}
{hasCallsToAction && (
<ButtonGroup
buttons={buttonValidation.buttons}
color="green"
forceColor
gap="small"
/>
)}
</div>
</div>
</PageGrid.Col>
<PageGrid.Col
span={{ base: 4, md: 8, lg: 6 }}
>
<div className="bds-featured-video-hero__video-container">
<video
{...videoElement}
className="bds-featured-video-hero__video"
/>
</div>
</PageGrid.Col>
</PageGrid.Row>
</PageGrid>
</header>
);
},
);
FeaturedVideoHero.displayName = "FeaturedVideoHero";
export default FeaturedVideoHero;

View File

@@ -0,0 +1,162 @@
# FeaturedVideoHero Pattern
A page-level hero pattern featuring a headline, optional subtitle, call-to-action buttons, and a featured video. The video uses native HTML `<video>` props and is displayed in a responsive two-column layout with content on the left and video on the right.
## Overview
The FeaturedVideoHero component provides a structured hero section with:
- Responsive two-column layout (content left, video right) that stacks on smaller screens
- Required headline and video; optional subtitle and call-to-action buttons
- Design-constrained CTAs: primary and optional secondary, with variant and color set by the component
- Development-time validation: returns `null` when required props are missing and logs warnings in development/test
## Basic Usage
```tsx
import { FeaturedVideoHero } from "shared/patterns/FeaturedVideoHero";
function MyPage() {
return (
<FeaturedVideoHero
headline="Build on XRPL"
subtitle={
<p>
Issue, manage, and trade real-world assets without needing to build
smart contracts.
</p>
}
callsToAction={[{ children: "Get Started", href: "/docs" }]}
videoElement={{
src: "/video/intro.mp4",
autoPlay: true,
loop: true,
muted: true,
playsInline: true,
}}
/>
);
}
```
## Props
| Prop | Type | Required | Description |
| --------------- | --------------------------------- | -------- | ---------------------------------------------------------------------------- |
| `headline` | `React.ReactNode` | Yes | Hero headline text (h-md typography) |
| `subtitle` | `React.ReactNode` | No | Hero subtitle content |
| `callsToAction` | `DesignConstrainedCallsToActions` | No | Array with primary CTA and optional secondary CTA. Omit to hide CTA section. |
| `videoElement` | `DesignConstrainedVideoProps` | Yes | Native `<video>` element props (e.g. `src`, `autoPlay`, `loop`, `muted`) |
| `className` | `string` | No | Additional CSS classes for the header element |
| `...rest` | `HTMLHeaderElement` attributes | No | Any other HTML header attributes |
### Calls to Action
The `callsToAction` prop is optional. When provided, at least one non-empty CTA is required to show the CTA section. The component uses design-constrained Button props; `variant` and `color` are set automatically:
- **Primary CTA**: `variant="primary"`, `color="green"`, `forceColor={true}`
- **Secondary CTA**: `variant="tertiary"`, `color="green"`, `forceColor={true}`
All other Button props are supported (e.g., `children`, `href`, `onClick`). Do not pass `variant` or `color` in the CTA objects.
### Video Element
`videoElement` accepts native HTML video element props. Required and commonly used props:
- `src` (required) Video URL
- `autoPlay`, `loop`, `muted`, `playsInline` Typical for background/hero autoplay
- `controls`, `preload`, `poster` Optional; use for user-controlled playback
The video is rendered with `object-fit: cover` and a 16:9 aspect ratio container.
## Examples
### With primary and secondary CTAs
```tsx
<FeaturedVideoHero
headline="Real-world asset tokenization"
subtitle="Learn how to issue crypto tokens and build tokenization solutions."
callsToAction={[
{ children: "Get Started", href: "/docs" },
{ children: "Learn More", href: "/about" },
]}
videoElement={{
src: "/video/tokenization.mp4",
autoPlay: true,
loop: true,
muted: true,
playsInline: true,
}}
/>
```
### Without subtitle
```tsx
<FeaturedVideoHero
headline="Headline Only"
callsToAction={[{ children: "Get Started", href: "/docs" }]}
videoElement={{
src: "/video/intro.mp4",
autoPlay: true,
loop: true,
muted: true,
playsInline: true,
}}
/>
```
### With video controls
```tsx
<FeaturedVideoHero
headline="Watch and Learn"
subtitle="Explore our video tutorials and guides."
callsToAction={[{ children: "Watch Tutorials", href: "/tutorials" }]}
videoElement={{
src: "/video/intro.mp4",
autoPlay: false,
loop: true,
muted: true,
playsInline: true,
controls: true,
preload: "metadata",
}}
/>
```
## Validation
- **Required props**: `headline`, `videoElement`. If either is missing or empty, the component returns `null` and (in development/test) logs a console warning.
- **Optional props**: `subtitle`, `callsToAction`. Omit `callsToAction` or pass an array with no renderable CTAs to hide the CTA section.
## Responsive Behavior
- **Mobile / small screens**: Content and video stack vertically; video appears below the content block with top margin.
- **Large (lg+)**: Two-column layout: content (5 cols) on the left, video (6 cols, offset 1) on the right. Video container uses 16:9 aspect ratio and `object-fit: cover`.
## CSS Classes
- `bds-featured-video-hero` Root header element
- `bds-featured-video-hero__content` Content column (headline, subtitle, CTAs)
- `bds-featured-video-hero__title` Headline (`h1`)
- `bds-featured-video-hero__subtitle` Subtitle row
- `bds-featured-video-hero__subtitle-col` Subtitle column
- `bds-featured-video-hero__cta-buttons` CTA buttons wrapper
- `bds-featured-video-hero__video-container` Video wrapper (16:9)
- `bds-featured-video-hero__video` Video element
## Best Practices
1. **Video format**: Use MP4 with H.264 for broad compatibility; keep file sizes reasonable for fast loading.
2. **Autoplay**: Use `muted` and `playsInline` with `autoPlay` for reliable autoplay on mobile.
3. **CTAs**: Keep CTA text concise and action-oriented; primary CTA should be the main action.
4. **Headlines**: Keep headlines concise; use the subtitle for additional context.
5. **Accessibility**: Provide an `aria-label` (or other accessible name) on the video when it conveys meaningful content.
## Showcase
An interactive showcase with more examples and prop documentation is available at:
- **Showcase page**: `/about/featured-video-hero-showcase.page.tsx`

View File

@@ -0,0 +1,88 @@
.bds-featured-video-hero{
padding: 24px 0;
@include media-breakpoint-up(md) {
padding: 32px 0;
}
@include media-breakpoint-up(lg) {
padding: 40px 0;
}
@include bds-theme-mode(light) {
background-color: $white;
}
@include bds-theme-mode(dark) {
background-color: $black;
}
&__content {
display: flex;
flex-direction: column;
gap: 24px;
height: 100%;
@include media-breakpoint-up(md) {
gap: 32px;
}
@include media-breakpoint-up(lg) {
gap: 40px;
}
}
&__subtitle {
width: 100%;
margin: 0;
padding: 0;
}
&__video-container {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
overflow: hidden;
margin-top: 16px;
@include media-breakpoint-up(md) {
margin-top: 24px;
}
@include media-breakpoint-up(lg) {
margin-top: 0px;
}
}
&__video {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
display: inline-block;
}
&__bottom-group {
display: flex;
flex-direction: column;
align-items: flex-start;
flex-wrap: wrap;
gap: 24px;
margin-top: auto;
width: 100%;
@include media-breakpoint-up(md) {
flex-direction: row !important;
align-items: center;
gap: 32px;
}
@include media-breakpoint-up(lg) {
gap: 40px;
}
p:last-child {
margin-bottom: 0;
}
}
}

View File

@@ -0,0 +1,209 @@
import React, { forwardRef, memo, useEffect } from "react";
import clsx from "clsx";
import { PageGrid } from "shared/components/PageGrid/page-grid";
import { Button } from "shared/components/Button/Button";
import {
isEmpty,
DesignConstrainedButtonProps,
isEnvironment,
} from "shared/utils";
import {
DesignConstrainedImageProps,
DesignConstrainedVideoProps,
} from "shared/utils/types";
/**
* Image media type - extends native img element props
*/
type ImageMediaProps = {
type: "image";
} & DesignConstrainedImageProps;
/**
* Video media type - extends native video element props
*/
type VideoMediaProps = {
type: "video";
} & DesignConstrainedVideoProps;
/**
* Custom element media type - allows passing any React element
* The element will be wrapped in a container with the required aspect ratio
*/
type CustomMediaProps = {
type: "custom";
element: React.ReactElement;
};
/**
* Discriminated union of all supported media types.
* Each type allows extending native React element props while ensuring
* the media container maintains the 9:16 aspect ratio and object-fit: cover.
*/
export type HeaderHeroMedia =
| ImageMediaProps
| VideoMediaProps
| CustomMediaProps;
export interface HeaderHeroPrimaryMediaProps extends React.ComponentPropsWithoutRef<"header"> {
/** Hero title text (display-md typography) */
headline: React.ReactNode;
/** Hero subtitle text (subhead-sm-l typography) */
subtitle: React.ReactNode;
callsToAction: [DesignConstrainedButtonProps, DesignConstrainedButtonProps?];
/** Media element - supports image, video, or custom React element */
media: HeaderHeroMedia;
}
/**
* Renders the appropriate media element based on the media type.
* All media is wrapped in a container with 9:16 aspect ratio and object-fit: cover.
*/
const MediaRenderer: React.FC<{ media: HeaderHeroMedia }> = memo(
({ media }) => {
const mediaContainerClassName =
"bds-header-hero-primary-media__media-container";
const mediaElementClassName =
"bds-header-hero-primary-media__media-element";
switch (media.type) {
case "image": {
const { type, ...imgProps } = media;
return (
<div className={mediaContainerClassName}>
<img {...imgProps} className={mediaElementClassName} />
</div>
);
}
case "video": {
// alt here is being used as a aria label value
const { type, alt, ...videoProps } = media;
return (
<div className={mediaContainerClassName}>
<video
{...videoProps}
className={mediaElementClassName}
aria-label={alt}
/>
</div>
);
}
case "custom": {
const { element } = media;
return (
<div className={mediaContainerClassName}>
<div className={mediaElementClassName}>{element}</div>
</div>
);
}
default: {
return null;
}
}
},
);
const HeaderHeroPrimaryMedia = forwardRef<
HTMLElement,
HeaderHeroPrimaryMediaProps
>((props, ref) => {
const { headline, subtitle, callsToAction, media, className, ...restProps } =
props;
const [primaryCta, secondaryCta] = callsToAction;
// Headline is critical - exit early if missing
if (!headline) {
if (isEnvironment("development")) {
console.error("Headline is required for HeaderHeroPrimaryMedia");
}
return null;
}
// Validate other props and log warnings for missing optional/required fields
// Note: These props log warnings but don't prevent rendering
useEffect(() => {
if (!isEnvironment(["development", "test"])) {
return;
}
const propsToValidate = {
subtitle,
callsToAction,
media,
};
Object.entries(propsToValidate).forEach(([key, value]) => {
if (isEmpty(value)) {
console.warn(`${key} is required for HeaderHeroPrimaryMedia`);
}
});
}, [subtitle, callsToAction, media]);
return (
<header
className={clsx("bds-header-hero-primary-media", className)}
ref={ref}
{...restProps}
>
<PageGrid>
<PageGrid.Row>
<PageGrid.Col
span={{ base: 12, md: 6, lg: 5 }}
className="bds-header-hero-primary-media__headline-container"
>
<h1 className="bds-header-hero-primary-media__headline display-md">
<span>{headline}</span>
</h1>
</PageGrid.Col>
<PageGrid.Col offset={{ base: 0, lg: 1 }} span={{ base: 12, lg: 5 }}>
<div className="bds-header-hero-primary-media__cta-container">
{!isEmpty(subtitle) && (
<div className="bds-header-hero-primary-media__subtitle body-l">
{subtitle}
</div>
)}
{(!isEmpty(primaryCta) || !isEmpty(secondaryCta)) && (
<div className="bds-header-hero-primary-media__cta-buttons">
{!isEmpty(primaryCta) && (
<Button
{...primaryCta!}
variant="primary"
color="green"
showIcon={true}
/>
)}
{!isEmpty(secondaryCta) && (
<Button
{...secondaryCta!}
className={clsx(
"bds-header-hero-primary-media__cta-button-tertiary",
secondaryCta?.className,
)}
variant="tertiary"
color="green"
showIcon={true}
/>
)}
</div>
)}
</div>
</PageGrid.Col>
</PageGrid.Row>
{/* Media */}
{!isEmpty(media) && (
<PageGrid.Row>
<PageGrid.Col span={12}>
<MediaRenderer media={media} />
</PageGrid.Col>
</PageGrid.Row>
)}
</PageGrid>
</header>
);
});
export default HeaderHeroPrimaryMedia;

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,69 @@
// BDS LinkSmallGrid Pattern Styles
// Brand Design System - Link grid section pattern with heading and responsive tile grid
//
// Naming Convention: BEM with 'bds' namespace
// .bds-link-small-grid - Base section container
// .bds-link-small-grid--gray - Gray color variant
// .bds-link-small-grid--lilac - Lilac color variant
// .bds-link-small-grid__header - Header container (heading + description)
// .bds-link-small-grid__heading - Heading text
// .bds-link-small-grid__description - Description text
@import '../../../styles/breakpoints';
// =============================================================================
// Design Tokens
// =============================================================================
// Spacing tokens
$bds-link-small-grid-spacing-base: 24px; // Vertical section padding
$bds-link-small-grid-spacing-md: 32px; // Vertical section padding
$bds-link-small-grid-spacing-lg: 40px; // Vertical section padding
// Typography tokens (using existing typography classes)
// - Heading: h-md class (handled in component)
// - Description: body-l class (handled in component)
// =============================================================================
// Base Section Styles
// =============================================================================
.bds-link-small-grid {
// Section spacing
padding-top: $bds-link-small-grid-spacing-base;
padding-bottom: $bds-link-small-grid-spacing-base;
@include media-breakpoint-up(md) {
padding-top: $bds-link-small-grid-spacing-md;
padding-bottom: $bds-link-small-grid-spacing-md;
}
@include media-breakpoint-up(lg) {
padding-top: $bds-link-small-grid-spacing-lg;
padding-bottom: $bds-link-small-grid-spacing-lg;
}
// Background color - default to transparent
background-color: transparent;
}
// =============================================================================
// Header Section
// =============================================================================
.bds-link-small-grid__header {
gap: 8px;
margin-bottom: $bds-link-small-grid-spacing-base;
@include media-breakpoint-up(md) {
gap: 16px;
margin-bottom: $bds-link-small-grid-spacing-md;
}
@include media-breakpoint-up(lg) {
gap: 16px;
margin-bottom: $bds-link-small-grid-spacing-lg;
}
}

View File

@@ -0,0 +1,130 @@
import React, { useMemo } from 'react';
import clsx from 'clsx';
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
import { TileLink, TileLinkProps } from '../TileLinks/TileLink';
import { calculateTileOffset } from 'shared/utils/helpers';
export interface LinkItem extends Omit<TileLinkProps, 'variant'> {}
export interface LinkSmallGridProps {
/** Color variant - determines tile background color */
variant?: 'gray' | 'lilac';
/** Heading text (required) */
heading: string;
/** Optional description text */
description?: string;
/** Array of link items to display in the grid */
links: LinkItem[];
/** Additional CSS classes */
className?: string;
}
/**
* LinkSmallGrid Component
*
* A responsive grid section pattern for displaying navigational links using TileLink components.
* Features a heading, optional description, and a grid of clickable tiles with 2 color variants
* and full light/dark mode support.
*
* Grid Layout (12-column grid system):
* - Base (< 576px): 1 tile per row (each tile spans 4 of 4 columns = full width)
* - MD (576px - 991px): 2 tiles per row (each tile spans 4 of 8 columns = 50% width)
* - LG (≥ 992px): 4 tiles per row (each tile spans 3 of 12 columns = 25% width)
*
* Right-Alignment Logic (applied when < 10 total tiles):
* The first tile of each row gets an offset to right-align the grid at LG breakpoint only:
* - LG: 1 tile = offset 9, 2 tiles = offset 6, 3 tiles = offset 3, 4 tiles = offset 0
* - 10+ tiles: no offset (left-aligned grid)
* - MD and Base: no offset applied
*
* Each tile uses the TileLink component which features:
* - Window shade hover animation
* - Arrow icon with animation
* - Responsive sizing (64px height at all breakpoints)
* - Support for both links (href) and buttons (onClick)
* - Gray and Lilac color variants
*
* @example
* // Basic usage with gray variant
* <LinkSmallGrid
* variant="gray"
* heading="Quick Links"
* description="Navigate to key sections"
* links={[
* { label: "Documentation", href: "/docs" },
* { label: "Tutorials", href: "/tutorials" }
* ]}
* />
*
* @example
* // Lilac variant with click handlers
* <LinkSmallGrid
* variant="lilac"
* heading="Get Started"
* links={[
* { label: "Quick Start", onClick: () => navigate('/start') },
* { label: "Examples", href: "/examples" }
* ]}
* />
*/
export const LinkSmallGrid: React.FC<LinkSmallGridProps> = ({
variant = 'gray',
heading,
description,
links,
className,
}) => {
// Build class names using BEM with bds namespace
const classNames = clsx(
'bds-link-small-grid',
`bds-link-small-grid--${variant}`,
className
);
// Memoize offset calculations - only recalculate when links array changes
const linkOffsets = useMemo(() => {
const total = links.length;
return links.map((_, index) => calculateTileOffset(index, total));
}, [links]);
return (
<section className={classNames}>
<PageGrid>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 6, lg: 8 }}>
{/* Header Section */}
<div className="bds-link-small-grid__header">
<h2 className="bds-link-small-grid__heading h-md">{heading}</h2>
{description && (
<p className="body-l mb-0">{description}</p>
)}
</div>
</PageGridCol>
</PageGridRow>
<PageGridRow>
{links.map((link, index) => {
const offset = linkOffsets[index];
const hasOffset = offset.lg > 0;
// Use href or label as key, fallback to index
const key = link.href || link.label || index;
return (
<PageGridCol
key={key}
span={{ base: 4, md: 4, lg: 3 }}
offset={hasOffset ? { lg: offset.lg } : undefined}
>
<TileLink
variant={variant}
{...link}
/>
</PageGridCol>
);
})}
</PageGridRow>
</PageGrid>
</section>
);
};
export default LinkSmallGrid;

View File

@@ -0,0 +1,154 @@
# LinkSmallGrid Component
A responsive grid section pattern for displaying navigational links using TileLink components.
## Overview
LinkSmallGrid is a pattern component that combines a heading, optional description, and a grid of TileLink components. It provides a consistent layout for presenting multiple navigation options or quick links.
## Features
- **Responsive Grid Layout**: Adapts from 1 column (mobile) to 4 columns (desktop)
- **Two Color Variants**: Gray and Lilac (applied to all tiles)
- **Light/Dark Mode**: Full theming support
- **Right-Alignment**: Automatically right-aligns grids with fewer than 10 tiles at desktop
- **Flexible Content**: Supports both links and click handlers
- **Accessible**: Semantic HTML with proper heading hierarchy
## Grid Layout
Based on a 12-column grid system:
| Breakpoint | Tiles per Row | Tile Span | Total Columns |
|------------|---------------|-----------|---------------|
| Base (< 576px) | 1 | 4 of 4 | Full width |
| MD (576px - 991px) | 2 | 4 of 8 | 50% width each |
| LG (≥ 992px) | 4 | 3 of 12 | 25% width each |
## Right-Alignment Logic
When there are **fewer than 10 total tiles**, the grid is right-aligned at the **LG breakpoint only**:
- **1 tile**: offset 9 columns
- **2 tiles**: offset 6 columns
- **3 tiles**: offset 3 columns
- **4+ tiles**: no offset (fills row)
- **10+ tiles**: no offset (left-aligned grid)
**Note**: MD and Base breakpoints never apply offset (always left-aligned).
## Usage
### Basic Usage (Gray Variant)
```tsx
<LinkSmallGrid
variant="gray"
heading="Quick Links"
description="Navigate to key sections"
links={[
{ label: "Documentation", href: "/docs" },
{ label: "Tutorials", href: "/tutorials" },
{ label: "API Reference", href: "/api" },
{ label: "Examples", href: "/examples" }
]}
/>
```
### Lilac Variant with Click Handlers
```tsx
<LinkSmallGrid
variant="lilac"
heading="Get Started"
links={[
{ label: "Quick Start", onClick: () => navigate('/start') },
{ label: "Examples", href: "/examples" },
{ label: "Templates", href: "/templates" }
]}
/>
```
### Without Description
```tsx
<LinkSmallGrid
variant="gray"
heading="Resources"
links={[
{ label: "Blog", href: "/blog" },
{ label: "Community", href: "/community" }
]}
/>
```
## Props
### LinkSmallGridProps
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `variant` | `'gray' \| 'lilac'` | `'gray'` | Color variant for all tiles |
| `heading` | `string` | Required | Section heading |
| `description` | `string` | - | Optional description text |
| `links` | `LinkItem[]` | Required | Array of link items |
| `className` | `string` | - | Additional CSS classes |
### LinkItem (extends TileLinkProps)
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `label` | `string` | Required | Link text/label |
| `href` | `string` | - | Link destination |
| `onClick` | `() => void` | - | Click handler |
| `disabled` | `boolean` | `false` | Disabled state |
| `className` | `string` | - | Additional CSS classes |
**Note**: `variant` is controlled by the parent LinkSmallGrid component.
## Layout Structure
```
<section className="bds-link-small-grid">
<PageGrid>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 6, lg: 8 }}>
<header>
<h2>{heading}</h2>
<p>{description}</p>
</header>
</PageGridCol>
</PageGridRow>
<PageGridRow>
{links.map(link => (
<PageGridCol span={{ base: 4, md: 4, lg: 3 }} offset={...}>
<TileLink {...link} />
</PageGridCol>
))}
</PageGridRow>
</PageGrid>
</section>
```
## Performance
- **Memoized Offset Calculations**: Uses `useMemo` to avoid recalculating offsets on every render
- **Optimized Keys**: Uses `href` or `label` as React keys instead of array index for better reconciliation
## Files
- `LinkSmallGrid.tsx` - React component
- `LinkSmallGrid.scss` - Styles with BEM naming convention
- `README.md` - This file
## Related Components
- **TileLink**: Atomic component used for each tile in the grid
- **PageGrid/PageGridRow/PageGridCol**: Grid system components
- **calculateTileOffset**: Utility function for offset calculations (in `shared/utils/helpers.ts`)
## Design System
Part of the Brand Design System (BDS) with `bds-` namespace prefix.
## Showcase
See `about/link-small-grid-showcase.page.tsx` for examples with different link counts and variants.

View File

@@ -0,0 +1,2 @@
export { LinkSmallGrid, type LinkSmallGridProps, type LinkItem } from './LinkSmallGrid';

View File

@@ -0,0 +1,109 @@
// BDS LogoRectangleGrid Component Styles
// Brand Design System - Logo grid pattern with rectangle tiles
//
// Naming Convention: BEM with 'bds' namespace
// .bds-logo-rectangle-grid - Base component
// .bds-logo-rectangle-grid--gray - Gray variant (maps to TileLogo 'neutral')
// .bds-logo-rectangle-grid--green - Green variant (maps to TileLogo 'green')
// .bds-logo-rectangle-grid__header - Header section container
// .bds-logo-rectangle-grid__text - Text content container
//
// Note: Individual logo tiles are rendered using the TileLogo component with shape="rectangle"
// Note: Alignment is handled dynamically by PageGridCol offset prop
// =============================================================================
// Design Tokens
// =============================================================================
// Note: Color variants are now handled by the TileLogo component
// LogoRectangleGrid 'gray' maps to TileLogo 'neutral'
// LogoRectangleGrid 'green' maps to TileLogo 'green'
// Spacing tokens - responsive
// Mobile (<768px)
$bds-lrg-header-gap-mobile: 24px;
$bds-lrg-text-gap-mobile: 8px;
// Tablet (768px-1023px)
$bds-lrg-header-gap-tablet: 32px;
// Desktop (≥1024px)
$bds-lrg-header-gap-desktop: 40px;
$bds-lrg-text-gap-desktop: 16px;
// =============================================================================
// Base Component Styles
// =============================================================================
.bds-logo-rectangle-grid {
@extend .d-flex;
@extend .flex-column;
@extend .w-100;
// Mobile-first gap
gap: $bds-lrg-header-gap-mobile;
// Tablet breakpoint
@include media-breakpoint-up(md) {
gap: $bds-lrg-header-gap-tablet;
}
// Desktop breakpoint
@include media-breakpoint-up(lg) {
gap: $bds-lrg-header-gap-desktop;
}
}
// =============================================================================
// Header Section
// =============================================================================
.bds-logo-rectangle-grid__header {
@extend .d-flex;
@extend .flex-column;
margin-top: 24px;
margin-bottom: 24px;
// Mobile-first gap
gap: $bds-lrg-header-gap-mobile;
// Tablet breakpoint
@include media-breakpoint-up(md) {
gap: $bds-lrg-header-gap-tablet;
margin-top: 32px;
margin-bottom: 32px;
}
// Desktop breakpoint
@include media-breakpoint-up(lg) {
gap: $bds-lrg-header-gap-desktop;
margin-top: 40px;
margin-bottom: 40px;
}
}
// =============================================================================
// Text Content
// =============================================================================
.bds-logo-rectangle-grid__text {
@extend .d-flex;
@extend .flex-column;
// Mobile-first gap
gap: $bds-lrg-text-gap-mobile;
// Desktop breakpoint
@include media-breakpoint-up(lg) {
gap: $bds-lrg-text-gap-desktop;
}
}
// =============================================================================
// Logo Grid Row
// =============================================================================
// Note: Grid layout is now handled by PageGridRow/PageGridCol
// Each tile uses PageGridCol with span={{ base: 2, md: 2, lg: 3 }}
// This gives us 2 columns on mobile (2/4), 3 columns on tablet (2/6),
// and 4 columns on desktop (3/12)
// Alignment is handled by offset prop based on logo count
// Tile rendering and styling is handled by the TileLogo component

Some files were not shown because too many files have changed in this diff Show More