vira
Version:
A simple and highly versatile design system using element-vir.
94 lines (93 loc) • 3.71 kB
JavaScript
import { assert } from '@augment-vir/assert';
import { LocalStorageClient } from '@electrovir/local-storage-client';
import { defineShape, enumShape } from 'object-shape-tester';
import { applyColorThemeViaStyleElement } from 'theme-vir/dist/color-theme/apply-color-theme.js';
import { listenTo } from 'typed-event-target';
import { viraTheme, viraThemeDarkOverride } from '../styles/vira-color-theme.js';
/**
* A user-facing selection of which theme to display. `Auto` follows the system color scheme; the
* other values force a specific theme regardless of system preference.
*
* @category Internal
*/
export var ViraThemeSelection;
(function (ViraThemeSelection) {
ViraThemeSelection["Light"] = "light";
ViraThemeSelection["Dark"] = "dark";
ViraThemeSelection["Auto"] = "auto";
})(ViraThemeSelection || (ViraThemeSelection = {}));
/**
* Default implementation of {@link ApplyThemeCallback}, which simply applies Vira themes.
*
* @category Internal
*/
export const defaultApplyThemeCallback = ({ useDarkTheme }) => {
applyColorThemeViaStyleElement(viraTheme, useDarkTheme ? viraThemeDarkOverride : undefined);
};
const darkSchemeMediaQuery = '(prefers-color-scheme: dark)';
const themeStorageShapes = {
selectedTheme: defineShape({
theme: enumShape(ViraThemeSelection),
}),
};
/**
* Tracks the user's {@link ViraThemeSelection} and bridges it to a consumer-supplied `applyTheme`
* callback. Persists the selection in LocalStorage via an internal `LocalStorageClient`, and
* listens for system color-scheme changes to re-apply the theme in auto mode without persisting.
*
* The initial theme is applied during construction.
*
* @category Util
*/
export class ViraThemeClient {
/** The callback that will be called to apply a new theme. */
applyThemeCallback = defaultApplyThemeCallback;
/** Contains the user's last selected theme, saving and loading it to disk for persistence. */
localStorageClient;
/** A callback to remove the global theme preference listener. */
removeThemePreferenceListener = listenTo(globalThis.matchMedia(darkSchemeMediaQuery), 'change', (event) => {
assert.instanceOf(event, MediaQueryListEvent);
if (this.currentTheme === ViraThemeSelection.Auto) {
void this.applyThemeCallback({
useDarkTheme: event.matches,
});
}
});
constructor(params = {}) {
if (params.applyTheme) {
this.applyThemeCallback = params.applyTheme;
}
this.localStorageClient = new LocalStorageClient(themeStorageShapes, {
storeName: params.storeName || 'vira-theme',
});
this.applySelection(this.currentTheme);
}
/**
* The currently selected theme. If you use multiple clients to set the same theme, this might
* get out of sync.
*/
get currentTheme() {
return this.localStorageClient.get.selectedTheme()?.theme || ViraThemeSelection.Auto;
}
/** Set the selected theme. */
setSelectedTheme(selection) {
this.applySelection(selection);
this.localStorageClient.set.selectedTheme({
theme: selection,
});
}
/** Cleanup internal state and listeners. */
destroy() {
this.removeThemePreferenceListener();
this.localStorageClient.destroy();
}
/** Apply the currently selected theme. */
applySelection(selection) {
const useDarkTheme = selection === ViraThemeSelection.Dark ||
(selection === ViraThemeSelection.Auto &&
globalThis.matchMedia(darkSchemeMediaQuery).matches);
void this.applyThemeCallback({
useDarkTheme,
});
}
}