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';
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user