diff --git a/sass/assets/css/_nav-ui.scss b/sass/assets/css/_nav-ui.scss new file mode 100644 index 0000000..8d46aca --- /dev/null +++ b/sass/assets/css/_nav-ui.scss @@ -0,0 +1,570 @@ +: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):not(.is-unavailable):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; +} + +.nav-ui-desktop-actions { + display: inline-flex; + align-items: center; + gap: 0.45rem; + margin-left: -0.3rem; + vertical-align: middle; +} + +.nav-ui-desktop-actions .nav-ui-icon-button { + width: 2.15rem; + height: 2.15rem; + color: var(--bs-body-color); + border-color: var(--bs-border-color); + background: rgba(var(--bs-body-bg-rgb), 0.82); +} + +.nav-ui-desktop-actions .nav-ui-icon-button:hover, +.nav-ui-desktop-actions .nav-ui-icon-button:focus-visible, +.nav-ui-desktop-actions .nav-ui-icon-button.is-active { + color: var(--bs-emphasis-color); + border-color: var(--bs-emphasis-color); + background: rgba(var(--bs-secondary-bg-rgb), 0.82); +} + +.nav-ui-desktop-actions .nav-ui-icon-button svg { + width: 1rem; + height: 1rem; +} + +.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):not(.is-unavailable):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, +.nav-ui-icon-button.is-unavailable { + 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, +.nav-ui-toc-content 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, +.nav-ui-toc-content a:hover { + border-color: #726E97; + text-decoration: none; +} + +.nav-ui-toc-content h2 { + display: none !important; +} + +.toc > .nav-ui-toc-heading { + display: inline-flex; + align-items: center; + gap: 0.45rem; +} + +.nav-ui-toc-heading svg { + width: 1.15em; + height: 1.15em; + color: currentColor; + flex: 0 0 auto; +} + +.nav-ui-toc-content ul { + margin: 0; + padding-left: 0; + list-style: none; +} + +.nav-ui-toc-content li + li { + margin-top: 0.4rem; +} + +.nav-ui-toc-content ul ul { + margin-top: 0.4rem; + padding-left: 0.9rem; +} + +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):not(.is-unavailable):hover { + background: rgba(255, 255, 255, 0.08); + border-color: var(--nav-ui-border); + color: #fff; + text-decoration: none; + } + + .nav-ui-desktop-actions .nav-ui-icon-button:hover { + color: var(--bs-body-color); + border-color: var(--bs-border-color); + background: rgba(var(--bs-body-bg-rgb), 0.82); + } + + #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):not(.is-unavailable):hover { + background: rgba(255, 255, 255, 0.16); + border-color: rgba(255, 255, 255, 0.7); + color: #fff; + text-decoration: none; + } + + .nav-ui-desktop-actions .nav-ui-icon-button:hover { + color: var(--bs-emphasis-color); + border-color: var(--bs-emphasis-color); + background: rgba(var(--bs-secondary-bg-rgb), 0.82); + } + + #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, + .nav-ui-desktop-actions { + 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..3db3082 --- /dev/null +++ b/static/assets/scripts/nav-ui.mjs @@ -0,0 +1,238 @@ +let overlay; +let modalTitle; +let modalBody; +let closeButton; +let lastFocusedElement; +let pressedTimer; + +const setActiveTool = (target) => { + document.querySelectorAll('[data-nav-ui-open="menu"], [data-nav-ui-open="toc"]').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 buildTocContent = () => { + const toc = document.querySelector(".toc"); + const content = document.createElement("div"); + content.className = "nav-ui-toc-content"; + + if (toc) { + content.appendChild(toc.cloneNode(true)); + } + + content.addEventListener("click", (event) => { + if (event.target.closest("a")) { + closeModal(); + } + }); + + return content; +}; + +const triggerSearch = () => { + const searchLink = document.getElementById("search"); + + if (searchLink) { + searchLink.click(); + } +}; + +const openMenuModal = () => { + setActiveTool("menu"); + openModal("Menu", buildMenuContent()); +}; + +const openTocModal = () => { + setActiveTool("toc"); + openModal("Table of Contents", buildTocContent()); +}; + +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 === "toc") { + openTocModal(); + } 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..30fdea9 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 -%} @@ -186,6 +201,50 @@ {% endblock nav %} + + + +
{% block content %} {% endblock content %} diff --git a/templates/eip.html b/templates/eip.html index 66e73cb..8a006f0 100644 --- a/templates/eip.html +++ b/templates/eip.html @@ -5,6 +5,34 @@ {{ macros::eip_number(page=page) }}: {{ page.title }} | {{ config.title -}} {% endblock title %} +{% block desktop_nav_tools %} +{% if page.toc %} + + + +{% 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 @@