All files / src theme.ts

95% Statements 38/40
91.66% Branches 22/24
92.3% Functions 12/13
100% Lines 32/32

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89            16x   16x     16x         16x 16x 16x       5x     16x 16x     12x 12x 1x                 32x 32x 32x       16x 3x     16x     43x       1x       14x 14x 14x 14x       14x 14x       15x 12x 12x       10x   10x 10x            
import { useSyncExternalStore } from 'react'
import { TOKEN_KEY } from './auth'
import { API_BASE } from './useApi'
 
export type ThemeMode = 'LIGHT' | 'DARK' | 'AUTO'
 
export const THEME_MODES = ['LIGHT', 'AUTO', 'DARK'] as const satisfies readonly ThemeMode[]
 
const STORAGE_KEY = 'theme'
 
const darkQuery =
	typeof window !== 'undefined' && window.matchMedia
		? window.matchMedia('(prefers-color-scheme: dark)')
		: null
 
function readStored(): ThemeMode {
	try {
		const stored = localStorage.getItem(STORAGE_KEY)
		if (stored && (THEME_MODES as readonly string[]).includes(stored)) return stored as ThemeMode
	} catch {
		// fall back to AUTO
	}
	return 'AUTO'
}
 
let currentTheme: ThemeMode = readStored()
const listeners = new Set<() => void>()
 
function persistTheme(newTheme: ThemeMode): void {
	const token = localStorage.getItem(TOKEN_KEY)
	if (!token) return
	fetch(`${API_BASE}/users/profile`, {
		method: 'PUT',
		headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
		body: JSON.stringify({ preferences: { theme: newTheme } }),
	}).catch(() => {
	})
}
 
function updateTheme(): void {
	Iif (typeof document === 'undefined') return
	const dark = currentTheme === 'DARK' || (currentTheme === 'AUTO' && (darkQuery?.matches ?? false))
	document.documentElement.classList.toggle('dark', dark)
}
 
// while in AUTO, follow the OS theme as it changes
darkQuery?.addEventListener('change', () => {
	if (currentTheme === 'AUTO') updateTheme()
})
 
updateTheme()
 
export function getThemeMode(): ThemeMode {
	return currentTheme
}
 
export function systemPrefersDark(): boolean {
	return darkQuery?.matches ?? false
}
 
export function applyTheme(newTheme: ThemeMode): void {
	Iif (newTheme === currentTheme) return
	currentTheme = newTheme
	try {
		localStorage.setItem(STORAGE_KEY, newTheme)
	} catch {
		// ignore persistence failures — the choice still applies for this session
	}
	updateTheme()
	listeners.forEach((listener) => listener())
}
 
export function setThemeMode(newTheme: ThemeMode): void {
	if (newTheme === currentTheme) return
	applyTheme(newTheme)
	persistTheme(newTheme)
}
 
export function useThemeMode(): ThemeMode {
	return useSyncExternalStore(
		(callback) => {
			listeners.add(callback)
			return () => listeners.delete(callback)
		},
		getThemeMode,
		getThemeMode,
	)
}