mirror of
https://github.com/cotes2020/jekyll-theme-chirpy.git
synced 2026-06-22 07:48:42 +00:00
7496dd41fa
- 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
157 lines
4.1 KiB
JavaScript
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;
|