diff --git a/@theme/components/Navbar/Navbar.tsx b/@theme/components/Navbar/Navbar.tsx index 5425919e50..7d9c7032b5 100644 --- a/@theme/components/Navbar/Navbar.tsx +++ b/@theme/components/Navbar/Navbar.tsx @@ -2,16 +2,27 @@ import 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"; -// @ts-ignore +// Icons +import xrpSymbolBlack from "../../../static/img/navbar/xrp-symbol-black.svg"; +import xrpLogotypeBlack from "../../../static/img/navbar/xrp-logotype-black.svg"; +import searchIcon from "../../../static/img/navbar/search-icon.svg"; +import modeToggleIcon from "../../../static/img/navbar/mode-toggle.svg"; +import globeIcon from "../../../static/img/navbar/globe-icon.svg"; +import chevronDown from "../../../static/img/navbar/chevron-down.svg"; +import hamburgerIcon from "../../../static/img/navbar/hamburger-icon.svg"; +import arrowUpRight from "../../../static/img/icons/arrow-up-right-custom.svg"; +// Wallet icons for submenu +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"; + +// Alert Banner Configuration const alertBanner = { show: false, message: "APEX 2025", @@ -19,26 +30,117 @@ const alertBanner = { 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" }; +// Nav items with submenu support +const navItems = [ + { label: "Develop", labelTranslationKey: "navbar.develop", href: "/docs", hasSubmenu: true }, + { label: "Use Cases", labelTranslationKey: "navbar.usecases", href: "/about/uses", hasSubmenu: false }, + { label: "Community", labelTranslationKey: "navbar.community", href: "/community", hasSubmenu: false }, + { label: "Network", labelTranslationKey: "navbar.network", href: "/docs/concepts/networks-and-servers", hasSubmenu: false }, +]; + +// Wallet icon mapping +const walletIcons: Record = { + green: greenWallet, + lilac: lilacWallet, + yellow: yellowWallet, + pink: pinkWallet, + blue: blueWallet, +}; + +// Types for submenu data +interface SubmenuChild { + label: string; + href: string; + active?: boolean; +} + +interface SubmenuItemBase { + label: string; + href: string; + icon: string; +} + +interface SubmenuItemWithChildren extends SubmenuItemBase { + children: SubmenuChild[]; +} + +type SubmenuItem = SubmenuItemBase | SubmenuItemWithChildren; + +// Develop submenu data structure +const developSubmenuData: { + left: SubmenuItemBase[]; + right: SubmenuItemWithChildren[]; +} = { + left: [ + { label: "Developer's Home", href: "/docs", icon: "green" }, + { label: "Learn", href: "/docs/tutorials", icon: "lilac" }, + { label: "Code Samples", href: "/_code-samples", icon: "yellow" }, + ], + right: [ + { + label: "Docs", + href: "/docs", + icon: "pink", + children: [ + { label: "API Reference", href: "/docs/references" }, + { label: "Tutorials", href: "/docs/tutorials", active: true }, + { label: "Concepts", href: "/docs/concepts" }, + { label: "Infrastructure", href: "/docs/infrastructure" }, + ], + }, + { + label: "Client Libraries", + href: "/docs/references/client-libraries", + icon: "blue", + children: [ + { label: "JavaScript", href: "/docs/references/xrpljs" }, + { label: "Python", href: "/docs/references/xrpl-py" }, + { label: "PHP", href: "/docs/references/xrpl-php" }, + { label: "Go", href: "/docs/references/xrpl-go" }, + ], + }, + ], +}; + +// Arrow Icon Component for submenu links +function SubmenuArrow({ className, color = "currentColor" }: { className?: string; color?: string }) { + return ( + + + + ); +} + +// Alert Banner Component export function AlertBanner({ message, button, link, show }) { const { useTranslate } = useThemeHooks(); const { translate } = useTranslate(); - const bannerRef = React.useRef(null); + 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"; } @@ -46,419 +148,619 @@ export function AlertBanner({ message, button, link, show }) { 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 ( - -
-
{translate(message)}
-
{displayDate}
-
-
-
{translate(button)}
- Get Tickets Icon -
-
- ); - } - return null; -} -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 = ""; - - const navItems = menu.map((item, index) => { - if (item.type === "group") { - return ( - - ); - } else { - return ( - - - {item.label} - - - ); - } - }); - - React.useEffect(() => { - // Bootstrap 5 uses vanilla JavaScript API instead of jQuery - // These events set classes so that the search bar and other - // submenus collapse on mobile when you expand one submenu. - const dropdowns = document.querySelectorAll("#topnav-pages .dropdown"); - const top_main_nav = document.querySelector("#top-main-nav"); - - const handleDropdownShow = () => { - top_main_nav?.classList.add("submenu-expanded"); - }; - - const handleDropdownHidden = () => { - top_main_nav?.classList.remove("submenu-expanded"); - }; - - // Attach Bootstrap 5 dropdown events - dropdowns.forEach((dropdown) => { - dropdown.addEventListener("show.bs.dropdown", handleDropdownShow); - dropdown.addEventListener("hidden.bs.dropdown", handleDropdownHidden); - }); - - // 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 - } - }; - - const dropdownItems = document.querySelectorAll(".dropdown-item"); - dropdownItems.forEach((item) => { - item.addEventListener("click", toggleNavbar); - }); - - // Cleanup function to remove event listeners - return () => { - dropdowns.forEach((dropdown) => { - dropdown.removeEventListener("show.bs.dropdown", handleDropdownShow); - dropdown.removeEventListener("hidden.bs.dropdown", handleDropdownHidden); - }); - dropdownItems.forEach((item) => { - item.removeEventListener("click", toggleNavbar); - }); - }; - }, []); + if (!show) return null; return ( - <> - - - - - - - - - {navItems} - -
- -
-
- -
-
-
-
- + +
+
{translate(message)}
+
{displayDate}
+
+
+
{translate(button)}
+ Get Tickets Icon +
+
); } -export function TopNavCollapsible({ children }) { +// Logo Component - Shows symbol on desktop/mobile, full logotype on tablet +function NavLogo() { return ( -
- {children} + + XRP Ledger + XRP Ledger + + ); +} + +// Desktop Develop Submenu Component +function DevelopSubmenu({ isActive }: { isActive: boolean }) { + return ( +
+ {/* Left Column */} +
+ {developSubmenuData.left.map((item) => ( +
+
+ + + + + {item.label} + + + + +
+
+ ))} +
+ + {/* Right Column */} +
+ {developSubmenuData.right.map((section) => ( +
+
+ + + + + {section.label} + + + + +
+ {section.children && ( +
+ {section.children.map((child) => ( + + {child.label} + {child.active && ( + + + + )} + + ))} +
+ )} +
+ ))} +
); } -export function NavDropdown(props) { - const { label, items, pathPrefix, labelTranslationKey } = props; +// Nav Items Component - Centered navigation links with submenu support +function NavItems({ submenuActive, onSubmenuEnter }: { + submenuActive: boolean; + onSubmenuEnter: () => void; +}) { const { useTranslate } = useThemeHooks(); const { translate } = useTranslate(); + const [activeItem, setActiveItem] = React.useState(null); - 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 ( - - {translate(item2.labelTranslationKey, item2.label)} - - ); - } - return ( - - {translate(item2.labelTranslationKey, item2.label)} - - ); - }); - - const clnm = "navcol col-for-" + slugify(item.label); - - return ( -
-
- {translate(item.labelTranslationKey, item.label)} -
- {groupLinks} -
- ); - } 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 ( - - {img_alt} -
-

{newlabel}

-

{description}

-
- - ); - } 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 ( - - {translate(item.labelTranslationKey, item.label)} - - ); + const handleMouseEnter = (itemLabel: string, hasSubmenu: boolean) => { + setActiveItem(itemLabel); + if (hasSubmenu) { + onSubmenuEnter(); } - }); + }; - const toggler_id = "topnav_" + slugify(label); - const dd_id = "topnav_dd_" + slugify(label); + const handleMouseLeave = (hasSubmenu: boolean) => { + if (!hasSubmenu) { + setActiveItem(null); + } + // Don't close submenu on leave - let the parent Navbar handle that + }; + + // Sync activeItem with submenuActive state + React.useEffect(() => { + if (!submenuActive) { + setActiveItem(null); + } + }, [submenuActive]); return ( -
  • - -
    - {dropdownGroups} -
    -
  • - ); -} - -export function NavWrapper(props) { - return ( -