✨ feature: Implement dynamic browser chrome tinting for header and footer visibility
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Chrome Tint
|
||||
* -
|
||||
* Tint the browser chrome (status bar + home indicator area) to match
|
||||
* the site header / footer when those elements are in view.
|
||||
*
|
||||
* - top of screen (status bar / notch / Dynamic Island):
|
||||
* controlled by the <meta name="theme-color"> tag. iOS Safari
|
||||
* picks up runtime updates.
|
||||
*
|
||||
* - bottom of screen (home indicator area, behind the address bar):
|
||||
* a fixed pseudo-element filled with `env(safe-area-inset-bottom)`
|
||||
* takes its background from a body class that this module toggles
|
||||
* when the footer is in view.
|
||||
*
|
||||
* The header color (#032F46) is the darker end of the .site-header
|
||||
* gradient. The footer color (#102C45) is the sRGB equivalent of
|
||||
* the `--color-cwc-blue-02` token used by .site-footer. If those
|
||||
* colors change in source, update HEADER_COLOR / FOOTER_COLOR below.
|
||||
*
|
||||
* - "In view" = any pixel of the element overlaps the viewport.
|
||||
*
|
||||
* No-ops if either the header or footer is missing, or if
|
||||
* IntersectionObserver isn't supported (older browsers).
|
||||
*/
|
||||
|
||||
const HEADER_COLOR = '#032F46';
|
||||
const FOOTER_COLOR = '#102C45';
|
||||
const THEME_META_NAME = 'theme-color';
|
||||
const FOOTER_IN_VIEW_CLASS = 'footer-in-view';
|
||||
|
||||
function ensureThemeColorMeta() {
|
||||
let meta = document.head.querySelector(`meta[name="${THEME_META_NAME}"]`);
|
||||
if (!meta) {
|
||||
meta = document.createElement('meta');
|
||||
meta.setAttribute('name', THEME_META_NAME);
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
return meta;
|
||||
}
|
||||
|
||||
function setTopTint(color) {
|
||||
const meta = ensureThemeColorMeta();
|
||||
if (color == null) {
|
||||
// Clear: iOS Safari falls back to the page background.
|
||||
meta.removeAttribute('content');
|
||||
} else {
|
||||
meta.setAttribute('content', color);
|
||||
}
|
||||
}
|
||||
|
||||
function setBottomTint(active) {
|
||||
document.body.classList.toggle(FOOTER_IN_VIEW_CLASS, active);
|
||||
}
|
||||
|
||||
function initChromeTint() {
|
||||
if (typeof IntersectionObserver === 'undefined') return;
|
||||
|
||||
const header = document.querySelector('.site-header');
|
||||
const footer = document.querySelector('.site-footer');
|
||||
if (!header && !footer) return;
|
||||
|
||||
// Top tint state. Truthy means the header is currently in view.
|
||||
let headerInView = false;
|
||||
// Bottom tint state. Truthy means the footer is currently in view.
|
||||
let footerInView = false;
|
||||
|
||||
const apply = () => {
|
||||
// Top tint is the header color ONLY when the header is in view.
|
||||
// When only the footer is in view, the top tint is cleared (the
|
||||
// user only asked for header-color at the top).
|
||||
if (headerInView) {
|
||||
setTopTint(HEADER_COLOR);
|
||||
} else {
|
||||
setTopTint(null);
|
||||
}
|
||||
// Bottom tint is the footer color ONLY when the footer is in view.
|
||||
setBottomTint(footerInView);
|
||||
};
|
||||
|
||||
const buildObserver = (el, onChange) => {
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
onChange(entry.isIntersecting);
|
||||
}
|
||||
apply();
|
||||
},
|
||||
// Zero threshold + no rootMargin = "any pixel in view".
|
||||
{ threshold: 0, rootMargin: '0px' },
|
||||
);
|
||||
io.observe(el);
|
||||
return io;
|
||||
};
|
||||
|
||||
if (header) buildObserver(header, (v) => { headerInView = v; });
|
||||
if (footer) buildObserver(footer, (v) => { footerInView = v; });
|
||||
}
|
||||
|
||||
export default initChromeTint;
|
||||
@@ -9,6 +9,7 @@ import tagExternalLinks from './modules/TagExternalLinks.js';
|
||||
import Navigation from './modules/Navigation.js';
|
||||
import initRecentPostsCarousels from './modules/RecentPostsCarousel.js';
|
||||
import { initContactInfoMap } from './modules/ContactInfoMap.js';
|
||||
import initChromeTint from './modules/ChromeTint.js';
|
||||
|
||||
// Add passive event listeners
|
||||
! function (e) {
|
||||
@@ -76,6 +77,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize Contact Info Leaflet maps.
|
||||
initContactInfoMap();
|
||||
|
||||
// Tint the browser chrome (status bar + home indicator) to match
|
||||
// the site header/footer when those elements are in view.
|
||||
initChromeTint();
|
||||
|
||||
// Add Back to Top button to body
|
||||
// const backToTop = document.createElement('x-back-to-top');
|
||||
// document.body.appendChild(backToTop);
|
||||
|
||||
Reference in New Issue
Block a user