All files / src/composables useTheme.ts

72.34% Statements 34/47
58.82% Branches 20/34
90% Functions 9/10
76.74% Lines 33/43

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 90 91 92 93 94 95 96 97 98 99 100 101 102          5x   5x 5x   5x 5x 5x     1x       2x 1x     1x       7x 2x     5x       7x   7x 7x   7x       4x 4x                   1x                             3x 2x 2x     1x 1x 1x         1x 1x   1x         4x 4x 4x     101x            
import { readonly, ref } from 'vue'
 
export type ThemePreference = 'light' | 'dark' | 'system'
type ResolvedTheme = 'light' | 'dark'
 
const THEME_STORAGE_KEY = 'uiThemePreference'
 
const themePreference = ref<ThemePreference>('system')
const resolvedTheme = ref<ResolvedTheme>('light')
 
let initialized = false
let mediaQueryList: MediaQueryList | null = null
let mediaQueryListenerBound = false
 
function isValidThemePreference(value: string | null): value is ThemePreference {
  return value === 'light' || value === 'dark' || value === 'system'
}
 
function getSystemTheme(): ResolvedTheme {
  if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
    return 'light'
  }
 
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
 
function computeResolvedTheme(preference: ThemePreference): ResolvedTheme {
  if (preference === 'system') {
    return getSystemTheme()
  }
 
  return preference
}
 
function applyTheme(preference: ThemePreference = themePreference.value): void {
  Iif (typeof document === 'undefined') return
 
  const nextResolvedTheme = computeResolvedTheme(preference)
  resolvedTheme.value = nextResolvedTheme
 
  document.documentElement.setAttribute('data-bs-theme', nextResolvedTheme)
}
 
function persistPreference(preference: ThemePreference): void {
  Iif (typeof window === 'undefined') return
  window.localStorage.setItem(THEME_STORAGE_KEY, preference)
}
 
function handleSystemThemeChange(): void {
  if (themePreference.value === 'system') {
    applyTheme('system')
  }
}
 
function bindSystemThemeListener(): void {
  Eif (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return
  if (mediaQueryListenerBound) return
 
  mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)')
 
  if (typeof mediaQueryList.addEventListener === 'function') {
    mediaQueryList.addEventListener('change', handleSystemThemeChange)
  } else if (typeof mediaQueryList.addListener === 'function') {
    mediaQueryList.addListener(handleSystemThemeChange)
  }
 
  mediaQueryListenerBound = true
}
 
export function initTheme(): void {
  if (initialized) {
    applyTheme(themePreference.value)
    return
  }
 
  Eif (typeof window !== 'undefined') {
    const storedPreference = window.localStorage.getItem(THEME_STORAGE_KEY)
    Iif (isValidThemePreference(storedPreference)) {
      themePreference.value = storedPreference
    }
  }
 
  bindSystemThemeListener()
  applyTheme(themePreference.value)
 
  initialized = true
}
 
export function useTheme() {
  function setThemePreference(preference: ThemePreference): void {
    themePreference.value = preference
    persistPreference(preference)
    applyTheme(preference)
  }
 
  return {
    resolvedTheme: readonly(resolvedTheme),
    setThemePreference,
    themePreference: readonly(themePreference)
  }
}