UNPKG

themeshift

Version:

A lightweight, customizable dark mode and theme toggler for React applications

221 lines (215 loc) 9 kB
'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