UNPKG

@medalsocial/meda

Version:

Shared Meda UI shell and runtime package.

121 lines (120 loc) 4.43 kB
'use client'; import { jsx as _jsx } from "react/jsx-runtime"; import { useCallback, useEffect, useInsertionEffect, useMemo, useState, } from 'react'; import { ThemeCtx } from './theme.js'; const STORAGE_KEY = 'theme'; const MEDIA_QUERY = '(prefers-color-scheme: dark)'; const THEMES = ['light', 'dark']; function narrow(value) { return value === 'light' || value === 'dark' || value === 'system' ? value : 'system'; } function getMediaQueryList() { /* v8 ignore next — SSR guard: window is always defined in jsdom test env */ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return undefined; return window.matchMedia(MEDIA_QUERY); } function resolveSystemTheme(mql) { if (mql) return mql.matches ? 'dark' : 'light'; /* v8 ignore next — null-safe branch: getMediaQueryList() always returns an MQL in jsdom */ return getMediaQueryList()?.matches ? 'dark' : 'light'; } function getStoredTheme() { /* v8 ignore next — SSR guard: window is always defined in jsdom test env */ if (typeof window === 'undefined') return 'system'; try { return narrow(window.localStorage.getItem(STORAGE_KEY)); } catch { return 'system'; } } function applyTheme(theme, resolvedTheme) { /* v8 ignore next — SSR guard: document is always defined in jsdom test env */ if (typeof document === 'undefined') return; const root = document.documentElement; const applied = theme === 'system' ? resolvedTheme : theme; root.classList.remove(...THEMES); root.classList.add(applied); root.style.colorScheme = applied; } function getClientThemeSnapshot() { const theme = getStoredTheme(); return { theme, resolvedTheme: theme === 'system' ? resolveSystemTheme() : theme, }; } function subscribeToSystemTheme(mql, listener) { if (!mql) return () => undefined; if (typeof mql.addEventListener === 'function') { mql.addEventListener('change', listener); return () => mql.removeEventListener('change', listener); } if (typeof mql.addListener === 'function') { mql.addListener(listener); return () => mql.removeListener?.(listener); } return () => undefined; } export function NextThemesAdapter({ children }) { const [theme, setThemeState] = useState('system'); const [resolvedTheme, setResolvedTheme] = useState('light'); const [hydrated, setHydrated] = useState(false); const setTheme = useCallback((next) => { setThemeState(next); const resolved = next === 'system' ? resolveSystemTheme() : next; setResolvedTheme(resolved); applyTheme(next, resolved); try { /* v8 ignore next — SSR guard: window is always defined in jsdom test env */ if (typeof window !== 'undefined') window.localStorage.setItem(STORAGE_KEY, next); } catch { // Ignore unavailable storage, matching next-themes' best-effort behavior. } }, []); useInsertionEffect(() => { const snapshot = getClientThemeSnapshot(); applyTheme(snapshot.theme, snapshot.resolvedTheme); }, []); useEffect(() => { const stored = getClientThemeSnapshot(); setThemeState(stored.theme); setResolvedTheme(stored.resolvedTheme); setHydrated(true); applyTheme(stored.theme, stored.resolvedTheme); }, []); useEffect(() => { if (!hydrated) return; const mql = getMediaQueryList(); const sync = (_event) => { const resolved = theme === 'system' ? resolveSystemTheme(mql) : theme; setResolvedTheme(resolved); applyTheme(theme, resolved); }; sync(); return subscribeToSystemTheme(mql, sync); }, [hydrated, theme]); useEffect(() => { const handleStorage = (event) => { if (event.key !== STORAGE_KEY) return; setThemeState(narrow(event.newValue)); }; window.addEventListener('storage', handleStorage); return () => window.removeEventListener('storage', handleStorage); }, []); const value = useMemo(() => ({ theme, setTheme, resolvedTheme, }), [theme, setTheme, resolvedTheme]); return _jsx(ThemeCtx.Provider, { value: value, children: children }); }