mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-11-20 03:25:52 +01:00
Merge branch 'master' into madlittlemods/assets-path-for-assets
This commit is contained in:
commit
fd3a0f0126
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.DS_Store
|
||||
node_modules
|
||||
fetchlogs
|
||||
sessionexports
|
||||
|
@ -1,10 +1,13 @@
|
||||
{
|
||||
"name": "hydrogen-web",
|
||||
"version": "0.2.29",
|
||||
"version": "0.2.32",
|
||||
"description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB",
|
||||
"directories": {
|
||||
"doc": "doc"
|
||||
},
|
||||
"enginesStrict": {
|
||||
"node": ">=15"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint --cache src/",
|
||||
"lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts",
|
||||
|
@ -13,7 +13,7 @@ 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.
|
||||
*/
|
||||
const path = require('path');
|
||||
const path = require('path').posix;
|
||||
|
||||
async function readCSSSource(location) {
|
||||
const fs = require("fs").promises;
|
||||
@ -246,6 +246,9 @@ module.exports = function buildThemes(options) {
|
||||
},
|
||||
|
||||
generateBundle(_, bundle) {
|
||||
// assetMap: Mapping from asset-name (eg: element-dark.css) to AssetInfo
|
||||
// chunkMap: Mapping from theme-location (eg: hydrogen-web/src/.../css/themes/element) to a list of ChunkInfo
|
||||
// types of AssetInfo and ChunkInfo can be found at https://rollupjs.org/guide/en/#generatebundle
|
||||
const { assetMap, chunkMap, runtimeThemeChunk } = parseBundle(bundle);
|
||||
const manifestLocations = [];
|
||||
for (const [location, chunkArray] of chunkMap) {
|
||||
@ -254,10 +257,6 @@ module.exports = function buildThemes(options) {
|
||||
const derivedVariables = compiledVariables["derived-variables"];
|
||||
const icon = compiledVariables["icon"];
|
||||
const builtAssets = {};
|
||||
/**
|
||||
* Generate a mapping from theme name to asset hashed location of said theme in build output.
|
||||
* This can be used to enumerate themes during runtime.
|
||||
*/
|
||||
for (const chunk of chunkArray) {
|
||||
const [, name, variant] = chunk.fileName.match(/theme-(.+)-(.+)\.css/);
|
||||
builtAssets[`${name}-${variant}`] = assetMap.get(chunk.fileName).fileName;
|
||||
|
@ -30,12 +30,7 @@ const valueParser = require("postcss-value-parser");
|
||||
* The actual derivation is done outside the plugin in a callback.
|
||||
*/
|
||||
|
||||
let aliasMap;
|
||||
let resolvedMap;
|
||||
let baseVariables;
|
||||
let isDark;
|
||||
|
||||
function getValueFromAlias(alias) {
|
||||
function getValueFromAlias(alias, {aliasMap, baseVariables, resolvedMap}) {
|
||||
const derivedVariable = aliasMap.get(alias);
|
||||
return baseVariables.get(derivedVariable) ?? resolvedMap.get(derivedVariable);
|
||||
}
|
||||
@ -68,14 +63,15 @@ function parseDeclarationValue(value) {
|
||||
return variables;
|
||||
}
|
||||
|
||||
function resolveDerivedVariable(decl, derive) {
|
||||
function resolveDerivedVariable(decl, derive, maps, isDark) {
|
||||
const { baseVariables, resolvedMap } = maps;
|
||||
const RE_VARIABLE_VALUE = /(?:--)?((.+)--(.+)-(.+))/;
|
||||
const variableCollection = parseDeclarationValue(decl.value);
|
||||
for (const variable of variableCollection) {
|
||||
const matches = variable.match(RE_VARIABLE_VALUE);
|
||||
if (matches) {
|
||||
const [, wholeVariable, baseVariable, operation, argument] = matches;
|
||||
const value = baseVariables.get(baseVariable) ?? getValueFromAlias(baseVariable);
|
||||
const value = baseVariables.get(baseVariable) ?? getValueFromAlias(baseVariable, maps);
|
||||
if (!value) {
|
||||
throw new Error(`Cannot derive from ${baseVariable} because it is neither defined in config nor is it an alias!`);
|
||||
}
|
||||
@ -85,7 +81,7 @@ function resolveDerivedVariable(decl, derive) {
|
||||
}
|
||||
}
|
||||
|
||||
function extract(decl) {
|
||||
function extract(decl, {aliasMap, baseVariables}) {
|
||||
if (decl.variable) {
|
||||
// see if right side is of form "var(--foo)"
|
||||
const wholeVariable = decl.value.match(/var\(--(.+)\)/)?.[1];
|
||||
@ -100,7 +96,7 @@ function extract(decl) {
|
||||
}
|
||||
}
|
||||
|
||||
function addResolvedVariablesToRootSelector(root, {Rule, Declaration}) {
|
||||
function addResolvedVariablesToRootSelector(root, {Rule, Declaration}, {resolvedMap}) {
|
||||
const newRule = new Rule({ selector: ":root", source: root.source });
|
||||
// Add derived css variables to :root
|
||||
resolvedMap.forEach((value, key) => {
|
||||
@ -110,7 +106,7 @@ function addResolvedVariablesToRootSelector(root, {Rule, Declaration}) {
|
||||
root.append(newRule);
|
||||
}
|
||||
|
||||
function populateMapWithDerivedVariables(map, cssFileLocation) {
|
||||
function populateMapWithDerivedVariables(map, cssFileLocation, {resolvedMap, aliasMap}) {
|
||||
const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1];
|
||||
const derivedVariables = [
|
||||
...([...resolvedMap.keys()].filter(v => !aliasMap.has(v))),
|
||||
@ -133,10 +129,10 @@ function populateMapWithDerivedVariables(map, cssFileLocation) {
|
||||
* @param {Map} opts.compiledVariables - A map that stores derived variables so that manifest source sections can be produced
|
||||
*/
|
||||
module.exports = (opts = {}) => {
|
||||
aliasMap = new Map();
|
||||
resolvedMap = new Map();
|
||||
baseVariables = new Map();
|
||||
isDark = false;
|
||||
const aliasMap = new Map();
|
||||
const resolvedMap = new Map();
|
||||
const baseVariables = new Map();
|
||||
const maps = { aliasMap, resolvedMap, baseVariables };
|
||||
|
||||
return {
|
||||
postcssPlugin: "postcss-compile-variables",
|
||||
@ -147,16 +143,16 @@ module.exports = (opts = {}) => {
|
||||
// If this is a runtime theme, don't derive variables.
|
||||
return;
|
||||
}
|
||||
isDark = cssFileLocation.includes("dark=true");
|
||||
const isDark = cssFileLocation.includes("dark=true");
|
||||
/*
|
||||
Go through the CSS file once to extract all aliases and base variables.
|
||||
We use these when resolving derived variables later.
|
||||
*/
|
||||
root.walkDecls(decl => extract(decl));
|
||||
root.walkDecls(decl => resolveDerivedVariable(decl, opts.derive));
|
||||
addResolvedVariablesToRootSelector(root, {Rule, Declaration});
|
||||
root.walkDecls(decl => extract(decl, maps));
|
||||
root.walkDecls(decl => resolveDerivedVariable(decl, opts.derive, maps, isDark));
|
||||
addResolvedVariablesToRootSelector(root, {Rule, Declaration}, maps);
|
||||
if (opts.compiledVariables){
|
||||
populateMapWithDerivedVariables(opts.compiledVariables, cssFileLocation);
|
||||
populateMapWithDerivedVariables(opts.compiledVariables, cssFileLocation, maps);
|
||||
}
|
||||
// Also produce a mapping from alias to completely resolved color
|
||||
const resolvedAliasMap = new Map();
|
||||
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||
|
||||
const valueParser = require("postcss-value-parser");
|
||||
const resolve = require("path").resolve;
|
||||
let cssPath;
|
||||
|
||||
function colorsFromURL(url, colorMap) {
|
||||
const params = new URL(`file://${url}`).searchParams;
|
||||
@ -36,7 +35,7 @@ function colorsFromURL(url, colorMap) {
|
||||
return [primaryColor, secondaryColor];
|
||||
}
|
||||
|
||||
function processURL(decl, replacer, colorMap) {
|
||||
function processURL(decl, replacer, colorMap, cssPath) {
|
||||
const value = decl.value;
|
||||
const parsed = valueParser(value);
|
||||
parsed.walk(node => {
|
||||
@ -84,8 +83,8 @@ module.exports = (opts = {}) => {
|
||||
Go through each declaration and if it contains an URL, replace the url with the result
|
||||
of running replacer(url)
|
||||
*/
|
||||
cssPath = root.source?.input.file.replace(/[^/]*$/, "");
|
||||
root.walkDecls(decl => processURL(decl, opts.replacer, colorMap));
|
||||
const cssPath = root.source?.input.file.replace(/[^/]*$/, "");
|
||||
root.walkDecls(decl => processURL(decl, opts.replacer, colorMap, cssPath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -20,11 +20,9 @@ const valueParser = require("postcss-value-parser");
|
||||
* This plugin extracts content inside url() into css variables and adds the variables to the root section.
|
||||
* This plugin is used in conjunction with css-url-processor plugin to colorize svg icons.
|
||||
*/
|
||||
let counter;
|
||||
let urlVariables;
|
||||
const idToPrepend = "icon-url";
|
||||
|
||||
function findAndReplaceUrl(decl) {
|
||||
function findAndReplaceUrl(decl, urlVariables, counter) {
|
||||
const value = decl.value;
|
||||
const parsed = valueParser(value);
|
||||
parsed.walk(node => {
|
||||
@ -35,7 +33,8 @@ function findAndReplaceUrl(decl) {
|
||||
if (!url.match(/\.svg\?primary=.+/)) {
|
||||
return;
|
||||
}
|
||||
const variableName = `${idToPrepend}-${counter++}`;
|
||||
const count = counter.next().value;
|
||||
const variableName = `${idToPrepend}-${count}`;
|
||||
urlVariables.set(variableName, url);
|
||||
node.value = "var";
|
||||
node.nodes = [{ type: "word", value: `--${variableName}` }];
|
||||
@ -43,7 +42,7 @@ function findAndReplaceUrl(decl) {
|
||||
decl.assign({prop: decl.prop, value: parsed.toString()})
|
||||
}
|
||||
|
||||
function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) {
|
||||
function addResolvedVariablesToRootSelector(root, { Rule, Declaration }, urlVariables) {
|
||||
const newRule = new Rule({ selector: ":root", source: root.source });
|
||||
// Add derived css variables to :root
|
||||
urlVariables.forEach((value, key) => {
|
||||
@ -53,29 +52,35 @@ function addResolvedVariablesToRootSelector(root, { Rule, Declaration }) {
|
||||
root.append(newRule);
|
||||
}
|
||||
|
||||
function populateMapWithIcons(map, cssFileLocation) {
|
||||
function populateMapWithIcons(map, cssFileLocation, urlVariables) {
|
||||
const location = cssFileLocation.match(/(.+)\/.+\.css/)?.[1];
|
||||
const sharedObject = map.get(location);
|
||||
sharedObject["icon"] = Object.fromEntries(urlVariables);
|
||||
}
|
||||
|
||||
function *createCounter() {
|
||||
for (let i = 0; ; ++i) {
|
||||
yield i;
|
||||
}
|
||||
}
|
||||
|
||||
/* *
|
||||
* @type {import('postcss').PluginCreator}
|
||||
*/
|
||||
module.exports = (opts = {}) => {
|
||||
urlVariables = new Map();
|
||||
counter = 0;
|
||||
return {
|
||||
postcssPlugin: "postcss-url-to-variable",
|
||||
|
||||
Once(root, { Rule, Declaration }) {
|
||||
root.walkDecls(decl => findAndReplaceUrl(decl));
|
||||
const urlVariables = new Map();
|
||||
const counter = createCounter();
|
||||
root.walkDecls(decl => findAndReplaceUrl(decl, urlVariables, counter));
|
||||
if (urlVariables.size) {
|
||||
addResolvedVariablesToRootSelector(root, { Rule, Declaration });
|
||||
addResolvedVariablesToRootSelector(root, { Rule, Declaration }, urlVariables);
|
||||
}
|
||||
if (opts.compiledVariables){
|
||||
const cssFileLocation = root.source.input.from;
|
||||
populateMapWithIcons(opts.compiledVariables, cssFileLocation);
|
||||
populateMapWithIcons(opts.compiledVariables, cssFileLocation, urlVariables);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
65
src/domain/rageshake.ts
Normal file
65
src/domain/rageshake.ts
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright 2022 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 {BlobHandle} from "../platform/web/dom/BlobHandle";
|
||||
import type {RequestFunction} from "../platform/types/types";
|
||||
|
||||
// see https://github.com/matrix-org/rageshake#readme
|
||||
type RageshakeData = {
|
||||
// A textual description of the problem. Included in the details.log.gz file.
|
||||
text: string | undefined;
|
||||
// Application user-agent. Included in the details.log.gz file.
|
||||
userAgent: string;
|
||||
// Identifier for the application (eg 'riot-web'). Should correspond to a mapping configured in the configuration file for github issue reporting to work.
|
||||
app: string;
|
||||
// Application version. Included in the details.log.gz file.
|
||||
version: string;
|
||||
// Label to attach to the github issue, and include in the details file.
|
||||
label: string | undefined;
|
||||
};
|
||||
|
||||
export async function submitLogsToRageshakeServer(data: RageshakeData, logsBlob: BlobHandle, submitUrl: string, request: RequestFunction): Promise<void> {
|
||||
const formData = new Map<string, string | {name: string, blob: BlobHandle}>();
|
||||
if (data.text) {
|
||||
formData.set("text", data.text);
|
||||
}
|
||||
formData.set("user_agent", data.userAgent);
|
||||
formData.set("app", data.app);
|
||||
formData.set("version", data.version);
|
||||
if (data.label) {
|
||||
formData.set("label", data.label);
|
||||
}
|
||||
formData.set("file", {name: "logs.json", blob: logsBlob});
|
||||
const headers: Map<string, string> = new Map();
|
||||
headers.set("Accept", "application/json");
|
||||
const result = request(submitUrl, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers
|
||||
});
|
||||
let response;
|
||||
try {
|
||||
response = await result.response();
|
||||
} catch (err) {
|
||||
throw new Error(`Could not submit logs to ${submitUrl}, got error ${err.message}`);
|
||||
}
|
||||
const {status, body} = response;
|
||||
if (status < 200 || status >= 300) {
|
||||
throw new Error(`Could not submit logs to ${submitUrl}, got status code ${status} with body ${body}`);
|
||||
}
|
||||
// we don't bother with reading report_url from the body as the rageshake server doesn't always return it
|
||||
// and would have to have CORS setup properly for us to be able to read it.
|
||||
}
|
@ -27,6 +27,29 @@ export class BaseMediaTile extends BaseMessageTile {
|
||||
this._decryptedFile = null;
|
||||
this._isVisible = false;
|
||||
this._error = null;
|
||||
this._downloading = false;
|
||||
this._downloadError = null;
|
||||
}
|
||||
|
||||
async downloadMedia() {
|
||||
if (this._downloading || this.isPending) {
|
||||
return;
|
||||
}
|
||||
const content = this._getContent();
|
||||
const filename = content.body;
|
||||
this._downloading = true;
|
||||
this.emitChange("status");
|
||||
let blob;
|
||||
try {
|
||||
blob = await this._mediaRepository.downloadAttachment(content);
|
||||
this.platform.saveFileAs(blob, filename);
|
||||
} catch (err) {
|
||||
this._downloadError = err;
|
||||
} finally {
|
||||
blob?.dispose();
|
||||
this._downloading = false;
|
||||
}
|
||||
this.emitChange("status");
|
||||
}
|
||||
|
||||
get isUploading() {
|
||||
@ -38,7 +61,7 @@ export class BaseMediaTile extends BaseMessageTile {
|
||||
return pendingEvent && Math.round((pendingEvent.attachmentsSentBytes / pendingEvent.attachmentsTotalBytes) * 100);
|
||||
}
|
||||
|
||||
get sendStatus() {
|
||||
get status() {
|
||||
const {pendingEvent} = this._entry;
|
||||
switch (pendingEvent?.status) {
|
||||
case SendStatus.Waiting:
|
||||
@ -53,6 +76,12 @@ export class BaseMediaTile extends BaseMessageTile {
|
||||
case SendStatus.Error:
|
||||
return this.i18n`Error: ${pendingEvent.error.message}`;
|
||||
default:
|
||||
if (this._downloadError) {
|
||||
return `Download failed`;
|
||||
}
|
||||
if (this._downloading) {
|
||||
return this.i18n`Downloading…`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import {ViewModel} from "../../ViewModel";
|
||||
import {KeyBackupViewModel} from "./KeyBackupViewModel.js";
|
||||
import {submitLogsToRageshakeServer} from "../../../domain/rageshake";
|
||||
|
||||
class PushNotificationStatus {
|
||||
constructor() {
|
||||
@ -51,6 +52,7 @@ export class SettingsViewModel extends ViewModel {
|
||||
this.maxSentImageSizeLimit = 4000;
|
||||
this.pushNotifications = new PushNotificationStatus();
|
||||
this._activeTheme = undefined;
|
||||
this._logsFeedbackMessage = undefined;
|
||||
}
|
||||
|
||||
get _session() {
|
||||
@ -131,18 +133,14 @@ export class SettingsViewModel extends ViewModel {
|
||||
return this._formatBytes(this._estimate?.usage);
|
||||
}
|
||||
|
||||
get themes() {
|
||||
return this.platform.themeLoader.themes;
|
||||
get themeMapping() {
|
||||
return this.platform.themeLoader.themeMapping;
|
||||
}
|
||||
|
||||
get activeTheme() {
|
||||
return this._activeTheme;
|
||||
}
|
||||
|
||||
setTheme(name) {
|
||||
this.platform.themeLoader.setTheme(name);
|
||||
}
|
||||
|
||||
_formatBytes(n) {
|
||||
if (typeof n === "number") {
|
||||
return Math.round(n / (1024 * 1024)).toFixed(1) + " MB";
|
||||
@ -156,6 +154,51 @@ export class SettingsViewModel extends ViewModel {
|
||||
this.platform.saveFileAs(logExport.asBlob(), `hydrogen-logs-${this.platform.clock.now()}.json`);
|
||||
}
|
||||
|
||||
get canSendLogsToServer() {
|
||||
return !!this.platform.config.bugReportEndpointUrl;
|
||||
}
|
||||
|
||||
get logsServer() {
|
||||
const {bugReportEndpointUrl} = this.platform.config;
|
||||
try {
|
||||
if (bugReportEndpointUrl) {
|
||||
return new URL(bugReportEndpointUrl).hostname;
|
||||
}
|
||||
} catch (e) {}
|
||||
return "";
|
||||
}
|
||||
|
||||
async sendLogsToServer() {
|
||||
const {bugReportEndpointUrl} = this.platform.config;
|
||||
if (bugReportEndpointUrl) {
|
||||
this._logsFeedbackMessage = this.i18n`Sending logs…`;
|
||||
this.emitChange();
|
||||
try {
|
||||
const logExport = await this.logger.export();
|
||||
await submitLogsToRageshakeServer(
|
||||
{
|
||||
app: "hydrogen",
|
||||
userAgent: this.platform.description,
|
||||
version: DEFINE_VERSION,
|
||||
text: `Submit logs from settings for user ${this._session.userId} on device ${this._session.deviceId}`,
|
||||
},
|
||||
logExport.asBlob(),
|
||||
bugReportEndpointUrl,
|
||||
this.platform.request
|
||||
);
|
||||
this._logsFeedbackMessage = this.i18n`Logs sent succesfully!`;
|
||||
this.emitChange();
|
||||
} catch (err) {
|
||||
this._logsFeedbackMessage = err.message;
|
||||
this.emitChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get logsFeedbackMessage() {
|
||||
return this._logsFeedbackMessage;
|
||||
}
|
||||
|
||||
async togglePushNotifications() {
|
||||
this.pushNotifications.updating = true;
|
||||
this.pushNotifications.enabledOnServer = null;
|
||||
@ -185,5 +228,11 @@ export class SettingsViewModel extends ViewModel {
|
||||
this.emitChange("pushNotifications.serverError");
|
||||
}
|
||||
}
|
||||
|
||||
changeThemeOption(themeName, themeVariant) {
|
||||
this.platform.themeLoader.setTheme(themeName, themeVariant);
|
||||
// emit so that radio-buttons become displayed/hidden
|
||||
this.emitChange("themeOption");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,9 +17,12 @@ limitations under the License.
|
||||
|
||||
import {BlobHandle} from "../../platform/web/dom/BlobHandle.js";
|
||||
|
||||
export type RequestBody = BlobHandle | string | Map<string, string | {blob: BlobHandle, name: string}>;
|
||||
|
||||
export type EncodedBody = {
|
||||
mimeType: string;
|
||||
body: BlobHandle | string;
|
||||
// the map gets transformed to a FormData object on the web
|
||||
body: RequestBody
|
||||
}
|
||||
|
||||
export function encodeQueryParams(queryParams?: object): string {
|
||||
@ -41,6 +44,11 @@ export function encodeBody(body: BlobHandle | object): EncodedBody {
|
||||
mimeType: blob.mimeType,
|
||||
body: blob // will be unwrapped in request fn
|
||||
};
|
||||
} else if (body instanceof Map) {
|
||||
return {
|
||||
mimeType: "multipart/form-data",
|
||||
body: body
|
||||
}
|
||||
} else if (typeof body === "object") {
|
||||
const json = JSON.stringify(body);
|
||||
return {
|
||||
|
@ -163,7 +163,7 @@ export class GapWriter {
|
||||
if (!Array.isArray(chunk)) {
|
||||
throw new Error("Invalid chunk in response");
|
||||
}
|
||||
if (typeof end !== "string") {
|
||||
if (typeof end !== "string" && typeof end !== "undefined") {
|
||||
throw new Error("Invalid end token in response");
|
||||
}
|
||||
|
||||
|
@ -15,13 +15,13 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import type {RequestResult} from "../web/dom/request/fetch.js";
|
||||
import type {EncodedBody} from "../../matrix/net/common";
|
||||
import type {RequestBody} from "../../matrix/net/common";
|
||||
import type {ILogItem} from "../../logging/types";
|
||||
|
||||
export interface IRequestOptions {
|
||||
uploadProgress?: (loadedBytes: number) => void;
|
||||
timeout?: number;
|
||||
body?: EncodedBody;
|
||||
body?: RequestBody;
|
||||
headers?: Map<string, string|number>;
|
||||
cache?: boolean;
|
||||
method?: string;
|
||||
|
@ -187,9 +187,13 @@ export class Platform {
|
||||
this._serviceWorkerHandler,
|
||||
this._config.push
|
||||
);
|
||||
const manifests = this.config["themeManifests"];
|
||||
await this._themeLoader?.init(manifests);
|
||||
this._themeLoader?.setTheme(await this._themeLoader.getActiveTheme(), log);
|
||||
if (this._themeLoader) {
|
||||
const manifests = this.config["themeManifests"];
|
||||
await this._themeLoader?.init(manifests, log);
|
||||
const { themeName, themeVariant } = await this._themeLoader.getActiveTheme();
|
||||
log.log({ l: "Active theme", name: themeName, variant: themeVariant });
|
||||
this._themeLoader.setTheme(themeName, themeVariant, log);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
this._container.innerText = err.message;
|
||||
@ -341,6 +345,10 @@ export class Platform {
|
||||
head.appendChild(styleTag);
|
||||
}
|
||||
|
||||
get description() {
|
||||
return navigator.userAgent ?? "<unknown>";
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._disposables.dispose();
|
||||
}
|
||||
|
@ -17,59 +17,191 @@ 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, string> = {};
|
||||
private _themeMapping: Record<string, ThemeInformation>;
|
||||
|
||||
constructor(platform: Platform) {
|
||||
this._platform = platform;
|
||||
}
|
||||
|
||||
async init(manifestLocations: string[]): Promise<void> {
|
||||
for (const manifestLocation of manifestLocations) {
|
||||
const { body } = await this._platform
|
||||
.request(manifestLocation, {
|
||||
method: "GET",
|
||||
format: "json",
|
||||
cache: true,
|
||||
})
|
||||
.response();
|
||||
/*
|
||||
After build has finished, the source section of each theme manifest
|
||||
contains `built-assets` which is a mapping from the theme-name to the
|
||||
location of the css file in build.
|
||||
*/
|
||||
Object.assign(this._themeMapping, body["source"]["built-assets"]);
|
||||
}
|
||||
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 }) => this._populateThemeMap(body, log));
|
||||
});
|
||||
}
|
||||
|
||||
setTheme(themeName: string, log?: ILogItem) {
|
||||
this._platform.logger.wrapOrRun(log, {l: "change theme", id: themeName}, () => {
|
||||
const themeLocation = this._themeMapping[themeName];
|
||||
if (!themeLocation) {
|
||||
throw new Error( `Cannot find theme location for theme "${themeName}"!`);
|
||||
private _populateThemeMap(manifest, 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 (const [themeId, cssLocation] of Object.entries(builtAssets)) {
|
||||
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
|
||||
};
|
||||
}
|
||||
this._platform.replaceStylesheet(themeLocation);
|
||||
this._platform.settingsStorage.setString("theme", themeName);
|
||||
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 });
|
||||
});
|
||||
}
|
||||
|
||||
get themes(): string[] {
|
||||
return Object.keys(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");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getActiveTheme(): Promise<string|undefined> {
|
||||
// check if theme is set via settings
|
||||
let theme = await this._platform.settingsStorage.getString("theme");
|
||||
if (theme) {
|
||||
return theme;
|
||||
/** 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 default theme
|
||||
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 this._platform.config["defaultTheme"].dark;
|
||||
} else if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
||||
return this._platform.config["defaultTheme"].light;
|
||||
return ColorSchemePreference.Dark;
|
||||
}
|
||||
else if (window.matchMedia("(prefers-color-scheme: light)").matches) {
|
||||
return ColorSchemePreference.Light;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
@ -4,5 +4,6 @@
|
||||
"gatewayUrl": "https://matrix.org",
|
||||
"applicationServerKey": "BC-gpSdVHEXhvHSHS0AzzWrQoukv2BE7KzpoPO_FfPacqOo3l1pdqz7rSgmB04pZCWaHPz7XRe6fjLaC-WPDopM"
|
||||
},
|
||||
"defaultHomeServer": "matrix.org"
|
||||
"defaultHomeServer": "matrix.org",
|
||||
"bugReportEndpointUrl": "https://element.io/bugreports/submit"
|
||||
}
|
||||
|
@ -27,6 +27,21 @@ export function addCacheBuster(urlStr, random = Math.random) {
|
||||
return urlStr + `_cacheBuster=${Math.ceil(random() * Number.MAX_SAFE_INTEGER)}`;
|
||||
}
|
||||
|
||||
export function mapAsFormData(map) {
|
||||
const formData = new FormData();
|
||||
for (const [name, value] of map) {
|
||||
let filename;
|
||||
// Special case {name: string, blob: BlobHandle} to set a filename.
|
||||
// This is the format returned by platform.openFile
|
||||
if (value.blob?.nativeBlob && value.name) {
|
||||
formData.set(name, value.blob.nativeBlob, value.name);
|
||||
} else {
|
||||
formData.set(name, value);
|
||||
}
|
||||
}
|
||||
return formData;
|
||||
}
|
||||
|
||||
export function tests() {
|
||||
return {
|
||||
"add cache buster": assert => {
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
ConnectionError
|
||||
} from "../../../../matrix/error.js";
|
||||
import {abortOnTimeout} from "../../../../utils/timeout";
|
||||
import {addCacheBuster} from "./common.js";
|
||||
import {addCacheBuster, mapAsFormData} from "./common.js";
|
||||
import {xhrRequest} from "./xhr.js";
|
||||
|
||||
class RequestResult {
|
||||
@ -70,6 +70,9 @@ export function createFetchRequest(createTimeout, serviceWorkerHandler) {
|
||||
if (body?.nativeBlob) {
|
||||
body = body.nativeBlob;
|
||||
}
|
||||
if (body instanceof Map) {
|
||||
body = mapAsFormData(body);
|
||||
}
|
||||
let options = {method, body};
|
||||
if (controller) {
|
||||
options = Object.assign(options, {
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
AbortError,
|
||||
ConnectionError
|
||||
} from "../../../../matrix/error.js";
|
||||
import {addCacheBuster} from "./common.js";
|
||||
import {addCacheBuster, mapAsFormData} from "./common.js";
|
||||
|
||||
class RequestResult {
|
||||
constructor(promise, xhr) {
|
||||
@ -94,6 +94,9 @@ export function xhrRequest(url, options) {
|
||||
if (body?.nativeBlob) {
|
||||
body = body.nativeBlob;
|
||||
}
|
||||
if (body instanceof Map) {
|
||||
body = mapAsFormData(body);
|
||||
}
|
||||
xhr.send(body || null);
|
||||
|
||||
return new RequestResult(promise, xhr);
|
||||
|
@ -1,45 +1,39 @@
|
||||
{
|
||||
"version": 1,
|
||||
"name": "element",
|
||||
"name": "Element",
|
||||
"values": {
|
||||
"font-faces": [
|
||||
{
|
||||
"font-family": "Inter",
|
||||
"src": [{"asset": "/fonts/Inter.ttf", "format": "ttf"}]
|
||||
}
|
||||
],
|
||||
"variants": {
|
||||
"light": {
|
||||
"base": true,
|
||||
"default": true,
|
||||
"name": "Light",
|
||||
"variables": {
|
||||
"background-color-primary": "#fff",
|
||||
"light": {
|
||||
"base": true,
|
||||
"default": true,
|
||||
"name": "Light",
|
||||
"variables": {
|
||||
"background-color-primary": "#fff",
|
||||
"background-color-secondary": "#f6f6f6",
|
||||
"text-color": "#2E2F32",
|
||||
"text-color": "#2E2F32",
|
||||
"accent-color": "#03b381",
|
||||
"error-color": "#FF4B55",
|
||||
"fixed-white": "#fff",
|
||||
"room-badge": "#61708b",
|
||||
"link-color": "#238cf5"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"dark": true,
|
||||
"default": true,
|
||||
"name": "Dark",
|
||||
"variables": {
|
||||
"background-color-primary": "#21262b",
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"dark": true,
|
||||
"default": true,
|
||||
"name": "Dark",
|
||||
"variables": {
|
||||
"background-color-primary": "#21262b",
|
||||
"background-color-secondary": "#2D3239",
|
||||
"text-color": "#fff",
|
||||
"text-color": "#fff",
|
||||
"accent-color": "#03B381",
|
||||
"error-color": "#FF4B55",
|
||||
"fixed-white": "#fff",
|
||||
"room-badge": "#61708b",
|
||||
"link-color": "#238cf5"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -233,7 +233,7 @@ only loads when the top comes into view*/
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.Timeline_messageBody .media > .sendStatus {
|
||||
.Timeline_messageBody .media > .status {
|
||||
align-self: end;
|
||||
justify-self: start;
|
||||
font-size: 0.8em;
|
||||
@ -251,7 +251,7 @@ only loads when the top comes into view*/
|
||||
}
|
||||
|
||||
.Timeline_messageBody .media > time,
|
||||
.Timeline_messageBody .media > .sendStatus {
|
||||
.Timeline_messageBody .media > .status {
|
||||
color: var(--text-color);
|
||||
display: block;
|
||||
padding: 2px;
|
||||
|
@ -54,3 +54,11 @@ export function insertAt(parentNode: Element, idx: number, childNode: Node): voi
|
||||
export function removeChildren(parentNode: Element): void {
|
||||
parentNode.innerHTML = '';
|
||||
}
|
||||
|
||||
export function disableTargetCallback(callback: (evt: Event) => Promise<void>): (evt: Event) => Promise<void> {
|
||||
return async (evt: Event) => {
|
||||
(evt.target as HTMLElement)?.setAttribute("disabled", "disabled");
|
||||
await callback(evt);
|
||||
(evt.target as HTMLElement)?.removeAttribute("disabled");
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {BaseMessageView} from "./BaseMessageView.js";
|
||||
import {Menu} from "../../../general/Menu.js";
|
||||
|
||||
export class BaseMediaView extends BaseMessageView {
|
||||
renderMessageBody(t, vm) {
|
||||
@ -35,24 +36,39 @@ export class BaseMediaView extends BaseMessageView {
|
||||
this.renderMedia(t, vm),
|
||||
t.time(vm.date + " " + vm.time),
|
||||
];
|
||||
const status = t.div({
|
||||
className: {
|
||||
status: true,
|
||||
hidden: vm => !vm.status
|
||||
},
|
||||
}, vm => vm.status);
|
||||
children.push(status);
|
||||
if (vm.isPending) {
|
||||
const sendStatus = t.div({
|
||||
className: {
|
||||
sendStatus: true,
|
||||
hidden: vm => !vm.sendStatus
|
||||
},
|
||||
}, vm => vm.sendStatus);
|
||||
const progress = t.progress({
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: vm => vm.uploadPercentage,
|
||||
className: {hidden: vm => !vm.isUploading}
|
||||
});
|
||||
children.push(sendStatus, progress);
|
||||
children.push(progress);
|
||||
}
|
||||
return t.div({className: "Timeline_messageBody"}, [
|
||||
t.div({className: "media", style: `max-width: ${vm.width}px`}, children),
|
||||
t.if(vm => vm.error, t => t.p({className: "error"}, vm.error))
|
||||
]);
|
||||
}
|
||||
|
||||
createMenuOptions(vm) {
|
||||
const options = super.createMenuOptions(vm);
|
||||
if (!vm.isPending) {
|
||||
let label;
|
||||
switch (vm.shape) {
|
||||
case "image": label = vm.i18n`Download image`; break;
|
||||
case "video": label = vm.i18n`Download video`; break;
|
||||
default: label = vm.i18n`Download media`; break;
|
||||
}
|
||||
options.push(Menu.option(label, () => vm.downloadMedia()));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {TemplateView} from "../../general/TemplateView";
|
||||
import {disableTargetCallback} from "../../general/utils";
|
||||
import {KeyBackupSettingsView} from "./KeyBackupSettingsView.js"
|
||||
|
||||
export class SettingsView extends TemplateView {
|
||||
@ -101,11 +102,17 @@ export class SettingsView extends TemplateView {
|
||||
return row(t, vm.i18n`Use the following theme`, this._themeOptions(t, vm));
|
||||
}),
|
||||
);
|
||||
const logButtons = [];
|
||||
if (vm.canSendLogsToServer) {
|
||||
logButtons.push(t.button({onClick: disableTargetCallback(() => vm.sendLogsToServer())}, `Submit logs to ${vm.logsServer}`));
|
||||
}
|
||||
logButtons.push(t.button({onClick: () => vm.exportLogs()}, "Download logs"));
|
||||
settingNodes.push(
|
||||
t.h3("Application"),
|
||||
row(t, vm.i18n`Version`, version),
|
||||
row(t, vm.i18n`Storage usage`, vm => `${vm.storageUsage} / ${vm.storageQuota}`),
|
||||
row(t, vm.i18n`Debug logs`, t.button({onClick: () => vm.exportLogs()}, "Export")),
|
||||
row(t, vm.i18n`Debug logs`, logButtons),
|
||||
t.p({className: {hidden: vm => !vm.logsFeedbackMessage}}, vm => vm.logsFeedbackMessage),
|
||||
t.p(["Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited, the usernames of other users and the names of files you send. They do not contain messages. For more information, review our ",
|
||||
t.a({href: "https://element.io/privacy", target: "_blank", rel: "noopener"}, "privacy policy"), "."]),
|
||||
);
|
||||
@ -140,11 +147,52 @@ export class SettingsView extends TemplateView {
|
||||
}
|
||||
|
||||
_themeOptions(t, vm) {
|
||||
const activeTheme = vm.activeTheme;
|
||||
const { themeName: activeThemeName, themeVariant: activeThemeVariant } = vm.activeTheme;
|
||||
const optionTags = [];
|
||||
for (const name of vm.themes) {
|
||||
optionTags.push(t.option({value: name, selected: name === activeTheme}, name));
|
||||
// 1. render the dropdown containing the themes
|
||||
for (const name of Object.keys(vm.themeMapping)) {
|
||||
optionTags.push( t.option({ value: name, selected: name === activeThemeName} , name));
|
||||
}
|
||||
return t.select({onChange: (e) => vm.setTheme(e.target.value)}, optionTags);
|
||||
const select = t.select({
|
||||
onChange: (e) => {
|
||||
const themeName = e.target.value;
|
||||
if(!("id" in vm.themeMapping[themeName])) {
|
||||
const colorScheme = darkRadioButton.checked ? "dark" : lightRadioButton.checked ? "light" : "default";
|
||||
// execute the radio-button callback so that the theme actually changes!
|
||||
// otherwise the theme would only change when another radio-button is selected.
|
||||
radioButtonCallback(colorScheme);
|
||||
return;
|
||||
}
|
||||
vm.changeThemeOption(themeName);
|
||||
}
|
||||
}, optionTags);
|
||||
// 2. render the radio-buttons used to choose variant
|
||||
const radioButtonCallback = (colorScheme) => {
|
||||
const selectedThemeName = select.options[select.selectedIndex].value;
|
||||
vm.changeThemeOption(selectedThemeName, colorScheme);
|
||||
};
|
||||
const isDarkSelected = activeThemeVariant === "dark";
|
||||
const isLightSelected = activeThemeVariant === "light";
|
||||
const darkRadioButton = t.input({ type: "radio", name: "radio-chooser", value: "dark", id: "dark", checked: isDarkSelected });
|
||||
const defaultRadioButton = t.input({ type: "radio", name: "radio-chooser", value: "default", id: "default", checked: !(isDarkSelected || isLightSelected) });
|
||||
const lightRadioButton = t.input({ type: "radio", name: "radio-chooser", value: "light", id: "light", checked: isLightSelected });
|
||||
const radioButtons = t.form({
|
||||
className: {
|
||||
hidden: () => {
|
||||
const themeName = select.options[select.selectedIndex].value;
|
||||
return "id" in vm.themeMapping[themeName];
|
||||
}
|
||||
},
|
||||
onChange: (e) => radioButtonCallback(e.target.value)
|
||||
},
|
||||
[
|
||||
defaultRadioButton,
|
||||
t.label({for: "default"}, "Match system theme"),
|
||||
darkRadioButton,
|
||||
t.label({for: "dark"}, "dark"),
|
||||
lightRadioButton,
|
||||
t.label({for: "light"}, "light"),
|
||||
]);
|
||||
return t.div({ className: "theme-chooser" }, [select, radioButtons]);
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ const commonOptions = {
|
||||
postcss: {
|
||||
plugins: [
|
||||
compileVariables({derive, compiledVariables}),
|
||||
urlVariables({compileVariables}),
|
||||
urlVariables({compiledVariables}),
|
||||
urlProcessor({replacer}),
|
||||
// cssvariables({
|
||||
// preserve: (declaration) => {
|
||||
|
Loading…
Reference in New Issue
Block a user