Files
Emailsorter/client/src/hooks/useTheme.ts
ANDJ 6da8ce1cbd huhuih
hzgjuigik
2026-01-27 21:06:48 +01:00

187 lines
6.4 KiB
TypeScript

/**
* Theme Hook
* Handles dark mode detection and Dark Reader compatibility
* Uses MutationObserver for efficient Dark Reader detection
*/
import { useEffect, useState } from 'react'
export function useTheme() {
const [isDark, setIsDark] = useState(false)
const [hasDarkReader, setHasDarkReader] = useState(false)
useEffect(() => {
const html = document.documentElement
// Helper function to apply/remove dark mode
const applyDarkMode = (shouldBeDark: boolean) => {
setIsDark(shouldBeDark)
if (shouldBeDark) {
html.classList.add('dark')
html.setAttribute('data-theme', 'dark')
} else {
html.classList.remove('dark')
html.setAttribute('data-theme', 'light')
}
}
// Enhanced Dark Reader detection with multiple methods
const detectDarkReader = (): boolean => {
// Method 1: Check for Dark Reader data attributes on html element
const hasDarkReaderAttributes =
html.hasAttribute('data-darkreader-mode') ||
html.hasAttribute('data-darkreader-scheme') ||
html.hasAttribute('data-darkreader-policy')
// Method 2: Check for Dark Reader stylesheet or meta tags
const hasDarkReaderMeta =
document.querySelector('meta[name="darkreader"]') !== null ||
document.querySelector('style[data-darkreader]') !== null
// Method 3: Check computed styles for filter/invert (Dark Reader uses CSS filters)
const computedStyle = window.getComputedStyle(html)
const hasFilter = computedStyle.filter && computedStyle.filter !== 'none'
const hasInvert = computedStyle.filter?.includes('invert') ||
computedStyle.filter?.includes('brightness')
// Method 4: Check for Dark Reader's characteristic background color
// Dark Reader often sets a specific dark background
const bgColor = computedStyle.backgroundColor
const isDarkReaderBg = bgColor === 'rgb(24, 26, 27)' ||
bgColor === 'rgb(18, 18, 18)' ||
(window.matchMedia('(prefers-color-scheme: dark)').matches &&
bgColor !== 'rgba(0, 0, 0, 0)' &&
bgColor !== 'transparent' &&
!html.classList.contains('dark'))
// Method 5: Check for Dark Reader injected styles
const styleSheets = Array.from(document.styleSheets)
const hasDarkReaderStylesheet = styleSheets.some(sheet => {
try {
const href = sheet.href || ''
return href.includes('darkreader') ||
(sheet.ownerNode as Element)?.getAttribute('data-darkreader') !== null
} catch {
return false
}
})
return hasDarkReaderAttributes ||
hasDarkReaderMeta ||
(hasFilter && hasInvert) ||
isDarkReaderBg ||
hasDarkReaderStylesheet
}
// Check system preference
const checkSystemPreference = (): boolean => {
return window.matchMedia('(prefers-color-scheme: dark)').matches
}
// Update theme based on current state
const updateTheme = () => {
const darkReaderDetected = detectDarkReader()
const systemPrefersDark = checkSystemPreference()
setHasDarkReader(darkReaderDetected)
// Only apply dark mode if system prefers it AND Dark Reader is not active
if (systemPrefersDark && !darkReaderDetected) {
applyDarkMode(true)
} else {
applyDarkMode(false)
}
}
// Initial check
updateTheme()
// Listen for system preference changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleSystemPreferenceChange = (e: MediaQueryListEvent) => {
updateTheme()
}
// Modern browsers
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleSystemPreferenceChange)
} else {
// Fallback for older browsers
mediaQuery.addListener(handleSystemPreferenceChange)
}
// MutationObserver for Dark Reader attribute changes (more efficient than setInterval)
const observer = new MutationObserver((mutations) => {
let shouldUpdate = false
mutations.forEach((mutation) => {
// Check if Dark Reader attributes were added/removed
if (mutation.type === 'attributes') {
const attrName = mutation.attributeName
if (attrName?.startsWith('data-darkreader') ||
attrName === 'class' ||
attrName === 'data-theme') {
shouldUpdate = true
}
}
// Check if Dark Reader elements were added/removed
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const el = node as Element
if (el.hasAttribute?.('data-darkreader') ||
el.tagName === 'META' && el.getAttribute('name') === 'darkreader' ||
el.tagName === 'STYLE' && el.hasAttribute('data-darkreader')) {
shouldUpdate = true
}
}
})
}
})
if (shouldUpdate) {
updateTheme()
}
})
// Observe html element for Dark Reader attribute changes
observer.observe(html, {
attributes: true,
attributeFilter: ['data-darkreader-mode', 'data-darkreader-scheme', 'data-darkreader-policy', 'class', 'data-theme'],
childList: true,
subtree: false
})
// Also observe document head for Dark Reader meta/stylesheets
if (document.head) {
observer.observe(document.head, {
childList: true,
subtree: false
})
}
// Fallback: Periodic check (reduced frequency, only as safety net)
// This catches edge cases where MutationObserver might miss something
const fallbackInterval = setInterval(() => {
const currentDarkReader = detectDarkReader()
if (currentDarkReader !== hasDarkReader) {
updateTheme()
}
}, 5000) // Check every 5 seconds (reduced from 2 seconds)
return () => {
// Cleanup
if (mediaQuery.removeEventListener) {
mediaQuery.removeEventListener('change', handleSystemPreferenceChange)
} else {
mediaQuery.removeListener(handleSystemPreferenceChange)
}
observer.disconnect()
clearInterval(fallbackInterval)
}
}, [])
return { isDark, hasDarkReader }
}