From d5a780cc3201a319affbbfc100e508d6c2914d2f Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Sat, 4 Oct 2025 10:26:49 +0200 Subject: [PATCH] feat: enable transparent navbar --- assets/js/critical/color.js | 7 +- assets/js/navbar.js | 190 +++++++++++++++--- assets/scss/components/_navbar.scss | 29 +++ config/_default/params.toml | 2 +- data/structures/navbar.yml | 12 +- exampleSite/config/_default/params.toml | 5 +- exampleSite/hugo_stats.json | 138 ++++++++----- .../_partials/assets/helpers/navbar-mode.html | 4 +- layouts/_partials/assets/navbar.html | 3 +- layouts/baseof.html | 5 +- 10 files changed, 315 insertions(+), 80 deletions(-) diff --git a/assets/js/critical/color.js b/assets/js/critical/color.js index 4bdd1f2a..4e4fb8b3 100644 --- a/assets/js/critical/color.js +++ b/assets/js/critical/color.js @@ -38,10 +38,11 @@ setLocalStorage('theme', theme, 'functional') if (theme === 'auto') { - document.documentElement.setAttribute('data-bs-theme', (getPreferredTheme())) - } else { - document.documentElement.setAttribute('data-bs-theme', theme) + theme = getPreferredTheme() } + document.documentElement.setAttribute('data-bs-theme', theme) + // store main theme separately, to avoid the navbar mode icon uses a local variable + document.documentElement.setAttribute('data-bs-main-theme', theme) updateSelectors() } diff --git a/assets/js/navbar.js b/assets/js/navbar.js index 2e7b8dda..517e2496 100644 --- a/assets/js/navbar.js +++ b/assets/js/navbar.js @@ -4,24 +4,161 @@ const togglers = document.querySelectorAll('.main-nav-toggler') const modeSelectors = document.querySelectorAll('.switch-mode-collapsed') const colorsBG = ['body', 'secondary', 'tertiary'] -function updateNavbar () { - let storedTheme - if (typeof getLocalStorage === "function") { - storedTheme = getLocalStorage('theme', null, 'functional') +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +function getStyle(el, styleProp) { + let y + + if (window.getComputedStyle) { + y = document.defaultView.getComputedStyle(el).getPropertyValue(styleProp) + } else if (el.currentStyle) { + y = el.currentStyle[styleProp] + } + + return y +} + +function updateNavbarColor () { + const scrollTop = window.pageYOffset + const scrollBottom = scrollTop + navbar.offsetHeight + + // find which section is currently under the navbar + let currentSection = null + const sections = document.querySelectorAll('article,section,footer') + let currentIndex = -1 + + sections.forEach(section => { + const rect = section.getBoundingClientRect() + const sectionTop = scrollTop + rect.top + const sectionBottom = sectionTop + section.offsetHeight - 1 + + // check if navbar overlaps with this section + if (scrollTop <= sectionBottom && scrollBottom >= sectionTop) { + let index = getStyle(section, 'z-index') + if (index === 'auto') { + index = 1 + } + if (index > currentIndex) { + currentSection = section + currentIndex = index + } + } + }) + + // use main part as backup (defined in baseof.html template) + if (!currentSection) { + currentSection = document.querySelector('main') } - if (window.scrollY > 75) { - navbar.classList.add('nav-active') - if (storedTheme) { - navbar.setAttribute('data-bs-theme', storedTheme) + if (currentSection) { + adaptToSection(currentSection) + } +} + +function getBackgroundColor (section) { + // get computed background color of the section + let color = window.getComputedStyle(section).backgroundColor + + // use body background when section background is undefined or transparent + if (color === 'rgba(0, 0, 0, 0)' || color === 'transparent') { + color = window.getComputedStyle(document.body).getPropertyValue('background-color') + } + + return color +} + +function adaptToSection (section) { + // retrieve the section background color, using body color as fallback + const color = getBackgroundColor(section) + + // determine if the background is light or dark + const isLightBackground = isLightColor(section, color) + + // set appropriate mode class + const nav = document.querySelector('.navbar') + if (isLightBackground) { + if (navbar.dataset.bsTheme !== 'light') { + navbar.dataset.bsTheme = 'light' } } else { - navbar.classList.remove('nav-active') - const defaultTheme = navbar.getAttribute('data-bs-overlay') + if (navbar.dataset.bsTheme !== 'dark') { + navbar.dataset.bsTheme = 'dark' + } + } - const targetTheme = defaultTheme ? defaultTheme : storedTheme - if (targetTheme) { - navbar.setAttribute('data-bs-theme', defaultTheme) + // update semi-transparent background color of navbar + const rgb = parseRGB(color) + if (rgb) { + navbar.style.backgroundColor = `rgba(${rgb.r},${rgb.g},${rgb.b},.4)` + } +} + +function isLightColor (section, color) { + if (section.dataset.bsTheme === 'light') { + return true + } + + if (section.dataset.bsTheme === 'dark') { + return false + } + + // parse RGB color of the section backgroiund + const rgb = parseRGB(color) + if (!rgb) return true // Default to light if can't parse + + // calculate relative luminance + const luminance = calculateLuminance(rgb.r, rgb.g, rgb.b) + + // return true if light (luminance > 0.5) + return luminance > 0.5 +} + +function parseRGB (color) { + const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/) + if (match) { + return { + r: parseInt(match[1]), + g: parseInt(match[2]), + b: parseInt(match[3]) + } + } + return null +} + +function calculateLuminance (r, g, b) { + // convert RGB to relative luminance using sRGB formula + const [rs, gs, bs] = [r, g, b].map(c => { + c = c / 255 + return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4) + }) + + return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs +} + +function updateNavbar () { + if (navbar.dataset.transparent) { + updateNavbarColor() + } else { + let storedTheme + if (typeof getLocalStorage === "function") { + storedTheme = getLocalStorage('theme', null, 'functional') + } + + if (window.scrollY > 75) { + navbar.classList.add('nav-active') + if (storedTheme) { + navbar.setAttribute('data-bs-theme', storedTheme) + } + } else { + navbar.classList.remove('nav-active') + const defaultTheme = navbar.getAttribute('data-bs-overlay') + + const targetTheme = defaultTheme ? defaultTheme : storedTheme + if (targetTheme) { + navbar.setAttribute('data-bs-theme', defaultTheme) + } } } } @@ -33,28 +170,35 @@ if ((navbar !== null) && (window.performance.getEntriesByType)) { } if (navbar !== null && togglers !== null) { + // initialize and update the navbar on load, on resize, and on scroll + document.addEventListener('DOMContentLoaded', () => { fixed && updateNavbar() }) + document.addEventListener('resize', () => fixed && updateNavbar()) + document.addEventListener('scroll', () => fixed && updateNavbar()) + // observe state changes to the site's color mode const html = document.querySelector('html') const config = { attributes: true, attributeFilter: ['data-bs-theme'] } - const Observer = new MutationObserver((mutationrecords) => { - fixed && updateNavbar() + const Observer = new MutationObserver(() => { + if (fixed) { + // wait for the theme animation to finish + sleep(600).then(() => { + updateNavbar() + }) + } }) Observer.observe(html, config) // initialize background color - const color = (navbar.getAttribute('data-navbar-color') || 'body') - const bg = colorsBG.includes(color) ? `var(--bs-${color}-bg)` : `var(--bs-navbar-color-${color})` - navbar.style.setProperty('--bs-navbar-expanded-color', bg) - - // set the navbar background color to opaque when scrolling past a breakpoint - window.onscroll = () => { - fixed && updateNavbar() + if (!navbar.dataset.transparent) { + const color = (navbar.getAttribute('data-navbar-color') || 'body') + const bg = colorsBG.includes(color) ? `var(--bs-${color}-bg)` : `var(--bs-navbar-color-${color})` + navbar.style.setProperty('--bs-navbar-expanded-color', bg) } - // set the navbar background color to opaque when expanded + // update the navbar background color when expanded for (let i = 0; i < togglers.length; ++i) { togglers[i].onclick = () => { navbar.classList.toggle('navbar-expanded') diff --git a/assets/scss/components/_navbar.scss b/assets/scss/components/_navbar.scss index b4a0130f..ad667b6d 100644 --- a/assets/scss/components/_navbar.scss +++ b/assets/scss/components/_navbar.scss @@ -95,6 +95,11 @@ } } +.navbar[data-transparent="true"] { + backdrop-filter: blur(10px); + transition: all 0.3s ease; +} + .nav-active, .navbar-expanded { background-color: var(--bs-navbar-expanded-color); border-bottom: 1px solid var(--bs-secondary-bg); @@ -393,6 +398,30 @@ border: 1px solid var(--bs-border-color) !important; } +.d-none-main-light, .d-none-inline-main-light { + display: none !important; +} + +.d-none-main-dark { + display: block !important; +} + +.d-none-inline-main-dark { + display: inline !important; +} + +[data-bs-main-theme="dark"] .d-none-main-light { + display: block !important; +} + +[data-bs-main-theme="dark"] .d-none-inline-main-light { + display: inline !important; +} + +[data-bs-main-theme="dark"] .d-none-main-dark, [data-bs-main-theme="dark"] .d-none-inline-main-dark { + display: none !important; +} + .inline-menu li { display: inline-block; padding: 0.5rem; diff --git a/config/_default/params.toml b/config/_default/params.toml index d12cd291..ac1e8be8 100644 --- a/config/_default/params.toml +++ b/config/_default/params.toml @@ -96,7 +96,7 @@ overlay = false overlayMode = "dark" horizontal = false - offset = "5.5rem" + offset = "5.7rem" breadcrumb = true toc = true sidebar = true diff --git a/data/structures/navbar.yml b/data/structures/navbar.yml index 91d6d101..7359ad0a 100644 --- a/data/structures/navbar.yml +++ b/data/structures/navbar.yml @@ -23,7 +23,6 @@ arguments: comment: Name of the menu configuration. breakpoint: release: v1.0.0 - style: type: select optional: true @@ -98,6 +97,17 @@ arguments: navbar searches for images having a matching color-mode suffix such as `-light` or `-dark`. release: v1.15.0 + transparent: + type: bool + default: false + optional: true + comment: >- + Flag indicating the navbar should be transparent. When set, the navbar + uses the current section's background color and applies a semi- + transparent blur filter. The site's body color is used as fallback. If + set, the color mode of the current section is used too. When no mode is + set, the navbar applies a mode with the best contrast. + release: v1.19.0 # deprecated arguments size: type: select diff --git a/exampleSite/config/_default/params.toml b/exampleSite/config/_default/params.toml index 2c54fb6e..f87e48eb 100644 --- a/exampleSite/config/_default/params.toml +++ b/exampleSite/config/_default/params.toml @@ -48,9 +48,10 @@ fixed = true overlay = false overlayMode = "dark" + transparent = false horizontal = false - offset = "5.5rem" - offsetXS = "5.5rem" + offset = "5.7rem" + offsetXS = "5.7rem" breadcrumb = true toc = true sidebar = true diff --git a/exampleSite/hugo_stats.json b/exampleSite/hugo_stats.json index a481ec0f..2eb9f006 100644 --- a/exampleSite/hugo_stats.json +++ b/exampleSite/hugo_stats.json @@ -29,6 +29,7 @@ "label", "li", "link", + "main", "mark", "math", "meta", @@ -212,6 +213,8 @@ "d-none", "d-none-dark", "d-none-light", + "d-none-main-dark", + "d-none-main-light", "d-sm-block", "d-sm-none", "data-table", @@ -676,11 +679,16 @@ "docs", "documentation", "dropdown-nav-0", - "dropdown-panel-1bc770c2a4522018a5d28ed335c8c66a", - "dropdown-panel-478805c6da766d19cb8529367880b5a0", - "dropdown-panel-6057bbdbb0f1e92046ca4cc546207938", - "dropdown-panel-7e3455997fcf483934b47b18978b5d36", - "dropdown-panel-dfd961725b28810e82ac3e47be3eb004", + "dropdown-panel-2b8de01298f17bfa4ef2f41a8b2c6435", + "dropdown-panel-2e5b2c8f1152552a22527c49478174ee", + "dropdown-panel-3a79089ba962a873cbebd26623a0ad40", + "dropdown-panel-3dd6f56b00612d3de3b125c1c360f592", + "dropdown-panel-a0bd455780b20f1f2bfda0050bfd2392", + "dropdown-panel-abfe30d02c0a523c8ec3648215b114b7", + "dropdown-panel-bbe2b50a448d584df2e182f25830b5af", + "dropdown-panel-cc10046cc5429efe0f01f5384daf9c94", + "dropdown-panel-f2d8c9c259691049ab9c2907caab7578", + "dropdown-panel-fc14dc77778d6aa0862cf6dc8cbfbd1d", "eerste-artikel", "elements-type", "entity-relationship-diagram", @@ -701,11 +709,16 @@ "fab-whatsapp", "fab-x-twitter", "faq", - "faq-72146768cde859a2886890cd67da8cc3", - "faq-72146768cde859a2886890cd67da8cc3-heading-faq-72146768cde859a2886890cd67da8cc3", - "faq-72146768cde859a2886890cd67da8cc3-item-0", - "faq-72146768cde859a2886890cd67da8cc3-item-1", - "faq-72146768cde859a2886890cd67da8cc3-item-2", + "faq-95cc6eedb4627fb738a7de6f99d7ad1b", + "faq-95cc6eedb4627fb738a7de6f99d7ad1b-heading-faq-95cc6eedb4627fb738a7de6f99d7ad1b", + "faq-95cc6eedb4627fb738a7de6f99d7ad1b-item-0", + "faq-95cc6eedb4627fb738a7de6f99d7ad1b-item-1", + "faq-95cc6eedb4627fb738a7de6f99d7ad1b-item-2", + "faq-c581a6286bf17cf9854fa7889afec9e6", + "faq-c581a6286bf17cf9854fa7889afec9e6-heading-faq-c581a6286bf17cf9854fa7889afec9e6", + "faq-c581a6286bf17cf9854fa7889afec9e6-item-0", + "faq-c581a6286bf17cf9854fa7889afec9e6-item-1", + "faq-c581a6286bf17cf9854fa7889afec9e6-item-2", "fas-1", "fas-2", "fas-3", @@ -801,11 +814,16 @@ "nav-0-btn-1", "nav-0-btn-2", "nav-nav-0", - "nav-panel-1bc770c2a4522018a5d28ed335c8c66a", - "nav-panel-478805c6da766d19cb8529367880b5a0", - "nav-panel-6057bbdbb0f1e92046ca4cc546207938", - "nav-panel-7e3455997fcf483934b47b18978b5d36", - "nav-panel-dfd961725b28810e82ac3e47be3eb004", + "nav-panel-2b8de01298f17bfa4ef2f41a8b2c6435", + "nav-panel-2e5b2c8f1152552a22527c49478174ee", + "nav-panel-3a79089ba962a873cbebd26623a0ad40", + "nav-panel-3dd6f56b00612d3de3b125c1c360f592", + "nav-panel-a0bd455780b20f1f2bfda0050bfd2392", + "nav-panel-abfe30d02c0a523c8ec3648215b114b7", + "nav-panel-bbe2b50a448d584df2e182f25830b5af", + "nav-panel-cc10046cc5429efe0f01f5384daf9c94", + "nav-panel-f2d8c9c259691049ab9c2907caab7578", + "nav-panel-fc14dc77778d6aa0862cf6dc8cbfbd1d", "navbar", "navbar-0-collapse", "navbar-mode", @@ -815,36 +833,66 @@ "notification", "over-mij", "overview", - "panel-1bc770c2a4522018a5d28ed335c8c66a-0", - "panel-1bc770c2a4522018a5d28ed335c8c66a-1", - "panel-1bc770c2a4522018a5d28ed335c8c66a-2", - "panel-1bc770c2a4522018a5d28ed335c8c66a-btn-0", - "panel-1bc770c2a4522018a5d28ed335c8c66a-btn-1", - "panel-1bc770c2a4522018a5d28ed335c8c66a-btn-2", - "panel-478805c6da766d19cb8529367880b5a0-0", - "panel-478805c6da766d19cb8529367880b5a0-1", - "panel-478805c6da766d19cb8529367880b5a0-2", - "panel-478805c6da766d19cb8529367880b5a0-btn-0", - "panel-478805c6da766d19cb8529367880b5a0-btn-1", - "panel-478805c6da766d19cb8529367880b5a0-btn-2", - "panel-6057bbdbb0f1e92046ca4cc546207938-0", - "panel-6057bbdbb0f1e92046ca4cc546207938-1", - "panel-6057bbdbb0f1e92046ca4cc546207938-2", - "panel-6057bbdbb0f1e92046ca4cc546207938-btn-0", - "panel-6057bbdbb0f1e92046ca4cc546207938-btn-1", - "panel-6057bbdbb0f1e92046ca4cc546207938-btn-2", - "panel-7e3455997fcf483934b47b18978b5d36-0", - "panel-7e3455997fcf483934b47b18978b5d36-1", - "panel-7e3455997fcf483934b47b18978b5d36-2", - "panel-7e3455997fcf483934b47b18978b5d36-btn-0", - "panel-7e3455997fcf483934b47b18978b5d36-btn-1", - "panel-7e3455997fcf483934b47b18978b5d36-btn-2", - "panel-dfd961725b28810e82ac3e47be3eb004-0", - "panel-dfd961725b28810e82ac3e47be3eb004-1", - "panel-dfd961725b28810e82ac3e47be3eb004-2", - "panel-dfd961725b28810e82ac3e47be3eb004-btn-0", - "panel-dfd961725b28810e82ac3e47be3eb004-btn-1", - "panel-dfd961725b28810e82ac3e47be3eb004-btn-2", + "panel-2b8de01298f17bfa4ef2f41a8b2c6435-0", + "panel-2b8de01298f17bfa4ef2f41a8b2c6435-1", + "panel-2b8de01298f17bfa4ef2f41a8b2c6435-2", + "panel-2b8de01298f17bfa4ef2f41a8b2c6435-btn-0", + "panel-2b8de01298f17bfa4ef2f41a8b2c6435-btn-1", + "panel-2b8de01298f17bfa4ef2f41a8b2c6435-btn-2", + "panel-2e5b2c8f1152552a22527c49478174ee-0", + "panel-2e5b2c8f1152552a22527c49478174ee-1", + "panel-2e5b2c8f1152552a22527c49478174ee-2", + "panel-2e5b2c8f1152552a22527c49478174ee-btn-0", + "panel-2e5b2c8f1152552a22527c49478174ee-btn-1", + "panel-2e5b2c8f1152552a22527c49478174ee-btn-2", + "panel-3a79089ba962a873cbebd26623a0ad40-0", + "panel-3a79089ba962a873cbebd26623a0ad40-1", + "panel-3a79089ba962a873cbebd26623a0ad40-2", + "panel-3a79089ba962a873cbebd26623a0ad40-btn-0", + "panel-3a79089ba962a873cbebd26623a0ad40-btn-1", + "panel-3a79089ba962a873cbebd26623a0ad40-btn-2", + "panel-3dd6f56b00612d3de3b125c1c360f592-0", + "panel-3dd6f56b00612d3de3b125c1c360f592-1", + "panel-3dd6f56b00612d3de3b125c1c360f592-2", + "panel-3dd6f56b00612d3de3b125c1c360f592-btn-0", + "panel-3dd6f56b00612d3de3b125c1c360f592-btn-1", + "panel-3dd6f56b00612d3de3b125c1c360f592-btn-2", + "panel-a0bd455780b20f1f2bfda0050bfd2392-0", + "panel-a0bd455780b20f1f2bfda0050bfd2392-1", + "panel-a0bd455780b20f1f2bfda0050bfd2392-2", + "panel-a0bd455780b20f1f2bfda0050bfd2392-btn-0", + "panel-a0bd455780b20f1f2bfda0050bfd2392-btn-1", + "panel-a0bd455780b20f1f2bfda0050bfd2392-btn-2", + "panel-abfe30d02c0a523c8ec3648215b114b7-0", + "panel-abfe30d02c0a523c8ec3648215b114b7-1", + "panel-abfe30d02c0a523c8ec3648215b114b7-2", + "panel-abfe30d02c0a523c8ec3648215b114b7-btn-0", + "panel-abfe30d02c0a523c8ec3648215b114b7-btn-1", + "panel-abfe30d02c0a523c8ec3648215b114b7-btn-2", + "panel-bbe2b50a448d584df2e182f25830b5af-0", + "panel-bbe2b50a448d584df2e182f25830b5af-1", + "panel-bbe2b50a448d584df2e182f25830b5af-2", + "panel-bbe2b50a448d584df2e182f25830b5af-btn-0", + "panel-bbe2b50a448d584df2e182f25830b5af-btn-1", + "panel-bbe2b50a448d584df2e182f25830b5af-btn-2", + "panel-cc10046cc5429efe0f01f5384daf9c94-0", + "panel-cc10046cc5429efe0f01f5384daf9c94-1", + "panel-cc10046cc5429efe0f01f5384daf9c94-2", + "panel-cc10046cc5429efe0f01f5384daf9c94-btn-0", + "panel-cc10046cc5429efe0f01f5384daf9c94-btn-1", + "panel-cc10046cc5429efe0f01f5384daf9c94-btn-2", + "panel-f2d8c9c259691049ab9c2907caab7578-0", + "panel-f2d8c9c259691049ab9c2907caab7578-1", + "panel-f2d8c9c259691049ab9c2907caab7578-2", + "panel-f2d8c9c259691049ab9c2907caab7578-btn-0", + "panel-f2d8c9c259691049ab9c2907caab7578-btn-1", + "panel-f2d8c9c259691049ab9c2907caab7578-btn-2", + "panel-fc14dc77778d6aa0862cf6dc8cbfbd1d-0", + "panel-fc14dc77778d6aa0862cf6dc8cbfbd1d-1", + "panel-fc14dc77778d6aa0862cf6dc8cbfbd1d-2", + "panel-fc14dc77778d6aa0862cf6dc8cbfbd1d-btn-0", + "panel-fc14dc77778d6aa0862cf6dc8cbfbd1d-btn-1", + "panel-fc14dc77778d6aa0862cf6dc8cbfbd1d-btn-2", "panels", "persona", "pie-chart", diff --git a/layouts/_partials/assets/helpers/navbar-mode.html b/layouts/_partials/assets/helpers/navbar-mode.html index b0615626..fc6d4623 100644 --- a/layouts/_partials/assets/helpers/navbar-mode.html +++ b/layouts/_partials/assets/helpers/navbar-mode.html @@ -9,11 +9,11 @@