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).
This commit is contained in:
parent
203c15f205
commit
8bbad9f653
9 changed files with 77 additions and 49 deletions
|
@ -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");
|
||||||
|
|
||||||
|
|
3
test/__snapshots__/theme-test.ts.snap
Normal file
3
test/__snapshots__/theme-test.ts.snap
Normal file
|
@ -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…
Add table
Add a link
Reference in a new issue