1
0
mirror of https://github.com/cotes2020/jekyll-theme-chirpy.git synced 2026-06-22 07:48:42 +00:00
Files
jekyll-theme-chirpy/_javascript/theme.js
T
Cotes Chung 7496dd41fa feat(theme): persist user theme preferences (#2756)
- Migrate theme persistence from `sessionStorage` to `localStorage`
- Rename theme HTML attribute to `data-bs-theme` for Bootstrap compatibility
- Trim and compile CSS according to the chosen theme mode
2026-06-17 23:20:12 +08:00

157 lines
4.1 KiB
JavaScript

/**
* A utility class that manages the site's theme mode.
*
* Concepts:
* - Mode: dark, light, or system. The latter follows the operating system's preference.
* - Theme: The actual theme applied to the DOM, either dark or light. Determined by the mode or system preference.
*/
class Theme {
/** @type {string} LocalStorage key for the selected theme mode. */
static #storageKey = 'theme';
static Mode = Object.freeze({
DARK: 'dark',
LIGHT: 'light',
SYSTEM: 'system'
});
static #root = document.documentElement;
/** @type {MediaQueryList} System dark-mode preference query. */
static #mediaDark = window.matchMedia('(prefers-color-scheme: dark)');
/** @returns {string|null} The theme currently set on the DOM. */
static get #domTheme() {
return this.#root.dataset.bsTheme || null;
}
/** @returns {string|null} The theme stored on the client. */
static get #storedTheme() {
return localStorage.getItem(this.#storageKey);
}
/** @returns {string} The theme preferred by the operating system. */
static get #systemTheme() {
return this.#prefersDark ? this.Mode.DARK : this.Mode.LIGHT;
}
/** @returns {boolean} Whether the operating system prefers dark mode. */
static get #prefersDark() {
return this.#mediaDark.matches;
}
/**
* Applies a theme and optionally persists it as a user preference.
*
* @param {'light'|'dark'} theme
* @param {{ persist?: boolean, domPersist?: boolean }} [options]
* - `persist`: Whether the theme is persisted in localStorage.
* - `domPersist`: Whether the theme is persisted in data attributes on the DOM.
*/
static #apply(theme, { persist = false, domPersist = false } = {}) {
this.#root.dataset.bsTheme = theme;
if (persist) {
localStorage.setItem(this.#storageKey, theme);
}
if (domPersist || persist) {
this.#root.toggleAttribute('data-theme-persisted', true);
}
}
/** Removes the stored user preference. */
static #clearStorage() {
localStorage.removeItem(this.#storageKey);
this.#root.toggleAttribute('data-theme-persisted', false);
}
/** Broadcasts a theme change event to dependent modules. */
static #notify() {
window.postMessage({ id: this.eventId }, '*');
}
/** @type {boolean} Whether the current page allows theme toggling. */
static isToggleable = this.#domTheme === null;
static eventId = 'theme-updated';
/** @returns {string} Resolved theme, falling back to the system preference. */
static get resolvedTheme() {
return this.#storedTheme || this.#systemTheme;
}
/** @returns {boolean} Whether the theme is determined by the system preference. */
static get isSystemTheme() {
return this.#storedTheme === null;
}
/** @returns {boolean} Whether the resolved theme is dark. */
static get isDark() {
return this.resolvedTheme === this.Mode.DARK;
}
/**
* Creates a mode-indexed value map.
*
* @template T
* @param {T} light Value for light mode.
* @param {T} dark Value for dark mode.
* @returns {{ light: T, dark: T }}
*/
static newThemeMap(light, dark) {
return {
[this.Mode.LIGHT]: light,
[this.Mode.DARK]: dark
};
}
/** Initializes the theme from the stored value or system preference. */
static init() {
if (!this.isToggleable) {
this.#clearStorage();
return;
}
const storedTheme = this.#storedTheme;
if (storedTheme) {
this.#apply(storedTheme, { domPersist: true });
} else {
this.#apply(this.#systemTheme);
}
this.#mediaDark.addEventListener('change', () => {
if (this.#storedTheme) {
return;
}
this.#apply(this.#systemTheme);
this.#notify();
});
}
/**
* Updates the theme by the specified mode.
*
* @param {'light'|'dark'|'system'} mode
*/
static update(mode) {
const newTheme = mode === this.Mode.SYSTEM ? this.#systemTheme : mode;
if (newTheme !== this.resolvedTheme) {
this.#notify();
}
this.#apply(newTheme, { persist: mode !== this.Mode.SYSTEM });
if (mode === this.Mode.SYSTEM) {
this.#clearStorage();
}
}
}
Theme.init();
export default Theme;