refactor(theme): replace themeManager singleton with lazy getThemeManager

Convert the eager themeManager singleton to a getThemeManager() lazy accessor
(+ __resetThemeManager for tests), so the persistent-store subscription is set
up on first access rather than at module load. Update the barrel and consumers
(Layout init/destroy, ThemeSwitch, story, test).
This commit is contained in:
Ilia Mashkov
2026-06-01 18:39:17 +03:00
parent 0b675635b3
commit 3dca11fea8
6 changed files with 33 additions and 9 deletions
+3 -1
View File
@@ -3,7 +3,7 @@
Application shell with providers and page wrapper Application shell with providers and page wrapper
--> -->
<script lang="ts"> <script lang="ts">
import { themeManager } from '$features/ChangeAppTheme'; import { getThemeManager } from '$features/ChangeAppTheme';
import G from '$shared/assets/G.svg'; import G from '$shared/assets/G.svg';
import { ResponsiveProvider } from '$shared/lib'; import { ResponsiveProvider } from '$shared/lib';
import { cn } from '$shared/lib'; import { cn } from '$shared/lib';
@@ -32,6 +32,8 @@ interface Props {
let { children }: Props = $props(); let { children }: Props = $props();
let fontsReady = $state(true); let fontsReady = $state(true);
const themeManager = getThemeManager();
const theme = $derived(themeManager.value); const theme = $derived(themeManager.value);
onMount(() => themeManager.init()); onMount(() => themeManager.init());
+1 -1
View File
@@ -1 +1 @@
export { themeManager } from './store/ThemeManager/ThemeManager.svelte'; export { getThemeManager } from './store/ThemeManager/ThemeManager.svelte';
@@ -194,15 +194,26 @@ class ThemeManager {
} }
} }
let _themeManager: ThemeManager | undefined;
/** /**
* Singleton theme manager instance * App-wide theme manager, created on first access.
* *
* Use throughout the app for consistent theme state. * Lazy so its persistent-store subscription isn't set up at module load.
* Call init() on mount and destroy() on unmount (see Layout).
*/ */
export const themeManager = new ThemeManager(); export function getThemeManager(): ThemeManager {
return (_themeManager ??= new ThemeManager());
}
// test-only reset, so specs don't share persisted theme state
export function __resetThemeManager() {
_themeManager?.destroy();
_themeManager = undefined;
}
/** /**
* ThemeManager class exported for testing purposes * ThemeManager class exported for testing purposes
* Use the singleton `themeManager` in application code. * Use the `getThemeManager()` accessor in application code.
*/ */
export { ThemeManager }; export { ThemeManager };
@@ -22,8 +22,9 @@ const { Story } = defineMeta({
</script> </script>
<script lang="ts"> <script lang="ts">
import { themeManager } from '$features/ChangeAppTheme'; import { getThemeManager } from '$features/ChangeAppTheme';
const themeManager = getThemeManager();
// Current theme state for display // Current theme state for display
const currentTheme = $derived(themeManager.value); const currentTheme = $derived(themeManager.value);
const themeSource = $derived(themeManager.source); const themeSource = $derived(themeManager.source);
@@ -8,10 +8,11 @@ import { IconButton } from '$shared/ui';
import MoonIcon from '@lucide/svelte/icons/moon'; import MoonIcon from '@lucide/svelte/icons/moon';
import SunIcon from '@lucide/svelte/icons/sun'; import SunIcon from '@lucide/svelte/icons/sun';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { themeManager } from '../../model'; import { getThemeManager } from '../../model';
const responsive = getContext<ResponsiveManager>('responsive'); const responsive = getContext<ResponsiveManager>('responsive');
const themeManager = getThemeManager();
const theme = $derived(themeManager.value); const theme = $derived(themeManager.value);
</script> </script>
@@ -3,16 +3,25 @@ import {
render, render,
screen, screen,
} from '@testing-library/svelte'; } from '@testing-library/svelte';
import { themeManager } from '../../model'; import { afterEach } from 'vitest';
import { getThemeManager } from '../../model';
import { __resetThemeManager } from '../../model/store/ThemeManager/ThemeManager.svelte';
import ThemeSwitch from './ThemeSwitch.svelte'; import ThemeSwitch from './ThemeSwitch.svelte';
const context = new Map([['responsive', { isMobile: false }]]); const context = new Map([['responsive', { isMobile: false }]]);
describe('ThemeSwitch', () => { describe('ThemeSwitch', () => {
let themeManager: ReturnType<typeof getThemeManager>;
beforeEach(() => { beforeEach(() => {
themeManager = getThemeManager();
themeManager.setTheme('light'); themeManager.setTheme('light');
}); });
afterEach(() => {
__resetThemeManager();
});
describe('Rendering', () => { describe('Rendering', () => {
it('renders an icon button', () => { it('renders an icon button', () => {
render(ThemeSwitch, { context }); render(ThemeSwitch, { context });