Last active
January 23, 2024 14:31
-
-
Save adnanalbeda/166c60f3e64a515ed674ea6083e0fa77 to your computer and use it in GitHub Desktop.
Vite_React_Tailwind
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ******************************************************* | |
// * for tailwind, in order for this to work, * | |
// * must have this config: (darkMode: "class"). * | |
// ******************************************************* | |
import { | |
createContext, | |
useCallback, | |
useContext, | |
useEffect, | |
useMemo, | |
useState, | |
} from "react"; | |
//****************** | |
//* types | |
// Why not React.ReactNode? Because react team changed their types api recently, so I'll use my own JIC. | |
export type Node = JSX.Element | string | number | boolean | null; | |
export type Nodes = Node | Node[]; | |
export type NodesBuilder<T> = (props: T) => Nodes; | |
export type Children<T> = Nodes | NodesBuilder<T>; | |
export type ThemeValues = | |
| typeof AUTO_LIGHT | |
| typeof AUTO_DARK | |
| typeof LIGHT | |
| typeof DARK; | |
interface ThemeManagerValues { | |
theme: ThemeValues; | |
setTheme: (theme: ThemeValues) => void; | |
toggleTheme: () => void; | |
isDarkMode: boolean; | |
toggleDarkMode: () => void; | |
isAutoMode: boolean; | |
} | |
//****************** | |
//* constants | |
const _ROOT_ = document.body; | |
export const _LOCAL_STORAGE_THEME_KEY_ = "tw-theme"; | |
export const _DARK_MODE_MEDIA_QUERY_ = "(prefers-color-scheme: dark)"; | |
const AUTO_LIGHT = "auto-light"; | |
const AUTO_DARK = "auto-dark"; | |
const DARK = "dark"; | |
const LIGHT = "light"; | |
//****************** | |
//* utils | |
export const mediaMatchesDarkModeQuery = () => | |
window.matchMedia(_DARK_MODE_MEDIA_QUERY_).matches; | |
export const getTheme = () => | |
window.localStorage.getItem(_LOCAL_STORAGE_THEME_KEY_) as ThemeValues; | |
const setThemeInStorage = (value: ThemeValues) => | |
window.localStorage.setItem(_LOCAL_STORAGE_THEME_KEY_, value); | |
const applyDarkStyles = () => { | |
_ROOT_.classList.add(DARK); | |
}; | |
const removeDarkStyles = () => { | |
_ROOT_.classList.remove(DARK); | |
}; | |
export const getCurrentAutoMode = () => | |
mediaMatchesDarkModeQuery() ? AUTO_DARK : AUTO_LIGHT; | |
//****************** | |
//* hooks | |
const useEffects = (theme: ThemeValues, setTheme: (v: ThemeValues) => void) => { | |
// track dark mode media changes. | |
const [mediaIsDarkMode, setMediaIsDarkMode] = useState( | |
mediaMatchesDarkModeQuery() | |
); | |
// subscribe to dark mode media query change events, only when using auto mode. | |
useEffect(() => { | |
if (theme !== AUTO_DARK && theme !== AUTO_LIGHT) return; | |
const callback = (e: MediaQueryListEvent) => setMediaIsDarkMode(e.matches); | |
window | |
.matchMedia(_DARK_MODE_MEDIA_QUERY_) | |
.addEventListener("change", callback); | |
return () => | |
window | |
.matchMedia(_DARK_MODE_MEDIA_QUERY_) | |
.removeEventListener("change", callback); | |
}, [theme, mediaIsDarkMode]); | |
// auto update theme for auto mode. | |
useEffect(() => { | |
if (theme === DARK || theme === LIGHT) return; | |
setTheme(mediaIsDarkMode ? AUTO_DARK : AUTO_LIGHT); | |
}, [mediaIsDarkMode]); | |
// store theme value in local storage to use later on refresh. | |
useEffect(() => { | |
setThemeInStorage(theme); | |
}, [theme]); | |
// update root element to add or remove "dark" class. | |
useEffect(() => { | |
if (theme === DARK || theme == AUTO_DARK) applyDarkStyles(); | |
else removeDarkStyles(); | |
}, [theme]); | |
return null; | |
}; | |
//**************************** | |
//* Context | |
const context = createContext<ThemeManagerValues>( | |
{} as unknown as ThemeManagerValues | |
); | |
export const useThemeManager = () => useContext(context); | |
const ThemeManagerConsumer = (props: { | |
children?: Children<ThemeManagerValues>; | |
}) => { | |
const store = useThemeManager(); | |
return ( | |
<> | |
{typeof props.children === "function" | |
? props.children(store) | |
: props.children} | |
</> | |
); | |
}; | |
const ThemeManagerProvider = (props: { | |
children: Nodes; | |
defaultTheme?: ThemeValues; | |
}) => { | |
// create theme state | |
const [theme, setTheme] = useState<ThemeValues>( | |
getTheme() === AUTO_DARK || getTheme() === AUTO_LIGHT // if stored was auto | |
? getCurrentAutoMode() | |
: getTheme() ?? props.defaultTheme ?? getCurrentAutoMode() // keep the original or the default if no original or auto if no default. | |
); | |
// prepare effects | |
useEffects(theme, setTheme); | |
// extended feature - switch to next style (light -> dark -> auto -> ...repeat) | |
const toggleTheme = useCallback(() => { | |
let value: ThemeValues = getCurrentAutoMode(); | |
switch (theme) { | |
case AUTO_DARK: | |
case AUTO_LIGHT: | |
value = LIGHT; | |
break; | |
case LIGHT: | |
value = DARK; | |
break; | |
} | |
setTheme(value); | |
}, [theme]); | |
// extended feature - switch between light and dark modes. P.S. only works when not using auto (dark - light) | |
const toggleDarkMode = useCallback(() => { | |
if (theme === AUTO_DARK || theme === AUTO_LIGHT) return; | |
setTheme(theme === DARK ? LIGHT : DARK); | |
}, [theme]); | |
// Prepare Theme Manager Values. | |
const manager = useMemo<ThemeManagerValues>( | |
() => ({ | |
theme, | |
isDarkMode: theme === DARK || theme === AUTO_DARK, | |
isAutoMode: theme === AUTO_DARK || theme === AUTO_LIGHT, | |
setTheme, | |
toggleTheme, | |
toggleDarkMode, | |
}), | |
[theme] | |
); | |
return <context.Provider value={manager}>{props.children}</context.Provider>; | |
}; | |
ThemeManagerConsumer.Provider = ThemeManagerProvider; | |
//****************** | |
//* EXPORTS | |
/** | |
* @example | |
* <ThemeManager.Provider defaultTheme="light"> | |
* <ThemeManager> | |
* { | |
* ({toggleTheme,theme}) => ( | |
* <button onClick={toggleTheme}> | |
* {theme} | |
* </button> | |
* ) | |
* } | |
* </ThemeManager> | |
* </ThemeManager.Provider> | |
*/ | |
export const ThemeManager = ThemeManagerConsumer; | |
export const WhenDarkMode = (props: { children?: Nodes }) => { | |
const { isDarkMode } = useThemeManager(); | |
if (!isDarkMode) return null; | |
return <>{props.children}</>; | |
}; | |
export const WhenLightMode = (props: { children?: Nodes }) => { | |
const { isDarkMode } = useThemeManager(); | |
if (isDarkMode) return null; | |
return <>{props.children}</>; | |
}; | |
export const WhenAutoMode = (props: { children?: Nodes }) => { | |
const { isAutoMode } = useThemeManager(); | |
if (!isAutoMode) return null; | |
return <>{props.children}</>; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment