/** * 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 } }