themeshift
Version:
A lightweight, customizable dark mode and theme toggler for React applications
221 lines (215 loc) • 9 kB
JavaScript
'use strict';
var React = require('react');
var framerMotion = require('framer-motion');
const lightTheme = {
name: 'light',
colors: {
background: '#ffffff',
text: '#1a1a1a',
primary: '#3b82f6',
secondary: '#64748b',
accent: '#f59e0b',
},
variables: {
'--theme-bg': '#ffffff',
'--theme-text': '#1a1a1a',
'--theme-primary': '#3b82f6',
'--theme-secondary': '#64748b',
'--theme-accent': '#f59e0b',
},
};
const darkTheme = {
name: 'dark',
colors: {
background: '#1a1a1a',
text: '#ffffff',
primary: '#60a5fa',
secondary: '#94a3b8',
accent: '#fbbf24',
},
variables: {
'--theme-bg': '#1a1a1a',
'--theme-text': '#ffffff',
'--theme-primary': '#60a5fa',
'--theme-secondary': '#94a3b8',
'--theme-accent': '#fbbf24',
},
};
const sepiaTheme = {
name: 'sepia',
colors: {
background: '#f5e6d3',
text: '#433422',
primary: '#8b4513',
secondary: '#6b4423',
accent: '#d2691e',
},
variables: {
'--theme-bg': '#f5e6d3',
'--theme-text': '#433422',
'--theme-primary': '#8b4513',
'--theme-secondary': '#6b4423',
'--theme-accent': '#d2691e',
},
};
const highContrastTheme = {
name: 'high-contrast',
colors: {
background: '#000000',
text: '#ffffff',
primary: '#ffff00',
secondary: '#00ff00',
accent: '#ff00ff',
},
variables: {
'--theme-bg': '#000000',
'--theme-text': '#ffffff',
'--theme-primary': '#ffff00',
'--theme-secondary': '#00ff00',
'--theme-accent': '#ff00ff',
},
};
const defaultThemes = {
light: lightTheme,
dark: darkTheme,
sepia: sepiaTheme,
'high-contrast': highContrastTheme,
};
const getThemeVariables = (theme) => {
return Object.entries(theme.variables)
.map(([key, value]) => `${key}: ${value};`)
.join('\n');
};
const STORAGE_KEY = 'themeshift-current-theme';
const CUSTOM_THEMES_KEY = 'themeshift-custom-themes';
function useThemeShift(options = {}) {
const { defaultTheme = 'light', transitionDuration = 300, storage = typeof window !== 'undefined' ? window.localStorage : null, } = options;
const [currentTheme, setCurrentTheme] = React.useState(defaultTheme);
const [customThemes, setCustomThemes] = React.useState({});
const [isTransitioning, setIsTransitioning] = React.useState(false);
// Load saved theme and custom themes from storage
React.useEffect(() => {
if (storage) {
const savedTheme = storage.getItem(STORAGE_KEY);
if (savedTheme) {
setCurrentTheme(savedTheme);
}
const savedCustomThemes = storage.getItem(CUSTOM_THEMES_KEY);
if (savedCustomThemes) {
setCustomThemes(JSON.parse(savedCustomThemes));
}
}
return undefined;
}, [storage]);
// Apply theme to document
React.useEffect(() => {
const theme = customThemes[currentTheme] || defaultThemes[currentTheme];
if (theme) {
const root = document.documentElement;
root.style.cssText = getThemeVariables(theme);
if (transitionDuration > 0) {
setIsTransitioning(true);
root.style.transition = `background ${transitionDuration}ms, color ${transitionDuration}ms`;
const timer = setTimeout(() => {
setIsTransitioning(false);
root.style.transition = '';
}, transitionDuration);
return () => clearTimeout(timer);
}
}
return undefined;
}, [currentTheme, customThemes, transitionDuration]);
const setTheme = React.useCallback((themeName) => {
if (storage) {
storage.setItem(STORAGE_KEY, themeName);
}
setCurrentTheme(themeName);
}, [storage]);
const addCustomTheme = React.useCallback((theme) => {
setCustomThemes(prev => {
const updated = { ...prev, [theme.id]: theme };
if (storage) {
storage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(updated));
}
return updated;
});
}, [storage]);
const removeCustomTheme = React.useCallback((themeId) => {
setCustomThemes(prev => {
const updated = { ...prev };
delete updated[themeId];
if (storage) {
storage.setItem(CUSTOM_THEMES_KEY, JSON.stringify(updated));
}
return updated;
});
}, [storage]);
return {
currentTheme,
setTheme,
isTransitioning,
availableThemes: { ...defaultThemes, ...customThemes },
customThemes,
addCustomTheme,
removeCustomTheme,
};
}
const iconVariants = {
initial: { scale: 0, opacity: 0, rotate: -180 },
animate: { scale: 1, opacity: 1, rotate: 0 },
exit: { scale: 0, opacity: 0, rotate: 180 },
};
const ThemeIcon = ({ theme, size = 24, color = 'currentColor', }) => {
const getIcon = () => {
switch (theme) {
case 'light':
return (React.createElement(framerMotion.motion.svg, { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", variants: iconVariants },
React.createElement("circle", { cx: "12", cy: "12", r: "5" }),
React.createElement("line", { x1: "12", y1: "1", x2: "12", y2: "3" }),
React.createElement("line", { x1: "12", y1: "21", x2: "12", y2: "23" }),
React.createElement("line", { x1: "4.22", y1: "4.22", x2: "5.64", y2: "5.64" }),
React.createElement("line", { x1: "18.36", y1: "18.36", x2: "19.78", y2: "19.78" }),
React.createElement("line", { x1: "1", y1: "12", x2: "3", y2: "12" }),
React.createElement("line", { x1: "21", y1: "12", x2: "23", y2: "12" }),
React.createElement("line", { x1: "4.22", y1: "19.78", x2: "5.64", y2: "18.36" }),
React.createElement("line", { x1: "18.36", y1: "5.64", x2: "19.78", y2: "4.22" })));
case 'dark':
return (React.createElement(framerMotion.motion.svg, { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", variants: iconVariants },
React.createElement("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" })));
case 'sepia':
return (React.createElement(framerMotion.motion.svg, { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", variants: iconVariants },
React.createElement("path", { d: "M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707" }),
React.createElement("path", { d: "M12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8z" })));
case 'high-contrast':
return (React.createElement(framerMotion.motion.svg, { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", variants: iconVariants },
React.createElement("circle", { cx: "12", cy: "12", r: "10" }),
React.createElement("path", { d: "M12 2v20" }),
React.createElement("path", { d: "M12 12L2 12" })));
default:
return null;
}
};
return (React.createElement(framerMotion.AnimatePresence, { mode: "wait" },
React.createElement(framerMotion.motion.div, { key: theme, initial: "initial", animate: "animate", exit: "exit", transition: { duration: 0.3 } }, getIcon())));
};
const ThemeToggleButton = ({ currentTheme, onClick, size = 24, color = 'currentColor' }) => {
return (React.createElement("button", { onClick: onClick, style: {
background: 'none',
border: 'none',
padding: 8,
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
}, "aria-label": `Toggle ${currentTheme} theme` },
React.createElement(ThemeIcon, { theme: currentTheme, size: size, color: color })));
};
exports.ThemeIcon = ThemeIcon;
exports.ThemeToggleButton = ThemeToggleButton;
exports.darkTheme = darkTheme;
exports.defaultThemes = defaultThemes;
exports.highContrastTheme = highContrastTheme;
exports.lightTheme = lightTheme;
exports.sepiaTheme = sepiaTheme;
exports.useThemeShift = useThemeShift;
//# sourceMappingURL=index.js.map