polen
Version:
A framework for delightful GraphQL developer portals
123 lines (120 loc) • 4.18 kB
JavaScript
/**
* Theme management utilities using CSS-first approach with cookie persistence.
*
* Strategy:
* - CSS `prefers-color-scheme` handles initial theme (no JS needed)
* - User toggle sets cookie + updates DOM immediately
* - Server reads cookie on next visit for SSR hydration match
*/
/**
* Create a theme manager instance
*/
export const createThemeManager = (options = {}) => {
const { cookieName = `theme`, classPrefix = ``, maxAge = 31536000, // 1 year
path = `/`, } = options;
const getThemeClass = (theme) => classPrefix ? `${classPrefix}${theme}` : theme;
const readCookie = (cookieString) => {
const cookies = cookieString || (typeof document !== `undefined` ? document.cookie : ``);
if (!cookies)
return null;
const match = new RegExp(`(^| )${cookieName}=([^;]+)`).exec(cookies);
const value = match?.[2];
return value === `light` || value === `dark` || value === `system` ? value : null;
};
const writeCookie = (theme) => {
// If system is selected, delete the cookie by setting Max-Age to 0
const cookieValue = theme === `system`
? `${cookieName}=; Max-Age=0; Path=${path}; SameSite=Strict`
: `${cookieName}=${theme}; Max-Age=${maxAge}; Path=${path}; SameSite=Strict`;
// Set cookie if in browser
if (typeof document !== `undefined`) {
document.cookie = cookieValue;
}
return cookieValue;
};
const applyToDOM = (theme) => {
if (typeof document === `undefined`)
return;
// If system, detect the actual theme to apply
const actualTheme = theme === `system`
? (globalThis.matchMedia(`(prefers-color-scheme: dark)`).matches ? `dark` : `light`)
: theme;
const themeClass = getThemeClass(actualTheme);
const otherTheme = actualTheme === `light` ? `dark` : `light`;
const otherClass = getThemeClass(otherTheme);
document.documentElement.classList.remove(otherClass);
document.documentElement.classList.add(themeClass);
// Also update data-theme attribute for consistency with SSR
document.documentElement.setAttribute('data-theme', actualTheme);
};
const getCurrentFromDOM = () => {
if (typeof document === `undefined`)
return null;
const classList = document.documentElement.classList;
if (classList.contains(getThemeClass(`dark`)))
return `dark`;
if (classList.contains(getThemeClass(`light`)))
return `light`;
return null;
};
const set = (theme) => {
writeCookie(theme);
applyToDOM(theme);
};
const toggle = () => {
// Get current theme preference from cookie
const cookieTheme = readCookie();
// Determine next theme in cycle: system → light → dark → system
let newTheme;
if (!cookieTheme || cookieTheme === `system`) {
// Currently on system, go to light
newTheme = `light`;
}
else if (cookieTheme === `light`) {
// Currently on light, go to dark
newTheme = `dark`;
}
else {
// Currently on dark, go back to system
newTheme = `system`;
}
set(newTheme);
return newTheme;
};
const getCSS = () => {
const lightClass = getThemeClass(`light`);
const darkClass = getThemeClass(`dark`);
return `
/* Theme CSS - handles both system preference and user override */
:root {
/* Default light theme variables */
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root {
/* Dark theme variables for system preference */
color-scheme: dark;
}
}
/* User preference overrides (set via cookie/JS) */
html.${lightClass} {
/* Force light theme */
color-scheme: light;
}
html.${darkClass} {
/* Force dark theme */
color-scheme: dark;
}
`.trim();
};
return {
readCookie,
writeCookie,
applyToDOM,
getCurrentFromDOM,
toggle,
set,
getCSS,
};
};
//# sourceMappingURL=theme.js.map