Files
xrpl-dev-portal/@theme/components/Navbar/Navbar.tsx
Calvin b0db5b3a5c Perf pass + migrate home/payments page sections to shared (#3771)
* Perf pass + migrate home/payments page sections to shared

Performance work across every page plus a refactor moving home/payments
page sections into reusable shared components.

JavaScript
- Defer xrpl.js, jQuery, and Bootstrap in the global <head> (redocly.yaml)
  and on the 11 interactive tool pages' inline <script> tags — removes
  ~1MB of render-blocking JS from first paint on every page.
- Drop moment/moment-timezone from the client: remove the dead APEX-2025
  countdown in the navbar AlertBanner; format/parse dates with native
  Date/Intl in the blog and events pages.

Fonts & icons
- Remove render-blocking font-awesome.min.css + webfont. CSS glyphs
  (callouts, breadcrumbs, status labels, tutorials) now render as
  self-contained data-URI SVG masks (styles/_fa-icons.scss); markup icons
  use a shared inline-SVG <Icon> component (shared/components/Icon).
- Preconnect to Google Fonts / osano / GTM origins; preload the primary
  Booton UI font so text no longer waits on the 775KB stylesheet to parse.

Images & rendering
- Re-encode/resize oversized assets: new-york.jpeg 3.86MB→60KB,
  resources-icon.svg 4.2MB→150KB, event date/location SVGs ~1MB→0.5MB.
- Add loading=lazy/decoding=async to shared card components and the
  Video embed iframe.
- Add content-visibility to the events page's ~126-card grid to skip
  off-screen rendering.
- Earlier QA fixes: light-mode link color (#141414), events card grid
  layout, close nav menu on client-side navigation.

Home / payments refactor
- Move home page sections from page-local pages/home/sections/* to reusable
  shared/sections/* components (HomeHero relocated to shared/sections/HomeHero;
  home page now composes CalloutMediaBanner, CardStatsList, LinkTextDirectory,
  LogoSquareGrid, StandardCardGroupSection, FeatureSingleTopic, CarouselFeatured).
- Remove the now-unused pages/payments/sections/* in the same migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Revert FontAwesome removal (keep it for open-source flexibility)

Restore the FontAwesome webfont + font-awesome.min.css and all FA icon
usages (markup fa-* classes and CSS ::before glyphs), reverting the
consolidation in d4cf4e8581. Other performance changes from that commit
(deferred head scripts, moment removal, image optimization, lazy-loading,
content-visibility, preconnect/preload) are retained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* cleaning up breadcrumb styling

* sticky side nav

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 14:03:02 -07:00

243 lines
9.4 KiB
TypeScript

import * as React from "react";
import { useLocation } from "react-router-dom";
import { useSearchDialog } from "@redocly/theme/core/hooks";
import { SearchDialog } from "@redocly/theme/components/Search/SearchDialog";
// 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";
// Re-export AlertBanner for backwards compatibility
export { AlertBanner } from "./components/AlertBanner";
// Props interface for Navbar (extensible for future use)
interface NavbarProps extends React.HTMLAttributes<HTMLElement> {}
/**
* 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({
className,
onMouseLeave,
...headerProps
}: 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);
// Mirror `activeSubmenu` in a ref so the (memoized) closeSubmenu callback
// can read the latest value without going stale and without triggering
// side-effects from inside a setState updater (which React may replay).
const activeSubmenuRef = React.useRef<string | null>(null);
React.useEffect(() => {
activeSubmenuRef.current = activeSubmenu;
}, [activeSubmenu]);
// Use Redocly's search dialog hook - shared across navbar and mobile menu
const { isOpen: isSearchOpen, onOpen: onSearchOpen, onClose: onSearchClose } = useSearchDialog();
// The Navbar persists across client-side (react-router) navigations, so any
// open menu would otherwise stay open after clicking a nav link. Close the
// mobile menu and desktop submenus whenever the route changes so the nav
// hides on each new page load.
const { pathname } = useLocation();
const isInitialRender = React.useRef(true);
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 closeSubmenu = React.useCallback(() => {
if (submenuTimeoutRef.current) {
clearTimeout(submenuTimeoutRef.current);
submenuTimeoutRef.current = null;
}
if (closingTimeoutRef.current) {
clearTimeout(closingTimeoutRef.current);
closingTimeoutRef.current = null;
}
// Snapshot the previously-active submenu so we can trigger its fade-out
// animation as a separate state update — keeps the setState updaters pure.
const previouslyActive = activeSubmenuRef.current;
setActiveSubmenu(null);
if (previouslyActive !== null) {
setClosingSubmenu(previouslyActive);
}
closingTimeoutRef.current = setTimeout(() => {
setClosingSubmenu(null);
closingTimeoutRef.current = null;
}, 350);
}, []);
const handleSubmenuMouseLeave = () => {
if (submenuTimeoutRef.current) {
clearTimeout(submenuTimeoutRef.current);
submenuTimeoutRef.current = null;
}
submenuTimeoutRef.current = setTimeout(() => {
submenuTimeoutRef.current = null;
closeSubmenu();
}, 150);
};
const handleSubmenuClose = () => {
closeSubmenu();
};
// Handle scroll lock + inert page content while a submenu is open.
// `inert` prevents Tab from reaching links/buttons in the page behind the
// open submenu. Scoped to the page wrappers + footer so the navbar itself
// stays interactive. Cleared when the submenu fully closes (no closingSubmenu).
React.useEffect(() => {
const isSubmenuVisible = Boolean(activeSubmenu || closingSubmenu);
const isFullyOpen = Boolean(activeSubmenu); // only inert while truly open, not during closing animation
if (isSubmenuVisible) {
document.body.classList.add('bds-submenu-open');
} else {
document.body.classList.remove('bds-submenu-open');
}
const inertTargets = document.querySelectorAll<HTMLElement>(
'main, .bds-page-wrapper, footer'
);
inertTargets.forEach((el) => {
if (isFullyOpen) {
el.setAttribute('inert', '');
} else {
el.removeAttribute('inert');
}
});
return () => {
document.body.classList.remove('bds-submenu-open');
document
.querySelectorAll<HTMLElement>('main, .bds-page-wrapper, footer')
.forEach((el) => el.removeAttribute('inert'));
};
}, [activeSubmenu, closingSubmenu]);
React.useEffect(() => {
return () => {
if (submenuTimeoutRef.current) {
clearTimeout(submenuTimeoutRef.current);
}
if (closingTimeoutRef.current) {
clearTimeout(closingTimeoutRef.current);
}
};
}, []);
// Close all menus on route change (client-side navigation). Skip the initial
// render — nothing is open yet — and clear pending submenu timers so no
// fade-out animation lingers onto the newly loaded page.
React.useEffect(() => {
if (isInitialRender.current) {
isInitialRender.current = false;
return;
}
if (submenuTimeoutRef.current) {
clearTimeout(submenuTimeoutRef.current);
submenuTimeoutRef.current = null;
}
if (closingTimeoutRef.current) {
clearTimeout(closingTimeoutRef.current);
closingTimeoutRef.current = null;
}
setMobileMenuOpen(false);
setActiveSubmenu(null);
setClosingSubmenu(null);
}, [pathname]);
const navbarClasses = [
"bds-navbar",
alertBanner.show ? "bds-navbar--with-banner" : "",
className ?? ""
].filter(Boolean).join(" ");
return (
<>
{/*
Preload Booton Regular — the navbar font, used above the fold on every
page. Without this the browser can't fetch the font until it has
downloaded + parsed the ~775KB devportal2024-v1.css (which declares the
`font-family: Booton` usage), so the custom font takes ~1s to swap in.
This starts the fetch immediately, in parallel with the CSS. The href
is literal (not bundler-resolved) so it matches the `url("../font/...")`
the stylesheet resolves to (/font/Booton-Regular.woff2); `crossOrigin`
is required because fonts are always fetched in CORS mode.
Only Regular (400) is preloaded globally: docs pages (the bulk of the
site) render body/headings in system fonts and only use Booton in the
navbar, so preloading heavier weights here would waste bandwidth on
those pages. Landing-page hero weights (Booton Light 300 / Bold 700,
Tobias) should be preloaded per-landing-page instead.
*/}
<link rel="preload" href="/font/Booton-Regular.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />
<AlertBanner {...alertBanner} />
{/* Backdrop blur overlay when submenu is open or closing */}
<div
className={`bds-submenu-backdrop ${activeSubmenu || closingSubmenu ? 'bds-submenu-backdrop--active' : ''}`}
onClick={closeSubmenu}
/>
<header
{...headerProps}
className={navbarClasses}
onMouseLeave={(event) => {
onMouseLeave?.(event);
handleSubmenuMouseLeave();
}}
>
<div className="bds-navbar__content">
<NavLogo />
<NavItems activeSubmenu={activeSubmenu} onSubmenuEnter={handleSubmenuMouseEnter} onSubmenuClose={handleSubmenuClose} />
<NavControls onSearch={onSearchOpen} />
<HamburgerButton onClick={handleHamburgerClick} isOpen={mobileMenuOpen} />
</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} />}
</>
);
}