@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
JavaScript
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