feat(web/m7-363): theme-color meta follows resolved theme
This commit is contained in:
@@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
} from '$lib/player/store.svelte';
|
} from '$lib/player/store.svelte';
|
||||||
import { useMediaSession } from '$lib/player/mediaSession.svelte';
|
import { useMediaSession } from '$lib/player/mediaSession.svelte';
|
||||||
import { useEventsDispatcher } from '$lib/player/events.svelte';
|
import { useEventsDispatcher } from '$lib/player/events.svelte';
|
||||||
|
import { applyMetaThemeColor } from '$lib/theme/applyMetaThemeColor.svelte';
|
||||||
|
|
||||||
let { children } = $props<{ children: import('svelte').Snippet }>();
|
let { children } = $props<{ children: import('svelte').Snippet }>();
|
||||||
let audioEl: HTMLAudioElement | undefined = $state();
|
let audioEl: HTMLAudioElement | undefined = $state();
|
||||||
@@ -66,6 +67,7 @@
|
|||||||
|
|
||||||
useMediaSession();
|
useMediaSession();
|
||||||
useEventsDispatcher();
|
useEventsDispatcher();
|
||||||
|
applyMetaThemeColor();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<audio
|
<audio
|
||||||
|
|||||||
Reference in New Issue
Block a user