feat(web/m7-363): theme-color meta follows resolved theme

This commit is contained in:
2026-05-03 17:44:01 -04:00
parent 07b228dd4e
commit af6f006242
4 changed files with 84 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
<script lang="ts">
import { applyMetaThemeColor } from '../applyMetaThemeColor.svelte';
applyMetaThemeColor();
</script>
@@ -0,0 +1,50 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { render } from '@testing-library/svelte';
import { setTheme } from '$lib/stores/theme.svelte';
import Probe from './__test__/Probe.svelte';
describe('applyMetaThemeColor', () => {
beforeEach(() => {
document.head.innerHTML = '';
});
afterEach(() => {
document.head.innerHTML = '';
// Reset to a known baseline; setTheme persists to localStorage,
// but each test starts with head cleared anyway.
setTheme('dark');
});
it('creates a meta[name="theme-color"]:not([media]) on mount with the dark hex', () => {
setTheme('dark');
render(Probe);
const meta = document.querySelector<HTMLMetaElement>(
'meta[name="theme-color"]:not([media])'
);
expect(meta).not.toBeNull();
expect(meta!.content).toBe('#14171A');
});
it('updates the meta content when the resolved theme flips to light', async () => {
setTheme('dark');
render(Probe);
setTheme('light');
await Promise.resolve();
const meta = document.querySelector<HTMLMetaElement>(
'meta[name="theme-color"]:not([media])'
);
expect(meta).not.toBeNull();
expect(meta!.content).toBe('#F8F5EE');
});
it('reuses an existing meta tag instead of stacking duplicates', async () => {
setTheme('dark');
render(Probe);
setTheme('light');
await Promise.resolve();
setTheme('dark');
await Promise.resolve();
const metas = document.querySelectorAll('meta[name="theme-color"]:not([media])');
expect(metas.length).toBe(1);
});
});
@@ -0,0 +1,28 @@
import { resolvedTheme } from '$lib/stores/theme.svelte';
const DARK = '#14171A';
const LIGHT = '#F8F5EE';
// applyMetaThemeColor wires a Svelte 5 $effect that keeps a single
// <meta name="theme-color"> (without a media query) in sync with the
// resolved theme. The Vite build plugin injects two media-query
// variants for first paint / pre-hydration; the meta tag this helper
// manages is added on hydration and tracks the user's explicit
// Dark / Light / System choice. On a phone, the mobile address bar
// retints the moment the user changes the theme in Settings.
//
// Must be called from a component setup (uses $effect).
export function applyMetaThemeColor(): void {
$effect(() => {
const target = resolvedTheme.value === 'light' ? LIGHT : DARK;
let meta = document.querySelector<HTMLMetaElement>(
'meta[name="theme-color"]:not([media])'
);
if (!meta) {
meta = document.createElement('meta');
meta.name = 'theme-color';
document.head.appendChild(meta);
}
meta.content = target;
});
}
+2
View File
@@ -15,6 +15,7 @@
} from '$lib/player/store.svelte';
import { useMediaSession } from '$lib/player/mediaSession.svelte';
import { useEventsDispatcher } from '$lib/player/events.svelte';
import { applyMetaThemeColor } from '$lib/theme/applyMetaThemeColor.svelte';
let { children } = $props<{ children: import('svelte').Snippet }>();
let audioEl: HTMLAudioElement | undefined = $state();
@@ -66,6 +67,7 @@
useMediaSession();
useEventsDispatcher();
applyMetaThemeColor();
</script>
<audio