mirror of https://github.com/vector-im/riot-web
Enable custom themes to theme Compound (#12240)
* Enable custom themes to theme Compound * Remove the now redundant username color variables They are replaced by the Compound theming options (specifically, username colors can be themed by changing the color of Compound's decorative color tokens).pull/28217/head
parent
203c15f205
commit
8bbad9f653
|
@ -17,7 +17,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css");
|
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css") layer(compound);
|
||||||
@import url("@vector-im/compound-web/dist/style.css");
|
@import url("@vector-im/compound-web/dist/style.css");
|
||||||
@import "./_font-sizes.pcss";
|
@import "./_font-sizes.pcss";
|
||||||
@import "./_animations.pcss";
|
@import "./_animations.pcss";
|
||||||
|
|
|
@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@charset "utf-8";
|
|
||||||
|
|
||||||
.mx_JumpToBottomButton {
|
.mx_JumpToBottomButton {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@charset "utf-8";
|
|
||||||
|
|
||||||
.mx_TopUnreadMessagesBar {
|
.mx_TopUnreadMessagesBar {
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -32,9 +32,6 @@ $background: var(--background, $background);
|
||||||
$panels: var(--panels, var(--cpd-color-gray-600));
|
$panels: var(--panels, var(--cpd-color-gray-600));
|
||||||
$panel-actions: var(--panels-actions, var(--cpd-color-gray-300));
|
$panel-actions: var(--panels-actions, var(--cpd-color-gray-300));
|
||||||
|
|
||||||
/* --accent-color */
|
|
||||||
$username-variant3-color: var(--accent-color);
|
|
||||||
|
|
||||||
/* --timeline-background-color */
|
/* --timeline-background-color */
|
||||||
$button-secondary-bg-color: var(--timeline-background-color);
|
$button-secondary-bg-color: var(--timeline-background-color);
|
||||||
$lightbox-border-color: var(--timeline-background-color);
|
$lightbox-border-color: var(--timeline-background-color);
|
||||||
|
@ -110,14 +107,6 @@ $accent-alt: var(--primary-color);
|
||||||
/* --warning-color */
|
/* --warning-color */
|
||||||
$button-danger-disabled-bg-color: var(--warning-color-50pct); /* still needs alpha at 0.5 */
|
$button-danger-disabled-bg-color: var(--warning-color-50pct); /* still needs alpha at 0.5 */
|
||||||
|
|
||||||
/* --username colors (which use a 0-based index) */
|
|
||||||
$username-variant1-color: var(--username-colors_0, $username-variant1-color);
|
|
||||||
$username-variant2-color: var(--username-colors_1, $username-variant2-color);
|
|
||||||
$username-variant3-color: var(--username-colors_2, $username-variant3-color);
|
|
||||||
$username-variant4-color: var(--username-colors_3, $username-variant4-color);
|
|
||||||
$username-variant5-color: var(--username-colors_4, $username-variant5-color);
|
|
||||||
$username-variant6-color: var(--username-colors_5, $username-variant6-color);
|
|
||||||
|
|
||||||
/* --timeline-highlights-color */
|
/* --timeline-highlights-color */
|
||||||
$event-selected-color: var(--timeline-highlights-color);
|
$event-selected-color: var(--timeline-highlights-color);
|
||||||
$event-highlight-bg-color: var(--timeline-highlights-color);
|
$event-highlight-bg-color: var(--timeline-highlights-color);
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css");
|
|
||||||
|
|
||||||
@import "../../../../res/css/_font-sizes.pcss";
|
@import "../../../../res/css/_font-sizes.pcss";
|
||||||
@import "_paths.pcss";
|
@import "_paths.pcss";
|
||||||
@import "_fonts.pcss";
|
@import "_fonts.pcss";
|
||||||
|
|
46
src/theme.ts
46
src/theme.ts
|
@ -35,17 +35,22 @@ interface IFontFaces extends Omit<Record<(typeof allowedFontFaceProps)[number],
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CompoundTheme {
|
||||||
|
[token: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type CustomTheme = {
|
export type CustomTheme = {
|
||||||
name: string;
|
name: string;
|
||||||
colors: {
|
is_dark?: boolean; // eslint-disable-line camelcase
|
||||||
|
colors?: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
fonts: {
|
fonts?: {
|
||||||
faces: IFontFaces[];
|
faces: IFontFaces[];
|
||||||
general: string;
|
general: string;
|
||||||
monospace: string;
|
monospace: string;
|
||||||
};
|
};
|
||||||
is_dark?: boolean; // eslint-disable-line camelcase
|
compound?: CompoundTheme;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -120,10 +125,10 @@ function clearCustomTheme(): void {
|
||||||
document.body.style.removeProperty(prop);
|
document.body.style.removeProperty(prop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const customFontFaceStyle = document.querySelector("head > style[title='custom-theme-font-faces']");
|
|
||||||
if (customFontFaceStyle) {
|
// remove the custom style sheets
|
||||||
customFontFaceStyle.remove();
|
document.querySelector("head > style[title='custom-theme-font-faces']")?.remove();
|
||||||
}
|
document.querySelector("head > style[title='custom-theme-compound']")?.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowedFontFaceProps = [
|
const allowedFontFaceProps = [
|
||||||
|
@ -177,6 +182,22 @@ function generateCustomFontFaceCSS(faces: IFontFaces[]): string {
|
||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const COMPOUND_TOKEN = /^--cpd-[a-z0-9-]+$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a style sheet to override Compound design tokens as specified in
|
||||||
|
* the given theme.
|
||||||
|
*/
|
||||||
|
function generateCustomCompoundCSS(theme: CompoundTheme): string {
|
||||||
|
const properties: string[] = [];
|
||||||
|
for (const [token, value] of Object.entries(theme))
|
||||||
|
if (COMPOUND_TOKEN.test(token)) properties.push(`${token}: ${value};`);
|
||||||
|
else logger.warn(`'${token}' is not a valid Compound token`);
|
||||||
|
// Insert the design token overrides into the 'custom' cascade layer as
|
||||||
|
// documented at https://compound.element.io/?path=/docs/develop-theming--docs
|
||||||
|
return `@layer compound.custom { :root, [class*="cpd-theme-"] { ${properties.join(" ")} } }`;
|
||||||
|
}
|
||||||
|
|
||||||
function setCustomThemeVars(customTheme: CustomTheme): void {
|
function setCustomThemeVars(customTheme: CustomTheme): void {
|
||||||
const { style } = document.body;
|
const { style } = document.body;
|
||||||
|
|
||||||
|
@ -218,6 +239,14 @@ function setCustomThemeVars(customTheme: CustomTheme): void {
|
||||||
style.setProperty("--font-family-monospace", fonts.monospace);
|
style.setProperty("--font-family-monospace", fonts.monospace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (customTheme.compound) {
|
||||||
|
const css = generateCustomCompoundCSS(customTheme.compound);
|
||||||
|
const style = document.createElement("style");
|
||||||
|
style.setAttribute("title", "custom-theme-compound");
|
||||||
|
style.setAttribute("type", "text/css");
|
||||||
|
style.appendChild(document.createTextNode(css));
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCustomTheme(themeName: string): CustomTheme {
|
export function getCustomTheme(themeName: string): CustomTheme {
|
||||||
|
@ -284,9 +313,6 @@ export async function setTheme(theme?: string): Promise<void> {
|
||||||
* Adds the Compound theme class to the top-most element in the document
|
* Adds the Compound theme class to the top-most element in the document
|
||||||
* This will automatically refresh the colour scales based on the OS or user
|
* This will automatically refresh the colour scales based on the OS or user
|
||||||
* preferences
|
* preferences
|
||||||
*
|
|
||||||
* Note: Theming through Compound is not yet established. Brand theming should
|
|
||||||
* be done in a similar manner as it used to be done.
|
|
||||||
*/
|
*/
|
||||||
document.body.classList.remove("cpd-theme-light", "cpd-theme-dark", "cpd-theme-light-hc", "cpd-theme-dark-hc");
|
document.body.classList.remove("cpd-theme-light", "cpd-theme-dark", "cpd-theme-light-hc", "cpd-theme-dark-hc");
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`theme setTheme applies a custom Compound theme 1`] = `"@layer compound.custom { :root, [class*="cpd-theme-"] { --cpd-color-icon-accent-tertiary: var(--cpd-color-blue-800); --cpd-color-text-action-accent: var(--cpd-color-blue-900); } }"`;
|
|
@ -21,31 +21,26 @@ describe("theme", () => {
|
||||||
describe("setTheme", () => {
|
describe("setTheme", () => {
|
||||||
let lightTheme: HTMLStyleElement;
|
let lightTheme: HTMLStyleElement;
|
||||||
let darkTheme: HTMLStyleElement;
|
let darkTheme: HTMLStyleElement;
|
||||||
|
let lightCustomTheme: HTMLStyleElement;
|
||||||
|
|
||||||
let spyQuerySelectorAll: jest.MockInstance<NodeListOf<Element>, [selectors: string]>;
|
let spyQuerySelectorAll: jest.MockInstance<NodeListOf<Element>, [selectors: string]>;
|
||||||
let spyClassList: jest.SpyInstance<void, string[], any>;
|
let spyClassList: jest.SpyInstance<void, string[], any>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const styles = [
|
const styles = ["light", "dark", "light-custom", "dark-custom"].map(
|
||||||
{
|
(theme) =>
|
||||||
dataset: {
|
({
|
||||||
mxTheme: "light",
|
dataset: {
|
||||||
},
|
mxTheme: theme,
|
||||||
disabled: true,
|
},
|
||||||
href: "urlLight",
|
disabled: true,
|
||||||
onload: (): void => void 0,
|
href: "fake URL",
|
||||||
} as unknown as HTMLStyleElement,
|
onload: (): void => void 0,
|
||||||
{
|
}) as unknown as HTMLStyleElement,
|
||||||
dataset: {
|
);
|
||||||
mxTheme: "dark",
|
|
||||||
},
|
|
||||||
disabled: true,
|
|
||||||
href: "urlDark",
|
|
||||||
onload: (): void => void 0,
|
|
||||||
} as unknown as HTMLStyleElement,
|
|
||||||
];
|
|
||||||
lightTheme = styles[0];
|
lightTheme = styles[0];
|
||||||
darkTheme = styles[1];
|
darkTheme = styles[1];
|
||||||
|
lightCustomTheme = styles[2];
|
||||||
|
|
||||||
jest.spyOn(document.body, "style", "get").mockReturnValue([] as any);
|
jest.spyOn(document.body, "style", "get").mockReturnValue([] as any);
|
||||||
spyQuerySelectorAll = jest.spyOn(document, "querySelectorAll").mockReturnValue(styles as any);
|
spyQuerySelectorAll = jest.spyOn(document, "querySelectorAll").mockReturnValue(styles as any);
|
||||||
|
@ -124,6 +119,27 @@ describe("theme", () => {
|
||||||
jest.advanceTimersByTime(200 * 10);
|
jest.advanceTimersByTime(200 * 10);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("applies a custom Compound theme", async () => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockReturnValue([
|
||||||
|
{
|
||||||
|
name: "blue",
|
||||||
|
compound: {
|
||||||
|
"--cpd-color-icon-accent-tertiary": "var(--cpd-color-blue-800)",
|
||||||
|
"--cpd-color-text-action-accent": "var(--cpd-color-blue-900)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const spy = jest.spyOn(document.head, "appendChild").mockImplementation();
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTheme("custom-blue").then(resolve);
|
||||||
|
lightCustomTheme.onload!({} as Event);
|
||||||
|
});
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
expect(spy.mock.calls[0][0].textContent).toMatchSnapshot();
|
||||||
|
spy.mockRestore();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("enumerateThemes", () => {
|
describe("enumerateThemes", () => {
|
||||||
|
|
|
@ -3123,9 +3123,9 @@
|
||||||
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
|
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
|
||||||
|
|
||||||
"@vector-im/compound-design-tokens@^1.0.0":
|
"@vector-im/compound-design-tokens@^1.0.0":
|
||||||
version "1.0.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.0.0.tgz#4fe7744bbe0bd093b064d42ca8bb475862bb2ce7"
|
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.1.0.tgz#9b1a91317c404a1cd0d76d2fd5a7f2df5f1bf0a6"
|
||||||
integrity sha512-/hKAxE/WsmnNZamlSmLoFeAhNDhRpFdJYuY8NrPLaS/dKS/QRnty6UYzs9yWOVNFeiBfkNsrb7wYIFMrYWSRJw==
|
integrity sha512-1HcCm6YsOda98rGXO4fg0WjEdrMnx/0tdtFmYIlnYkDYTbnfpFg+ffIDY7jgammWbOYwUZpZhM5q9ofb7/EgkA==
|
||||||
dependencies:
|
dependencies:
|
||||||
svg2vectordrawable "^2.9.1"
|
svg2vectordrawable "^2.9.1"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue