diff --git a/res/css/_common.pcss b/res/css/_common.pcss index 855c64cb73..1ab0e8bf5d 100644 --- a/res/css/_common.pcss +++ b/res/css/_common.pcss @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and 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 "./_font-sizes.pcss"; @import "./_animations.pcss"; diff --git a/res/css/views/rooms/_JumpToBottomButton.pcss b/res/css/views/rooms/_JumpToBottomButton.pcss index 08e3ad9031..0a760e2cd6 100644 --- a/res/css/views/rooms/_JumpToBottomButton.pcss +++ b/res/css/views/rooms/_JumpToBottomButton.pcss @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -@charset "utf-8"; - .mx_JumpToBottomButton { z-index: 1000; position: absolute; diff --git a/res/css/views/rooms/_TopUnreadMessagesBar.pcss b/res/css/views/rooms/_TopUnreadMessagesBar.pcss index 258adb759a..e72fedafef 100644 --- a/res/css/views/rooms/_TopUnreadMessagesBar.pcss +++ b/res/css/views/rooms/_TopUnreadMessagesBar.pcss @@ -14,8 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -@charset "utf-8"; - .mx_TopUnreadMessagesBar { z-index: 1000; position: absolute; diff --git a/res/themes/light-custom/css/_custom.pcss b/res/themes/light-custom/css/_custom.pcss index 5ecac33ffc..ed345a28ec 100644 --- a/res/themes/light-custom/css/_custom.pcss +++ b/res/themes/light-custom/css/_custom.pcss @@ -32,9 +32,6 @@ $background: var(--background, $background); $panels: var(--panels, var(--cpd-color-gray-600)); $panel-actions: var(--panels-actions, var(--cpd-color-gray-300)); -/* --accent-color */ -$username-variant3-color: var(--accent-color); - /* --timeline-background-color */ $button-secondary-bg-color: var(--timeline-background-color); $lightbox-border-color: var(--timeline-background-color); @@ -110,14 +107,6 @@ $accent-alt: var(--primary-color); /* --warning-color */ $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 */ $event-selected-color: var(--timeline-highlights-color); $event-highlight-bg-color: var(--timeline-highlights-color); diff --git a/res/themes/light/css/light.pcss b/res/themes/light/css/light.pcss index 612e8fdce6..361484828f 100644 --- a/res/themes/light/css/light.pcss +++ b/res/themes/light/css/light.pcss @@ -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 "_paths.pcss"; @import "_fonts.pcss"; diff --git a/src/theme.ts b/src/theme.ts index 1d5376b6c1..8e2e893334 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -35,17 +35,22 @@ interface IFontFaces extends Omit style[title='custom-theme-font-faces']"); - if (customFontFaceStyle) { - customFontFaceStyle.remove(); - } + + // remove the custom style sheets + document.querySelector("head > style[title='custom-theme-font-faces']")?.remove(); + document.querySelector("head > style[title='custom-theme-compound']")?.remove(); } const allowedFontFaceProps = [ @@ -177,6 +182,22 @@ function generateCustomFontFaceCSS(faces: IFontFaces[]): string { .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 { const { style } = document.body; @@ -218,6 +239,14 @@ function setCustomThemeVars(customTheme: CustomTheme): void { 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 { @@ -284,9 +313,6 @@ export async function setTheme(theme?: string): Promise { * 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 * 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"); diff --git a/test/__snapshots__/theme-test.ts.snap b/test/__snapshots__/theme-test.ts.snap new file mode 100644 index 0000000000..754786eb70 --- /dev/null +++ b/test/__snapshots__/theme-test.ts.snap @@ -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); } }"`; diff --git a/test/theme-test.ts b/test/theme-test.ts index 3789028f81..fdba6d0d18 100644 --- a/test/theme-test.ts +++ b/test/theme-test.ts @@ -21,31 +21,26 @@ describe("theme", () => { describe("setTheme", () => { let lightTheme: HTMLStyleElement; let darkTheme: HTMLStyleElement; + let lightCustomTheme: HTMLStyleElement; let spyQuerySelectorAll: jest.MockInstance, [selectors: string]>; let spyClassList: jest.SpyInstance; beforeEach(() => { - const styles = [ - { - dataset: { - mxTheme: "light", - }, - disabled: true, - href: "urlLight", - onload: (): void => void 0, - } as unknown as HTMLStyleElement, - { - dataset: { - mxTheme: "dark", - }, - disabled: true, - href: "urlDark", - onload: (): void => void 0, - } as unknown as HTMLStyleElement, - ]; + const styles = ["light", "dark", "light-custom", "dark-custom"].map( + (theme) => + ({ + dataset: { + mxTheme: theme, + }, + disabled: true, + href: "fake URL", + onload: (): void => void 0, + }) as unknown as HTMLStyleElement, + ); lightTheme = styles[0]; darkTheme = styles[1]; + lightCustomTheme = styles[2]; jest.spyOn(document.body, "style", "get").mockReturnValue([] as any); spyQuerySelectorAll = jest.spyOn(document, "querySelectorAll").mockReturnValue(styles as any); @@ -124,6 +119,27 @@ describe("theme", () => { 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", () => { diff --git a/yarn.lock b/yarn.lock index 3c5af658a7..a7760b3f5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3123,9 +3123,9 @@ integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== "@vector-im/compound-design-tokens@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.0.0.tgz#4fe7744bbe0bd093b064d42ca8bb475862bb2ce7" - integrity sha512-/hKAxE/WsmnNZamlSmLoFeAhNDhRpFdJYuY8NrPLaS/dKS/QRnty6UYzs9yWOVNFeiBfkNsrb7wYIFMrYWSRJw== + version "1.1.0" + resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.1.0.tgz#9b1a91317c404a1cd0d76d2fd5a7f2df5f1bf0a6" + integrity sha512-1HcCm6YsOda98rGXO4fg0WjEdrMnx/0tdtFmYIlnYkDYTbnfpFg+ffIDY7jgammWbOYwUZpZhM5q9ofb7/EgkA== dependencies: svg2vectordrawable "^2.9.1"