import { computed, observable, reaction } from "mobx"; import { autobind } from "./utils/autobind"; import { userStore } from "../common/user-store"; import logger from "../main/logger"; export type ThemeId = string; export enum ThemeType { DARK = "dark", LIGHT = "light", } export interface Theme { id: ThemeId; // filename without .json-extension type: ThemeType; name?: string; colors?: Record; description?: string; author?: string; } @autobind() export class ThemeStore { protected styles: HTMLStyleElement; // bundled themes from `themes/${themeId}.json` @observable themes: Theme[] = [ { id: "kontena-dark", type: ThemeType.DARK }, { id: "kontena-light", type: ThemeType.LIGHT }, ]; @computed get activeTheme(): Theme { const activeTheme = this.themes.find(theme => theme.id === userStore.preferences.colorTheme) || this.themes[0]; return { colors: {}, ...activeTheme, } } constructor() { // auto-apply active theme reaction(() => this.activeTheme, async ({ id }) => { try { await this.loadTheme(id); this.applyTheme(); } catch (err) { userStore.resetTheme(); } }, { fireImmediately: true, }) } async init() { // preload all themes await Promise.all( this.themes.map(theme => this.loadTheme(theme.id)) ); } getThemeById(themeId: ThemeId): Theme { return this.themes.find(theme => theme.id === themeId) } protected async loadTheme(themeId: ThemeId): Promise { try { // todo: figure out why await import() doesn't work const theme = require( // eslint-disable-line @typescript-eslint/no-var-requires /* webpackChunkName: "themes/[name]" */ `./themes/${themeId}.json` ); const existingTheme = this.getThemeById(themeId); if (existingTheme) { Object.assign(existingTheme, theme); // merge } return existingTheme; } catch (err) { logger.error(`Can't load theme "${themeId}": ${err}`); } } protected applyTheme(theme = this.activeTheme) { if (!this.styles) { this.styles = document.createElement("style"); this.styles.id = "lens-theme" document.head.prepend(this.styles); } const cssVars = Object.entries(theme.colors).map(([cssName, color]) => { return `--${cssName}: ${color} !important;` }); this.styles.textContent = `:root {\n${cssVars.join("\n")}}`; // Adding universal theme flag which can be used in component styles const body = document.querySelector("body"); body.classList.toggle("theme-light", theme.type === ThemeType.LIGHT); } } export const themeStore = new ThemeStore();