WIP - 1
This commit is contained in:
parent
599e519f22
commit
8c02541b69
3 changed files with 233 additions and 13 deletions
155
src/platform/web/ThemeBuilder.ts
Normal file
155
src/platform/web/ThemeBuilder.ts
Normal file
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import type {ThemeInformation} from "./ThemeLoader";
|
||||
import {ColorSchemePreference} from "./ThemeLoader";
|
||||
import {offColor} from 'off-color';
|
||||
|
||||
function derive(value, operation, argument, isDark) {
|
||||
const argumentAsNumber = parseInt(argument);
|
||||
if (isDark) {
|
||||
// For dark themes, invert the operation
|
||||
if (operation === 'darker') {
|
||||
operation = "lighter";
|
||||
}
|
||||
else if (operation === 'lighter') {
|
||||
operation = "darker";
|
||||
}
|
||||
}
|
||||
switch (operation) {
|
||||
case "darker": {
|
||||
const newColorString = offColor(value).darken(argumentAsNumber / 100).hex();
|
||||
return newColorString;
|
||||
}
|
||||
case "lighter": {
|
||||
const newColorString = offColor(value).lighten(argumentAsNumber / 100).hex();
|
||||
return newColorString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ThemeBuilder {
|
||||
// todo: replace any with manifest type when PR is merged
|
||||
private _idToManifest: Map<string, any>;
|
||||
private _themeMapping: Record<string, ThemeInformation> = {};
|
||||
private _themeToVariables: Record<string, any> = {};
|
||||
private _preferredColorScheme?: ColorSchemePreference;
|
||||
|
||||
constructor(manifestMap: Map<string, any>, preferredColorScheme?: ColorSchemePreference) {
|
||||
this._idToManifest = manifestMap;
|
||||
this._preferredColorScheme = preferredColorScheme;
|
||||
}
|
||||
|
||||
populateDerivedTheme(manifest) {
|
||||
const { manifest: baseManifest, location } = this._idToManifest.get(manifest.extends);
|
||||
const runtimeCssLocation = baseManifest.source?.["runtime-asset"];
|
||||
const cssLocation = new URL(runtimeCssLocation, new URL(location, window.location.origin)).href;
|
||||
const derivedVariables = baseManifest.source?.["derived-variables"];
|
||||
const themeName = manifest.name;
|
||||
let defaultDarkVariant: any = {}, defaultLightVariant: any = {};
|
||||
for (const [variant, variantDetails] of Object.entries(manifest.values.variants) as [string, any][]) {
|
||||
const themeId = `${manifest.id}-${variant}`;
|
||||
const { name: variantName, default: isDefault, dark, variables } = variantDetails;
|
||||
const resolvedVariables = this.deriveVariables(variables, derivedVariables, dark);
|
||||
console.log("resolved", resolvedVariables);
|
||||
Object.assign(variables, resolvedVariables);
|
||||
const themeDisplayName = `${themeName} ${variantName}`;
|
||||
if (isDefault) {
|
||||
/**
|
||||
* This is a default variant!
|
||||
* We'll add these to the themeMapping (separately) keyed with just the
|
||||
* theme-name (i.e "Element" instead of "Element Dark").
|
||||
* We need to be able to distinguish them from other variants!
|
||||
*
|
||||
* This allows us to render radio-buttons with "dark" and
|
||||
* "light" options.
|
||||
*/
|
||||
const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant;
|
||||
defaultVariant.variantName = variantName;
|
||||
defaultVariant.id = themeId;
|
||||
defaultVariant.cssLocation = cssLocation;
|
||||
defaultVariant.variables = variables;
|
||||
continue;
|
||||
}
|
||||
// Non-default variants are keyed in themeMapping with "theme_name variant_name"
|
||||
// eg: "Element Dark"
|
||||
this._themeMapping[themeDisplayName] = {
|
||||
cssLocation,
|
||||
id: themeId,
|
||||
variables: variables,
|
||||
};
|
||||
}
|
||||
if (defaultDarkVariant.id && defaultLightVariant.id) {
|
||||
/**
|
||||
* As mentioned above, if there's both a default dark and a default light variant,
|
||||
* add them to themeMapping separately.
|
||||
*/
|
||||
const defaultVariant = this._preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant;
|
||||
this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant };
|
||||
}
|
||||
else {
|
||||
/**
|
||||
* If only one default variant is found (i.e only dark default or light default but not both),
|
||||
* treat it like any other variant.
|
||||
*/
|
||||
const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant;
|
||||
this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation };
|
||||
}
|
||||
}
|
||||
|
||||
get themeMapping() {
|
||||
return this._themeMapping;
|
||||
}
|
||||
|
||||
injectCSSVariables(variables: Record<string, string>) {
|
||||
const root = document.documentElement;
|
||||
for (const [variable, value] of Object.entries(variables)) {
|
||||
root.style.setProperty(`--${variable}`, value);
|
||||
}
|
||||
}
|
||||
|
||||
removeCSSVariables(variables: string[]) {
|
||||
const root = document.documentElement;
|
||||
for (const variable of variables) {
|
||||
root.style.removeProperty(`--${variable}`);
|
||||
}
|
||||
}
|
||||
|
||||
deriveVariables(variables: Record<string, string>, derivedVariables: string[], isDark: boolean) {
|
||||
const aliases: any = {};
|
||||
const resolvedVariables: any = {};
|
||||
const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/;
|
||||
for (const variable of derivedVariables) {
|
||||
// If this is an alias, store it for processing later
|
||||
const [alias, value] = variable.split("=");
|
||||
if (value) {
|
||||
aliases[alias] = value;
|
||||
continue;
|
||||
}
|
||||
// Resolve derived variables
|
||||
const matches = variable.match(RE_VARIABLE_VALUE);
|
||||
if (matches) {
|
||||
const [, baseVariable, operation, argument] = matches;
|
||||
const value = variables[baseVariable];
|
||||
const resolvedValue = derive(value, operation, argument, isDark);
|
||||
resolvedVariables[variable] = resolvedValue;
|
||||
}
|
||||
}
|
||||
for (const [alias, variable] of Object.entries(aliases) as any) {
|
||||
resolvedVariables[alias] = variables[variable] ?? resolvedVariables[variable];
|
||||
}
|
||||
return resolvedVariables;
|
||||
}
|
||||
}
|
|
@ -14,33 +14,29 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type {ILogItem} from "../../logging/types.js";
|
||||
import type {ILogItem} from "../../logging/types";
|
||||
import type {Platform} from "./Platform.js";
|
||||
import {ThemeBuilder} from "./ThemeBuilder";
|
||||
|
||||
type NormalVariant = {
|
||||
id: string;
|
||||
cssLocation: string;
|
||||
variables?: any;
|
||||
};
|
||||
|
||||
type DefaultVariant = {
|
||||
dark: {
|
||||
id: string;
|
||||
cssLocation: string;
|
||||
dark: NormalVariant & {
|
||||
variantName: string;
|
||||
};
|
||||
light: {
|
||||
id: string;
|
||||
cssLocation: string;
|
||||
light: NormalVariant & {
|
||||
variantName: string;
|
||||
};
|
||||
default: {
|
||||
id: string;
|
||||
cssLocation: string;
|
||||
default: NormalVariant & {
|
||||
variantName: string;
|
||||
};
|
||||
}
|
||||
|
||||
type ThemeInformation = NormalVariant | DefaultVariant;
|
||||
export type ThemeInformation = NormalVariant | DefaultVariant;
|
||||
|
||||
export enum ColorSchemePreference {
|
||||
Dark,
|
||||
|
@ -50,18 +46,31 @@ export enum ColorSchemePreference {
|
|||
export class ThemeLoader {
|
||||
private _platform: Platform;
|
||||
private _themeMapping: Record<string, ThemeInformation>;
|
||||
private _themeBuilder: ThemeBuilder;
|
||||
|
||||
constructor(platform: Platform) {
|
||||
this._platform = platform;
|
||||
}
|
||||
|
||||
async init(manifestLocations: string[], log?: ILogItem): Promise<void> {
|
||||
const idToManifest = new Map();
|
||||
await this._platform.logger.wrapOrRun(log, "ThemeLoader.init", async (log) => {
|
||||
this._themeMapping = {};
|
||||
const results = await Promise.all(
|
||||
manifestLocations.map( location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response())
|
||||
);
|
||||
results.forEach(({ body }, i) => this._populateThemeMap(body, manifestLocations[i], log));
|
||||
results.forEach(({ body }, i) => idToManifest.set(body.id, { manifest: body, location: manifestLocations[i] }));
|
||||
this._themeBuilder = new ThemeBuilder(idToManifest, this.preferredColorScheme);
|
||||
results.forEach(({ body }, i) => {
|
||||
if (body.extends) {
|
||||
this._themeBuilder.populateDerivedTheme(body);
|
||||
}
|
||||
else {
|
||||
this._populateThemeMap(body, manifestLocations[i], log);
|
||||
}
|
||||
});
|
||||
Object.assign(this._themeMapping, this._themeBuilder.themeMapping);
|
||||
console.log("derived theme mapping", this._themeBuilder.themeMapping);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -144,18 +153,23 @@ export class ThemeLoader {
|
|||
|
||||
setTheme(themeName: string, themeVariant?: "light" | "dark" | "default", log?: ILogItem) {
|
||||
this._platform.logger.wrapOrRun(log, { l: "change theme", name: themeName, variant: themeVariant }, () => {
|
||||
let cssLocation: string;
|
||||
let cssLocation: string, variables: Record<string, string>;
|
||||
let themeDetails = this._themeMapping[themeName];
|
||||
if ("id" in themeDetails) {
|
||||
cssLocation = themeDetails.cssLocation;
|
||||
variables = themeDetails.variables;
|
||||
}
|
||||
else {
|
||||
if (!themeVariant) {
|
||||
throw new Error("themeVariant is undefined!");
|
||||
}
|
||||
cssLocation = themeDetails[themeVariant].cssLocation;
|
||||
variables = themeDetails[themeVariant].variables;
|
||||
}
|
||||
this._platform.replaceStylesheet(cssLocation);
|
||||
if (variables) {
|
||||
this._themeBuilder.injectCSSVariables(variables);
|
||||
}
|
||||
this._platform.settingsStorage.setString("theme-name", themeName);
|
||||
if (themeVariant) {
|
||||
this._platform.settingsStorage.setString("theme-variant", themeVariant);
|
||||
|
|
51
theme.json
Normal file
51
theme.json
Normal file
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"name": "Customer",
|
||||
"extends": "element",
|
||||
"id": "customer",
|
||||
"values": {
|
||||
"variants": {
|
||||
"dark": {
|
||||
"dark": true,
|
||||
"default": true,
|
||||
"name": "Dark",
|
||||
"variables": {
|
||||
"background-color-primary": "#21262b",
|
||||
"background-color-secondary": "#2D3239",
|
||||
"text-color": "#fff",
|
||||
"accent-color": "#F03F5B",
|
||||
"error-color": "#FF4B55",
|
||||
"fixed-white": "#fff",
|
||||
"room-badge": "#61708b",
|
||||
"link-color": "#238cf5"
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"default": true,
|
||||
"name": "Dark",
|
||||
"variables": {
|
||||
"background-color-primary": "#21262b",
|
||||
"background-color-secondary": "#2D3239",
|
||||
"text-color": "#fff",
|
||||
"accent-color": "#F03F5B",
|
||||
"error-color": "#FF4B55",
|
||||
"fixed-white": "#fff",
|
||||
"room-badge": "#61708b",
|
||||
"link-color": "#238cf5"
|
||||
}
|
||||
},
|
||||
"red": {
|
||||
"name": "Red",
|
||||
"variables": {
|
||||
"background-color-primary": "#1F1F1F",
|
||||
"background-color-secondary": "#2B243E",
|
||||
"text-color": "#fff",
|
||||
"accent-color": "#F23041",
|
||||
"error-color": "#FF4B55",
|
||||
"fixed-white": "#fff",
|
||||
"room-badge": "#F23030",
|
||||
"link-color": "#238cf5"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in a new issue