/** * Keeps 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 * 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(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 . */ 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; }