diff --git a/docs/docs/reference/cli/_category_.yml b/docs/docs/reference/cli/_category_.yml index 184af4a2efd..392f4efc2e7 100644 --- a/docs/docs/reference/cli/_category_.yml +++ b/docs/docs/reference/cli/_category_.yml @@ -1,4 +1,4 @@ position: 4 label: CLI Reference -collapsible: true -collapsed: true \ No newline at end of file +collapsible: false +collapsed: false \ No newline at end of file diff --git a/docs/docs/reference/project-files/_category_.yml b/docs/docs/reference/project-files/_category_.yml index 3a6d2a3edd2..846359dbac7 100644 --- a/docs/docs/reference/project-files/_category_.yml +++ b/docs/docs/reference/project-files/_category_.yml @@ -1,4 +1,4 @@ position: 00 label: Project Files -collapsible: true +collapsible: false collapsed: false diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 1e7a88bf22a..4c8cfcd78c8 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -130,29 +130,40 @@ const config = { to: "/", label: "Docs", position: "left", - className: "navbar-docs-link", activeBaseRegex: "^(?!/(reference|api|contact|notes)).*", // Keep Docs active for all doc pages }, { - to: "/reference/project-files", + type: "dropdown", label: "Reference", position: "left", - className: "navbar-reference-link", - activeBasePath: "/reference", - }, - { - to: "/api/admin/", - label: "API", - position: "left", - className: "navbar-api-link", - activeBasePath: "/api/admin", + to: "/reference/project-files", + className: 'my-custom-dropdown', + activeBaseRegex: "^(/reference|/api/admin)", + items: [ + { + to: "/reference/project-files", + label: "Project Files", + }, + { + to: "/reference/cli", + label: "CLI", + }, + { + to: "/reference/rill-iso-extensions", + label: "Rill ISO 8601", + }, + { + to: "/api/admin/", + label: "REST API", + }, + + ], }, { to: "/contact", label: "Contact Us", position: "left", - className: "navbar-contact-link", - activeBasePath: "/contact", + activeBaseRegex: "^/contact", }, diff --git a/docs/src/css/_navbar.scss b/docs/src/css/_navbar.scss index 1d4f1c5dfbd..e06a1442d24 100644 --- a/docs/src/css/_navbar.scss +++ b/docs/src/css/_navbar.scss @@ -1,265 +1,370 @@ - /* ========================= - Search Bar Custom Styles + Navbar Styles ========================= */ -.navbar { - height: 60px; - padding: 0 16px; -} -// all of the navbar items -.navbar__link { - height: 70%; - display: flex; - align-items: center; - justify-content: center; - padding: 0px 12px; - position: relative; - transition: color 0.2s ease; /* Smooth text color transition */ -} - -// on hover of navbar items -.navbar__link:hover { - background: var(--palette_slate_50); - border-radius: 8px; - padding: 12px 12px; -} - -// Dark mode hover adjustment -[data-theme='dark'] .navbar__link:hover { - background: rgba(255, 255, 255, 0.1); -} - -// Remove hover background for active items -.navbar__link--active:hover { - background: transparent; -} - -// Keep default color on hover (don't change text color) -.navbar__link.navbar-docs-link:hover, -.navbar__link.navbar-reference-link:hover, -.navbar__link.navbar-contact-link:hover, -.navbar__link.navbar-api-link:hover { - color: var(--ifm-navbar-link-color); -} - -// But if it's active, keep the primary color even when hovering -.navbar__link--active.navbar-docs-link:hover, -.navbar__link--active.navbar-reference-link:hover, -.navbar__link--active.navbar-contact-link:hover, -.navbar__link--active.navbar-api-link:hover { - color: var(--ifm-color-primary); -} - -.navbar button[class*="toggleButton"] { - background: transparent; - border: none; - box-shadow: none; -} - -.navbar button[class*="toggleButton"]:hover { - background: transparent; - box-shadow: none; -} -/* Active state styling for navbar links */ -.navbar__link--active.navbar-docs-link, -.navbar__link--active.navbar-reference-link, -.navbar__link--active.navbar-contact-link, -.navbar__link--active.navbar-api-link, -.navbar__link--active.navbar-icon-link { - color: var(--ifm-color-primary); -} - -/* Underline that only spans text width */ -.navbar__link--active.navbar-docs-link::after, -.navbar__link--active.navbar-reference-link::after, -.navbar__link--active.navbar-contact-link::after, -.navbar__link--active.navbar-api-link::after { - content: ''; - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - height: 2px; - background: var(--ifm-color-primary); - border-radius: 1px 1px 0 0; - width: calc(100% - 24px); +/* Hide caret for custom dropdown links - global rule */ +.navbar .navbar__item.dropdown:has(.navbar__link.my-custom-dropdown) .menu__caret, +.navbar .navbar__item.dropdown:has(.navbar__link.my-custom-dropdown) button.menu__caret, +.navbar .navbar__item.dropdown:has(.navbar__link.my-custom-dropdown) .clean-btn.menu__caret, +.navbar .navbar__item.dropdown:has(.navbar__link.my-custom-dropdown) .menu__caret svg, +.navbar .navbar__item.dropdown:has(.navbar__link.my-custom-dropdown) button[class*="caret"], +.navbar .navbar__item.dropdown:has(.navbar__link.my-custom-dropdown) button[aria-label*="dropdown"], +.navbar .navbar__item.dropdown:has(.navbar__link.my-custom-dropdown) button[aria-label*="Collapse"] { + display: none !important; + visibility: hidden !important; + opacity: 0 !important; + width: 0 !important; + height: 0 !important; } - -// NavBar Icon Link (GitHub and Blog) Change to Icons later -// NavBar Icon Link (GitHub and Blog) Change to Icons later -.navbar-icon-link { - width: 24px; - height: 24px; - margin-right: 6px; - margin-left: 6px; +/* Base navbar link styles - all text black, no decorations */ +.navbar .navbar__link { + color: var(--ifm-color-black); text-decoration: none; - color: var(--ifm-menu-color); - font-size: 0.875rem; font-weight: 500; - transition: color 0.2s; - display: inline-flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; + padding: 0.5rem 0.75rem; + border-radius: 0.375rem; + transition: background-color 0.15s ease; + + &:hover { + background-color: rgba(0, 0, 0, 0.03); + } + + /* Active links don't need hover effect */ + &.navbar__link--active:hover { + background-color: transparent; + } } -.navbar-icon-link:hover { - background: transparent; - box-shadow: none; - text-decoration: none; - color: var(--ifm-menu-color); +/* Active navbar link - theme color */ +.navbar .navbar__link--active, +.navbar__item .navbar__link--active { + color: var(--ifm-color-primary); + text-decoration: underline; + text-decoration-thickness: 2px; + text-underline-offset: 14px; + text-decoration-color: var(--ifm-color-primary); + text-decoration-style: solid; + text-decoration-skip-ink: none; + text-decoration-skip: none; } -// NavBar Search Bar -.navbar .navbar__items--right .DocSearch-Button { - background: transparent; - border: 0.5px solid #e5e7eb; - border-radius: 8px; - padding: 12px 12px; - width: 250px; /* Adjust this value to change width */ - min-width: 150px; /* Minimum width */ - height: 100%; +/* Dropdown items - black text, no decorations */ +/* Target elements with both navbar__item and dropdown classes */ +.navbar .navbar__item.dropdown { + /* Dropdown link - inherits base .navbar__link styles, only adds extra right padding for chevron */ + > .navbar__link { + position: relative; + padding-right: 2rem; + } + /* Hide the default chevron icon for custom dropdown links */ + &:has(.navbar__link.my-custom-dropdown) { + .menu__caret, + button.menu__caret, + .clean-btn.menu__caret, + button[class*="caret"], + button[aria-label*="dropdown"], + button[aria-label*="Collapse"], + .navbar__link.my-custom-dropdown ~ .menu__caret, + .navbar__link.my-custom-dropdown + .menu__caret, + .navbar__link.my-custom-dropdown .menu__caret, + .navbar__link.my-custom-dropdown::after { + display: none !important; + visibility: hidden !important; + opacity: 0 !important; + width: 0 !important; + height: 0 !important; + padding: 0 !important; + margin: 0 !important; + } + } + + /* Rotate custom chevron on hover and when dropdown is open */ + &:has(.navbar__link.my-custom-dropdown) { + .navbar__link.my-custom-dropdown .custom-chevron { + transition: transform 0.2s ease; + } + + &:hover .navbar__link.my-custom-dropdown .custom-chevron, + &.dropdown--show .navbar__link.my-custom-dropdown .custom-chevron, + .navbar__link.my-custom-dropdown[aria-expanded="true"] .custom-chevron { + transform: translateY(0%) rotate(180deg); + } + } + /* Active dropdown - theme color when any sub-page is active */ + /* JavaScript adds 'navbar__dropdown--has-active' class when dropdown menu has active item */ + &.navbar__dropdown--has-active > .navbar__link { + color: var(--ifm-color-primary); + text-decoration: underline; + text-decoration-thickness: 2px; + text-underline-offset: 14px; + text-decoration-color: var(--ifm-color-primary); + text-decoration-style: solid; + text-decoration-skip-ink: none; + text-decoration-skip: none; + } + + /* Extend hover area downward to bridge gap to dropdown menu */ position: relative; - &::after { - content: "⌘K"; + &::before { + content: ''; position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - font-size: 0.75rem; - color: var(--docsearch-muted-color); - font-family: monospace; - pointer-events: none; + bottom: -8px; + left: 0; + right: 0; + height: 8px; + background: transparent; + pointer-events: auto; + z-index: 1000; } } -// Dark mode specific fixes for Search Bar -[data-theme='dark'] .navbar .navbar__items--right .DocSearch-Button { - background: transparent; - border: 0.5px solid var(--docsearch-muted-color); - border-radius: 8px; - padding: 12px 12px; +/* Dropdown menu - add breathing room between trigger and popover */ +.navbar .dropdown__menu { + margin-top: 8px; } -[data-theme='dark'] .navbar .navbar__items--right .DocSearch-Button:hover { - box-shadow: none; - color: var(--docsearch-muted-color); - border: 0.5px solid var(--ifm-color-primary); -} - -// On hover of Search Bar -.navbar .navbar__items--right .DocSearch-Button:hover { - box-shadow: none; - color: var(--docsearch-muted-color); - border: 0.5px solid var(--ifm-color-primary); +/* Temporarily disable dropdown hover when closing - prevents re-opening from hover bridge */ +.navbar.navbar--closing-dropdown { + .navbar__item.dropdown { + /* Disable the hover bridge */ + &::before { + pointer-events: none !important; + } + + /* Hide dropdown menu regardless of hover state */ + .dropdown__menu { + display: none !important; + opacity: 0 !important; + visibility: hidden !important; + pointer-events: none !important; + } + } } -// Search Bar Search Icon -.navbar .navbar__items--right .DocSearch-Search-Icon { - height: 16px; - width: 16px; -} +/* Dropdown menu items - black text with hover effects */ +.navbar .dropdown__link { + color: var(--ifm-color-black); + text-decoration: none; + border-radius: 0.375rem; + padding: 0.375rem 0.75rem; + display: block; + + &:hover, + &:focus { + color: var(--ifm-color-black); + text-decoration: none; + background-color: rgba(0, 0, 0, 0.05); + } + + /* Active dropdown menu items - theme color */ + &.dropdown__link--active { + color: var(--ifm-color-primary); + text-decoration: none; -// Search Bar Search Placeholder -.navbar .navbar__items--right .DocSearch-Button-Placeholder { - font-size: 0.875rem; + } } - -.navbar .navbar__items--right .DocSearch-Button-Keys { - display: none; +/* Custom HTML links (GitHub, Blog) */ +.navbar .navbar-icon-link { + color: var(--ifm-color-black); + text-decoration: none; + + &:hover, + &:focus { + color: var(--ifm-color-black); + text-decoration: none; + } } -.DocSearch--active .DocSearch-Footer { - display: none; +/* DocSearch button - smaller text */ +.navbar .DocSearch-Button { + font-size: 0.875rem; /* 14px */ + min-width: 16rem; /* Increased width */ + border: 0.5px solid var(--ifm-color-gray-300, #e5e7eb); + + .DocSearch-Button-Placeholder { + font-size: 0.875rem; + } } - - +/* Right-hand navbar items - only for smaller widths */ @media (max-width: 996px) { - /* Make custom HTML navbar items look like menu__link in mobile menu */ - .navbar .menu__list .menu__link, - .navbar .menu__list .navbar-icon-link { - display: flex; - align-items: center; - width: 100%; - gap: 6px; - padding: 2.5px 9px; - color: var(--ifm-menu-color); - font-size: var(--default-font); - text-decoration: none; - border-radius: 4px; - margin: 0; - background: none; - box-shadow: none; + .navbar .navbar-icon-link { + padding: 0.25rem 0rem; + + &:hover, + &:focus { + color: var(--ifm-color-primary); + } } +} - .navbar .navbar__items--left .navbar-icon-link { - width: 100%; - display: flex; - align-items: center; +/* Dark mode adjustments */ +[data-theme='dark'] .navbar { + .navbar__link { + color: var(--ifm-color-white); + + &:hover, + &:focus { + color: var(--ifm-color-white); + background-color: rgba(255, 255, 255, 0.06); + } + + /* Active links don't need hover effect */ + &.navbar__link--active:hover { + background-color: transparent; + } } - - .navbar .navbar-icon-link { - display: none !important; + + /* Active navbar link - theme color in dark mode */ + .navbar__item--has-active-link .navbar__link, + .navbar__item .navbar__link--active, + .navbar__link--active { + color: var(--ifm-color-primary); } - -} - -@media (max-width: 768px) { - .navbar .navbar__item { - display: none + + .navbar__item.dropdown { + /* Active dropdown in dark mode - theme color */ + &.navbar__dropdown--has-active > .navbar__link { + color: var(--ifm-color-primary); + } } -} - -@media (max-width: 600px) { - .navbar .navbar__items--right .DocSearch-Button { - width: 100%; - min-width: 0; - margin-left: 0; - height: 36px; - box-sizing: border-box; + + /* Dropdown menu in dark mode - maintain breathing room */ + .dropdown__menu { + margin-top: 8px; } - .navbar .navbar__items--right .DocSearch-Button::after { - display: none !important; + + .dropdown__link { + color: var(--ifm-color-white); + + &:hover, + &:focus { + color: var(--ifm-color-white); + background-color: rgba(255, 255, 255, 0.1); + } + + /* Active dropdown menu items - theme color */ + &.dropdown__link--active { + color: var(--ifm-color-primary); + } } -} - - - -/* Hide text on very small screens for right-side items */ -@media (max-width: 480px) { - .navbar .navbar__items--right .navbar-icon-link { - display: none; + + .navbar-icon-link { + color: var(--ifm-color-white); + + &:hover, + &:focus { + color: var(--ifm-color-white); + } + } + + /* DocSearch button in dark mode */ + .DocSearch-Button { + border: 0.5px solid rgba(255, 255, 255, 0.2); + } + + /* Right-hand navbar items in dark mode - only for smaller widths */ + @media (max-width: 996px) { + .navbar-icon-link { + &:hover, + &:focus { + background-color: rgba(255, 255, 255, 0.1); + } + } + + /* Search button styling in dark mode */ + .navbar__item .DocSearch-Button { + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } + } } -} - -//Mobile Hamburger Menu Header - -.navbar-sidebar__brand { - height: 60px; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 16px; -} - -.navbar-sidebar__brand .mobile-nav-icon-links { - display: flex; - align-items: center; - gap: 24px; -} - -.navbar-sidebar__brand .mobile-nav-icon-link { - display: inline-flex; - align-items: center; - justify-content: center; } -.navbar-sidebar__brand .mobile-nav-icon-link img { - width: 100%; - height: 100%; +/* ========================= + Mobile Sidebar Panel Styles + ========================= */ +.navbar-sidebar { + /* Fix spacing for navbar items in mobile sidebar */ + .navbar-sidebar__item { + margin: 0; + padding: 0; + } + + /* Fix spacing for dropdown items in mobile sidebar */ + .navbar-sidebar__item.dropdown { + .dropdown__menu { + padding: 0.5rem 0; + margin: 0; + } + + .dropdown__link { + padding: 0.625rem 1.5rem 0.625rem 1.75rem; + margin: 0.125rem 0; + border-radius: 0.375rem; + } + } + + /* Right-hand items in mobile sidebar - match left-hand styling */ + .navbar-sidebar__item { + /* Apply same styling to search button */ + .DocSearch-Button { + padding: 0.75rem 1rem; + margin: 0.25rem 0; + border-radius: 0.375rem; + width: 100%; + justify-content: flex-start; + font-size: 0.875rem; /* 14px */ + + .DocSearch-Button-Placeholder { + font-size: 0.875rem; + } + } + + /* Apply same styling to HTML links (GitHub, Blog) to match left-side items */ + .navbar-icon-link { + display: block; + padding: 0.2rem 0rem; + margin: 0.25rem 0; + border-radius: 0.375rem; + color: var(--ifm-color-black); + text-decoration: none; + font-weight: 500; + font-size: 0.875rem; + transition: background-color 0.2s ease; + + &:hover, + &:focus { + color: var(--ifm-color-primary); + background-color: rgba(0, 0, 0, 0.05); + font-weight: 600; + } + } + } + + /* Dark mode for mobile sidebar HTML links */ + [data-theme='dark'] & { + .navbar-sidebar__item { + .navbar-icon-link { + color: var(--ifm-color-white); + + &:hover, + &:focus { + color: var(--ifm-color-primary); + background-color: rgba(255, 255, 255, 0.1); + } + } + } + } + + /* Ensure consistent spacing between all items */ + .navbar-sidebar__items { + padding: 1rem 0.5rem; + + > .navbar-sidebar__item { + margin-bottom: 0.25rem; + } + } } diff --git a/docs/src/theme/Navbar/index.js b/docs/src/theme/Navbar/index.js index 20c9deffc37..986486c3675 100644 --- a/docs/src/theme/Navbar/index.js +++ b/docs/src/theme/Navbar/index.js @@ -1,69 +1,41 @@ import { useColorMode } from '@docusaurus/theme-common'; +import { useLocation } from '@docusaurus/router'; import Navbar from '@theme-original/Navbar'; import { useEffect, useLayoutEffect } from 'react'; -const MOBILE_ICON_LINKS = [ - { - href: 'https://github.com/rilldata/rill', - label: 'GitHub', - src: '/icons/Github.svg', - }, - { - href: '/notes', - label: 'Release Notes', - src: '/icons/ReleaseNotes.svg', - }, - { - href: 'https://www.rilldata.com/blog', - label: 'Blog', - src: '/icons/MessageSquareQuote.svg', - }, -]; - -function createIconLink({ href, label, src }) { - const anchor = document.createElement('a'); - anchor.href = href; - anchor.target = '_blank'; - anchor.rel = 'noopener noreferrer'; - anchor.className = 'mobile-nav-icon-link'; - anchor.setAttribute('aria-label', label); - - const img = document.createElement('img'); - img.src = src; - img.alt = label; - img.width = 24; - img.height = 24; - - anchor.appendChild(img); - return anchor; -} - -function ensureSidebarIcons() { - const brand = document.querySelector('.navbar-sidebar__brand'); - // Check if icons already exist - if (!brand || brand.querySelector('.mobile-nav-icon-links')) { - return; - } - - const container = document.createElement('div'); - container.className = 'mobile-nav-icon-links'; - - MOBILE_ICON_LINKS.forEach((link) => { - container.appendChild(createIconLink(link)); - }); - - const closeButton = brand.querySelector('.navbar-sidebar__close'); - if (closeButton) { - brand.insertBefore(container, closeButton); - } else { - brand.appendChild(container); - } -} +// Mobile icon functionality removed export default function NavbarWrapper(props) { // We only need colorMode to determine which icon to show. // The toggle logic is handled by the original button's onClick. const { colorMode } = useColorMode(); + const location = useLocation(); + + // Close all open dropdowns when route changes + // This fixes the issue where hover-based dropdowns stay open after clicking an item + useEffect(() => { + const closeAllDropdowns = () => { + // Remove dropdown--show class from all dropdowns + const openDropdowns = document.querySelectorAll('.navbar__item.dropdown.dropdown--show'); + openDropdowns.forEach((dropdown) => { + dropdown.classList.remove('dropdown--show'); + }); + + // Also reset aria-expanded attributes + const expandedLinks = document.querySelectorAll('.navbar__link[aria-expanded="true"]'); + expandedLinks.forEach((link) => { + link.setAttribute('aria-expanded', 'false'); + }); + + // Blur any focused navbar elements to prevent hover state from re-triggering + const activeElement = document.activeElement; + if (activeElement && activeElement.closest('.navbar__item.dropdown')) { + activeElement.blur(); + } + }; + + closeAllDropdowns(); + }, [location.pathname]); // Handle Dark Mode Toggle Icons // useLayoutEffect fires synchronously before paint, reducing flicker @@ -80,9 +52,9 @@ export default function NavbarWrapper(props) { if (!iconContainer) { iconContainer = document.createElement('span'); iconContainer.className = 'icon-container'; - + // Clear existing Docusaurus toggle content (text/emojis) - btn.innerHTML = ''; + btn.innerHTML = ''; btn.appendChild(iconContainer); } @@ -90,7 +62,7 @@ export default function NavbarWrapper(props) { // If Dark Mode -> Show Sun (to switch to Light) // If Light Mode -> Show Moon (to switch to Dark) const isDark = colorMode === 'dark'; - + iconContainer.innerHTML = ` `; - + btn.setAttribute('aria-label', isDark ? 'Switch to light mode' : 'Switch to dark mode'); }); }, [colorMode]); - // Handle Mobile Sidebar Icons + // Mobile sidebar icons removed - no longer needed + + // Consolidated MutationObserver for navbar updates useEffect(() => { - // Observer to inject icons when the mobile menu opens/renders - const observer = new MutationObserver(() => { - ensureSidebarIcons(); - }); + const markActiveDropdowns = () => { + // Target elements with both navbar__item and dropdown classes + const dropdownItems = document.querySelectorAll('.navbar__item.dropdown'); + dropdownItems.forEach((dropdownItem) => { + const activeDropdownLink = dropdownItem.querySelector('.dropdown__link--active'); + if (activeDropdownLink) { + dropdownItem.classList.add('navbar__dropdown--has-active'); + } else { + dropdownItem.classList.remove('navbar__dropdown--has-active'); + } + }); + }; + + const markActiveNavItems = () => { + // Target non-dropdown navbar items + const navItems = document.querySelectorAll('.navbar__item:not(.dropdown)'); + navItems.forEach((navItem) => { + const activeLink = navItem.querySelector('.navbar__link--active'); + if (activeLink) { + navItem.classList.add('navbar__item--has-active-link'); + } else { + navItem.classList.remove('navbar__item--has-active-link'); + } + }); + }; + + const addDataTextAttributes = () => { + const navLinks = document.querySelectorAll('.navbar__link'); + navLinks.forEach((link) => { + // Only add if not already present + if (!link.hasAttribute('data-text')) { + const text = link.textContent?.trim() || ''; + if (text) { + link.setAttribute('data-text', text); + } + } + }); + }; + + const replaceCustomDropdownCarets = () => { + // Add custom SVG chevron for custom dropdown links + // CSS already hides the default caret, so we just need to add our custom one + const customDropdownLinks = document.querySelectorAll('.navbar__link.my-custom-dropdown'); + customDropdownLinks.forEach((link) => { + const dropdownItem = link.closest('.navbar__item.dropdown'); + if (dropdownItem && !link.hasAttribute('data-custom-chevron-added')) { + // Mark as processed + link.setAttribute('data-custom-chevron-added', 'true'); + + // Create a container for the custom chevron + let chevronContainer = link.querySelector('.custom-chevron'); + if (!chevronContainer) { + chevronContainer = document.createElement('span'); + chevronContainer.className = 'custom-chevron'; + chevronContainer.style.position = 'absolute'; + chevronContainer.style.right = '0.5rem'; + chevronContainer.style.top = '30%'; + chevronContainer.style.transform = 'translateY(0%)'; + chevronContainer.style.transition = 'transform 0.2s ease'; + chevronContainer.style.opacity = '0.7'; + chevronContainer.style.width = '14px'; + chevronContainer.style.height = '14px'; + chevronContainer.style.display = 'block'; + chevronContainer.style.pointerEvents = 'none'; + link.style.position = 'relative'; + link.appendChild(chevronContainer); + } + + // Clear and add custom SVG chevron + chevronContainer.innerHTML = ''; + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('height', '14px'); + svg.setAttribute('viewBox', '0 0 24 24'); + svg.setAttribute('fill', 'currentColor'); + svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); + svg.style.width = '14px'; + svg.style.height = '14px'; + svg.style.display = 'block'; + + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('fill-rule', 'evenodd'); + path.setAttribute('clip-rule', 'evenodd'); + path.setAttribute('d', 'M19.189 9.43683C19.3842 9.63209 19.3842 9.94867 19.189 10.1439L11.9999 17.333L4.81075 10.1439C4.61549 9.94867 4.61549 9.63209 4.81075 9.43683L5.98898 8.2586C6.18424 8.06334 6.50082 8.06334 6.69609 8.2586L11.9999 13.5624L17.3036 8.2586C17.4989 8.06334 17.8155 8.06334 18.0108 8.2586L19.189 9.43683Z'); + + svg.appendChild(path); + chevronContainer.appendChild(svg); + + // CSS will handle the rotation on hover/open + } + }); + }; + + // Combined update function + const updateNavbar = () => { + markActiveDropdowns(); + markActiveNavItems(); + addDataTextAttributes(); + replaceCustomDropdownCarets(); + }; + // Run on mount and when DOM changes + updateNavbar(); + + // Staggered timeouts to handle dynamically rendered navbar elements. + // Docusaurus may hydrate or lazy-load navbar items at different times, + // especially for dropdowns and client-side navigation. These delays + // ensure our custom styling (data-text attrs, active states, chevrons) + // is applied even if elements render after initial mount. + setTimeout(updateNavbar, 100); + setTimeout(updateNavbar, 500); + setTimeout(updateNavbar, 1000); + + const observer = new MutationObserver(updateNavbar); observer.observe(document.body, { childList: true, subtree: true }); - ensureSidebarIcons(); // Initial check return () => { observer.disconnect(); - const existing = document.querySelector('.mobile-nav-icon-links'); - if (existing) { - existing.remove(); - } }; }, []); diff --git a/docs/src/theme/NavbarItem/DropdownNavbarItem.js b/docs/src/theme/NavbarItem/DropdownNavbarItem.js new file mode 100644 index 00000000000..da1a5c3536e --- /dev/null +++ b/docs/src/theme/NavbarItem/DropdownNavbarItem.js @@ -0,0 +1,65 @@ +import React, { useEffect, useCallback } from 'react'; +import DropdownNavbarItem from '@theme-original/NavbarItem/DropdownNavbarItem'; +import { useLocation } from '@docusaurus/router'; + +/** + * Swizzled DropdownNavbarItem component that closes dropdown on item click. + * + * This fixes the issue where hover-based dropdowns stay open after clicking + * a menu item because the ::before pseudo-element (hover bridge) keeps the + * dropdown open if the mouse is still in that area. + */ +export default function DropdownNavbarItemWrapper(props) { + const location = useLocation(); + + // Helper to close all dropdowns and temporarily disable hover + const closeAllDropdowns = useCallback(() => { + // Add a class to the navbar to temporarily disable hover + const navbar = document.querySelector('.navbar'); + if (navbar) { + navbar.classList.add('navbar--closing-dropdown'); + } + + // Remove dropdown--show class from all dropdowns + const openDropdowns = document.querySelectorAll('.navbar__item.dropdown.dropdown--show'); + openDropdowns.forEach((dropdown) => { + dropdown.classList.remove('dropdown--show'); + }); + + // Reset aria-expanded attributes and blur + const expandedLinks = document.querySelectorAll('.navbar__link[aria-expanded="true"]'); + expandedLinks.forEach((link) => { + link.setAttribute('aria-expanded', 'false'); + link.blur(); + }); + + // Remove the closing class after mouse has likely moved away + setTimeout(() => { + if (navbar) { + navbar.classList.remove('navbar--closing-dropdown'); + } + }, 300); + }, []); + + // Close dropdown when route changes + useEffect(() => { + closeAllDropdowns(); + }, [location.pathname, closeAllDropdowns]); + + // Global click handler for dropdown links + useEffect(() => { + const handleClick = (e) => { + const dropdownLink = e.target.closest('.dropdown__link'); + if (dropdownLink) { + // Immediately close + closeAllDropdowns(); + } + }; + + // Use capture phase to catch events before they bubble + document.addEventListener('click', handleClick, true); + return () => document.removeEventListener('click', handleClick, true); + }, [closeAllDropdowns]); + + return ; +}