Add CSS cascade layers for Compound, shared components, and app/web styles (#33302)

* Layer Compound and shared component CSS

* Layer app theme CSS

* Remove !important flags from ActionBarView

* Remove unnecessary !important statements from shared components

* Avoid dead code errors for *.pcss just because layer is specified after @import url

* Remove unnecessary !important styling

* Override Banner defaults in RoomStatusBarView

* Updated snaps

* Updated snaps

* Fix styling of media body in app/web

* Fix styling for Compound anchors

* Fix styling issues in app/web

* More styling fixes

* Fix a problem extracting css for HTMLExport

* Revert changes

* Fix for theme styling

* Add test to improve coverage

* Prettier

* Fix styling issues

* Add data-kind attribute to avoid global styling override

* Update screenshot that now is correct

* Revert data-kind attribute

* Handle LinkPreview styling in .pcss

* Fix flaky test: Avoid racing the lazy-loaded ManageEventIndexDialog

* Take care of review comments

* Updated snaps

* Updated snaps again after merge

* Remove !important from RoomStatusBar
This commit is contained in:
rbondesson
2026-04-30 11:54:49 +00:00
committed by GitHub
parent f2d355bb2c
commit b0ee6f5323
46 changed files with 350 additions and 165 deletions
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
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); } }"`;
exports[`theme setTheme applies a custom Compound theme 1`] = `"@layer compound-tokens { :root, [class*="cpd-theme-"] { --cpd-color-icon-accent-tertiary: var(--cpd-color-blue-800); --cpd-color-text-action-accent: var(--cpd-color-blue-900); } }"`;
@@ -237,7 +237,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
class="_content_n7ud0_38"
>
<p
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _title_1xryk_24"
class="_typography_6v6n8_153 _font-body-md-medium_6v6n8_60 _title_mcq5y_24"
id="_r_1cl_"
>
Could not start a chat with this user
@@ -247,7 +247,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
class="_actions_n7ud0_61"
>
<button
class="_button_1nw83_8 _primaryAction_1xryk_20 _has-icon_1nw83_60"
class="_button_1nw83_8 _primaryAction_mcq5y_20 _has-icon_1nw83_60"
data-kind="primary"
data-size="md"
role="button"
@@ -89,13 +89,13 @@ exports[`<MessageEditHistory /> should match the snapshot 1`] = `
<div
aria-label="Message Actions"
aria-live="off"
class="_flex_4dswl_9 mx_ThreadActionBar mx_HistoryActionBar _toolbar_1ax4y_8"
class="_flex_4dswl_9 mx_ThreadActionBar mx_HistoryActionBar _toolbar_1ksun_8"
role="toolbar"
style="--mx-flex-display: inline-flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
>
<button
aria-label="Remove"
class="_button_1nw83_8 _toolbar_item_1ax4y_14"
class="_button_1nw83_8 _toolbar_item_1ksun_14"
data-kind="tertiary"
data-presentation="label"
data-size="md"
@@ -236,13 +236,13 @@ exports[`<MessageEditHistory /> should support events with 1`] = `
<div
aria-label="Message Actions"
aria-live="off"
class="_flex_4dswl_9 mx_ThreadActionBar mx_HistoryActionBar _toolbar_1ax4y_8"
class="_flex_4dswl_9 mx_ThreadActionBar mx_HistoryActionBar _toolbar_1ksun_8"
role="toolbar"
style="--mx-flex-display: inline-flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
>
<button
aria-label="Remove"
class="_button_1nw83_8 _toolbar_item_1ax4y_14"
class="_button_1nw83_8 _toolbar_item_1ksun_14"
data-kind="tertiary"
data-presentation="label"
data-size="md"
@@ -298,13 +298,13 @@ exports[`<MessageEditHistory /> should support events with 1`] = `
<div
aria-label="Message Actions"
aria-live="off"
class="_flex_4dswl_9 mx_ThreadActionBar mx_HistoryActionBar _toolbar_1ax4y_8"
class="_flex_4dswl_9 mx_ThreadActionBar mx_HistoryActionBar _toolbar_1ksun_8"
role="toolbar"
style="--mx-flex-display: inline-flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
>
<button
aria-label="Remove"
class="_button_1nw83_8 _toolbar_item_1ax4y_14"
class="_button_1nw83_8 _toolbar_item_1ksun_14"
data-kind="tertiary"
data-presentation="label"
data-size="md"
@@ -342,13 +342,13 @@ exports[`<MessageEditHistory /> should support events with 1`] = `
<div
aria-label="Message Actions"
aria-live="off"
class="_flex_4dswl_9 mx_ThreadActionBar mx_HistoryActionBar _toolbar_1ax4y_8"
class="_flex_4dswl_9 mx_ThreadActionBar mx_HistoryActionBar _toolbar_1ksun_8"
role="toolbar"
style="--mx-flex-display: inline-flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
>
<button
aria-label="Remove"
class="_button_1nw83_8 _toolbar_item_1ax4y_14"
class="_button_1nw83_8 _toolbar_item_1ksun_14"
data-kind="tertiary"
data-presentation="label"
data-size="md"
@@ -3,7 +3,7 @@
exports[`MBodyFactory renderMBody fallback shows m.audio generic placeholder when showFileInfo is true 1`] = `
<div>
<span
class="_content_f1s5h_8 mx_MFileBody"
class="_content_1t2mx_8 mx_MFileBody"
>
<div
class="mx_MediaBody _mediaBody_rgndh_8"
@@ -44,7 +44,7 @@ exports[`MBodyFactory renderMBody fallback shows m.audio generic placeholder whe
exports[`MBodyFactory renderMBody fallback shows m.file generic placeholder when showFileInfo is true 1`] = `
<div>
<span
class="_content_f1s5h_8 mx_MFileBody"
class="_content_1t2mx_8 mx_MFileBody"
>
<div
class="mx_MediaBody _mediaBody_rgndh_8"
@@ -82,7 +82,7 @@ exports[`MBodyFactory renderMBody fallback shows m.file generic placeholder when
exports[`MBodyFactory renderMBody renders download button for m.file in file rendering type 1`] = `
<div>
<span
class="_content_f1s5h_8 mx_MFileBody"
class="_content_1t2mx_8 mx_MFileBody"
>
<div
data-type="download"
@@ -93,7 +93,7 @@ exports[`<MImageBody/> should open ImageView using thumbnail for encrypted svg 1
</div>
<a
aria-live="off"
class="mx_MessageTimestamp _content_kc5mt_8"
class="mx_MessageTimestamp _content_1r034_8"
href="https://matrix.to/#/!room:server/undefined"
>
Thu, Jan 15, 1970, 06:56
@@ -300,7 +300,7 @@ exports[`<MImageBody/> should open ImageView using thumbnail for encrypted svg 1
exports[`<MImageBody/> should render MFileBody for svg with no thumbnail 1`] = `
<DocumentFragment>
<span
class="_content_f1s5h_8 mx_MFileBody"
class="_content_1t2mx_8 mx_MFileBody"
>
<div
class="mx_MediaBody _mediaBody_rgndh_8"
@@ -3,13 +3,13 @@
exports[`<RoomListSearch /> renders 1`] = `
<DocumentFragment>
<div
class="_flex_4dswl_9 _view_z7ks9_8"
class="_flex_4dswl_9 _view_zk0ja_8"
data-testid="room-list-search"
role="search"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
>
<button
class="_button_1nw83_8 _search_z7ks9_16 _has-icon_1nw83_60"
class="_button_1nw83_8 _search_zk0ja_16 _has-icon_1nw83_60"
data-kind="secondary"
data-size="md"
id="room-list-search-button"
@@ -29,11 +29,11 @@ exports[`<RoomListSearch /> renders 1`] = `
/>
</svg>
<span
class="_flex_4dswl_9 _search_container_z7ks9_29"
class="_flex_4dswl_9 _search_container_zk0ja_28"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
>
<span
class="_search_text_z7ks9_41"
class="_search_text_zk0ja_40"
>
Search
</span>
File diff suppressed because one or more lines are too long
@@ -6,13 +6,103 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import fetchMock from "@fetch-mock/jest";
import getExportCSS from "../../../../src/utils/exportUtils/exportCSS";
describe("exportCSS", () => {
describe("getExportCSS", () => {
beforeEach(() => {
document.head.replaceChildren();
});
it("supports documents missing stylesheets", async () => {
const css = await getExportCSS(new Set());
expect(css).not.toContain("color-scheme: light");
});
it("fetches export stylesheets and filters unused css", async () => {
document.head.innerHTML = `
<link rel="stylesheet" href="/bundle.css" />
<link rel="stylesheet" href="/theme-light.css" />
<link rel="stylesheet" href="/theme-dark.css" />
`;
fetchMock.get(
"end:/bundle.css",
`
@font-face { font-family: Inter; src: url(inter.woff2); }
body { margin: 0; }
.mx_Used { font-family: Inter; color: #111111; }
.mx_Code { font-family: Fira Code; }
.mx_Unused { color: #123456; }
.mx_Empty {}
.mx_Used, .mx_UnusedComma { color: #abcdef; }
@media screen {
.mx_Used { --cpd-font-family-sans: "Inter"; }
.mx_UnusedNested { color: #654321; }
}
@supports (display: grid) {
.mx_UnusedSupported { color: #fedcba; }
}
`,
);
fetchMock.get("end:/theme-light.css", ".mx_Theme { color: #222222; }");
const css = await getExportCSS(new Set(["mx_Used", "mx_Code", "mx_Theme"]));
expect(fetchMock).toHaveFetchedTimes(1, "end:/bundle.css");
expect(fetchMock).toHaveFetchedTimes(1, "end:/theme-light.css");
expect(fetchMock).not.toHaveFetched("end:/theme-dark.css");
expect(css).toContain("margin:0");
expect(css).toContain("#111");
expect(css).toContain("#222");
expect(css).toContain("#abcdef");
expect(css).not.toContain("@font-face");
expect(css).not.toContain("#123456");
expect(css).not.toContain("#654321");
expect(css).not.toContain("#fedcba");
expect(css).not.toContain("font-family:Inter");
expect(css).not.toContain("font-family:Fira Code");
expect(css).toContain("BlinkMacSystemFont");
expect(css).toContain("Menlo, Consolas");
});
it("keeps export-only css in the app cascade layer after layered font rules", async () => {
document.head.innerHTML = `
<link rel="stylesheet" href="/bundle.css" />
`;
fetchMock.get(
"end:/bundle.css",
`
@layer compound-tokens, compound-web, shared-components, app-web;
@layer compound-web {
.mx_Typography {
font: var(--cpd-font-heading-lg-regular);
}
}
@layer app-web {
body {
font: var(--cpd-font-body-md-regular) !important;
}
}
`,
);
const css = await getExportCSS(new Set(["mx_Typography"]));
expect(css).toContain("@layer compound-web{.mx_Typography{font:var(--cpd-font-heading-lg-regular)}}");
expect(css).toContain("@layer app-web{body{font:var(--cpd-font-body-md-regular)!important}}");
const exportCssLayerIndex = css.indexOf("@layer app-web {");
expect(exportCssLayerIndex).toBeGreaterThan(
css.indexOf("@layer app-web{body{font:var(--cpd-font-body-md-regular)!important}}"),
);
expect(css.slice(exportCssLayerIndex)).toBe("@layer app-web {css-file-stub}");
});
});
});