Compare commits

...

246 Commits

Author SHA1 Message Date
Calvin Jhunjhuwala
bdd0c24919 clean up, tighter code 2026-02-20 18:20:39 -08:00
Calvin Jhunjhuwala
14d1d76193 adding CardStatsList 2026-02-20 09:21:43 -08:00
Calvin Jhunjhuwala
cb0c06f404 adding showcase page 2026-02-18 17:31:12 -08:00
Calvin
a97a009d93 Add CardsIconGrid and CardsTextGrid Section Patterns (#3505)
* CardsIconGrid and CardsTextGrid + showcase pages

* cleaning up code

* more code clean up

* using the spacing tokens for values
2026-02-18 16:19:01 -08:00
Aria Keshmiri
df074e625b Merge pull request #3488 from XRPLF/feature/optimize
Feature/optimize
2026-02-17 14:30:46 -08:00
Calvin
a4b1925b31 [feat] Add LinkTextCard and LinkTextDirectory Components (#3501)
* adding linktextcard + linktextdirectory

* adding dark mode

* code review clean up

* code review comments
2026-02-17 12:21:10 -08:00
akcodez
26d1cf102c test 2026-02-13 10:50:27 -08:00
akcodez
a41a9e31cc fix optimization 2026-02-10 10:54:28 -08:00
akcodez
159ac52acc stashing work 2026-02-10 10:36:41 -08:00
Calvin
c7e01d322a Merge pull request #3485 from XRPLF/pattern/tile-link
Add TileLink Component and LinkSmallGrid Pattern
2026-02-09 15:03:21 -08:00
Calvin Jhunjhuwala
17582d543d updating comments in scss file 2026-02-09 14:58:48 -08:00
Calvin Jhunjhuwala
986ca23ff7 removing import for break points 2026-02-09 14:54:44 -08:00
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
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
Aria Keshmiri
9b05da7131 Merge pull request #3448 from XRPLF/pattern/cards-featured
Add CardsFeatured pattern component and styles
2026-01-20 10:12:57 -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
akcodez
daa8b7d292 add markdoc 2026-01-15 12:08:09 -08:00
akcodez
3873ae0085 design QA and changes with Design team 2026-01-15 11:23:24 -08:00
akcodez
f630796da0 53 ---> 52 2026-01-15 10:34:41 -08:00
akcodez
eac1859507 changes with design review 2026-01-15 10:31:35 -08:00
akcodez
2d75a0a727 Add CardsFeatured pattern component and styles
- Introduced the CardsFeatured component to showcase a grid of card images with headings and descriptions.
- Implemented responsive design for mobile, tablet, and desktop views.
- Added SCSS styles for light and dark mode support.
- Created a dedicated page for the CardsFeatured pattern showcase, including design specifications and examples.
2026-01-15 09:44:08 -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
akcodez
161e4305e6 add bun lock as build container is using bun 2026-01-14 10:30:58 -08:00
akcodez
8e7d7ecba1 resolve build erros 2026-01-14 10:27:07 -08:00
Aria Keshmiri
32f6a1de2d Merge pull request #3436 from XRPLF/go/feat/pattern/smallTiles
[feat][site] - Add SmallTilesSection component and styles
2026-01-14 10:22:42 -08:00
akcodez
e3459b336e merge main branch in rebundle css 2026-01-14 10:21:36 -08:00
Aria Keshmiri
1a1e1b30a6 Merge pull request #3445 from XRPLF/brand/merge-master-in
Brand/merge master in
2026-01-14 10:19:02 -08:00
akcodez
5eb98cacac merge master in, resolve conflicts 2026-01-14 10:06:48 -08:00
gabriel-ortiz
9a7f9479d4 Merge branch 'xrpl-brand-update-2026' into go/feat/pattern/smallTiles 2026-01-13 16:12:21 -08:00
Aria Keshmiri
c21785855f Merge pull request #3443 from XRPLF/brand-update-header
Brand update header
2026-01-13 13:19:10 -08:00
akcodez
887e1f38f5 Enhance Navbar and Submenu components for improved accessibility and keyboard navigation
- Added keyboard navigation support to NavItems, allowing users to close submenus with the Escape key and toggle with Enter/Space.
- Implemented onClose callback in Submenu components to facilitate submenu closure.
- Updated Navbar to handle submenu closing for keyboard navigation.
- Adjusted styles for better layout and responsiveness in submenus.
2026-01-13 12:30:51 -08:00
Aria Keshmiri
79294acb05 Merge pull request #3434 from XRPLF/pattern/header-hero-split-media
Pattern/header hero split media
2026-01-13 12:13:30 -08:00
akcodez
5cb06eaf86 merge xrpl branch in, resolve comments 2026-01-13 12:11:34 -08:00
Calvin Jhunjhuwala
fe0057aa9f css clean up 2026-01-13 12:07:29 -08:00
Calvin Jhunjhuwala
1e3ca30ace fixing per comments 2026-01-13 12:07:29 -08:00
Calvin Jhunjhuwala
66356984b4 added light image variant for example page 2026-01-13 12:07:29 -08:00
Calvin Jhunjhuwala
1fcc294ffb fixing button spacing + image background 2026-01-13 12:07:29 -08:00
Calvin Jhunjhuwala
0d0fc38344 cleaning up css 2026-01-13 12:07:28 -08:00
Calvin Jhunjhuwala
cd1759332d fixing errors 2026-01-13 12:07:28 -08:00
Calvin Jhunjhuwala
62d23ce36b updated and working showcase for call out media 2026-01-13 12:07:28 -08:00
Calvin Jhunjhuwala
678e168029 adding a README 2026-01-13 12:07:28 -08:00
akcodez
31a9cac20b merge main in, resolve comments 2026-01-13 12:06:52 -08:00
akcodez
79f40fb2c6 fix card 2026-01-13 12:05:49 -08:00
akcodez
9738402921 Focus should not activate button hover - cardimage 2026-01-13 12:05:49 -08:00
akcodez
e064ce02d0 Card - image : hovering card should not activate button hover state 2026-01-13 12:05:49 -08:00
akcodez
862a5c42d8 fix disasbled dark mode link color 2026-01-13 12:05:49 -08:00
akcodez
d29a5083d1 Refactor button typography for responsiveness and consistency
- Updated Button component styles to use responsive typography mixins for better adaptability across devices.
- Removed redundant media queries and adjusted font sizes and line heights for both button types.
- Ensured margin resets to maintain consistent spacing in various contexts.
- Enhanced CSS for improved readability and maintainability.
2026-01-13 12:05:49 -08:00
akcodez
60fc8eb22e Update CardImage and TileLogo components for improved responsiveness and layout
- Adjusted column spans in CardImage showcase to support a 4-column layout on larger screens.
- Updated TileLogo showcase to reflect changes in aspect ratios and responsive behavior, ensuring proper display across different screen sizes.
- Enhanced SCSS styles for both components to maintain consistency in appearance and functionality in light and dark themes.
- Refined documentation to clarify usage and responsive design principles for both CardImage and TileLogo components.
2026-01-13 12:05:49 -08:00
akcodez
ec4ef6e9fc Update HeaderHeroSplitMedia styles for aspect ratio and title alignment
- Changed tablet aspect ratio from 2:1 to 16:9 for better compatibility with an 8-column grid.
- Adjusted title alignment in the title-only layout from center to top for improved visual hierarchy.
2026-01-13 12:02:11 -08:00
akcodez
42552e4d24 add translations 2026-01-13 11:56:47 -08:00
akcodez
e46e4006d5 Refactor submenu styles for improved layout and spacing
- Updated .bds-submenu__section to use justify-content: space-between for better distribution of space between header and items.
- Removed gap property and set margin-top to 0 for .bds-submenu__tier2 to enhance visual alignment in Use Cases section.
2026-01-13 10:25:35 -08:00
akcodez
5cf22174dc Add LanguageDropdown component to Navbar and enhance LanguagePill functionality
- Introduced LanguageDropdown for improved language selection in Navbar and MobileMenu.
- Updated LanguagePill to display the current language dynamically and manage dropdown state.
- Enhanced styles for language-related components to support open/close states and transitions.
- Added mobile-specific dropdown positioning and styles for better user experience.
2026-01-13 10:20:37 -08:00
akcodez
7fd39abb2b Integrate search functionality into Navbar and MobileMenu components, utilizing Redocly's search dialog. Update AlertBanner to prevent hydration mismatch with null initial state for displayDate. Adjust NavControls and NetworkSubmenu to handle theme detection more effectively. 2026-01-13 10:05:02 -08:00
Calvin
d7e042bdb6 Merge pull request #3437 from XRPLF/layout/callout-media-banner
Layout/callout media banner
2026-01-13 09:40:31 -08:00
akcodez
9006dc3812 Refactor Navbar component structure by modularizing subcomponents and removing legacy exports for improved maintainability and clarity. 2026-01-13 09:33:25 -08:00
gabriel-ortiz
f3bef3784f Add theme mode mixin and update CardIcon focus styles
- Introduced a new SCSS mixin for theme mode that applies styles based on light or dark themes, with validation for mode parameters.
- Updated CardIcon styles to maintain background color on focus for both light and green themes, enhancing accessibility and user experience.
- Removed unnecessary import from SmallTilesSection styles to streamline the codebase.
2026-01-12 21:02:29 -08:00
Calvin Jhunjhuwala
b8286bf6b4 css clean up 2026-01-12 19:30:21 -08:00
Calvin Jhunjhuwala
3feb69a1da fixing per comments 2026-01-12 14:28:57 -08:00
akcodez
e94be3ca20 PERFECT THE ANIMATION 2026-01-12 13:01:19 -08:00
akcodez
4776c45c33 Enhance Navbar with XRP Ledger text animation and update styles for dark mode and submenu transitions 2026-01-12 12:55:28 -08:00
akcodez
1278b1aca9 add proper icons 2026-01-12 11:25:24 -08:00
akcodez
da529fd71e add arrow moving logic 2026-01-12 10:05:17 -08:00
akcodez
e467e27448 add dark mode styles 2026-01-12 09:59:32 -08:00
Calvin Jhunjhuwala
de84fa25a8 added light image variant for example page 2026-01-09 15:50:48 -08:00
Calvin Jhunjhuwala
b4ddfa7955 fixing button spacing + image background 2026-01-09 15:29:22 -08:00
akcodez
ce75b4388c minor qa changes 2026-01-09 12:58:07 -08:00
Calvin Jhunjhuwala
46add22436 cleaning up css 2026-01-08 16:41:47 -08:00
Calvin Jhunjhuwala
93ca38ed76 fixing errors 2026-01-08 15:24:04 -08:00
Calvin Jhunjhuwala
e0430b9899 updated and working showcase for call out media 2026-01-08 14:08:51 -08:00
akcodez
5df5f38e83 fix animation 2026-01-08 13:18:17 -08:00
akcodez
2b15495835 udpate animations 2026-01-08 13:14:36 -08:00
gabriel-ortiz
ab366c79ef Add warning for empty cards in SmallTilesSection
- Implemented a console warning in the SmallTilesSection component to notify when no cards are provided, improving debugging and user feedback.
2026-01-08 11:29:01 -08:00
akcodez
1074670da7 fix nav heights 2026-01-08 11:27:42 -08:00
akcodez
1a2cb105f3 add all submenus 2026-01-08 11:23:41 -08:00
gabriel-ortiz
3badef78c1 Add SmallTilesSection component and styles
- Introduced the SmallTilesSection component for displaying multiple CardIcon components in a responsive grid layout.
- Implemented SCSS styles for the SmallTilesSection, including responsive grid behavior and spacing adjustments.
- Added documentation for the SmallTilesSection component, detailing usage, props, and examples.
- Updated main stylesheet to include SmallTilesSection styles.
2026-01-08 10:09:48 -08:00
Calvin Jhunjhuwala
a1f4c82e3a adding a README 2026-01-07 16:30:43 -08:00
Calvin Jhunjhuwala
478f5784ee updating callout media banner, styling for extra width 2026-01-07 16:28:21 -08:00
akcodez
471bf7f193 add basis for navbar 2026-01-07 13:48:30 -08:00
akcodez
f346a80ce0 Refactor HeaderHeroSplitMedia styles for tablet and accent surfaces
- Updated spacing variables for tablet view to enhance layout consistency.
- Introduced accent-specific spacing for title and description groups on tablet and desktop.
- Adjusted margin and gap values for improved responsiveness and visual hierarchy.
2026-01-06 13:28:20 -08:00
akcodez
e849cc95b9 Update HeaderHeroSplitMedia styles for improved color consistency and dark mode support
- Refactored color variables to use design tokens for better maintainability.
- Ensured consistent text colors for titles and descriptions in both light and dark modes.
- Adjusted color values for better contrast and readability across themes.
2026-01-06 13:20:09 -08:00
akcodez
0bff1aab4c Add HeaderHeroSplitMedia styles and adjust typography margins
- Introduced new styles for the HeaderHeroSplitMedia component, including layout, typography, and responsive design adjustments.
- Updated margin-bottom for paragraph and label-r elements to enhance spacing consistency.
2026-01-06 13:15:12 -08:00
Aria Keshmiri
e6aa704841 Merge pull request #3431 from XRPLF/design-qa-review
Design qa review
2026-01-06 10:43:41 -08:00
akcodez
9415ae085a fix card 2026-01-05 13:10:51 -08:00
akcodez
4cb232a068 Focus should not activate button hover - cardimage 2026-01-05 13:01:02 -08:00
akcodez
35958ebede Card - image : hovering card should not activate button hover state 2026-01-05 12:59:59 -08:00
akcodez
688ac5dc91 fix disasbled dark mode link color 2026-01-05 12:54:05 -08:00
Aria Keshmiri
e298a45902 Merge pull request #3430 from XRPLF/fix-typography-010526
fixing typography for label-r
2026-01-05 12:53:31 -08:00
akcodez
176e187c6a Refactor button typography for responsiveness and consistency
- Updated Button component styles to use responsive typography mixins for better adaptability across devices.
- Removed redundant media queries and adjusted font sizes and line heights for both button types.
- Ensured margin resets to maintain consistent spacing in various contexts.
- Enhanced CSS for improved readability and maintainability.
2026-01-05 12:44:14 -08:00
Calvin Jhunjhuwala
15046f431e fixing typography for label-r 2026-01-05 12:44:11 -08:00
akcodez
ecd4a1bb66 Merge remote-tracking branch 'origin/xrpl-brand-update-2026' into design-qa-review 2026-01-05 12:22:06 -08:00
Calvin
3fbed79209 Merge pull request #3421 from XRPLF/fix-bds-session-121125
Fix typography
2026-01-05 12:07:55 -08:00
Aria Keshmiri
fc472a4f77 Merge pull request #3417 from XRPLF/link-rename
rename link
2026-01-05 11:55:15 -08:00
Aria Keshmiri
bebc019daa Merge pull request #3420 from XRPLF/component/button-href-fix
Add link button functionality and styles
2026-01-05 11:55:04 -08:00
Calvin Jhunjhuwala
b3f235ded6 adding typography showcase page 2026-01-05 11:33:11 -08:00
akcodez
7b223aafc2 Update CardImage and TileLogo components for improved responsiveness and layout
- Adjusted column spans in CardImage showcase to support a 4-column layout on larger screens.
- Updated TileLogo showcase to reflect changes in aspect ratios and responsive behavior, ensuring proper display across different screen sizes.
- Enhanced SCSS styles for both components to maintain consistency in appearance and functionality in light and dark themes.
- Refined documentation to clarify usage and responsive design principles for both CardImage and TileLogo components.
2026-01-05 10:12:11 -08:00
Calvin Jhunjhuwala
ffe0eff61a lg + xl separate for the display-lg font class 2025-12-11 15:29:01 -08:00
Calvin Jhunjhuwala
be0e324d0b working font styles and updated card grid 2025-12-11 15:01:08 -08:00
akcodez
fadfde1775 Add link button functionality and styles
- Introduced link button support in the Button component, allowing buttons to render as anchor elements when an `href` prop is provided.
- Updated Button showcase to demonstrate link buttons for internal and external navigation.
- Enhanced Button SCSS styles to ensure proper appearance and behavior for link buttons in both light and dark themes.
- Added documentation for link button usage in the Button showcase.
2025-12-11 11:23:23 -08:00
akcodez
32899e9c41 rename link 2025-12-10 13:53:21 -08:00
Aria Keshmiri
15f48991c3 Merge pull request #3415 from XRPLF/component/card-image
Add CardImage component with showcase and documentation
2025-12-10 12:45:37 -08:00
akcodez
642c0dd2ce add bottom padding 2025-12-10 12:45:04 -08:00
akcodez
a08b24ed5d Enhance CardImage showcase with image scaling demo and responsive layout adjustments
- Added a new image scaling demonstration to the CardImage showcase, illustrating a zoom effect on hover.
- Updated layout of CardImage components to improve responsiveness, adjusting column spans for various screen sizes.
- Introduced a new image from Figma for the scaling demo and included detailed descriptions of the scaling behavior.
- Refined SCSS styles to support the new layout and ensure consistent spacing across different screen sizes.
2025-12-10 10:35:14 -08:00
Aria Keshmiri
74e8be5a13 Merge pull request #3413 from XRPLF/component/card-icon
Add CardIcon component with showcase and documentation
2025-12-10 09:37:01 -08:00
Calvin
7895d6dee9 Merge pull request #3410 from XRPLF/component/card-stat
[feat][site] Component/Card-Stat
2025-12-09 12:33:19 -08:00
akcodez
8e6d8f7c30 Add CardImage component with showcase and documentation
- Introduced the CardImage component, a responsive card designed to display an image, title, subtitle, and call-to-action button.
- Implemented three responsive size variants (LG, MD, SM) that adapt to viewport width.
- Added interactive states including default, hover, focus, pressed, and disabled.
- Created a comprehensive showcase page demonstrating all variants and states of the CardImage component.
- Included detailed documentation covering usage examples, props API, design tokens, and accessibility features.
- Added SCSS styles for the CardImage component, ensuring compatibility with both light and dark themes.
2025-12-09 12:29:03 -08:00
Calvin Jhunjhuwala
36785bc0f1 fixing css conflict 2025-12-09 12:28:14 -08:00
Calvin Jhunjhuwala
977c37ef83 minor copy change, pushiong fixed color 2025-12-09 12:00:11 -08:00
Calvin Jhunjhuwala
52d895b1d0 edits for dark mode card 2025-12-09 11:56:34 -08:00
akcodez
230ddcbe21 Add CardIcon component with showcase and documentation
- Introduced the CardIcon component, a clickable card featuring an icon and label text, supporting two color variants (Neutral and Green) and five interaction states (Default, Hover, Focused, Pressed, Disabled).
- Implemented responsive sizing that adapts at breakpoints (SM, MD, LG).
- Created a comprehensive showcase page demonstrating all variants, states, and usage examples.
- Added detailed documentation covering usage guidelines, best practices, and API reference.
- Included SCSS styles for the CardIcon component, ensuring compatibility with both light and dark themes.
2025-12-09 10:37:40 -08:00
Aria Keshmiri
1ee5828747 Merge pull request #3406 from XRPLF/component/card-offgrid
Add CardOffgrid component with showcase and documentation
2025-12-09 10:13:16 -08:00
Aria Keshmiri
314980a667 Merge pull request #3409 from XRPLF/component/tile-logo
Add TileLogo component with showcase and documentation
2025-12-09 10:13:06 -08:00
akcodez
2783d90cf6 Refactor CardOffgrid typography styles to utilize predefined classes
- Updated CardOffgrid component to use .subhead-lg-l and .body-l classes for title and description typography, respectively.
- Removed custom font size and line height definitions from SCSS and CSS files for improved consistency and maintainability.
- Adjusted related styles to ensure compatibility with the new typography approach.
2025-12-09 06:15:22 -08:00
akcodez
5fbdbb8d42 Refactor TileLogo showcase layout to use PageGrid components
- Replaced div-based layout with PageGridRow and PageGridCol components for better responsiveness and alignment.
- Updated all instances of logo displays within the TileLogo showcase to utilize the new grid structure.
- Ensured consistent spacing and alignment across different logo variants and states.
2025-12-09 06:05:54 -08:00
Calvin Jhunjhuwala
b7ba976fb2 consolidating classes, still need superscript specs 2025-12-08 21:06:56 -08:00
Calvin Jhunjhuwala
a6eb9e63e5 complete showcase pagfe 2025-12-08 15:30:42 -08:00
Calvin Jhunjhuwala
7d694c76a5 working card stat, finalizing sup, cleaning up code cleanliness 2025-12-08 13:58:57 -08:00
akcodez
cbc56937e6 Refactor TileLogo component styles and class naming to align with BDS conventions
- Updated SCSS styles to replace 'tile-logo' namespace with 'bds-tile-logo' for consistency with the Brand Design System.
- Adjusted class names in the TileLogo component to reflect the new naming convention.
- Ensured all styles and variants (square, rectangle, neutral, green) are updated accordingly for both light and dark themes.
2025-12-08 10:57:02 -08:00
akcodez
1a9fa9b970 Add TileLogo component with showcase and documentation
- Introduced the TileLogo component, designed for displaying brand logos with interactive states.
- Implemented two shape variants (Square and Rectangle) and two color variants (Neutral and Green), with responsive sizing.
- Created a comprehensive showcase page demonstrating all variants, states, and usage examples.
- Added detailed documentation covering usage guidelines, best practices, and API reference.
- Included SCSS styles for the TileLogo component, ensuring compatibility with both light and dark themes.
2025-12-08 10:52:37 -08:00
akcodez
cd82ea5484 rm link 2025-12-08 10:13:05 -08:00
akcodez
57898ab010 Add CardOffgrid component with showcase and documentation
- Introduced the CardOffgrid component, designed for displaying feature highlights with customizable icons, titles, and descriptions.
- Implemented two color variants: neutral and green, with interactive states and a unique hover animation.
- Created a comprehensive showcase page demonstrating all variants and states of the CardOffgrid component.
- Added detailed documentation covering usage guidelines, best practices, and API reference.
- Included SCSS styles for the CardOffgrid component, ensuring compatibility with both light and dark themes.
2025-12-05 11:41:18 -08:00
Aria Keshmiri
2dbb111943 Merge pull request #3405 from XRPLF/component/divider
Add Divider component and documentation
2025-12-04 15:34:04 -08:00
akcodez
cb6323d153 Update Divider component to replace 'black' color variant with 'base' for improved theme adaptability
- Changed all instances of 'black' to 'base' in the Divider component and its documentation to reflect the new high-contrast color that adapts to light and dark themes.
- Updated showcase page and styles to ensure consistency with the new color naming convention.
2025-12-04 15:33:32 -08:00
akcodez
08941588aa fix colors 2025-12-04 13:51:04 -08:00
akcodez
e183369ef6 add height 2025-12-04 13:48:23 -08:00
akcodez
702e180de6 Add Divider component and documentation
- Introduced the Divider component following the XRPL Brand Design System, supporting horizontal and vertical orientations, three stroke weights (thin, regular, strong), and three color variants (gray, black, green).
- Created a comprehensive showcase page for the Divider component, demonstrating its usage and variations.
- Added detailed documentation for the Divider component, including props API, usage examples, and best practices.
- Included styles for the Divider component in SCSS format, ensuring compatibility with light and dark themes.
2025-12-04 12:20:18 -08:00
Calvin
a265f82980 Merge pull request #3404 from XRPLF/grid-optimization
[fix] Grid optimization
2025-12-03 16:17:54 -08:00
Calvin Jhunjhuwala
021899906d removing unnecessary npm package 2025-12-03 16:14:09 -08:00
Calvin Jhunjhuwala
f022c48f6c changing showcase file 2025-12-03 16:06:59 -08:00
Calvin Jhunjhuwala
3e07b8400d removing extra css 2025-12-03 16:02:08 -08:00
Calvin Jhunjhuwala
5433894f20 renaming class name to bds 2025-12-03 14:51:59 -08:00
Calvin Jhunjhuwala
a6d84de417 working grid for fill and auto 2025-12-03 13:34:05 -08:00
Calvin Jhunjhuwala
6f76d4ece5 clean up of grid, working on all screen sizes, logic built out 2025-12-03 13:09:39 -08:00
Aria Keshmiri
17778ad84b Merge pull request #3399 from XRPLF/component/button
Component/button
2025-12-03 10:58:04 -08:00
Aria Keshmiri
518585227d Merge pull request #3401 from XRPLF/component/link
Add BDS link styles and update existing link styles
2025-12-03 10:57:25 -08:00
akcodez
ad0631f701 add arialbel 2025-12-03 10:56:43 -08:00
akcodez
01c19628a9 fix link spacing 2025-12-02 15:27:52 -08:00
akcodez
44614dba9d Refactor Button Styles for Responsive Design
Updated button styles to enhance responsiveness across devices by replacing fixed breakpoints with a mixin for the xl breakpoint. Adjusted typography and padding for smaller screens to ensure consistent appearance and usability. Improved comments for clarity on the import order and design tokens.
2025-12-02 12:40:27 -08:00
akcodez
621db81c7d add proper breakpoint sizing 2025-12-02 12:23:35 -08:00
akcodez
c01749eba2 rm deprecated linkCOlor 2025-12-02 11:52:23 -08:00
akcodez
2ff14e4224 Add BDS link styles and update existing link styles
- Introduced new styles for BDS link icons, including hover and focus states.
- Updated existing link styles to exclude BDS links from certain color and hover effects.
- Ensured consistent styling across light and dark themes for BDS links.
- Refactored landing page link styles to accommodate new BDS link classes.
2025-12-02 10:21:52 -08:00
akcodez
7685c2eb1e Refactor Tertiary Button Styles for Dark Mode
Updated the styles for the tertiary button in dark mode, including hover, focus, active, and disabled states. Adjusted padding and added text decoration for better visual feedback. Enhanced responsiveness for smaller screens to ensure consistent behavior across devices.
2025-12-01 14:27:02 -08:00
akcodez
2de2bac211 Enhance dark mode styles for primary and secondary buttons, adding detailed states for hover, focus, active, and disabled conditions. Update CSS to reflect new design specifications for secondary button in dark mode. 2025-12-01 14:21:14 -08:00
akcodez
bdc69f047a add primary dark mode 2025-12-01 14:17:25 -08:00
akcodez
f20177b5f9 Add Tertiary Button Showcase Component
Introduced a new ButtonShowcaseTertiary component to demonstrate the BDS Tertiary Button, including its various states and usage examples. Removed the previous ButtonShowcase component to streamline the showcase and focus on the Tertiary variant. Updated Button component to support the new variant and adjusted related styles in the CSS for both green and black themes.
2025-12-01 14:07:29 -08:00
akcodez
73b2127f87 add proper focus and active / hover states 2025-12-01 13:45:21 -08:00
akcodez
32b309c878 merge main brand branch in 2025-12-01 12:03:09 -08:00
akcodez
9cf1b07954 add full primary button 2025-12-01 12:01:50 -08:00
Calvin
5b73ccb8be Merge pull request #3393 from XRPLF/bds-font-update
[fix][fonts] Clean up of fonts, folder re-structuring
2025-12-01 11:01:58 -08:00
Calvin Jhunjhuwala
c4188c47d6 updating css for medium 2025-11-26 16:06:00 -08:00
Calvin Jhunjhuwala
2429574182 removing px reversion for medium 2025-11-26 16:03:51 -08:00
Calvin Jhunjhuwala
42ec50df27 removing fake button folder 2025-11-26 16:00:14 -08:00
Calvin Jhunjhuwala
37e96a9dae more documentation around page grid 2025-11-25 21:36:38 -08:00
Calvin Jhunjhuwala
f3ae760c40 move page grid css to page grid folder, removing more text class styling 2025-11-25 21:30:25 -08:00
Calvin Jhunjhuwala
97c302822a adding folder structure, cleaning up font, add readme for page grid 2025-11-25 17:29:58 -08:00
Calvin Jhunjhuwala
e92929e148 removing old background images, updating font classes 2025-11-13 12:08:33 -08:00
Calvin Jhunjhuwala
9d3d11800a adding page grid component + stylersheet 2025-11-03 10:42:35 -08:00
Calvin Jhunjhuwala
a956d5ae78 removing empty card footer 2025-10-30 10:42:30 -07:00
Calvin Jhunjhuwala
52e070dcf6 removing background from css as well 2025-10-30 10:38:53 -07:00
Calvin Jhunjhuwala
605eb70aed removed all old fonts, remove all old backgrounds 2025-10-30 10:33:42 -07:00
akcodez
0c2a1bc249 Refactor color migration strategy to a clean implementation, removing backward compatibility aliases and consolidating color scales. Update SCSS files to reflect new design tokens and ensure all references are migrated. Adjust event card layout in community events page for improved responsiveness. Modify CSS styles for better alignment with updated color palette and remove unnecessary padding in card components. 2025-10-21 14:04:55 -07:00
akcodez
51e763b967 Enhance PostCSS configuration with expanded safelist for Bootstrap utility classes, update layout in about and resources pages, and refine color palette in CSS files for improved design consistency. Adjusted card styles and link behaviors to align with new design tokens. 2025-10-21 13:42:07 -07:00
akcodez
86998c82d6 Implement CSS optimization pipeline with PurgeCSS, Autoprefixer, and cssnano; reduce bundle size by 42% (uncompressed) and 39% (gzipped). Add analysis tool for CSS metrics and update build scripts in package.json. Create comprehensive documentation for CSS optimization process. 2025-10-21 10:06:53 -07:00
akcodez
c2287a7fe6 run css compiler 2025-10-21 09:23:47 -07:00
akcodez
f09ab44280 upgrade bootstrap, add new fonts 2025-10-21 09:22:02 -07:00
Calvin Jhunjhuwala
08807db2e9 ran css 2025-10-21 09:17:17 -07:00
Calvin Jhunjhuwala
201479ced6 new fonts added working across website 2025-10-21 09:09:56 -07:00
Calvin Jhunjhuwala
ce49c8b6ba updating bootstrap to v5 2025-10-17 16:28:07 -07:00
380 changed files with 50470 additions and 3762 deletions

2
.gitignore vendored
View File

@@ -8,6 +8,8 @@ yarn-error.log
*.iml
.venv/
_code-samples/*/js/package-lock.json
*.css.map
# PHP
composer.lock
.cursor/

View File

@@ -1,449 +1,152 @@
import * as React from "react";
import { useThemeConfig, useThemeHooks } from "@redocly/theme/core/hooks";
import { LanguagePicker } from "@redocly/theme/components/LanguagePicker/LanguagePicker";
import { slugify } from "../../helpers";
import { Link } from "@redocly/theme/components/Link/Link";
import { ColorModeSwitcher } from "@redocly/theme/components/ColorModeSwitcher/ColorModeSwitcher";
import { Search } from "@redocly/theme/components/Search/Search";
import arrowUpRight from "../../../static/img/icons/arrow-up-right-custom.svg";
import moment from "moment-timezone";
import { useSearchDialog } from "@redocly/theme/core/hooks";
import { SearchDialog } from "@redocly/theme/components/Search/SearchDialog";
// @ts-ignore
// Import from modular components
import { AlertBanner } from "./components/AlertBanner";
import { NavLogo } from "./components/NavLogo";
import { NavItems } from "./components/NavItems";
import { NavControls, HamburgerButton } from "./controls";
import { DevelopSubmenu, UseCasesSubmenu, CommunitySubmenu, NetworkSubmenu } from "./submenus";
import { MobileMenu } from "./mobile-menu";
import { alertBanner } from "./constants/navigation";
const alertBanner = {
show: false,
message: "APEX 2025",
button: "REGISTER",
link: "https://www.xrpledgerapex.com/?utm_source=xrplwebsite&utm_medium=direct&utm_campaign=xrpl-event-ho-xrplapex-glb-2025-q1_xrplwebsite_ari_arp_bf_rsvp&utm_content=cta_btn_english_pencilbanner"
};
// Re-export AlertBanner for backwards compatibility
export { AlertBanner } from "./components/AlertBanner";
export function AlertBanner({ message, button, link, show }) {
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
const bannerRef = React.useRef(null);
const [displayDate, setDisplayDate] = React.useState("JUNE 10-12");
React.useEffect(() => {
const calculateCountdown = () => {
// Calculate days until June 11, 2025 8AM Singapore time
// This will automatically adjust for the user's timezone
const target = moment.tz('2025-06-11 08:00:00', 'Asia/Singapore');
const now = moment();
const daysUntil = target.diff(now, 'days');
// Show countdown if event is in the future, otherwise show the provided date
let newDisplayDate = "JUNE 10-12";
if (daysUntil > 0) {
newDisplayDate = daysUntil === 1 ? 'IN 1 DAY' : `IN ${daysUntil} DAYS`;
} else if (daysUntil === 0) {
// Check if it's today
const hoursUntil = target.diff(now, 'hours');
newDisplayDate = hoursUntil > 0 ? 'TODAY' : "JUNE 10-12";
}
setDisplayDate(newDisplayDate);
};
// Calculate immediately
calculateCountdown();
// Update every hour
const interval = setInterval(calculateCountdown, 60 * 60 * 1000);
return () => clearInterval(interval);
}, []);
React.useEffect(() => {
const banner = bannerRef.current;
if (!banner) return;
const handleMouseEnter = () => {
banner.classList.add("has-hover");
};
// Attach the event listener
banner.addEventListener("mouseenter", handleMouseEnter);
// Clean up the event listener on unmount
return () => {
banner.removeEventListener("mouseenter", handleMouseEnter);
};
}, []);
if (show) {
return (
<a
href={link}
target="_blank"
ref={bannerRef}
className="top-banner fixed-top web-banner"
rel="noopener noreferrer"
aria-label="Get Tickets for the APEX 2025 Event"
>
<div className="banner-event-details">
<div className="event-info">{translate(message)}</div>
<div className="event-date">{displayDate}</div>
</div>
<div className="banner-button">
<div className="button-text">{translate(button)}</div>
<img
className="button-icon"
src={arrowUpRight}
alt="Get Tickets Icon"
/>
</div>
</a>
);
}
return null;
// Props interface for Navbar (extensible for future use)
interface NavbarProps {
className?: string;
}
export function Navbar(props) {
// const [isOpen, setIsOpen] = useMobileMenu(false);
const themeConfig = useThemeConfig();
const { useL10n } = useThemeHooks();
const { changeLanguage } = useL10n();
const menu = themeConfig.navbar?.items;
const logo = themeConfig.logo;
const { href, altText, items } = props;
const pathPrefix = "";
/**
* Main Navbar Component.
* Renders the complete navigation bar including:
* - Alert banner (when enabled)
* - Logo
* - Navigation items with desktop submenus
* - Control buttons (search, theme toggle, language)
* - Mobile menu
*/
export function Navbar(_props: NavbarProps = {}) {
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
const [activeSubmenu, setActiveSubmenu] = React.useState<string | null>(null);
const [closingSubmenu, setClosingSubmenu] = React.useState<string | null>(null);
const submenuTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
const closingTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
const navItems = menu.map((item, index) => {
if (item.type === "group") {
return (
<NavDropdown
key={index}
label={item.label}
labelTranslationKey={item.labelTranslationKey}
items={item.items}
pathPrefix={pathPrefix}
/>
);
} else {
return (
<NavItem key={index}>
<Link to={item.link} className="nav-link">
{item.label}
</Link>
</NavItem>
);
// Use Redocly's search dialog hook - shared across navbar and mobile menu
const { isOpen: isSearchOpen, onOpen: onSearchOpen, onClose: onSearchClose } = useSearchDialog();
const handleHamburgerClick = () => {
setMobileMenuOpen(true);
};
const handleMobileMenuClose = () => {
setMobileMenuOpen(false);
};
const handleSubmenuMouseEnter = (itemLabel: string) => {
// Clear any pending close/closing timeouts
if (submenuTimeoutRef.current) {
clearTimeout(submenuTimeoutRef.current);
submenuTimeoutRef.current = null;
}
});
if (closingTimeoutRef.current) {
clearTimeout(closingTimeoutRef.current);
closingTimeoutRef.current = null;
}
// Cancel closing state and activate the new submenu
setClosingSubmenu(null);
setActiveSubmenu(itemLabel);
};
const handleSubmenuMouseLeave = () => {
submenuTimeoutRef.current = setTimeout(() => {
// Start closing animation
const currentSubmenu = activeSubmenu;
if (currentSubmenu) {
setClosingSubmenu(currentSubmenu);
setActiveSubmenu(null);
// After animation completes (300ms), clear closing state
closingTimeoutRef.current = setTimeout(() => {
setClosingSubmenu(null);
}, 350); // Slightly longer than animation to ensure completion
}
}, 150);
};
const handleSubmenuClose = () => {
// Close submenu immediately (for keyboard navigation)
if (activeSubmenu) {
setClosingSubmenu(activeSubmenu);
setActiveSubmenu(null);
// After animation completes, clear closing state
closingTimeoutRef.current = setTimeout(() => {
setClosingSubmenu(null);
}, 350);
}
};
// Handle scroll lock when submenu is open or closing
React.useEffect(() => {
if (activeSubmenu || closingSubmenu) {
document.body.classList.add('bds-submenu-open');
} else {
document.body.classList.remove('bds-submenu-open');
}
return () => {
document.body.classList.remove('bds-submenu-open');
};
}, [activeSubmenu, closingSubmenu]);
React.useEffect(() => {
// Turns out jQuery is necessary for firing events on Bootstrap v4
// dropdowns. These events set classes so that the search bar and other
// submenus collapse on mobile when you expand one submenu.
const dds = $("#topnav-pages .dropdown");
const top_main_nav = document.querySelector("#top-main-nav");
dds.on("show.bs.dropdown", (evt) => {
top_main_nav.classList.add("submenu-expanded");
});
dds.on("hidden.bs.dropdown", (evt) => {
top_main_nav.classList.remove("submenu-expanded");
});
// Close navbar on .dropdown-item click
const toggleNavbar = () => {
const navbarToggler = document.querySelector(".navbar-toggler");
const isNavbarCollapsed =
navbarToggler.getAttribute("aria-expanded") === "true";
if (isNavbarCollapsed) {
navbarToggler?.click(); // Simulate click to toggle navbar
return () => {
if (submenuTimeoutRef.current) {
clearTimeout(submenuTimeoutRef.current);
}
if (closingTimeoutRef.current) {
clearTimeout(closingTimeoutRef.current);
}
};
const dropdownItems = document.querySelectorAll(".dropdown-item");
dropdownItems.forEach((item) => {
item.addEventListener("click", toggleNavbar);
});
// Cleanup function to remove event listeners
return () => {
dropdownItems.forEach((item) => {
item.removeEventListener("click", toggleNavbar);
});
};
}, []);
const navbarClasses = [
"bds-navbar",
alertBanner.show ? "bds-navbar--with-banner" : ""
].filter(Boolean).join(" ");
return (
<>
<AlertBanner {...alertBanner} />
<NavWrapper belowAlertBanner={alertBanner.show}>
<LogoBlock to={href} img={logo} alt={altText} />
<NavControls>
<MobileMenuIcon />
</NavControls>
<TopNavCollapsible>
<NavItems>
{navItems}
<div id="topnav-search" className="nav-item search">
<Search className="topnav-search" />
</div>
<div id="topnav-language" className="nav-item">
<LanguagePicker
onChangeLanguage={changeLanguage}
onlyIcon
alignment="end"
/>
</div>
<div id="topnav-theme" className="nav-item">
<ColorModeSwitcher />
</div>
</NavItems>
</TopNavCollapsible>
</NavWrapper>
{/* Backdrop blur overlay when submenu is open or closing */}
<div
className={`bds-submenu-backdrop ${activeSubmenu || closingSubmenu ? 'bds-submenu-backdrop--active' : ''}`}
onClick={() => setActiveSubmenu(null)}
/>
<header
className={navbarClasses}
onMouseLeave={handleSubmenuMouseLeave}
>
<div className="bds-navbar__content">
<NavLogo />
<NavItems activeSubmenu={activeSubmenu} onSubmenuEnter={handleSubmenuMouseEnter} onSubmenuClose={handleSubmenuClose} />
<NavControls onSearch={onSearchOpen} />
<HamburgerButton onClick={handleHamburgerClick} />
</div>
{/* Submenus positioned relative to navbar */}
<div onMouseEnter={() => activeSubmenu && handleSubmenuMouseEnter(activeSubmenu)}>
<DevelopSubmenu isActive={activeSubmenu === 'Develop'} isClosing={closingSubmenu === 'Develop'} onClose={handleSubmenuClose} />
<UseCasesSubmenu isActive={activeSubmenu === 'Use Cases'} isClosing={closingSubmenu === 'Use Cases'} onClose={handleSubmenuClose} />
<CommunitySubmenu isActive={activeSubmenu === 'Community'} isClosing={closingSubmenu === 'Community'} onClose={handleSubmenuClose} />
<NetworkSubmenu isActive={activeSubmenu === 'Network'} isClosing={closingSubmenu === 'Network'} onClose={handleSubmenuClose} />
</div>
</header>
<MobileMenu isOpen={mobileMenuOpen} onClose={handleMobileMenuClose} onSearch={onSearchOpen} />
{/* Render SearchDialog when open - this is the actual search modal */}
{isSearchOpen && <SearchDialog onClose={onSearchClose} />}
</>
);
}
export function TopNavCollapsible({ children }) {
return (
<div
className="collapse navbar-collapse justify-content-between"
id="top-main-nav"
>
{children}
</div>
);
}
export function NavDropdown(props) {
const { label, items, pathPrefix, labelTranslationKey } = props;
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
const dropdownGroups = items.map((item, index) => {
if (item.items) {
const groupLinks = item.items.map((item2, index2) => {
const cls2 = item2.external
? "dropdown-item external-link"
: "dropdown-item";
let item2_href = item2.link;
if (item2_href && !item2_href.match(/^https?:/)) {
item2_href = pathPrefix + item2_href;
}
//conditional specific for brand kit
if (item2.link === "/XRPL_Brand_Kit.zip") {
return (
<a target="_blank" key={index2} href="/XRPL_Brand_Kit.zip" className={cls2}>
{translate(item2.labelTranslationKey, item2.label)}
</a>
);
}
return (
<Link key={index2} className={cls2} to={item2_href}>
{translate(item2.labelTranslationKey, item2.label)}
</Link>
);
});
const clnm = "navcol col-for-" + slugify(item.label);
return (
<div key={index} className={clnm}>
<h5 className="dropdown-item">
{translate(item.labelTranslationKey, item.label)}
</h5>
{groupLinks}
</div>
);
} else if (item.icon) {
const hero_id = "dropdown-hero-for-" + slugify(label);
const img_alt = item.label + " icon";
let hero_href = item.link;
if (hero_href && !hero_href.match(/^https?:/)) {
hero_href = pathPrefix + hero_href;
}
const splitlabel = item.label.split(" || ");
let splittranslationkey = ["", ""];
if (item.labelTranslationKey) {
splittranslationkey = item.labelTranslationKey.split(" || ");
}
const newlabel = translate(splittranslationkey[0], splitlabel[0]);
const description = translate(splittranslationkey[1], splitlabel[1]); // splitlabel[1] might be undefined, that's ok
return (
<Link
key={index}
className="dropdown-item dropdown-hero"
id={hero_id}
to={hero_href}
>
<img id={item.hero} alt={img_alt} src={item.icon} />
<div className="dropdown-hero-text">
<h4>{newlabel}</h4>
<p>{description}</p>
</div>
</Link>
);
} else {
const cls = item.external
? "dropdown-item ungrouped external-link"
: "dropdown-item ungrouped";
let item_href = item.link;
if (item_href && !item_href.match(/^https?:/)) {
item_href = pathPrefix + item_href;
}
return (
<Link key={index} className={cls} to={item_href}>
{translate(item.labelTranslationKey, item.label)}
</Link>
);
}
});
const toggler_id = "topnav_" + slugify(label);
const dd_id = "topnav_dd_" + slugify(label);
return (
<li className="nav-item dropdown">
<a
className="nav-link dropdown-toggle"
href="#"
id={toggler_id}
role="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<span>{translate(labelTranslationKey, label)}</span>
</a>
<div className="dropdown-menu" aria-labelledby={toggler_id} id={dd_id}>
{dropdownGroups}
</div>
</li>
);
}
export function NavWrapper(props) {
return (
<nav
className="top-nav navbar navbar-expand-lg navbar-dark fixed-top"
style={props.belowAlertBanner ? { marginTop: "52px" } : {}}
>
{props.children}
</nav>
);
}
export function NavControls(props) {
return (
<button
className="navbar-toggler collapsed"
type="button"
data-toggle="collapse"
data-target="#top-main-nav"
aria-controls="navbarHolder"
aria-expanded="false"
aria-label="Toggle navigation"
>
{props.children}
</button>
);
}
export function MobileMenuIcon() {
return (
<span className="navbar-toggler-icon">
<div></div>
</span>
);
}
export function GetStartedButton() {
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
return (
<Link
className="btn btn-primary"
to={"/docs/tutorials"}
style={{ height: "38px", paddingTop: "11px" }}
>
{translate("Get Started")}
</Link>
);
}
export function NavItems(props) {
return (
<ul className="nav navbar-nav" id="topnav-pages">
{props.children}
</ul>
);
}
export function NavItem(props) {
return <li className="nav-item">{props.children}</li>;
}
export function LogoBlock(props) {
const { to, img, altText } = props;
return (
<Link className="navbar-brand" to="/">
<img className="logo" alt={"XRP LEDGER"} height="40" src="data:," />
</Link>
);
}
export class ThemeToggle extends React.Component {
auto_update_theme() {
const upc = window.localStorage.getItem("user-prefers-color");
let theme = "dark"; // Default to dark theme
if (!upc) {
// User hasn't saved a preference specifically for this site; check
// the browser-level preferences.
if (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: light)").matches
) {
theme = "light";
}
} else {
// Follow user's saved setting.
theme = upc == "light" ? "light" : "dark";
}
const disable_theme = theme == "dark" ? "light" : "dark";
document.documentElement.classList.add(theme);
document.documentElement.classList.remove(disable_theme);
}
user_choose_theme() {
const new_theme = document.documentElement.classList.contains("dark")
? "light"
: "dark";
window.localStorage.setItem("user-prefers-color", new_theme);
document.body.style.transition = "background-color .2s ease";
const disable_theme = new_theme == "dark" ? "light" : "dark";
document.documentElement.classList.add(new_theme);
document.documentElement.classList.remove(disable_theme);
}
render() {
return (
<div className="nav-item" id="topnav-theme">
<form className="form-inline">
<div
className="custom-control custom-theme-toggle form-inline-item"
title=""
data-toggle="tooltip"
data-placement="left"
data-original-title="Toggle Dark Mode"
>
<input
type="checkbox"
className="custom-control-input"
id="css-toggle-btn"
onClick={this.user_choose_theme}
/>
<label className="custom-control-label" htmlFor="css-toggle-btn">
<span className="d-lg-none">Light/Dark Theme</span>
</label>
</div>
</form>
</div>
);
}
componentDidMount() {
this.auto_update_theme();
}
}

View File

@@ -0,0 +1,82 @@
import * as React from "react";
import { useThemeHooks } from "@redocly/theme/core/hooks";
import moment from "moment-timezone";
import { arrowUpRight } from "../constants/icons";
interface AlertBannerProps {
message: string;
button: string;
link: string;
show: boolean;
}
/**
* Alert Banner Component.
* Displays a promotional banner at the top of the page.
*/
export function AlertBanner({ message, button, link, show }: AlertBannerProps) {
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
const bannerRef = React.useRef<HTMLAnchorElement>(null);
// Use null initial state to avoid hydration mismatch - server and client both render null initially
const [displayDate, setDisplayDate] = React.useState<string | null>(null);
React.useEffect(() => {
const calculateCountdown = () => {
const target = moment.tz('2025-06-11 08:00:00', 'Asia/Singapore');
const now = moment();
const daysUntil = target.diff(now, 'days');
let newDisplayDate = "JUNE 10-12";
if (daysUntil > 0) {
newDisplayDate = daysUntil === 1 ? 'IN 1 DAY' : `IN ${daysUntil} DAYS`;
} else if (daysUntil === 0) {
const hoursUntil = target.diff(now, 'hours');
newDisplayDate = hoursUntil > 0 ? 'TODAY' : "JUNE 10-12";
}
setDisplayDate(newDisplayDate);
};
calculateCountdown();
const interval = setInterval(calculateCountdown, 60 * 60 * 1000);
return () => clearInterval(interval);
}, []);
React.useEffect(() => {
const banner = bannerRef.current;
if (!banner) return;
const handleMouseEnter = () => {
banner.classList.add("has-hover");
};
banner.addEventListener("mouseenter", handleMouseEnter);
return () => {
banner.removeEventListener("mouseenter", handleMouseEnter);
};
}, []);
if (!show) return null;
return (
<a
href={link}
target="_blank"
ref={bannerRef}
className="top-banner fixed-top web-banner"
rel="noopener noreferrer"
aria-label={translate("Get Tickets for the APEX 2025 Event")}
>
<div className="banner-event-details">
<div className="event-info">{translate(message)}</div>
<div className="event-date">{displayDate ?? translate("JUNE 10-12")}</div>
</div>
<div className="banner-button">
<div className="button-text">{translate(button)}</div>
<img className="button-icon" src={arrowUpRight} alt="" />
</div>
</a>
);
}

View File

@@ -0,0 +1,114 @@
import * as React from "react";
import { useThemeHooks } from "@redocly/theme/core/hooks";
import { BdsLink } from "../../../../shared/components/Link/Link";
import { navItems } from "../constants/navigation";
interface NavItemsProps {
activeSubmenu: string | null;
onSubmenuEnter: (itemLabel: string) => void;
onSubmenuClose?: () => void;
}
/**
* Nav Items Component.
* Centered navigation links with submenu support.
* ARIA compliant with full keyboard navigation support.
*/
export function NavItems({ activeSubmenu, onSubmenuEnter, onSubmenuClose }: NavItemsProps) {
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
const [activeItem, setActiveItem] = React.useState<string | null>(null);
const handleMouseEnter = (itemLabel: string, hasSubmenu: boolean) => {
setActiveItem(itemLabel);
if (hasSubmenu) {
onSubmenuEnter(itemLabel);
}
};
const handleMouseLeave = (hasSubmenu: boolean) => {
if (!hasSubmenu) {
setActiveItem(null);
}
// Don't close submenu on leave - let the parent Navbar handle that
};
const handleKeyDown = (event: React.KeyboardEvent, itemLabel: string) => {
switch (event.key) {
case 'Enter':
case ' ':
event.preventDefault();
// Toggle submenu on Enter/Space
if (activeSubmenu === itemLabel) {
onSubmenuClose?.();
} else {
onSubmenuEnter(itemLabel);
}
break;
case 'Escape':
event.preventDefault();
onSubmenuClose?.();
break;
case 'Tab':
// If submenu is open and Tab is pressed (without Shift), move focus into submenu
if (activeSubmenu === itemLabel && !event.shiftKey) {
event.preventDefault();
// Focus first focusable element in submenu
const submenu = document.querySelector('.bds-submenu--active');
const firstFocusable = submenu?.querySelector<HTMLElement>('a, button');
firstFocusable?.focus();
}
break;
case 'ArrowDown':
// If submenu is open, move focus into submenu
if (activeSubmenu === itemLabel) {
event.preventDefault();
// Focus first focusable element in submenu
const submenu = document.querySelector('.bds-submenu--active');
const firstFocusable = submenu?.querySelector<HTMLElement>('a, button');
firstFocusable?.focus();
}
break;
}
};
// Sync activeItem with activeSubmenu state
React.useEffect(() => {
if (!activeSubmenu) {
setActiveItem(null);
}
}, [activeSubmenu]);
return (
<nav className="bds-navbar__items" aria-label={translate("Main navigation")}>
{navItems.map((item) => (
item.hasSubmenu ? (
<button
key={item.label}
type="button"
className={`bds-navbar__item ${activeItem === item.label || activeSubmenu === item.label ? 'bds-navbar__item--active' : ''}`}
onMouseEnter={() => handleMouseEnter(item.label, true)}
onMouseLeave={() => handleMouseLeave(true)}
onKeyDown={(e) => handleKeyDown(e, item.label)}
aria-expanded={activeSubmenu === item.label}
aria-haspopup="menu"
>
{translate(item.labelTranslationKey, item.label)}
</button>
) : (
<BdsLink
key={item.label}
href={item.href}
className={`bds-navbar__item ${activeItem === item.label ? 'bds-navbar__item--active' : ''}`}
onMouseEnter={() => handleMouseEnter(item.label, false)}
onMouseLeave={() => handleMouseLeave(false)}
variant="inline"
>
{translate(item.labelTranslationKey, item.label)}
</BdsLink>
)
))}
</nav>
);
}

View File

@@ -0,0 +1,34 @@
import { useThemeHooks } from "@redocly/theme/core/hooks";
import { BdsLink } from "../../../../shared/components/Link/Link";
import { xrpSymbolBlack, xrpLogotypeBlack, xrpLedgerNav } from "../constants/icons";
/**
* Nav Logo Component.
* Shows symbol on desktop/mobile, full logotype on tablet.
* On desktop hover, the "XRP LEDGER" text animates out to the right.
*/
export function NavLogo() {
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
return (
<BdsLink href="/" className="bds-navbar__logo" aria-label={translate("XRP Ledger Home")} variant="inline">
<img
src={xrpSymbolBlack}
alt={translate("XRP Ledger")}
className="bds-navbar__logo-symbol"
/>
<img
src={xrpLedgerNav}
alt=""
className="bds-navbar__logo-text"
/>
<img
src={xrpLogotypeBlack}
alt={translate("XRP Ledger")}
className="bds-navbar__logo-full"
/>
</BdsLink>
);
}

View File

@@ -0,0 +1,5 @@
// Re-export navbar components
export { AlertBanner } from './AlertBanner';
export { NavLogo } from './NavLogo';
export { NavItems } from './NavItems';

View File

@@ -0,0 +1,83 @@
// Navbar icon imports
// Main navbar icons
export { default as xrpSymbolBlack } from "../../../../static/img/navbar/xrp-symbol-black.svg";
export { default as xrpLogotypeBlack } from "../../../../static/img/navbar/xrp-logotype-black.svg";
export { default as xrpLedgerNav } from "../../../../static/img/navbar/xrp-ledger-nav.svg";
export { default as searchIcon } from "../../../../static/img/navbar/search-icon.svg";
export { default as modeToggleIcon } from "../../../../static/img/navbar/mode-toggle.svg";
export { default as globeIcon } from "../../../../static/img/navbar/globe-icon.svg";
export { default as chevronDown } from "../../../../static/img/navbar/chevron-down.svg";
export { default as hamburgerIcon } from "../../../../static/img/navbar/hamburger-icon.svg";
export { default as arrowUpRight } from "../../../../static/img/icons/arrow-up-right-custom.svg";
// Wallet icons for submenu
export { default as greenWallet } from "../../../../static/img/navbar/green-wallet.svg";
export { default as lilacWallet } from "../../../../static/img/navbar/lilac-wallet.svg";
export { default as yellowWallet } from "../../../../static/img/navbar/yellow-wallet.svg";
export { default as pinkWallet } from "../../../../static/img/navbar/pink-wallet.svg";
export { default as blueWallet } from "../../../../static/img/navbar/blue-wallet.svg";
// Develop submenu icons
export { default as devHomeIcon } from "../../../../static/img/navbar/dev_home.svg";
export { default as learnIcon } from "../../../../static/img/navbar/learn.svg";
export { default as codeSamplesIcon } from "../../../../static/img/navbar/code_samples.svg";
export { default as docsIcon } from "../../../../static/img/navbar/docs.svg";
export { default as clientLibIcon } from "../../../../static/img/navbar/client_lib.svg";
// Use Cases submenu icons
export { default as paymentsIcon } from "../../../../static/img/navbar/payments.svg";
export { default as tokenizationIcon } from "../../../../static/img/navbar/tokenization.svg";
export { default as creditIcon } from "../../../../static/img/navbar/credit.svg";
export { default as tradingIcon } from "../../../../static/img/navbar/trading.svg";
// Community submenu icons
export { default as communityIcon } from "../../../../static/img/navbar/community.svg";
// Network submenu icons
export { default as insightsIcon } from "../../../../static/img/navbar/insights.svg";
export { default as resourcesIcon } from "../../../../static/img/navbar/resources.svg";
// Network submenu pattern images (used for both light and dark mode)
export { default as resourcesIconPattern } from "../../../../static/img/navbar/resources-icon.svg";
export { default as insightsIconPattern } from "../../../../static/img/navbar/insights-icon.svg";
// Wallet icon mapping for dynamic icon lookup
import greenWallet from "../../../../static/img/navbar/green-wallet.svg";
import lilacWallet from "../../../../static/img/navbar/lilac-wallet.svg";
import yellowWallet from "../../../../static/img/navbar/yellow-wallet.svg";
import pinkWallet from "../../../../static/img/navbar/pink-wallet.svg";
import blueWallet from "../../../../static/img/navbar/blue-wallet.svg";
import devHomeIcon from "../../../../static/img/navbar/dev_home.svg";
import learnIcon from "../../../../static/img/navbar/learn.svg";
import codeSamplesIcon from "../../../../static/img/navbar/code_samples.svg";
import docsIcon from "../../../../static/img/navbar/docs.svg";
import clientLibIcon from "../../../../static/img/navbar/client_lib.svg";
import paymentsIcon from "../../../../static/img/navbar/payments.svg";
import tokenizationIcon from "../../../../static/img/navbar/tokenization.svg";
import creditIcon from "../../../../static/img/navbar/credit.svg";
import tradingIcon from "../../../../static/img/navbar/trading.svg";
import communityIcon from "../../../../static/img/navbar/community.svg";
import insightsIcon from "../../../../static/img/navbar/insights.svg";
import resourcesIcon from "../../../../static/img/navbar/resources.svg";
export const walletIcons: Record<string, string> = {
green: greenWallet,
lilac: lilacWallet,
yellow: yellowWallet,
pink: pinkWallet,
blue: blueWallet,
dev_home: devHomeIcon,
learn: learnIcon,
code_samples: codeSamplesIcon,
docs: docsIcon,
client_lib: clientLibIcon,
payments: paymentsIcon,
tokenization: tokenizationIcon,
credit: creditIcon,
trading: tradingIcon,
community: communityIcon,
insights: insightsIcon,
resources: resourcesIcon,
};

View File

@@ -0,0 +1,4 @@
// Re-export all constants
export * from './icons';
export * from './navigation';

View File

@@ -0,0 +1,161 @@
import type { NavItem, SubmenuItemBase, SubmenuItemWithChildren, SubmenuItem, NetworkSubmenuSection } from '../types';
// Alert Banner Configuration
export const alertBanner = {
show: false,
message: "APEX 2025",
button: "REGISTER",
link: "https://www.xrpledgerapex.com/?utm_source=xrplwebsite&utm_medium=direct&utm_campaign=xrpl-event-ho-xrplapex-glb-2025-q1_xrplwebsite_ari_arp_bf_rsvp&utm_content=cta_btn_english_pencilbanner"
};
// Main navigation items
export const navItems: NavItem[] = [
{ label: "Develop", labelTranslationKey: "navbar.develop", href: "/develop", hasSubmenu: true },
{ label: "Use Cases", labelTranslationKey: "navbar.usecases", href: "/use-cases", hasSubmenu: true },
{ label: "Community", labelTranslationKey: "navbar.community", href: "/community", hasSubmenu: true },
{ label: "Network", labelTranslationKey: "navbar.network", href: "/resources", hasSubmenu: true },
{ label: "Showcase", labelTranslationKey: "navbar.network", href: "/showcase", hasSubmenu: false },
];
// Develop submenu data structure
export const developSubmenuData: {
left: SubmenuItemBase[];
right: SubmenuItemWithChildren[];
} = {
left: [
{ label: "Developer's Home", href: "/develop", icon: "dev_home" },
{ label: "Learn", href: "https://learn.xrpl.org", icon: "learn" },
{ label: "Code Samples", href: "/resources/code-samples", icon: "code_samples" },
],
right: [
{
label: "Docs",
href: "/docs",
icon: "docs",
children: [
{ label: "API Reference", href: "/references" },
{ label: "Tutorials", href: "/docs/tutorials" },
{ label: "Concepts", href: "/concepts" },
{ label: "Infrastructure", href: "/docs/infrastructure" },
],
},
{
label: "Client Libraries",
href: "#",
icon: "client_lib",
children: [
{ label: "JavaScript", href: "#" },
{ label: "Python", href: "#" },
{ label: "PHP", href: "#" },
{ label: "Go", href: "#" },
],
},
],
};
// Use Cases submenu data structure
export const useCasesSubmenuData: {
left: SubmenuItemWithChildren[];
right: SubmenuItemWithChildren[];
} = {
left: [
{
label: "Payments",
href: "/use-cases/payments",
icon: "payments",
children: [
{ label: "Direct XRP Payments", href: "/use-cases/payments/direct-xrp-payments" },
{ label: "Cross-currency Payments", href: "/use-cases/payments/cross-currency-payments" },
{ label: "Escrow", href: "/use-cases/payments/escrow" },
{ label: "Checks", href: "/use-cases/payments/checks" },
],
},
{
label: "Tokenization",
href: "/use-cases/tokenization",
icon: "tokenization",
children: [
{ label: "Stablecoin", href: "/use-cases/tokenization/stablecoin" },
{ label: "NFT", href: "/use-cases/tokenization/nft" },
],
},
],
right: [
{
label: "Credit",
href: "/use-cases/credit",
icon: "credit",
children: [
{ label: "Lending", href: "/use-cases/credit/lending" },
{ label: "Collateralization", href: "/use-cases/credit/collateralization" },
{ label: "Sustainability", href: "/use-cases/credit/sustainability" },
],
},
{
label: "Trading",
href: "/use-cases/trading",
icon: "trading",
children: [
{ label: "DEX", href: "/use-cases/trading/dex" },
{ label: "Permissioned Trading", href: "/use-cases/trading/permissioned-trading" },
{ label: "AMM", href: "/use-cases/trading/amm" },
],
},
],
};
// Community submenu data structure
export const communitySubmenuData: {
left: SubmenuItem[];
right: SubmenuItem[];
} = {
left: [
{
label: "Community",
href: "/community",
icon: "community",
children: [
{ label: "Events", href: "/community/events" },
{ label: "Blog", href: "/blog" },
],
},
{ label: "Funding", href: "/community/developer-funding", icon: "code_samples" },
],
right: [
{
label: "Contribute",
href: "/community/contribute",
icon: "client_lib",
children: [
{ label: "Bug Bounty", href: "/blog/2020/rippled-1.5.0#bug-bounties-and-responsible-disclosures" },
{ label: "Research", href: "https://xls.xrpl.org/" },
],
},
{ label: "Ecosystem Map", href: "/about/uses", icon: "learn" },
],
};
// Network submenu data
export const networkSubmenuData: NetworkSubmenuSection[] = [
{
label: "Resources",
href: "/resources",
icon: "resources",
children: [
{ label: "About", href: "/about/history" },
{ label: "XRPL Brand Kit", href: "/community/brand-kit" },
],
patternColor: 'lilac',
},
{
label: "Insights",
href: "/insights",
icon: "insights",
children: [
{ label: "Explorer", href: "https://livenet.xrpl.org" },
{ label: "Amendment Voting Status", href: "https://xrpl.org/resources/known-amendments" },
],
patternColor: 'green',
},
];

View File

@@ -0,0 +1,19 @@
import { useThemeHooks } from "@redocly/theme/core/hooks";
import { IconButton } from "./IconButton";
import { hamburgerIcon } from "../constants/icons";
interface HamburgerButtonProps {
onClick?: () => void;
}
/**
* Hamburger Menu Button Component.
* Mobile-only button that opens the mobile menu.
*/
export function HamburgerButton({ onClick }: HamburgerButtonProps) {
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
return <IconButton icon={hamburgerIcon} ariaLabel={translate("Open menu")} className="bds-navbar__hamburger" onClick={onClick} />;
}

View File

@@ -0,0 +1,28 @@
interface IconButtonProps {
/** The icon image source */
icon: string;
/** Accessible label for the button */
ariaLabel: string;
/** Optional click handler */
onClick?: () => void;
/** CSS class name for styling variants */
className?: string;
}
/**
* Unified Icon Button component.
* Used for search, mode toggle, hamburger menu, and other icon-only buttons.
*/
export function IconButton({ icon, ariaLabel, onClick, className = "bds-navbar__icon" }: IconButtonProps) {
return (
<button
type="button"
className={className}
aria-label={ariaLabel}
onClick={onClick}
>
<img src={icon} alt="" />
</button>
);
}

View File

@@ -0,0 +1,85 @@
import * as React from "react";
import { useLanguagePicker, useThemeHooks } from "@redocly/theme/core/hooks";
interface LanguageDropdownProps {
isOpen: boolean;
onClose: () => void;
}
/**
* Language Dropdown Component.
* Displays available language options in a dropdown menu.
* Based on Figma design: dark background with rounded corners.
*/
export function LanguageDropdown({ isOpen, onClose }: LanguageDropdownProps) {
const { currentLocale, locales, setLocale } = useLanguagePicker();
const { useL10n, useTranslate } = useThemeHooks();
const { changeLanguage } = useL10n();
const { translate } = useTranslate();
const dropdownRef = React.useRef<HTMLDivElement>(null);
// Handle clicking outside to close
React.useEffect(() => {
if (!isOpen) return;
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
// Check if click was on the language pill (parent trigger)
const target = event.target as HTMLElement;
if (!target.closest('.bds-navbar__lang-pill')) {
onClose();
}
}
};
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEscape);
};
}, [isOpen, onClose]);
if (!isOpen || locales.length < 2) {
return null;
}
const handleLanguageSelect = (localeCode: string) => {
setLocale(localeCode);
changeLanguage(localeCode);
onClose();
};
return (
<div
ref={dropdownRef}
className="bds-lang-dropdown"
role="menu"
aria-label={translate("Language selection")}
>
{locales.map((locale) => {
const isActive = locale.code === currentLocale?.code;
return (
<button
key={locale.code}
type="button"
role="menuitem"
className={`bds-lang-dropdown__item ${isActive ? 'bds-lang-dropdown__item--active' : ''}`}
onClick={() => handleLanguageSelect(locale.code)}
aria-current={isActive ? 'true' : undefined}
>
{locale.name || locale.code}
</button>
);
})}
</div>
);
}

View File

@@ -0,0 +1,53 @@
import { useLanguagePicker, useThemeHooks } from "@redocly/theme/core/hooks";
import { globeIcon, chevronDown } from "../constants/icons";
interface LanguagePillProps {
onClick?: () => void;
isOpen?: boolean;
}
/**
* Get short display name for a locale code.
* e.g., "en-US" -> "En", "ja" -> "日本語"
*/
function getLocaleShortName(code: string | undefined): string {
if (!code) return "En";
// Map locale codes to short display names
const shortNames: Record<string, string> = {
"en-US": "En",
"en": "En",
"ja": "日本語",
};
return shortNames[code] || code.substring(0, 2).toUpperCase();
}
/**
* Language Pill Button Component.
* Shows current language and opens language selector.
*/
export function LanguagePill({ onClick, isOpen }: LanguagePillProps) {
const { currentLocale } = useLanguagePicker();
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
const displayName = getLocaleShortName(currentLocale?.code);
return (
<button
type="button"
className={`bds-navbar__lang-pill ${isOpen ? 'bds-navbar__lang-pill--open' : ''}`}
aria-label={translate("Select language")}
aria-expanded={isOpen}
aria-haspopup="menu"
onClick={onClick}
>
<img src={globeIcon} alt="" className="bds-navbar__lang-pill-icon" />
<span className="bds-navbar__lang-pill-text">
<span>{displayName}</span>
<img src={chevronDown} alt="" className="bds-navbar__lang-pill-chevron" />
</span>
</button>
);
}

View File

@@ -0,0 +1,19 @@
import { useThemeHooks } from "@redocly/theme/core/hooks";
import { IconButton } from "./IconButton";
import { modeToggleIcon } from "../constants/icons";
interface ModeToggleButtonProps {
onClick?: () => void;
}
/**
* Mode Toggle Button Component.
* Icon button that toggles between light and dark mode.
*/
export function ModeToggleButton({ onClick }: ModeToggleButtonProps) {
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
return <IconButton icon={modeToggleIcon} ariaLabel={translate("Toggle color mode")} onClick={onClick} />;
}

View File

@@ -0,0 +1,46 @@
import * as React from "react";
import { SearchButton } from "./SearchButton";
import { ModeToggleButton } from "./ModeToggleButton";
import { LanguagePill } from "./LanguagePill";
import { LanguageDropdown } from "./LanguageDropdown";
interface NavControlsProps {
onSearch?: () => void;
}
/**
* Nav Controls Component.
* Right side of the navbar containing search, mode toggle, and language selector.
*/
export function NavControls({ onSearch }: NavControlsProps) {
const [isLanguageDropdownOpen, setIsLanguageDropdownOpen] = React.useState(false);
const handleModeToggle = () => {
// Toggle between light and dark theme
const newTheme = document.documentElement.classList.contains("dark") ? "light" : "dark";
window.localStorage.setItem("user-prefers-color", newTheme);
document.body.style.transition = "background-color .2s ease";
document.documentElement.classList.remove("dark", "light");
document.documentElement.classList.add(newTheme);
};
const handleLanguageClick = () => {
setIsLanguageDropdownOpen(!isLanguageDropdownOpen);
};
const handleLanguageDropdownClose = () => {
setIsLanguageDropdownOpen(false);
};
return (
<div className="bds-navbar__controls">
<SearchButton onClick={onSearch} />
<ModeToggleButton onClick={handleModeToggle} />
<div className="bds-navbar__lang-wrapper">
<LanguagePill onClick={handleLanguageClick} isOpen={isLanguageDropdownOpen} />
<LanguageDropdown isOpen={isLanguageDropdownOpen} onClose={handleLanguageDropdownClose} />
</div>
</div>
);
}

View File

@@ -0,0 +1,19 @@
import { useThemeHooks } from "@redocly/theme/core/hooks";
import { IconButton } from "./IconButton";
import { searchIcon } from "../constants/icons";
interface SearchButtonProps {
onClick?: () => void;
}
/**
* Search Button Component.
* Icon button that triggers the search modal.
*/
export function SearchButton({ onClick }: SearchButtonProps) {
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
return <IconButton icon={searchIcon} ariaLabel={translate("Search")} onClick={onClick} />;
}

View File

@@ -0,0 +1,11 @@
// Unified base component
export { IconButton } from './IconButton';
// Specific button implementations (use IconButton internally)
export { NavControls } from './NavControls';
export { SearchButton } from './SearchButton';
export { ModeToggleButton } from './ModeToggleButton';
export { LanguagePill } from './LanguagePill';
export { LanguageDropdown } from './LanguageDropdown';
export { HamburgerButton } from './HamburgerButton';

View File

@@ -0,0 +1,30 @@
import * as React from "react";
interface ChevronIconProps {
expanded: boolean;
}
/**
* Chevron Icon Component for Mobile Accordion
*/
export function ChevronIcon({ expanded }: ChevronIconProps) {
return (
<svg
className={`bds-mobile-menu__chevron ${expanded ? 'bds-mobile-menu__chevron--expanded' : ''}`}
width="13"
height="8"
viewBox="0 0 13 8"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 1L6.5 6.5L12 1"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}

View File

@@ -0,0 +1,14 @@
import * as React from "react";
/**
* Close Icon Component for Mobile Menu
*/
export function CloseIcon() {
return (
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="7" y1="7" x2="21" y2="21" stroke="#141414" strokeWidth="2" strokeLinecap="round" />
<line x1="21" y1="7" x2="7" y2="21" stroke="#141414" strokeWidth="2" strokeLinecap="round" />
</svg>
);
}

View File

@@ -0,0 +1,56 @@
interface ArrowIconProps {
className?: string;
color?: string;
/**
* When true, the horizontal line has a class for CSS animation (parent links).
* When false, the full arrow is shown without animation class (child links).
*/
animated?: boolean;
}
/**
* Unified Arrow Icon component.
* - For parent links (animated=true): horizontal line animates away on hover
* - For child links (animated=false): full static arrow
*/
export function ArrowIcon({ className, color = "currentColor", animated = true }: ArrowIconProps) {
return (
<svg
className={className}
width="15"
height="14"
viewBox="0 0 26 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
{/* Chevron part */}
<path
d="M14.0019 1.00191L24.0015 11.0015L14.0019 21.001"
stroke={color}
strokeWidth="2"
strokeMiterlimit="10"
strokeLinecap="round"
/>
{/* Horizontal line */}
<path
d="M23.999 10.999H0"
stroke={color}
strokeWidth="2"
strokeMiterlimit="10"
strokeLinecap="round"
className={animated ? "arrow-horizontal" : undefined}
/>
</svg>
);
}
// Backwards-compatible aliases
export const SubmenuArrow = (props: Omit<ArrowIconProps, 'animated'>) => (
<ArrowIcon {...props} animated={true} />
);
export const SubmenuChildArrow = (props: Omit<ArrowIconProps, 'animated'>) => (
<ArrowIcon {...props} animated={false} />
);

View File

@@ -0,0 +1,6 @@
// Re-export all icon components
// Unified arrow icon with backwards-compatible aliases
export { ArrowIcon, SubmenuArrow, SubmenuChildArrow } from './SubmenuArrow';
export { CloseIcon } from './CloseIcon';
export { ChevronIcon } from './ChevronIcon';

View File

@@ -0,0 +1,13 @@
// Main Navbar component
export { Navbar } from './Navbar';
// Re-export types
export * from './types';
// Re-export components for advanced usage
export * from './components';
export * from './controls';
export * from './submenus';
export * from './mobile-menu';
export * from './icons';

View File

@@ -0,0 +1,215 @@
import * as React from "react";
import { useThemeHooks, useLanguagePicker } from "@redocly/theme/core/hooks";
import { BdsLink } from "../../../../shared/components/Link/Link";
import { CloseIcon, ChevronIcon } from "../icons";
import { xrpSymbolBlack, globeIcon, chevronDown, modeToggleIcon, searchIcon } from "../constants/icons";
import { navItems } from "../constants/navigation";
import { MobileMenuContent, type MobileMenuKey } from "./MobileMenuContent";
interface MobileMenuProps {
isOpen: boolean;
onClose: () => void;
onSearch?: () => void;
}
/**
* Mobile Menu Component.
* Full-screen slide-out menu for mobile devices.
*/
export function MobileMenu({ isOpen, onClose, onSearch }: MobileMenuProps) {
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
const [expandedItem, setExpandedItem] = React.useState<string | null>("Develop");
// Handle body scroll lock
React.useEffect(() => {
if (isOpen) {
document.body.classList.add('bds-mobile-menu-open');
} else {
document.body.classList.remove('bds-mobile-menu-open');
}
return () => {
document.body.classList.remove('bds-mobile-menu-open');
};
}, [isOpen]);
const toggleAccordion = (item: string) => {
setExpandedItem(expandedItem === item ? null : item);
};
const handleSearch = () => {
if (onSearch) {
onSearch();
}
onClose();
};
const handleModeToggle = () => {
const newTheme = document.documentElement.classList.contains("dark") ? "light" : "dark";
window.localStorage.setItem("user-prefers-color", newTheme);
document.body.style.transition = "background-color .2s ease";
document.documentElement.classList.remove("dark", "light");
document.documentElement.classList.add(newTheme);
};
const renderAccordionContent = (label: string) => {
// All nav items with submenus use the unified MobileMenuContent
const validKeys: MobileMenuKey[] = ['Develop', 'Use Cases', 'Community', 'Network'];
if (validKeys.includes(label as MobileMenuKey)) {
return <MobileMenuContent menuKey={label as MobileMenuKey} />;
}
return null;
};
return (
<div className={`bds-mobile-menu ${isOpen ? 'bds-mobile-menu--open' : ''}`}>
{/* Header */}
<div className="bds-mobile-menu__header">
<BdsLink href="/" className="bds-navbar__logo" aria-label={translate("XRP Ledger Home")} onClick={onClose} variant="inline">
<img src={xrpSymbolBlack} alt={translate("XRP Ledger")} className="bds-navbar__logo-symbol" style={{ width: 33, height: 28 }} />
</BdsLink>
<button
type="button"
className="bds-mobile-menu__close"
aria-label={translate("Close menu")}
onClick={onClose}
>
<CloseIcon />
</button>
</div>
{/* Content */}
<div className="bds-mobile-menu__content">
<div className="bds-mobile-menu__accordion">
{navItems.map((item) => (
<React.Fragment key={item.label}>
<button
type="button"
className="bds-mobile-menu__accordion-header"
onClick={() => item.hasSubmenu ? toggleAccordion(item.label) : null}
aria-expanded={expandedItem === item.label}
>
{item.hasSubmenu ? (
<>
<span>{translate(item.labelTranslationKey, item.label)}</span>
<ChevronIcon expanded={expandedItem === item.label} />
</>
) : (
<BdsLink
href={item.href}
onClick={onClose}
variant="inline"
style={{
display: 'flex',
width: '100%',
justifyContent: 'space-between',
alignItems: 'center',
color: 'inherit',
textDecoration: 'none'
}}
>
<span>{translate(item.labelTranslationKey, item.label)}</span>
<ChevronIcon expanded={false} />
</BdsLink>
)}
</button>
{item.hasSubmenu && (
<div
className={`bds-mobile-menu__accordion-content ${
expandedItem === item.label ? 'bds-mobile-menu__accordion-content--expanded' : ''
}`}
>
{renderAccordionContent(item.label)}
</div>
)}
</React.Fragment>
))}
</div>
</div>
{/* Footer */}
<MobileMenuFooter
onModeToggle={handleModeToggle}
onSearch={handleSearch}
/>
</div>
);
}
interface MobileMenuFooterProps {
onModeToggle: () => void;
onSearch: () => void;
}
/**
* Get short display name for a locale code.
*/
function getLocaleShortName(code: string | undefined): string {
if (!code) return "En";
const shortNames: Record<string, string> = {
"en-US": "En",
"en": "En",
"ja": "日本語",
};
return shortNames[code] || code.substring(0, 2).toUpperCase();
}
function MobileMenuFooter({ onModeToggle, onSearch }: MobileMenuFooterProps) {
const { currentLocale, locales, setLocale } = useLanguagePicker();
const { useL10n, useTranslate } = useThemeHooks();
const { changeLanguage } = useL10n();
const { translate } = useTranslate();
const [isLangOpen, setIsLangOpen] = React.useState(false);
const displayName = getLocaleShortName(currentLocale?.code);
const handleLanguageSelect = (localeCode: string) => {
setLocale(localeCode);
changeLanguage(localeCode);
setIsLangOpen(false);
};
return (
<div className="bds-mobile-menu__footer">
<div className="bds-mobile-menu__lang-wrapper">
<button
type="button"
className={`bds-mobile-menu__lang-pill ${isLangOpen ? 'bds-mobile-menu__lang-pill--open' : ''}`}
aria-label={translate("Select language")}
aria-expanded={isLangOpen}
onClick={() => setIsLangOpen(!isLangOpen)}
>
<img src={globeIcon} alt="" className="bds-mobile-menu__lang-pill-icon" />
<span className="bds-mobile-menu__lang-pill-text">
<span>{displayName}</span>
<img src={chevronDown} alt="" className="bds-mobile-menu__lang-pill-chevron" />
</span>
</button>
{isLangOpen && locales.length >= 2 && (
<div className="bds-lang-dropdown bds-lang-dropdown--mobile" role="menu">
{locales.map((locale) => {
const isActive = locale.code === currentLocale?.code;
return (
<button
key={locale.code}
type="button"
role="menuitem"
className={`bds-lang-dropdown__item ${isActive ? 'bds-lang-dropdown__item--active' : ''}`}
onClick={() => handleLanguageSelect(locale.code)}
>
{locale.name || locale.code}
</button>
);
})}
</div>
)}
</div>
<button type="button" className="bds-mobile-menu__footer-icon" aria-label={translate("Toggle color mode")} onClick={onModeToggle}>
<img src={modeToggleIcon} alt="" />
</button>
<button type="button" className="bds-mobile-menu__footer-icon" aria-label={translate("Search")} onClick={onSearch}>
<img src={searchIcon} alt="" />
</button>
</div>
);
}

View File

@@ -0,0 +1,47 @@
import { MobileMenuSection } from "./MobileMenuSection";
import { developSubmenuData, useCasesSubmenuData, communitySubmenuData, networkSubmenuData } from "../constants/navigation";
import type { SubmenuItem, SubmenuItemWithChildren, NetworkSubmenuSection } from "../types";
export type MobileMenuKey = 'Develop' | 'Use Cases' | 'Community' | 'Network';
interface MobileMenuContentProps {
/** Which menu section to render */
menuKey: MobileMenuKey;
}
/** Get flattened menu items based on menu key */
function getMenuItems(menuKey: MobileMenuKey): (SubmenuItem | SubmenuItemWithChildren | NetworkSubmenuSection)[] {
switch (menuKey) {
case 'Develop':
return [...developSubmenuData.left, ...developSubmenuData.right];
case 'Use Cases':
return [...useCasesSubmenuData.left, ...useCasesSubmenuData.right];
case 'Community':
return [...communitySubmenuData.left, ...communitySubmenuData.right];
case 'Network':
return networkSubmenuData;
}
}
/**
* Unified Mobile Menu Content component.
* Renders accordion content for any menu section.
*/
export function MobileMenuContent({ menuKey }: MobileMenuContentProps) {
const items = getMenuItems(menuKey);
return (
<div className="bds-mobile-menu__tier-list">
{items.map((item) => (
<MobileMenuSection key={item.label} item={item} />
))}
</div>
);
}
// Backwards-compatible named exports
export const MobileMenuDevelopContent = () => <MobileMenuContent menuKey="Develop" />;
export const MobileMenuUseCasesContent = () => <MobileMenuContent menuKey="Use Cases" />;
export const MobileMenuCommunityContent = () => <MobileMenuContent menuKey="Community" />;
export const MobileMenuNetworkContent = () => <MobileMenuContent menuKey="Network" />;

View File

@@ -0,0 +1,54 @@
import * as React from "react";
import { useThemeHooks } from "@redocly/theme/core/hooks";
import { SubmenuArrow, SubmenuChildArrow } from "../icons";
import { walletIcons } from "../constants/icons";
import { hasChildren, type SubmenuItem } from "../types";
interface MobileMenuSectionProps {
item: SubmenuItem;
}
/**
* Reusable mobile menu section component.
* Renders a parent link with icon, and optionally child links.
*/
export function MobileMenuSection({ item }: MobileMenuSectionProps) {
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
const itemHasChildren = hasChildren(item);
return (
<React.Fragment>
<a href={item.href} className="bds-mobile-menu__tier1 bds-mobile-menu__parent-link">
<span className="bds-mobile-menu__icon">
<img src={walletIcons[item.icon]} alt="" />
</span>
<span className="bds-mobile-menu__link bds-mobile-menu__link--bold">
{translate(item.label)}
<span className="bds-mobile-menu__arrow">
<SubmenuArrow />
</span>
</span>
</a>
{itemHasChildren && (
<div className="bds-mobile-menu__tier2">
{item.children.map((child) => (
<a
key={child.label}
href={child.href}
className="bds-mobile-menu__sublink"
target={child.href.startsWith('http') ? '_blank' : undefined}
rel={child.href.startsWith('http') ? 'noopener noreferrer' : undefined}
>
{translate(child.label)}
<span className="bds-mobile-menu__sublink-arrow">
<SubmenuChildArrow />
</span>
</a>
))}
</div>
)}
</React.Fragment>
);
}

View File

@@ -0,0 +1,16 @@
// Main mobile menu component
export { MobileMenu } from './MobileMenu';
// Unified content component with backwards-compatible aliases
export {
MobileMenuContent,
MobileMenuDevelopContent,
MobileMenuUseCasesContent,
MobileMenuCommunityContent,
MobileMenuNetworkContent,
type MobileMenuKey
} from './MobileMenuContent';
// Reusable section component
export { MobileMenuSection } from './MobileMenuSection';

View File

@@ -0,0 +1,16 @@
import { Submenu } from "./Submenu";
interface CommunitySubmenuProps {
isActive: boolean;
isClosing: boolean;
onClose?: () => void;
}
/**
* Desktop Community Submenu Component.
* Wrapper for unified Submenu component with 'community' variant.
*/
export function CommunitySubmenu({ isActive, isClosing, onClose }: CommunitySubmenuProps) {
return <Submenu variant="community" isActive={isActive} isClosing={isClosing} onClose={onClose} />;
}

View File

@@ -0,0 +1,16 @@
import { Submenu } from "./Submenu";
interface DevelopSubmenuProps {
isActive: boolean;
isClosing: boolean;
onClose?: () => void;
}
/**
* Desktop Develop Submenu Component.
* Wrapper for unified Submenu component with 'develop' variant.
*/
export function DevelopSubmenu({ isActive, isClosing, onClose }: DevelopSubmenuProps) {
return <Submenu variant="develop" isActive={isActive} isClosing={isClosing} onClose={onClose} />;
}

View File

@@ -0,0 +1,16 @@
import { Submenu } from "./Submenu";
interface NetworkSubmenuProps {
isActive: boolean;
isClosing: boolean;
onClose?: () => void;
}
/**
* Desktop Network Submenu Component.
* Wrapper for unified Submenu component with 'network' variant.
*/
export function NetworkSubmenu({ isActive, isClosing, onClose }: NetworkSubmenuProps) {
return <Submenu variant="network" isActive={isActive} isClosing={isClosing} onClose={onClose} />;
}

View File

@@ -0,0 +1,277 @@
import * as React from "react";
import { useThemeHooks } from "@redocly/theme/core/hooks";
import { SubmenuSection } from "./SubmenuSection";
import { ArrowIcon } from "../icons";
import { walletIcons, resourcesIconPattern, insightsIconPattern } from "../constants/icons";
import { developSubmenuData, useCasesSubmenuData, communitySubmenuData, networkSubmenuData } from "../constants/navigation";
import type { SubmenuItem, SubmenuItemWithChildren, NetworkSubmenuSection } from "../types";
export type SubmenuVariant = 'develop' | 'use-cases' | 'community' | 'network';
interface SubmenuProps {
/** Which submenu variant to render */
variant: SubmenuVariant;
/** Whether this submenu is currently active (visible) */
isActive: boolean;
/** Whether this submenu is in closing animation */
isClosing: boolean;
/** Callback when submenu should close (e.g., Escape key) */
onClose?: () => void;
}
/** Get submenu data based on variant */
function getSubmenuData(variant: SubmenuVariant) {
switch (variant) {
case 'develop': return developSubmenuData;
case 'use-cases': return useCasesSubmenuData;
case 'community': return communitySubmenuData;
case 'network': return networkSubmenuData;
}
}
/** Get CSS modifier class for variant */
function getVariantClass(variant: SubmenuVariant): string {
if (variant === 'develop') return '';
return `bds-submenu--${variant}`;
}
/**
* Get all focusable elements within a container
*/
function getFocusableElements(container: HTMLElement | null): HTMLElement[] {
if (!container) return [];
return Array.from(
container.querySelectorAll<HTMLElement>('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])')
);
}
/**
* Find the next nav item button after the current expanded one
*/
function getNextNavItem(): HTMLElement | null {
const navItems = document.querySelectorAll<HTMLElement>('.bds-navbar__item');
const currentIndex = Array.from(navItems).findIndex(item =>
item.getAttribute('aria-expanded') === 'true'
);
if (currentIndex >= 0 && currentIndex < navItems.length - 1) {
return navItems[currentIndex + 1];
}
// If at the last nav item, go to the first control button (search, etc.)
const controls = document.querySelector<HTMLElement>('.bds-navbar__controls button, .bds-navbar__controls a');
return controls;
}
/**
* Unified Submenu component.
* Handles all submenu variants (develop, use-cases, community, network).
* ARIA compliant with full keyboard navigation support.
*/
export function Submenu({ variant, isActive, isClosing, onClose }: SubmenuProps) {
const submenuRef = React.useRef<HTMLDivElement>(null);
// Handle keyboard events for accessibility
const handleKeyDown = React.useCallback((event: KeyboardEvent) => {
if (!isActive) return;
if (event.key === 'Escape') {
event.preventDefault();
onClose?.();
// Return focus to the trigger button
const triggerButton = document.querySelector<HTMLButtonElement>(
`.bds-navbar__item[aria-expanded="true"]`
);
triggerButton?.focus();
}
// Handle Tab at end of submenu - move to next nav item
if (event.key === 'Tab' && !event.shiftKey) {
const activeSubmenu = document.querySelector<HTMLElement>('.bds-submenu--active');
const focusableElements = getFocusableElements(activeSubmenu);
const lastFocusable = focusableElements[focusableElements.length - 1];
if (document.activeElement === lastFocusable) {
event.preventDefault();
onClose?.();
const nextItem = getNextNavItem();
nextItem?.focus();
}
}
// Handle Shift+Tab at start of submenu - move back to trigger button
if (event.key === 'Tab' && event.shiftKey) {
const activeSubmenu = document.querySelector<HTMLElement>('.bds-submenu--active');
const focusableElements = getFocusableElements(activeSubmenu);
const firstFocusable = focusableElements[0];
if (document.activeElement === firstFocusable) {
event.preventDefault();
onClose?.();
// Return focus to the trigger button
const triggerButton = document.querySelector<HTMLButtonElement>(
`.bds-navbar__item[aria-expanded="true"]`
);
triggerButton?.focus();
}
}
}, [isActive, onClose]);
// Add keyboard event listener when submenu is active
React.useEffect(() => {
if (isActive) {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}
}, [isActive, handleKeyDown]);
// Network submenu needs special handling for theme-aware patterns
if (variant === 'network') {
return <NetworkSubmenuContent isActive={isActive} isClosing={isClosing} onClose={onClose} />;
}
const data = getSubmenuData(variant);
const classNames = [
'bds-submenu',
getVariantClass(variant),
isActive ? 'bds-submenu--active' : '',
isClosing ? 'bds-submenu--closing' : '',
].filter(Boolean).join(' ');
// Standard two-column layout
const leftItems = 'left' in data ? data.left : [];
const rightItems = 'right' in data ? data.right : [];
return (
<div
ref={submenuRef}
className={classNames}
role="menu"
aria-hidden={!isActive}
>
<div className="bds-submenu__left">
{leftItems.map((item: SubmenuItem | SubmenuItemWithChildren) => (
<SubmenuSection key={item.label} item={item} />
))}
</div>
<div className="bds-submenu__right">
{rightItems.map((item: SubmenuItem | SubmenuItemWithChildren) => (
<SubmenuSection key={item.label} item={item} />
))}
</div>
</div>
);
}
/** 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();
// Handle keyboard events for accessibility
const handleKeyDown = React.useCallback((event: KeyboardEvent) => {
if (!isActive) return;
if (event.key === 'Escape') {
event.preventDefault();
onClose?.();
// Return focus to the trigger button
const triggerButton = document.querySelector<HTMLButtonElement>(
`.bds-navbar__item[aria-expanded="true"]`
);
triggerButton?.focus();
}
// Handle Tab at end of submenu - move to next nav item
if (event.key === 'Tab' && !event.shiftKey) {
const activeSubmenu = document.querySelector<HTMLElement>('.bds-submenu--active');
const focusableElements = getFocusableElements(activeSubmenu);
const lastFocusable = focusableElements[focusableElements.length - 1];
if (document.activeElement === lastFocusable) {
event.preventDefault();
onClose?.();
const nextItem = getNextNavItem();
nextItem?.focus();
}
}
// Handle Shift+Tab at start of submenu - move back to trigger button
if (event.key === 'Tab' && event.shiftKey) {
const activeSubmenu = document.querySelector<HTMLElement>('.bds-submenu--active');
const focusableElements = getFocusableElements(activeSubmenu);
const firstFocusable = focusableElements[0];
if (document.activeElement === firstFocusable) {
event.preventDefault();
onClose?.();
// Return focus to the trigger button
const triggerButton = document.querySelector<HTMLButtonElement>(
`.bds-navbar__item[aria-expanded="true"]`
);
triggerButton?.focus();
}
}
}, [isActive, onClose]);
// Add keyboard event listener when submenu is active
React.useEffect(() => {
if (isActive) {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}
}, [isActive, handleKeyDown]);
// Use same pattern images for both light and dark mode
const patternImages = {
lilac: resourcesIconPattern,
green: insightsIconPattern,
};
const classNames = [
'bds-submenu',
'bds-submenu--network',
isActive ? 'bds-submenu--active' : '',
isClosing ? 'bds-submenu--closing' : '',
].filter(Boolean).join(' ');
return (
<div className={classNames} role="menu" aria-hidden={!isActive}>
{networkSubmenuData.map((section: NetworkSubmenuSection) => (
<div key={section.label} className="bds-submenu__section">
<a href={section.href} className="bds-submenu__tier1 bds-submenu__parent-link">
<span className="bds-submenu__icon">
<img src={walletIcons[section.icon]} alt="" />
</span>
<span className="bds-submenu__link bds-submenu__link--bold">
{translate(section.label)}
<span className="bds-submenu__arrow">
<ArrowIcon animated />
</span>
</span>
</a>
<div className="bds-submenu__network-content">
<div className="bds-submenu__tier2">
{section.children.map((child) => (
<a
key={child.label}
href={child.href}
className="bds-submenu__sublink"
target={child.href.startsWith('http') ? '_blank' : undefined}
rel={child.href.startsWith('http') ? 'noopener noreferrer' : undefined}
>
{translate(child.label)}
<span className="bds-submenu__sublink-arrow">
<ArrowIcon animated={false} />
</span>
</a>
))}
</div>
<div className="bds-submenu__pattern-container">
<img src={patternImages[section.patternColor]} alt="" className="bds-submenu__pattern" />
</div>
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,70 @@
import { useThemeHooks } from "@redocly/theme/core/hooks";
import { ArrowIcon } from "../icons";
import { walletIcons } from "../constants/icons";
import { hasChildren, type SubmenuItem, type SubmenuItemWithChildren, type SubmenuItemBase } from "../types";
interface SubmenuSectionProps {
/** The menu item data */
item: SubmenuItem | SubmenuItemWithChildren | SubmenuItemBase;
/** Whether to render children links (default: true) */
showChildren?: boolean;
}
/**
* Unified submenu section component.
* Renders a parent link with icon, and optionally child links if the item has them.
*
* Usage:
* - For items that may or may not have children: <SubmenuSection item={item} />
* - For parent-only rendering: <SubmenuSection item={item} showChildren={false} />
*/
export function SubmenuSection({ item, showChildren = true }: SubmenuSectionProps) {
const { useTranslate } = useThemeHooks();
const { translate } = useTranslate();
const itemHasChildren = hasChildren(item as SubmenuItem);
const shouldShowChildren = showChildren && itemHasChildren;
return (
<div className="bds-submenu__section">
<a href={item.href} className="bds-submenu__tier1 bds-submenu__parent-link">
<span className="bds-submenu__icon">
<img src={walletIcons[item.icon]} alt="" />
</span>
<span className="bds-submenu__link bds-submenu__link--bold">
{translate(item.label)}
<span className="bds-submenu__arrow">
<ArrowIcon animated />
</span>
</span>
</a>
{shouldShowChildren && (
<div className="bds-submenu__tier2">
{(item as SubmenuItemWithChildren).children.map((child) => (
<a
key={child.label}
href={child.href}
className="bds-submenu__sublink"
target={child.href.startsWith('http') ? '_blank' : undefined}
rel={child.href.startsWith('http') ? 'noopener noreferrer' : undefined}
>
{translate(child.label)}
<span className="bds-submenu__sublink-arrow">
<ArrowIcon animated={false} />
</span>
</a>
))}
</div>
)}
</div>
);
}
// Backwards-compatible aliases (all use the unified SubmenuSection)
export const SubmenuParentOnly = ({ item }: { item: SubmenuItemBase }) => (
<SubmenuSection item={item} showChildren={false} />
);
export const SubmenuWithChildren = ({ item }: { item: SubmenuItemWithChildren }) => (
<SubmenuSection item={item} showChildren={true} />
);

View File

@@ -0,0 +1,16 @@
import { Submenu } from "./Submenu";
interface UseCasesSubmenuProps {
isActive: boolean;
isClosing: boolean;
onClose?: () => void;
}
/**
* Desktop Use Cases Submenu Component.
* Wrapper for unified Submenu component with 'use-cases' variant.
*/
export function UseCasesSubmenu({ isActive, isClosing, onClose }: UseCasesSubmenuProps) {
return <Submenu variant="use-cases" isActive={isActive} isClosing={isClosing} onClose={onClose} />;
}

View File

@@ -0,0 +1,12 @@
// Unified submenu component
export { Submenu, type SubmenuVariant } from './Submenu';
// Variant-specific wrappers (all use Submenu internally)
export { DevelopSubmenu } from './DevelopSubmenu';
export { UseCasesSubmenu } from './UseCasesSubmenu';
export { CommunitySubmenu } from './CommunitySubmenu';
export { NetworkSubmenu } from './NetworkSubmenu';
// Reusable submenu section component
export { SubmenuSection, SubmenuParentOnly, SubmenuWithChildren } from './SubmenuSection';

View File

@@ -0,0 +1,42 @@
// Types for submenu data structures
export interface SubmenuChild {
label: string;
href: string;
active?: boolean;
}
export interface SubmenuItemBase {
label: string;
href: string;
icon: string;
}
export interface SubmenuItemWithChildren extends SubmenuItemBase {
children: SubmenuChild[];
}
export type SubmenuItem = SubmenuItemBase | SubmenuItemWithChildren;
// Network submenu section with decorative images
export interface NetworkSubmenuSection {
label: string;
href: string;
icon: string;
children: SubmenuChild[];
patternColor: 'lilac' | 'green';
}
// Nav item type
export interface NavItem {
label: string;
labelTranslationKey: string;
href: string;
hasSubmenu: boolean;
}
// Type guard to check if item has children
export function hasChildren(item: SubmenuItem): item is SubmenuItemWithChildren {
return 'children' in item && Array.isArray((item as SubmenuItemWithChildren).children);
}

View File

@@ -25,7 +25,7 @@ export function XRPLCard(props: XRPLCardProps) {
<p className="card-text">{props.body}</p>
)}
</div>
<div className="card-footer">&nbsp;</div>
{/* <div className="card-footer">&nbsp;</div> */}
</Link>
)
}

178
COLOR-MIGRATION-SUMMARY.md Normal file
View File

@@ -0,0 +1,178 @@
# Color System Migration Summary
**Date:** October 21, 2025
**Source:** [XRPL.org Design Tokens - Figma](https://figma.com/design/zRyhXG4hRP3Lk3B6Owr3eo/XRPL.org-Design-Tokens)
## Migration Strategy: Clean Migration
The old 10-level color scale (100-1000) has been completely migrated to a new 5-level scale (100-500). All references in the codebase have been updated, and backward compatibility aliases have been removed for a clean implementation.
**Mapping Applied:**
```
Old System → New System
100 → 100 (lightest)
200 → 100
300 → 200
400 → 200
500 → 300 (mid-tone, default)
600 → 300
700 → 400
800 → 400
900 → 500 (darkest)
1000 → 500
```
**Migration Approach:**
1. All color usages (600-1000) were found and replaced with their new equivalents (300-500)
2. Backward compatibility aliases were removed from `_colors.scss`
3. Only 100-500 design tokens remain for each color family
## Color Families Updated
### Primary Colors
#### Gray (Neutral) ⏸️ NOT UPDATED
- **Status:** Original values retained - design tokens not ready
- **Current values:** #FCFCFD, #F5F5F7, #E0E0E1, #C1C1C2, #A2A2A4, #838386, #454549, #343437, #232325, #111112
- **Note:** Gray/Neutral design tokens will be migrated in a future update
#### Green ✅
- **New Design Tokens:** #EAFCF1, #70EE97, #21E46B, #0DAA3E, #078139
- **Variables:** `$green-100` through `$green-500` only
- **Migrated:** 14 file references updated
- **Special:** `$apex-2023-green` (#00FF76) retained
#### Lilac (Primary) ✅ *replaces blue-purple*
- **New Design Tokens:** #F2EDFF, #D9CAFF, #C0A7FF, #7649E3, #5429A1
- **Variables:** `$lilac-100` through `$lilac-500` only
- **Legacy aliases:** `$blue-purple-100` through `$blue-purple-500` map to lilac (600-900 removed)
- **Migrated:** 16 file references updated
- **Note:** This is a new color name in the design system
### Secondary Colors
#### Red ✅ *NEW*
- **New Design Tokens:** #FDECE7, #F27A66, #F0643A, #DA4518, #A22514
- **Variables:** `$red-100` through `$red-500` only
- **Note:** This is a completely new color family added to the design system
#### Pink ✅ *replaces magenta*
- **New Design Tokens:** #FDF1F4, #F2B5C3, #F18DA5, #FF577F, #DC466F
- **Variables:** `$pink-100` through `$pink-500` only
- **Legacy aliases:** `$magenta-100` through `$magenta-500` map to pink (600-900 removed)
- **Migrated:** 7 file references updated
#### Blue ✅
- **New Design Tokens:** #EDF4FF, #93BFF1, #428CFF, #0179E7, #0A4DC0
- **Variables:** `$blue-100` through `$blue-500` only
- **Migrated:** 8 file references updated
- **Special:** `$accent-blue-90` (#001133) retained
#### Yellow ✅
- **New Design Tokens:** #F3F1EB, #E6F1A7, #DBF15E, #E1DB26, #D4C02D
- **Variables:** `$yellow-100` through `$yellow-500` only
- **Migrated:** 11 file references updated
## Colors Retained (No Design Token Replacement)
### Orange
- **Status:** Legacy values retained
- **Values:** #FFEEE5, #FFCCB2, #FFAA80, #FF884B, #FF6719, #E54D00, #B23C00, #802B00, #4C1A00
- **Reason:** No corresponding design token in new system
### Red-purple
- **Status:** Legacy values retained
- **Values:** #FBE5FF, #F2B2FF, #EA80FF, #E24CFF, #D919FF, #C000E5, #9500B2, #6B0080, #40004C
- **Reason:** No corresponding design token in new system
### Special Event Colors
- `$apex-2023-green: #00FF76`
- `$token-2049-purple: #410bb9`
- `$accent-blue-90: #001133`
## Bootstrap & Component Colors
All Bootstrap theme variables remain functional:
- `$primary``$purple` (now `$lilac-400`)
- `$secondary``$gray-200`
- `$success``$green-500`
- `$info``$blue-500`
- `$warning``$yellow-500`
- `$danger``$magenta-500` (now `$pink-500`)
## Breaking Changes
**Removed Variables:**
- All color variables from 600-1000 have been removed for: Green, Blue, Lilac, Pink, Red, Yellow
- `$blue-purple-600` through `$blue-purple-900` removed (use 100-500)
- `$magenta-600` through `$magenta-900` removed (use 100-500)
**No Impact:**
- All usages in the codebase have been updated
- Legacy color name aliases maintained (100-500 only):
- `$blue-purple-100` through `$blue-purple-500` → maps to `$lilac-*`
- `$magenta-100` through `$magenta-500` → maps to `$pink-*`
## Color Name Changes
| Old Name | New Name | Reason |
|----------|----------|--------|
| `blue-purple-*` | `lilac-*` | Design system rebranding |
| `magenta-*` | `pink-*` | Design system rebranding |
| N/A | `red-*` | New color family added |
## Usage Recommendations
### Current Best Practices
Use the new 5-level design tokens (100-500):
```scss
// Primary colors
color: $gray-300; // Gray (not yet migrated - still uses old values)
color: $green-300; // Default green
color: $lilac-400; // Primary purple
// Secondary colors
color: $red-300; // Default red
color: $pink-300; // Default pink
color: $blue-300; // Default blue
color: $yellow-300; // Default yellow
```
### Legacy Aliases Still Available
```scss
// These legacy names work (100-500 only)
color: $blue-purple-400; // Same as $lilac-400
color: $magenta-300; // Same as $pink-300
```
## Files Modified
- `styles/_colors.scss` - Complete color system update
## Validation Status
✅ All SCSS variables resolve correctly
✅ No linter errors
✅ Bootstrap theme colors functional
✅ All old color references (600-1000) removed from codebase
✅ Special event colors preserved
⏸️ Gray/Neutral colors - pending future update
## Migration Statistics
**Files Updated:** 11 SCSS files
- `styles/_colors.scss` - Color definitions cleaned up
- `styles/light/_light-theme.scss` - 11 color references updated
- `styles/_status-labels.scss` - 39 color references updated
- `styles/_diagrams.scss` - 6 color references updated
- `styles/_code-tabs.scss` - 2 color references updated
- `styles/_content.scss` - 1 color reference updated
- `styles/_buttons.scss` - 7 color references updated
- `styles/_pages.scss` - 3 color references updated
- `styles/_blog.scss` - 2 color references updated
- `styles/_feedback.scss` - 2 color references updated
- `styles/_rpc-tool.scss` - 1 color reference updated
- `styles/_landings.scss` - 1 color reference updated
**Total Color References Updated:** 75+ instances

154
CSS-OPTIMIZATION-SUMMARY.md Normal file
View File

@@ -0,0 +1,154 @@
# CSS Optimization - Implementation Summary
## ✅ Successfully Completed
The CSS build pipeline has been modernized with industry-standard optimization tools, resulting in significant performance improvements.
## Results
### Bundle Size Improvements
\`\`\`
=== CSS Bundle Comparison ===
Master (Bootstrap 4):
Uncompressed: 405.09 KB
Gzipped: 63.44 KB
This Branch BEFORE Optimization (Bootstrap 5):
Uncompressed: 486.64 KB
Gzipped: 71.14 KB
This Branch AFTER Optimization (Bootstrap 5 + PurgeCSS):
Uncompressed: 280.92 KB ✅ 42% smaller
Gzipped: 43.32 KB ✅ 39% smaller (network transfer)
\`\`\`
### Key Improvements
| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| **Network Transfer (Gzipped)** | 71.14 KB | 43.32 KB | **39% smaller** |
| **Uncompressed Size** | 486.64 KB | 280.92 KB | **42% smaller** |
| **CSS Selectors** | 5,423 | 2,681 | **51% fewer** |
| **DevTools Filter Performance** | ~60 seconds | <1 second | **98% faster** |
### Real-World Impact
- **Page Load:** 40% faster CSS download on 3G connections
- **Developer Experience:** DevTools CSS filtering is now instant (<1s vs 60s)
- **Bandwidth Savings:** ~28 KB less per page load
- **Maintainability:** Modern tooling with source maps in development
## What Was Implemented
### 1. Modern Build Pipeline
- **Upgraded Sass** from 1.26.10 (2020) 1.93.2 (latest)
- **Added PostCSS** with optimization plugins:
- **PurgeCSS** - Removes unused CSS selectors
- **Autoprefixer** - Browser compatibility
- **cssnano** - Advanced minification
### 2. Build Scripts
```json
{
"scripts": {
"build-css": "Production build with full optimization",
"build-css-dev": "Development build with source maps",
"build-css-watch": "Watch mode for continuous compilation",
"analyze-css": "Bundle analysis tool"
}
}
```
### 3. PurgeCSS Configuration
- Scans all `.tsx`, `.md`, `.yaml`, `.html` files for class names
- Intelligent safelist for dynamically-added classes
- Preserves Bootstrap JS components, CodeMirror, custom tools
- Only runs in production (dev builds are fast)
### 4. CSS Analysis Tool
Created `scripts/analyze-css.js` to monitor:
- Bundle size and composition
- Bootstrap component usage
- Optimization opportunities
- Before/after comparisons
## Files Created/Modified
### New Files
- `postcss.config.cjs` - PostCSS and PurgeCSS configuration
- `scripts/analyze-css.js` - CSS bundle analysis tool
- `CSS-OPTIMIZATION.md` - Comprehensive optimization guide
- `CSS-OPTIMIZATION-SUMMARY.md` - This summary
### Modified Files
- `package.json` - Updated dependencies and build scripts
- `styles/README.md` - Updated build documentation
### Configuration Files
All configuration files include extensive inline documentation explaining decisions and patterns.
## Usage
### For Production
```bash
npm run build-css # Full optimization
npm run analyze-css # Check results
```
### For Development
```bash
npm run build-css:dev # Fast build with source maps
npm run build-css:watch # Auto-rebuild on changes
```
## Backward Compatibility
**No breaking changes** - All existing styles are preserved
Visual appearance is identical
All Bootstrap components still work
Dynamic classes are safelisted
## Documentation
- **`styles/README.md`** - Build process and troubleshooting
- **`CSS-OPTIMIZATION.md`** - Detailed implementation guide
- **`postcss.config.cjs`** - Inline configuration documentation
## Maintenance
### Adding New Styles
1. Create `_component.scss` file
2. Import in `xrpl.scss`
3. Add dynamic classes to safelist if needed
4. Test: `npm run build-css:dev` and `npm run build-css`
5. Analyze: `npm run analyze-css`
### Troubleshooting Missing Styles
If styles are missing in production:
1. Check if class is added dynamically
2. Add pattern to safelist in `postcss.config.cjs`
3. Rebuild with `npm run build-css`
## Next Steps (Optional Future Optimizations)
1. **Code Splitting** - Separate vendor CSS from custom styles
2. **Critical CSS** - Extract above-the-fold styles
3. **Bootstrap Customization** - Import only needed components
4. **CSS Modules** - Component-scoped styles for React pages
## Conclusion
The CSS optimization is complete and working perfectly. The bundle size has been reduced by 42% (uncompressed) and 39% (gzipped), resulting in faster page loads and dramatically improved developer experience.
**Status: ✅ Ready for Production**
---
*Last Updated: October 2025*

381
CSS-OPTIMIZATION.md Normal file
View File

@@ -0,0 +1,381 @@
# CSS Optimization Guide
## Overview
This document describes the CSS optimization implementation for the XRPL Dev Portal, including the rationale, implementation details, performance improvements, and maintenance guidelines.
## The Problem
### Before Optimization
The dev portal was serving a **486 KB** minified CSS bundle that included:
- **Entire Bootstrap 5.3.8 framework** (~200+ KB)
- Thousands of unused CSS selectors
- No tree-shaking or dead code elimination
- All styles loaded on every page, regardless of usage
- **1-minute lag** in Chrome DevTools when filtering CSS
#### Impact
- **Developer Experience:** DevTools filter took 60+ seconds to respond
- **Page Performance:** 486 KB CSS downloaded on every page load
- **Build Process:** Outdated Sass 1.26.10 (from 2020)
- **Debugging:** No source maps, even in development
### Analysis Results
Initial analysis showed:
```
Bundle Size: 486.64 KB
Total Selectors: 5,423
Unique Selectors: 4,678
Bootstrap Component Usage:
- Pagination: 998 usages
- Cards: 428 usages
- Grid System: 253 usages
- ...but also...
- Toast: 8 usages
- Spinner: 8 usages
- Accordion: 0 usages (unused!)
```
## The Solution
### Modern Build Pipeline
Implemented a three-stage optimization pipeline:
```
SCSS → Sass Compiler → PostCSS → Optimized CSS
├─ PurgeCSS (removes unused)
├─ Autoprefixer (adds vendor prefixes)
└─ cssnano (minifies)
```
### Key Technologies
1. **Sass (latest)** - Modern SCSS compilation with better performance
2. **PostCSS** - Industry-standard CSS processing
3. **PurgeCSS** - Intelligent unused CSS removal
4. **Autoprefixer** - Browser compatibility
5. **cssnano** - Advanced minification
## Implementation
### 1. Dependency Upgrades
```json
{
"devDependencies": {
"sass": "^1.93.2", // was 1.26.10
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1",
"@fullhuman/postcss-purgecss": "^7.0.2",
"autoprefixer": "^10.4.21",
"cssnano": "^7.1.1"
}
}
```
### 2. Build Scripts
Created separate development and production builds:
```json
{
"scripts": {
"build-css": "Production build with full optimization",
"build-css:dev": "Development build with source maps",
"build-css:watch": "Watch mode for continuous compilation",
"analyze-css": "node scripts/analyze-css.js"
}
}
```
**Production Build:**
- ✅ Full PurgeCSS optimization
- ✅ Minified and compressed
- ✅ Autoprefixed
- ❌ No source maps
**Development Build:**
- ✅ Source maps for debugging
- ✅ Autoprefixed
- ❌ No PurgeCSS (faster builds)
- ❌ Not minified (readable)
### 3. PurgeCSS Configuration
Created `postcss.config.cjs` with intelligent safelist:
```javascript
// Content paths - scan these for class names
content: [
'./**/*.tsx',
'./**/*.md',
'./**/*.yaml',
'./**/*.html',
'./static/js/**/*.js',
]
// Safelist - preserve these classes
safelist: {
standard: [
'html', 'body', 'light', 'dark',
/^show$/, /^active$/, /^disabled$/,
],
deep: [
/dropdown-menu/, /modal-backdrop/,
/cm-/, /CodeMirror/, // Third-party
/rpc-tool/, /websocket/, // Custom components
],
}
```
**Safelist Strategy:**
- **Standard:** State classes added by JavaScript
- **Deep:** Component patterns (keeps parent and children)
- **Greedy:** Attribute-based matching
### 4. Analysis Tool
Created `scripts/analyze-css.js` to track optimization:
- Bundle size metrics
- Selector counts
- Bootstrap component usage
- Custom pattern detection
- Optimization recommendations
## Results
### Performance Improvements
| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| **Bundle Size (Uncompressed)** | 486.64 KB | 280.92 KB | **42% smaller** |
| **Bundle Size (Gzipped)** | 71.14 KB | 43.32 KB | **39% smaller** |
| **Total Selectors** | 5,423 | 2,681 | **51% fewer** |
| **Unique Selectors** | 4,678 | 2,167 | **54% fewer** |
| **DevTools Filter** | ~60 seconds | <1 second | **98% faster** |
| **Download Time (3G)** | ~2.0s | ~1.2s | **40% faster** |
**Note:** Gzipped size is what actually gets transmitted over the network, representing the real-world bandwidth savings.
### Bootstrap Component Optimization
| Component | Before | After | Reduction |
|-----------|--------|-------|-----------|
| Pagination | 998 | 831 | 17% |
| Cards | 428 | 306 | 29% |
| Grid System | 253 | 94 | 63% |
| Badge | 253 | 0 | 100% (unused) |
| Navbar | 171 | 78 | 54% |
| Buttons | 145 | 77 | 47% |
| Forms | 179 | 70 | 61% |
### Developer Experience
**Before:**
```
Build time: 5-10 seconds
DevTools CSS filter: 60 seconds
Debugging: No source maps
```
**After:**
```
Production build: 8-12 seconds
Development build: 3-5 seconds (no PurgeCSS)
DevTools CSS filter: <1 second
Debugging: Source maps in dev mode
```
## Maintenance
### Adding New Styles
When adding new component styles:
1. **Create the SCSS file:**
```scss
// styles/_my-component.scss
.my-component {
// styles here
}
```
2. **Import in xrpl.scss:**
```scss
@import "_my-component.scss";
```
3. **If using dynamic classes, update safelist:**
```javascript
// postcss.config.cjs
deep: [
/my-component/, // Keeps all .my-component-* classes
]
```
4. **Test both builds:**
```bash
npm run build-css:dev # Test development build
npm run build-css # Test production build
npm run analyze-css # Check bundle size impact
```
### Troubleshooting Missing Styles
If styles are missing after a production build:
1. **Identify the missing class:**
```bash
# Search for class usage in codebase
grep -r "missing-class" .
```
2. **Check if it's dynamically added:**
- Bootstrap JavaScript components
- React state-based classes
- Third-party library classes
3. **Add to PurgeCSS safelist:**
```javascript
// postcss.config.cjs
safelist: {
deep: [
/missing-class/, // Preserve this pattern
],
}
```
4. **Rebuild and verify:**
```bash
npm run build-css
npm run analyze-css
```
### Monitoring Bundle Size
Run the analysis tool regularly:
```bash
npm run analyze-css
```
**Watch for:**
- Bundle size > 350 KB (indicates regression)
- Components with 0 usages (can be removed from Bootstrap import)
- Significant selector count increases
### Future Optimizations
Potential next steps for further optimization:
1. **Code Splitting**
- Split vendor CSS (Bootstrap) from custom styles
- Lazy-load page-specific styles
- Critical CSS extraction
2. **Bootstrap Customization**
- Import only needed Bootstrap components
- Remove unused variables and mixins
- Custom Bootstrap build
3. **Component-Level CSS**
- CSS Modules for page components
- CSS-in-JS for dynamic styles
- Scoped styles per route
4. **Advanced Compression**
- Brotli compression (88% ratio vs 76% gzip)
- CSS splitting by media queries
- HTTP/2 server push for critical CSS
## Migration Notes
### Breaking Changes
**None** - This optimization is backward-compatible. All existing classes and styles are preserved.
### Testing Checklist
When testing the optimization:
- [ ] Homepage loads correctly
- [ ] Documentation pages display properly
- [ ] Blog posts render correctly
- [ ] Dev tools (RPC tool, WebSocket tool) function
- [ ] Navigation menus work
- [ ] Dropdowns and modals open correctly
- [ ] Forms are styled properly
- [ ] Code syntax highlighting works
- [ ] Print styles work
- [ ] Light/dark theme switching works
### Rollback Procedure
If issues are found:
1. **Temporarily revert to old build:**
```bash
# In package.json, change build-css to:
"build-css": "sass --load-path styles/scss styles/xrpl.scss ./static/css/devportal2024-v1.css --style compressed --no-source-map"
```
2. **Rebuild:**
```bash
npm run build-css
```
3. **Report the issue** with:
- Missing class names
- Page where issue appears
- Expected vs actual behavior
## Resources
### Documentation
- [PurgeCSS Documentation](https://purgecss.com/)
- [PostCSS Documentation](https://postcss.org/)
- [Sass Documentation](https://sass-lang.com/)
- [Bootstrap Customization](https://getbootstrap.com/docs/5.3/customize/sass/)
### Tools
- `npm run build-css` - Production build
- `npm run build-css:dev` - Development build
- `npm run build-css:watch` - Watch mode
- `npm run analyze-css` - Bundle analysis
### Files
- `styles/README.md` - Build process documentation
- `postcss.config.cjs` - PostCSS and PurgeCSS configuration
- `scripts/analyze-css.js` - Bundle analysis tool
- `package.json` - Build scripts
## Conclusion
This optimization reduces the CSS bundle by 42% (486 KB 281 KB), dramatically improving both developer experience and end-user performance. The implementation uses industry-standard tools and maintains full backward compatibility while providing a foundation for future optimizations.
**Key Takeaways:**
- 42% smaller uncompressed CSS bundle (486 KB 281 KB)
- 39% smaller gzipped bundle (71 KB 43 KB network transfer)
- 98% faster DevTools filtering (60s <1s)
- Modern build tooling (Sass + PostCSS + PurgeCSS)
- Source maps in development mode
- Backward compatible - no breaking changes
- Well documented and maintainable
---
*Last updated: October 2025*
*Contributors: CSS Optimization Initiative*

View File

@@ -92,7 +92,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="send-xrp-modal-label">Send XRP</h1>
<h1 class="modal-title subhead-sm-r" id="send-xrp-modal-label">Send XRP</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">

View File

@@ -92,7 +92,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="send-xrp-modal-label">Send XRP</h1>
<h1 class="modal-title subhead-sm-r" id="send-xrp-modal-label">Send XRP</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">

View File

@@ -24,14 +24,6 @@ export default function History() {
return (
<div className="landing">
<div className="overflow-hidden">
<div className="position-relative">
<img
alt="background orange waves"
src={require("../static/img/backgrounds/history-orange.svg")}
className="landing-bg"
id="history-orange"
/>
</div>
<section className="py-26 text-center">
<div className="col-lg-5 mx-auto text-center">
<div className="d-flex flex-column-reverse">
@@ -61,13 +53,6 @@ export default function History() {
</p>
</div>
</section>
<div className="position-relative d-none-sm">
<img
alt="background purple waves"
src={require("../static/img/backgrounds/history-purple.svg")}
id="history-purple"
/>
</div>
<div className="container-new marketing-wrapper">
<section className="row mb-60">
<div className="timeline">

View File

@@ -32,14 +32,6 @@ export default function Impact() {
return (
<div className="landing page-impact">
<div className="overflow-hidden">
<div className="position-relative d-none-sm">
<img
alt="purple waves"
src={require("../static/img/backgrounds/community-purple.svg")}
className="landing-bg"
id="impact-purple"
/>
</div>
<section className="container-new py-26 text-lg-center">
<div className="p-0 col-lg-8 mx-lg-auto">
<div className="d-flex flex-column-reverse">
@@ -52,13 +44,6 @@ export default function Impact() {
</div>
</div>
</section>
<div className="position-relative d-none-sm">
<img
alt="green waves"
src={require("../static/img/backgrounds/home-green.svg")}
id="impact-green"
/>
</div>
{/* World map */}
<section className="container-new py-10">
<div className="col-sm-10 col-lg-6 offset-md-3 p-10-until-sm pl-0-sm pr-0-sm">
@@ -133,16 +118,6 @@ export default function Impact() {
{/* Card */}
<section className="container-new py-26">
<div className="col-md-6 offset-md-3 p-6-sm p-10-until-sm br-8 cta-card">
<img
alt="purple waves"
src={require("../static/img/backgrounds/cta-community-purple.svg")}
className="cta cta-top-left"
/>
<img
alt="green waves"
src={require("../static/img/backgrounds/cta-calculator-green.svg")}
className="cta cta-bottom-right"
/>
<div className="z-index-1 position-relative">
<div className="d-flex flex-column-reverse">
<h2 className="h4 h2-sm mb-10-until-sm mb-8-sm">

View File

@@ -1,6 +1,7 @@
import * as React from "react";
import { useThemeHooks } from '@redocly/theme/core/hooks';
import { Link } from '@redocly/theme/components/Link/Link';
import { PageGrid, PageGridCol, PageGridRow } from "shared/components/PageGrid/page-grid";
export const frontmatter = {
seo: {
@@ -78,14 +79,6 @@ export default function XrplOverview() {
/>
</div>
</div>
<div className="position-relative">
<img
alt="purple waves"
src={require("../static/img/backgrounds/xrpl-overview-purple.svg")}
className="landing-bg"
id="xrpl-overview-purple"
/>
</div>
<section className="py-26 text-center">
<div className="col-lg-5 mx-auto text-center">
<div className="d-flex flex-column-reverse">
@@ -100,13 +93,6 @@ export default function XrplOverview() {
</div>
</div>
</section>
<div className="position-relative d-none-sm">
<img
alt="orange waves"
src={require("../static/img/backgrounds/xrpl-overview-orange.svg")}
id="xrpl-overview-orange"
/>
</div>
<section className="container-new py-26">
<div className="card-grid card-grid-2xN">
<div className="col">
@@ -133,7 +119,7 @@ export default function XrplOverview() {
{translate("Read Technical Docs")}
</Link>{" "}
<a
className="ml-4 video-external-link"
className="ms-4 video-external-link"
target="_blank"
href="https://www.youtube.com/playlist?list=PLJQ55Tj1hIVZtJ_JdTvSum2qMTsedWkNi"
>
@@ -168,7 +154,7 @@ export default function XrplOverview() {
{translate("Read Technical Docs")}
</Link>{" "}
<a
className="ml-4 video-external-link"
className="ms-4 video-external-link"
target="_blank"
href="https://www.youtube.com/playlist?list=PLJQ55Tj1hIVZtJ_JdTvSum2qMTsedWkNi"
>
@@ -178,9 +164,9 @@ export default function XrplOverview() {
</div>
</div>
</section>
<section className="container-new py-26">
<div className="card-grid card-grid-2xN">
<div className="col">
<PageGrid className="py-26">
<PageGridRow>
<PageGrid.Col span={{ base: 4, lg: 6 }}>
<div className="d-flex flex-column-reverse">
<h2 className="h4 h2-sm mb-8">
{translate("How the Consensus Protocol works")}
@@ -207,25 +193,23 @@ export default function XrplOverview() {
<p className="mb-0">
{translate('about.index.consensus.ppart1', 'Currently, over 120 ')}
<a href="https://livenet.xrpl.org/network/validators" target="_blank">{translate('about.index.consensus.ppart2', 'validators')}</a>
{translate('about.index.consensus.ppart3', ' are active on the ledger, operated by universities, exchanges, businesses, and individuals. As the validator pool grows, the consensus protocol ensures decentralization of the blockchain over time.')}
{translate('about.index.consensus.ppart3', ' are active on the ledger, operated by universities, exchanges, businesses, and individuals. As the validator pool grows, the consensus protocol ensures decentralization of the blockchain over time.')}
</p>
</div>
<div className="col mb-16-sm">
<img
className="mw-100"
id="validator-graphic"
alt="(Graphic: Validators in Consensus)"
/>
</div>
</div>
</section>
<section className="container-new py-26">
<div className="col-md-6 offset-md-3 p-6-sm p-10-until-sm br-8 cta-card">
<img
alt="green waves"
src={require("../static/img/backgrounds/cta-xrpl-overview-green.svg")}
className="cta cta-bottom-right"
/>
</PageGrid.Col>
<PageGrid.Col span={{ base: 4, lg: 6 }}>
<div className="col mb-16-sm">
<img
className="mw-100"
id="validator-graphic"
alt="(Graphic: Validators in Consensus)"
/>
</div>
</PageGrid.Col>
</PageGridRow>
</PageGrid>
<PageGrid className="py-26">
<PageGridRow>
<PageGrid.Col span={{ base: 4, lg: 6 }} offset={{ lg: 3 }} className="p-6-sm p-10-until-sm br-8 cta-card">
<div className="z-index-1 position-relative">
<h2 className="h4 mb-10-until-sm mb-8-sm">
{translate("A Sustainable Blockchain")}
@@ -239,11 +223,13 @@ export default function XrplOverview() {
{translate("Learn More")}
</a>
</div>
</div>
</section>
<section className="container-new py-26">
<div className="card-grid card-grid-2xN">
<div className="col">
</PageGrid.Col>
</PageGridRow>
</PageGrid>
<PageGrid className="py-26">
<PageGridRow>
<PageGrid.Col span={{ base: 4, lg: 6 }}>
<div className="d-flex flex-column-reverse">
<h4 className="h4 h2-sm mb-8">
{translate("Building with confidence on ")}
@@ -265,8 +251,8 @@ export default function XrplOverview() {
<a className="btn btn-primary btn-arrow mb-10-sm" href="/about/uses">
{translate("Explore More")}
</a>
</div>
<div className="col mb-0">
</PageGrid.Col>
<PageGrid.Col span={{ base: 4, lg: 6 }}>
<div className="d-flex flex-column-reverse">
<h4 className="h4 h2-sm mb-8">
{translate("Creating new value for long-term growth")}
@@ -283,11 +269,11 @@ export default function XrplOverview() {
"Significant investment in development, along with low transaction costs and energy usage, is fueling growth and opening up a wide variety of use cases at scale."
)}
</p>
</div>
</div>
</section>
</PageGrid.Col>
</PageGridRow>
</PageGrid>
<section className="container-new py-26">
<div className="d-flex flex-column-reverse col-xl-6 mb-lg-4 pl-0 ">
<div className="d-flex flex-column-reverse col-xl-6 mb-lg-4 ps-0 ">
<h2 className="h4 h2-sm">
{translate(
"Watch the explainer video series to learn more about the XRP Ledger"
@@ -375,11 +361,6 @@ export default function XrplOverview() {
</section>
<section className="container-new py-26">
<div className="col-md-6 offset-md-3 p-6-sm p-10-until-sm br-8 cta-card">
<img
alt="orange waves"
src={require("../static/img/backgrounds/cta-xrpl-overview-orange.svg")}
className="cta cta-bottom-right"
/>
<div className="z-index-1 position-relative">
<h4 className="h4 mb-10-until-sm mb-8-sm">
{translate("Tomorrows Blockchain Starts With You")}
@@ -407,7 +388,7 @@ export default function XrplOverview() {
</section>
<section className="container-new py-26">
<div
className="col-md-6 offset-md-3 w-100 pl-0 pr-0 mini-faq"
className="col-md-10 offset-md-1 col-lg-8 offset-lg-2 ps-0 pe-0 mini-faq"
id="minifaq-accordion"
>
{faqs.map((faq, index) => (
@@ -415,8 +396,8 @@ export default function XrplOverview() {
<a
href={`#heading${index + 1}`}
className="expander collapsed"
data-toggle="collapse"
data-target={`#answer${index + 1}`}
data-bs-toggle="collapse"
data-bs-target={`#answer${index + 1}`}
aria-expanded="false"
aria-controls={`answer${index + 1}`}
>

View File

@@ -850,17 +850,17 @@ export default function Uses() {
</div>
<a
className="btn d-block d-lg-none"
data-toggle="modal"
data-target="#categoryFilterModal"
data-bs-toggle="modal"
data-bs-target="#categoryFilterModal"
>
<span className="mr-3">
<span className="me-3">
<img
src={require("../static/img/uses/usecase-filter.svg")}
alt="Filter button"
/>
</span>
{translate("Filter by Categories")}
<span className="ml-3 total_count category_count">2</span>
<span className="ms-3 total_count category_count">2</span>
</a>
{/* Start company cards */}
<div className="row col-12 m-0 p-0 mt-4 pt-2">

View File

@@ -116,400 +116,380 @@ export default function XrpOverview() {
const totalCols = Math.max(softwallets.length, hardwallets.length) + 1;
return (
<div className="landing">
<div>
<div className="position-relative">
<img
alt="blue waves"
src={require("../static/img/backgrounds/xrp-overview-blue.svg")}
className="landing-bg"
id="xrp-overview-blue"
/>
</div>
<section className="py-26 text-center">
<div className="col-lg-5 mx-auto text-center">
<div className="d-flex flex-column-reverse">
<h1 className="mb-0">
{translate("Your Questions About XRP, Answered")}
</h1>
<h6 className="eyebrow mb-3">{translate("XRP Overview")}</h6>
</div>
<section className="py-26 text-center">
<div className="col-lg-5 mx-auto text-center">
<div className="d-flex flex-column-reverse">
<h1 className="mb-0">
{translate("Your Questions About XRP, Answered")}
</h1>
<h6 className="eyebrow mb-3">{translate("XRP Overview")}</h6>
</div>
</section>
<section className="container-new my-20">
<div className="card-grid card-grid-1x2">
<div className="d-none-sm mt-lg-0">
<ul className="page-toc no-sideline p-0 sticky-top floating-nav">
{links.map((link) => (
<li
key={link.hash}
className={`nav-item ${
</div>
</section>
<section className="container-new my-20">
<div className="card-grid card-grid-1x2">
<div className="d-none-sm mt-lg-0">
<ul className="page-toc no-sideline p-0 sticky-top floating-nav">
{links.map((link) => (
<li
key={link.hash}
className={`nav-item ${
activeSection === link.hash.substring(1) ? "active" : ""
}`}
>
<a
className={`sidelinks nav-link ${
activeSection === link.hash.substring(1) ? "active" : ""
}`}
href={link.hash}
>
{translate(link.text)}
</a>
</li>
))}
</ul>
</div>
<div className="col mt-lg-0">
<div className="link-section pb-26" id="about-xrp">
<h2 className="h4 h2-sm mb-8">{translate("What Is XRP?")}</h2>
<h5 className="longform mb-10">
{translate(
"about.xrp.what-is-xrp.ppart1",
"XRP is a digital asset thats native to the XRP Ledger—an open-source, permissionless and decentralized ",
)}
<a
href="https://www.distributedagreement.com/2018/09/24/what-is-a-blockchain/"
target="_blank"
rel="noopener noreferrer"
>
{translate("about.xrp.what-is-xrp.ppart2", "blockchain technology.")}
</a>
{translate("about.xrp.what-is-xrp.ppart3", " ")}
</h5>
<p className="mb-6">
{translate(
"Created in 2012 specifically for payments, XRP can settle transactions on the ledger in 3-5 seconds. It was built to be a better Bitcoin—faster, cheaper and greener than any other digital asset."
)}
</p>
<div className="overflow-x-xs">
<table className="mb-10 landing-table">
<thead>
<tr>
<th>
<h6>{translate("Benefits")}</h6>
</th>
<th>
<h6>{translate("XRP")}</h6>
</th>
<th>
<h6>{translate("Bitcoin")}</h6>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>{translate("Fast")}</td>
<td>{translate("3-5 seconds to settle")}</td>
<td>{translate("500 seconds to settle")}</td>
</tr>
<tr>
<td>{translate("Low-Cost")}</td>
<td>{translate("$0.0002/tx")}</td>
<td>{translate("$0.50/tx")}</td>
</tr>
<tr>
<td>{translate("Scalable")}</td>
<td>{translate("1,500 tx per second")}</td>
<td>{translate("3 tx per second")}</td>
</tr>
<tr>
<td>{translate("Sustainable")}</td>
<td>
{translate(
"Environmentally sustainable (negligible energy consumption)"
)}
</td>
<td>
{translate("0.3% of global energy consumption")}
</td>
</tr>
</tbody>
</table>
</div>
<p className="mb-10">
{translate(
"XRP can be sent directly without needing a central intermediary, making it a convenient instrument in bridging two different currencies quickly and efficiently. It is freely exchanged on the open market and used in the real world for enabling cross-border payments and microtransactions."
)}
</p>
<div className="card-grid card-grid-2xN mb-10">
<div>
<img
alt="briefcase"
className="mw-100 mb-2 invertible-img"
src={briefcaseIcon}
/>
<h6 className="subhead-sm-r">
{translate("Financial Institutions")}
</h6>
<p className="">
{translate(
"Leverage XRP as a bridge currency to facilitate faster, more affordable cross-border payments around the world."
)}
</p>
</div>
<div>
<img
alt="user"
className="mw-100 mb-2 invertible-img"
src={userIcon}
/>
<h6 className="subhead-sm-r">
{translate("Individual Consumers")}
</h6>
<p>
{translate(
"Use XRP to move different currencies around the world."
)}
</p>
</div>
</div>
<div className="mt-10 p-10 br-8 cta-card position-relative">
<div className="z-index-1 position-relative">
<h2 className="h4 mb-10-until-sm mb-8-sm">
{translate(
"The XRP Ledger is built for business."
)}
</h2>
<p className="mb-10">
{translate(
"The only major L-1 blockchain thats built for business and designed specifically to power finance use cases and applications at scale. Powerful enough to bootstrap a new economy, the XRP Ledger (XRPL) is fast, scalable, and sustainable."
)}
</p>
</div>
</div>
</div>
<div className="py-26 link-section" id="xrp-trading">
<h2 className="h4 h2-sm mb-8">
{translate("How Is XRP Used in Trading?")}
</h2>
<h5 className="longform mb-10">
{translate(
"XRP is traded on more than 100 markets and exchanges worldwide."
)}
</h5>
<p className="mb-6">
{translate(
"about.xrp.xrp-in-trading.ppart1",
"XRPs low transaction fees, reliability and high-speed enable traders to use the digital asset as high-speed, cost-efficient and reliable collateral across trading venues—"
)}
<a
href="https://ripple.com/insights/xrp-a-preferred-base-currency-for-arbitrage-trading/"
target="_blank"
>
{translate("about.xrp.xrp-in-trading.ppart2","seizing arbitrage opportunities")}
</a>
{translate(
"about.xrp.xrp-in-trading.ppart3",
", servicing margin calls and managing general trading inventory in real time."
)}
</p>
<p>
{translate(
"Because of the properties inherent to XRP and the ecosystem around it, traders worldwide are able to shift collateral, bridge currencies and switch from one crypto into another nearly instantly, across any exchange on the planet."
)}
</p>
</div>
<div className="py-26 link-section" id="ripple">
<h2 className="h4 h2-sm mb-8">
{translate(
"What Is the Relationship Between Ripple and XRP?"
)}
</h2>
<h5 className="longform mb-10">
<a href="https://ripple.com" target="_blank">
{translate("Ripple")}
</a>
{translate(
" is a technology company that makes it easier to build a high-performance, global payments business. XRP is a digital asset independent of this."
)}
</h5>
<p>
{translate(
"There is a finite amount of XRP. All XRP is already in existence today—no more than the original 100 billion can be created. The XRPL founders gifted 80 billion XRP, the platforms native currency, to Ripple. To provide predictability to the XRP supply, Ripple has locked 55 billion XRP (55% of the total possible supply) into a series of escrows using the XRP Ledger itself. The XRPL's transaction processing rules, enforced by the consensus protocol, control the release of the XRP."
)}
</p>
<div className="mt-10 p-10 br-8 cta-card position-relative">
<div className="z-index-1 position-relative">
<h3 className="h4">
{translate("about.xrp.ripple-escrow.ppart1","As of ")}
<span className="stat-highlight" id="ripple-escrow-as-of">
{translate("about.xrp.ripple-escrow.ppart2","October 2024")}
</span>
{translate("about.xrp.ripple-escrow.ppart3"," ")}
<br />
<span className="d-inline-flex">
<img
id="xrp-mark-overview"
className="mw-100 invertible-img me-2"
src={require("../static/img/logos/xrp-mark.svg")}
alt="XRP Logo Mark"
/>
<span
className="numbers stat-highlight"
id="ripple-escrow-amount"
>
{translate("38B")}
</span>
</span>
<br />
{translate("XRP remains in escrow")}
</h3>
</div>
</div>
</div>
<div className="link-section py-26" id="wallets">
<h2 className="h4 h2-sm mb-8">
{translate("What Wallets Support XRP?")}
</h2>
<h5 className="longform mb-10">
{translate(
"Digital wallets are pieces of software that allow people to send, receive, and store cryptocurrencies, including XRP. There are two types of digital wallets: hardware and software."
)}
</h5>
<ul className={`nav nav-grid-lg cols-of-${totalCols}`} id="wallets">
<li className="nav-item nav-grid-head">
<h6 className="subhead-sm-r">{translate("Software Wallets")}</h6>
</li>
{softwallets.map((wallet) => (
<li key={wallet.id} className="nav-item">
<a
className={`sidelinks nav-link ${
activeSection === link.hash.substring(1) ? "active" : ""
}`}
href={link.hash}
className="nav-link external-link"
href={wallet.href}
target="_blank"
>
{translate(link.text)}
<img
className={`mw-100 ${
!!wallet?.imgclasses && wallet.imgclasses
}`}
id={wallet.id}
alt={wallet.alt}
/>
</a>
</li>
))}
<li className="nav-item nav-grid-head">
<h6 className="subhead-sm-r">{translate("Hardware Wallets")}</h6>
</li>
{hardwallets.map((wallet) => (
<li className="nav-item" key={wallet.id}>
<a
className="nav-link external-link"
href={wallet.href}
target="_blank"
>
<img
className={`mw-100 ${
!!wallet.imgclasses && wallet.imgclasses
}`}
id={wallet.id}
alt={wallet.alt}
/>
</a>
</li>
))}
</ul>
<p className="label-l mt-10">
{translate(
"Disclaimer: This information is drawn from other sources on the internet. XRPL.org does not endorse or recommend any exchanges or make any representations with respect to exchanges or the purchase or sale of digital assets more generally. Its advisable to conduct your own due diligence before relying on any third party or third-party technology, and providers may vary significantly in their compliance, data security, and privacy practices."
)}
</p>
</div>
<div className="col mt-lg-0">
<div className="link-section pb-26" id="about-xrp">
<h2 className="h4 h2-sm mb-8">{translate("What Is XRP?")}</h2>
<h5 className="longform mb-10">
{translate(
"about.xrp.what-is-xrp.ppart1",
"XRP is a digital asset thats native to the XRP Ledger—an open-source, permissionless and decentralized ",
)}
<a
href="https://www.distributedagreement.com/2018/09/24/what-is-a-blockchain/"
target="_blank"
rel="noopener noreferrer"
>
{translate("about.xrp.what-is-xrp.ppart2", "blockchain technology.")}
</a>
{translate("about.xrp.what-is-xrp.ppart3", " ")}
</h5>
<p className="mb-6">
{translate(
"Created in 2012 specifically for payments, XRP can settle transactions on the ledger in 3-5 seconds. It was built to be a better Bitcoin—faster, cheaper and greener than any other digital asset."
)}
</p>
<div className="overflow-x-xs">
<table className="mb-10 landing-table">
<thead>
<tr>
<th>
<h6>{translate("Benefits")}</h6>
</th>
<th>
<h6>{translate("XRP")}</h6>
</th>
<th>
<h6>{translate("Bitcoin")}</h6>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>{translate("Fast")}</td>
<td>{translate("3-5 seconds to settle")}</td>
<td>{translate("500 seconds to settle")}</td>
</tr>
<tr>
<td>{translate("Low-Cost")}</td>
<td>{translate("$0.0002/tx")}</td>
<td>{translate("$0.50/tx")}</td>
</tr>
<tr>
<td>{translate("Scalable")}</td>
<td>{translate("1,500 tx per second")}</td>
<td>{translate("3 tx per second")}</td>
</tr>
<tr>
<td>{translate("Sustainable")}</td>
<td>
{translate(
"Environmentally sustainable (negligible energy consumption)"
)}
</td>
<td>
{translate("0.3% of global energy consumption")}
</td>
</tr>
</tbody>
</table>
<div className="py-26 link-section" id="exchanges">
<h2 className="h4 h2-sm mb-8">
{translate("What Exchanges Support XRP?")}
</h2>
<h5 className="longform mb-10">
{translate(
"Exchanges are where people trade currencies. XRP is traded on more than 100 markets and exchanges worldwide."
)}
</h5>
<p className="mb-10">
{translate(
"There are different types of exchanges that vary depending on the type of market (spot, futures, options, swaps), and the type of security model (custodial, non-custodial)."
)}
</p>
<div className="card-grid card-grid-2xN mb-10">
<div>
<h6 className="subhead-sm-r">{translate("Spot Exchanges")}</h6>
<p className="mb-0">
{translate(
"Spot exchanges allow people to buy and sell cryptocurrencies at current (spot) market rates."
)}
</p>
</div>
<p className="mb-10">
{translate(
"XRP can be sent directly without needing a central intermediary, making it a convenient instrument in bridging two different currencies quickly and efficiently. It is freely exchanged on the open market and used in the real world for enabling cross-border payments and microtransactions."
)}
</p>
<div className="card-grid card-grid-2xN mb-10">
<div>
<img
alt="briefcase"
className="mw-100 mb-2 invertible-img"
src={briefcaseIcon}
/>
<h6 className="fs-4-5">
{translate("Financial Institutions")}
</h6>
<p className="">
{translate(
"Leverage XRP as a bridge currency to facilitate faster, more affordable cross-border payments around the world."
)}
</p>
</div>
<div>
<img
alt="user"
className="mw-100 mb-2 invertible-img"
src={userIcon}
/>
<h6 className="fs-4-5">
{translate("Individual Consumers")}
</h6>
<p>
{translate(
"Use XRP to move different currencies around the world."
)}
</p>
</div>
<div>
<h6 className="subhead-sm-r">
{translate("Futures, Options and Swap Exchanges")}
</h6>
<p className="mb-0">
{translate(
"Futures, options and swap exchanges allow people to buy and sell standardized contracts of cryptocurrency market rates in the future."
)}
</p>
</div>
<div className="mt-10 p-10 br-8 cta-card position-relative">
<img
alt="magenta waves"
src={require("../static/img/backgrounds/cta-xrp-overview-magenta.svg")}
className="cta cta-bottom-right"
/>
<div className="z-index-1 position-relative">
<h2 className="h4 mb-10-until-sm mb-8-sm">
{translate(
"The XRP Ledger is built for business."
)}
</h2>
<p className="mb-10">
{translate(
"The only major L-1 blockchain thats built for business and designed specifically to power finance use cases and applications at scale. Powerful enough to bootstrap a new economy, the XRP Ledger (XRPL) is fast, scalable, and sustainable."
)}
</p>
</div>
<div>
<h6 className="subhead-sm-r">
{translate("Custodial Exchanges")}
</h6>
<p className="mb-0">
{translate(
"Custodial exchanges manage a users private keys, and publish centralized order books of buyers and sellers."
)}
</p>
</div>
<div>
<h6 className="subhead-sm-r">
{translate("Non-Custodial Exchanges")}
</h6>
<p className="mb-0">
{translate(
"Non-custodial exchanges, also known as decentralized exchanges, do not manage a users private keys, and publish decentralized order books of buyers and sellers on a blockchain."
)}
</p>
</div>
</div>
<div className="py-26 link-section" id="xrp-trading">
<h2 className="h4 h2-sm mb-8">
{translate("How Is XRP Used in Trading?")}
</h2>
<h5 className="longform mb-10">
{translate(
"XRP is traded on more than 100 markets and exchanges worldwide."
)}
</h5>
<p className="mb-6">
{translate(
"about.xrp.xrp-in-trading.ppart1",
"XRPs low transaction fees, reliability and high-speed enable traders to use the digital asset as high-speed, cost-efficient and reliable collateral across trading venues—"
)}
<a
href="https://ripple.com/insights/xrp-a-preferred-base-currency-for-arbitrage-trading/"
target="_blank"
>
{translate("about.xrp.xrp-in-trading.ppart2","seizing arbitrage opportunities")}
</a>
{translate(
"about.xrp.xrp-in-trading.ppart3",
", servicing margin calls and managing general trading inventory in real time."
)}
</p>
<p>
{translate(
"Because of the properties inherent to XRP and the ecosystem around it, traders worldwide are able to shift collateral, bridge currencies and switch from one crypto into another nearly instantly, across any exchange on the planet."
)}
</p>
</div>
<div className="py-26 link-section" id="ripple">
<h2 className="h4 h2-sm mb-8">
{translate(
"What Is the Relationship Between Ripple and XRP?"
)}
</h2>
<h5 className="longform mb-10">
<a href="https://ripple.com" target="_blank">
{translate("Ripple")}
</a>
{translate(
" is a technology company that makes it easier to build a high-performance, global payments business. XRP is a digital asset independent of this."
)}
</h5>
<p>
{translate(
"There is a finite amount of XRP. All XRP is already in existence today—no more than the original 100 billion can be created. The XRPL founders gifted 80 billion XRP, the platforms native currency, to Ripple. To provide predictability to the XRP supply, Ripple has locked 55 billion XRP (55% of the total possible supply) into a series of escrows using the XRP Ledger itself. The XRPL's transaction processing rules, enforced by the consensus protocol, control the release of the XRP."
)}
</p>
<div className="mt-10 p-10 br-8 cta-card position-relative">
<img
alt="green waves"
src={require("../static/img/backgrounds/cta-xrp-overview-green-2.svg")}
className="landing-bg cta cta-bottom-right"
/>
<div className="z-index-1 position-relative">
<h3 className="h4">
{translate("about.xrp.ripple-escrow.ppart1","As of ")}
<span className="stat-highlight" id="ripple-escrow-as-of">
{translate("about.xrp.ripple-escrow.ppart2","October 2024")}
</span>
{translate("about.xrp.ripple-escrow.ppart3"," ")}
<br />
<span className="d-inline-flex">
<img
id="xrp-mark-overview"
className="mw-100 invertible-img mr-2"
src={require("../static/img/logos/xrp-mark.svg")}
alt="XRP Logo Mark"
/>
<span
className="numbers stat-highlight"
id="ripple-escrow-amount"
>
{translate("38B")}
</span>
</span>
<br />
{translate("XRP remains in escrow")}
</h3>
</div>
</div>
</div>
<div className="link-section py-26" id="wallets">
<h2 className="h4 h2-sm mb-8">
{translate("What Wallets Support XRP?")}
</h2>
<h5 className="longform mb-10">
{translate(
"Digital wallets are pieces of software that allow people to send, receive, and store cryptocurrencies, including XRP. There are two types of digital wallets: hardware and software."
)}
</h5>
<ul className={`nav nav-grid-lg cols-of-${totalCols}`} id="wallets">
<li className="nav-item nav-grid-head">
<h6 className="fs-4-5">{translate("Software Wallets")}</h6>
<h6>
{translate("Top Exchanges, according to CryptoCompare")}
</h6>
<ul
className="nav nav-grid-lg cols-of-5 mb-10"
id="top-exchanges"
>
{exchanges.map((exch, i) => (
<li className="nav-item" key={exch.id}>
<a
className="nav-link external-link"
href={exch.href}
target="_blank"
>
<span className="longform me-3">{i+1}</span>
<img className="mw-100" id={exch.id} alt={exch.alt} />
</a>
</li>
{softwallets.map((wallet) => (
<li key={wallet.id} className="nav-item">
<a
className="nav-link external-link"
href={wallet.href}
target="_blank"
>
<img
className={`mw-100 ${
!!wallet?.imgclasses && wallet.imgclasses
}`}
id={wallet.id}
alt={wallet.alt}
/>
</a>
</li>
))}
<li className="nav-item nav-grid-head">
<h6 className="fs-4-5">{translate("Hardware Wallets")}</h6>
</li>
{hardwallets.map((wallet) => (
<li className="nav-item" key={wallet.id}>
<a
className="nav-link external-link"
href={wallet.href}
target="_blank"
>
<img
className={`mw-100 ${
!!wallet.imgclasses && wallet.imgclasses
}`}
id={wallet.id}
alt={wallet.alt}
/>
</a>
</li>
))}
</ul>
<p className="fs-3 mt-10">
{translate(
"Disclaimer: This information is drawn from other sources on the internet. XRPL.org does not endorse or recommend any exchanges or make any representations with respect to exchanges or the purchase or sale of digital assets more generally. Its advisable to conduct your own due diligence before relying on any third party or third-party technology, and providers may vary significantly in their compliance, data security, and privacy practices."
)}
</p>
</div>
<div className="py-26 link-section" id="exchanges">
<h2 className="h4 h2-sm mb-8">
{translate("What Exchanges Support XRP?")}
</h2>
<h5 className="longform mb-10">
{translate(
"Exchanges are where people trade currencies. XRP is traded on more than 100 markets and exchanges worldwide."
)}
</h5>
<p className="mb-10">
{translate(
"There are different types of exchanges that vary depending on the type of market (spot, futures, options, swaps), and the type of security model (custodial, non-custodial)."
)}
</p>
<div className="card-grid card-grid-2xN mb-10">
<div>
<h6 className="fs-4-5">{translate("Spot Exchanges")}</h6>
<p className="mb-0">
{translate(
"Spot exchanges allow people to buy and sell cryptocurrencies at current (spot) market rates."
)}
</p>
</div>
<div>
<h6 className="fs-4-5">
{translate("Futures, Options and Swap Exchanges")}
</h6>
<p className="mb-0">
{translate(
"Futures, options and swap exchanges allow people to buy and sell standardized contracts of cryptocurrency market rates in the future."
)}
</p>
</div>
<div>
<h6 className="fs-4-5">
{translate("Custodial Exchanges")}
</h6>
<p className="mb-0">
{translate(
"Custodial exchanges manage a users private keys, and publish centralized order books of buyers and sellers."
)}
</p>
</div>
<div>
<h6 className="fs-4-5">
{translate("Non-Custodial Exchanges")}
</h6>
<p className="mb-0">
{translate(
"Non-custodial exchanges, also known as decentralized exchanges, do not manage a users private keys, and publish decentralized order books of buyers and sellers on a blockchain."
)}
</p>
</div>
</div>
<h6>
{translate("Top Exchanges, according to CryptoCompare")}
</h6>
<ul
className="nav nav-grid-lg cols-of-5 mb-10"
id="top-exchanges"
>
{exchanges.map((exch, i) => (
<li className="nav-item" key={exch.id}>
<a
className="nav-link external-link"
href={exch.href}
target="_blank"
>
<span className="longform mr-3">{i+1}</span>
<img className="mw-100" id={exch.id} alt={exch.alt} />
</a>
</li>
))}
</ul>
<p className="fs-3 mt-10 mb-0">
{translate(
"Disclaimer: This information is drawn from other sources on the internet. XRPL.org does not endorse or recommend any exchanges or make any representations with respect to exchanges or the purchase or sale of digital assets more generally. Its advisable to conduct your own due diligence before relying on any third party or third-party technology, and providers may vary significantly in their compliance, data security, and privacy practices."
)}
</p>
</div>
))}
</ul>
<p className="label-l mt-10 mb-0">
{translate(
"Disclaimer: This information is drawn from other sources on the internet. XRPL.org does not endorse or recommend any exchanges or make any representations with respect to exchanges or the purchase or sale of digital assets more generally. Its advisable to conduct your own due diligence before relying on any third party or third-party technology, and providers may vary significantly in their compliance, data security, and privacy practices."
)}
</p>
</div>
</div>
</section>
</div>
</div>
</section>
</div>
);
}

View File

@@ -57,13 +57,6 @@ export default function Index() {
return (
<div className="landing dev-blog">
<div className="justify-content-center align-items-lg-center">
<div className="position-relative d-none-sm">
<img
alt="background purple waves"
src={require("../static/img/backgrounds/home-purple.svg")}
id="blog-purple"
/>
</div>
<section className="py-lg-5 text-center mt-lg-5">
<div className="mx-auto text-center col-lg-5">
<div className="d-flex flex-column">

2025
bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@ import { useThemeHooks } from '@redocly/theme/core/hooks';
export const frontmatter = {
seo: {
title: 'Ambassadors',
description: "The XRPL Campus Ambassador program connects and empowers student champions of the XRPL.",
title: 'Ambassadors',
description: "The XRPL Campus Ambassador program connects and empowers student champions of the XRPL.",
}
};
@@ -17,409 +17,403 @@ export default function Ambassadors() {
const { translate } = useTranslate();
return (
<div className="landing page-ambassadors">
<div>
<div className="position-relative d-none-sm">
<img alt="background purple waves" src={require("../static/img/backgrounds/ambassador-purple.svg")} className="position-absolute" style={{top: 0, right: 0}} />
</div>
<div className="landing page-ambassadors">
<section className="container-new py-26 text-lg-center">
{/* For translater: This section could change dynamically based on the time of year */}
<div className="p-0 col-lg-8 mx-lg-auto">
<div className="d-flex flex-column-reverse">
<h1 className="mb-0">{translate("Become an XRP Ledger Campus Ambassador")}</h1>
<h6 className="eyebrow mb-3">{translate("Join the Student Cohort")}</h6>
</div>
<p className="mt-3 pt-3 col-lg-8 mx-lg-auto p-0">{translate("This fall, the ")} <b>{translate("XRPL Student Builder Residency ")}</b> {translate("offers top technical students a 3-week online program (Oct 21 - Nov 13) to develop XRPL projects with expert mentorship. Apply by Oct 14, 2024")}</p>
<p className=" col-lg-8 mx-lg-auto p-0">{translate("This program will run from October 21 - November 13 and will be conducted entirely online. ")}</p>
<p className="pb-3 col-lg-8 mx-lg-auto p-0"><b>{translate("Applications due October 14, 2024")}</b>{translate(" @ 11:59pm PDT")}</p>
<button className="btn btn-primary btn-arrow-out" onClick={() => window.open('https://share.hsforms.com/1k47bfuX2SL2DKZtZoJzArg4vgrs', "_blank")} >{translate("Apply for Fall 2024")}</button>
{/* For translater: This section could change dynamically based on the time of year */}
<div className="p-0 col-lg-8 mx-lg-auto">
<div className="d-flex flex-column-reverse">
<h1 className="mb-0">{translate("Become an XRP Ledger Campus Ambassador")}</h1>
<h6 className="eyebrow mb-3">{translate("Join the Student Cohort")}</h6>
</div>
<p className="mt-3 pt-3 col-lg-8 mx-lg-auto p-0">{translate("This fall, the ")} <b>{translate("XRPL Student Builder Residency ")}</b> {translate("offers top technical students a 3-week online program (Oct 21 - Nov 13) to develop XRPL projects with expert mentorship. Apply by Oct 14, 2024")}</p>
<p className=" col-lg-8 mx-lg-auto p-0">{translate("This program will run from October 21 - November 13 and will be conducted entirely online. ")}</p>
<p className="pb-3 col-lg-8 mx-lg-auto p-0"><b>{translate("Applications due October 14, 2024")}</b>{translate(" @ 11:59pm PDT")}</p>
<button className="btn btn-primary btn-arrow-out" onClick={() => window.open('https://share.hsforms.com/1k47bfuX2SL2DKZtZoJzArg4vgrs', "_blank")} >{translate("Apply for Fall 2024")}</button>
</div>
</section>
{/* Current Students */}
<section className="container-new py-26">
<div className="d-flex flex-column flex-lg-row align-items-lg-center">
<div className="order-lg-2 mx-lg-4 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0 pr-lg-5">
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("XRPL Campus Ambassadors")}</h3>
<h6 className="eyebrow mb-3">{translate("Empowering Students")}</h6>
</div>
<p className="p-lg-3 mb-2 longform">{translate("The XRPL Campus Ambassador program aims to elevate the impact of college students who are passionate about blockchain technology.")}</p>
<div className="d-none d-lg-block p-lg-3">
<button className="btn btn-primary btn-arrow-out" onClick={() => window.open('https://share.hsforms.com/1k47bfuX2SL2DKZtZoJzArg4vgrs', "_blank")} >{translate("Apply for Fall 2024")}</button>
</div>
</div>
<div className="order-lg-1 col-lg-6 px-0 mr-lg-4">
<div className="row m-0">
<img alt="Person speaking and person taking photo" src={require("../static/img/ambassadors/developer-hero@2x.png")} className="w-100" />
</div>
</div>
<div className="d-lg-none order-3 mt-4 pt-3 p-lg-3">
<button className="btn btn-primary btn-arrow-out" onClick={() => window.open('https://share.hsforms.com/1k47bfuX2SL2DKZtZoJzArg4vgrs', "_blank")} >{translate("Apply for Fall 2024")}</button>
</div>
</div>
<div className="d-flex flex-column flex-lg-row align-items-lg-center">
<div className="order-lg-2 mx-lg-4 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0 pr-lg-5">
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("XRPL Campus Ambassadors")}</h3>
<h6 className="eyebrow mb-3">{translate("Empowering Students")}</h6>
</div>
<p className="p-lg-3 mb-2 longform">{translate("The XRPL Campus Ambassador program aims to elevate the impact of college students who are passionate about blockchain technology.")}</p>
<div className="d-none d-lg-block p-lg-3">
<button className="btn btn-primary btn-arrow-out" onClick={() => window.open('https://share.hsforms.com/1k47bfuX2SL2DKZtZoJzArg4vgrs', "_blank")} >{translate("Apply for Fall 2024")}</button>
</div>
</div>
<div className="order-lg-1 col-lg-6 px-0 mr-lg-4">
<div className="row m-0">
<img alt="Person speaking and person taking photo" src={require("../static/img/ambassadors/developer-hero@2x.png")} className="w-100" />
</div>
</div>
<div className="d-lg-none order-3 mt-4 pt-3 p-lg-3">
<button className="btn btn-primary btn-arrow-out" onClick={() => window.open('https://share.hsforms.com/1k47bfuX2SL2DKZtZoJzArg4vgrs', "_blank")} >{translate("Apply for Fall 2024")}</button>
</div>
</div>
</section>
{/* Benefits */}
<section className="container-new py-26">
{/* flex. Col for mobile. Row for large. on large align content to the center */}
<div className="d-flex flex-column flex-lg-row align-items-lg-center">
<div className="order-1 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0">
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("Why become an XRPL Campus Ambassador?")}</h3>
<h6 className="eyebrow mb-3">{translate("Benefits")}</h6>
</div>
<p className="p-lg-3 mb-2 longform">{translate("Join a global cohort of students empowering others to build on the XRPL.")}</p>
</div>
<div className="order-2 col-lg-6 px-0 mr-lg-5">
<div className="row align-items-center m-0" id="benefits-list">
{/* benefitslist */}
<div className="col-12 col-lg-6 p-0 pr-lg-4">
<div className="px-lg-3 pb-3">
<img alt="Smiley face" id="benefits-01" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Exclusive Opportunities")}</h6>
<p>{translate("Get access and invitations to Ambassador-only events and opportunities")}</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none ">
<img alt="Book" id="benefits-02" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Education")}</h6>
<p>{translate("Tutorials and workshops from leading XRPL and blockchain developers")}</p>
</div>
</div>
<div className="px-lg-3 pb-3">
<img alt="Gift" id="benefits-03" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Swag")}</h6>
<p>{translate("New XRPL swag for Ambassadors and swag to share with other students")}</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="Medallion" id="benefits-04" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Mentorship")}</h6>
<p>{translate("Meet with and learn from influential builders and leaders across the XRPL community")}</p>
</div>
</div>
<div className="px-lg-3 pb-3">
<img alt="Up Arrow" id="benefits-05" className="pl-lg-3" />
<div className="p-lg-3 pt-3 pb-lg-0">
<h6 className="mb-3">{translate("Career Acceleration")}</h6>
<p className="pb-lg-0">{translate("Gain hands-on experience building communities and grow your professional network in the blockchain industry")}</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="Dollar Sign" id="benefits-06" className="pl-lg-3" />
<div className="pb-lg-0">
<h6 className="mb-3">{translate("Stipend")}</h6>
<p className="pb-lg-0">{translate("Receive a stipend to fund your ideas and initiatives that fuel XRPL growth in your community")}</p>
</div>
</div>
</div>
{/* end col 1 */}
{/* Show on large */}
<div className="col-12 col-lg-6 p-0 pl-lg-4 d-none d-lg-block">
<div className="px-lg-3 pb-3 pt-5 mt-5">
<img alt="Book" id="benefits-02" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Education")}</h6>
<p>{translate("Tutorials and workshops from leading XRPL and blockchain developers")}</p>
</div>
</div>
<div className="px-lg-3 pb-3 ">
<img alt="Medallion" id="benefits-04" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Mentorship")}</h6>
<p>{translate("Meet with and learn from influential builders and leaders across the XRPL community")}</p>
</div>
</div>
<div className="px-lg-3 pb-3">
<img alt="Dollar Sign" id="benefits-06" className="pl-lg-3" />
<div className="p-lg-3 pt-3 pb-lg-0">
<h6 className="mb-3">{translate("Stipend")}</h6>
<p className="pb-lg-0">{translate("Receive a stipend to fund your ideas and initiatives that fuel XRPL growth in your community")}</p>
</div>
</div>
</div>
{/* end col 2 */}
</div>
</div>
{/* flex. Col for mobile. Row for large. on large align content to the center */}
<div className="d-flex flex-column flex-lg-row align-items-lg-center">
<div className="order-1 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0">
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("Why become an XRPL Campus Ambassador?")}</h3>
<h6 className="eyebrow mb-3">{translate("Benefits")}</h6>
</div>
<p className="p-lg-3 mb-2 longform">{translate("Join a global cohort of students empowering others to build on the XRPL.")}</p>
</div>
<div className="order-2 col-lg-6 px-0 mr-lg-5">
<div className="row align-items-center m-0" id="benefits-list">
{/* benefitslist */}
<div className="col-12 col-lg-6 p-0 pr-lg-4">
<div className="px-lg-3 pb-3">
<img alt="Smiley face" id="benefits-01" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Exclusive Opportunities")}</h6>
<p>{translate("Get access and invitations to Ambassador-only events and opportunities")}</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none ">
<img alt="Book" id="benefits-02" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Education")}</h6>
<p>{translate("Tutorials and workshops from leading XRPL and blockchain developers")}</p>
</div>
</div>
<div className="px-lg-3 pb-3">
<img alt="Gift" id="benefits-03" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Swag")}</h6>
<p>{translate("New XRPL swag for Ambassadors and swag to share with other students")}</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="Medallion" id="benefits-04" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Mentorship")}</h6>
<p>{translate("Meet with and learn from influential builders and leaders across the XRPL community")}</p>
</div>
</div>
<div className="px-lg-3 pb-3">
<img alt="Up Arrow" id="benefits-05" className="pl-lg-3" />
<div className="p-lg-3 pt-3 pb-lg-0">
<h6 className="mb-3">{translate("Career Acceleration")}</h6>
<p className="pb-lg-0">{translate("Gain hands-on experience building communities and grow your professional network in the blockchain industry")}</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="Dollar Sign" id="benefits-06" className="pl-lg-3" />
<div className="pb-lg-0">
<h6 className="mb-3">{translate("Stipend")}</h6>
<p className="pb-lg-0">{translate("Receive a stipend to fund your ideas and initiatives that fuel XRPL growth in your community")}</p>
</div>
</div>
</div>
{/* end col 1 */}
{/* Show on large */}
<div className="col-12 col-lg-6 p-0 pl-lg-4 d-none d-lg-block">
<div className="px-lg-3 pb-3 pt-5 mt-5">
<img alt="Book" id="benefits-02" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Education")}</h6>
<p>{translate("Tutorials and workshops from leading XRPL and blockchain developers")}</p>
</div>
</div>
<div className="px-lg-3 pb-3 ">
<img alt="Medallion" id="benefits-04" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Mentorship")}</h6>
<p>{translate("Meet with and learn from influential builders and leaders across the XRPL community")}</p>
</div>
</div>
<div className="px-lg-3 pb-3">
<img alt="Dollar Sign" id="benefits-06" className="pl-lg-3" />
<div className="p-lg-3 pt-3 pb-lg-0">
<h6 className="mb-3">{translate("Stipend")}</h6>
<p className="pb-lg-0">{translate("Receive a stipend to fund your ideas and initiatives that fuel XRPL growth in your community")}</p>
</div>
</div>
</div>
{/* end col 2 */}
</div>
</div>
</div>
</section>
{/* Eligibility */}
<section className="container-new py-26">
{/* flex. Col for mobile. Row for large. on large align content to the center */}
<div className="d-flex flex-column flex-lg-row align-items-lg-center mr-lg-4">
<div className="order-1 order-lg-2 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0 mr-lg-5">
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("Should You Apply?")}</h3>
<h6 className="eyebrow mb-3">{translate("Eligibility for XRPL Campus Ambassadors")}</h6>
</div>
<p className="p-lg-3 mb-2 longform">{translate("Students currently enrolled in an undergraduate or postgraduate program at an accredited college or university are eligible to apply.")}</p>
</div>
<div className="order-2 order-lg-1 col-lg-6 px-0">
<div className="row align-items-center m-0" id="eligibility-list">
<div className="col-12 col-lg-6 p-0 pr-lg-4">
<div className="px-lg-3 pb-3">
<img alt="Calendar" id="eligibility-01" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("A Leader")}</h6>
<p>{translate("Interested in leading meetups and workshops for your local campus community")}</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none ">
<img alt="Book" id="eligibility-02" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Active")}</h6>
<p>{translate("An active participant in the XRPL community or interested in blockchain and crypto technologies")}</p>
</div>
</div>
<div className="px-lg-3 pb-3">
<img alt="CPU" id="eligibility-03" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Curious")}</h6>
<p>{translate("Eager to learn more about technical blockchain topics and the XRPL")}</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="Quote Bubble" id="eligibility-04" className="pl-lg-3" />
<div className="p-lg-3 pt-3 pb-lg-0">
<h6 className="mb-3">{translate("Passionate")}</h6>
<p>{translate("Passionate about increasing XRPL education and awareness through events, content, and classroom engagement")}</p>
</div>
</div>
<div className="p-lg-3 pb-3">
<img alt="People" id="eligibility-05" className="pl-lg-3" />
<div className="p-lg-3 pt-3 pb-lg-0">
<h6 className="mb-3">{translate("Creative")}</h6>
<p className="pb-lg-0 mb-0">{translate("Ability to think outside the box and have an impact in the XRPL student community")}</p>
</div>
</div>
</div>
{/* end col 1 */}
{/* Show on large */}
<div className="col-12 col-lg-6 p-0 pl-lg-4 d-none d-lg-block">
<div className="px-lg-3 pb-3 ">
<img alt="Book" id="eligibility-02" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Active")}</h6>
<p>{translate("An active participant in the XRPL community or interested in blockchain and crypto technologies")} </p>
</div>
</div>
<div className="px-lg-3 pb-3 ">
<img alt="Quote Bubble" id="eligibility-04" className="pl-lg-3" />
<div className="p-lg-3 pt-3 pb-lg-0">
<h6 className="mb-3">{translate("Passionate")}</h6>
<p> {translate("Passionate about increasing XRPL education and awareness through events, content, and classroom engagement")}</p>
</div>
</div>
</div>
{/* end col 2 */}
</div>
</div>
{/* flex. Col for mobile. Row for large. on large align content to the center */}
<div className="d-flex flex-column flex-lg-row align-items-lg-center mr-lg-4">
<div className="order-1 order-lg-2 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0 mr-lg-5">
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("Should You Apply?")}</h3>
<h6 className="eyebrow mb-3">{translate("Eligibility for XRPL Campus Ambassadors")}</h6>
</div>
<p className="p-lg-3 mb-2 longform">{translate("Students currently enrolled in an undergraduate or postgraduate program at an accredited college or university are eligible to apply.")}</p>
</div>
<div className="order-2 order-lg-1 col-lg-6 px-0">
<div className="row align-items-center m-0" id="eligibility-list">
<div className="col-12 col-lg-6 p-0 pr-lg-4">
<div className="px-lg-3 pb-3">
<img alt="Calendar" id="eligibility-01" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("A Leader")}</h6>
<p>{translate("Interested in leading meetups and workshops for your local campus community")}</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none ">
<img alt="Book" id="eligibility-02" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Active")}</h6>
<p>{translate("An active participant in the XRPL community or interested in blockchain and crypto technologies")}</p>
</div>
</div>
<div className="px-lg-3 pb-3">
<img alt="CPU" id="eligibility-03" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Curious")}</h6>
<p>{translate("Eager to learn more about technical blockchain topics and the XRPL")}</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="Quote Bubble" id="eligibility-04" className="pl-lg-3" />
<div className="p-lg-3 pt-3 pb-lg-0">
<h6 className="mb-3">{translate("Passionate")}</h6>
<p>{translate("Passionate about increasing XRPL education and awareness through events, content, and classroom engagement")}</p>
</div>
</div>
<div className="p-lg-3 pb-3">
<img alt="People" id="eligibility-05" className="pl-lg-3" />
<div className="p-lg-3 pt-3 pb-lg-0">
<h6 className="mb-3">{translate("Creative")}</h6>
<p className="pb-lg-0 mb-0">{translate("Ability to think outside the box and have an impact in the XRPL student community")}</p>
</div>
</div>
</div>
{/* end col 1 */}
{/* Show on large */}
<div className="col-12 col-lg-6 p-0 pl-lg-4 d-none d-lg-block">
<div className="px-lg-3 pb-3 ">
<img alt="Book" id="eligibility-02" className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Active")}</h6>
<p>{translate("An active participant in the XRPL community or interested in blockchain and crypto technologies")} </p>
</div>
</div>
<div className="px-lg-3 pb-3 ">
<img alt="Quote Bubble" id="eligibility-04" className="pl-lg-3" />
<div className="p-lg-3 pt-3 pb-lg-0">
<h6 className="mb-3">{translate("Passionate")}</h6>
<p> {translate("Passionate about increasing XRPL education and awareness through events, content, and classroom engagement")}</p>
</div>
</div>
</div>
{/* end col 2 */}
</div>
</div>
</div>
</section>
{/* Current Students */}
<section className="container-new py-26">
{/* Quotes */}
<div id="carouselSlidesOnly" className="carousel slide col-lg-10 mx-auto px-0" data-ride="carousel">
<div className="carousel-inner">
<div className="carousel-item active">
<div className="p-0">
<div className="mb-4 p-lg-3">
<img alt="I have learned so much through creating programs and connecting with the XRPL community. Im truly grateful for everyone's support along the way and for the opportunity to gain so much knowledge from this expierence" src={require("../static/img/ambassadors/quote1-small.svg")} className="h-100 d-lg-none mb-4" />
<img alt="I have learned so much through creating programs and connecting with the XRPL community. Im truly grateful for everyone's support along the way and for the opportunity to gain so much knowledge from this expierence" src={require("../static/img/ambassadors/quote1.svg")} className="h-100 d-none d-lg-block" />
<div className="p-0 col-lg-7 mx-lg-auto">
<p className="p-lg-3 mb-2"><strong>Derrick N.</strong><br />
Toronto Metropolitan University<br />
Spring 2023 XRPL Campus Ambassador</p>
</div>
</div>
</div>
</div>
<div className="carousel-item mb-20">
<div className="p-0">
<div className="mb-4 p-lg-3">
<img alt="The XRPL Campus Ambassador program really helped broaden my view of the blockchain industry with their learning resource and virtual community. Being an ambassador allowed me to meet industry professionals and likeminded peers which have given me invaluable experiences and insights." src={require("../static/img/ambassadors/quote2-small.svg")} className="h-150 d-lg-none mb-4" />
<img alt="The XRPL Campus Ambassador program really helped broaden my view of the blockchain industry with their learning resource and virtual community. Being an ambassador allowed me to meet industry professionals and likeminded peers which have given me invaluable experiences and insights." src={require("../static/img/ambassadors/quote2.svg")} className="h-100 d-none d-lg-block" />
<div className="p-0 col-lg-7 mx-lg-auto">
<p className="p-lg-3 mb-2"><strong>Sally Z.</strong><br />
Toronto Metropolitan University<br />
Spring 2023 XRPL Campus Ambassador</p>
</div>
</div>
</div>
</div>
<div className="carousel-item mb-40">
<div className="p-0">
<div className="mb-4 p-lg-3">
<img alt="Ive had the pleasure over the course of this program to speak with amazing individuals, I encourage you all to reach out to other people in this program and make as many connections as you can. You will quickly find out that by speaking with other people in this cohort you can learn just about anything if you ask the right people." src={require("../static/img/ambassadors/quote3-small.svg")} className="h-150 d-lg-none mb-4" />
<img alt="Ive had the pleasure over the course of this program to speak with amazing individuals, I encourage you all to reach out to other people in this program and make as many connections as you can. You will quickly find out that by speaking with other people in this cohort you can learn just about anything if you ask the right people." src={require("../static/img/ambassadors/quote3.svg")} className="h-100 d-none d-lg-block" />
<div className="p-0 col-lg-7 mx-lg-auto">
<p className="p-lg-3 mb-2"><strong>Nick D.</strong><br />
Miami University<br />
Spring 2023 XRPL Campus Ambassador</p>
</div>
</div>
</div>
{/* Quotes */}
<div id="carouselSlidesOnly" className="carousel slide col-lg-10 mx-auto px-0" data-ride="carousel">
<div className="carousel-inner">
<div className="carousel-item active">
<div className="p-0">
<div className="mb-4 p-lg-3">
<img alt="I have learned so much through creating programs and connecting with the XRPL community. Im truly grateful for everyone's support along the way and for the opportunity to gain so much knowledge from this expierence" src={require("../static/img/ambassadors/quote1-small.svg")} className="h-100 d-lg-none mb-4" />
<img alt="I have learned so much through creating programs and connecting with the XRPL community. Im truly grateful for everyone's support along the way and for the opportunity to gain so much knowledge from this expierence" src={require("../static/img/ambassadors/quote1.svg")} className="h-100 d-none d-lg-block" />
<div className="p-0 col-lg-7 mx-lg-auto">
<p className="p-lg-3 mb-2"><strong>Derrick N.</strong><br />
Toronto Metropolitan University<br />
Spring 2023 XRPL Campus Ambassador</p>
</div>
</div>
</div>
</div>
<div className="carousel-item mb-20">
<div className="p-0">
<div className="mb-4 p-lg-3">
<img alt="The XRPL Campus Ambassador program really helped broaden my view of the blockchain industry with their learning resource and virtual community. Being an ambassador allowed me to meet industry professionals and likeminded peers which have given me invaluable experiences and insights." src={require("../static/img/ambassadors/quote2-small.svg")} className="h-150 d-lg-none mb-4" />
<img alt="The XRPL Campus Ambassador program really helped broaden my view of the blockchain industry with their learning resource and virtual community. Being an ambassador allowed me to meet industry professionals and likeminded peers which have given me invaluable experiences and insights." src={require("../static/img/ambassadors/quote2.svg")} className="h-100 d-none d-lg-block" />
<div className="p-0 col-lg-7 mx-lg-auto">
<p className="p-lg-3 mb-2"><strong>Sally Z.</strong><br />
Toronto Metropolitan University<br />
Spring 2023 XRPL Campus Ambassador</p>
</div>
</div>
</div>
</div>
<div className="carousel-item mb-40">
<div className="p-0">
<div className="mb-4 p-lg-3">
<img alt="Ive had the pleasure over the course of this program to speak with amazing individuals, I encourage you all to reach out to other people in this program and make as many connections as you can. You will quickly find out that by speaking with other people in this cohort you can learn just about anything if you ask the right people." src={require("../static/img/ambassadors/quote3-small.svg")} className="h-150 d-lg-none mb-4" />
<img alt="Ive had the pleasure over the course of this program to speak with amazing individuals, I encourage you all to reach out to other people in this program and make as many connections as you can. You will quickly find out that by speaking with other people in this cohort you can learn just about anything if you ask the right people." src={require("../static/img/ambassadors/quote3.svg")} className="h-100 d-none d-lg-block" />
<div className="p-0 col-lg-7 mx-lg-auto">
<p className="p-lg-3 mb-2"><strong>Nick D.</strong><br />
Miami University<br />
Spring 2023 XRPL Campus Ambassador</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* How it Works */}
<section className="container-new py-26">
{/* flex. Col for mobile. Row for large. on large align content to the center */}
<div className="d-flex flex-column flex-lg-row align-items-lg-center">
<div className="order-1 mr-lg-4 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0">
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("Process to become a Campus Ambassador")}</h3>
<h6 className="eyebrow mb-3">{translate("How it Works")}</h6>
</div>
<p className="p-lg-3 mb-2 longform">{translate("Apply now to become an XRPL Campus Ambassador.")}</p>
<div className="d-none d-lg-block p-lg-3">
<button className="btn btn-primary btn-arrow-out" onClick={() => window.open('https://share.hsforms.com/1k47bfuX2SL2DKZtZoJzArg4vgrs', "_blank")} >{translate("Apply for Fall 2024")}</button>
</div>
</div>
<div className="order-2 col-lg-6 px-0 ml-lg-2">
<div className="row m-0">
<div className="col-12 col-lg-6 p-0 pr-lg-4">
<div className="px-lg-3 pb-3">
<img src={require("../static/img/ambassadors/01.svg")} className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Apply")}</h6>
<p>{translate("Submit an application to be considered for the Campus Ambassador program.")}</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none ">
<img src={require("../static/img/ambassadors/02.svg")} className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Interview")}</h6>
<p>{translate("Tell the XRPL community-led panel more about yourself and your interest in the program during an interview.")}</p>
</div>
</div>
<div className="px-lg-3 pb-3">
<img src={require("../static/img/ambassadors/03.svg")} className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Join")}</h6>
<p>{translate("Congrats on your new role! Join the global cohort of Ambassadors and meet with community participants during onboarding.")}</p>
</div>
</div>
{/* Hide on large */}
<div className="p-lg-3 pb-3 d-lg-none">
<img src={require("../static/img/ambassadors/04.svg")} className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Learn")}</h6>
<p> {translate("Participate in personalized learning and training sessions for Ambassadors on the XRPL and blockchain technology.")}</p>
</div>
</div>
</div>
{/* end col 1 */}
{/* Show on large */}
<div className="col-12 col-lg-6 p-0 pl-lg-4 d-none d-lg-block mt-5">
<div className="px-lg-3 pb-3 mt-5">
<img src={require("../static/img/ambassadors/02.svg")} className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Interview")}</h6>
<p>{translate("Tell the XRPL community-led panel more about yourself and your interest in the program during an interview.")}</p>
</div>
</div>
<div className="p-lg-3 pb-3 ">
<img src={require("../static/img/ambassadors/04.svg")} className="pl-lg-3" />
<div className="p-lg-3 pt-3 pb-lg-0">
<h6 className="mb-3">{translate("Learn")}</h6>
<p className="pb-lg-0">{translate("Participate in personalized learning and training sessions for Ambassadors on the XRPL and blockchain technology.")}</p>
</div>
</div>
</div>
{/* end col 2 */}
</div>
</div>
<div className="d-lg-none order-3 mt-4 pt-3">
<button className="btn btn-primary btn-arrow-out" onClick={() => window.open('https://share.hsforms.com/1k47bfuX2SL2DKZtZoJzArg4vgrs', "_blank")} >{translate("Apply for Fall 2024")}</button>
</div>
{/* flex. Col for mobile. Row for large. on large align content to the center */}
<div className="d-flex flex-column flex-lg-row align-items-lg-center">
<div className="order-1 mr-lg-4 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0">
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("Process to become a Campus Ambassador")}</h3>
<h6 className="eyebrow mb-3">{translate("How it Works")}</h6>
</div>
<p className="p-lg-3 mb-2 longform">{translate("Apply now to become an XRPL Campus Ambassador.")}</p>
<div className="d-none d-lg-block p-lg-3">
<button className="btn btn-primary btn-arrow-out" onClick={() => window.open('https://share.hsforms.com/1k47bfuX2SL2DKZtZoJzArg4vgrs', "_blank")} >{translate("Apply for Fall 2024")}</button>
</div>
</div>
<div className="order-2 col-lg-6 px-0 ml-lg-2">
<div className="row m-0">
<div className="col-12 col-lg-6 p-0 pr-lg-4">
<div className="px-lg-3 pb-3">
<img src={require("../static/img/ambassadors/01.svg")} className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Apply")}</h6>
<p>{translate("Submit an application to be considered for the Campus Ambassador program.")}</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none ">
<img src={require("../static/img/ambassadors/02.svg")} className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Interview")}</h6>
<p>{translate("Tell the XRPL community-led panel more about yourself and your interest in the program during an interview.")}</p>
</div>
</div>
<div className="px-lg-3 pb-3">
<img src={require("../static/img/ambassadors/03.svg")} className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Join")}</h6>
<p>{translate("Congrats on your new role! Join the global cohort of Ambassadors and meet with community participants during onboarding.")}</p>
</div>
</div>
{/* Hide on large */}
<div className="p-lg-3 pb-3 d-lg-none">
<img src={require("../static/img/ambassadors/04.svg")} className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Learn")}</h6>
<p> {translate("Participate in personalized learning and training sessions for Ambassadors on the XRPL and blockchain technology.")}</p>
</div>
</div>
</div>
{/* end col 1 */}
{/* Show on large */}
<div className="col-12 col-lg-6 p-0 pl-lg-4 d-none d-lg-block mt-5">
<div className="px-lg-3 pb-3 mt-5">
<img src={require("../static/img/ambassadors/02.svg")} className="pl-lg-3" />
<div className="p-lg-3 pt-3">
<h6 className="mb-3">{translate("Interview")}</h6>
<p>{translate("Tell the XRPL community-led panel more about yourself and your interest in the program during an interview.")}</p>
</div>
</div>
<div className="p-lg-3 pb-3 ">
<img src={require("../static/img/ambassadors/04.svg")} className="pl-lg-3" />
<div className="p-lg-3 pt-3 pb-lg-0">
<h6 className="mb-3">{translate("Learn")}</h6>
<p className="pb-lg-0">{translate("Participate in personalized learning and training sessions for Ambassadors on the XRPL and blockchain technology.")}</p>
</div>
</div>
</div>
{/* end col 2 */}
</div>
</div>
<div className="d-lg-none order-3 mt-4 pt-3">
<button className="btn btn-primary btn-arrow-out" onClick={() => window.open('https://share.hsforms.com/1k47bfuX2SL2DKZtZoJzArg4vgrs', "_blank")} >{translate("Apply for Fall 2024")}</button>
</div>
</div>
</section>
{/* Image Block */}
<div>
<img alt="Ripple Conferences and two people Sitting" src={require("../static/img/ambassadors/students-large.png")} className="w-100" />
<img alt="Ripple Conferences and two people Sitting" src={require("../static/img/ambassadors/students-large.png")} className="w-100" />
</div>
{/* Global Community Carousel */}
<section className="container-new pt-26">
<div className="p-0 col-lg-5">
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("Join a global cohort of Student Ambassadors")}</h3>
<h6 className="eyebrow mb-3">{translate("Global Community")}</h6>
</div>
<div className="p-0 col-lg-5">
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("Join a global cohort of Student Ambassadors")}</h3>
<h6 className="eyebrow mb-3">{translate("Global Community")}</h6>
</div>
</div>
</section>
<div id="container-scroll">
<div className="photobanner">
<img src={require("../static/img/ambassadors/locations-row-1.png")} alt="Ambassador locations" height="48px" className="px-5" />
<img src={require("../static/img/ambassadors/locations-row-1.png")} alt="Ambassador locations" height="48px" className="px-5" />
<img src={require("../static/img/ambassadors/locations-row-1.png")} alt="Ambassador locations" height="48px" className="px-5" />
</div>
<div className="photobanner photobanner-bottom">
<img src={require("../static/img/ambassadors/locations-row-2.png")} alt="Ambassador locations" height="48px" className="px-5" />
<img src={require("../static/img/ambassadors/locations-row-2.png")} alt="Ambassador locations" height="48px" className="px-5" />
<img src={require("../static/img/ambassadors/locations-row-2.png")} alt="Ambassador locations" height="48px" className="px-5" />
</div>
<div className="photobanner">
<img src={require("../static/img/ambassadors/locations-row-1.png")} alt="Ambassador locations" height="48px" className="px-5" />
<img src={require("../static/img/ambassadors/locations-row-1.png")} alt="Ambassador locations" height="48px" className="px-5" />
<img src={require("../static/img/ambassadors/locations-row-1.png")} alt="Ambassador locations" height="48px" className="px-5" />
</div>
<div className="photobanner photobanner-bottom">
<img src={require("../static/img/ambassadors/locations-row-2.png")} alt="Ambassador locations" height="48px" className="px-5" />
<img src={require("../static/img/ambassadors/locations-row-2.png")} alt="Ambassador locations" height="48px" className="px-5" />
<img src={require("../static/img/ambassadors/locations-row-2.png")} alt="Ambassador locations" height="48px" className="px-5" />
</div>
</div>
{/* Connect */}
<section className="container-new py-26">
{/* flex. Col for mobile. Row for large. on large align content to the center */}
<div className="d-flex flex-column flex-lg-row align-items-lg-center">
<div className="order-1 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0">
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("Stay connected to the XRPL Community")}</h3>
<h6 className="eyebrow mb-3">{translate("Connect")}</h6>
</div>
<p className="p-lg-3 mb-2 longform">{translate("To stay up-to-date on the latest activity, meetups, and events of the XRPL Community be sure to follow these channels:")}</p>
<div className="d-none d-lg-block p-lg-3">
<button className="btn btn-primary btn-arrow-out" onClick={() => window.open('https://share.hsforms.com/1k47bfuX2SL2DKZtZoJzArg4vgrs', "_blank")} >{translate("Apply for Fall 2024")}</button>
</div>
</div>
<div className="order-2 col-lg-6 px-0 ml-lg-5">
<div className="row align-items-center m-0">
<div className="col-12 col-lg-6 p-0 pr-lg-4">
<div className="p-lg-3 mb-3 pb-3">
<img alt="meetup" src={require("../static/img/ambassadors/icon_meetup.svg")} className="mb-3" />
<div>
<h6 className="mb-3"><a className="btn-arrow" href="https://www.meetup.com/pro/xrpl-community/">{translate("MeetUp")}</a></h6>
<p>{translate("Attend an XRPL Meetup in your local area")}</p>
</div>
</div>
<div className="p-lg-3 mb-3 pb-3">
<img alt="devto" src={require("../static/img/ambassadors/icon_devto.svg")} className="mb-3" />
<div>
<h6 className="mb-3"><a className="btn-arrow" href="https://dev.to/t/xrpl">{translate("Dev.to Blog")}</a></h6>
<p>{translate("Read more about the activity of the XRPL Ambassadors")}</p>
</div>
</div>
</div>
<div className="col-12 col-lg-6 p-0 pl-lg-4">
<div className="p-lg-3 mb-3 pb-3 ">
<img alt="discord" src={require("../static/img/ambassadors/icon_discord.svg")} className="mb-3" />
<div>
<h6 className="mb-3"><a className="btn-arrow" href="https://xrpldevs.org">{translate("Discord")}</a></h6>
<p>{translate("Join the conversation on the XRPL Developer Discord")}</p>
</div>
</div>
</div>
</div>
</div>
<div className="d-lg-none order-3 mt-4 pt-3">
<button className="btn btn-primary btn-arrow-out" onClick={() => window.open('https://share.hsforms.com/1k47bfuX2SL2DKZtZoJzArg4vgrs', "_blank")} >{translate("Apply for Fall 2024")}</button>
</div>
{/* flex. Col for mobile. Row for large. on large align content to the center */}
<div className="d-flex flex-column flex-lg-row align-items-lg-center">
<div className="order-1 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0">
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("Stay connected to the XRPL Community")}</h3>
<h6 className="eyebrow mb-3">{translate("Connect")}</h6>
</div>
<p className="p-lg-3 mb-2 longform">{translate("To stay up-to-date on the latest activity, meetups, and events of the XRPL Community be sure to follow these channels:")}</p>
<div className="d-none d-lg-block p-lg-3">
<button className="btn btn-primary btn-arrow-out" onClick={() => window.open('https://share.hsforms.com/1k47bfuX2SL2DKZtZoJzArg4vgrs', "_blank")} >{translate("Apply for Fall 2024")}</button>
</div>
</div>
<div className="order-2 col-lg-6 px-0 ml-lg-5">
<div className="row align-items-center m-0">
<div className="col-12 col-lg-6 p-0 pr-lg-4">
<div className="p-lg-3 mb-3 pb-3">
<img alt="meetup" src={require("../static/img/ambassadors/icon_meetup.svg")} className="mb-3" />
<div>
<h6 className="mb-3"><a className="btn-arrow" href="https://www.meetup.com/pro/xrpl-community/">{translate("MeetUp")}</a></h6>
<p>{translate("Attend an XRPL Meetup in your local area")}</p>
</div>
</div>
<div className="p-lg-3 mb-3 pb-3">
<img alt="devto" src={require("../static/img/ambassadors/icon_devto.svg")} className="mb-3" />
<div>
<h6 className="mb-3"><a className="btn-arrow" href="https://dev.to/t/xrpl">{translate("Dev.to Blog")}</a></h6>
<p>{translate("Read more about the activity of the XRPL Ambassadors")}</p>
</div>
</div>
</div>
<div className="col-12 col-lg-6 p-0 pl-lg-4">
<div className="p-lg-3 mb-3 pb-3 ">
<img alt="discord" src={require("../static/img/ambassadors/icon_discord.svg")} className="mb-3" />
<div>
<h6 className="mb-3"><a className="btn-arrow" href="https://xrpldevs.org">{translate("Discord")}</a></h6>
<p>{translate("Join the conversation on the XRPL Developer Discord")}</p>
</div>
</div>
</div>
</div>
</div>
<div className="d-lg-none order-3 mt-4 pt-3">
<button className="btn btn-primary btn-arrow-out" onClick={() => window.open('https://share.hsforms.com/1k47bfuX2SL2DKZtZoJzArg4vgrs', "_blank")} >{translate("Apply for Fall 2024")}</button>
</div>
</div>
</section>
</div>
</div>
</div>
)
}

View File

@@ -19,261 +19,148 @@ export default function Funding() {
return (
<div className="landing page-funding">
<div>
<div className="position-relative d-none-sm">
<img
alt="purple waves"
src={require("../static/img/backgrounds/funding-purple.svg")}
className="position-absolute"
style={{ top: 0, right: 0 }}
/>
</div>
<section className="container-new py-26 text-lg-center">
<div className="p-0 col-lg-6 mx-lg-auto">
<div className="d-flex flex-column-reverse">
<h1 className="mb-0">
{translate("XRPL Developer Funding Programs")}
</h1>
<h6 className="eyebrow mb-3">{translate("Project Resources")}</h6>
</div>
<section className="container-new py-26 text-lg-center">
<div className="p-0 col-lg-6 mx-lg-auto">
<div className="d-flex flex-column-reverse">
<h1 className="mb-0">
{translate("XRPL Developer Funding Programs")}
</h1>
<h6 className="eyebrow mb-3">{translate("Project Resources")}</h6>
</div>
</section>
<section className="container-new py-26">
<div className="p-0 col-lg-6 mx-lg-auto" style={{ maxWidth: 520 }}>
<div className="d-flex flex-column-reverse">
<h1 className="mb-0 h4 h2-sm">
{translate(
"Explore funding opportunities for developers and teams"
)}
</h1>
<h6 className="eyebrow mb-3">{translate("Funding Overview")}</h6>
</div>
<p className="mt-3 py-3 p-0 longform">
</div>
</section>
<section className="container-new py-26">
<div className="p-0 col-lg-6 mx-lg-auto" style={{ maxWidth: 520 }}>
<div className="d-flex flex-column-reverse">
<h1 className="mb-0 h4 h2-sm">
{translate(
"If youre a software developer or team looking to build your next project or venture on the XRP Ledger (XRPL), there are a number of opportunities to fund your next innovation."
"Explore funding opportunities for developers and teams"
)}
</h1>
<h6 className="eyebrow mb-3">{translate("Funding Overview")}</h6>
</div>
<p className="mt-3 py-3 p-0 longform">
{translate(
"If youre a software developer or team looking to build your next project or venture on the XRP Ledger (XRPL), there are a number of opportunities to fund your next innovation."
)}
</p>
</div>
</section>
{/* Hackathons */}
<section className="container-new py-26">
{/* flex. Col for mobile. Row for large. on large align content to the center */}
<div className="d-flex flex-column flex-lg-row align-items-lg-center">
<div
className="order-1 order-lg-2 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0"
style={{ maxWidth: 520 }}
>
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("XRPL Hackathons")}</h3>
<h6 className="eyebrow mb-3">{translate("Join an Event")}</h6>
</div>
<p className="p-lg-3 mb-2 longform">
{translate(
"Hackathons are open to all developers to explore and invent a project on the XRP Ledger. Visit the events page for updates on upcoming hackathons."
)}
</p>
</div>
</section>
{/* Hackathons */}
<section className="container-new py-26">
{/* flex. Col for mobile. Row for large. on large align content to the center */}
<div className="d-flex flex-column flex-lg-row align-items-lg-center">
<div
className="order-1 order-lg-2 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0"
style={{ maxWidth: 520 }}
>
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("XRPL Hackathons")}</h3>
<h6 className="eyebrow mb-3">{translate("Join an Event")}</h6>
</div>
<p className="p-lg-3 mb-2 longform">
{translate(
"Hackathons are open to all developers to explore and invent a project on the XRP Ledger. Visit the events page for updates on upcoming hackathons."
)}
</p>
<div className="d-none d-lg-block p-lg-3">
<Link className="btn btn-primary btn-arrow" to="/community/events">
{translate("See Upcoming Events")}
</Link>
</div>
<div className="d-none d-lg-block p-lg-3">
<Link className="btn btn-primary btn-arrow" to="/community/events">
{translate("See Upcoming Events")}
</Link>
</div>
<div className="order-2 order-lg-1 col-lg-6 px-0">
<div className="row align-items-center m-0 funding-list">
{/* funding list */}
<div className="col-12 col-lg-6 p-0">
<div className="px-lg-3 pb-3">
<img alt="user" id="funding-01" />
<div className="pt-3">
<h6 className="mb-3">{translate("Best for")}</h6>
<p>
{translate(
"Software developers and teams building directly on the XRP Ledger"
)}
</p>
</div>
</div>
<div className="order-2 order-lg-1 col-lg-6 px-0">
<div className="row align-items-center m-0 funding-list">
{/* funding list */}
<div className="col-12 col-lg-6 p-0">
<div className="px-lg-3 pb-3">
<img alt="user" id="funding-01" />
<div className="pt-3">
<h6 className="mb-3">{translate("Best for")}</h6>
<p>
{translate(
"Software developers and teams building directly on the XRP Ledger"
)}
</p>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="book" id="funding-02" />
<div className="pt-3">
<h6 className="mb-3">{translate("Required")}</h6>
<p>{translate("Some coding experience")}</p>
</div>
</div>
<div className="px-lg-3 pb-3 pt-lg-5">
<img alt="arrow" id="funding-03" />
<div className="pt-3">
<h6 className="mb-3">{translate("Level")}</h6>
<p>{translate("XRPL beginner to advanced developers")}</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="dollar sign" id="funding-04" />
<div className="pt-3">
<h6 className="mb-3">{translate("Funding Levels")}</h6>
<p>{translate("Prize money and awards")}</p>
</div>
</div>
</div>
{/* end col 1 */}
{/* Show on large */}
<div className="col-12 col-lg-6 p-0 d-none d-lg-block">
<div className="px-lg-3 pb-3 pt-5 mt-5">
<div className="pt-1 mt-3">
<img alt="book" id="funding-02" />
<div className="pt-3">
<h6 className="mb-3">{translate("Required")}</h6>
<p>{translate("Some coding experience")}</p>
</div>
</div>
<div className="px-lg-3 pb-3 pt-lg-5">
<img alt="arrow" id="funding-03" />
<div className="pt-3">
<h6 className="mb-3">{translate("Level")}</h6>
<p>{translate("XRPL beginner to advanced developers")}</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="dollar sign" id="funding-04" />
<div className="pt-3">
<h6 className="mb-3">{translate("Funding Levels")}</h6>
<p>{translate("Prize money and awards")}</p>
</div>
</div>
<div className="px-lg-3 pb-3 pt-5">
<img alt="dollar sign" id="funding-04" />
<div className="pt-3">
<h6 className="mb-3">{translate("Funding Levels")}</h6>
<p>{translate("Prize money and awards")}</p>
</div>
</div>
{/* end col 1 */}
{/* Show on large */}
<div className="col-12 col-lg-6 p-0 d-none d-lg-block">
<div className="px-lg-3 pb-3 pt-5 mt-5">
<div className="pt-1 mt-3">
<img alt="book" id="funding-02" />
<div className="pt-3">
<h6 className="mb-3">{translate("Required")}</h6>
<p>{translate("Some coding experience")}</p>
</div>
</div>
</div>
<div className="px-lg-3 pb-3 pt-5">
<img alt="dollar sign" id="funding-04" />
<div className="pt-3">
<h6 className="mb-3">{translate("Funding Levels")}</h6>
<p>{translate("Prize money and awards")}</p>
</div>
</div>
</div>
{/* end col 2 */}
</div>
</div>
<div className="d-lg-none order-3 mt-4 pt-3">
<Link className="btn btn-primary btn-arrow" to="/community/events">
{translate("See Upcoming Events")}
</Link>
{/* end col 2 */}
</div>
</div>
</section>
{/* Eligibility */}
<section className="container-new py-26">
{/* flex. Col for mobile. Row for large. on large align content to the center */}
<div className="d-flex flex-column flex-lg-row align-items-lg-center mr-lg-4">
<div className="order-1 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0 p-lg-3">
<div className="d-flex flex-column-reverse py-lg-3">
<h3 className="h4 h2-sm">{translate("XRPL Grants")}</h3>
<h6 className="eyebrow mb-3">
{translate("Fund Your Project")}
</h6>
</div>
<p className="py-lg-3 mb-2 longform" style={{ maxWidth: 520 }}>
{translate(
"Developer grants for projects that contribute to the growing XRP Ledger community."
)}
</p>
<div className="mt-4 pt-3" style={{ maxWidth: 520 }}>
<span className="h6" style={{ fontSize: "1rem" }}>
{translate("Past awardees include:")}
</span>
<div className="mb-4 py-3" id="xrplGrantsDark" />
</div>
<div className="d-none d-lg-block py-lg-3">
<a
className="btn btn-primary btn-arrow-out"
target="_blank"
href="https://xrplgrants.org/"
>
{translate("Visit XRPL Grants")}
</a>
</div>
<div className="d-lg-none order-3 mt-4 pt-3">
<Link className="btn btn-primary btn-arrow" to="/community/events">
{translate("See Upcoming Events")}
</Link>
</div>
</div>
</section>
{/* Eligibility */}
<section className="container-new py-26">
{/* flex. Col for mobile. Row for large. on large align content to the center */}
<div className="d-flex flex-column flex-lg-row align-items-lg-center mr-lg-4">
<div className="order-1 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0 p-lg-3">
<div className="d-flex flex-column-reverse py-lg-3">
<h3 className="h4 h2-sm">{translate("XRPL Grants")}</h3>
<h6 className="eyebrow mb-3">
{translate("Fund Your Project")}
</h6>
</div>
<div className="order-2 col-lg-6 px-0 pl-lg-3">
<div className="row align-items-center m-0 funding-list">
{/* funding list */}
<div className="col-12 col-lg-6 p-0">
<div className="px-lg-3 pb-3">
<img alt="user" id="funding-01" />
<div className="pt-3">
<h6 className="mb-3">{translate("Best for")}</h6>
<p>
{translate(
"Software developers, teams, and start-ups building directly on the XRP Ledger"
)}
</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="book" id="funding-02" />
<div className="pt-3">
<h6 className="mb-3">{translate("Required")}</h6>
<p>
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Coding experience")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Github repository")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Project narrative/description")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("At least one developer on the core team")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Budget and milestones")}
</p>
</div>
</div>
<div className="px-lg-3 pb-3 pt-lg-5">
<img alt="arrow" id="funding-03" />
<div className="pt-3">
<h6 className="mb-3">{translate("Level")}</h6>
<p>
{translate("XRPL intermediate to advanced developers")}
</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="dollar sign" id="funding-04" />
<div className="pt-3">
<h6 className="mb-3">{translate("Funding Levels")}</h6>
<p>{translate("$10,000 - $200,000")}</p>
</div>
</div>
</div>
{/* end col 1 */}
{/* Show on large */}
<div className="col-12 col-lg-6 p-0 d-none d-lg-block">
<div className="px-lg-3 pb-3 pt-5 mt-5">
<div className="pt-1 mt-3">
<img alt="book" id="funding-02" />
<div className="pt-3">
<h6 className="mb-3">{translate("Required")}</h6>
<p>
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Coding experience")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Github repository")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Project narrative/description")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("At least one developer on the core team")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Budget and milestones")}
</p>
</div>
</div>
</div>
<div className="px-lg-3 pb-3 pt-5">
<img alt="dollar sign" id="funding-04" />
<div className="pt-3">
<h6 className="mb-3">{translate("Funding Levels")}</h6>
<p>{translate("$10,000 - $200,000")}</p>
</div>
</div>
</div>
{/* end col 2 */}
</div>
<p className="py-lg-3 mb-2 longform" style={{ maxWidth: 520 }}>
{translate(
"Developer grants for projects that contribute to the growing XRP Ledger community."
)}
</p>
<div className="mt-4 pt-3" style={{ maxWidth: 520 }}>
<span className="h6" style={{ fontSize: "1rem" }}>
{translate("Past awardees include:")}
</span>
<div className="mb-4 py-3" id="xrplGrantsDark" />
</div>
<div className="d-lg-none order-3 mt-4 pt-3">
<div className="d-none d-lg-block py-lg-3">
<a
className="btn btn-primary btn-arrow-out"
target="_blank"
@@ -283,52 +170,202 @@ export default function Funding() {
</a>
</div>
</div>
</section>
{/* Accelerator */}
<section className="container-new py-26">
{/* flex. Col for mobile. Row for large. on large align content to the center */}
<div className="d-flex flex-column flex-lg-row align-items-lg-center">
<div
className="order-1 order-lg-2 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0"
style={{ maxWidth: 520 }}
>
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("XRPL Accelerator")}</h3>
<h6 className="eyebrow mb-3">
{translate("Advance your project")}
</h6>
<div className="order-2 col-lg-6 px-0 pl-lg-3">
<div className="row align-items-center m-0 funding-list">
{/* funding list */}
<div className="col-12 col-lg-6 p-0">
<div className="px-lg-3 pb-3">
<img alt="user" id="funding-01" />
<div className="pt-3">
<h6 className="mb-3">{translate("Best for")}</h6>
<p>
{translate(
"Software developers, teams, and start-ups building directly on the XRP Ledger"
)}
</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="book" id="funding-02" />
<div className="pt-3">
<h6 className="mb-3">{translate("Required")}</h6>
<p>
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Coding experience")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Github repository")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Project narrative/description")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("At least one developer on the core team")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Budget and milestones")}
</p>
</div>
</div>
<div className="px-lg-3 pb-3 pt-lg-5">
<img alt="arrow" id="funding-03" />
<div className="pt-3">
<h6 className="mb-3">{translate("Level")}</h6>
<p>
{translate("XRPL intermediate to advanced developers")}
</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="dollar sign" id="funding-04" />
<div className="pt-3">
<h6 className="mb-3">{translate("Funding Levels")}</h6>
<p>{translate("$10,000 - $200,000")}</p>
</div>
</div>
</div>
<p className="p-lg-3 mb-2 longform">
{translate(
"12-week program for entrepreneurs building on the XRP Ledger to scale their projects into thriving businesses."
)}
</p>
<div className="d-none d-lg-block p-lg-3">
<a
className="btn btn-primary btn-arrow"
href="https://xrplaccelerator.org/"
>
{translate("View XRPL Accelerator")}
</a>
</div>
</div>
<div className="order-2 order-lg-1 col-lg-6 px-0">
<div className="row align-items-center m-0 funding-list">
{/* funding list */}
<div className="col-12 col-lg-6 p-0">
<div className="px-lg-3 pb-3">
<img alt="user" id="funding-01" />
{/* end col 1 */}
{/* Show on large */}
<div className="col-12 col-lg-6 p-0 d-none d-lg-block">
<div className="px-lg-3 pb-3 pt-5 mt-5">
<div className="pt-1 mt-3">
<img alt="book" id="funding-02" />
<div className="pt-3">
<h6 className="mb-3">{translate("Best for")}</h6>
<h6 className="mb-3">{translate("Required")}</h6>
<p>
{translate(
"Start-ups building scalable products on XRPL that can capture a large market opportunity"
)}
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Coding experience")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Github repository")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Project narrative/description")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("At least one developer on the core team")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Budget and milestones")}
</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
</div>
<div className="px-lg-3 pb-3 pt-5">
<img alt="dollar sign" id="funding-04" />
<div className="pt-3">
<h6 className="mb-3">{translate("Funding Levels")}</h6>
<p>{translate("$10,000 - $200,000")}</p>
</div>
</div>
</div>
{/* end col 2 */}
</div>
</div>
<div className="d-lg-none order-3 mt-4 pt-3">
<a
className="btn btn-primary btn-arrow-out"
target="_blank"
href="https://xrplgrants.org/"
>
{translate("Visit XRPL Grants")}
</a>
</div>
</div>
</section>
{/* Accelerator */}
<section className="container-new py-26">
{/* flex. Col for mobile. Row for large. on large align content to the center */}
<div className="d-flex flex-column flex-lg-row align-items-lg-center">
<div
className="order-1 order-lg-2 mb-4 pb-3 mb-lg-0 pb-lg-0 col-lg-6 px-0"
style={{ maxWidth: 520 }}
>
<div className="d-flex flex-column-reverse p-lg-3">
<h3 className="h4 h2-sm">{translate("XRPL Accelerator")}</h3>
<h6 className="eyebrow mb-3">
{translate("Advance your project")}
</h6>
</div>
<p className="p-lg-3 mb-2 longform">
{translate(
"12-week program for entrepreneurs building on the XRP Ledger to scale their projects into thriving businesses."
)}
</p>
<div className="d-none d-lg-block p-lg-3">
<a
className="btn btn-primary btn-arrow"
href="https://xrplaccelerator.org/"
>
{translate("View XRPL Accelerator")}
</a>
</div>
</div>
<div className="order-2 order-lg-1 col-lg-6 px-0">
<div className="row align-items-center m-0 funding-list">
{/* funding list */}
<div className="col-12 col-lg-6 p-0">
<div className="px-lg-3 pb-3">
<img alt="user" id="funding-01" />
<div className="pt-3">
<h6 className="mb-3">{translate("Best for")}</h6>
<p>
{translate(
"Start-ups building scalable products on XRPL that can capture a large market opportunity"
)}
</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="book" id="funding-02" />
<div className="pt-3">
<h6 className="mb-3">{translate("Required")}</h6>
<p>
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Strong founding team")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Bold, ambitious vision")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Ideally an MVP and monetization strategy")}
</p>
</div>
</div>
<div className="px-lg-3 pb-3 pt-lg-5">
<img alt="arrow" id="funding-03" />
<div className="pt-3">
<h6 className="mb-3">{translate("Level")}</h6>
<p>
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("XRPL advanced developers")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Business acumen")}
</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="dollar sign" id="funding-04" />
<div className="pt-3">
<h6 className="mb-3">{translate("Funding Levels")}</h6>
<p>
{translate(
"$50,000 (grant) + pitch for venture funding"
)}
</p>
</div>
</div>
</div>
{/* end col 1 */}
{/* Show on large */}
<div className="col-12 col-lg-6 p-0 d-none d-lg-block">
<div className="px-lg-3 pb-3 pt-5 mt-5">
<div className="pt-1 mt-3">
<img alt="book" id="funding-02" />
<div className="pt-3">
<h6 className="mb-3">{translate("Required")}</h6>
@@ -340,92 +377,38 @@ export default function Funding() {
{translate("Bold, ambitious vision")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Ideally an MVP and monetization strategy")}
</p>
</div>
</div>
<div className="px-lg-3 pb-3 pt-lg-5">
<img alt="arrow" id="funding-03" />
<div className="pt-3">
<h6 className="mb-3">{translate("Level")}</h6>
<p>
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("XRPL advanced developers")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Business acumen")}
</p>
</div>
</div>
{/* Hide on large */}
<div className="px-lg-3 pb-3 d-lg-none">
<img alt="dollar sign" id="funding-04" />
<div className="pt-3">
<h6 className="mb-3">{translate("Funding Levels")}</h6>
<p>
{translate(
"$50,000 (grant) + pitch for venture funding"
"Ideally an MVP and monetization strategy"
)}
</p>
</div>
</div>
</div>
{/* end col 1 */}
{/* Show on large */}
<div className="col-12 col-lg-6 p-0 d-none d-lg-block">
<div className="px-lg-3 pb-3 pt-5 mt-5">
<div className="pt-1 mt-3">
<img alt="book" id="funding-02" />
<div className="pt-3">
<h6 className="mb-3">{translate("Required")}</h6>
<p>
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Strong founding team")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate("Bold, ambitious vision")}
<br />
<span style={{ color: "#7919FF" }}></span>{" "}
{translate(
"Ideally an MVP and monetization strategy"
)}
</p>
</div>
</div>
</div>
<div className="px-lg-3 pb-3 pt-5">
<img alt="dollar sign" id="funding-04" />
<div className="pt-3">
<h6 className="mb-3">{translate("Funding Levels")}</h6>
<p>
{translate(
"$50,000 (grant) + pitch for venture funding"
)}
</p>
</div>
<div className="px-lg-3 pb-3 pt-5">
<img alt="dollar sign" id="funding-04" />
<div className="pt-3">
<h6 className="mb-3">{translate("Funding Levels")}</h6>
<p>
{translate(
"$50,000 (grant) + pitch for venture funding"
)}
</p>
</div>
</div>
{/* end col 2 */}
</div>
</div>
<div className="d-lg-none order-3 mt-4 pt-3">
<a
className="btn btn-primary btn-arrow"
href="https://xrplaccelerator.org/"
>
{translate("View XRPL Accelerator")}
</a>
{/* end col 2 */}
</div>
</div>
</section>
<div className="position-relative d-none-sm">
<img
alt="orange waves"
src={require("../static/img/backgrounds/funding-orange.svg")}
id="funding-orange"
/>
<div className="d-lg-none order-3 mt-4 pt-3">
<a
className="btn btn-primary btn-arrow"
href="https://xrplaccelerator.org/"
>
{translate("View XRPL Accelerator")}
</a>
</div>
</div>
</div>
</section>
</div>
);
}

View File

@@ -1375,311 +1375,303 @@ export default function Events() {
return (
<div className="landing page-events">
<div>
<div className="position-relative d-none-sm">
<img
alt="orange waves"
src={require("../static/img/backgrounds/events-orange.svg")}
id="events-orange"
/>
<section className="text-center py-26">
<div className="mx-auto text-center col-lg-5">
<div className="d-flex flex-column-reverse">
<h1 className="mb-0">
{translate("Find the XRPL Community Around the World")}
</h1>
<h6 className="mb-3 eyebrow">{translate("Events")}</h6>
</div>
</div>
<section className="text-center py-26">
<div className="mx-auto text-center col-lg-5">
</section>
<section className="container-new py-26">
<div className="event-hero card-grid card-grid-2xN">
<div className="pe-2 col">
<img
alt="xrp ledger events hero"
src={require("../static/img/events/xrp-community-night.png")}
className="w-100"
/>
</div>
<div className="pt-5 pe-2 col">
<div className="d-flex flex-column-reverse">
<h1 className="mb-0">
{translate("Find the XRPL Community Around the World")}
</h1>
<h6 className="mb-3 eyebrow">{translate("Events")}</h6>
<h2 className="mb-8 h4 h2-sm">
{translate("XRP Community Night NYC")}
</h2>
<h6 className="mb-3 eyebrow">{translate("Save the Date")}</h6>
</div>
</div>
</section>
<section className="container-new py-26">
<div className="event-hero card-grid card-grid-2xN">
<div className="pr-2 col">
<img
alt="xrp ledger events hero"
src={require("../static/img/events/xrp-community-night.png")}
className="w-100"
/>
</div>
<div className="pt-5 pr-2 col">
<div className="d-flex flex-column-reverse">
<h2 className="mb-8 h4 h2-sm">
{translate("XRP Community Night NYC")}
</h2>
<h6 className="mb-3 eyebrow">{translate("Save the Date")}</h6>
</div>
<p className="mb-4">
{translate(
"Join the XRP community in NYC—meet builders, users, and projects innovating on the XRP Ledger."
)}
</p>
<div className=" my-3 event-small-gray">
{translate("Location: New York, NY")}
</div>
<div className="py-2 my-3 event-small-gray">
{translate("November 5, 2025")}
</div>
<div className="d-lg-block">
<a
className="btn btn-primary btn-arrow-out"
target="_blank"
href="https://lu.ma/g5uja58m?utm_source=xrpleventspage"
>
{translate("Register Now")}
</a>
</div>
</div>
</div>
</section>
{/* Upcoming Events */}
<section className="container-new py-26" id="upcoming-events">
<div className="p-0 pb-2 mb-4 d-flex flex-column-reverse col-lg-6 pr-lg-5">
<h3 className="h4 h2-sm">
<p className="mb-4">
{translate(
"Check out meetups, hackathons, and other events hosted by the XRPL Community"
"Join the XRP community in NYC—meet builders, users, and projects innovating on the XRP Ledger."
)}
</h3>
<h6 className="mb-3 eyebrow">{translate("Upcoming Events")}</h6>
</div>
<div className="filter row col-12 mt-lg-5 d-flex flex-column">
<h6 className="mb-3">{translate("Filter By:")}</h6>
<div>
<div className="form-check form-check-inline">
<input
defaultValue="conference"
id="conference-upcoming"
name="conference-upcoming"
type="checkbox"
className="events-filter"
checked={upcomingFilters.conference}
onChange={handleUpcomingFilterChange}
/>
<label htmlFor="conference-upcoming">
{translate("Conference")}
</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="meetup"
id="meetup-upcoming"
name="meetup-upcoming"
type="checkbox"
className="events-filter"
checked={upcomingFilters.meetup}
onChange={handleUpcomingFilterChange}
/>
<label htmlFor="meetup-upcoming">{translate("Meetups")}</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="hackathon"
id="hackathon-upcoming"
name="hackathon-upcoming"
type="checkbox"
className="events-filter"
checked={upcomingFilters.hackathon}
onChange={handleUpcomingFilterChange}
/>
<label htmlFor="hackathon-upcoming">
{translate("Hackathons")}
</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="ama"
id="ama-upcoming"
name="ama-upcoming"
type="checkbox"
className="events-filter"
checked={upcomingFilters.ama}
onChange={handleUpcomingFilterChange}
/>
<label htmlFor="ama-upcoming">{translate("AMAs")}</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="cc"
id="cc-upcoming"
name="cc-upcoming"
type="checkbox"
className="events-filter"
checked={upcomingFilters.cc}
onChange={handleUpcomingFilterChange}
/>
<label htmlFor="cc-upcoming">
{translate("Community Calls")}
</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="zone"
id="zone-upcoming"
name="zone-upcoming"
type="checkbox"
className="events-filter"
checked={upcomingFilters.zone}
onChange={handleUpcomingFilterChange}
/>
<label htmlFor="zone-upcoming">{translate("XRPL Zone")}</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="info"
id="info-upcoming"
name="info-upcoming"
type="checkbox"
className="events-filter"
checked={upcomingFilters["info"]}
onChange={handleUpcomingFilterChange}
/>
<label htmlFor="info-upcoming">
{translate("Info Session")}
</label>
</div>
</p>
<div className=" my-3 event-small-gray">
{translate("Location: New York, NY")}
</div>
<div className="py-2 my-3 event-small-gray">
{translate("November 5, 2025")}
</div>
<div className="d-lg-block">
<a
className="btn btn-primary btn-arrow-out"
target="_blank"
href="https://lu.ma/g5uja58m?utm_source=xrpleventspage"
>
{translate("Register Now")}
</a>
</div>
</div>
{/* # Available Types - conference, hackathon, ama, cc, zone, meetup, info */}
<div className="mt-2 row row-cols-1 row-cols-lg-3 card-deck">
{filteredUpcoming.map((event, i) => (
</div>
</section>
{/* Upcoming Events */}
<section className="container-new py-26" id="upcoming-events">
<div className="p-0 pb-2 mb-4 d-flex flex-column-reverse col-lg-6 pr-lg-5">
<h3 className="h4 h2-sm">
{translate(
"Check out meetups, hackathons, and other events hosted by the XRPL Community"
)}
</h3>
<h6 className="mb-3 eyebrow">{translate("Upcoming Events")}</h6>
</div>
<div className="filter row col-12 mt-lg-5 d-flex flex-column">
<h6 className="mb-3">{translate("Filter By:")}</h6>
<div>
<div className="form-check form-check-inline">
<input
defaultValue="conference"
id="conference-upcoming"
name="conference-upcoming"
type="checkbox"
className="events-filter"
checked={upcomingFilters.conference}
onChange={handleUpcomingFilterChange}
/>
<label htmlFor="conference-upcoming">
{translate("Conference")}
</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="meetup"
id="meetup-upcoming"
name="meetup-upcoming"
type="checkbox"
className="events-filter"
checked={upcomingFilters.meetup}
onChange={handleUpcomingFilterChange}
/>
<label htmlFor="meetup-upcoming">{translate("Meetups")}</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="hackathon"
id="hackathon-upcoming"
name="hackathon-upcoming"
type="checkbox"
className="events-filter"
checked={upcomingFilters.hackathon}
onChange={handleUpcomingFilterChange}
/>
<label htmlFor="hackathon-upcoming">
{translate("Hackathons")}
</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="ama"
id="ama-upcoming"
name="ama-upcoming"
type="checkbox"
className="events-filter"
checked={upcomingFilters.ama}
onChange={handleUpcomingFilterChange}
/>
<label htmlFor="ama-upcoming">{translate("AMAs")}</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="cc"
id="cc-upcoming"
name="cc-upcoming"
type="checkbox"
className="events-filter"
checked={upcomingFilters.cc}
onChange={handleUpcomingFilterChange}
/>
<label htmlFor="cc-upcoming">
{translate("Community Calls")}
</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="zone"
id="zone-upcoming"
name="zone-upcoming"
type="checkbox"
className="events-filter"
checked={upcomingFilters.zone}
onChange={handleUpcomingFilterChange}
/>
<label htmlFor="zone-upcoming">{translate("XRPL Zone")}</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="info"
id="info-upcoming"
name="info-upcoming"
type="checkbox"
className="events-filter"
checked={upcomingFilters["info"]}
onChange={handleUpcomingFilterChange}
/>
<label htmlFor="info-upcoming">
{translate("Info Session")}
</label>
</div>
</div>
</div>
{/* # Available Types - conference, hackathon, ama, cc, zone, meetup, info */}
<div className="row row-cols-1 row-cols-lg-3 g-4 mt-2">
{filteredUpcoming.map((event, i) => (
<div key={event.name + i} className="col">
<a
key={event.name + i}
className={`event-card ${event.type}`}
className={`event-card ${event.type} h-100`}
href={event.link}
style={{}}
target="_blank"
>
<div
className="event-card-header"
style={{
background: `url(${event.image}) no-repeat`,
}}
>
<div className="event-card-title">
{translate(event.name)}
</div>
<div
className="event-card-header"
style={{
background: `url(${event.image}) no-repeat`,
}}
>
<div className="event-card-title">
{translate(event.name)}
</div>
<div className="event-card-body">
<p>{translate(event.description)}</p>
</div>
<div className="mt-lg-auto event-card-footer d-flex flex-column">
<span className="mb-2 d-flex icon icon-location">
{event.location}
</span>
<span className="d-flex icon icon-date">{event.date}</span>
</div>
</a>
))}
</div>
<div className="event-card-body">
<p>{translate(event.description)}</p>
</div>
<div className="mt-lg-auto event-card-footer d-flex flex-column">
<span className="mb-2 d-flex icon icon-location">
{event.location}
</span>
<span className="d-flex icon icon-date">{event.date}</span>
</div>
</a>
</div>
</section>
{/* Past Events */}
<section className="container-new pt-26" id="past-events">
<div className="p-0 pb-2 mb-4 d-flex flex-column-reverse col-lg-6 pr-lg-5">
<h3 className="h4 h2-sm">
{translate("Explore past community-hosted events")}
</h3>
<h6 className="mb-3 eyebrow">{translate("Past Events")}</h6>
</div>
<div className="filter row col-12 mt-lg-5 d-flex flex-column">
<h6 className="mb-3">{translate("Filter By:")}</h6>
<div>
<div className="form-check form-check-inline">
<input
defaultValue="conference"
id="conference-past"
name="conference-past"
type="checkbox"
className="events-filter"
checked={pastFilters.conference}
onChange={handlePastFilterChange}
/>
<label htmlFor="conference-past">
{translate("Conference")}
</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="meetup"
id="meetup-past"
name="meetup-past"
type="checkbox"
className="events-filter"
checked={pastFilters.meetup}
onChange={handlePastFilterChange}
/>
<label htmlFor="meetup-past">{translate("Meetups")}</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="hackathon"
id="hackathon-past"
name="hackathon-past"
type="checkbox"
className="events-filter"
checked={pastFilters.hackathon}
onChange={handlePastFilterChange}
/>
<label htmlFor="hackathon-past">
{translate("Hackathons")}
</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="ama"
id="ama-past"
name="ama-past"
type="checkbox"
className="events-filter"
checked={pastFilters.ama}
onChange={handlePastFilterChange}
/>
<label htmlFor="ama-past">{translate("AMAs")}</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="cc"
id="cc-past"
name="cc-past"
type="checkbox"
className="events-filter"
checked={pastFilters.cc}
onChange={handlePastFilterChange}
/>
<label htmlFor="cc-past">{translate("Community Calls")}</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="zone"
id="zone-past"
name="zone-past"
type="checkbox"
className="events-filter"
checked={pastFilters.zone}
onChange={handlePastFilterChange}
/>
<label htmlFor="zone-past">{translate("XRPL Zone")}</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="info"
id="info-past"
name="info-past"
type="checkbox"
className="events-filter"
checked={pastFilters["info"]}
onChange={handlePastFilterChange}
/>
<label htmlFor="info-past">
{translate("Info Session")}
</label>
</div>
))}
</div>
</section>
{/* Past Events */}
<section className="container-new pt-26" id="past-events">
<div className="p-0 pb-2 mb-4 d-flex flex-column-reverse col-lg-6 pr-lg-5">
<h3 className="h4 h2-sm">
{translate("Explore past community-hosted events")}
</h3>
<h6 className="mb-3 eyebrow">{translate("Past Events")}</h6>
</div>
<div className="filter row col-12 mt-lg-5 d-flex flex-column">
<h6 className="mb-3">{translate("Filter By:")}</h6>
<div>
<div className="form-check form-check-inline">
<input
defaultValue="conference"
id="conference-past"
name="conference-past"
type="checkbox"
className="events-filter"
checked={pastFilters.conference}
onChange={handlePastFilterChange}
/>
<label htmlFor="conference-past">
{translate("Conference")}
</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="meetup"
id="meetup-past"
name="meetup-past"
type="checkbox"
className="events-filter"
checked={pastFilters.meetup}
onChange={handlePastFilterChange}
/>
<label htmlFor="meetup-past">{translate("Meetups")}</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="hackathon"
id="hackathon-past"
name="hackathon-past"
type="checkbox"
className="events-filter"
checked={pastFilters.hackathon}
onChange={handlePastFilterChange}
/>
<label htmlFor="hackathon-past">
{translate("Hackathons")}
</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="ama"
id="ama-past"
name="ama-past"
type="checkbox"
className="events-filter"
checked={pastFilters.ama}
onChange={handlePastFilterChange}
/>
<label htmlFor="ama-past">{translate("AMAs")}</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="cc"
id="cc-past"
name="cc-past"
type="checkbox"
className="events-filter"
checked={pastFilters.cc}
onChange={handlePastFilterChange}
/>
<label htmlFor="cc-past">{translate("Community Calls")}</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="zone"
id="zone-past"
name="zone-past"
type="checkbox"
className="events-filter"
checked={pastFilters.zone}
onChange={handlePastFilterChange}
/>
<label htmlFor="zone-past">{translate("XRPL Zone")}</label>
</div>
<div className="form-check form-check-inline">
<input
defaultValue="info"
id="info-past"
name="info-past"
type="checkbox"
className="events-filter"
checked={pastFilters["info"]}
onChange={handlePastFilterChange}
/>
<label htmlFor="info-past">
{translate("Info Session")}
</label>
</div>
</div>
<div className="mt-2 mb-0 row row-cols-1 row-cols-lg-3 card-deck ">
{filteredPast.map((event, i) => (
</div>
<div className="row row-cols-1 row-cols-lg-3 g-4 mt-2 mb-0">
{filteredPast.map((event, i) => (
<div key={event.name + i} className="col">
<a
key={event.name + i}
className="event-card {event.type}"
className={`event-card ${event.type} h-100`}
href={event.link}
target="_blank"
>
@@ -1700,13 +1692,13 @@ export default function Events() {
<span className="mb-2 d-flex icon icon-location">
{event.location}
</span>
<span className="d-flex icon icon-date">{event.date}</span>
</div>
</a>
))}
</div>
</section>
</div>
<span className="d-flex icon icon-date">{event.date}</span>
</div>
</a>
</div>
))}
</div>
</section>
</div>
);
}

View File

@@ -630,7 +630,7 @@ const CommunityPage: React.FC = () => {
/>
</div>
<div className="mx-auto text-left col-lg-6 text-md-center hero-title">
<div className="mx-auto text-start col-lg-6 text-md-center hero-title">
<div className="d-flex flex-column-reverse align-items-center sm-align-items-start">
<img
src={require("../static/img/icons/arrow-down.svg")}
@@ -882,7 +882,6 @@ const CommunityPage: React.FC = () => {
{/* Bottom Cards Section 2 cards */}
<section className="bottom-cards-section bug-bounty">
<div className="com-card ripplex-bug-bounty">
<img className="top-right-img bug-bounty-card-bg" alt="Top Right Image" />
<div className="card-content">
<h6 className="card-title">
{translate("RippleX Bug Bounty Program")}
@@ -922,7 +921,6 @@ const CommunityPage: React.FC = () => {
</div>
</div>
<div className="com-card">
<img className="bottom-right-img bug-bounty-card-bg-2" alt="Bottom Right Image" />
<div className="card-content">
<h6 className="card-title">{translate("Report a Scam")}</h6>
<h6 className="card-subtitle pr-bt28">
@@ -950,7 +948,6 @@ const CommunityPage: React.FC = () => {
{/* Bottom Cards Section */}
<section className="bottom-cards-section">
<div className="com-card">
<img className="top-left-img" alt="Top Left Image" />
<div className="card-content">
<h6 className="card-title">
{translate("Contribute to Consensus")}
@@ -996,7 +993,6 @@ const CommunityPage: React.FC = () => {
</div>
</div>
<div className="com-card">
<img className="bottom-right-img" alt="Bottom Right Image" />
<div className="card-content">
<h6 className="card-title">{translate("XRPL Careers")}</h6>
<h6 className="card-subtitle pr-bt16">
@@ -1021,7 +1017,6 @@ const CommunityPage: React.FC = () => {
</div>
</div>
<div className="com-card">
<img className="top-right-img" alt="Top Right Image" />
<div className="card-content">
<h6 className="card-title">
{translate("Contribute to XRPL.org")}

View File

@@ -396,8 +396,6 @@ export default function Docs() {
</div>
<div className="col">
<div className="card cta-card p-8-sm p-10-until-sm br-8">
<img src={require('../static/img/backgrounds/cta-home-purple.svg')} className="d-none-sm cta cta-top-left" />
<img src={require('../static/img/backgrounds/cta-home-green.svg')} className="cta cta-bottom-right" />
<div className="z-index-1 position-relative">
<h2 className="h4 mb-8-sm mb-10-until-sm">{translate('Get Free Test XRP')}</h2>
<p className="mb-10">

View File

@@ -1106,7 +1106,7 @@ const { sendXrp } = require('./library/7_helpers')
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="send-xrp-modal-label">Send XRP</h1>
<h1 class="modal-title subhead-sm-r" id="send-xrp-modal-label">Send XRP</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">

View File

@@ -221,7 +221,7 @@ const PaymentsPage: React.FC = () => {
</div>
<div className="text-content">
<h6 className="eyebrow mb-3 text-large">
<h6 className="eyebrow mb-3 subhead-sm-r">
{translate("Payments")}
</h6>
<h2 className="h4 h2-sm mb-10">

View File

@@ -199,7 +199,7 @@ export default function Tokenization() {
"Work with a variety of tokens supported by the XRP Ledger."
)}
</h2>
<h6 className="eyebrow mb-3 text-large">
<h6 className="eyebrow mb-3 subhead-sm-r">
{translate("Tokenization")}
</h6>
</div>
@@ -220,7 +220,7 @@ export default function Tokenization() {
{translate("Quick Start")}
</Link>{" "}
<a
className="ml-4 video-external-link btn-none"
className="ms-4 video-external-link btn-none"
target="_blank"
href="https://www.youtube.com/playlist?list=PLJQ55Tj1hIVZtJ_JdTvSum2qMTsedWkNi"
>
@@ -273,7 +273,7 @@ export default function Tokenization() {
target="_blank"
href={article.url}
>
<div className="time h4 normal mb-8">
<div className="time h4 mb-8">
{translate(article.time)}
</div>
<div className="h5 mb-4">{translate(article.title)}</div>

View File

@@ -399,13 +399,6 @@ function TokenHeroSection() {
const { translate } = useTranslate();
return (
<section className="token-hero-section">
<div className="position-relative d-none-sm">
<img
alt="orange waves"
src={require("./../../../static/img/backgrounds/events-orange.svg")}
id="events-orange"
/>
</div>
<div className="token-title-container">
<h1 className="token-title">
{translate("Real-World Asset (RWA) Tokenization")}

View File

@@ -1,6 +1,7 @@
import { useThemeHooks } from '@redocly/theme/core/hooks';
import { Link } from '@redocly/theme/components/Link/Link';
import { BenefitsSection } from 'shared/components/benefits-section';
import { PageGrid, PageGridCol, PageGridRow } from 'shared/components/PageGrid/page-grid';
export const frontmatter = {
seo: {
@@ -117,7 +118,7 @@ export default function Index() {
</div>
<div className="col-lg-6 mx-auto text-center pl-0 pr-0">
<div className="d-flex flex-column-reverse">
<h1 className="mb-10">
<h1 className="display-lg mb-10">
{translate('home.hero.h1part1', 'The Blockchain')}
<br className="until-sm" />
{translate('home.hero.h1part2', 'Built for Business')}
@@ -129,12 +130,8 @@ export default function Index() {
</Link>
</div>
</section>
<div className="position-relative d-none-sm">
<img src={require('./static/img/backgrounds/home-purple.svg')} id="home-purple" loading="lazy" />
<img src={require('./static/img/backgrounds/home-green.svg')} id="home-green" loading="lazy" />
</div>
<section className="container-new py-26">
<div className="col-lg-6 offset-lg-3 pl-0-sm pr-0-sm p-8-sm p-10-until-sm">
<PageGrid className="py-26">
<PageGrid.Col span={{ base: 12, lg: 6 }} offset={{ lg: 3 }}>
<h2 className="h4 mb-8 h2-sm">{translate('The XRP Ledger: The Blockchain Built for Business')}</h2>
<h6 className="longform mb-10">
{translate(
@@ -146,8 +143,8 @@ export default function Index() {
'Proven reliable over more than a decade of error-free functioning, the XRPL offers streamlined development, low transaction costs, high performance, and sustainability. So you can build with confidenceand move your most critical projects forward.'
)}
</p>
</div>
</section>
</PageGrid.Col>
</PageGrid>
<BenefitsSection
eyebrow="Benefits"
title="Why developers choose the XRP Ledger"
@@ -194,8 +191,6 @@ export default function Index() {
</section>
<section className="container-new py-26">
<div className="col-lg-6 offset-lg-3 p-6-sm p-10-until-sm br-8 cta-card">
<img src={require('./static/img/backgrounds/cta-home-purple.svg')} className="d-none-sm cta cta-top-left" />
<img src={require('./static/img/backgrounds/cta-home-green.svg')} className="cta cta-bottom-right" />
<div className="z-index-1 position-relative">
<h2 className="h4 mb-8-sm mb-10-until-sm">{translate('Our Shared Vision for XRPLs Future')}</h2>
<p className="mb-10">
@@ -232,7 +227,6 @@ export default function Index() {
</section>
<section className="container-new py-26">
<div className="col-md-6 offset-md-3 p-8-sm p-10-until-sm br-8 cta-card">
<img alt="" src={require('./static/img/backgrounds/cta-home-magenta.svg')} className="cta cta-bottom-right" />
<div className="z-index-1 position-relative">
<div className="d-flex flex-column-reverse">
<h2 className="h4 mb-8-sm mb-10-until-sm">

2927
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,10 @@
"type": "module",
"description": "The XRP Ledger Dev Portal is the authoritative source for XRP Ledger documentation, including the `rippled` server, client libraries, and other open-source XRP Ledger software.",
"scripts": {
"build-css": "sass --load-path styles/scss styles/xrpl.scss ./static/css/devportal2024-v1.css --style compressed --no-source-map",
"build-css-watch": "sass --watch --load-path styles/scss styles/xrpl.scss ./static/css/devportal2024-v1.css --style compressed --no-source-map",
"analyze-css": "node scripts/analyze-css.js",
"build-css": "sass --load-path styles/scss --load-path . styles/xrpl.scss ./static/css/devportal2024-v1.tmp.css && NODE_ENV=production postcss ./static/css/devportal2024-v1.tmp.css -o ./static/css/devportal2024-v1.css && rm -f ./static/css/devportal2024-v1.tmp.css",
"build-css:dev": "sass --load-path styles/scss --load-path . styles/xrpl.scss ./static/css/devportal2024-v1.tmp.css --source-map && postcss ./static/css/devportal2024-v1.tmp.css -o ./static/css/devportal2024-v1.css && rm -f ./static/css/devportal2024-v1.tmp.css",
"build-css:watch": "sass --watch --load-path styles/scss --load-path . styles/xrpl.scss:static/css/devportal2024-v1.css --source-map",
"start": "realm develop"
},
"keywords": [],
@@ -34,11 +36,19 @@
},
"overrides": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
"react-dom": "^19.1.0",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"devDependencies": {
"bootstrap": "^4.6.2",
"@fullhuman/postcss-purgecss": "^7.0.2",
"autoprefixer": "^10.4.21",
"bootstrap": "^5.3.3",
"cssnano": "^7.1.1",
"htmltojsx": "^0.3.0",
"sass": "1.26.10"
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1",
"sass": "^1.93.2"
}
}

154
postcss.config.cjs Normal file
View File

@@ -0,0 +1,154 @@
/**
* PostCSS Configuration
*
* Processes compiled Sass output through:
* 1. PurgeCSS - Removes unused CSS selectors
* 2. Autoprefixer - Adds vendor prefixes for browser compatibility
* 3. cssnano - Minifies and optimizes CSS (production only)
*/
const purgecss = require('@fullhuman/postcss-purgecss').default;
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
plugins: [
// Only run PurgeCSS in production or when explicitly enabled
...(isProduction || process.env.PURGECSS === 'true'
? [
purgecss({
// Scan all content files for class names
content: [
'./**/*.tsx',
'./**/*.ts',
'./**/*.md',
'./**/*.yaml',
'./**/*.html',
'./static/js/**/*.js',
'./static/vendor/**/*.js',
// Ignore node_modules except for specific libraries that inject classes
'!./node_modules/**/*',
],
// Default extractor - looks for class names in content
defaultExtractor: content => {
// Match all words, including those with dashes and numbers
const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [];
// Match class names in className="..." or class="..."
const classMatches = content.match(/(?:class|className)=["']([^"']*)["']/g) || [];
const classes = classMatches.flatMap(match => {
const m = match.match(/["']([^"']*)["']/);
return m ? m[1].split(/\s+/) : [];
});
return [...broadMatches, ...classes];
},
// Safelist - classes that should never be removed
safelist: {
// Standard safelist - dynamic state classes
standard: [
'html',
'body',
'light',
'dark',
'show',
'hide',
'active',
'disabled',
'open',
'collapsed',
'collapsing',
'lang-ja', // Japanese language class
/^lang-/, // All language classes
// Common Bootstrap utility patterns that should always be kept
/^container/, // All container classes
/^row$/, // Row class
/^col-/, // Column classes
/^bds-grid__col/, // PageGrid column classes (dynamic span values)
/^bds-grid__offset/, // PageGrid offset classes
/^bds-[a-z0-9-]+--/, // BDS BEM modifier classes (e.g. bds-callout-media-banner--green, bds-tile-link--primary)
/^g-/, // Gap utilities
/^p-/, // Padding utilities
/^m-/, // Margin utilities
/^px-/, /^py-/, /^pt-/, /^pb-/, /^ps-/, /^pe-/, // Directional padding
/^mx-/, /^my-/, /^mt-/, /^mb-/, /^ms-/, /^me-/, // Directional margin
/^d-/, // Display utilities
/^flex-/, // Flexbox utilities
/^justify-/, // Justify content
/^align-/, // Align items
/^w-/, // Width utilities
/^h-/, // Height utilities
/^text-/, // Text utilities
/^bg-/, // Background utilities
/^border/, // Border utilities
/^rounded/, // Border radius
],
// Deep safelist - MINIMAL - only truly dynamic components
deep: [
// Bootstrap JS components (only if actually used with JS)
/dropdown-menu/,
/dropdown-item/,
/modal-backdrop/,
/fade/,
// Third-party libraries
/cm-/,
/CodeMirror/,
/lottie/,
],
// Greedy safelist - VERY MINIMAL
greedy: [
/data-theme/, // Theme switching
],
},
// Reject specific patterns - don't remove these even if not found
rejected: [],
// Variables - keep CSS custom properties
variables: true,
// Keyframes - keep animation keyframes
keyframes: true,
// Font-face rules
fontFace: true,
}),
]
: []),
// Autoprefixer - adds vendor prefixes
autoprefixer({
overrideBrowserslist: [
'>0.2%',
'not dead',
'not op_mini all',
'last 2 versions',
],
}),
// cssnano - minification (production only)
...(isProduction
? [
cssnano({
preset: [
'default',
{
discardComments: {
removeAll: true,
},
normalizeWhitespace: true,
colormin: true,
minifySelectors: true,
},
],
}),
]
: []),
],
};

View File

@@ -54,16 +54,12 @@ scripts:
head:
- src: https://cmp.osano.com/AzyjT6TIZMlgyLyy8/f11f7772-8ed5-4b73-bd17-c0814edcc440/osano.js
- src: ./static/js/xrpl-2.11.0.min.js
- src: ./static/vendor/jquery-3.7.1.min.js
# - src: ./static/vendor/jquery-3.7.1.min.js
- src: ./static/vendor/bootstrap.min.js
- src: ./static/js/osano.js
type: text/javascript
links:
- href: https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700&display=swap
rel: stylesheet
- href: https://fonts.googleapis.com/css?family=Source+Code+Pro:300,400,600,700&display=swap
rel: stylesheet
- href: https://fonts.googleapis.com/css?family=Space+Grotesk:300,400,600,700&display=swap
- href: https://fonts.googleapis.com/css2?family=Noto+Sans:wght@300;400;500;600;700&family=Noto+Serif:wght@400;500;600;700&family=Noto+Sans+JP:wght@300;400;500;600;700&display=swap
rel: stylesheet
- href: ./static/css/devportal2024-v1.css
rel: stylesheet

View File

@@ -40,25 +40,17 @@ export default function CodeSamples() {
{/* <a className="mt-12 btn btn-primary btn-arrow">Submit Code Samples</a> */}
</div>
</section>
<div className="position-relative d-none-sm">
<img
alt="orange waves"
src={require('../static/img/backgrounds/xrpl-overview-orange.svg')}
id="xrpl-overview-orange"
/>
</div>
<section className="container-new py-26">
<div className="d-flex flex-column col-sm-8 p-0">
<h3 className="h4 h2-sm">
{translate('Browse sample code for building common use cases on the XRP Ledger')}
</h3>
</div>
<div className="row col-12 card-deck mt-10" id="code-samples-deck">
<div className="row col-md-12 px-0" id="code_samples_list">
{codeSamples.map(card => (
<div className="row gx-4 gy-5 mt-10 mb-20" id="code-samples-deck">
{codeSamples.map(card => (
<div key={card.href} className="col-12 col-lg-6 mb-4">
<a
key={card.href}
className={`card cardtest col-12 col-lg-5 ${card.langs.join(' ')}`}
className={`card cardtest h-100 ${card.langs.join(' ')}`}
href={target.github_forkurl + `/tree/${target.github_branch}/${card.href}`.replace('/content','')}
>
<div className="card-header">
@@ -72,10 +64,9 @@ export default function CodeSamples() {
<h4 className="card-title h5">{card.title}</h4>
<p className="card-text">{card.description}</p>
</div>
<div className="card-footer">&nbsp;</div>
</a>
))}
</div>
</div>
))}
</div>
</section>
<section className="container-new py-26">
@@ -86,8 +77,8 @@ export default function CodeSamples() {
{translate('Help the XRPL community by submitting your own code samples')}
</h6>
</div>
<div className="row pl-4">
<div className=" col-lg-3 pl-4 pl-lg-0 pr-4 contribute dot contribute_1">
<div className="row ps-4">
<div className=" col-lg-3 ps-4 ps-lg-0 pe-4 contribute dot contribute_1">
<span className="dot" />
<h5 className="pb-4 pt-md-5">{translate('Fork and clone')}</h5>
<p className="pb-4">
@@ -98,7 +89,7 @@ export default function CodeSamples() {
{translate('resources.contribute.1.part3', '. Using git, clone the fork to your computer.')}
</p>
</div>
<div className=" col-lg-3 pl-4 pl-lg-0 pr-4 contribute dot contribute_2">
<div className=" col-lg-3 ps-4 ps-lg-0 pe-4 contribute dot contribute_2">
<span className="dot" />
<h5 className="pb-4 pt-md-5">{translate('Add to folder')}</h5>
<p className="pb-4">

View File

@@ -62,8 +62,8 @@ export function CurlButton ({selectedConnection, currentBody}: CurlButtonProps)
return <>
<button
className="btn btn-outline-secondary curl"
data-toggle="modal"
data-target="#wstool-1-curl"
data-bs-toggle="modal"
data-bs-target="#wstool-1-curl"
title={translate("cURL Syntax")}
onClick={() => setShowCurlModal(true)}
>

View File

@@ -62,8 +62,8 @@ export function PermalinkButton ({currentBody, selectedConnection}: PermaLinkBut
return <>
<button
className="btn btn-outline-secondary permalink"
data-toggle="modal"
data-target="#wstool-1-permalink"
data-bs-toggle="modal"
data-bs-target="#wstool-1-permalink"
title={translate("Permalink")}
onClick={() => setShowPermalinkModal(true)}
>

View File

@@ -146,8 +146,8 @@ export default function DevTools() {
<button
className="nav-link active dev-tools-tab"
id="explorers-tab"
data-toggle="tab"
data-target="#explorers"
data-bs-toggle="tab"
data-bs-target="#explorers"
role="tab"
aria-controls="explorers"
aria-selected="true"
@@ -159,8 +159,8 @@ export default function DevTools() {
<button
className="nav-link dev-tools-tab"
id="api-access-tab"
data-toggle="tab"
data-target="#api-access"
data-bs-toggle="tab"
data-bs-target="#api-access"
role="tab"
aria-controls="api-access"
aria-selected="false"
@@ -172,8 +172,8 @@ export default function DevTools() {
<button
className="nav-link dev-tools-tab"
id="other-tab"
data-toggle="tab"
data-target="#other"
data-bs-toggle="tab"
data-bs-target="#other"
role="tab"
aria-controls="other"
aria-selected="false"
@@ -278,16 +278,6 @@ export default function DevTools() {
</section>
<section className="container-new py-10 px-0">
<div className="col-lg-12 p-6-sm p-10-until-sm br-8 cta-card">
<img
alt="purple waves"
src={require("../../static/img/backgrounds/cta-home-purple.svg")}
className="d-none-sm cta cta-top-left"
/>
<img
alt="green waves"
src={require("../../static/img/backgrounds/cta-home-green.svg")}
className="cta cta-bottom-right"
/>
<div className="z-index-1 position-relative">
<h2 className="h4 mb-8-sm mb-10-until-sm">
{translate("Have an Idea For a Tool?")}

View File

@@ -251,7 +251,7 @@ export function WebsocketApiTool() {
className="btn-toolbar justify-content-between pt-4"
role="toolbar"
>
<div className="btn-group mr-3" role="group">
<div className="btn-group me-3" role="group">
<button
className="btn btn-outline-secondary send-request"
onClick={() => sendWebSocketMessage(currentBody)}
@@ -272,8 +272,8 @@ export function WebsocketApiTool() {
connected ? "btn-success" : "btn-outline-secondary"
} ${connectionError ?? "btn-danger"}`}
onClick={openConnectionModal}
data-toggle="modal"
data-target="#wstool-1-connection-settings"
data-bs-toggle="modal"
data-bs-target="#wstool-1-connection-settings"
>
{`${selectedConnection.shortname}${
connected ? ` (${translate('Connected')})` : ` (${translate('Not Connected')})`

View File

@@ -184,7 +184,7 @@ function TestCredentials({selectedFaucet, translate}) {
setBalance,
setSequence,
translate)
} className="btn btn-primary mr-2 mb-2">
} className="btn btn-primary me-2 mb-2">
{`${translate('resources.dev-tools.faucet.cred-btn.part1', 'Generate ')}${selectedFaucet.shortName}${translate('resources.dev-tools.faucet.cred-btn.part2', ' credentials')}`}
</button>
</div>

166
scripts/analyze-css.js Normal file
View File

@@ -0,0 +1,166 @@
#!/usr/bin/env node
/**
* CSS Analysis Script
*
* Analyzes the compiled CSS bundle to identify:
* - Total size and line count
* - Bootstrap vs custom CSS breakdown
* - Most common selectors and patterns
* - Potential optimization opportunities
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const CSS_FILE = path.join(__dirname, '../static/css/devportal2024-v1.css');
function analyzeCSS() {
console.log('🔍 Analyzing CSS Bundle...\n');
if (!fs.existsSync(CSS_FILE)) {
console.error(`❌ CSS file not found: ${CSS_FILE}`);
console.log('💡 Run "npm run build-css" first to generate the CSS bundle.');
process.exit(1);
}
const css = fs.readFileSync(CSS_FILE, 'utf-8');
const stats = fs.statSync(CSS_FILE);
// Basic stats
const lines = css.split('\n').length;
const sizeKB = (stats.size / 1024).toFixed(2);
const selectors = css.match(/[^{}]+(?=\{)/g) || [];
const uniqueSelectors = new Set(selectors.map(s => s.trim())).size;
console.log('📊 Bundle Statistics:');
console.log('━'.repeat(50));
console.log(` Size: ${sizeKB} KB`);
console.log(` Lines: ${lines.toLocaleString()}`);
console.log(` Total Selectors: ${selectors.length.toLocaleString()}`);
console.log(` Unique Selectors: ${uniqueSelectors.toLocaleString()}`);
console.log('');
// Bootstrap component detection
const bootstrapPatterns = {
'Grid System': /\.(container|row|col-)/g,
'Buttons': /\.btn-/g,
'Forms': /\.(form-control|form-select|form-check)/g,
'Cards': /\.card-/g,
'Navbar': /\.navbar-/g,
'Dropdown': /\.dropdown-/g,
'Modal': /\.modal-/g,
'Alert': /\.alert-/g,
'Badge': /\.badge-/g,
'Breadcrumb': /\.breadcrumb/g,
'Pagination': /\.page-/g,
'Accordion': /\.accordion/g,
'Carousel': /\.carousel/g,
'Tooltip': /\.tooltip/g,
'Popover': /\.popover/g,
'Toast': /\.toast/g,
'Spinner': /\.spinner-/g,
};
console.log('🎨 Bootstrap Component Usage:');
console.log('━'.repeat(50));
const componentUsage = [];
for (const [component, pattern] of Object.entries(bootstrapPatterns)) {
const matches = css.match(pattern);
const count = matches ? matches.length : 0;
componentUsage.push({ component, count });
}
componentUsage.sort((a, b) => b.count - a.count);
componentUsage.forEach(({ component, count }) => {
const bar = '█'.repeat(Math.min(Math.floor(count / 10), 40));
console.log(` ${component.padEnd(20)} ${count.toString().padStart(4)} ${bar}`);
});
console.log('');
// Custom classes analysis
const customPatterns = [
{ name: 'Dev Tools', pattern: /\.(rpc-tool|websocket|code-tab)/g },
{ name: 'Navigation', pattern: /\.(top-nav|side-nav|breadcrumb)/g },
{ name: 'Content', pattern: /\.(content-|landing-|page-)/g },
{ name: 'Cards', pattern: /\.(card-deck|project-card)/g },
{ name: 'Video', pattern: /\.video-/g },
{ name: 'Blog', pattern: /\.blog-/g },
];
console.log('🎯 Custom Component Patterns:');
console.log('━'.repeat(50));
customPatterns.forEach(({ name, pattern }) => {
const matches = css.match(pattern);
const count = matches ? matches.length : 0;
if (count > 0) {
console.log(` ${name.padEnd(20)} ${count.toString().padStart(4)}`);
}
});
console.log('');
// Optimization recommendations
console.log('💡 Optimization Recommendations:');
console.log('━'.repeat(50));
const recommendations = [];
// Check for unused Bootstrap components
const lowUsageComponents = componentUsage.filter(c => c.count < 5 && c.count > 0);
if (lowUsageComponents.length > 0) {
recommendations.push({
priority: 'HIGH',
message: `${lowUsageComponents.length} Bootstrap components with <5 usages detected`,
action: 'Consider manually importing only needed Bootstrap modules'
});
}
const noUsageComponents = componentUsage.filter(c => c.count === 0);
if (noUsageComponents.length > 0) {
recommendations.push({
priority: 'HIGH',
message: `${noUsageComponents.length} Bootstrap components with 0 usages detected`,
action: 'Remove unused components from Bootstrap import'
});
}
if (sizeKB > 200) {
recommendations.push({
priority: 'CRITICAL',
message: 'Bundle size exceeds 200KB',
action: 'Implement PurgeCSS to remove unused styles'
});
}
recommendations.push({
priority: 'MEDIUM',
message: 'No code splitting detected',
action: 'Consider splitting vendor CSS from custom styles'
});
recommendations.forEach(({ priority, message, action }) => {
const emoji = priority === 'CRITICAL' ? '🔴' : priority === 'HIGH' ? '🟡' : '🔵';
console.log(` ${emoji} [${priority}] ${message}`);
console.log(`${action}`);
console.log('');
});
// Estimate potential savings
const estimatedReduction = Math.round(parseFloat(sizeKB) * 0.75);
const estimatedFinal = Math.round(parseFloat(sizeKB) * 0.25);
console.log('📈 Estimated Optimization Potential:');
console.log('━'.repeat(50));
console.log(` Current Size: ${sizeKB} KB`);
console.log(` Potential Savings: ~${estimatedReduction} KB (75%)`);
console.log(` Expected Size: ~${estimatedFinal} KB`);
console.log('');
}
analyzeCSS();

View File

@@ -0,0 +1,652 @@
# Button Component Documentation
## Overview
The Button component is a scalable, accessible button implementation following the XRPL Brand Design System (BDS). It supports three visual variants (Primary, Secondary, Tertiary) and two color themes (Green, Black), with comprehensive state management and smooth animations.
## Features
- **Three Variants**: Primary (solid), Secondary (outline), Tertiary (text-only)
- **Two Color Themes**: Green (default) and Black
- **Link Support**: Can render as anchor elements for navigation via `href` prop
- **Animated Arrow Icon**: Optional icon with smooth hover animations
- **Full State Support**: Enabled, Hover, Focus, Active, and Disabled states
- **Responsive Design**: Adaptive padding and spacing across breakpoints
- **Accessibility**: WCAG compliant with keyboard navigation and screen reader support
- **Smooth Animations**: 150ms transitions with custom bezier timing
## Props API
```typescript
interface ButtonProps {
/** Button variant - determines visual style */
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 */
onClick?: () => void;
/** Disabled state */
disabled?: boolean;
/** Button type attribute */
type?: 'button' | 'submit' | 'reset';
/** Additional CSS classes */
className?: string;
/** Whether to show the arrow icon */
showIcon?: boolean;
/** Accessibility label - defaults to button text if not provided */
ariaLabel?: string;
/** URL to navigate to - renders as a Link instead of button */
href?: string;
/** Link target - only applies when href is provided */
target?: '_self' | '_blank';
}
```
### Default Values
- `variant`: `'primary'`
- `color`: `'green'`
- `forceColor`: `false`
- `disabled`: `false`
- `type`: `'button'`
- `className`: `''`
- `showIcon`: `true`
- `ariaLabel`: (derived from children text)
- `href`: `undefined`
- `target`: `'_self'`
## Variants
### Primary Button
The Primary button is used for the main call-to-action on a page. It features a solid background that fills from bottom-to-top on hover.
**Visual Characteristics:**
- Solid background (Green 300 / Black)
- High visual emphasis
- Background color transitions on hover
- Black text on green background, white text on black background
**Usage:**
```tsx
<Button variant="primary" onClick={handleClick}>
Get Started
</Button>
```
### Secondary Button
The Secondary button is used for supporting actions. It features an outline style with a transparent background that fills on hover.
**Visual Characteristics:**
- Transparent background with 2px border
- Medium visual emphasis
- Background fills from bottom-to-top on hover
- Green/Black text and border
**Usage:**
```tsx
<Button variant="secondary" onClick={handleClick}>
Learn More
</Button>
```
### Tertiary Button
The Tertiary button is used for low-emphasis or contextual actions. It appears as text-only with optional underline on hover.
**Visual Characteristics:**
- Text-only, no background or border
- Lowest visual emphasis
- Underline appears on hover/focus
- Different typography (Body R token vs Label R)
**Usage:**
```tsx
<Button variant="tertiary" onClick={handleClick}>
View Details
</Button>
```
## Color Themes
### Green Theme (Default)
The green theme uses the XRPL brand green colors:
- **Primary**: Green 300 background (#21E46B), Green 200 hover (#70EE97)
- **Secondary**: Green 400 text/border (#0DAA3E), Green 500 hover (#078139)
- **Tertiary**: Green 400 text (#0DAA3E), Green 500 hover (#078139)
### Black Theme
The black theme provides an alternative color scheme:
- **Primary**: Black background (#141414), 80% black hover
- **Secondary**: Black text/border (#141414), 15% black hover fill
- **Tertiary**: Black text (#141414)
**Usage:**
```tsx
<Button variant="primary" color="black" onClick={handleClick}>
Dark Button
</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.
### How It Works
- When `href` is provided and button is not disabled, renders as `<Link>` (anchor element)
- When `href` is not provided or button is disabled, renders as `<button>` element
- All visual styles and animations remain identical regardless of element type
- The Redocly Link component handles internal routing and external link handling
### Internal Navigation
For navigating within the application:
```tsx
<Button variant="primary" href="/docs">
View Documentation
</Button>
<Button variant="secondary" href="/about">
About Us
</Button>
```
### External Links
For external URLs, use `target="_blank"` to open in a new tab:
```tsx
<Button variant="primary" href="https://xrpl.org" target="_blank">
Visit XRPL.org
</Button>
```
### Link with Click Handler
You can combine `href` with `onClick` for tracking or additional logic:
```tsx
<Button
variant="primary"
href="/signup"
onClick={() => trackEvent('signup_click')}
>
Sign Up
</Button>
```
### Disabled Link Buttons
When `disabled={true}` and `href` is provided, the component renders as a `<button>` element instead of a link to prevent navigation:
```tsx
<Button variant="primary" href="/checkout" disabled>
Checkout (Unavailable)
</Button>
```
### All Variants as Links
All button variants support link functionality:
```tsx
{/* Primary link button */}
<Button variant="primary" href="/get-started">
Get Started
</Button>
{/* Secondary link button */}
<Button variant="secondary" href="/learn-more">
Learn More
</Button>
{/* Tertiary link button */}
<Button variant="tertiary" href="/view-details">
View Details
</Button>
{/* Black theme link button */}
<Button variant="primary" color="black" href="/dashboard">
Dashboard
</Button>
```
## States
### Enabled State
The default interactive state of the button. All variants display their base styling.
### Hover State
Triggered when the user hovers over the button with a mouse:
- **Primary/Secondary**: Background fills from bottom-to-top
- **Tertiary**: Underline appears, text color darkens
- **All Variants**: Arrow icon line shrinks, gap increases (with icon)
### Focus State
Triggered when the button receives keyboard focus (Tab key):
- Similar visual changes to hover state
- Additional focus outline (2px border/outline)
- Ensures keyboard accessibility
### Active State
Triggered when the button is being pressed:
- Returns to default padding/gap
- Background resets (for Primary/Secondary)
- Maintains visual feedback during press
### Disabled State
When `disabled={true}`:
- Icon is automatically hidden
- Gray text and background (Primary) or border (Secondary)
- Cursor changes to `not-allowed`
- `pointer-events: none` prevents interaction
- `aria-disabled` attribute set for screen readers
**Usage:**
```tsx
<Button variant="primary" disabled>
Unavailable
</Button>
```
## How It Works
### Component Structure
The Button component uses BEM (Block Element Modifier) naming convention with the `bds` namespace:
- `.bds-btn` - Base button class
- `.bds-btn--primary` - Primary variant modifier
- `.bds-btn--secondary` - Secondary variant modifier
- `.bds-btn--tertiary` - Tertiary variant modifier
- `.bds-btn--green` - Green color theme (default)
- `.bds-btn--black` - Black color theme
- `.bds-btn--disabled` - Disabled state modifier
- `.bds-btn--no-icon` - No icon modifier
- `.bds-btn__label` - Label element
- `.bds-btn__icon` - Icon container
- `.bds-btn__icon-line` - Arrow horizontal line
- `.bds-btn__icon-chevron` - Arrow chevron
### Background Animation
Primary and Secondary variants use a shared animation pattern:
1. **Pseudo-element (`::before`)**: Creates the hover background fill
2. **Transform Origin**: Set to `bottom center` for bottom-to-top fill
3. **Initial State**: `scaleY(0)` - background hidden
4. **Hover/Focus**: `scaleY(1)` - background fills from bottom
5. **Active**: `scaleY(0)` - background resets during press
This creates a smooth, directional fill animation that feels natural and responsive.
### Arrow Icon Animation
The arrow icon consists of two parts:
1. **Horizontal Line**: Shrinks from right to left (`scaleX(0)`) on hover/focus
2. **Chevron**: Stays visible, shifts right via increased gap
The gap between label and icon increases on hover/focus:
- **Default**: 16px (desktop), 16px (mobile)
- **Hover/Focus**: 22px (desktop), 21px (mobile)
This creates the illusion of the arrow "moving forward" as the line disappears.
### Padding Adjustments
On hover/focus, padding adjusts to accommodate the increased gap:
- **Primary**: `8px 19px 8px 20px``8px 13px 8px 20px` (desktop)
- **Secondary**: `6px 17px 6px 18px``6px 11px 6px 18px` (desktop)
- **Tertiary**: `8px 20px``8px 14px 8px 20px` (desktop)
These adjustments maintain visual balance while allowing the icon animation to work smoothly.
### Responsive Behavior
The component adapts to screen size at the `1023px` breakpoint:
**Desktop (≥1024px):**
- Larger padding values
- 22px gap on hover/focus
**Tablet/Mobile (≤1023px):**
- Reduced padding values
- 21px gap on hover/focus
All transitions remain smooth across breakpoints.
## Typography
### Primary & Secondary Variants
- **Font**: Booton, sans-serif
- **Size**: 16px (Label R token)
- **Weight**: 400
- **Line Height**: 23.2px
- **Letter Spacing**: 0px
### Tertiary Variant
- **Font**: Booton, sans-serif
- **Size**: 18px (Body R token)
- **Weight**: 400
- **Line Height**: 26.1px
- **Letter Spacing**: -0.5px
## Spacing & Layout
- **Border Radius**: 100px (fully rounded)
- **Max Height**: 40px
- **Icon Size**: 15px × 14px
- **Transitions**: 150ms with `cubic-bezier(0.98, 0.12, 0.12, 0.98)`
## Usage Examples
### Basic Usage
```tsx
import { Button } from 'shared/components/Button';
// Primary button (default)
<Button onClick={handleClick}>
Get Started
</Button>
// Secondary button
<Button variant="secondary" onClick={handleClick}>
Learn More
</Button>
// Tertiary button
<Button variant="tertiary" onClick={handleClick}>
View Details
</Button>
```
### Form Integration
```tsx
<form onSubmit={handleSubmit}>
<Button variant="primary" type="submit">
Submit
</Button>
<Button variant="tertiary" type="reset">
Reset
</Button>
<Button variant="tertiary" type="button" onClick={handleCancel}>
Cancel
</Button>
</form>
```
### Without Icon
```tsx
<Button variant="primary" showIcon={false} onClick={handleClick}>
No Arrow
</Button>
```
### Disabled State
```tsx
<Button variant="primary" disabled>
Unavailable
</Button>
```
### Color Themes
```tsx
{/* Green theme (default) */}
<Button variant="primary" color="green" onClick={handleClick}>
Green Button
</Button>
{/* Black theme */}
<Button variant="primary" color="black" onClick={handleClick}>
Black Button
</Button>
```
### Link Buttons
```tsx
{/* Internal navigation */}
<Button variant="primary" href="/docs">
View Documentation
</Button>
{/* External link (opens in new tab) */}
<Button variant="primary" href="https://xrpl.org" target="_blank">
Visit XRPL.org
</Button>
{/* Link with click tracking */}
<Button
variant="secondary"
href="/signup"
onClick={() => analytics.track('signup_button')}
>
Sign Up
</Button>
{/* Black theme link button */}
<Button variant="primary" color="black" href="/dashboard">
Go to Dashboard
</Button>
```
### Visual Hierarchy
```tsx
{/* Use variants to create clear visual hierarchy */}
<Button variant="primary" onClick={handlePrimaryAction}>
Main Action
</Button>
<Button variant="secondary" onClick={handleSecondaryAction}>
Secondary Action
</Button>
<Button variant="tertiary" onClick={handleTertiaryAction}>
Tertiary Action
</Button>
```
## Accessibility
### Keyboard Navigation
- **Tab**: Focus next button
- **Shift+Tab**: Focus previous button
- **Enter/Space**: Activate button
- **Focus Indicator**: Visible outline/border (2px)
- **Disabled buttons**: Not focusable
### Screen Reader Support
- Semantic `<button>` element (or `<a>` for link buttons)
- Button label announced via `aria-label` attribute
- `aria-disabled` attribute for disabled state
- Icon marked with `aria-hidden="true"`
- Link buttons use proper anchor semantics for navigation
### Color Contrast
All variants meet WCAG AA standards:
- **Primary**: Black on Green 300 = sufficient contrast
- **Secondary/Tertiary**: Green 400/500 on White = 4.52:1 / 5.12:1
- **Disabled**: Gray 400/500 indicates non-interactive state
### Focus Management
- Focus outline appears on keyboard navigation (`:focus-visible`)
- Focus styles match hover styles for consistency
- Square corners on Tertiary focus outline for better visibility
## Design Tokens
The component uses design tokens from the XRPL Brand Design System:
### Colors
- `$green-100` through `$green-500`
- `$gray-200`, `$gray-400`, `$gray-500`
- `$white`
- Neutral black (`#141414`)
### Spacing
- Border radius: `100px`
- Focus border width: `2px`
- Responsive breakpoint: `1023px`
### Motion
- Transition duration: `150ms`
- Timing function: `cubic-bezier(0.98, 0.12, 0.12, 0.98)`
## Best Practices
1. **Use Primary for main actions**: Reserve primary buttons for the most important action on a page
2. **Use Secondary for supporting actions**: Use secondary buttons for actions that support the primary action
3. **Use Tertiary for low-emphasis actions**: Use tertiary buttons for cancel, skip, or less important actions
4. **Maintain visual hierarchy**: Don't use multiple primary buttons on the same page
5. **Provide clear labels**: Button text should clearly indicate the action
6. **Handle disabled states**: Always provide feedback when actions are unavailable
7. **Test keyboard navigation**: Ensure all buttons are accessible via keyboard
8. **Consider context**: Choose color theme based on background and design context
9. **Use `href` for navigation**: When the button navigates to a new page, use the `href` prop instead of `onClick` with router navigation
10. **Use `target="_blank"` for external links**: Always open external URLs in a new tab for better UX
11. **Combine `href` with `onClick` for tracking**: When you need both navigation and analytics tracking
## Implementation Details
### Class Name Generation
The component builds class names dynamically:
```typescript
const classNames = [
'bds-btn',
`bds-btn--${variant}`,
`bds-btn--${color}`,
disabled ? 'bds-btn--disabled' : '',
!shouldShowIcon ? 'bds-btn--no-icon' : '',
className,
]
.filter(Boolean)
.join(' ');
```
### Icon Visibility Logic
The icon is automatically hidden when:
- `showIcon={false}` is passed
- `disabled={true}` is set
This ensures disabled buttons don't show interactive elements.
### Link Rendering Logic
The component conditionally renders as different elements:
```typescript
// Render as Link when href is provided and not disabled
if (href && !disabled) {
return (
<Link to={href} target={target} className={classNames}>
{content}
</Link>
);
}
// Otherwise render as button
return (
<button type={type} className={classNames} disabled={disabled}>
{content}
</button>
);
```
This ensures:
- Link buttons use proper anchor semantics for navigation
- Disabled state always renders as a button to prevent navigation
- Visual styles remain consistent across both element types
### State Management
The component manages states through CSS classes and props:
- **Disabled**: Controlled via `disabled` prop and `aria-disabled` attribute
- **Hover/Focus**: Handled by CSS `:hover` and `:focus-visible` pseudo-classes
- **Active**: Handled by CSS `:active` pseudo-class
- **Link vs Button**: Determined by presence of `href` prop
## Browser Support
The component uses modern CSS features:
- CSS Grid/Flexbox (widely supported)
- `:focus-visible` (supported in modern browsers)
- CSS transforms and transitions (widely supported)
- CSS custom properties (supported in modern browsers)
For older browser support, consider polyfills or fallbacks as needed.
## Related Components
- See showcase pages for interactive examples:
- `about/button-showcase-tertiary.page.tsx`
- Other variant showcase pages
## File Structure
```
shared/components/Button/
├── Button.tsx # Component implementation
├── Button.scss # Component styles
├── Button.md # This documentation
└── index.ts # Exports
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,183 @@
import React from 'react';
import clsx from 'clsx';
import { Link } from '@redocly/theme/components/Link/Link';
export interface ButtonProps {
/** Button variant - determines visual style */
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 */
onClick?: () => void;
/** Disabled state */
disabled?: boolean;
/** Button type attribute */
type?: 'button' | 'submit' | 'reset';
/** Additional CSS classes */
className?: string;
/** Whether to show the arrow icon */
showIcon?: boolean;
/** Accessibility label - defaults to button text if not provided */
ariaLabel?: string;
/** URL to navigate to - renders as a Link instead of button */
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;
}
/**
* Animated Arrow Icon Component
* The horizontal line shrinks from left to right on hover/focus,
* while the chevron shifts right via the gap change.
*/
const ArrowIcon: React.FC = () => (
<svg
className="bds-btn__icon"
width="15"
height="14"
viewBox="0 0 15 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
{/* Horizontal line - shrinks on hover */}
<line
className="bds-btn__icon-line"
x1="0"
y1="7"
x2="14"
y2="7"
stroke="currentColor"
strokeWidth="1.3"
strokeMiterlimit="10"
/>
{/* Chevron - stays visible */}
<path
className="bds-btn__icon-chevron"
d="M8.16755 1.16743L14.0005 7.00038L8.16755 12.8333"
stroke="currentColor"
strokeWidth="1.3"
strokeMiterlimit="10"
fill="none"
/>
</svg>
);
/**
* BDS Button Component
*
* A scalable button component following the XRPL Brand Design System.
* Supports Primary, Secondary, and Tertiary variants with green (default) or black color themes.
*
* @example
* <Button variant="primary" onClick={handleClick}>Get Started</Button>
* <Button variant="secondary" onClick={handleClick}>Learn More</Button>
* <Button variant="tertiary" onClick={handleClick}>View Details</Button>
* <Button variant="primary" color="black" onClick={handleClick}>Dark Button</Button>
*/
/**
* Helper function to extract text content from ReactNode
*/
const getTextFromChildren = (children: React.ReactNode): string => {
if (typeof children === 'string' || typeof children === 'number') {
return String(children);
}
if (Array.isArray(children)) {
return children.map(getTextFromChildren).join('');
}
if (React.isValidElement(children)) {
const props = children.props as { children?: React.ReactNode };
if (props.children) {
return getTextFromChildren(props.children);
}
}
return '';
};
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
color = 'green',
forceColor = false,
children,
onClick,
disabled = false,
type = 'button',
className = '',
showIcon = true,
ariaLabel,
href,
target = '_self',
forceNoPadding = false,
}) => {
// Hide icon when disabled (per design spec)
const shouldShowIcon = showIcon && !disabled;
// Default ariaLabel to button text if not provided
const buttonAriaLabel = ariaLabel || getTextFromChildren(children);
// Build class names using BEM with bds namespace
const classNames = clsx(
'bds-btn',
`bds-btn--${variant}`,
`bds-btn--${color}`,
{
'bds-btn--disabled': disabled,
'bds-btn--no-icon': !shouldShowIcon,
'bds-btn--force-color': forceColor,
'bds-btn--no-padding': forceNoPadding,
},
className
);
// Inner content shared between button and link
const content = (
<>
<span className="bds-btn__label">{children}</span>
{shouldShowIcon && <ArrowIcon />}
</>
);
// Render as Link when href is provided
if (href && !disabled) {
return (
<Link
to={href}
target={target}
className={classNames}
onClick={onClick}
aria-label={buttonAriaLabel}
>
{content}
</Link>
);
}
return (
<button
type={type}
className={classNames}
onClick={onClick}
disabled={disabled}
aria-disabled={disabled}
aria-label={buttonAriaLabel}
>
{content}
</button>
);
};
export default Button;

View File

@@ -0,0 +1,2 @@
export { Button, type ButtonProps } from './Button';
export { default } from './Button';

View File

@@ -0,0 +1,167 @@
# CardIcon Component
A clickable card component featuring an icon (top-left) and label text with arrow (bottom). Supports two color variants with responsive sizing that adapts at breakpoints.
## Features
- **Two Color Variants**: Neutral (gray) and Green
- **Five Interaction States**: Default, Hover, Focused, Pressed, Disabled
- **Responsive Sizing**: Automatically adapts at SM/MD/LG breakpoints
- **Window Shade Animation**: Smooth hover effect with color wipe
- **Accessible**: Full keyboard navigation and ARIA support
- **Flexible Rendering**: Renders as `<a>` or `<button>` based on props
## Responsive Sizes
The component automatically adapts its dimensions based on viewport width:
| Breakpoint | Height | Icon Size | Padding |
|------------|--------|-----------|---------|
| SM (< 576px) | 136px | 56x56 | 8px |
| MD (576px - 991px) | 140px | 60x60 | 12px |
| LG (>= 992px) | 144px | 64x64 | 16px |
## Color States
### Light Mode
#### Neutral Variant
- **Default**: Gray 200 (#E6EAF0) - black text
- **Hover**: Gray 300 (#CAD4DF) - black text
- **Focused**: Gray 300 + 2px black border - black text
- **Pressed**: Gray 400 (#8A919A) - black text
- **Disabled**: Gray 100 (#F0F3F7) - muted text (Gray 400), 50% icon opacity
#### Green Variant
- **Default**: Green 200 (#70EE97) - black text
- **Hover**: Green 300 (#21E46B) - black text
- **Focused**: Green 300 + 2px black border - black text
- **Pressed**: Green 400 (#0DAA3E) - black text
- **Disabled**: Green 100 (#EAFCF1) - muted text (Gray 400), 50% icon opacity
### Dark Mode
#### Neutral Variant
- **Default**: Gray 500 (#72777E) - white text
- **Hover**: Gray 400 (#8A919A) - white text
- **Focused**: Gray 400 + 2px white border - white text
- **Pressed**: Gray 500 at 70% opacity - white text
- **Disabled**: Gray 500 at 30% opacity - white text
#### Green Variant
- **Default**: Green 300 (#21E46B) - black text
- **Hover**: Green 200 (#70EE97) - black text
- **Focused**: Green 200 + 2px white border - black text
- **Pressed**: Green 400 (#0DAA3E) - black text
- **Disabled**: Gray 500 at 30% opacity - white text
## Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `variant` | `'neutral' \| 'green'` | `'neutral'` | Color variant |
| `icon` | `string` | *required* | Icon image source (URL or path) |
| `iconAlt` | `string` | `''` | Alt text for the icon image |
| `label` | `string` | *required* | Card label text |
| `href` | `string` | - | Link destination (renders as `<a>`) |
| `onClick` | `() => void` | - | Click handler (renders as `<button>`) |
| `disabled` | `boolean` | `false` | Disabled state |
| `className` | `string` | `''` | Additional CSS classes |
## Usage Examples
### Basic Link Card
```tsx
import { CardIcon } from '@/shared/components/CardIcon';
<CardIcon
variant="neutral"
icon="/icons/javascript.svg"
iconAlt="JavaScript logo"
label="Get Started with Javascript"
href="/docs/tutorials/javascript"
/>
```
### Green Variant with Click Handler
```tsx
<CardIcon
variant="green"
icon="/icons/python.svg"
iconAlt="Python logo"
label="Python Tutorial"
onClick={() => openTutorial('python')}
/>
```
### Disabled State
```tsx
<CardIcon
variant="neutral"
icon="/icons/coming-soon.svg"
label="Coming Soon"
disabled
/>
```
### Within a Grid Layout
```tsx
import { PageGrid, PageGridItem } from '@/shared/components/PageGrid';
import { CardIcon } from '@/shared/components/CardIcon';
<PageGrid>
<PageGridItem colSpan={{ sm: 2, md: 4, lg: 3 }}>
<CardIcon
variant="neutral"
icon="/icons/javascript.svg"
label="JavaScript"
href="/docs/javascript"
/>
</PageGridItem>
<PageGridItem colSpan={{ sm: 2, md: 4, lg: 3 }}>
<CardIcon
variant="green"
icon="/icons/python.svg"
label="Python"
href="/docs/python"
/>
</PageGridItem>
</PageGrid>
```
## Accessibility
- Uses semantic `<a>` or `<button>` elements based on interaction type
- Includes `aria-label` for screen readers
- Supports keyboard navigation (Tab, Enter, Space)
- Focus states meet WCAG 2.1 AA contrast requirements
- Disabled state includes `aria-disabled` attribute
## CSS Classes (BEM)
| Class | Description |
|-------|-------------|
| `.bds-card-icon` | Base card styles |
| `.bds-card-icon--neutral` | Neutral color variant |
| `.bds-card-icon--green` | Green color variant |
| `.bds-card-icon--hovered` | Hover state (JS-controlled) |
| `.bds-card-icon--disabled` | Disabled state |
| `.bds-card-icon__overlay` | Hover animation overlay |
| `.bds-card-icon__icon` | Icon container |
| `.bds-card-icon__icon-img` | Icon image |
| `.bds-card-icon__content` | Bottom content row |
| `.bds-card-icon__label` | Text label |
| `.bds-card-icon__arrow` | Arrow icon wrapper |
## Design Tokens
The component uses these design tokens from the style system:
- **Colors**: `$gray-100` through `$gray-400`, `$green-100` through `$green-400`
- **Typography**: `body-r` token from `_font.scss`
- **Breakpoints**: `$grid-breakpoints` from `_breakpoints.scss`
- **Animation**: `cubic-bezier(0.98, 0.12, 0.12, 0.98)` timing function

View File

@@ -0,0 +1,524 @@
// BDS CardIcon Component Styles
// Brand Design System - Icon card component with responsive sizing
//
// Naming Convention: BEM with 'bds' namespace
// .bds-card-icon - Base card (responsive layout)
// .bds-card-icon--neutral - Neutral color variant (gray tones)
// .bds-card-icon--green - Green color variant
// .bds-card-icon--hovered - Hovered state (triggers overlay animation)
// .bds-card-icon--disabled - Disabled state modifier
// .bds-card-icon__overlay - Hover gradient overlay (window shade animation)
// .bds-card-icon__icon - Icon container (responsive size)
// .bds-card-icon__icon-img - Icon image element
// .bds-card-icon__content - Bottom content row
// .bds-card-icon__label - Text label
// .bds-card-icon__arrow - Arrow icon wrapper
// =============================================================================
// Design Tokens
// =============================================================================
// Note: Uses centralized spacing tokens from _spacing.scss where applicable.
// Component-specific values (heights, icon sizes) remain local.
// Focus border - uses centralized token
$bds-card-icon-focus-border-color: $black;
$bds-card-icon-focus-border-width: $bds-focus-border-width; // 2px from _spacing.scss
// Animation - uses centralized tokens from _animations.scss
$bds-card-icon-transition-duration: $bds-transition-duration; // 200ms
$bds-card-icon-transition-timing: $bds-transition-timing;
// -----------------------------------------------------------------------------
// Responsive Size Tokens
// -----------------------------------------------------------------------------
// Heights and icon sizes are component-specific (not part of spacing scale)
// Padding uses centralized spacing tokens
// SM breakpoint (mobile - default)
$bds-card-icon-height-sm: 136px;
$bds-card-icon-padding-sm: $bds-space-sm; // 8px - spacing('sm')
$bds-card-icon-icon-size-sm: 56px;
// MD breakpoint (tablet)
$bds-card-icon-height-md: 140px;
$bds-card-icon-padding-md: $bds-space-md; // 12px - spacing('md')
$bds-card-icon-icon-size-md: 60px;
// LG breakpoint (desktop)
$bds-card-icon-height-lg: 144px;
$bds-card-icon-padding-lg: $bds-space-lg; // 16px - spacing('lg')
$bds-card-icon-icon-size-lg: 64px;
// =============================================================================
// Base Card Styles
// =============================================================================
.bds-card-icon {
// Reset button/anchor styles
appearance: none;
border: none;
background: none;
margin: 0;
font: inherit;
color: inherit;
text-decoration: none;
text-align: left;
// Layout
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
box-sizing: border-box;
width: 100%;
// Responsive sizing - SM (mobile-first)
height: $bds-card-icon-height-sm;
padding: $bds-card-icon-padding-sm;
@media (min-width: map-get($grid-breakpoints, md)) {
height: $bds-card-icon-height-md;
padding: $bds-card-icon-padding-md;
}
@media (min-width: map-get($grid-breakpoints, lg)) {
height: $bds-card-icon-height-lg;
padding: $bds-card-icon-padding-lg;
}
// Interaction
cursor: pointer;
// Transitions
transition:
background-color $bds-card-icon-transition-duration $bds-card-icon-transition-timing,
opacity $bds-card-icon-transition-duration $bds-card-icon-transition-timing;
// Hover styles - prevent text underline
&:hover {
text-decoration: none;
}
// Focus styles
&:focus {
outline: $bds-card-icon-focus-border-width solid $bds-card-icon-focus-border-color;
outline-offset: 1px;
}
&:focus:not(:focus-visible) {
outline: none;
}
&:focus-visible {
outline: $bds-card-icon-focus-border-width solid $bds-card-icon-focus-border-color;
outline-offset: 2px;
}
}
// =============================================================================
// Overlay (Color wipe animation - "Window Shade" effect)
// =============================================================================
.bds-card-icon__overlay {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
// Default: hidden (shade is "rolled up" at bottom)
clip-path: inset(100% 0 0 0);
transition: clip-path $bds-card-icon-transition-duration $bds-card-icon-transition-timing;
}
// Hovered state: shade fully raised (visible)
.bds-card-icon--hovered .bds-card-icon__overlay {
clip-path: inset(0 0 0 0);
}
// =============================================================================
// Icon Container
// =============================================================================
.bds-card-icon__icon {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
// Responsive icon size - SM
width: $bds-card-icon-icon-size-sm;
height: $bds-card-icon-icon-size-sm;
@media (min-width: map-get($grid-breakpoints, md)) {
width: $bds-card-icon-icon-size-md;
height: $bds-card-icon-icon-size-md;
}
@media (min-width: map-get($grid-breakpoints, lg)) {
width: $bds-card-icon-icon-size-lg;
height: $bds-card-icon-icon-size-lg;
}
}
.bds-card-icon__icon-img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain;
}
// =============================================================================
// Content Row (Bottom)
// =============================================================================
.bds-card-icon__content {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.bds-card-icon__label {
// Typography from Figma - Body R token
font-family: $font-family-sans-serif;
font-weight: 400;
color: $black;
margin-bottom: 0;
// SM/MD breakpoint (mobile-first)
font-size: 16px;
line-height: 26.1px;
letter-spacing: 0px;
// LG breakpoint (desktop)
@media (min-width: map-get($grid-breakpoints, lg)) {
font-size: 18px;
line-height: 26.1px;
letter-spacing: -0.5px;
}
}
.bds-card-icon__arrow {
flex-shrink: 0;
color: $black;
}
// Arrow animation on hover - works for both <a> and <button> elements
.bds-card-icon:hover .bds-card-icon__arrow .bds-link-icon--internal:not(.bds-link-icon--disabled) svg .arrow-horizontal,
.bds-card-icon:focus .bds-card-icon__arrow .bds-link-icon--internal:not(.bds-link-icon--disabled) svg .arrow-horizontal,
.bds-card-icon--hovered .bds-card-icon__arrow .bds-link-icon--internal:not(.bds-link-icon--disabled) svg .arrow-horizontal {
transform: scaleX(0);
}
.bds-card-icon:hover .bds-card-icon__arrow .bds-link-icon--external:not(.bds-link-icon--disabled) svg .arrow-horizontal,
.bds-card-icon:focus .bds-card-icon__arrow .bds-link-icon--external:not(.bds-link-icon--disabled) svg .arrow-horizontal,
.bds-card-icon--hovered .bds-card-icon__arrow .bds-link-icon--external:not(.bds-link-icon--disabled) svg .arrow-horizontal {
transform: scale(0);
}
// =============================================================================
// Neutral Variant
// =============================================================================
.bds-card-icon--neutral {
background-color: $gray-200;
// Overlay color for hover wipe
.bds-card-icon__overlay {
background-color: $gray-300;
}
// Pressed state
&:active:not(.bds-card-icon--disabled) {
.bds-card-icon__overlay {
background-color: $gray-400;
clip-path: inset(0 0 0 0);
}
}
}
// =============================================================================
// Green Variant
// =============================================================================
.bds-card-icon--green {
background-color: $green-200;
// Overlay color for hover wipe
.bds-card-icon__overlay {
background-color: $green-300;
}
// Pressed state
&:active:not(.bds-card-icon--disabled) {
.bds-card-icon__overlay {
background-color: $green-400;
clip-path: inset(0 0 0 0);
}
}
}
// =============================================================================
// Disabled State
// =============================================================================
.bds-card-icon--disabled {
cursor: not-allowed;
pointer-events: none;
&:focus,
&:focus-visible {
outline: none;
}
// Neutral disabled
&.bds-card-icon--neutral {
background-color: $gray-100;
.bds-card-icon__label,
.bds-card-icon__arrow {
color: $gray-400;
}
.bds-card-icon__icon-img {
opacity: 0.5;
}
}
// Green disabled
&.bds-card-icon--green {
background-color: $green-100;
.bds-card-icon__label,
.bds-card-icon__arrow {
color: $gray-400;
}
.bds-card-icon__icon-img {
opacity: 0.5;
}
}
}
// =============================================================================
// Dark Mode Styles (html.dark)
// =============================================================================
// Dark mode uses different color palette per Figma specs:
// - Neutral: gray-500 base with white text
// - Green: green-300 base with black text
// - Focus border: white
// - Pressed: 70% opacity
// - Disabled: 30% opacity
html.dark {
// Focus styles - white border in dark mode
.bds-card-icon {
&:focus {
outline-color: $white;
}
&:focus-visible {
outline-color: $white;
}
}
// ---------------------------------------------------------------------------
// Neutral Variant - Dark Mode
// Default: gray-500, Hover: gray-400, Pressed: 70% gray-500, Disabled: 30% opacity
// ---------------------------------------------------------------------------
.bds-card-icon--neutral {
background-color: $gray-500;
.bds-card-icon__label,
.bds-card-icon__arrow {
color: $white;
}
// Overlay color for hover wipe
.bds-card-icon__overlay {
background-color: $gray-400;
}
// Pressed state - 70% opacity background
&:active:not(.bds-card-icon--disabled) {
.bds-card-icon__overlay {
background-color: rgba($gray-500, 0.7);
clip-path: inset(0 0 0 0);
}
}
}
// ---------------------------------------------------------------------------
// Green Variant - Dark Mode
// Default: green-300, Hover: green-200, Pressed: green-400, Disabled: 30% gray-500
// ---------------------------------------------------------------------------
.bds-card-icon--green {
background-color: $green-300;
.bds-card-icon__label,
.bds-card-icon__arrow {
color: $black;
}
// Overlay color for hover wipe
.bds-card-icon__overlay {
background-color: $green-200;
}
// Pressed state
&:active:not(.bds-card-icon--disabled) {
.bds-card-icon__overlay {
background-color: $green-400;
clip-path: inset(0 0 0 0);
}
}
}
// ---------------------------------------------------------------------------
// Disabled State - Dark Mode
// Both variants: 30% opacity with white text
// ---------------------------------------------------------------------------
.bds-card-icon--disabled {
opacity: 0.3;
&.bds-card-icon--neutral {
background-color: $gray-500;
.bds-card-icon__label,
.bds-card-icon__arrow {
color: $white;
}
.bds-card-icon__icon-img {
opacity: 1; // Reset since parent has opacity
}
}
&.bds-card-icon--green {
background-color: $gray-500;
.bds-card-icon__label,
.bds-card-icon__arrow {
color: $white;
}
.bds-card-icon__icon-img {
opacity: 1; // Reset since parent has opacity
}
}
}
}
// =============================================================================
// Light Mode Styles (html.light)
// =============================================================================
// Light mode matches the default styles (mobile-first approach)
// Explicitly defined for specificity when html.light class is present
html.light {
// Focus styles - black border in light mode
.bds-card-icon {
&:focus {
outline-color: $black;
}
&:focus-visible {
outline-color: $black;
}
}
// ---------------------------------------------------------------------------
// Neutral Variant - Light Mode
// ---------------------------------------------------------------------------
.bds-card-icon--neutral {
background-color: $gray-200;
.bds-card-icon__label,
.bds-card-icon__arrow {
color: $black;
}
.bds-card-icon__overlay {
background-color: $gray-300;
}
// Maintain background color on focus (override generic link focus styles)
&:focus {
background-color: $gray-200 !important;
}
&:active:not(.bds-card-icon--disabled) {
.bds-card-icon__overlay {
background-color: $gray-400;
clip-path: inset(0 0 0 0);
}
}
}
// ---------------------------------------------------------------------------
// Green Variant - Light Mode
// ---------------------------------------------------------------------------
.bds-card-icon--green {
background-color: $green-200;
.bds-card-icon__label,
.bds-card-icon__arrow {
color: $black;
}
.bds-card-icon__overlay {
background-color: $green-300;
}
// Maintain background color on focus (override generic link focus styles)
&:focus {
background-color: $green-200 !important;
}
&:active:not(.bds-card-icon--disabled) {
.bds-card-icon__overlay {
background-color: $green-400;
clip-path: inset(0 0 0 0);
}
}
}
// ---------------------------------------------------------------------------
// Disabled State - Light Mode
// ---------------------------------------------------------------------------
.bds-card-icon--disabled {
opacity: 1; // Reset opacity for light mode
&.bds-card-icon--neutral {
background-color: $gray-100;
.bds-card-icon__label,
.bds-card-icon__arrow {
color: $gray-400;
}
.bds-card-icon__icon-img {
opacity: 0.5;
}
}
&.bds-card-icon--green {
background-color: $green-100;
.bds-card-icon__label,
.bds-card-icon__arrow {
color: $gray-400;
}
.bds-card-icon__icon-img {
opacity: 0.5;
}
}
}
}

View File

@@ -0,0 +1,150 @@
import React, { useState } from 'react';
import { LinkArrow } from '../Link/LinkArrow';
export interface CardIconProps {
/** Color variant: 'neutral' (default) or 'green' */
variant?: 'neutral' | 'green';
/** Icon image source (URL or path) */
icon: string;
/** Alt text for the icon image */
iconAlt?: string;
/** Card label text */
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;
}
/**
* CardIcon Component
*
* A clickable card component featuring an icon (top-left) and label text with
* arrow (bottom). Supports two color variants (Neutral and Green) with five
* interaction states (Default, Hover, Focused, Pressed, Disabled).
*
* The component is fully responsive, adapting its size at breakpoints:
* - SM (< 576px): 136px height, 56x56 icon, 8px padding
* - MD (576px - 991px): 140px height, 60x60 icon, 12px padding
* - LG (>= 992px): 144px height, 64x64 icon, 16px padding
*
* Features a "window shade" hover animation where the hover color wipes from
* bottom to top on mouse enter, and top to bottom on mouse leave.
*
* @example
* // Basic usage with link
* <CardIcon
* variant="neutral"
* icon="/icons/javascript.svg"
* label="Get Started with Javascript"
* href="/docs/javascript"
* />
*
* @example
* // Green variant with click handler
* <CardIcon
* variant="green"
* icon="/icons/python.svg"
* label="Python Tutorial"
* onClick={() => openTutorial('python')}
* />
*
* @example
* // Disabled state
* <CardIcon
* variant="neutral"
* icon="/icons/coming-soon.svg"
* label="Coming Soon"
* disabled
* />
*/
export const CardIcon: React.FC<CardIconProps> = ({
variant = 'neutral',
icon,
iconAlt = '',
label,
href,
onClick,
disabled = false,
className = '',
}) => {
// Track hover state for animation
const [isHovered, setIsHovered] = useState(false);
// Build class names using BEM convention
const classNames = [
'bds-card-icon',
`bds-card-icon--${variant}`,
disabled ? 'bds-card-icon--disabled' : '',
isHovered && !disabled ? 'bds-card-icon--hovered' : '',
className,
]
.filter(Boolean)
.join(' ');
// Hover handlers
const handleMouseEnter = () => !disabled && setIsHovered(true);
const handleMouseLeave = () => setIsHovered(false);
// Common content
const content = (
<>
{/* Hover overlay for window shade animation */}
<div className="bds-card-icon__overlay" aria-hidden="true" />
{/* Icon container */}
<div className="bds-card-icon__icon">
<img
src={icon}
alt={iconAlt}
className="bds-card-icon__icon-img"
/>
</div>
{/* Bottom content row */}
<div className="bds-card-icon__content">
<span className="bds-card-icon__label">{label}</span>
<span className="bds-card-icon__arrow">
<LinkArrow variant="internal" size="medium" />
</span>
</div>
</>
);
// Render as anchor tag when href is provided
if (href && !disabled) {
return (
<a
href={href}
className={classNames}
aria-label={label}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{content}
</a>
);
}
// Render as button (for onClick or disabled state)
return (
<button
type="button"
className={classNames}
onClick={onClick}
disabled={disabled}
aria-disabled={disabled}
aria-label={label}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{content}
</button>
);
};
export default CardIcon;

View File

@@ -0,0 +1,2 @@
export { CardIcon, type CardIconProps } from './CardIcon';
export { default } from './CardIcon';

View File

@@ -0,0 +1,243 @@
# CardImage Component Documentation
## Overview
The CardImage component is a responsive card implementation following the XRPL Brand Design System (BDS). It displays an image, title, subtitle, and call-to-action button with three responsive size variants that adapt to viewport width.
## Features
- **Three Responsive Variants**: LG (≥992px), MD (576px-991px), SM (<576px)
- **Interactive States**: Default, Hover, Focus, Pressed, Disabled
- **Button Animation on Card Hover**: Hovering the card triggers the button's hover animation
- **Flexible Usage**: Supports both link navigation and click handlers
- **Accessibility**: Keyboard navigation and screen reader support
## Props API
```typescript
interface CardImageProps {
/** Image source URL */
image: string;
/** Alt text for the image */
imageAlt: string;
/** Card title (1 line only) */
title: string;
/** Card subtitle (max 3 lines) */
subtitle: string;
/** Button label text */
buttonLabel: string;
/** Link destination (renders card as clickable link) */
href?: string;
/** Click handler for the button */
onClick?: () => void;
/** Disabled state */
disabled?: boolean;
/** Optional className for custom styling */
className?: string;
}
```
### Default Values
- `disabled`: `false`
- `className`: `''`
## Responsive Variants
### LG (Large) - Desktop (≥992px)
- Card height: 620px
- Image height: 400px (1:1 aspect ratio preserved)
- 3-column grid width
### MD (Medium) - Tablet (576px - 991px)
- Card height: 560px
- Image height: 280px
- 2-column grid width
### SM (Small) - Mobile (<576px)
- Card height: 536px
- Image height: 268px
- 1-column grid width (full width)
## States
### Default State
The default interactive state with the button showing its primary green background.
### Hover State
When the user hovers over the card:
- Button background fills from bottom-to-top (green-300 green-200)
- Arrow icon line shrinks
- Gap between label and icon increases
### Focus State
When the card receives keyboard focus:
- Black border outline around the card
- Button shows focus styling
### Pressed State
When the button is being pressed:
- Button returns to default styling momentarily
### Disabled State
When `disabled={true}`:
- Card is non-interactive
- Button shows disabled styling (gray background, no icon)
- Text colors muted
- Cursor changes to `not-allowed`
## Usage Examples
### Basic Card with Link
```tsx
import { CardImage } from 'shared/components/CardImage';
<CardImage
image="/images/docs-hero.png"
imageAlt="Documentation illustration"
title="Documentation"
subtitle="Access everything you need to get started working with the XRPL."
buttonLabel="Get Started"
href="/docs"
/>
```
### Card with Click Handler
```tsx
<CardImage
image="/images/feature.png"
imageAlt="Feature illustration"
title="New Feature"
subtitle="Learn about our latest feature and how it can help you build better applications."
buttonLabel="Learn More"
onClick={() => console.log('clicked')}
/>
```
### Disabled Card
```tsx
<CardImage
image="/images/coming-soon.png"
imageAlt="Coming soon"
title="Coming Soon"
subtitle="This feature is not yet available."
buttonLabel="Unavailable"
disabled
/>
```
### Card with Custom Class
```tsx
<CardImage
image="/images/hero.png"
imageAlt="Hero image"
title="Custom Styled"
subtitle="Card with additional custom styling."
buttonLabel="Explore"
href="/explore"
className="my-custom-card"
/>
```
## Design Tokens
### Spacing
| Token | Value | Description |
|-------|-------|-------------|
| Image-to-content gap | 24px | Gap between image and content area |
| Title-to-subtitle gap | 12px | Gap between title and subtitle |
| Content horizontal padding | 8px | Left/right padding for content |
| Button margin-top | 30px | Button locked to bottom |
| Border radius | 16px | Card corner radius |
### Colors
| Element | Light Mode | Dark Mode |
|---------|------------|-----------|
| Card background | #FFFFFF | Gray 800 |
| Card border | Gray 300 (#CAD4DF) | Gray 700 |
| Image background | Gray 100 (#F0F3F7) | Gray 700 |
| Text color | #141414 | White |
### Typography
| Element | Token | Specs |
|---------|-------|-------|
| Title | `.sh-md-l` | 28px/35px, -0.5px letter-spacing, light weight |
| Subtitle | `.body-l` | 18px/26.1px, -0.5px letter-spacing, light weight |
## How It Works
### Hover Animation
The card tracks hover state via React's `useState`. When hovered:
1. A `bds-card-image--hovered` class is added to the card
2. CSS rules target the nested Button component and apply hover styles
3. The button's `::before` pseudo-element animates (background fill)
4. The arrow icon line shrinks via `scaleX(0)`
This approach ensures the button animation triggers even when hovering areas of the card outside the button itself.
### Link Navigation
When `href` is provided:
- The entire card becomes clickable
- Clicking anywhere on the card navigates to the href
- The card is keyboard accessible (Enter/Space to activate)
### Button Integration
The component uses the existing BDS Button component with:
- `variant="primary"` - Solid green background
- `color="green"` - Green color theme
## Accessibility
### Keyboard Navigation
- **Tab**: Focus the card
- **Enter/Space**: Activate the card (navigate to href or trigger onClick)
### Screen Reader Support
- Card has appropriate `role="link"` when href is provided
- Image has descriptive alt text
- `aria-disabled` attribute for disabled state
## File Structure
```
shared/components/CardImage/
├── CardImage.tsx # Component implementation
├── CardImage.scss # Component styles
├── CardImage.md # This documentation
└── index.ts # Exports
```
## Related Components
- **Button**: Used internally for the CTA button
- **CardOffgrid**: Similar card component with different layout
## Browser Support
The component uses modern CSS features:
- CSS Grid/Flexbox
- CSS transforms and transitions
- `:focus-visible` pseudo-class
- `-webkit-line-clamp` for text truncation
All features are widely supported in modern browsers.

View File

@@ -0,0 +1,370 @@
// BDS CardImage Component Styles
// Brand Design System - Responsive card with image, title, subtitle, and CTA button
//
// Naming Convention: BEM with 'bds' namespace
// .bds-card-image - Base card container
// .bds-card-image--hovered - Hover state (scales image only)
// .bds-card-image--disabled - Disabled state
// .bds-card-image__image-container - Gray background image wrapper
// .bds-card-image__image - The actual image element
// .bds-card-image__content - Title/subtitle/button wrapper
// .bds-card-image__text - Title and subtitle wrapper
// .bds-card-image__title - Card title (uses .sh-md-r)
// .bds-card-image__subtitle - Card subtitle (uses .body-l)
//
// 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)
// =============================================================================
// Note: Uses centralized spacing tokens from _spacing.scss where applicable.
// Component-specific values (heights, dimensions) remain local.
// Card dimensions - LG (Large) variant (default, ≥992px)
$bds-card-image-height-lg: 620px;
$bds-card-image-image-height-lg: 400px;
// Card dimensions - MD (Medium) variant (576px - 991px)
$bds-card-image-height-md: 560px;
$bds-card-image-image-height-md: 280px;
// Card dimensions - SM (Small) variant (<576px)
$bds-card-image-height-sm: 536px;
$bds-card-image-image-height-sm: 268px;
// Spacing - LG (Large) variant (default, ≥992px)
$bds-card-image-gap-lg: $bds-space-2xl; // 24px - Gap between image and content
$bds-card-image-title-gap-lg: $bds-space-md; // 12px - Gap between title and subtitle
$bds-card-image-content-padding: $bds-space-sm; // 8px - Horizontal padding for content
$bds-card-image-button-margin-lg: $bds-space-3xl; // 32px - Margin above button
// Spacing - MD (Medium) variant (576px - 991px)
$bds-card-image-gap-md: $bds-space-lg; // 16px - Gap between image and content
$bds-card-image-title-gap-md: $bds-space-sm; // 8px - Gap between title and subtitle
$bds-card-image-button-margin-md: $bds-space-2xl; // 24px - Margin above button
// Spacing - SM (Small) variant (<576px)
$bds-card-image-gap-sm: $bds-space-lg; // 16px - Gap between image and content
$bds-card-image-title-gap-sm: $bds-space-sm; // 8px - Gap between title and subtitle
$bds-card-image-button-margin-sm: $bds-space-2xl; // 24px - Margin above button
// Colors
$bds-card-image-bg: $white;
$bds-card-image-image-bg: $gray-100;
$bds-card-image-text-color: #141414; // Neutral black from Figma
// Animation - uses centralized tokens from _animations.scss
$bds-card-image-transition-duration: 150ms; // Component-specific (faster than default)
$bds-card-image-transition-timing: $bds-transition-timing;
// =============================================================================
// Base Card Styles
// =============================================================================
.bds-card-image {
// Card container
display: flex;
flex-direction: column;
gap: $bds-card-image-gap-lg; // Gap between image container and content
width: 100%; // Fill available width (4-column span on LG+)
background-color: $bds-card-image-bg;
cursor: pointer;
// When inside a grid column, fill the column width
.bds-grid__col & {
flex: 1 1 auto;
width: 100%;
}
// Focus styles
&:focus {
outline: 2px solid $bds-card-image-text-color;
outline-offset: 2px;
}
&:focus:not(:focus-visible) {
outline: none;
}
&:focus-visible {
outline: 2px solid $bds-card-image-text-color;
outline-offset: 2px;
}
}
// =============================================================================
// Image Container
// =============================================================================
// Image container has fixed height per breakpoint.
// Image within maintains its aspect ratio using object-fit: contain.
.bds-card-image__image-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: $bds-card-image-image-height-lg;
background-color: var(--bds-card-image-bg, $bds-card-image-image-bg);
overflow: hidden;
flex-shrink: 0;
}
.bds-card-image__image {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
object-fit: contain; // Maintain image aspect ratio (1:1 for this design)
object-position: center;
transition: transform $bds-card-image-transition-duration $bds-card-image-transition-timing;
}
// =============================================================================
// Full Bleed Modifier
// =============================================================================
// When fullBleed is true, image fills entire container with object-fit: cover
// and uses aspect-ratio for responsive sizing
.bds-card-image--full-bleed {
// Override fixed heights - use auto height with aspect ratio
height: auto;
width: 100%; // Ensure card fills parent container
.bds-card-image__image-container {
// Use aspect-ratio instead of fixed height for responsive scaling
width: 100%;
height: auto;
// 1:1 aspect ratio for all breakpoints (from Figma designs)
aspect-ratio: 1 / 1;
}
.bds-card-image__image {
width: 100%;
height: 100%;
max-width: none;
max-height: none;
object-fit: cover;
}
}
// =============================================================================
// Content Area
// =============================================================================
.bds-card-image__content {
display: flex;
flex-direction: column;
justify-content: space-between; // Distribute space between text and button
flex: 1;
padding: 0 $bds-card-image-content-padding; // Horizontal padding only
padding-bottom: $bds-card-image-content-padding;
min-height: 0; // Allow flex shrinking
}
.bds-card-image__text {
display: flex;
flex-direction: column;
gap: $bds-card-image-title-gap-lg;
flex-shrink: 0; // Don't shrink text container
}
.bds-card-image__title {
// Typography handled by .sh-md-r class from _font.scss
color: $bds-card-image-text-color;
margin: 0;
text-align: left; // Always left-align, regardless of parent
// Title should be 1 line only
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bds-card-image__subtitle {
// Typography handled by .body-l class from _font.scss
color: $bds-card-image-text-color;
margin: 0;
text-align: left; // Always left-align, regardless of parent
// Subtitle max 3 lines
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
// =============================================================================
// Button Positioning
// =============================================================================
.bds-card-image__content .bds-btn {
align-self: flex-start;
flex-shrink: 0; // Don't shrink button
margin-top: $bds-card-image-button-margin-sm; // Mobile: 24px
@include media-breakpoint-up(md) {
margin-top: $bds-card-image-button-margin-md; // Tablet: 24px
}
@include media-breakpoint-up(lg) {
margin-top: $bds-card-image-button-margin-lg; // Desktop: 32px
}
}
// =============================================================================
// Hover State - Image Animation Only
// =============================================================================
// When the card is hovered, scale the image (button hover state is independent)
.bds-card-image--hovered:not(.bds-card-image--disabled),
.bds-card-image:hover:not(.bds-card-image--disabled) {
// Scale image by 10% on hover
.bds-card-image__image {
transform: scale(1.1);
}
}
// =============================================================================
// Focus State
// =============================================================================
// Focus styles are handled by base card styles (outline)
// Button focus state is independent
// =============================================================================
// Disabled State
// =============================================================================
.bds-card-image--disabled {
cursor: not-allowed;
pointer-events: none;
.bds-card-image__title,
.bds-card-image__subtitle {
color: $gray-500;
}
}
// =============================================================================
// Responsive Styles - MD (Medium) Variant
// =============================================================================
@include media-breakpoint-down(lg) {
.bds-card-image {
gap: $bds-card-image-gap-md; // Gap between image container and content for MD
}
.bds-card-image__image-container {
height: $bds-card-image-image-height-md;
}
.bds-card-image__content {
padding: 0 $bds-card-image-content-padding; // Horizontal padding only
}
.bds-card-image__text {
gap: $bds-card-image-title-gap-md;
}
}
// =============================================================================
// Responsive Styles - SM (Small) Variant
// =============================================================================
@include media-breakpoint-down(sm) {
.bds-card-image {
gap: $bds-card-image-gap-sm; // Gap between image container and content for SM
}
.bds-card-image__image-container {
height: $bds-card-image-image-height-sm;
}
.bds-card-image__content {
padding: 0 $bds-card-image-content-padding; // Horizontal padding only
}
.bds-card-image__text {
gap: $bds-card-image-title-gap-sm;
}
}
// =============================================================================
// Light Mode Styles
// =============================================================================
html.light {
.bds-card-image {
&:focus {
outline-color: $bds-card-image-text-color;
}
&:focus-visible {
outline-color: $bds-card-image-text-color;
}
}
}
// =============================================================================
// Dark Mode Styles (from Figma design tokens)
// =============================================================================
// Design tokens:
// - Card background: neutral/black (#141414) → $gray-900
// - Image container: neutral/500 (#72777E) → $gray-500
// - Title: neutral/white (#FFFFFF) → $white
// - Subtitle: neutral/200 (#E6EAF0) → $gray-200
// - Focus outline: neutral/white (#FFFFFF) → $white
// - Hover overlay: 15% black (rgba(114,119,126,0.15))
// - Pressed overlay: 45% black (rgba(114,119,126,0.45))
// - Disabled button bg: neutral/500 (#72777E) → $gray-500
// - Disabled button text: neutral/300 (#CAD4DF) → $gray-300
html.dark {
.bds-card-image {
background-color: $gray-900;
border-color: $gray-900; // Border blends with background in dark mode
// Focus styles - white outline
&:focus {
outline: 2px solid $white;
outline-offset: 2px;
}
&:focus-visible {
outline: 2px solid $white;
outline-offset: 2px;
}
}
// Image container - neutral/500 (#72777E)
.bds-card-image__image-container {
background-color: var(--bds-card-image-bg, $gray-500);
}
// Title - neutral/white (#FFFFFF)
.bds-card-image__title {
color: $white;
}
// Subtitle - neutral/200 (#E6EAF0)
.bds-card-image__subtitle {
color: $gray-200;
}
// ---------------------------------------------------------------------------
// Dark Mode - Disabled State
// ---------------------------------------------------------------------------
.bds-card-image--disabled {
// Text colors remain but muted
.bds-card-image__title {
color: $white;
}
.bds-card-image__subtitle {
color: $gray-200;
}
// Button in disabled state uses gray-500 bg and gray-300 text
// (handled by Button component's disabled styles)
}
}

View File

@@ -0,0 +1,185 @@
import React, { useState, useCallback } from 'react';
import clsx from 'clsx';
import { Button } from '../Button';
export interface CardImageProps {
/** Image source URL */
image: string;
/** Alt text for the image */
imageAlt: string;
/** Card title (1 line only) */
title: string;
/** Card subtitle (max 3 lines) */
subtitle: string;
/** Button label text */
buttonLabel: string;
/** Link destination (renders card as clickable link) */
href?: string;
/** Click handler for the button */
onClick?: () => void;
/** Disabled state */
disabled?: boolean;
/** Optional className for custom styling */
className?: string;
/** When true, image fills entire container with object-fit: cover (no visible background) */
fullBleed?: boolean;
/** Custom background color for image container (defaults to gray-100) */
backgroundColor?: string;
}
/**
* BDS CardImage Component
*
* A responsive card component displaying an image, title, subtitle, and CTA button.
* Features three responsive size variants (LG/MD/SM) that adapt to viewport width.
*
* Key behaviors:
* - Hovering the card triggers the button's hover animation
* - Card can link to a URL or trigger a click handler
* - Supports disabled state
*
* @example
* // Basic card with link
* <CardImage
* image="/images/docs-hero.png"
* imageAlt="Documentation illustration"
* title="Documentation"
* subtitle="Access everything you need to get started working with the XRPL."
* buttonLabel="Get Started"
* href="/docs"
* />
*
* @example
* // Card with click handler
* <CardImage
* image="/images/feature.png"
* imageAlt="Feature illustration"
* title="New Feature"
* subtitle="Learn about our latest feature."
* buttonLabel="Learn More"
* onClick={() => console.log('clicked')}
* />
*/
export const CardImage: React.FC<CardImageProps> = ({
image,
imageAlt,
title,
subtitle,
buttonLabel,
href,
onClick,
disabled = false,
className = '',
fullBleed = false,
backgroundColor,
}) => {
// Track hover state for button animation
const [isHovered, setIsHovered] = useState(false);
const handleMouseEnter = useCallback(() => {
if (!disabled) {
setIsHovered(true);
}
}, [disabled]);
const handleMouseLeave = useCallback(() => {
if (!disabled) {
setIsHovered(false);
}
}, [disabled]);
// Build class names using BEM with bds namespace
const classNames = clsx(
'bds-card-image',
disabled && 'bds-card-image--disabled',
isHovered && 'bds-card-image--hovered',
fullBleed && 'bds-card-image--full-bleed',
className
);
// Handle card click for linked cards
const handleCardClick = useCallback(
(e: React.MouseEvent) => {
// If clicking the button directly, don't navigate via card
if ((e.target as HTMLElement).closest('.bds-btn')) {
return;
}
if (href && !disabled) {
window.location.href = href;
}
},
[href, disabled]
);
// Handle button click
const handleButtonClick = useCallback(() => {
if (href) {
window.location.href = href;
} else if (onClick) {
onClick();
}
}, [href, onClick]);
// Build inline style for image container background color
const imageContainerStyle = backgroundColor
? { '--bds-card-image-bg': backgroundColor } as React.CSSProperties
: undefined;
// Common content structure
const content = (
<>
{/* Image container with customizable background */}
<div className="bds-card-image__image-container" style={imageContainerStyle}>
<img
src={image}
alt={imageAlt}
className="bds-card-image__image"
/>
</div>
{/* Content area: title, subtitle, and button */}
<div className="bds-card-image__content">
<div className="bds-card-image__text">
<h3 className="bds-card-image__title sh-md-l">{title}</h3>
<p className="bds-card-image__subtitle body-l">{subtitle}</p>
</div>
<Button
variant="primary"
color="green"
disabled={disabled}
onClick={handleButtonClick}
>
{buttonLabel}
</Button>
</div>
</>
);
// Render as clickable div (card itself handles navigation)
return (
<div
className={classNames}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={href ? handleCardClick : undefined}
role={href ? 'link' : undefined}
tabIndex={href && !disabled ? 0 : undefined}
onKeyDown={
href && !disabled
? (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
window.location.href = href;
}
}
: undefined
}
aria-disabled={disabled}
>
{content}
</div>
);
};
export default CardImage;

View File

@@ -0,0 +1,2 @@
export { CardImage, type CardImageProps } from './CardImage';
export { default } from './CardImage';

View File

@@ -0,0 +1,436 @@
# CardOffgrid Component - Usage Guide
## Overview
`CardOffgrid` is a feature highlight card component designed to showcase key capabilities, features, or resources. It combines an icon, title, and description in a visually engaging, interactive card format with smooth hover animations.
**Use CardOffgrid when:**
- Highlighting key features or capabilities
- Creating feature grids or showcases
- Linking to important documentation or resources
- Presenting product/service highlights
**Don't use CardOffgrid for:**
- Simple content cards (use standard Bootstrap cards)
- Navigation items (use navigation components)
- Data display (use tables or data components)
- Long-form content (use article/page layouts)
---
## When to Use Each Variant
### Neutral Variant (`variant="neutral"`)
**Use for:**
- General feature highlights
- Standard content cards
- Secondary or supporting features
- When you want subtle, professional presentation
**Example use cases:**
- Documentation sections
- Feature lists
- Service offerings
- Standard informational cards
### Green Variant (`variant="green"`)
**Use for:**
- Primary or featured highlights
- Call-to-action cards
- Important announcements
- Brand-emphasized content
**Example use cases:**
- Hero feature cards
- Primary CTAs
- Featured resources
- Branded highlights
---
## Content Best Practices
### Title Guidelines
**Do:**
- Keep titles concise (1-3 words ideal)
- Use line breaks (`\n`) for multi-word titles when needed
- Make titles action-oriented or descriptive
- Examples: "Onchain Metadata", "Token\nManagement", "Cross-Chain\nBridges"
**Don't:**
- Write long sentences as titles
- Use more than 2 lines
- Include punctuation (periods, commas)
- Make titles too generic ("Feature", "Service")
### Description Guidelines
**Do:**
- Write 1-2 sentences (15-25 words ideal)
- Focus on benefits or key information
- Use clear, simple language
- Keep descriptions scannable
**Don't:**
- Write paragraphs (save for full pages)
- Use jargon without context
- Include multiple ideas in one description
- Make descriptions too short (< 10 words) or too long (> 40 words)
### Icon Guidelines
**Do:**
- Use SVG icons for crisp rendering
- Choose icons that represent the feature clearly
- Ensure icons are recognizable at 68×68px
- Use consistent icon style across cards
**Don't:**
- Use low-resolution raster images
- Choose overly complex icons
- Mix icon styles within a single grid
- Use icons that don't relate to the content
---
## Interaction Patterns
### Using `onClick` vs `href`
**Use `onClick` when:**
- Triggering JavaScript actions (modals, analytics, state changes)
- Opening external links in new tabs
- Performing client-side navigation
- Handling complex interactions
```tsx
<CardOffgrid
variant="neutral"
icon={<AnalyticsIcon />}
title="View Analytics"
description="See detailed usage statistics and insights."
onClick={() => {
trackEvent('analytics_viewed');
openModal('analytics');
}}
/>
```
**Use `href` when:**
- Navigating to internal pages
- Linking to documentation
- Simple page navigation
- SEO-friendly links
```tsx
<CardOffgrid
variant="green"
icon="/icons/docs.svg"
title="API\nReference"
description="Complete API documentation and examples."
href="/docs/api"
/>
```
### Disabled State
Use `disabled` when:
- Feature is coming soon
- Feature requires authentication
- Feature is temporarily unavailable
- You want to show but not allow interaction
```tsx
<CardOffgrid
variant="neutral"
icon={<BetaIcon />}
title="Coming\nSoon"
description="This feature will be available in the next release."
disabled
/>
```
---
## Layout Best Practices
### Grid Layouts
**Recommended grid patterns:**
```tsx
// 2-column grid (desktop)
<div className="row">
<div className="col-md-6 mb-4">
<CardOffgrid {...props1} />
</div>
<div className="col-md-6 mb-4">
<CardOffgrid {...props2} />
</div>
</div>
// 3-column grid (desktop)
<div className="row">
{cards.map(card => (
<div key={card.id} className="col-md-4 mb-4">
<CardOffgrid {...card} />
</div>
))}
</div>
```
**Spacing:**
- Use Bootstrap spacing utilities (`mb-4`, `mb-5`) between cards
- Maintain consistent spacing in grids
- Cards are responsive and stack on mobile automatically
### Single Card Usage
For hero sections or featured highlights:
```tsx
<div className="d-flex justify-content-center">
<CardOffgrid
variant="green"
icon={<FeaturedIcon />}
title="New Feature"
description="Introducing our latest capability..."
href="/features/new"
/>
</div>
```
---
## Accessibility Best Practices
### Semantic HTML
The component automatically renders as:
- `<button>` when using `onClick`
- `<a>` when using `href`
This ensures proper semantic meaning for screen readers.
### Keyboard Navigation
**Always test:**
- Tab navigation moves focus to cards
- Enter/Space activates cards
- Focus ring is clearly visible
- Focus order follows logical reading order
### Screen Reader Content
**Ensure:**
- Titles are descriptive and unique
- Descriptions provide context
- Icons have appropriate `aria-hidden="true"` (handled automatically)
- Disabled cards communicate their state
### Color Contrast
All variants meet WCAG AA standards:
- Dark mode: White text on colored backgrounds
- Light mode: Dark text on light backgrounds
- Focus rings provide sufficient contrast
---
## Common Patterns
### Feature Showcase Grid
```tsx
const features = [
{
variant: 'green',
icon: <TokenIcon />,
title: 'Token\nManagement',
description: 'Create and manage fungible and non-fungible tokens.',
href: '/docs/tokens'
},
{
variant: 'neutral',
icon: <MetadataIcon />,
title: 'Onchain\nMetadata',
description: 'Store key asset information using simple APIs.',
href: '/docs/metadata'
},
// ... more features
];
<div className="row">
{features.map((feature, index) => (
<div key={index} className="col-md-4 mb-4">
<CardOffgrid {...feature} />
</div>
))}
</div>
```
### Mixed Variants for Hierarchy
Use green variant for primary features, neutral for supporting:
```tsx
<div className="row">
<div className="col-md-6 mb-4">
<CardOffgrid
variant="green" // Primary feature
icon={<PrimaryIcon />}
title="Main Feature"
description="Our flagship capability..."
href="/feature/main"
/>
</div>
<div className="col-md-6 mb-4">
<CardOffgrid
variant="neutral" // Supporting feature
icon={<SupportIcon />}
title="Supporting Feature"
description="Complementary capability..."
href="/feature/support"
/>
</div>
</div>
```
### Coming Soon Pattern
```tsx
<CardOffgrid
variant="neutral"
icon={<ComingSoonIcon />}
title="Coming\nSoon"
description="This feature is currently in development and will be available soon."
disabled
/>
```
---
## Performance Considerations
### Icon Optimization
**Best practices:**
- Use SVG React components (inlined) for small icons
- Use optimized SVG files for image icons
- Avoid large raster images
- Consider lazy loading for below-the-fold cards
### Rendering Performance
- Cards are lightweight components
- Hover animations use CSS transforms (GPU-accelerated)
- No heavy JavaScript calculations
- Suitable for grids with 10+ cards
---
## Troubleshooting
### Common Issues
**Card not clickable:**
- Ensure `onClick` or `href` is provided
- Check that `disabled` is not set to `true`
- Verify no parent element is blocking pointer events
**Icon not displaying:**
- Verify icon path is correct (if using string)
- Check icon component is properly imported
- Ensure icon fits within 68×68px bounds
**Hover animation not working:**
- Check browser supports CSS `clip-path`
- Verify no conflicting CSS is overriding transitions
- Test in different browsers
**Focus ring not visible:**
- Ensure keyboard navigation (Tab key)
- Check focus ring color contrasts with background
- Verify `outline-offset: 2px` is applied
---
## Design System Integration
### Color Tokens
All colors reference `styles/_colors.scss`:
- Dark mode (default): Uses `$gray-500`, `$gray-400`, `$green-300`, `$green-200`
- Light mode (`html.light`): Uses `$gray-200`, `$gray-300`, `$green-200`, `$green-300`
### Typography
- Title: Booton Light, 32px, -1px letter-spacing
- Description: Booton Light, 18px, -0.5px letter-spacing
### Spacing
- Card padding: 24px
- Content gap: 40px (between title and description)
- Icon container: 84×84px
---
## Figma References
- **Light Mode Colors**: [Figma Design - Light Mode](https://www.figma.com/design/vwDwMJ3mFrAklj5zvZwX5M/Card---OffGrid?node-id=8001-1963&m=dev)
- **Dark Mode Colors**: [Figma Design - Dark Mode](https://www.figma.com/design/vwDwMJ3mFrAklj5zvZwX5M/Card---OffGrid?node-id=8001-2321&m=dev)
- **Animation Specs**: [Figma Design - Storyboard](https://www.figma.com/design/vwDwMJ3mFrAklj5zvZwX5M/Card---OffGrid?node-id=8007-1096&m=dev)
---
## Component API
```typescript
interface CardOffgridProps {
/** Color variant: 'neutral' (default) or 'green' */
variant?: 'neutral' | 'green';
/** Icon element (ReactNode) or image path (string) */
icon: React.ReactNode | string;
/** Card title - use \n for line breaks */
title: string;
/** Card description text (1-2 sentences) */
description: string;
/** Click handler - renders as <button> */
onClick?: () => void;
/** Link destination - renders as <a> */
href?: string;
/** Disabled state - prevents interaction */
disabled?: boolean;
/** Additional CSS classes */
className?: string;
}
```
---
## Quick Reference
| Use Case | Variant | Interaction |
|----------|---------|-------------|
| Standard feature | `neutral` | `href` or `onClick` |
| Primary feature | `green` | `href` or `onClick` |
| Coming soon | `neutral` | `disabled` |
| Feature grid | Mix both | `href` preferred |
| Hero section | `green` | `href` |
---
## Examples
See the [CardOffgrid Showcase](/about/card-offgrid-showcase) for live examples and interactive demos.

View File

@@ -0,0 +1,362 @@
// BDS CardOffgrid Component Styles
// Brand Design System - Feature card with icon, title, and description
//
// Naming Convention: BEM with 'bds' namespace
// .bds-card-offgrid - Base card (resets button/anchor styles)
// .bds-card-offgrid--neutral - Neutral gray color variant (default)
// .bds-card-offgrid--green - Green color variant
// .bds-card-offgrid--disabled - Disabled state
// .bds-card-offgrid__overlay - Hover gradient overlay
// .bds-card-offgrid__icon-container - Icon wrapper (84x84px)
// .bds-card-offgrid__icon-image - Image icon styling
// .bds-card-offgrid__content - Title and description wrapper
// .bds-card-offgrid__title - Card title (uses .subhead-lg-l)
// .bds-card-offgrid__description - Card description (uses .body-l)
//
// Note: This file is imported within xrpl.scss after Bootstrap and project
// variables are loaded, so $grid-breakpoints, colors, and mixins are available.
//
// Theme: Site defaults to DARK mode. Light mode uses html.light selector.
// =============================================================================
// Design Tokens
// =============================================================================
// Note: Uses centralized spacing tokens from _spacing.scss where applicable.
// Component-specific values (dimensions, icon sizes) remain local.
// Dimensions (from Figma design spec)
$bds-card-offgrid-width: 400px;
$bds-card-offgrid-height: 480px;
$bds-card-offgrid-padding: $bds-space-2xl; // 24px - spacing('2xl')
$bds-card-offgrid-icon-container: 84px;
$bds-card-offgrid-icon-size: 68px;
$bds-card-offgrid-content-gap: $bds-space-4xl; // 40px - spacing('4xl')
// Animation - uses centralized tokens from _animations.scss
$bds-card-offgrid-transition-duration: $bds-transition-duration; // 200ms
$bds-card-offgrid-transition-timing: $bds-transition-timing;
// Typography - Title (now using .subhead-lg-l from _font.scss)
// Typography - Description (now using .body-l from _font.scss)
// -----------------------------------------------------------------------------
// Dark Mode Colors (Default)
// -----------------------------------------------------------------------------
// Neutral variant - Dark Mode
$bds-card-offgrid-neutral-default-dark: $gray-500; // #72777E
$bds-card-offgrid-neutral-hover-dark: $gray-400; // #8A919A
$bds-card-offgrid-neutral-pressed-dark: rgba($gray-500, 0.7); // 70% opacity
$bds-card-offgrid-neutral-text-dark: $white; // #FFFFFF
// Green variant - Dark Mode
$bds-card-offgrid-green-default-dark: $green-300; // #21E46B
$bds-card-offgrid-green-hover-dark: $green-200; // #70EE97
$bds-card-offgrid-green-pressed-dark: $green-400; // #0DAA3E
$bds-card-offgrid-green-text-dark: $black; // #000000
// Disabled - Dark Mode (30% opacity on gray-500)
$bds-card-offgrid-disabled-opacity-dark: 0.3;
$bds-card-offgrid-disabled-text-dark: $white; // #FFFFFF
// Focus ring - Dark Mode
$bds-card-offgrid-focus-color-dark: $white; // #FFFFFF
// -----------------------------------------------------------------------------
// Light Mode Colors
// -----------------------------------------------------------------------------
// Neutral variant - Light Mode
$bds-card-offgrid-neutral-default-light: $gray-200; // #E6EAF0
$bds-card-offgrid-neutral-hover-light: $gray-300; // #CAD4DF
$bds-card-offgrid-neutral-pressed-light: $gray-400; // #8A919A
$bds-card-offgrid-neutral-text-light: $gray-900; // #111112
$bds-card-offgrid-neutral-text-hover-light: $black; // #000000
// Green variant - Light Mode
$bds-card-offgrid-green-default-light: $green-200; // #70EE97
$bds-card-offgrid-green-hover-light: $green-300; // #21E46B
$bds-card-offgrid-green-pressed-light: $green-400; // #0DAA3E
$bds-card-offgrid-green-text-light: $black; // #000000
// Disabled - Light Mode
$bds-card-offgrid-disabled-bg-light: $gray-100; // #F0F3F7
$bds-card-offgrid-disabled-text-light: $gray-500; // #72777E
// Focus ring - Light Mode
$bds-card-offgrid-focus-color-light: $gray-900; // #111112
// =============================================================================
// Base Card Styles
// =============================================================================
.bds-card-offgrid {
// Reset button/anchor styles
appearance: none;
border: none;
background: none;
padding: 0;
margin: 0;
font: inherit;
color: inherit;
text-decoration: none;
cursor: pointer;
text-align: left;
// Card layout
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
width: $bds-card-offgrid-width;
height: $bds-card-offgrid-height;
padding: $bds-card-offgrid-padding;
overflow: hidden;
// Animation
transition: background-color $bds-card-offgrid-transition-duration $bds-card-offgrid-transition-timing,
opacity $bds-card-offgrid-transition-duration $bds-card-offgrid-transition-timing;
// Focus styles - Dark Mode (default)
// 1px gap between card and focus ring
&:focus {
outline: 2px solid $bds-card-offgrid-focus-color-dark;
outline-offset: 1px;
}
&:focus:not(:focus-visible) {
outline: none;
}
&:focus-visible {
outline: 2px solid $bds-card-offgrid-focus-color-dark;
outline-offset: 2px;
}
}
// =============================================================================
// Overlay (Color wipe animation - "Window Shade" effect)
// =============================================================================
// Hover in: shade rises from bottom to top (reveals)
// Hover out: shade falls from top to bottom (hides)
.bds-card-offgrid__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-card-offgrid-transition-duration $bds-card-offgrid-transition-timing;
}
// Hovered state: shade fully raised (visible)
// When transitioning TO this state, the top inset decreases = shade rises up
.bds-card-offgrid--hovered .bds-card-offgrid__overlay {
clip-path: inset(0 0 0 0);
}
// =============================================================================
// Icon Container
// =============================================================================
.bds-card-offgrid__icon-container {
position: relative;
z-index: 1; // Above overlay
display: flex;
align-items: center;
justify-content: center;
width: $bds-card-offgrid-icon-container;
height: $bds-card-offgrid-icon-container;
flex-shrink: 0;
// Icon sizing
> * {
max-width: $bds-card-offgrid-icon-size;
max-height: $bds-card-offgrid-icon-size;
}
}
.bds-card-offgrid__icon-image {
width: auto;
height: auto;
max-width: $bds-card-offgrid-icon-size;
max-height: $bds-card-offgrid-icon-size;
object-fit: contain;
}
// =============================================================================
// Content (Title + Description)
// =============================================================================
.bds-card-offgrid__content {
position: relative;
z-index: 1; // Above overlay
display: flex;
flex-direction: column;
gap: $bds-card-offgrid-content-gap;
}
.bds-card-offgrid__title {
// Typography handled by .subhead-lg-l class from _font.scss
margin-bottom: 0;
white-space: pre-wrap;
}
// =============================================================================
// DARK MODE (Default)
// =============================================================================
// -----------------------------------------------------------------------------
// Neutral Variant - Dark Mode
// -----------------------------------------------------------------------------
.bds-card-offgrid--neutral {
background-color: $bds-card-offgrid-neutral-default-dark;
color: $bds-card-offgrid-neutral-text-dark;
// Overlay color for hover wipe
.bds-card-offgrid__overlay {
background-color: $bds-card-offgrid-neutral-hover-dark;
}
// Pressed state
&:active:not(.bds-card-offgrid--disabled) {
.bds-card-offgrid__overlay {
background-color: $bds-card-offgrid-neutral-pressed-dark;
clip-path: inset(0 0 0 0);
}
}
}
// -----------------------------------------------------------------------------
// Green Variant - Dark Mode
// -----------------------------------------------------------------------------
.bds-card-offgrid--green {
background-color: $bds-card-offgrid-green-default-dark;
color: $bds-card-offgrid-green-text-dark;
// Overlay color for hover wipe
.bds-card-offgrid__overlay {
background-color: $bds-card-offgrid-green-hover-dark;
}
// Pressed state
&:active:not(.bds-card-offgrid--disabled) {
.bds-card-offgrid__overlay {
background-color: $bds-card-offgrid-green-pressed-dark;
clip-path: inset(0 0 0 0);
}
}
}
// -----------------------------------------------------------------------------
// Disabled State - Dark Mode
// -----------------------------------------------------------------------------
.bds-card-offgrid--disabled {
background-color: $bds-card-offgrid-neutral-default-dark;
color: $bds-card-offgrid-disabled-text-dark;
opacity: $bds-card-offgrid-disabled-opacity-dark;
cursor: not-allowed;
&:focus,
&:focus-visible {
outline: none;
}
}
// =============================================================================
// LIGHT MODE (html.light)
// =============================================================================
html.light {
// Focus styles - Light Mode
.bds-card-offgrid {
&:focus {
outline-color: $bds-card-offgrid-focus-color-light;
}
&:focus-visible {
outline-color: $bds-card-offgrid-focus-color-light;
}
}
// ---------------------------------------------------------------------------
// Neutral Variant - Light Mode
// ---------------------------------------------------------------------------
.bds-card-offgrid--neutral {
background-color: $bds-card-offgrid-neutral-default-light;
color: $bds-card-offgrid-neutral-text-light;
// Overlay color for hover wipe
.bds-card-offgrid__overlay {
background-color: $bds-card-offgrid-neutral-hover-light;
}
// Text color on hover
&.bds-card-offgrid--hovered {
color: $bds-card-offgrid-neutral-text-hover-light;
}
// Pressed state
&:active:not(.bds-card-offgrid--disabled) {
.bds-card-offgrid__overlay {
background-color: $bds-card-offgrid-neutral-pressed-light;
clip-path: inset(0 0 0 0);
}
}
}
// ---------------------------------------------------------------------------
// Green Variant - Light Mode
// ---------------------------------------------------------------------------
.bds-card-offgrid--green {
background-color: $bds-card-offgrid-green-default-light;
color: $bds-card-offgrid-green-text-light;
// Overlay color for hover wipe
.bds-card-offgrid__overlay {
background-color: $bds-card-offgrid-green-hover-light;
}
// Pressed state
&:active:not(.bds-card-offgrid--disabled) {
.bds-card-offgrid__overlay {
background-color: $bds-card-offgrid-green-pressed-light;
clip-path: inset(0 0 0 0);
}
}
}
// ---------------------------------------------------------------------------
// Disabled State - Light Mode
// ---------------------------------------------------------------------------
.bds-card-offgrid--disabled {
background-color: $bds-card-offgrid-disabled-bg-light;
color: $bds-card-offgrid-disabled-text-light;
opacity: 1; // Reset opacity, use background color instead
.bds-card-offgrid__icon-container {
opacity: 0.5;
}
}
}
// =============================================================================
// Responsive Styles
// =============================================================================
@media (max-width: 767px) {
.bds-card-offgrid {
width: 100%;
min-height: $bds-card-offgrid-height;
height: auto;
}
}

View File

@@ -0,0 +1,168 @@
import React, { useState, useCallback } from 'react';
import clsx from 'clsx';
export interface CardOffgridProps {
/** Color variant of the card */
variant?: 'neutral' | 'green';
/** Icon element or image source */
icon: React.ReactNode | string;
/** Card title (supports multi-line via \n) */
title: string;
/** Card description text */
description: string;
/** Click handler */
onClick?: () => void;
/** Link destination (renders as anchor if provided) */
href?: string;
/** Disabled state */
disabled?: boolean;
/** Optional className for custom styling */
className?: string;
}
/**
* BDS CardOffgrid Component
*
* A versatile card component for displaying feature highlights with an icon,
* title, and description. Supports neutral and green color variants with
* interactive states (hover, focus, pressed, disabled).
*
* Features a "window shade" color wipe animation:
* - Hover in: shade rises from bottom to top (reveals hover color)
* - Hover out: shade falls from top to bottom (hides hover color)
*
* @example
* // Basic neutral card
* <CardOffgrid
* variant="neutral"
* icon={<MetadataIcon />}
* title="Onchain Metadata"
* description="Easily store key asset information."
* onClick={() => console.log('clicked')}
* />
*
* @example
* // Green card with link
* <CardOffgrid
* variant="green"
* icon="/icons/metadata.svg"
* title="Onchain Metadata"
* description="Easily store key asset information."
* href="/docs/metadata"
* />
*/
export const CardOffgrid: React.FC<CardOffgridProps> = ({
variant = 'neutral',
icon,
title,
description,
onClick,
href,
disabled = false,
className = '',
}) => {
// Track hover state for animation
const [isHovered, setIsHovered] = useState(false);
const handleMouseEnter = useCallback(() => {
if (!disabled) {
setIsHovered(true);
}
}, [disabled]);
const handleMouseLeave = useCallback(() => {
if (!disabled) {
setIsHovered(false);
}
}, [disabled]);
// Build class names using BEM with bds namespace
const classNames = clsx(
'bds-card-offgrid',
`bds-card-offgrid--${variant}`,
{
'bds-card-offgrid--disabled': disabled,
'bds-card-offgrid--hovered': isHovered,
},
className
);
// Render icon - supports both React nodes and image URLs
const renderIcon = () => {
if (typeof icon === 'string') {
return (
<img
src={icon}
alt=""
className="bds-card-offgrid__icon-image"
aria-hidden="true"
/>
);
}
return icon;
};
// Split title by newline for multi-line support
const renderTitle = () => {
const lines = title.split('\n');
return lines.map((line, index) => (
<React.Fragment key={index}>
{line}
{index < lines.length - 1 && <br />}
</React.Fragment>
));
};
// Common content for both button and anchor
const content = (
<>
{/* Hover color wipe overlay */}
<span className="bds-card-offgrid__overlay" aria-hidden="true" />
<span className="bds-card-offgrid__icon-container">
{renderIcon()}
</span>
<span className="bds-card-offgrid__content">
<span className="bds-card-offgrid__title sh-lg-r">
{renderTitle()}
</span>
<span className="bds-card-offgrid__description body-l">
{description}
</span>
</span>
</>
);
// Render as anchor if href is provided
if (href && !disabled) {
return (
<a
href={href}
className={classNames}
aria-disabled={disabled}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{content}
</a>
);
}
// Render as button for onClick or disabled state
return (
<button
type="button"
className={classNames}
onClick={disabled ? undefined : onClick}
disabled={disabled}
aria-disabled={disabled}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{content}
</button>
);
};
export default CardOffgrid;

View File

@@ -0,0 +1,2 @@
export { CardOffgrid, type CardOffgridProps } from './CardOffgrid';
export { default } from './CardOffgrid';

View File

@@ -0,0 +1,456 @@
# CardStat Component Documentation
## Overview
The CardStat component is a statistics card that displays prominent numerical data with a descriptive label and optional call-to-action buttons. Following the XRPL Brand Design System (BDS), it supports four color variants and responsive layouts to showcase key metrics and statistics effectively.
## Features
- **Four Color Variants**: Lilac (default), Green, Light Gray, and Dark Gray
- **Flexible Content**: Large statistic text with descriptive label
- **Optional CTAs**: Support for 0, 1, or 2 buttons (Primary and/or Secondary)
- **Responsive Design**: Adaptive sizing and typography across breakpoints
- **Theme Support**: Automatic light/dark mode adaptation
- **Accessible**: Semantic HTML with proper button accessibility
## Props API
```typescript
interface ButtonConfig {
/** Button label text */
label: string;
/** Click handler for button */
onClick?: () => void;
/** Link href for button */
href?: string;
}
interface CardStatProps {
/** The main statistic to display (e.g., "6 Million+") */
statistic: string;
/** Descriptive label for the statistic */
label: string;
/** Background color variant */
variant?: 'lilac' | 'green' | 'light-gray' | 'dark-gray';
/** Primary button configuration */
primaryButton?: ButtonConfig;
/** Secondary button configuration */
secondaryButton?: ButtonConfig;
/** Additional CSS classes */
className?: string;
}
```
### Default Values
- `variant`: `'lilac'`
- `primaryButton`: `undefined` (no button)
- `secondaryButton`: `undefined` (no button)
- `className`: `''`
## Color Variants
### Lilac (Default)
A purple/lavender background that provides a soft, friendly appearance.
**Best For:**
- User-focused statistics (active wallets, users)
- Community metrics
- Engagement statistics
**Usage:**
```tsx
<CardStat
statistic="6 Million+"
label="Active wallets"
variant="lilac"
/>
```
### Green
XRPL brand green background that reinforces brand identity.
**Best For:**
- Financial metrics (transaction volume, value moved)
- Growth statistics
- Positive indicators
**Usage:**
```tsx
<CardStat
statistic="$1 Trillion+"
label="Value moved"
variant="green"
/>
```
### Light Gray
Light gray background for technology and reliability metrics.
**Best For:**
- Technical statistics (uptime, performance)
- Infrastructure metrics
- Stability indicators
**Usage:**
```tsx
<CardStat
statistic="12+"
label="Continuous uptime years"
variant="light-gray"
/>
```
### Dark Gray
Neutral dark gray background for general-purpose statistics.
**Best For:**
- Neutral metrics
- Secondary information
- Supporting statistics
**Usage:**
```tsx
<CardStat
statistic="70+"
label="Ecosystem partners"
variant="dark-gray"
/>
```
## Button Configurations
### No Buttons
Display statistics without call-to-action buttons for informational purposes.
```tsx
<CardStat
statistic="6 Million+"
label="Active wallets"
variant="lilac"
/>
```
### Single Button (Primary)
Include one primary button for a main call-to-action.
```tsx
<CardStat
statistic="6 Million+"
label="Active wallets"
variant="lilac"
primaryButton={{
label: "Learn More",
href: "/wallets"
}}
/>
```
### Two Buttons (Primary + Secondary)
Include both primary and secondary buttons for multiple actions.
```tsx
<CardStat
statistic="6 Million+"
label="Active wallets"
variant="lilac"
primaryButton={{
label: "Learn More",
href: "/wallets"
}}
secondaryButton={{
label: "Get Started",
onClick: handleGetStarted
}}
/>
```
## How It Works
### Component Structure
The CardStat component uses BEM (Block Element Modifier) naming convention with the `bds` namespace:
- `.bds-card-stat` - Base card class
- `.bds-card-stat--lilac` - Lilac variant modifier
- `.bds-card-stat--green` - Green variant modifier
- `.bds-card-stat--light-gray` - Light gray variant modifier
- `.bds-card-stat--dark-gray` - Dark gray variant modifier
- `.bds-card-stat__content` - Inner content wrapper
- `.bds-card-stat__text` - Text section container
- `.bds-card-stat__statistic` - Large statistic text
- `.bds-card-stat__label` - Descriptive label text
- `.bds-card-stat__buttons` - Button container
- `.bds-card-stat__button-link` - Link wrapper for buttons
### Layout
The card uses flexbox for vertical layout:
1. **Content wrapper** (`.bds-card-stat__content`): Main container with padding
2. **Text section** (`.bds-card-stat__text`): Contains statistic and label
3. **Buttons section** (`.bds-card-stat__buttons`): Optional CTA buttons
The layout automatically adjusts spacing between sections using flexbox `justify-content: space-between`.
### Responsive Behavior
The component adapts to different screen sizes:
**Base (Mobile, <576px):**
- Padding: 8px
- Min height: 200px
- Statistic: 64px font size
- Label: 16px font size
**MD (Tablet, ≥576px):**
- Padding: 12px
- Min height: 208px
- Same typography as mobile
**LG+ (Desktop, ≥992px):**
- Padding: 16px
- Min height: 298px
- Statistic: 72px font size
- Label: 18px font size
### Button Integration
Buttons use the existing BDS Button component with `color="black"` variant to ensure proper contrast on colored backgrounds. Buttons can be either:
- **Links** (`href` provided): Wrapped in an anchor tag for navigation
- **Actions** (`onClick` provided): Rendered as interactive buttons
## Typography
### Statistic Text
- **Font**: Booton, sans-serif
- **Weight**: 228 (Light)
- **Size**: 64px (mobile), 72px (desktop)
- **Line Height**: 70.4px (mobile), 79.2px (desktop)
- **Letter Spacing**: -4.5px (mobile), -5px (desktop)
### Label Text
- **Font**: Booton, sans-serif
- **Weight**: 400 (Regular)
- **Size**: 16px (mobile), 18px (desktop)
- **Line Height**: 23.2px (mobile), 26.1px (desktop)
- **Letter Spacing**: 0px (mobile), -0.5px (desktop)
## Spacing & Layout
- **Border Radius**: 8px
- **Button Gap**: 8px between buttons
- **Text Gap**: 4px between statistic and label
- **Min Heights**: 200px (mobile), 208px (tablet), 298px (desktop)
## Usage Examples
### Basic Usage
```tsx
import { CardStat } from 'shared/components/CardStat';
// Simple statistic card
<CardStat
statistic="6 Million+"
label="Active wallets"
/>
// With color variant
<CardStat
statistic="$1 Trillion+"
label="Value moved"
variant="green"
/>
```
### With Buttons
```tsx
// Single button (Light Gray variant)
<CardStat
statistic="12+"
label="Continuous uptime years"
variant="light-gray"
primaryButton={{
label: "Learn More",
href: "/about/uptime"
}}
/>
// Two buttons (Dark Gray variant)
<CardStat
statistic="70+"
label="Ecosystem partners"
variant="dark-gray"
primaryButton={{
label: "View Partners",
href: "/partners"
}}
secondaryButton={{
label: "Become Partner",
onClick: handlePartnerSignup
}}
/>
```
### In PageGrid Layout
```tsx
import { CardStat } from 'shared/components/CardStat';
import { PageGrid, PageGridRow, PageGridCol } from 'shared/components/PageGrid/page-grid';
<PageGrid>
<PageGridRow>
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
<CardStat
statistic="6 Million+"
label="Active wallets"
variant="lilac"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
<CardStat
statistic="$1 Trillion+"
label="Value moved"
variant="green"
/>
</PageGridCol>
<PageGridCol span={{ base: 4, md: 4, lg: 4 }}>
<CardStat
statistic="12+"
label="Continuous uptime years"
variant="light-gray"
/>
</PageGridCol>
</PageGridRow>
</PageGrid>
```
### With Click Handlers
```tsx
const handleLearnMore = () => {
console.log('Learn more clicked');
};
<CardStat
statistic="6 Million+"
label="Active wallets"
variant="lilac"
primaryButton={{
label: "Learn More",
onClick: handleLearnMore
}}
/>
```
## Accessibility
### Semantic HTML
- Uses `<div>` elements with proper structure
- Button components handle their own accessibility
- Text content is readable by screen readers
### Keyboard Navigation
- Buttons are fully keyboard accessible (Tab, Enter, Space)
- Links are keyboard navigable (Tab, Enter)
- Focus indicators provided by Button component
### Color Contrast
All variants meet WCAG AA standards:
- **Black text on colored backgrounds**: Sufficient contrast
- **Buttons**: Use black variant with proper contrast on all backgrounds
- **Dark mode dark gray**: Switches to white text for proper contrast
## Best Practices
1. **Keep statistics concise** - Use abbreviations (M for million, K for thousand)
2. **Use descriptive labels** - Make it clear what the statistic represents
3. **Choose appropriate variants** - Match color to the type of metric
4. **Limit button usage** - Use buttons only when actionable context is needed
5. **Consider grid layouts** - Use PageGrid for consistent spacing and alignment
6. **Test responsiveness** - Verify cards adapt properly at all breakpoints
## When to Use
Use CardStat to:
- **Highlight key metrics** - Show important numbers prominently
- **Dashboard sections** - Create stat-focused areas on landing pages
- **About pages** - Showcase company or product statistics
- **Feature sections** - Emphasize quantitative benefits
- **Report summaries** - Display high-level data points
## When NOT to Use
Avoid CardStat when:
- **Complex data** - Use charts or tables for detailed information
- **Multiple related metrics** - Consider data visualization components
- **Non-numeric content** - Use regular cards for text-heavy content
- **Real-time updates** - Consider dedicated dashboard components
## Design Tokens
The component uses design tokens from the XRPL Brand Design System:
### Colors
- `$bds-card-stat-lilac-bg`: `#D9CAFF`
- `$bds-card-stat-green-bg`: `$green-300` (#EAFCF1)
- `$bds-card-stat-light-gray-bg`: `#CAD4DF` (light gray)
- `$bds-card-stat-dark-gray-bg`: `#8A919A` (dark gray)
- Text: `#141414` (neutral black)
### Spacing
- Border radius: `8px`
- Button gap: `8px`
- Text gap: `4px`
- Content padding: `8px` (mobile), `12px` (tablet), `16px` (desktop)
### Motion
- Transition duration: `150ms`
- Timing function: `cubic-bezier(0.98, 0.12, 0.12, 0.98)`
## Theme Support
The component automatically adapts for light and dark modes:
### Light Mode (Default)
- All color variants use their defined light backgrounds
- Black text on colored backgrounds
### Dark Mode
- **Dark Gray variant**: Switches to darker gray background (`$gray-400`) with white text
- **Other variants**: Maintain colored backgrounds with black text
## Showcase
See the interactive showcase at `/about/cardstat-showcase` for live examples of all variants, button configurations, and responsive behavior in PageGrid layouts.
## File Structure
```
shared/components/CardStat/
├── CardStat.tsx # Component implementation
├── CardStat.scss # Component styles
├── CardStat.md # This documentation
└── index.ts # Exports
```
## Related Components
- **Button** - Used for CTA buttons within cards
- **PageGrid** - For laying out multiple cards in responsive grids
- **Divider** - Can be used to separate card sections if needed

View File

@@ -0,0 +1,157 @@
// BDS CardStat Component Styles
// Brand Design System - Statistics card component
//
// Naming Convention: BEM with 'bds' namespace
// .bds-card-stat - Base card
// .bds-card-stat--lilac - Lilac variant
// .bds-card-stat--green - Green variant
// .bds-card-stat--light-gray - Light gray variant
// .bds-card-stat--dark-gray - Dark gray variant
// .bds-card-stat__content - Inner content wrapper
// .bds-card-stat__text - Text section
// .bds-card-stat__statistic - Large statistic text
// .bds-card-stat__label - Descriptive label
// .bds-card-stat__buttons - Button container
// .bds-card-stat__button-link - Link wrapper for buttons
// =============================================================================
// Design Tokens
// =============================================================================
// Note: Uses centralized spacing tokens from _spacing.scss where applicable.
// 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
// Spacing - uses centralized tokens
$bds-card-stat-gap: $bds-space-sm; // 8px - spacing('sm')
// Transitions
$bds-card-stat-transition-duration: 150ms; // Component-specific (faster than default)
$bds-card-stat-transition-timing: $bds-transition-timing;
// =============================================================================
// Base Card Styles
// =============================================================================
.bds-card-stat {
// Layout
display: flex !important;
flex-direction: column;
width: 100%;
min-height: 200px;
height: 100%;
justify-content: space-between;
flex: 1;
padding: $bds-space-sm; // 8px - spacing('sm')
gap: $bds-space-xs; // 4px - spacing('xs')
// 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;
// Tablet (md) breakpoint
@include media-breakpoint-up(md) {
padding: $bds-space-md; // 12px - spacing('md')
}
// Desktop (lg+) breakpoint
@include media-breakpoint-up(lg) {
padding: $bds-space-lg; // 16px - spacing('lg')
}
// Text section
&__text {
display: flex;
flex-direction: column;
gap: $bds-space-sm; // 8px - spacing('sm')
}
// 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;
flex-wrap: wrap;
gap: $bds-card-stat-gap;
align-items: flex-end;
}
}
// =============================================================================
// Color Variants (generated from map)
// =============================================================================
@each $variant, $colors in $bds-card-stat-variants {
$light-bg: nth($colors, 1);
$dark-bg: nth($colors, 2);
.bds-card-stat--#{$variant} {
background-color: $light-bg;
}
// Dark mode override (only if dark-mode color is defined)
@if $dark-bg != null {
html.dark .bds-card-stat--#{$variant} {
background-color: $dark-bg;
}
}
}
// =============================================================================
// Responsive Height Adjustments
// =============================================================================
.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;
}
}
.bds-grid__col-lg-3 {
.bds-card-stat {
@include media-breakpoint-up(lg) {
aspect-ratio: 1 / 1;
min-height: unset;
height: auto;
}
}
}

View File

@@ -0,0 +1,129 @@
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 */
label: string;
/** Click handler for button */
onClick?: () => void;
/** Link href for button */
href?: string;
}
/** Responsive span configuration for PageGridCol */
type SpanConfig = Partial<Record<PageGridBreakpoint, number>>;
export interface CardStatProps {
/** The main statistic to display (e.g., "6 Million+") */
statistic: string;
/** Superscript text for the statistic (symbols like '*', '+' or numeric strings like '1', '12') */
superscript?: string;
/** Descriptive label for the statistic */
label: string;
/** Background color variant
* - 'lilac': Purple/lavender background
* - 'green': XRPL brand green background
* - 'light-gray': Light gray background (#CAD4DF)
* - 'dark-gray': Dark gray background (#8A919A)
*/
variant?: 'lilac' | 'green' | 'light-gray' | 'dark-gray';
/** Primary button configuration */
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;
}
/**
* BDS CardStat Component
*
* A statistics card component following the XRPL Brand Design System.
* Displays a prominent statistic with a descriptive label and optional CTA buttons.
*
* Color variants:
* - lilac: Purple/lavender background
* - green: XRPL brand green background
* - light-gray: Light gray background
* - dark-gray: Dark gray background
*
* @example
* <CardStat
* statistic="6 Million+"
* label="Active wallets"
* variant="lilac"
* 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,
label,
variant = 'lilac',
primaryButton,
secondaryButton,
span = DEFAULT_SPAN,
className = '',
}) => {
// Build class names using BEM with bds namespace
const classNames = [
'bds-card-stat',
`bds-card-stat--${variant}`,
className,
]
.filter(Boolean)
.join(' ');
const hasButtons = primaryButton || secondaryButton;
// Check if superscript is a number (one or more digits), excluding + or - signs
const isNumericSuperscript = superscript && /^[0-9]+$/.test(superscript);
return (
<PageGridCol span={span} as="li" className={classNames}>
{/* Text section */}
<div className="bds-card-stat__text">
<div className="bds-card-stat__statistic">
{statistic}{superscript && <sup className={isNumericSuperscript ? 'bds-card-stat__superscript--numeric' : ''}>{superscript}</sup>}</div>
<div className="body-r">{label}</div>
</div>
{/* Buttons section */}
{hasButtons && (
<div className="bds-card-stat__buttons">
{primaryButton && (
<Button
forceColor
variant="primary"
color="black"
href={primaryButton.href}
onClick={primaryButton.onClick}
>
{primaryButton.label}
</Button>
)}
{secondaryButton && (
<Button
forceColor
variant="secondary"
color="black"
href={secondaryButton.href}
onClick={secondaryButton.onClick}
>
{secondaryButton.label}
</Button>
)}
</div>
)}
</PageGridCol>
);
};
export default CardStat;

View File

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

View File

@@ -0,0 +1,130 @@
// BDS CardTextIconCard Pattern Styles
// Brand Design System - Card with icon, heading, and description
//
// Naming Convention: BEM with 'bds' namespace
// .bds-card-text-icon-card - Base card container
// .bds-card-text-icon-card__icon - Icon container
// .bds-card-text-icon-card__icon-img - Icon image
// .bds-card-text-icon-card__heading - Card heading
// .bds-card-text-icon-card__description - Card description (supports ReactNode/links)
//
// Built from Section Cards - Icon and Section Cards - Text Grid Figma designs
// =============================================================================
// Design Tokens
// =============================================================================
// Icon sizes (responsive)
$bds-card-text-icon-icon-size-sm: 32px;
$bds-card-text-icon-icon-size-md: 36px;
$bds-card-text-icon-icon-size-lg: 40px;
// Padding (uses BDS card presets from _spacing.scss)
$bds-card-text-icon-padding-sm: $bds-padding-card-sm;
$bds-card-text-icon-padding-md: $bds-padding-card-md;
$bds-card-text-icon-padding-lg: $bds-padding-card-lg;
// Gaps (between icon, heading, description)
$bds-card-text-icon-gap-sm: $bds-padding-card-sm;
$bds-card-text-icon-gap-md: $bds-padding-card-md;
$bds-card-text-icon-gap-lg: $bds-padding-card-lg;
// =============================================================================
// Card Component
// =============================================================================
.bds-card-text-icon-card {
display: flex !important;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
text-decoration: none;
box-sizing: border-box;
border-left: 1px solid $gray-300;
margin-bottom: $bds-space-2xl;
// When used standalone (not as grid column), take full width
&:not(.bds-card-text-icon-card--grid-col) {
width: 100%;
}
padding: $bds-card-text-icon-padding-sm;
gap: $bds-card-text-icon-gap-sm;
@include media-breakpoint-up(md) {
padding: $bds-card-text-icon-padding-md;
gap: $bds-card-text-icon-gap-md;
margin-bottom: $bds-space-3xl;
}
@include media-breakpoint-up(lg) {
padding: $bds-card-text-icon-padding-lg;
gap: $bds-card-text-icon-gap-lg;
margin-bottom: $bds-space-4xl;
}
// Aspect ratio foundation - when CSS variable is set via aspectRatio prop
&[style*="--bds-card-text-icon-aspect-ratio"] {
aspect-ratio: var(--bds-card-text-icon-aspect-ratio);
}
@include bds-theme-mode(dark) {
color: $white;
}
}
// =============================================================================
// Icon
// =============================================================================
.bds-card-text-icon-card__icon {
width: 100%;
display: flex;
flex-direction: column;
gap: $bds-space-lg;
&-img {
object-fit: contain;
display: block;
flex-shrink: 0;
width: $bds-card-text-icon-icon-size-sm;
height: $bds-card-text-icon-icon-size-sm;
@include media-breakpoint-up(md) {
width: $bds-card-text-icon-icon-size-md;
height: $bds-card-text-icon-icon-size-md;
}
@include media-breakpoint-up(lg) {
width: $bds-card-text-icon-icon-size-lg;
height: $bds-card-text-icon-icon-size-lg;
}
}
}
// =============================================================================
// Heading
// =============================================================================
.bds-card-text-icon-card__heading {
margin: 0;
color: inherit;
}
// =============================================================================
// Description (supports ReactNode - links, formatted text, etc.)
// =============================================================================
.bds-card-text-icon-card__description {
margin: 0;
color: inherit;
min-width: 0;
position: relative;
a {
color: inherit;
text-decoration: underline;
text-underline-offset: 2px;
&:hover {
text-decoration-thickness: 2px;
}
}
}

View File

@@ -0,0 +1,121 @@
import React from 'react';
import clsx from 'clsx';
import { PageGrid } from '../PageGrid/page-grid';
import type { ResponsiveValue, PageGridSpanValue } from '../PageGrid/page-grid';
export interface CardTextIconCardProps {
/** Icon image URL */
icon?: string;
/** Alt text for the icon image */
iconAlt?: string;
/** Card heading */
heading: string;
/** Card description; accepts rich content (e.g., text with inline links) */
description: React.ReactNode | string;
/** Optional aspect ratio for future use; applied via CSS variable */
aspectRatio?: number;
/** When provided, renders as PageGrid.Col as="li" with this span—card becomes the grid column */
gridColSpan?: ResponsiveValue<PageGridSpanValue>;
/** Additional CSS classes */
className?: string;
/** Optional height and width for the icon image */
height?: number;
width?: number;
}
/**
* CardTextIconCard Component
*
* A card component featuring an icon, heading, and description.
* Built from Section Cards - Icon and Section Cards - Text Grid Figma designs.
*
* The description accepts ReactNode so it can include hyperlinks and other rich content.
*
* @example
* // Basic usage
* <CardTextIconCard
* icon="/icons/docs.svg"
* iconAlt="Documentation"
* heading="Documentation"
* description="Access everything you need to get started with the XRPL."
* />
*
* @example
* // With inline link in description
* <CardTextIconCard
* icon="/icons/docs.svg"
* heading="Documentation"
* description={
* <>
* Learn more in our{' '}
* <a href="/docs">documentation</a>.
* </>
* }
* />
*/
const cardContent = (
heading: string,
description: React.ReactNode | string,
icon?: string,
iconAlt?: string,
iconHeight?: number,
iconWidth?: number
) => (
<>
<div className="bds-card-text-icon-card__icon">
{icon && (
<img
src={icon}
alt={iconAlt}
{...(iconHeight != null && { height: iconHeight })}
{...(iconWidth != null && { width: iconWidth })}
className="bds-card-text-icon-card__icon-img"
/>
)}
<strong className="bds-card-text-icon-card__heading sh-md-r">{heading}</strong>
</div>
<p className="bds-card-text-icon-card__description body-l">
{description}
</p>
</>
);
export const CardTextIconCard: React.FC<CardTextIconCardProps> = ({
icon,
iconAlt = '',
heading,
description,
aspectRatio,
gridColSpan,
className,
height,
width
}) => {
const style = aspectRatio
? ({ '--bds-card-text-icon-aspect-ratio': aspectRatio } as React.CSSProperties)
: undefined;
if (gridColSpan) {
return (
<PageGrid.Col
as="li"
span={gridColSpan}
className={clsx('bds-card-text-icon-card', 'bds-card-text-icon-card--grid-col', className)}
style={style}
>
{cardContent(heading, description, icon, iconAlt, height, width)}
</PageGrid.Col>
);
}
return (
<div
className={clsx('bds-card-text-icon-card', className)}
style={style}
>
{cardContent(heading, description, icon, iconAlt, height, width)}
</div>
);
};
export default CardTextIconCard;

View File

@@ -0,0 +1,116 @@
# CardTextIconCard Component
A card component featuring an icon, heading, and description. Built from Section Cards - Icon and Section Cards - Text Grid Figma designs.
## Overview
CardTextIconCard displays an icon at the top, followed by a heading and description. The description accepts `ReactNode`, so it can include hyperlinks and other rich content. No buttons; links are inline within the description.
## Features
- **Icon + Text Layout**: Icon container, heading, and description in a vertical stack (optional)
- **Rich Description**: `description` accepts `ReactNode` for inline links and formatted content
- **Aspect Ratio Foundation**: Optional `aspectRatio` prop for future responsive sizing
- **Light/Dark Mode**: Full theming support
- **Responsive Design**: Adaptive icon size and spacing across breakpoints
## Usage
### Basic Usage
```tsx
<CardTextIconCard
icon="/icons/docs.svg"
iconAlt="Documentation"
heading="Documentation"
description="Access everything you need to get started with the XRPL."
/>
```
### With Inline Link in Description
```tsx
<CardTextIconCard
icon="/icons/docs.svg"
heading="Documentation"
description={
<>
Learn more in our{' '}
<a href="/docs">documentation</a>.
</>
}
/>
```
### With Aspect Ratio
```tsx
<CardTextIconCard
icon="/icons/docs.svg"
heading="Documentation"
description="Access everything you need."
aspectRatio={4 / 3}
/>
```
## Props
### CardTextIconCardProps
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `icon` | `string` | Optional | Icon image URL |
| `iconAlt` | `string` | `''` | Optional | Alt text for the icon image |
| `heading` | `string` | Required | Card heading |
| `description` | `React.ReactNode` | Required | Card description; accepts rich content (e.g., text with inline links) |
| `aspectRatio` | `number` | - | Optional ratio for future use; applied via CSS variable |
| `className` | `string` | - | Additional CSS classes |
## Component Structure
```tsx
<>
<div className="bds-card-text-icon-card__icon">
{icon && (
<img
src={icon}
alt={iconAlt}
{...(iconHeight != null && { height: iconHeight })}
{...(iconWidth != null && { width: iconWidth })}
className="bds-card-text-icon-card__icon-img"
/>
)}
<strong className="bds-card-text-icon-card__heading sh-md-r">{heading}</strong>
</div>
<p className="bds-card-text-icon-card__description body-l">
{description}
</p>
</>
```
## Responsive Sizing
| Breakpoint | Icon Size | Padding | Gap |
|------------|-----------|---------|-----|
| Base (< 576px) | 40px | 16px | 16px |
| MD (576px - 991px) | 36px | 20px | 20px |
| LG (≥ 992px) | 64px | 32px | 24px |
## Files
- `CardTextIconCard.tsx` - React component with TypeScript
- `CardTextIconCard.scss` - Styles with BEM naming
- `index.ts` - Barrel exports
- `README.md` - This file
## Import
```tsx
import { CardTextIconCard } from 'shared/components/CardTextIcon';
// or
import { CardTextIconCard, type CardTextIconCardProps } from 'shared/components/CardTextIcon';
```
## Design System
Part of the Brand Design System (BDS) with `bds-` namespace prefix.

View File

@@ -0,0 +1 @@
export { CardTextIconCard, type CardTextIconCardProps } from './CardTextIconCard';

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

@@ -0,0 +1,350 @@
# Divider Component Documentation
## Overview
The Divider component is a visual separator that creates clear boundaries between content sections, elements, or groups. Following the XRPL Brand Design System (BDS), it supports two orientations, three stroke weights, and three color variants to adapt to different visual contexts and hierarchy needs.
## Features
- **Two Orientations**: Horizontal (default) and Vertical
- **Three Stroke Weights**: Thin (0.5px), Regular (1px), Strong (2px)
- **Three Color Variants**: Gray (default), Base (adapts to theme), Green
- **Theme Support**: Automatic light/dark mode adaptation
- **Accessibility**: Configurable for decorative or semantic use
- **Flexible Sizing**: Inherits width/height from parent container
## Props API
```typescript
interface DividerProps {
/** Divider orientation - horizontal separates vertical content, vertical separates horizontal content */
orientation?: 'horizontal' | 'vertical';
/** Stroke weight - controls visual thickness */
weight?: 'thin' | 'regular' | 'strong';
/** Color variant - gray (default), base for high contrast (adapts to theme), green for brand emphasis */
color?: 'gray' | 'base' | 'green';
/** Additional CSS classes */
className?: string;
/** Whether the divider is purely decorative (hides from screen readers) */
decorative?: boolean;
}
```
### Default Values
- `orientation`: `'horizontal'`
- `weight`: `'regular'`
- `color`: `'gray'`
- `className`: `''`
- `decorative`: `true`
## Orientations
### Horizontal Divider
Horizontal dividers extend left to right to separate vertically stacked content. They span the full width of their container by default.
**Common Uses:**
- Between content blocks or sections
- Separating list items
- Within cards to divide content areas
**Usage:**
```tsx
<Divider orientation="horizontal" />
// or simply (horizontal is default)
<Divider />
```
### Vertical Divider
Vertical dividers extend top to bottom to separate horizontally aligned content. They inherit height from their parent container.
**Common Uses:**
- Between columns in a layout
- Separating navigation items
- Within toolbars or action bars
**Usage:**
```tsx
<Divider orientation="vertical" />
```
**Note:** Vertical dividers require a parent container with a defined height.
## Stroke Weights
### Thin (0.5px)
The lightest weight for subtle, unobtrusive separation. Use when content is closely related but needs minimal visual distinction.
**Best For:**
- Within cards or small content areas
- Between tightly grouped elements
- Dense layouts where heavier dividers would feel cluttered
**Usage:**
```tsx
<Divider weight="thin" />
```
### Regular (1px) - Default
The standard weight for most use cases. Provides clear separation without dominating the visual hierarchy.
**Best For:**
- Most layout and section separations
- Between content blocks
- General-purpose dividers
**Usage:**
```tsx
<Divider weight="regular" />
// or simply (regular is default)
<Divider />
```
### Strong (2px)
The heaviest weight for maximum emphasis. Use sparingly to highlight major transitions or boundaries.
**Best For:**
- Major section breaks
- Key boundaries between distinct content areas
- Emphasizing important transitions
**Usage:**
```tsx
<Divider weight="strong" />
```
## Color Variants
### Gray (Default)
Neutral, subtle separation that works in most contexts without drawing attention.
**Best For:**
- Most separations
- Subtle visual breaks
- Backgrounds where you don't want the divider to stand out
**Usage:**
```tsx
<Divider color="gray" />
// or simply (gray is default)
<Divider />
```
### Base
High-contrast separation that adapts to the theme - renders as white in dark mode and black in light mode.
**Best For:**
- When maximum contrast is needed
- Important structural boundaries
- Universal high-visibility dividers
**Usage:**
```tsx
<Divider color="base" />
```
### Green
Brand-colored separation that reinforces XRPL identity or indicates active/important areas.
**Best For:**
- Highlighting branded sections
- Active or selected states
- Drawing attention to specific content areas
**Usage:**
```tsx
<Divider color="green" />
```
## When to Use
Use a Divider to:
- **Separate content sections** - Create clear boundaries between distinct content groups
- **Organize lists** - Divide list items for better scanability
- **Structure cards** - Separate header, body, and footer areas within cards
- **Define columns** - Visually separate side-by-side content
- **Indicate transitions** - Mark boundaries between zones of information
## When NOT to Use
Avoid using a Divider when:
- **Spacing alone is sufficient** - If whitespace provides enough separation, skip the divider
- **Backgrounds provide contrast** - Different background colors may eliminate the need for dividers
- **It adds clutter** - In minimal designs, too many dividers can distract from content
- **To compensate for poor spacing** - Dividers should enhance, not replace, proper layout structure
## Usage Examples
### Basic Usage
```tsx
import { Divider } from 'shared/components/Divider';
// Default horizontal divider
<Divider />
// Vertical divider
<Divider orientation="vertical" />
// Strong green divider for emphasis
<Divider weight="strong" color="green" />
```
### List Separation
```tsx
<div className="list">
<div className="list-item">Item One</div>
<Divider weight="thin" />
<div className="list-item">Item Two</div>
<Divider weight="thin" />
<div className="list-item">Item Three</div>
</div>
```
### Card Content Separation
```tsx
<div className="card">
<div className="card-header">
<h3>Card Title</h3>
</div>
<Divider weight="thin" color="gray" />
<div className="card-body">
<p>Card content goes here...</p>
</div>
<Divider weight="thin" color="gray" />
<div className="card-footer">
<button>Action</button>
</div>
</div>
```
### Column Separation
```tsx
<div className="d-flex align-items-stretch" style={{ height: '200px' }}>
<div className="column">Column One</div>
<Divider orientation="vertical" color="gray" />
<div className="column">Column Two</div>
<Divider orientation="vertical" color="gray" />
<div className="column">Column Three</div>
</div>
```
### Navigation Separation
```tsx
<nav className="d-flex align-items-center" style={{ height: '24px', gap: '1rem' }}>
<a href="/">Home</a>
<Divider orientation="vertical" weight="thin" />
<a href="/docs">Documentation</a>
<Divider orientation="vertical" weight="thin" />
<a href="/api">API</a>
</nav>
```
### Major Section Break
```tsx
<section>
<h2>Primary Content</h2>
<p>Main content area...</p>
</section>
<Divider weight="strong" color="green" />
<section>
<h2>Secondary Content</h2>
<p>Supporting content area...</p>
</section>
```
### Semantic Divider (Accessible)
```tsx
// For dividers that should be announced by screen readers
<Divider decorative={false} />
```
## Accessibility
### Decorative vs Semantic
By default, dividers are decorative (`decorative={true}`) and hidden from screen readers:
- Sets `aria-hidden="true"`
- Uses `role="presentation"`
For semantic dividers that should be announced:
- Set `decorative={false}`
- Adds `role="separator"`
- Includes `aria-orientation` attribute
### Keyboard Navigation
Dividers are non-interactive elements and do not receive focus.
### Color Contrast
- **Gray variant**: Meets contrast requirements on dark backgrounds; may need weight adjustment on light backgrounds
- **Base variant**: High contrast in both themes (adapts to theme - white in dark, black in light)
- **Green variant**: Brand color provides good contrast in both themes
## Best Practices
1. **Use consistent weights** - Stick to one weight within the same context (e.g., all list dividers should be the same weight)
2. **Match hierarchy to importance** - Use thinner dividers for minor separations, stronger for major breaks
3. **Don't overuse dividers** - If every element has a divider, none stand out; use sparingly for maximum effect
4. **Consider spacing first** - Before adding a divider, try adjusting margins or padding
5. **Maintain alignment** - Dividers should align with content; avoid full-width dividers in padded containers
6. **Use color purposefully** - Reserve green for branded emphasis; gray for most cases; base for high contrast needs
7. **Test in both themes** - Verify dividers are visible and appropriate in both light and dark modes
8. **Parent container setup** - For vertical dividers, ensure the parent has `display: flex`, `align-items: stretch`, and a defined height
## Theme Support
The component automatically adapts colors for light and dark modes:
| Color | Dark Mode (Default) | Light Mode |
|-------|---------------------|------------|
| Gray | `$gray-600` (#454549) | `$gray-300` (#C1C1C2) |
| Base | `$white` (#FFFFFF) | `$gray-900` (#111112) |
| Green | `$green-300` (#21E46B) | `$green-300` (#21E46B) |
## Related Components
- **Card** - Often uses horizontal dividers between sections
- **List** - May use dividers between list items
- **Navigation** - Vertical dividers separate nav groups
- **Form** - Dividers separate form sections
## Showcase
See the interactive showcase at `/about/divider-showcase` for live examples of all variants, weights, colors, and real-world usage patterns.
## File Structure
```
shared/components/Divider/
├── Divider.tsx # Component implementation
├── Divider.scss # Component styles
├── Divider.md # This documentation
└── index.ts # Exports
```

View File

@@ -0,0 +1,173 @@
// BDS Divider Component Styles
// Brand Design System - Visual separator component
//
// Naming Convention: BEM with 'bds' namespace
// .bds-divider - Base divider (removes default hr styling)
// .bds-divider--horizontal - Horizontal orientation (default)
// .bds-divider--vertical - Vertical orientation
// .bds-divider--thin - Thin stroke weight (0.5px)
// .bds-divider--regular - Regular stroke weight (1px, default)
// .bds-divider--strong - Strong stroke weight (2px)
// .bds-divider--gray - Gray color variant (default)
// .bds-divider--base - Base color variant (high contrast, adapts to theme)
// .bds-divider--green - Green color variant
//
// 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
// =============================================================================
// Stroke Weights (from Figma design spec)
$bds-divider-thin: 0.5px;
$bds-divider-regular: 1px;
$bds-divider-strong: 2px;
// Colors - Dark Mode (default, mapped from _colors.scss)
// Site defaults to dark mode, uses html.light for light mode
$bds-divider-gray-dark: $gray-600; // #454549 - visible on dark backgrounds
$bds-divider-base-dark: $white; // #FFFFFF - high contrast for dark mode
$bds-divider-green-dark: $green-300; // #21E46B - brand color stays same
// Colors - Light Mode (mapped from _colors.scss)
// Figma Neutral300 (#CAD4DF) → closest match: $gray-300
// Figma Black (#141414) → closest match: $gray-900
// Figma Green 300 (#21E46B) → exact match: $green-300
$bds-divider-gray-light: $gray-300; // #C1C1C2
$bds-divider-base-light: $gray-900; // #111112 - high contrast for light mode
$bds-divider-green-light: $green-300; // #21E46B
// =============================================================================
// Base Divider Styles
// =============================================================================
// Use more specific selector to override Bootstrap hr styles
// hr.bds-divider has higher specificity than just hr
hr.bds-divider,
.bds-divider {
// Reset default <hr> styles and override Bootstrap/global hr styles
margin: 0;
padding: 0;
border: none;
border-top: none;
border-bottom: none;
border-left: none;
border-right: none;
background: transparent;
opacity: 1; // Override Bootstrap's hr opacity: 0.25
color: transparent; // Override inherited color
// Default to gray color (dark mode default)
background-color: $bds-divider-gray-dark;
}
// =============================================================================
// Orientation Modifiers
// =============================================================================
// Horizontal divider - spans full width, uses height for stroke
.bds-divider--horizontal {
display: block;
width: 100%;
min-width: 100%;
// Height set by weight modifier
}
// Vertical divider - spans full height, uses width for stroke
.bds-divider--vertical {
display: block;
height: auto;
min-height: 1px; // Fallback minimum
align-self: stretch; // Stretch to fill flex container height
flex-shrink: 0; // Prevent collapsing in flex layouts
// Width set by weight modifier
}
// =============================================================================
// Weight Modifiers
// =============================================================================
// Thin weight (0.5px)
.bds-divider--thin {
&.bds-divider--horizontal {
height: $bds-divider-thin;
}
&.bds-divider--vertical {
width: $bds-divider-thin;
}
}
// Regular weight (1px) - default
.bds-divider--regular {
&.bds-divider--horizontal {
height: $bds-divider-regular;
}
&.bds-divider--vertical {
width: $bds-divider-regular;
}
}
// Strong weight (2px)
.bds-divider--strong {
&.bds-divider--horizontal {
height: $bds-divider-strong;
}
&.bds-divider--vertical {
width: $bds-divider-strong;
}
}
// =============================================================================
// Color Modifiers - Dark Mode (Default)
// =============================================================================
// Site defaults to dark mode, so base styles are for dark backgrounds
// Use hr.bds-divider--* for higher specificity to override base hr.bds-divider
// Gray variant (default) - subtle, neutral separation
hr.bds-divider--gray,
.bds-divider--gray {
background-color: $bds-divider-gray-dark;
}
// Base variant - high contrast, adapts to theme (white in dark mode, black in light mode)
hr.bds-divider--base,
.bds-divider--base {
background-color: $bds-divider-base-dark;
}
// Green variant - branded, accent separation
hr.bds-divider--green,
.bds-divider--green {
background-color: $bds-divider-green-dark;
}
// =============================================================================
// Light Mode Styles
// =============================================================================
// Site uses html.light class for light mode
html.light {
// Gray variant in light mode
hr.bds-divider--gray,
.bds-divider--gray {
background-color: $bds-divider-gray-light;
}
// Base variant in light mode - use dark color for contrast
hr.bds-divider--base,
.bds-divider--base {
background-color: $bds-divider-base-light;
}
// Green variant stays the same in light mode (brand color)
hr.bds-divider--green,
.bds-divider--green {
background-color: $bds-divider-green-light;
}
}

View File

@@ -0,0 +1,62 @@
import React from 'react';
import clsx from 'clsx';
export interface DividerProps {
/** Divider orientation - horizontal separates vertical content, vertical separates horizontal content */
orientation?: 'horizontal' | 'vertical';
/** Stroke weight - controls visual thickness */
weight?: 'thin' | 'regular' | 'strong';
/** Color variant - gray (default), base for high contrast (adapts to theme), green for brand emphasis */
color?: 'gray' | 'base' | 'green';
/** Additional CSS classes */
className?: string;
/** Whether the divider is purely decorative (hides from screen readers) */
decorative?: boolean;
}
/**
* BDS Divider Component
*
* A visual separator component following the XRPL Brand Design System.
* Provides clear visual separation between sections, elements, or content groups.
*
* @example
* // Horizontal divider (default)
* <Divider />
*
* // Vertical divider between columns
* <Divider orientation="vertical" />
*
* // Strong green divider for emphasis
* <Divider weight="strong" color="green" />
*
* // Thin base divider (high contrast - adapts to theme)
* <Divider weight="thin" color="base" />
*/
export const Divider: React.FC<DividerProps> = ({
orientation = 'horizontal',
weight = 'regular',
color = 'gray',
className = '',
decorative = true,
}) => {
// Build class names using BEM with bds namespace
const classNames = clsx(
'bds-divider',
`bds-divider--${orientation}`,
`bds-divider--${weight}`,
`bds-divider--${color}`,
className
);
return (
<hr
className={classNames}
aria-hidden={decorative}
role={decorative ? 'presentation' : 'separator'}
aria-orientation={!decorative ? orientation : undefined}
/>
);
};
export default Divider;

View File

@@ -0,0 +1,2 @@
export { Divider, type DividerProps } from './Divider';
export { default } from './Divider';

View File

@@ -0,0 +1,395 @@
# BdsLink Component
A comprehensive, accessible link component from the XRPL.org Brand Design System (BDS). Supports multiple variants, sizes, and automatic theme-aware color states with animated arrow icons.
## Table of Contents
- [Installation](#installation)
- [Basic Usage](#basic-usage)
- [Props API](#props-api)
- [Variants](#variants)
- [Sizes](#sizes)
- [Color States](#color-states)
- [Icon Animations](#icon-animations)
- [Accessibility](#accessibility)
- [Best Practices](#best-practices)
- [Examples](#examples)
- [Related Components](#related-components)
---
## Installation
```tsx
import { BdsLink } from 'shared/components/Link';
// or
import { BdsLink } from 'shared/components/Link/Link';
```
---
## Basic Usage
```tsx
// Internal link (default)
<BdsLink href="/docs">View Documentation</BdsLink>
// External link
<BdsLink href="https://example.com" variant="external" target="_blank" rel="noopener noreferrer">
External Resource
</BdsLink>
// Inline link (no icon)
<p>
Learn more about <BdsLink href="/about" variant="inline">our mission</BdsLink>.
</p>
```
---
## Props API
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `href` | `string` | **required** | The URL the link points to |
| `variant` | `'internal' \| 'external' \| 'inline'` | `'internal'` | Link variant determining icon and behavior |
| `size` | `'small' \| 'medium' \| 'large'` | `'medium'` | Size of the link text and icon |
| `icon` | `'arrow' \| 'external' \| null` | `null` | Override icon type (auto-determined by variant if `null`) |
| `disabled` | `boolean` | `false` | Disables the link, preventing navigation |
| `children` | `React.ReactNode` | **required** | Link text content |
| `className` | `string` | - | Additional CSS classes |
| `...rest` | `AnchorHTMLAttributes` | - | All standard anchor attributes (`target`, `rel`, etc.) |
---
## Variants
### Internal (Default)
For navigation within the same website. Displays a horizontal arrow (→) that animates to a chevron (>) on hover.
```tsx
<BdsLink href="/docs" variant="internal">
Internal Documentation
</BdsLink>
```
### External
For links to external websites. Displays a diagonal arrow with corner bracket (↗) that animates on hover. Always use with `target="_blank"` and `rel="noopener noreferrer"` for security.
```tsx
<BdsLink
href="https://github.com/XRPLF"
variant="external"
target="_blank"
rel="noopener noreferrer"
>
GitHub Repository
</BdsLink>
```
### Inline
For links embedded within body text. No icon is displayed, making the link flow naturally within paragraphs.
```tsx
<p>
The XRP Ledger is a decentralized blockchain. You can{" "}
<BdsLink href="/docs" variant="inline">read the documentation</BdsLink>{" "}
to learn more.
</p>
```
---
## Sizes
| Size | Font Size | Line Height | Icon Gap |
|------|-----------|-------------|----------|
| `small` | 14px | 1.5 | 6px |
| `medium` | 16px | 1.5 | 8px |
| `large` | 20px | 1.5 | 10px |
```tsx
<BdsLink href="/docs" size="small">Small Link</BdsLink>
<BdsLink href="/docs" size="medium">Medium Link</BdsLink>
<BdsLink href="/docs" size="large">Large Link</BdsLink>
```
---
## Color States
The Link component automatically handles color states based on the current theme. Colors are applied via CSS and follow the Figma design specifications.
### Light Mode
| State | Color Token | Hex Value | Additional Styles |
|-------|-------------|-----------|-------------------|
| **Enabled** | Green 400 | `#0DAA3E` | No underline |
| **Hover** | Green 500 | `#078139` | Underline, arrow animates |
| **Focus** | Green 500 | `#078139` | Underline, black outline |
| **Active** | Green 400 | `#0DAA3E` | Underline |
| **Visited** | Lilac 400 | `#7649E3` | No underline |
| **Disabled** | Gray 400 | `#A2A2A4` | No underline, no pointer |
### Dark Mode
| State | Color Token | Hex Value | Additional Styles |
|-------|-------------|-----------|-------------------|
| **Enabled** | Green 300 | `#21E46B` | No underline |
| **Hover** | Green 200 | `#70EE97` | Underline, arrow animates |
| **Focus** | Green 200 | `#70EE97` | Underline, white outline |
| **Active** | Green 300 | `#21E46B` | Underline |
| **Visited** | Lilac 300 | `#C0A7FF` | No underline |
| **Disabled** | Gray 500 | `#838386` | No underline, no pointer |
### Focus Outline
- **Light Mode**: 2px solid black (`#000000`)
- **Dark Mode**: 2px solid white (`#FFFFFF`)
---
## Icon Animations
Both internal and external arrow icons feature a smooth animation on hover/focus:
- **Animation Duration**: 150ms
- **Timing Function**: `cubic-bezier(0.98, 0.12, 0.12, 0.98)`
### Internal Arrow Animation
The horizontal line of the arrow (→) shrinks from left to right, revealing a chevron shape (>).
### External Arrow Animation
The diagonal line of the external arrow (↗) scales down toward the top-right corner, leaving just the corner bracket.
### Disabled State
When disabled, the animation is disabled and the icon opacity is reduced to 50%.
---
## Accessibility
The Link component follows accessibility best practices:
### Keyboard Navigation
- Focusable via Tab key
- Activatable via Enter key
- Clear focus indicator with high-contrast outline
### ARIA Attributes
- `aria-disabled="true"` is applied when the link is disabled
- Icons are marked with `aria-hidden="true"` to prevent screen reader announcement
### Best Practices
1. **Use descriptive link text** - Avoid "click here" or "read more"
2. **External links** - Consider adding "(opens in new tab)" for screen readers
3. **Disabled state** - Provide context for why the link is disabled
```tsx
// Good - Descriptive link text
<BdsLink href="/pricing">View pricing plans</BdsLink>
// Bad - Non-descriptive
<BdsLink href="/pricing">Click here</BdsLink>
// External with screen reader context
<BdsLink
href="https://example.com"
variant="external"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub Repository (opens in new tab)"
>
GitHub Repository
</BdsLink>
```
---
## Best Practices
### Do's
1. **Use appropriate variants**
```tsx
// Internal navigation
<BdsLink href="/about" variant="internal">About Us</BdsLink>
// External sites
<BdsLink href="https://github.com" variant="external" target="_blank" rel="noopener noreferrer">
GitHub
</BdsLink>
// Within paragraphs
<p>Learn about <BdsLink href="/xrp" variant="inline">XRP</BdsLink> today.</p>
```
2. **Match size to context**
```tsx
// Navigation/CTA - use large
<BdsLink href="/get-started" size="large">Get Started</BdsLink>
// Body content - use medium
<BdsLink href="/docs" size="medium">Documentation</BdsLink>
// Footnotes/captions - use small
<BdsLink href="/terms" size="small">Terms of Service</BdsLink>
```
3. **Always use security attributes for external links**
```tsx
<BdsLink
href="https://external-site.com"
variant="external"
target="_blank"
rel="noopener noreferrer"
>
External Site
</BdsLink>
```
### Don'ts
1. **Don't use disabled for navigation prevention** - Use proper routing instead
```tsx
// Bad - Using disabled for auth gate
<BdsLink href="/dashboard" disabled={!isAuthenticated}>Dashboard</BdsLink>
// Good - Handle in onClick or router
<BdsLink href="/dashboard" onClick={handleAuthCheck}>Dashboard</BdsLink>
```
2. **Don't mix variants inappropriately**
```tsx
// Bad - External link with internal variant
<BdsLink href="https://example.com" variant="internal">External Site</BdsLink>
// Good
<BdsLink href="https://example.com" variant="external">External Site</BdsLink>
```
3. **Don't use inline variant for standalone links**
```tsx
// Bad - Standalone inline link
<BdsLink href="/docs" variant="inline">Documentation</BdsLink>
// Good - Use internal for standalone
<BdsLink href="/docs" variant="internal">Documentation</BdsLink>
```
---
## Examples
### Navigation Menu
```tsx
<nav className="d-flex flex-column gap-3">
<BdsLink href="/docs" size="medium">Documentation</BdsLink>
<BdsLink href="/tutorials" size="medium">Tutorials</BdsLink>
<BdsLink href="/api" size="medium">API Reference</BdsLink>
<BdsLink
href="https://github.com/XRPLF"
variant="external"
size="medium"
target="_blank"
rel="noopener noreferrer"
>
GitHub
</BdsLink>
</nav>
```
### Call-to-Action Section
```tsx
<div className="d-flex flex-column gap-4">
<BdsLink href="/get-started" size="large">
Get Started with XRPL
</BdsLink>
<BdsLink href="/use-cases" size="large">
Explore Use Cases
</BdsLink>
</div>
```
### Rich Text Content
```tsx
<article>
<p>
The XRP Ledger (XRPL) is a decentralized, public blockchain led by a{" "}
<BdsLink href="/community" variant="inline">global community</BdsLink>{" "}
of businesses and developers. It supports a wide variety of{" "}
<BdsLink href="/use-cases" variant="inline">use cases</BdsLink>{" "}
including payments, tokenization, and DeFi.
</p>
<p>
To learn more, check out the{" "}
<BdsLink href="/docs" variant="inline">official documentation</BdsLink>{" "}
or visit the{" "}
<BdsLink
href="https://github.com/XRPLF"
variant="inline"
target="_blank"
rel="noopener noreferrer"
>
GitHub repository
</BdsLink>.
</p>
</article>
```
### Disabled State
```tsx
<div className="d-flex flex-column gap-3">
<BdsLink href="/premium" size="medium" disabled>
Premium Features (Coming Soon)
</BdsLink>
<span className="text-muted">This feature is not yet available.</span>
</div>
```
---
## Related Components
- **LinkArrow** - The animated arrow icon component used internally by Link
- **Button** - For actions that don't navigate (forms, modals, etc.)
---
## File Structure
```
shared/components/Link/
├── index.ts # Exports
├── Link.tsx # Main component
├── Link.md # This documentation
├── LinkArrow.tsx # Arrow icon component
├── _link.scss # Link styles
└── _link-icons.scss # Arrow icon styles & animations
```
---
## Changelog
### v1.0.0
- Initial release with internal, external, and inline variants
- Three size options (small, medium, large)
- Theme-aware color states (light/dark mode)
- Animated arrow icons
- Full accessibility support

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