124 lines
4.2 KiB
JavaScript
124 lines
4.2 KiB
JavaScript
/**
|
|
* Chrome Tint
|
|
* -
|
|
* Tint the browser chrome (status bar + home indicator area) to match
|
|
* the site header / footer when those elements are in view.
|
|
*
|
|
* Implementation notes — what each browser honors:
|
|
*
|
|
* - Chrome / older iOS Safari (≤18):
|
|
* <meta name="theme-color"> drives the top status bar tint. We
|
|
* update it dynamically. (Ignored by iOS 26+, kept for compatibility.)
|
|
*
|
|
* - iOS 26+ Safari:
|
|
* The <meta name="theme-color"> tag is ignored. WebKit instead
|
|
* samples the <body> background-color via a live observer, and
|
|
* tints the chrome to match. We set body.style.backgroundColor
|
|
* dynamically to the same color the meta tag would carry.
|
|
*
|
|
* - Bottom of the 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. (iOS 26 only samples ONE color for
|
|
* the entire chrome, so the bottom of the screen will share the
|
|
* same color as the top — we don't get a true top/bottom split on
|
|
* iOS 26. The CSS pseudo-element still serves as a no-op fallback
|
|
* for browsers that do support per-region tinting.)
|
|
*
|
|
* Colors:
|
|
* - HEADER_COLOR (#032F46) is the darker end of the .site-header
|
|
* gradient.
|
|
* - FOOTER_COLOR (#102C45) is the sRGB equivalent of the
|
|
* --color-cwc-blue-02 token used by .site-footer.
|
|
*
|
|
* "In view" = any pixel of the element overlaps the viewport.
|
|
*
|
|
* No-ops if neither the header nor footer is found, or if
|
|
* IntersectionObserver isn't supported.
|
|
*/
|
|
|
|
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 setThemeColor(color) {
|
|
const meta = ensureThemeColorMeta();
|
|
if (color == null) {
|
|
meta.removeAttribute('content');
|
|
} else {
|
|
meta.setAttribute('content', color);
|
|
}
|
|
}
|
|
|
|
function setBodyBackground(color) {
|
|
// For iOS 26+: WebKit's live observer on <body> background-color
|
|
// drives the chrome tint. Removing the inline style lets any CSS
|
|
// background (or the default transparent) show through.
|
|
if (color == null) {
|
|
document.body.style.removeProperty('background-color');
|
|
} else {
|
|
document.body.style.backgroundColor = color;
|
|
}
|
|
}
|
|
|
|
function setBottomTintClass(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.
|
|
if (headerInView) {
|
|
setThemeColor(HEADER_COLOR);
|
|
setBodyBackground(HEADER_COLOR);
|
|
} else {
|
|
setThemeColor(null);
|
|
setBodyBackground(null);
|
|
}
|
|
// Bottom tint is the footer color ONLY when the footer is in view.
|
|
setBottomTintClass(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;
|