mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 16:13:15 +00:00
89 lines
3.0 KiB
TypeScript
89 lines
3.0 KiB
TypeScript
/**
|
|
* Keeps <meta name="theme-color"> in step with the active theme.
|
|
*
|
|
* Chromium colours an installed desktop PWA's title bar from this meta tag in
|
|
* preference to the manifest's `theme_color` (#671). The tag is server-rendered
|
|
* from the admin `pwaThemeColor` branding key, but that is a single static
|
|
* value: it can't follow the user's light/dark choice or an installed theme
|
|
* extension, both of which live entirely client-side.
|
|
*
|
|
* So when no explicit `pwaThemeColor` is configured, the root layout mounts
|
|
* <ThemeColorSync /> which calls `enableThemeColorSync()`, and every point that
|
|
* can change the page background - the light/dark class flip and theme/skin CSS
|
|
* injection - calls `syncThemeColorMeta()`. When an admin HAS configured a
|
|
* colour, the component is never mounted, `enabled` stays false, and all of
|
|
* those calls are no-ops so the configured value stands.
|
|
*/
|
|
|
|
const META_SELECTOR = 'meta[name="theme-color"]';
|
|
/** rgba() with zero alpha, i.e. an unpainted background we can't derive a colour from. */
|
|
const TRANSPARENT = /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0\s*\)$/;
|
|
|
|
let enabled = false;
|
|
let scheduled = false;
|
|
|
|
/**
|
|
* Read the colour the page actually paints. `body` carries
|
|
* `background-color: var(--color-background)`, and getComputedStyle resolves it
|
|
* to an `rgb()`/`rgba()` string - so this works no matter how a theme authored
|
|
* the token (hex, oklch, nested var()) and needs no knowledge of theme
|
|
* internals.
|
|
*/
|
|
function readBackgroundColor(): string | null {
|
|
const el = document.body ?? document.documentElement;
|
|
if (!el) return null;
|
|
|
|
const color = getComputedStyle(el).backgroundColor;
|
|
if (!color || color === 'transparent' || TRANSPARENT.test(color)) return null;
|
|
return color;
|
|
}
|
|
|
|
function writeMeta(): void {
|
|
const color = readBackgroundColor();
|
|
if (!color) return;
|
|
|
|
let meta = document.head?.querySelector<HTMLMetaElement>(META_SELECTOR);
|
|
if (!meta) {
|
|
if (!document.head) return;
|
|
meta = document.createElement('meta');
|
|
meta.name = 'theme-color';
|
|
document.head.appendChild(meta);
|
|
}
|
|
|
|
if (meta.content !== color) meta.content = color;
|
|
}
|
|
|
|
/**
|
|
* Update the meta tag to match the current background, coalescing bursts (a
|
|
* theme switch flips the class AND re-injects CSS) into a single read on the
|
|
* next frame, after the new styles have applied.
|
|
*/
|
|
export function syncThemeColorMeta(): void {
|
|
if (!enabled || typeof document === 'undefined') return;
|
|
if (scheduled) return;
|
|
|
|
scheduled = true;
|
|
const run = () => {
|
|
scheduled = false;
|
|
writeMeta();
|
|
};
|
|
|
|
if (typeof requestAnimationFrame === 'function') {
|
|
requestAnimationFrame(run);
|
|
} else {
|
|
setTimeout(run, 0);
|
|
}
|
|
}
|
|
|
|
/** Turn syncing on and take a first reading. Called by <ThemeColorSync />. */
|
|
export function enableThemeColorSync(): void {
|
|
enabled = true;
|
|
syncThemeColorMeta();
|
|
}
|
|
|
|
/** Test seam - restores the module to its pre-enable state. */
|
|
export function resetThemeColorSyncForTests(): void {
|
|
enabled = false;
|
|
scheduled = false;
|
|
}
|