UNPKG

@madooei/omni-themes

Version:

Universal theme library that works with any JavaScript framework. Provides perfect dark mode, system theme detection, and no-flash loading.

357 lines (352 loc) 12 kB
import { atom, computed, onMount } from 'nanostores'; // src/store.ts // src/util.ts function getSystemTheme(darkMediaQuery) { if (typeof window === "undefined") return void 0; return window.matchMedia(darkMediaQuery).matches ? "dark" : "light"; } function updateDOM(newTheme, themes, themesMap, dataAttributes, enableColorScheme, updateClassAttribute, forcedThemeFlagAttribute) { if (!newTheme || typeof window === "undefined") return; const root = document.documentElement; if (forcedThemeFlagAttribute && root.getAttribute(forcedThemeFlagAttribute) === "true") { return; } if (updateClassAttribute) { root.classList.remove(...themes); root.classList.add(newTheme); } dataAttributes.forEach((dataAttribute) => { if (dataAttribute && dataAttribute.startsWith("data-")) { root.setAttribute(dataAttribute, newTheme); } }); if (enableColorScheme) { const colorScheme = themesMap.dark?.includes(newTheme) ? "dark" : themesMap.light?.includes(newTheme) ? "light" : null; if (colorScheme) { root.style.colorScheme = colorScheme; } } } function disableAnimation() { const css = document.createElement("style"); css.appendChild( document.createTextNode(` *, *::before, *::after { -webkit-transition: none !important; -moz-transition: none !important; -o-transition: none !important; -ms-transition: none !important; transition: none !important; } `) ); document.head.appendChild(css); return () => { window.getComputedStyle(document.body); requestAnimationFrame(() => { document.head.removeChild(css); }); }; } // src/script.ts function createThemeScript({ themes, themesMap, defaultTheme, defaultLightTheme, defaultDarkTheme, dataAttributes, themeStorageKey, darkMediaQuery, enableColorScheme, enableSystem, updateClassAttribute, forcedThemeFlagAttribute }) { const applyThemeScript = (themes2 = ["light", "dark"], themesMap2 = { light: ["light"], dark: ["dark"] }, defaultTheme2 = "light", defaultLightTheme2 = "light", defaultDarkTheme2 = "dark", dataAttributes2 = [`data-theme`], themeStorageKey2 = "omni-theme", darkMediaQuery2 = "(prefers-color-scheme: dark)", enableColorScheme2 = true, enableSystem2 = true, updateClassAttribute2 = true, forcedThemeFlagAttribute2 = "data-theme-forced") => { if (typeof window === "undefined") return; const root = document.documentElement; if (forcedThemeFlagAttribute2 && root.getAttribute(forcedThemeFlagAttribute2) === "true") return; let resolvedTheme = defaultTheme2; const theme = localStorage && localStorage.getItem(themeStorageKey2); if (theme && theme !== "system") { resolvedTheme = theme; } else if (enableSystem2) { const systemTheme = window.matchMedia(darkMediaQuery2).matches ? "dark" : "light"; resolvedTheme = systemTheme === "dark" ? defaultDarkTheme2 : defaultLightTheme2; } if (updateClassAttribute2) { root.classList.remove(...themes2); root.classList.add(resolvedTheme); } if (dataAttributes2) { dataAttributes2.forEach((dataAttribute) => { if (dataAttribute && dataAttribute.startsWith("data-")) { root.setAttribute(dataAttribute, resolvedTheme); } }); } if (enableColorScheme2 && themesMap2) { if (themesMap2.dark && themesMap2.dark.includes(resolvedTheme)) { root.style.colorScheme = "dark"; } else if (themesMap2.light && themesMap2.light.includes(resolvedTheme)) { root.style.colorScheme = "light"; } } }; const scriptArgs = JSON.stringify([ themes, themesMap, defaultTheme, defaultLightTheme, defaultDarkTheme, dataAttributes, themeStorageKey, darkMediaQuery, enableColorScheme, enableSystem, updateClassAttribute, forcedThemeFlagAttribute ]).slice(1, -1); const scriptString = `(${applyThemeScript.toString()})(${scriptArgs})`; return scriptString; } function createForcedThemeScriptFactory({ themes, themesMap, dataAttributes, enableColorScheme, updateClassAttribute, forcedThemeFlagAttribute }) { return (forcedTheme) => { const applyForcedThemeScript = (forcedTheme2 = "light", themes2 = ["light", "dark"], themesMap2 = { light: ["light"], dark: ["dark"] }, dataAttributes2 = [`data-theme`], enableColorScheme2 = true, updateClassAttribute2 = true, forcedThemeFlagAttribute2 = "data-theme-forced") => { if (typeof window === "undefined") return; const root = document.documentElement; if (forcedThemeFlagAttribute2) { root.setAttribute(forcedThemeFlagAttribute2, "true"); } if (updateClassAttribute2) { root.classList.remove(...themes2); root.classList.add(forcedTheme2); } if (dataAttributes2) { dataAttributes2.forEach((dataAttribute) => { if (dataAttribute && dataAttribute.startsWith("data-")) { root.setAttribute(dataAttribute, forcedTheme2); } }); } if (enableColorScheme2 && themesMap2) { if (themesMap2.dark && themesMap2.dark.includes(forcedTheme2)) { root.style.colorScheme = "dark"; } else if (themesMap2.light && themesMap2.light.includes(forcedTheme2)) { root.style.colorScheme = "light"; } } }; const scriptArgs = JSON.stringify([ forcedTheme, themes, themesMap, dataAttributes, enableColorScheme, updateClassAttribute, forcedThemeFlagAttribute ]).slice(1, -1); const scriptString = `(${applyForcedThemeScript.toString()})(${scriptArgs})`; return scriptString; }; } // src/store.ts var isServer = typeof window === "undefined"; function createThemeStore(config) { const { themes, defaultTheme, defaultLightTheme, defaultDarkTheme, themesMap, themeStorageKey, darkMediaQuery, dataAttributes, enableColorScheme, enableSystem, updateClassAttribute, disableTransitionOnChange, forcedThemeFlagAttribute, debug } = validateAndInitializeThemeStoreConfiguration(config); const $theme = atom( defaultTheme ? defaultTheme : enableSystem ? "system" : themes[0] ); const $systemTheme = atom( getSystemTheme(darkMediaQuery) ); const $resolvedTheme = computed([$theme, $systemTheme], (theme, systemTheme) => { if (theme !== "system") return theme; if (systemTheme === "dark") return defaultDarkTheme; if (systemTheme === "light") return defaultLightTheme; return defaultTheme; }); onMount($theme, () => { if (debug) console.log("\u{1F680} theme mounted:", $theme.get()); if (isServer) return; const storedValue = localStorage.getItem(themeStorageKey); if (storedValue !== null) { $theme.set(storedValue); } const handleStorageChange = (e) => { if (e.key === themeStorageKey) { if (e.newValue !== null) $theme.set(e.newValue); } }; window.addEventListener("storage", handleStorageChange); return () => { if (debug) console.log("\u{1F680} theme unmounted"); window.removeEventListener("storage", handleStorageChange); }; }); onMount($systemTheme, () => { if (debug) console.log("\u{1F680} system theme mounted:", $systemTheme.get()); if (isServer) return; const darkModeMediaQuery = window.matchMedia(darkMediaQuery); const handleMediaQueryChange = (e) => { $systemTheme.set(e.matches ? "dark" : "light"); }; darkModeMediaQuery.addEventListener("change", handleMediaQueryChange); return () => { if (debug) console.log("\u{1F680} system theme unmounted"); darkModeMediaQuery.removeEventListener("change", handleMediaQueryChange); }; }); onMount($resolvedTheme, () => { if (debug) console.log("\u{1F680} resolved theme mounted:", $resolvedTheme.get()); return () => { if (debug) console.log("\u{1F680} resolved theme unmounted"); }; }); $theme.subscribe((theme) => { if (isServer) return; localStorage.setItem(themeStorageKey, theme); }); $resolvedTheme.subscribe((resolvedTheme) => { if (isServer) return; const enableAnimations = disableTransitionOnChange ? disableAnimation() : null; updateDOM( resolvedTheme, themes, themesMap, dataAttributes, enableColorScheme, updateClassAttribute, forcedThemeFlagAttribute ); enableAnimations?.(); }); if (debug) { $theme.listen((value) => console.log("\u{1F680} theme:", value)); $systemTheme.listen((value) => console.log("\u{1F680} system theme:", value)); $resolvedTheme.listen((value) => console.log("\u{1F680} resolved theme:", value)); } const applyThemeScriptString = createThemeScript({ themes, themesMap, defaultTheme, defaultLightTheme, defaultDarkTheme, dataAttributes, themeStorageKey, darkMediaQuery, enableColorScheme, enableSystem, updateClassAttribute, forcedThemeFlagAttribute }); const createForcedThemeScriptString = createForcedThemeScriptFactory({ themes, themesMap, dataAttributes, enableColorScheme, updateClassAttribute, forcedThemeFlagAttribute }); return { themes: enableSystem ? [...themes, "system"] : themes, $theme, $resolvedTheme, $systemTheme, setTheme: (theme) => $theme.set(theme), applyThemeScriptString, createForcedThemeScriptString }; } function validateAndInitializeThemeStoreConfiguration(config) { const { themes = [], defaultTheme, defaultLightTheme, defaultDarkTheme, themesMap, themeStorageKey = "omni-theme", darkMediaQuery = "(prefers-color-scheme: dark)", dataAttributes = ["data-theme"], enableColorScheme = true, enableSystem = true, updateClassAttribute = true, disableTransitionOnChange = false, forcedThemeFlagAttribute = "data-theme-forced", debug = false } = config; const validatedThemes = themes.length > 0 ? themes : ["light", "dark"]; const validatedDefaultTheme = defaultTheme && validatedThemes.includes(defaultTheme) ? defaultTheme : enableSystem ? "system" : validatedThemes[0]; const validatedDefaultLightTheme = defaultLightTheme && validatedThemes.includes(defaultLightTheme) ? defaultLightTheme : validatedThemes.find((t) => t.toLowerCase().includes("light")) || validatedThemes[0]; const validatedDefaultDarkTheme = defaultDarkTheme && validatedThemes.includes(defaultDarkTheme) ? defaultDarkTheme : validatedThemes.find((t) => t.toLowerCase().includes("dark")) || validatedThemes[validatedThemes.length - 1]; const validatedThemesMap = themesMap || { light: [validatedDefaultLightTheme], dark: [validatedDefaultDarkTheme] }; Object.values(validatedThemesMap).flat().forEach((theme) => { if (!validatedThemes.includes(theme)) { throw new Error( `Invalid theme "${theme}" in themesMap. Must be one of ${validatedThemes.join( ", " )}.` ); } }); if (enableSystem) { if (!validatedDefaultLightTheme || !validatedDefaultDarkTheme) { throw new Error( "If enableSystem is true, both defaultLightTheme and defaultDarkTheme must be defined" ); } if (!validatedThemesMap.light || !validatedThemesMap.dark) { throw new Error( "If enableSystem is true, themesMap must include both 'light' and 'dark' keys" ); } } return { themes: validatedThemes, defaultTheme: validatedDefaultTheme, defaultLightTheme: validatedDefaultLightTheme, defaultDarkTheme: validatedDefaultDarkTheme, themesMap: validatedThemesMap, themeStorageKey, darkMediaQuery, dataAttributes, enableColorScheme, enableSystem, updateClassAttribute, disableTransitionOnChange, forcedThemeFlagAttribute, debug }; } export { createThemeStore }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map