Merge pull request #769 from vector-im/implement-derived-theme
Support for derived themes
This commit is contained in:
commit
041e628520
20 changed files with 869 additions and 247 deletions
|
@ -50,8 +50,9 @@
|
||||||
"postcss-flexbugs-fixes": "^5.0.2",
|
"postcss-flexbugs-fixes": "^5.0.2",
|
||||||
"postcss-value-parser": "^4.2.0",
|
"postcss-value-parser": "^4.2.0",
|
||||||
"regenerator-runtime": "^0.13.7",
|
"regenerator-runtime": "^0.13.7",
|
||||||
|
"svgo": "^2.8.0",
|
||||||
"text-encoding": "^0.7.0",
|
"text-encoding": "^0.7.0",
|
||||||
"typescript": "^4.3.5",
|
"typescript": "^4.7.0",
|
||||||
"vite": "^2.9.8",
|
"vite": "^2.9.8",
|
||||||
"xxhashjs": "^0.2.2"
|
"xxhashjs": "^0.2.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
const path = require('path').posix;
|
const path = require('path').posix;
|
||||||
|
const {optimize} = require('svgo');
|
||||||
|
|
||||||
async function readCSSSource(location) {
|
async function readCSSSource(location) {
|
||||||
const fs = require("fs").promises;
|
const fs = require("fs").promises;
|
||||||
const path = require("path");
|
|
||||||
const resolvedLocation = path.resolve(__dirname, "../../", `${location}/theme.css`);
|
const resolvedLocation = path.resolve(__dirname, "../../", `${location}/theme.css`);
|
||||||
const data = await fs.readFile(resolvedLocation);
|
const data = await fs.readFile(resolvedLocation);
|
||||||
return data;
|
return data;
|
||||||
|
@ -43,6 +43,45 @@ function addThemesToConfig(bundle, manifestLocations, defaultThemes) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object where keys are the svg file names and the values
|
||||||
|
* are the svg code (optimized)
|
||||||
|
* @param {*} icons Object where keys are css variable names and values are locations of the svg
|
||||||
|
* @param {*} manifestLocation Location of manifest used for resolving path
|
||||||
|
*/
|
||||||
|
async function generateIconSourceMap(icons, manifestLocation) {
|
||||||
|
const sources = {};
|
||||||
|
const fileNames = [];
|
||||||
|
const promises = [];
|
||||||
|
const fs = require("fs").promises;
|
||||||
|
for (const icon of Object.values(icons)) {
|
||||||
|
const [location] = icon.split("?");
|
||||||
|
// resolve location against manifestLocation
|
||||||
|
const resolvedLocation = path.resolve(manifestLocation, location);
|
||||||
|
const iconData = fs.readFile(resolvedLocation);
|
||||||
|
promises.push(iconData);
|
||||||
|
const fileName = path.basename(resolvedLocation);
|
||||||
|
fileNames.push(fileName);
|
||||||
|
}
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
for (let i = 0; i < results.length; ++i) {
|
||||||
|
const svgString = results[i].toString();
|
||||||
|
const result = optimize(svgString, {
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: "preset-default",
|
||||||
|
params: {
|
||||||
|
overrides: { convertColors: false, },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const optimizedSvgString = result.data;
|
||||||
|
sources[fileNames[i]] = optimizedSvgString;
|
||||||
|
}
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a mapping from location (of manifest file) to an array containing all the chunks (of css files) generated from that location.
|
* Returns a mapping from location (of manifest file) to an array containing all the chunks (of css files) generated from that location.
|
||||||
* To understand what chunk means in this context, see https://rollupjs.org/guide/en/#generatebundle.
|
* To understand what chunk means in this context, see https://rollupjs.org/guide/en/#generatebundle.
|
||||||
|
@ -278,7 +317,7 @@ module.exports = function buildThemes(options) {
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
generateBundle(_, bundle) {
|
async generateBundle(_, bundle) {
|
||||||
const assetMap = getMappingFromFileNameToAssetInfo(bundle);
|
const assetMap = getMappingFromFileNameToAssetInfo(bundle);
|
||||||
const chunkMap = getMappingFromLocationToChunkArray(bundle);
|
const chunkMap = getMappingFromLocationToChunkArray(bundle);
|
||||||
const runtimeThemeChunkMap = getMappingFromLocationToRuntimeChunk(bundle);
|
const runtimeThemeChunkMap = getMappingFromLocationToRuntimeChunk(bundle);
|
||||||
|
@ -299,13 +338,29 @@ module.exports = function buildThemes(options) {
|
||||||
const locationRelativeToManifest = path.relative(manifestLocation, locationRelativeToBuildRoot);
|
const locationRelativeToManifest = path.relative(manifestLocation, locationRelativeToBuildRoot);
|
||||||
builtAssets[`${name}-${variant}`] = locationRelativeToManifest;
|
builtAssets[`${name}-${variant}`] = locationRelativeToManifest;
|
||||||
}
|
}
|
||||||
|
// Emit the base svg icons as asset
|
||||||
|
const nameToAssetHashedLocation = [];
|
||||||
|
const nameToSource = await generateIconSourceMap(icon, location);
|
||||||
|
for (const [name, source] of Object.entries(nameToSource)) {
|
||||||
|
const ref = this.emitFile({ type: "asset", name, source });
|
||||||
|
const assetHashedName = this.getFileName(ref);
|
||||||
|
nameToAssetHashedLocation[name] = assetHashedName;
|
||||||
|
}
|
||||||
|
// Update icon section in output manifest with paths to the icon in build output
|
||||||
|
for (const [variable, location] of Object.entries(icon)) {
|
||||||
|
const [locationWithoutQueryParameters, queryParameters] = location.split("?");
|
||||||
|
const name = path.basename(locationWithoutQueryParameters);
|
||||||
|
const locationRelativeToBuildRoot = nameToAssetHashedLocation[name];
|
||||||
|
const locationRelativeToManifest = path.relative(manifestLocation, locationRelativeToBuildRoot);
|
||||||
|
icon[variable] = `${locationRelativeToManifest}?${queryParameters}`;
|
||||||
|
}
|
||||||
const runtimeThemeChunk = runtimeThemeChunkMap.get(location);
|
const runtimeThemeChunk = runtimeThemeChunkMap.get(location);
|
||||||
const runtimeAssetLocation = path.relative(manifestLocation, assetMap.get(runtimeThemeChunk.fileName).fileName);
|
const runtimeAssetLocation = path.relative(manifestLocation, assetMap.get(runtimeThemeChunk.fileName).fileName);
|
||||||
manifest.source = {
|
manifest.source = {
|
||||||
"built-assets": builtAssets,
|
"built-assets": builtAssets,
|
||||||
"runtime-asset": runtimeAssetLocation,
|
"runtime-asset": runtimeAssetLocation,
|
||||||
"derived-variables": derivedVariables,
|
"derived-variables": derivedVariables,
|
||||||
"icon": icon
|
"icon": icon,
|
||||||
};
|
};
|
||||||
const name = `theme-${themeKey}.json`;
|
const name = `theme-${themeKey}.json`;
|
||||||
manifestLocations.push(`${manifestLocation}/${name}`);
|
manifestLocations.push(`${manifestLocation}/${name}`);
|
||||||
|
|
|
@ -81,7 +81,8 @@ module.exports = (opts = {}) => {
|
||||||
const urlVariables = new Map();
|
const urlVariables = new Map();
|
||||||
const counter = createCounter();
|
const counter = createCounter();
|
||||||
root.walkDecls(decl => findAndReplaceUrl(decl, urlVariables, counter));
|
root.walkDecls(decl => findAndReplaceUrl(decl, urlVariables, counter));
|
||||||
if (urlVariables.size) {
|
const cssFileLocation = root.source.input.from;
|
||||||
|
if (urlVariables.size && !cssFileLocation.includes("type=runtime")) {
|
||||||
addResolvedVariablesToRootSelector(root, { Rule, Declaration }, urlVariables);
|
addResolvedVariablesToRootSelector(root, { Rule, Declaration }, urlVariables);
|
||||||
}
|
}
|
||||||
if (opts.compiledVariables){
|
if (opts.compiledVariables){
|
||||||
|
|
|
@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require("fs");
|
import {readFileSync, mkdirSync, writeFileSync} from "fs";
|
||||||
const path = require("path");
|
import {resolve} from "path";
|
||||||
const xxhash = require('xxhashjs');
|
import {h32} from "xxhashjs";
|
||||||
|
import {getColoredSvgString} from "../../src/platform/web/theming/shared/svg-colorizer.mjs";
|
||||||
|
|
||||||
function createHash(content) {
|
function createHash(content) {
|
||||||
const hasher = new xxhash.h32(0);
|
const hasher = new h32(0);
|
||||||
hasher.update(content);
|
hasher.update(content);
|
||||||
return hasher.digest();
|
return hasher.digest();
|
||||||
}
|
}
|
||||||
|
@ -30,18 +31,14 @@ function createHash(content) {
|
||||||
* @param {string} primaryColor Primary color for the new svg
|
* @param {string} primaryColor Primary color for the new svg
|
||||||
* @param {string} secondaryColor Secondary color for the new svg
|
* @param {string} secondaryColor Secondary color for the new svg
|
||||||
*/
|
*/
|
||||||
module.exports.buildColorizedSVG = function (svgLocation, primaryColor, secondaryColor) {
|
export function buildColorizedSVG(svgLocation, primaryColor, secondaryColor) {
|
||||||
const svgCode = fs.readFileSync(svgLocation, { encoding: "utf8"});
|
const svgCode = readFileSync(svgLocation, { encoding: "utf8"});
|
||||||
let coloredSVGCode = svgCode.replaceAll("#ff00ff", primaryColor);
|
const coloredSVGCode = getColoredSvgString(svgCode, primaryColor, secondaryColor);
|
||||||
coloredSVGCode = coloredSVGCode.replaceAll("#00ffff", secondaryColor);
|
|
||||||
if (svgCode === coloredSVGCode) {
|
|
||||||
throw new Error("svg-colorizer made no color replacements! The input svg should only contain colors #ff00ff (primary, case-sensitive) and #00ffff (secondary, case-sensitive).");
|
|
||||||
}
|
|
||||||
const fileName = svgLocation.match(/.+[/\\](.+\.svg)/)[1];
|
const fileName = svgLocation.match(/.+[/\\](.+\.svg)/)[1];
|
||||||
const outputName = `${fileName.substring(0, fileName.length - 4)}-${createHash(coloredSVGCode)}.svg`;
|
const outputName = `${fileName.substring(0, fileName.length - 4)}-${createHash(coloredSVGCode)}.svg`;
|
||||||
const outputPath = path.resolve(__dirname, "../../.tmp");
|
const outputPath = resolve(__dirname, "../../.tmp");
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(outputPath);
|
mkdirSync(outputPath);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
if (e.code !== "EEXIST") {
|
if (e.code !== "EEXIST") {
|
||||||
|
@ -49,6 +46,6 @@ module.exports.buildColorizedSVG = function (svgLocation, primaryColor, secondar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const outputFile = `${outputPath}/${outputName}`;
|
const outputFile = `${outputPath}/${outputName}`;
|
||||||
fs.writeFileSync(outputFile, coloredSVGCode);
|
writeFileSync(outputFile, coloredSVGCode);
|
||||||
return outputFile;
|
return outputFile;
|
||||||
}
|
}
|
5
scripts/test-theme.sh
Executable file
5
scripts/test-theme.sh
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/zsh
|
||||||
|
cp theme.json target/assets/theme-customer.json
|
||||||
|
cat target/config.json | jq '.themeManifests += ["assets/theme-customer.json"]' | cat > target/config.temp.json
|
||||||
|
rm target/config.json
|
||||||
|
mv target/config.temp.json target/config.json
|
|
@ -22,6 +22,13 @@ export type ThemeManifest = Partial<{
|
||||||
version: number;
|
version: number;
|
||||||
// A user-facing string that is the name for this theme-collection.
|
// A user-facing string that is the name for this theme-collection.
|
||||||
name: string;
|
name: string;
|
||||||
|
// An identifier for this theme
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* Id of the theme that this theme derives from.
|
||||||
|
* Only present for derived/runtime themes.
|
||||||
|
*/
|
||||||
|
extends: string;
|
||||||
/**
|
/**
|
||||||
* This is added to the manifest during the build process and includes data
|
* This is added to the manifest during the build process and includes data
|
||||||
* that is needed to load themes at runtime.
|
* that is needed to load themes at runtime.
|
||||||
|
@ -42,6 +49,12 @@ export type ThemeManifest = Partial<{
|
||||||
"runtime-asset": string;
|
"runtime-asset": string;
|
||||||
// Array of derived-variables
|
// Array of derived-variables
|
||||||
"derived-variables": Array<string>;
|
"derived-variables": Array<string>;
|
||||||
|
/**
|
||||||
|
* Mapping from icon variable to location of icon in build output with query parameters
|
||||||
|
* indicating how it should be colored for this particular theme.
|
||||||
|
* eg: "icon-url-1": "element-logo.86bc8565.svg?primary=accent-color"
|
||||||
|
*/
|
||||||
|
icon: Record<string, string>;
|
||||||
};
|
};
|
||||||
values: {
|
values: {
|
||||||
/**
|
/**
|
||||||
|
@ -60,6 +73,8 @@ type Variant = Partial<{
|
||||||
default: boolean;
|
default: boolean;
|
||||||
// A user-facing string that is the name for this variant.
|
// A user-facing string that is the name for this variant.
|
||||||
name: string;
|
name: string;
|
||||||
|
// A boolean indicating whether this is a dark theme or not
|
||||||
|
dark: boolean;
|
||||||
/**
|
/**
|
||||||
* Mapping from css variable to its value.
|
* Mapping from css variable to its value.
|
||||||
* eg: {"background-color-primary": "#21262b", ...}
|
* eg: {"background-color-primary": "#21262b", ...}
|
||||||
|
|
|
@ -38,7 +38,7 @@ import {downloadInIframe} from "./dom/download.js";
|
||||||
import {Disposables} from "../../utils/Disposables";
|
import {Disposables} from "../../utils/Disposables";
|
||||||
import {parseHTML} from "./parsehtml.js";
|
import {parseHTML} from "./parsehtml.js";
|
||||||
import {handleAvatarError} from "./ui/avatar";
|
import {handleAvatarError} from "./ui/avatar";
|
||||||
import {ThemeLoader} from "./ThemeLoader";
|
import {ThemeLoader} from "./theming/ThemeLoader";
|
||||||
|
|
||||||
function addScript(src) {
|
function addScript(src) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
|
|
|
@ -1,217 +0,0 @@
|
||||||
/*
|
|
||||||
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 {ILogItem} from "../../logging/types.js";
|
|
||||||
import type {Platform} from "./Platform.js";
|
|
||||||
|
|
||||||
type NormalVariant = {
|
|
||||||
id: string;
|
|
||||||
cssLocation: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DefaultVariant = {
|
|
||||||
dark: {
|
|
||||||
id: string;
|
|
||||||
cssLocation: string;
|
|
||||||
variantName: string;
|
|
||||||
};
|
|
||||||
light: {
|
|
||||||
id: string;
|
|
||||||
cssLocation: string;
|
|
||||||
variantName: string;
|
|
||||||
};
|
|
||||||
default: {
|
|
||||||
id: string;
|
|
||||||
cssLocation: string;
|
|
||||||
variantName: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type ThemeInformation = NormalVariant | DefaultVariant;
|
|
||||||
|
|
||||||
export enum ColorSchemePreference {
|
|
||||||
Dark,
|
|
||||||
Light
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ThemeLoader {
|
|
||||||
private _platform: Platform;
|
|
||||||
private _themeMapping: Record<string, ThemeInformation>;
|
|
||||||
|
|
||||||
constructor(platform: Platform) {
|
|
||||||
this._platform = platform;
|
|
||||||
}
|
|
||||||
|
|
||||||
async init(manifestLocations: string[], log?: ILogItem): Promise<void> {
|
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _populateThemeMap(manifest, manifestLocation: string, log: ILogItem) {
|
|
||||||
log.wrap("populateThemeMap", (l) => {
|
|
||||||
/*
|
|
||||||
After build has finished, the source section of each theme manifest
|
|
||||||
contains `built-assets` which is a mapping from the theme-id to
|
|
||||||
cssLocation of theme
|
|
||||||
*/
|
|
||||||
const builtAssets: Record<string, string> = manifest.source?.["built-assets"];
|
|
||||||
const themeName = manifest.name;
|
|
||||||
let defaultDarkVariant: any = {}, defaultLightVariant: any = {};
|
|
||||||
for (let [themeId, cssLocation] of Object.entries(builtAssets)) {
|
|
||||||
try {
|
|
||||||
/**
|
|
||||||
* This cssLocation is relative to the location of the manifest file.
|
|
||||||
* So we first need to resolve it relative to the root of this hydrogen instance.
|
|
||||||
*/
|
|
||||||
cssLocation = new URL(cssLocation, new URL(manifestLocation, window.location.origin)).href;
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const variant = themeId.match(/.+-(.+)/)?.[1];
|
|
||||||
const { name: variantName, default: isDefault, dark } = manifest.values.variants[variant!];
|
|
||||||
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;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Non-default variants are keyed in themeMapping with "theme_name variant_name"
|
|
||||||
// eg: "Element Dark"
|
|
||||||
this._themeMapping[themeDisplayName] = {
|
|
||||||
cssLocation,
|
|
||||||
id: themeId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
//Add the default-theme as an additional option to the mapping
|
|
||||||
const defaultThemeId = this.getDefaultTheme();
|
|
||||||
if (defaultThemeId) {
|
|
||||||
const themeDetails = this._findThemeDetailsFromId(defaultThemeId);
|
|
||||||
if (themeDetails) {
|
|
||||||
this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.cssLocation };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
l.log({ l: "Default Theme", theme: defaultThemeId});
|
|
||||||
l.log({ l: "Preferred colorscheme", scheme: this.preferredColorScheme === ColorSchemePreference.Dark ? "dark" : "light" });
|
|
||||||
l.log({ l: "Result", themeMapping: this._themeMapping });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 themeDetails = this._themeMapping[themeName];
|
|
||||||
if ("id" in themeDetails) {
|
|
||||||
cssLocation = themeDetails.cssLocation;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (!themeVariant) {
|
|
||||||
throw new Error("themeVariant is undefined!");
|
|
||||||
}
|
|
||||||
cssLocation = themeDetails[themeVariant].cssLocation;
|
|
||||||
}
|
|
||||||
this._platform.replaceStylesheet(cssLocation);
|
|
||||||
this._platform.settingsStorage.setString("theme-name", themeName);
|
|
||||||
if (themeVariant) {
|
|
||||||
this._platform.settingsStorage.setString("theme-variant", themeVariant);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this._platform.settingsStorage.remove("theme-variant");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Maps theme display name to theme information */
|
|
||||||
get themeMapping(): Record<string, ThemeInformation> {
|
|
||||||
return this._themeMapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getActiveTheme(): Promise<{themeName: string, themeVariant?: string}> {
|
|
||||||
let themeName = await this._platform.settingsStorage.getString("theme-name");
|
|
||||||
let themeVariant = await this._platform.settingsStorage.getString("theme-variant");
|
|
||||||
if (!themeName || !this._themeMapping[themeName]) {
|
|
||||||
themeName = "Default" in this._themeMapping ? "Default" : Object.keys(this._themeMapping)[0];
|
|
||||||
if (!this._themeMapping[themeName][themeVariant]) {
|
|
||||||
themeVariant = "default" in this._themeMapping[themeName] ? "default" : undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { themeName, themeVariant };
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultTheme(): string | undefined {
|
|
||||||
switch (this.preferredColorScheme) {
|
|
||||||
case ColorSchemePreference.Dark:
|
|
||||||
return this._platform.config["defaultTheme"]?.dark;
|
|
||||||
case ColorSchemePreference.Light:
|
|
||||||
return this._platform.config["defaultTheme"]?.light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _findThemeDetailsFromId(themeId: string): {themeName: string, cssLocation: string, variant?: string} | undefined {
|
|
||||||
for (const [themeName, themeData] of Object.entries(this._themeMapping)) {
|
|
||||||
if ("id" in themeData && themeData.id === themeId) {
|
|
||||||
return { themeName, cssLocation: themeData.cssLocation };
|
|
||||||
}
|
|
||||||
else if ("light" in themeData && themeData.light?.id === themeId) {
|
|
||||||
return { themeName, cssLocation: themeData.light.cssLocation, variant: "light" };
|
|
||||||
}
|
|
||||||
else if ("dark" in themeData && themeData.dark?.id === themeId) {
|
|
||||||
return { themeName, cssLocation: themeData.dark.cssLocation, variant: "dark" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get preferredColorScheme(): ColorSchemePreference | undefined {
|
|
||||||
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
||||||
return ColorSchemePreference.Dark;
|
|
||||||
}
|
|
||||||
else if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
|
||||||
return ColorSchemePreference.Light;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -115,6 +115,9 @@ export function createFetchRequest(createTimeout, serviceWorkerHandler) {
|
||||||
} else if (format === "buffer") {
|
} else if (format === "buffer") {
|
||||||
body = await response.arrayBuffer();
|
body = await response.arrayBuffer();
|
||||||
}
|
}
|
||||||
|
else if (format === "text") {
|
||||||
|
body = await response.text();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// some error pages return html instead of json, ignore error
|
// some error pages return html instead of json, ignore error
|
||||||
if (!(err.name === "SyntaxError" && status >= 400)) {
|
if (!(err.name === "SyntaxError" && status >= 400)) {
|
||||||
|
|
130
src/platform/web/theming/DerivedVariables.ts
Normal file
130
src/platform/web/theming/DerivedVariables.ts
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
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 {derive} from "./shared/color.mjs";
|
||||||
|
|
||||||
|
export class DerivedVariables {
|
||||||
|
private _baseVariables: Record<string, string>;
|
||||||
|
private _variablesToDerive: string[]
|
||||||
|
private _isDark: boolean
|
||||||
|
private _aliases: Record<string, string> = {};
|
||||||
|
private _derivedAliases: string[] = [];
|
||||||
|
|
||||||
|
constructor(baseVariables: Record<string, string>, variablesToDerive: string[], isDark: boolean) {
|
||||||
|
this._baseVariables = baseVariables;
|
||||||
|
this._variablesToDerive = variablesToDerive;
|
||||||
|
this._isDark = isDark;
|
||||||
|
}
|
||||||
|
|
||||||
|
toVariables(): Record<string, string> {
|
||||||
|
const resolvedVariables: any = {};
|
||||||
|
this._detectAliases();
|
||||||
|
for (const variable of this._variablesToDerive) {
|
||||||
|
const resolvedValue = this._derive(variable);
|
||||||
|
if (resolvedValue) {
|
||||||
|
resolvedVariables[variable] = resolvedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [alias, variable] of Object.entries(this._aliases) as any) {
|
||||||
|
resolvedVariables[alias] = this._baseVariables[variable] ?? resolvedVariables[variable];
|
||||||
|
}
|
||||||
|
for (const variable of this._derivedAliases) {
|
||||||
|
const resolvedValue = this._deriveAlias(variable, resolvedVariables);
|
||||||
|
if (resolvedValue) {
|
||||||
|
resolvedVariables[variable] = resolvedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolvedVariables;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _detectAliases(): void {
|
||||||
|
const newVariablesToDerive: string[] = [];
|
||||||
|
for (const variable of this._variablesToDerive) {
|
||||||
|
const [alias, value] = variable.split("=");
|
||||||
|
if (value) {
|
||||||
|
this._aliases[alias] = value;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
newVariablesToDerive.push(variable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._variablesToDerive = newVariablesToDerive;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _derive(variable: string): string | undefined {
|
||||||
|
const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/;
|
||||||
|
const matches = variable.match(RE_VARIABLE_VALUE);
|
||||||
|
if (matches) {
|
||||||
|
const [, baseVariable, operation, argument] = matches;
|
||||||
|
const value = this._baseVariables[baseVariable];
|
||||||
|
if (!value ) {
|
||||||
|
if (this._aliases[baseVariable]) {
|
||||||
|
this._derivedAliases.push(variable);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(`Cannot find value for base variable "${baseVariable}"!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const resolvedValue = derive(value, operation, argument, this._isDark);
|
||||||
|
return resolvedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _deriveAlias(variable: string, resolvedVariables: Record<string, string>): string | undefined {
|
||||||
|
const RE_VARIABLE_VALUE = /(.+)--(.+)-(.+)/;
|
||||||
|
const matches = variable.match(RE_VARIABLE_VALUE);
|
||||||
|
if (matches) {
|
||||||
|
const [, baseVariable, operation, argument] = matches;
|
||||||
|
const value = resolvedVariables[baseVariable];
|
||||||
|
if (!value ) {
|
||||||
|
throw new Error(`Cannot find value for alias "${baseVariable}" when trying to derive ${variable}!`);
|
||||||
|
}
|
||||||
|
const resolvedValue = derive(value, operation, argument, this._isDark);
|
||||||
|
return resolvedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
import pkg from "off-color";
|
||||||
|
const {offColor} = pkg;
|
||||||
|
|
||||||
|
export function tests() {
|
||||||
|
return {
|
||||||
|
"Simple variable derivation": assert => {
|
||||||
|
const deriver = new DerivedVariables({ "background-color": "#ff00ff" }, ["background-color--darker-5"], false);
|
||||||
|
const result = deriver.toVariables();
|
||||||
|
const resultColor = offColor("#ff00ff").darken(5/100).hex();
|
||||||
|
assert.deepEqual(result, {"background-color--darker-5": resultColor});
|
||||||
|
},
|
||||||
|
|
||||||
|
"For dark themes, lighten and darken are inverted": assert => {
|
||||||
|
const deriver = new DerivedVariables({ "background-color": "#ff00ff" }, ["background-color--darker-5"], true);
|
||||||
|
const result = deriver.toVariables();
|
||||||
|
const resultColor = offColor("#ff00ff").lighten(5/100).hex();
|
||||||
|
assert.deepEqual(result, {"background-color--darker-5": resultColor});
|
||||||
|
},
|
||||||
|
|
||||||
|
"Aliases can be derived": assert => {
|
||||||
|
const deriver = new DerivedVariables({ "background-color": "#ff00ff" }, ["my-awesome-alias=background-color","my-awesome-alias--darker-5"], false);
|
||||||
|
const result = deriver.toVariables();
|
||||||
|
const resultColor = offColor("#ff00ff").darken(5/100).hex();
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
"my-awesome-alias": "#ff00ff",
|
||||||
|
"my-awesome-alias--darker-5": resultColor,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
79
src/platform/web/theming/IconColorizer.ts
Normal file
79
src/platform/web/theming/IconColorizer.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
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 {Platform} from "../Platform.js";
|
||||||
|
import {getColoredSvgString} from "./shared/svg-colorizer.mjs";
|
||||||
|
|
||||||
|
type ParsedStructure = {
|
||||||
|
[variableName: string]: {
|
||||||
|
svg: Promise<{ status: number; body: string }>;
|
||||||
|
primary: string | null;
|
||||||
|
secondary: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export class IconColorizer {
|
||||||
|
private _iconVariables: Record<string, string>;
|
||||||
|
private _resolvedVariables: Record<string, string>;
|
||||||
|
private _manifestLocation: string;
|
||||||
|
private _platform: Platform;
|
||||||
|
|
||||||
|
constructor(platform: Platform, iconVariables: Record<string, string>, resolvedVariables: Record<string, string>, manifestLocation: string) {
|
||||||
|
this._platform = platform;
|
||||||
|
this._iconVariables = iconVariables;
|
||||||
|
this._resolvedVariables = resolvedVariables;
|
||||||
|
this._manifestLocation = manifestLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
async toVariables(): Promise<Record<string, string>> {
|
||||||
|
const { parsedStructure, promises } = await this._fetchAndParseIcons();
|
||||||
|
await Promise.all(promises);
|
||||||
|
return this._produceColoredIconVariables(parsedStructure);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchAndParseIcons(): Promise<{ parsedStructure: ParsedStructure, promises: any[] }> {
|
||||||
|
const promises: any[] = [];
|
||||||
|
const parsedStructure: ParsedStructure = {};
|
||||||
|
for (const [variable, url] of Object.entries(this._iconVariables)) {
|
||||||
|
const urlObject = new URL(`https://${url}`);
|
||||||
|
const pathWithoutQueryParams = urlObject.hostname;
|
||||||
|
const relativePath = new URL(pathWithoutQueryParams, new URL(this._manifestLocation, window.location.origin));
|
||||||
|
const responsePromise = this._platform.request(relativePath, { method: "GET", format: "text", cache: true, }).response()
|
||||||
|
promises.push(responsePromise);
|
||||||
|
const searchParams = urlObject.searchParams;
|
||||||
|
parsedStructure[variable] = {
|
||||||
|
svg: responsePromise,
|
||||||
|
primary: searchParams.get("primary"),
|
||||||
|
secondary: searchParams.get("secondary")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { parsedStructure, promises };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _produceColoredIconVariables(parsedStructure: ParsedStructure): Promise<Record<string, string>> {
|
||||||
|
let coloredVariables: Record<string, string> = {};
|
||||||
|
for (const [variable, { svg, primary, secondary }] of Object.entries(parsedStructure)) {
|
||||||
|
const { body: svgCode } = await svg;
|
||||||
|
if (!primary) {
|
||||||
|
throw new Error(`Primary color variable ${primary} not in list of variables!`);
|
||||||
|
}
|
||||||
|
const primaryColor = this._resolvedVariables[primary], secondaryColor = this._resolvedVariables[secondary!];
|
||||||
|
const coloredSvgCode = getColoredSvgString(svgCode, primaryColor, secondaryColor);
|
||||||
|
const dataURI = `url('data:image/svg+xml;utf8,${encodeURIComponent(coloredSvgCode)}')`;
|
||||||
|
coloredVariables[variable] = dataURI;
|
||||||
|
}
|
||||||
|
return coloredVariables;
|
||||||
|
}
|
||||||
|
}
|
188
src/platform/web/theming/ThemeLoader.ts
Normal file
188
src/platform/web/theming/ThemeLoader.ts
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
/*
|
||||||
|
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 {ILogItem} from "../../../logging/types";
|
||||||
|
import type {Platform} from "../Platform.js";
|
||||||
|
import {RuntimeThemeParser} from "./parsers/RuntimeThemeParser";
|
||||||
|
import type {Variant, ThemeInformation} from "./parsers/types";
|
||||||
|
import {ColorSchemePreference} from "./parsers/types";
|
||||||
|
import {BuiltThemeParser} from "./parsers/BuiltThemeParser";
|
||||||
|
|
||||||
|
export class ThemeLoader {
|
||||||
|
private _platform: Platform;
|
||||||
|
private _themeMapping: Record<string, ThemeInformation>;
|
||||||
|
private _injectedVariables?: Record<string, string>;
|
||||||
|
|
||||||
|
constructor(platform: Platform) {
|
||||||
|
this._platform = platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(manifestLocations: string[], log?: ILogItem): Promise<void> {
|
||||||
|
await this._platform.logger.wrapOrRun(log, "ThemeLoader.init", async (log) => {
|
||||||
|
const results = await Promise.all(
|
||||||
|
manifestLocations.map(location => this._platform.request(location, { method: "GET", format: "json", cache: true, }).response())
|
||||||
|
);
|
||||||
|
const runtimeThemeParser = new RuntimeThemeParser(this._platform, this.preferredColorScheme);
|
||||||
|
const builtThemeParser = new BuiltThemeParser(this.preferredColorScheme);
|
||||||
|
const runtimeThemePromises: Promise<void>[] = [];
|
||||||
|
for (let i = 0; i < results.length; ++i) {
|
||||||
|
const { body } = results[i];
|
||||||
|
try {
|
||||||
|
if (body.extends) {
|
||||||
|
const indexOfBaseManifest = results.findIndex(manifest => manifest.body.id === body.extends);
|
||||||
|
if (indexOfBaseManifest === -1) {
|
||||||
|
throw new Error(`Base manifest for derived theme at ${manifestLocations[i]} not found!`);
|
||||||
|
}
|
||||||
|
const {body: baseManifest} = results[indexOfBaseManifest];
|
||||||
|
const baseManifestLocation = manifestLocations[indexOfBaseManifest];
|
||||||
|
const promise = runtimeThemeParser.parse(body, baseManifest, baseManifestLocation, log);
|
||||||
|
runtimeThemePromises.push(promise);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
builtThemeParser.parse(body, manifestLocations[i], log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(runtimeThemePromises);
|
||||||
|
this._themeMapping = { ...builtThemeParser.themeMapping, ...runtimeThemeParser.themeMapping };
|
||||||
|
Object.assign(this._themeMapping, builtThemeParser.themeMapping, runtimeThemeParser.themeMapping);
|
||||||
|
this._addDefaultThemeToMapping(log);
|
||||||
|
log.log({ l: "Preferred colorscheme", scheme: this.preferredColorScheme === ColorSchemePreference.Dark ? "dark" : "light" });
|
||||||
|
log.log({ l: "Result", themeMapping: this._themeMapping });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(themeName: string, themeVariant?: "light" | "dark" | "default", log?: ILogItem) {
|
||||||
|
this._platform.logger.wrapOrRun(log, { l: "change theme", name: themeName, variant: themeVariant }, () => {
|
||||||
|
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) {
|
||||||
|
log?.log({l: "Derived Theme", variables});
|
||||||
|
this._injectCSSVariables(variables);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._removePreviousCSSVariables();
|
||||||
|
}
|
||||||
|
this._platform.settingsStorage.setString("theme-name", themeName);
|
||||||
|
if (themeVariant) {
|
||||||
|
this._platform.settingsStorage.setString("theme-variant", themeVariant);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._platform.settingsStorage.remove("theme-variant");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _injectCSSVariables(variables: Record<string, string>): void {
|
||||||
|
const root = document.documentElement;
|
||||||
|
for (const [variable, value] of Object.entries(variables)) {
|
||||||
|
root.style.setProperty(`--${variable}`, value);
|
||||||
|
}
|
||||||
|
this._injectedVariables = variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removePreviousCSSVariables(): void {
|
||||||
|
if (!this._injectedVariables) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const root = document.documentElement;
|
||||||
|
for (const variable of Object.keys(this._injectedVariables)) {
|
||||||
|
root.style.removeProperty(`--${variable}`);
|
||||||
|
}
|
||||||
|
this._injectedVariables = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Maps theme display name to theme information */
|
||||||
|
get themeMapping(): Record<string, ThemeInformation> {
|
||||||
|
return this._themeMapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActiveTheme(): Promise<{themeName: string, themeVariant?: string}> {
|
||||||
|
let themeName = await this._platform.settingsStorage.getString("theme-name");
|
||||||
|
let themeVariant = await this._platform.settingsStorage.getString("theme-variant");
|
||||||
|
if (!themeName || !this._themeMapping[themeName]) {
|
||||||
|
themeName = "Default" in this._themeMapping ? "Default" : Object.keys(this._themeMapping)[0];
|
||||||
|
if (!this._themeMapping[themeName][themeVariant]) {
|
||||||
|
themeVariant = "default" in this._themeMapping[themeName] ? "default" : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { themeName, themeVariant };
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultTheme(): string | undefined {
|
||||||
|
switch (this.preferredColorScheme) {
|
||||||
|
case ColorSchemePreference.Dark:
|
||||||
|
return this._platform.config["defaultTheme"]?.dark;
|
||||||
|
case ColorSchemePreference.Light:
|
||||||
|
return this._platform.config["defaultTheme"]?.light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _findThemeDetailsFromId(themeId: string): {themeName: string, themeData: Partial<Variant>} | undefined {
|
||||||
|
for (const [themeName, themeData] of Object.entries(this._themeMapping)) {
|
||||||
|
if ("id" in themeData && themeData.id === themeId) {
|
||||||
|
return { themeName, themeData };
|
||||||
|
}
|
||||||
|
else if ("light" in themeData && themeData.light?.id === themeId) {
|
||||||
|
return { themeName, themeData: themeData.light };
|
||||||
|
}
|
||||||
|
else if ("dark" in themeData && themeData.dark?.id === themeId) {
|
||||||
|
return { themeName, themeData: themeData.dark };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addDefaultThemeToMapping(log: ILogItem) {
|
||||||
|
log.wrap("addDefaultThemeToMapping", l => {
|
||||||
|
const defaultThemeId = this.getDefaultTheme();
|
||||||
|
if (defaultThemeId) {
|
||||||
|
const themeDetails = this._findThemeDetailsFromId(defaultThemeId);
|
||||||
|
if (themeDetails) {
|
||||||
|
this._themeMapping["Default"] = { id: "default", cssLocation: themeDetails.themeData.cssLocation! };
|
||||||
|
const variables = themeDetails.themeData.variables;
|
||||||
|
if (variables) {
|
||||||
|
this._themeMapping["Default"].variables = variables;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l.log({ l: "Default Theme", theme: defaultThemeId});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get preferredColorScheme(): ColorSchemePreference | undefined {
|
||||||
|
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
|
return ColorSchemePreference.Dark;
|
||||||
|
}
|
||||||
|
else if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
||||||
|
return ColorSchemePreference.Light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
106
src/platform/web/theming/parsers/BuiltThemeParser.ts
Normal file
106
src/platform/web/theming/parsers/BuiltThemeParser.ts
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
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 "./types";
|
||||||
|
import type {ThemeManifest} from "../../../types/theme";
|
||||||
|
import type {ILogItem} from "../../../../logging/types";
|
||||||
|
import {ColorSchemePreference} from "./types";
|
||||||
|
|
||||||
|
export class BuiltThemeParser {
|
||||||
|
private _themeMapping: Record<string, ThemeInformation> = {};
|
||||||
|
private _preferredColorScheme?: ColorSchemePreference;
|
||||||
|
|
||||||
|
constructor(preferredColorScheme?: ColorSchemePreference) {
|
||||||
|
this._preferredColorScheme = preferredColorScheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(manifest: ThemeManifest, manifestLocation: string, log: ILogItem) {
|
||||||
|
log.wrap("BuiltThemeParser.parse", () => {
|
||||||
|
/*
|
||||||
|
After build has finished, the source section of each theme manifest
|
||||||
|
contains `built-assets` which is a mapping from the theme-id to
|
||||||
|
cssLocation of theme
|
||||||
|
*/
|
||||||
|
const builtAssets: Record<string, string> = manifest.source?.["built-assets"];
|
||||||
|
const themeName = manifest.name;
|
||||||
|
if (!themeName) {
|
||||||
|
throw new Error(`Theme name not found in manifest at ${manifestLocation}`);
|
||||||
|
}
|
||||||
|
let defaultDarkVariant: any = {}, defaultLightVariant: any = {};
|
||||||
|
for (let [themeId, cssLocation] of Object.entries(builtAssets)) {
|
||||||
|
try {
|
||||||
|
/**
|
||||||
|
* This cssLocation is relative to the location of the manifest file.
|
||||||
|
* So we first need to resolve it relative to the root of this hydrogen instance.
|
||||||
|
*/
|
||||||
|
cssLocation = new URL(cssLocation, new URL(manifestLocation, window.location.origin)).href;
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const variant = themeId.match(/.+-(.+)/)?.[1];
|
||||||
|
const variantDetails = manifest.values?.variants[variant!];
|
||||||
|
if (!variantDetails) {
|
||||||
|
throw new Error(`Variant ${variant} is missing in manifest at ${manifestLocation}`);
|
||||||
|
}
|
||||||
|
const { name: variantName, default: isDefault, dark } = variantDetails;
|
||||||
|
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;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Non-default variants are keyed in themeMapping with "theme_name variant_name"
|
||||||
|
// eg: "Element Dark"
|
||||||
|
this._themeMapping[themeDisplayName] = {
|
||||||
|
cssLocation,
|
||||||
|
id: themeId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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(): Record<string, ThemeInformation> {
|
||||||
|
return this._themeMapping;
|
||||||
|
}
|
||||||
|
}
|
98
src/platform/web/theming/parsers/RuntimeThemeParser.ts
Normal file
98
src/platform/web/theming/parsers/RuntimeThemeParser.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
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 "./types";
|
||||||
|
import type {Platform} from "../../Platform.js";
|
||||||
|
import type {ThemeManifest} from "../../../types/theme";
|
||||||
|
import {ColorSchemePreference} from "./types";
|
||||||
|
import {IconColorizer} from "../IconColorizer";
|
||||||
|
import {DerivedVariables} from "../DerivedVariables";
|
||||||
|
import {ILogItem} from "../../../../logging/types";
|
||||||
|
|
||||||
|
export class RuntimeThemeParser {
|
||||||
|
private _themeMapping: Record<string, ThemeInformation> = {};
|
||||||
|
private _preferredColorScheme?: ColorSchemePreference;
|
||||||
|
private _platform: Platform;
|
||||||
|
|
||||||
|
constructor(platform: Platform, preferredColorScheme?: ColorSchemePreference) {
|
||||||
|
this._preferredColorScheme = preferredColorScheme;
|
||||||
|
this._platform = platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
async parse(manifest: ThemeManifest, baseManifest: ThemeManifest, baseManifestLocation: string, log: ILogItem): Promise<void> {
|
||||||
|
await log.wrap("RuntimeThemeParser.parse", async () => {
|
||||||
|
const {cssLocation, derivedVariables, icons} = this._getSourceData(baseManifest, baseManifestLocation, log);
|
||||||
|
const themeName = manifest.name;
|
||||||
|
if (!themeName) {
|
||||||
|
throw new Error(`Theme name not found in manifest!`);
|
||||||
|
}
|
||||||
|
let defaultDarkVariant: any = {}, defaultLightVariant: any = {};
|
||||||
|
for (const [variant, variantDetails] of Object.entries(manifest.values?.variants!) as [string, any][]) {
|
||||||
|
try {
|
||||||
|
const themeId = `${manifest.id}-${variant}`;
|
||||||
|
const { name: variantName, default: isDefault, dark, variables } = variantDetails;
|
||||||
|
const resolvedVariables = new DerivedVariables(variables, derivedVariables, dark).toVariables();
|
||||||
|
Object.assign(variables, resolvedVariables);
|
||||||
|
const iconVariables = await new IconColorizer(this._platform, icons, variables, baseManifestLocation).toVariables();
|
||||||
|
Object.assign(variables, resolvedVariables, iconVariables);
|
||||||
|
const themeDisplayName = `${themeName} ${variantName}`;
|
||||||
|
if (isDefault) {
|
||||||
|
const defaultVariant = dark ? defaultDarkVariant : defaultLightVariant;
|
||||||
|
Object.assign(defaultVariant, { variantName, id: themeId, cssLocation, variables });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this._themeMapping[themeDisplayName] = { cssLocation, id: themeId, variables: variables, };
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (defaultDarkVariant.id && defaultLightVariant.id) {
|
||||||
|
const defaultVariant = this._preferredColorScheme === ColorSchemePreference.Dark ? defaultDarkVariant : defaultLightVariant;
|
||||||
|
this._themeMapping[themeName] = { dark: defaultDarkVariant, light: defaultLightVariant, default: defaultVariant };
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const variant = defaultDarkVariant.id ? defaultDarkVariant : defaultLightVariant;
|
||||||
|
this._themeMapping[`${themeName} ${variant.variantName}`] = { id: variant.id, cssLocation: variant.cssLocation };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getSourceData(manifest: ThemeManifest, location: string, log: ILogItem)
|
||||||
|
: { cssLocation: string, derivedVariables: string[], icons: Record<string, string>} {
|
||||||
|
return log.wrap("getSourceData", () => {
|
||||||
|
const runtimeCSSLocation = manifest.source?.["runtime-asset"];
|
||||||
|
if (!runtimeCSSLocation) {
|
||||||
|
throw new Error(`Run-time asset not found in source section for theme at ${location}`);
|
||||||
|
}
|
||||||
|
const cssLocation = new URL(runtimeCSSLocation, new URL(location, window.location.origin)).href;
|
||||||
|
const derivedVariables = manifest.source?.["derived-variables"];
|
||||||
|
if (!derivedVariables) {
|
||||||
|
throw new Error(`Derived variables not found in source section for theme at ${location}`);
|
||||||
|
}
|
||||||
|
const icons = manifest.source?.["icon"];
|
||||||
|
if (!icons) {
|
||||||
|
throw new Error(`Icon mapping not found in source section for theme at ${location}`);
|
||||||
|
}
|
||||||
|
return { cssLocation, derivedVariables, icons };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get themeMapping(): Record<string, ThemeInformation> {
|
||||||
|
return this._themeMapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
38
src/platform/web/theming/parsers/types.ts
Normal file
38
src/platform/web/theming/parsers/types.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type NormalVariant = {
|
||||||
|
id: string;
|
||||||
|
cssLocation: string;
|
||||||
|
variables?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Variant = NormalVariant & {
|
||||||
|
variantName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DefaultVariant = {
|
||||||
|
dark: Variant;
|
||||||
|
light: Variant;
|
||||||
|
default: Variant;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ThemeInformation = NormalVariant | DefaultVariant;
|
||||||
|
|
||||||
|
export enum ColorSchemePreference {
|
||||||
|
Dark,
|
||||||
|
Light
|
||||||
|
};
|
|
@ -13,10 +13,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import pkg from 'off-color';
|
||||||
|
const offColor = pkg.offColor;
|
||||||
|
|
||||||
const offColor = require("off-color").offColor;
|
export function derive(value, operation, argument, isDark) {
|
||||||
|
|
||||||
module.exports.derive = function (value, operation, argument, isDark) {
|
|
||||||
const argumentAsNumber = parseInt(argument);
|
const argumentAsNumber = parseInt(argument);
|
||||||
if (isDark) {
|
if (isDark) {
|
||||||
// For dark themes, invert the operation
|
// For dark themes, invert the operation
|
24
src/platform/web/theming/shared/svg-colorizer.mjs
Normal file
24
src/platform/web/theming/shared/svg-colorizer.mjs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function getColoredSvgString(svgString, primaryColor, secondaryColor) {
|
||||||
|
let coloredSVGCode = svgString.replaceAll("#ff00ff", primaryColor);
|
||||||
|
coloredSVGCode = coloredSVGCode.replaceAll("#00ffff", secondaryColor);
|
||||||
|
if (svgString === coloredSVGCode) {
|
||||||
|
throw new Error("svg-colorizer made no color replacements! The input svg should only contain colors #ff00ff (primary, case-sensitive) and #00ffff (secondary, case-sensitive).");
|
||||||
|
}
|
||||||
|
return coloredSVGCode;
|
||||||
|
}
|
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": "Gruvbox",
|
||||||
|
"variables": {
|
||||||
|
"background-color-primary": "#282828",
|
||||||
|
"background-color-secondary": "#3c3836",
|
||||||
|
"text-color": "#fbf1c7",
|
||||||
|
"accent-color": "#8ec07c",
|
||||||
|
"error-color": "#fb4934",
|
||||||
|
"fixed-white": "#fff",
|
||||||
|
"room-badge": "#cc241d",
|
||||||
|
"link-color": "#fe8019"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,8 +8,8 @@ const path = require("path");
|
||||||
const manifest = require("./package.json");
|
const manifest = require("./package.json");
|
||||||
const version = manifest.version;
|
const version = manifest.version;
|
||||||
const compiledVariables = new Map();
|
const compiledVariables = new Map();
|
||||||
const derive = require("./scripts/postcss/color").derive;
|
import {buildColorizedSVG as replacer} from "./scripts/postcss/svg-builder.mjs";
|
||||||
const replacer = require("./scripts/postcss/svg-colorizer").buildColorizedSVG;
|
import {derive} from "./src/platform/web/theming/shared/color.mjs";
|
||||||
|
|
||||||
const commonOptions = {
|
const commonOptions = {
|
||||||
logLevel: "warn",
|
logLevel: "warn",
|
||||||
|
|
58
yarn.lock
58
yarn.lock
|
@ -77,6 +77,11 @@
|
||||||
"@nodelib/fs.scandir" "2.1.5"
|
"@nodelib/fs.scandir" "2.1.5"
|
||||||
fastq "^1.6.0"
|
fastq "^1.6.0"
|
||||||
|
|
||||||
|
"@trysound/sax@0.2.0":
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
|
||||||
|
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
|
||||||
|
|
||||||
"@types/json-schema@^7.0.7":
|
"@types/json-schema@^7.0.7":
|
||||||
version "7.0.9"
|
version "7.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
|
||||||
|
@ -347,6 +352,11 @@ commander@^6.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
|
||||||
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
|
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
|
||||||
|
|
||||||
|
commander@^7.2.0:
|
||||||
|
version "7.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
||||||
|
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
|
@ -382,11 +392,26 @@ css-select@^4.1.3:
|
||||||
domutils "^2.6.0"
|
domutils "^2.6.0"
|
||||||
nth-check "^2.0.0"
|
nth-check "^2.0.0"
|
||||||
|
|
||||||
|
css-tree@^1.1.2, css-tree@^1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
|
||||||
|
integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
|
||||||
|
dependencies:
|
||||||
|
mdn-data "2.0.14"
|
||||||
|
source-map "^0.6.1"
|
||||||
|
|
||||||
css-what@^5.0.0:
|
css-what@^5.0.0:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad"
|
resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad"
|
||||||
integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==
|
integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==
|
||||||
|
|
||||||
|
csso@^4.2.0:
|
||||||
|
version "4.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
|
||||||
|
integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
|
||||||
|
dependencies:
|
||||||
|
css-tree "^1.1.2"
|
||||||
|
|
||||||
cuint@^0.2.2:
|
cuint@^0.2.2:
|
||||||
version "0.2.2"
|
version "0.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
|
resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
|
||||||
|
@ -1197,6 +1222,11 @@ lru-cache@^6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
|
mdn-data@2.0.14:
|
||||||
|
version "2.0.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
|
||||||
|
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
|
||||||
|
|
||||||
mdn-polyfills@^5.20.0:
|
mdn-polyfills@^5.20.0:
|
||||||
version "5.20.0"
|
version "5.20.0"
|
||||||
resolved "https://registry.yarnpkg.com/mdn-polyfills/-/mdn-polyfills-5.20.0.tgz#ca8247edf20a4f60dec6804372229812b348260b"
|
resolved "https://registry.yarnpkg.com/mdn-polyfills/-/mdn-polyfills-5.20.0.tgz#ca8247edf20a4f60dec6804372229812b348260b"
|
||||||
|
@ -1500,7 +1530,7 @@ source-map-js@^1.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||||
|
|
||||||
source-map@~0.6.1:
|
source-map@^0.6.1, source-map@~0.6.1:
|
||||||
version "0.6.1"
|
version "0.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||||
|
@ -1510,6 +1540,11 @@ sprintf-js@~1.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||||
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
||||||
|
|
||||||
|
stable@^0.1.8:
|
||||||
|
version "0.1.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
|
||||||
|
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
|
||||||
|
|
||||||
string-width@^4.2.0:
|
string-width@^4.2.0:
|
||||||
version "4.2.2"
|
version "4.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
|
||||||
|
@ -1550,6 +1585,19 @@ supports-preserve-symlinks-flag@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||||
|
|
||||||
|
svgo@^2.8.0:
|
||||||
|
version "2.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24"
|
||||||
|
integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==
|
||||||
|
dependencies:
|
||||||
|
"@trysound/sax" "0.2.0"
|
||||||
|
commander "^7.2.0"
|
||||||
|
css-select "^4.1.3"
|
||||||
|
css-tree "^1.1.3"
|
||||||
|
csso "^4.2.0"
|
||||||
|
picocolors "^1.0.0"
|
||||||
|
stable "^0.1.8"
|
||||||
|
|
||||||
table@^6.0.9:
|
table@^6.0.9:
|
||||||
version "6.7.1"
|
version "6.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2"
|
resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2"
|
||||||
|
@ -1617,10 +1665,10 @@ type-fest@^0.20.2:
|
||||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
|
||||||
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
|
||||||
|
|
||||||
typescript@^4.3.5:
|
typescript@^4.7.0:
|
||||||
version "4.3.5"
|
version "4.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
|
||||||
integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
|
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
|
||||||
|
|
||||||
typeson-registry@^1.0.0-alpha.20:
|
typeson-registry@^1.0.0-alpha.20:
|
||||||
version "1.0.0-alpha.39"
|
version "1.0.0-alpha.39"
|
||||||
|
|
Reference in a new issue