From 6e1affab13812ae801d36d6b115a5cf43591f85a Mon Sep 17 00:00:00 2001 From: Rito Rhymes Date: Mon, 11 May 2026 02:49:27 -0400 Subject: [PATCH 1/2] Rework desktop and mobile navigation UI Use a fixed flex-based desktop header with a matching body spacer so page content remains positioned below the header. Add the Ethereum logomark beside the site title. Add a mobile bottom bar for primary page actions, including scroll-to-top, search, and menu controls. Keep the scroll-to-top control in its layout slot with disabled muted styling until the page scroll position makes it available. Hide the bottom bar when it overlaps the footer unless the page height is too short to need footer avoidance. Add an explicit Home link to the shared navigation so the homepage path is visible in the navigation model. Introduce a shared navigation modal shell for mobile menu interactions. Use an inline SVG icon for the search entrypoint and restyle the existing search modal for the dark theme. --- sass/assets/css/_nav-ui.scss | 493 ++++++++++++++++++++++++++++++ sass/assets/css/style.scss | 16 + static/assets/images/logomark.svg | 1 + static/assets/scripts/nav-ui.mjs | 213 +++++++++++++ static/assets/scripts/search.mjs | 8 +- templates/base.html | 57 +++- 6 files changed, 781 insertions(+), 7 deletions(-) create mode 100644 sass/assets/css/_nav-ui.scss create mode 100644 static/assets/images/logomark.svg create mode 100644 static/assets/scripts/nav-ui.mjs diff --git a/sass/assets/css/_nav-ui.scss b/sass/assets/css/_nav-ui.scss new file mode 100644 index 0000000..d076552 --- /dev/null +++ b/sass/assets/css/_nav-ui.scss @@ -0,0 +1,493 @@ +:root { + --nav-ui-bg: rgba(var(--bs-body-bg-rgb), 0.96); + --nav-ui-panel-bg: rgb(var(--bs-body-bg-rgb)); + --nav-ui-border: rgba(255, 255, 255, 0.18); + --nav-ui-shadow: 0 -0.4rem 1.6rem rgba(0, 0, 0, 0.28); + --nav-ui-desktop-header-height: 7.65rem; +} + +.nav-ui-icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.75rem; + height: 2.75rem; + padding: 0; + border: 1px solid var(--nav-ui-border); + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + color: #fff; + text-decoration: none; + cursor: pointer; + transition: + background-color 0.18s ease, + border-color 0.18s ease, + opacity 0.18s ease, + transform 0.18s ease, + visibility 0.18s ease; +} + +.nav-ui-icon-button:not(:disabled):hover, +.nav-ui-icon-button:focus-visible { + background: rgba(255, 255, 255, 0.16); + border-color: rgba(255, 255, 255, 0.7); + color: #fff; + text-decoration: none; +} + +.nav-ui-icon-button:focus-visible { + background: rgba(255, 255, 255, 0.16); + border-color: rgba(255, 255, 255, 0.7); + color: #fff; + text-decoration: none; +} + +.nav-ui-icon-button svg { + width: 1.28rem; + height: 1.28rem; + stroke: currentColor; +} + +.site-nav .trigger.p-2 { + padding: 0 !important; +} + +.nav-ui-bottom-bar { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; + display: flex; + justify-content: center; + padding: 0.55rem 0.8rem calc(0.55rem + env(safe-area-inset-bottom)); + border-top: 1px solid #e8e8e8; + background: rgb(var(--bs-body-bg-rgb)); + box-shadow: var(--nav-ui-shadow); + transition: + opacity 0.22s ease, + transform 0.22s ease, + visibility 0.22s ease; +} + +.nav-ui-bottom-bar.is-hidden { + opacity: 0; + pointer-events: none; + transform: translateY(1rem); + visibility: hidden; +} + +.nav-ui-bottom-inner { + display: grid; + grid-auto-columns: minmax(0, 1fr); + grid-auto-flow: column; + width: 100%; + max-width: 400px; + gap: 0.5rem; +} + +.nav-ui-bottom-inner .nav-ui-icon-button { + width: 100%; + min-width: 0; +} + +.nav-ui-bottom-inner .nav-ui-icon-button:not(:disabled):active, +.nav-ui-bottom-inner .nav-ui-icon-button.is-active, +.nav-ui-bottom-inner .nav-ui-icon-button.is-pressed { + background: rgba(255, 255, 255, 0.16); + border-color: rgba(255, 255, 255, 0.7); + color: #fff; + text-decoration: none; +} + +.nav-ui-icon-button:disabled { + background: rgba(var(--bs-secondary-bg-rgb), 0.45); + color: var(--bs-secondary-color); + cursor: default; + opacity: 0.5; + pointer-events: none; +} + +.nav-ui-overlay { + position: fixed; + inset: 0; + z-index: 1040; + display: none; + align-items: center; + justify-content: center; + padding: 1rem; + background: rgba(0, 0, 0, 0.56); +} + +.nav-ui-overlay.is-open { + display: flex; +} + +.nav-ui-modal { + width: min(100%, 42rem); + max-height: 90vh; + overflow: hidden; + border: 1px solid var(--nav-ui-border); + border-radius: 0.75rem; + background: var(--nav-ui-panel-bg); + color: var(--bs-body-color); + box-shadow: 0 1.2rem 4rem rgba(0, 0, 0, 0.42); +} + +.nav-ui-modal-header { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1rem 1rem 0.75rem; + border-bottom: 1px solid var(--bs-border-color); +} + +.nav-ui-modal-title { + position: absolute; + left: 50%; + max-width: calc(100% - 7rem); + margin: 0; + transform: translateX(-50%); + font-size: 1rem; + font-weight: 600; + text-align: center; +} + +.nav-ui-modal-close { + margin-left: auto; + width: 2.25rem; + height: 2.25rem; + color: var(--bs-body-color); + border-color: var(--bs-border-color); + background: transparent; +} + +.nav-ui-modal-body { + max-height: calc(90vh - 4.25rem); + overflow-y: auto; + padding: 0.9rem 1rem 1rem; +} + +.nav-ui-modal-body a { + color: #726E97; +} + +.nav-ui-modal-body a:visited { + color: #8E6680; +} + +.nav-ui-menu-list { + display: grid; + gap: 0.4rem; + margin: 0; + padding: 0; + list-style: none; +} + +.nav-ui-menu-list a { + text-align: center; +} + +.nav-ui-menu-list a { + display: block; + padding: 0.72rem 0.8rem; + border: 1px solid var(--bs-border-color); + border-radius: 0.55rem; + background: rgba(var(--bs-secondary-bg-rgb), 0.5); + text-decoration: none; +} + +.nav-ui-menu-list a:hover { + border-color: #726E97; + text-decoration: none; +} + +body.nav-ui-lock { + overflow: hidden; +} + +#search, +#search.nav-ui-search-link { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.15rem; + height: 2.15rem; + margin-left: 0; + border: 1px solid var(--bs-border-color); + border-radius: 999px; + color: var(--bs-body-color); + background: rgba(var(--bs-body-bg-rgb), 0.82); + font-size: 0; + vertical-align: middle; +} + +#search.nav-ui-search-link:hover, +#search.nav-ui-search-link:focus-visible { + color: var(--bs-emphasis-color); + border-color: var(--bs-emphasis-color); + background: rgba(var(--bs-secondary-bg-rgb), 0.82); + text-decoration: none; +} + +@media (hover: none), (pointer: coarse) { + .nav-ui-icon-button:not(:disabled):hover { + background: rgba(255, 255, 255, 0.08); + border-color: var(--nav-ui-border); + color: #fff; + text-decoration: none; + } + + #search.nav-ui-search-link:hover { + color: var(--bs-body-color); + border-color: var(--bs-border-color); + background: rgba(var(--bs-body-bg-rgb), 0.82); + text-decoration: none; + } +} + +@media (hover: hover) and (pointer: fine) { + .nav-ui-icon-button:not(:disabled):hover { + background: rgba(255, 255, 255, 0.16); + border-color: rgba(255, 255, 255, 0.7); + color: #fff; + text-decoration: none; + } + + #search.nav-ui-search-link:hover { + color: var(--bs-emphasis-color); + border-color: var(--bs-emphasis-color); + background: rgba(var(--bs-secondary-bg-rgb), 0.82); + text-decoration: none; + } +} + +#search.nav-ui-search-link svg { + width: 1rem; + height: 1rem; +} + +#search-modal { + position: fixed; + inset: 0; + z-index: 1040; + display: flex; + align-items: flex-start; + justify-content: center; + padding: min(8vh, 100px) 1rem 1rem; + background: rgba(0, 0, 0, 0.58) !important; +} + +#search-modal .search-container { + width: 600px; + max-width: 90%; + max-height: 70vh; + display: flex; + flex-direction: column; + overflow: hidden; + border: 1px solid var(--bs-border-color); + border-radius: 8px; + background: rgb(var(--bs-body-bg-rgb)) !important; + color: var(--bs-body-color) !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +#search-modal .search-header { + display: flex; + padding: 16px; + border-bottom: 1px solid var(--bs-border-color); +} + +#search-modal #search-input { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--bs-border-color) !important; + outline: none; + background: rgb(var(--bs-secondary-bg-rgb)) !important; + color: var(--bs-body-color) !important; + font-size: 16px; +} + +#search-modal #close-search { + padding: 0 8px; + border: none; + background: none; + color: var(--bs-body-color) !important; + cursor: pointer; + font-size: 24px; +} + +#search-modal #search-results { + max-height: none !important; + overflow-y: auto; +} + +#search-modal .search-result-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +#search-modal .search-result-header-meta { + margin-top: -4px; +} + +#search-modal .search-result { + display: block; + padding: 12px 16px; + border-bottom: 1px solid var(--bs-border-color) !important; + color: var(--bs-body-color) !important; + text-decoration: none; + transition: background-color 0.2s; +} + +#search-modal .search-result:hover { + background: rgba(var(--bs-secondary-bg-rgb), 0.7) !important; + text-decoration: none; +} + +#search-modal .search-result:hover h3, +#search-modal .search-result:hover p { + text-decoration: underline; +} + +#search-modal .search-result h3 { + margin: 0 0 8px; + color: var(--bs-body-color) !important; + font-size: 16px; +} + +#search-modal .search-result p { + margin: 0; + color: var(--bs-body-color) !important; + font-size: 14px; +} + +#search-modal .no-results { + padding: 16px; + color: var(--bs-body-color); + text-align: center; +} + +@media (max-width: 999.98px) { + body { + padding-bottom: 4.4rem; + } + + .site-header .wrapper { + align-items: center !important; + text-align: center; + } + + .site-title { + float: none !important; + padding-right: 0 !important; + margin-right: 0 !important; + text-align: center; + } + + .site-header .site-nav.d-flex { + display: none !important; + } + + .site-nav .nav-trigger, + .site-nav label[for="nav-trigger"], + .site-nav .trigger { + display: none !important; + } + + #search.nav-ui-search-link { + display: none !important; + } +} + +@media (max-width: 400px) { + .site-title { + font-size: 1.4rem; + } +} + +@media (max-width: 350px) { + .site-title { + gap: 0.35rem; + font-size: 1.25rem; + } +} + +@media (min-width: 1000px) { + body { + padding-top: var(--nav-ui-desktop-header-height); + } + + .site-header { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1020; + background: rgb(var(--bs-body-bg-rgb)); + line-height: normal; + } + + .site-header .wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.4rem; + padding-top: 0.2rem; + } + + .site-title { + float: none; + margin: 0; + line-height: 1.2; + } + + .site-nav { + position: static; + float: none; + line-height: normal; + border: 0; + background: transparent; + text-align: center; + } + + .site-nav .trigger { + display: block; + padding: 0 !important; + } + + .site-nav .trigger.row { + --bs-gutter-x: 0; + --bs-gutter-y: 0; + margin: 0; + } + + .site-nav .trigger > span { + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 0.65rem; + } + + .site-nav .page-link { + display: inline-flex; + align-items: center; + width: auto; + padding: 0; + margin: 0; + line-height: 1.2; + } + + .nav-ui-bottom-bar { + display: none; + } + + .nav-ui-overlay { + align-items: center; + } +} diff --git a/sass/assets/css/style.scss b/sass/assets/css/style.scss index 4bf4052..7655162 100644 --- a/sass/assets/css/style.scss +++ b/sass/assets/css/style.scss @@ -12,6 +12,20 @@ $content-width: 1152px; } } +.site-title { + display: inline-flex; + align-items: center; + gap: 0.45rem; +} + +.site-logomark { + display: block; + width: auto; + height: 0.78em; + flex: 0 0 auto; + transform: translateY(0.03em); +} + .page-link.active { font-weight: bold; } @@ -202,6 +216,8 @@ pre,code { opacity: 0.8; } +@import "nav-ui"; + @media print { header, diff --git a/static/assets/images/logomark.svg b/static/assets/images/logomark.svg new file mode 100644 index 0000000..b15b246 --- /dev/null +++ b/static/assets/images/logomark.svg @@ -0,0 +1 @@ + diff --git a/static/assets/scripts/nav-ui.mjs b/static/assets/scripts/nav-ui.mjs new file mode 100644 index 0000000..8960aa9 --- /dev/null +++ b/static/assets/scripts/nav-ui.mjs @@ -0,0 +1,213 @@ +let overlay; +let modalTitle; +let modalBody; +let closeButton; +let lastFocusedElement; +let pressedTimer; + +const setActiveTool = (target) => { + document.querySelectorAll('[data-nav-ui-open="menu"]').forEach((button) => { + button.classList.toggle("is-active", button.getAttribute("data-nav-ui-open") === target); + }); +}; + +const ensureModal = () => { + overlay = overlay || document.querySelector(".nav-ui-overlay"); + modalTitle = modalTitle || document.querySelector(".nav-ui-modal-title"); + modalBody = modalBody || document.querySelector(".nav-ui-modal-body"); + closeButton = closeButton || document.querySelector(".nav-ui-modal-close"); + + return Boolean(overlay && modalTitle && modalBody && closeButton); +}; + +const closeModal = () => { + if (!ensureModal()) { + return; + } + + overlay.classList.remove("is-open"); + overlay.setAttribute("aria-hidden", "true"); + document.body.classList.remove("nav-ui-lock"); + setActiveTool(null); + + if (lastFocusedElement instanceof HTMLElement) { + lastFocusedElement.focus(); + } +}; + +const openModal = (title, content) => { + if (!ensureModal()) { + return; + } + + lastFocusedElement = document.activeElement; + modalTitle.textContent = title; + modalBody.textContent = ""; + modalBody.appendChild(content); + overlay.classList.add("is-open"); + overlay.setAttribute("aria-hidden", "false"); + document.body.classList.add("nav-ui-lock"); + closeButton.focus(); +}; + +const buildMenuContent = () => { + const list = document.createElement("ul"); + list.className = "nav-ui-menu-list"; + const links = document.querySelectorAll(".site-nav .trigger .page-link"); + + for (const link of links) { + if (link.id === "search") { + continue; + } + + const item = document.createElement("li"); + const clone = link.cloneNode(true); + clone.removeAttribute("class"); + item.appendChild(clone); + list.appendChild(item); + } + + return list; +}; + +const triggerSearch = () => { + const searchLink = document.getElementById("search"); + + if (searchLink) { + searchLink.click(); + } +}; + +const openMenuModal = () => { + setActiveTool("menu"); + openModal("Menu", buildMenuContent()); +}; + +const bindModal = () => { + if (!ensureModal()) { + return; + } + + overlay.addEventListener("click", (event) => { + if (event.target === overlay) { + closeModal(); + } + }); + + closeButton.addEventListener("click", closeModal); +}; + +const bindActions = () => { + document.querySelectorAll("[data-nav-ui-open]").forEach((button) => { + button.addEventListener("click", () => { + const target = button.getAttribute("data-nav-ui-open"); + + if (target === "menu") { + openMenuModal(); + } else if (target === "search") { + triggerSearch(); + } + }); + }); + + document.querySelectorAll("[data-nav-ui-scroll-top]").forEach((button) => { + button.addEventListener("click", () => { + button.classList.add("is-pressed"); + window.clearTimeout(pressedTimer); + pressedTimer = window.setTimeout(() => { + button.classList.remove("is-pressed"); + }, 450); + window.scrollTo({ top: 0, behavior: "smooth" }); + }); + }); +}; + +const bindTopButton = () => { + const topButton = document.querySelector(".nav-ui-top-button"); + + if (!topButton) { + return; + } + + const updateTopButton = () => { + const shouldEnable = window.scrollY > 180; + topButton.disabled = !shouldEnable; + + if (!shouldEnable && document.activeElement === topButton) { + topButton.blur(); + } + }; + + updateTopButton(); + window.addEventListener("scroll", updateTopButton, { passive: true }); +}; + +const bindFooterVisibility = () => { + const bar = document.querySelector(".nav-ui-bottom-bar"); + const footer = document.querySelector(".site-footer"); + const mobileQuery = window.matchMedia("(max-width: 999.98px)"); + let observer; + + if (!bar || !footer || !("IntersectionObserver" in window)) { + return; + } + + const isShortPage = () => { + return document.documentElement.scrollHeight < window.innerHeight * 1.5; + }; + + const disconnectObserver = () => { + if (observer) { + observer.disconnect(); + observer = null; + } + }; + + const updateFooterObserver = () => { + disconnectObserver(); + bar.classList.remove("is-hidden"); + + if (!mobileQuery.matches || isShortPage()) { + return; + } + + observer = new IntersectionObserver((entries) => { + const isVisible = entries.some((entry) => entry.isIntersecting); + bar.classList.toggle("is-hidden", isVisible); + }, { + threshold: 0.01, + rootMargin: "0px 0px -8% 0px", + }); + + observer.observe(footer); + }; + + updateFooterObserver(); + window.addEventListener("resize", updateFooterObserver); + + if (typeof mobileQuery.addEventListener === "function") { + mobileQuery.addEventListener("change", updateFooterObserver); + } +}; + +const bindEscape = () => { + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + closeModal(); + } + }); +}; + +const init = () => { + bindModal(); + bindActions(); + bindTopButton(); + bindFooterVisibility(); + bindEscape(); +}; + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); +} else { + init(); +} diff --git a/static/assets/scripts/search.mjs b/static/assets/scripts/search.mjs index 21b874d..9e034a7 100644 --- a/static/assets/scripts/search.mjs +++ b/static/assets/scripts/search.mjs @@ -488,13 +488,13 @@ const addSearchStyles = () => { }; const onContentLoaded = () => { - // Initialize the search UI (just styles, not the search functionality) - addSearchStyles(); - // Add click listener to search button if it exists const searchButton = document.getElementById("search"); if (searchButton) { - searchButton.addEventListener("click", openSearchModal); + searchButton.addEventListener("click", (event) => { + event.preventDefault(); + openSearchModal(); + }); } }; diff --git a/templates/base.html b/templates/base.html index 22155e5..68bdeff 100644 --- a/templates/base.html +++ b/templates/base.html @@ -128,6 +128,7 @@ crossorigin="anonymous" > + {% block nav %} @@ -137,8 +138,15 @@ class="site-title" rel="author" href="{{ get_url(path='/', trailing_slash=false) }}" - >{{ config.title }} + + {{ config.title }} + {%- if config.taxonomies -%} @@ -208,6 +209,13 @@ + {% block mobile_toc_button %} + + {% endblock mobile_toc_button %} + +{% endif %} +{% endblock desktop_nav_tools %} + +{% block mobile_toc_button %} +{% if page.toc %} + +{% else %} + +{% endif %} +{% endblock mobile_toc_button %} + {% block extra_head %} @@ -272,7 +300,12 @@