diff --git a/apps/web/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/window-custom-theme-linux.png b/apps/web/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/window-custom-theme-linux.png index ad8bd39b6d..dc175017bb 100644 Binary files a/apps/web/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/window-custom-theme-linux.png and b/apps/web/playwright/snapshots/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts/window-custom-theme-linux.png differ diff --git a/apps/web/res/css/_common.pcss b/apps/web/res/css/_common.pcss index d55cb07606..58bebb609b 100644 --- a/apps/web/res/css/_common.pcss +++ b/apps/web/res/css/_common.pcss @@ -163,9 +163,10 @@ b { font-weight: bold; } -a:hover, -a:link, -a:visited { +/* Keep the legacy link colour without overriding Compound anchors. */ +a:where(:not([data-kind])):hover, +a:where(:not([data-kind])):link, +a:where(:not([data-kind])):visited { color: $accent-alt; } diff --git a/apps/web/res/css/_compound.pcss b/apps/web/res/css/_compound.pcss index 2bc482551d..823cf1ef7b 100644 --- a/apps/web/res/css/_compound.pcss +++ b/apps/web/res/css/_compound.pcss @@ -1,3 +1,6 @@ +/* Shared cascade order: Compound tokens, Compound Web, shared components, then app overrides. */ +@layer compound-tokens, compound-web, shared-components, app-web; + /* Modules bundled with compound apply compound lastly. In order to catch issue due to css class ordering, we put compound at the end */ -@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-design-tokens/assets/web/css/compound-design-tokens.css") layer(compound-tokens); +@import url("@vector-im/compound-web/dist/style.css") layer(compound-web); diff --git a/apps/web/res/css/views/audio_messages/_PlaybackContainer.pcss b/apps/web/res/css/views/audio_messages/_PlaybackContainer.pcss index 21980b67e6..087726a1f7 100644 --- a/apps/web/res/css/views/audio_messages/_PlaybackContainer.pcss +++ b/apps/web/res/css/views/audio_messages/_PlaybackContainer.pcss @@ -11,9 +11,8 @@ Please see LICENSE files in the repository root for full details. /* Container for live recording and playback controls */ .mx_MediaBody.mx_VoiceMessagePrimaryContainer { - /* The waveform (right) has a 1px padding on it that we want to account for, otherwise */ - /* inherit from mx_MediaBody */ - padding-right: 11px; + /* Match mx_MediaBody spacing, offsetting the waveform's 1px internal right padding. */ + padding: 6px 11px 6px 12px; /* Cheat at alignment a bit */ display: flex; diff --git a/apps/web/res/css/views/auth/_CompleteSecurityBody.pcss b/apps/web/res/css/views/auth/_CompleteSecurityBody.pcss index 3aa9cbf518..d17914ead1 100644 --- a/apps/web/res/css/views/auth/_CompleteSecurityBody.pcss +++ b/apps/web/res/css/views/auth/_CompleteSecurityBody.pcss @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. .mx_CompleteSecurityBody { color: $authpage-primary-color; background-color: $background; - border-radius: 4px; + border-radius: 24px; padding: 20px 20px 60px 20px; box-sizing: border-box; diff --git a/apps/web/res/css/views/messages/_MFileBody.pcss b/apps/web/res/css/views/messages/_MFileBody.pcss index 1f8ab80576..783a58c63d 100644 --- a/apps/web/res/css/views/messages/_MFileBody.pcss +++ b/apps/web/res/css/views/messages/_MFileBody.pcss @@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details. */ .mx_MFileBody [data-type="download"] { - color: $accent; height: var(--cpd-space-9x); & object { diff --git a/apps/web/res/css/views/messages/_MediaBody.pcss b/apps/web/res/css/views/messages/_MediaBody.pcss index d3eb02ca65..aadf220885 100644 --- a/apps/web/res/css/views/messages/_MediaBody.pcss +++ b/apps/web/res/css/views/messages/_MediaBody.pcss @@ -17,6 +17,8 @@ Please see LICENSE files in the repository root for full details. color: $secondary-content; font: var(--cpd-font-body-md-regular); line-height: $font-24px; - - padding: 6px 12px; +} + +.mx_MAudioBody > .mx_MediaBody { + border-radius: var(--MBody-border-radius); } diff --git a/apps/web/res/css/views/messages/_TextualEvent.pcss b/apps/web/res/css/views/messages/_TextualEvent.pcss index bf7061f2ba..3dbde24791 100644 --- a/apps/web/res/css/views/messages/_TextualEvent.pcss +++ b/apps/web/res/css/views/messages/_TextualEvent.pcss @@ -19,3 +19,12 @@ Please see LICENSE files in the repository root for full details. opacity: unset; /* Unset the opacity value specified above on the search results panel */ } } + +.mx_TextualBody_urlPreviews { + /* Let shared-components own preview link colours instead of the app-wide anchor colour. */ + a:where(:not([data-kind])):hover, + a:where(:not([data-kind])):link, + a:where(:not([data-kind])):visited { + color: revert-layer; + } +} diff --git a/apps/web/res/css/views/settings/_ThemeChoicePanel.pcss b/apps/web/res/css/views/settings/_ThemeChoicePanel.pcss index e61671f4f5..d5e9f5c619 100644 --- a/apps/web/res/css/views/settings/_ThemeChoicePanel.pcss +++ b/apps/web/res/css/views/settings/_ThemeChoicePanel.pcss @@ -26,12 +26,22 @@ Please see LICENSE files in the repository root for full details. &.mx_ThemeChoicePanel_themeSelector_disabled { border-color: var(--cpd-color-border-disabled); + + .mx_ThemeChoicePanel_themeSelector_Label { + color: var(--cpd-color-text-disabled); + cursor: not-allowed; + } } .mx_ThemeChoicePanel_themeSelector_Label { - color: var(--cpd-color-text-primary); font: var(--cpd-font-body-md-semibold); } + + &:not(.mx_ThemeChoicePanel_themeSelector_disabled) { + .mx_ThemeChoicePanel_themeSelector_Label { + color: var(--cpd-color-text-primary); + } + } } } diff --git a/apps/web/res/css/views/settings/tabs/_SettingsSection.pcss b/apps/web/res/css/views/settings/tabs/_SettingsSection.pcss index c98732e884..0a70169248 100644 --- a/apps/web/res/css/views/settings/tabs/_SettingsSection.pcss +++ b/apps/web/res/css/views/settings/tabs/_SettingsSection.pcss @@ -12,7 +12,8 @@ Please see LICENSE files in the repository root for full details. color: $primary-content; - a { + /* Compound links carry data-kind and provide their own colour. */ + a:not([data-kind]) { color: $links; } diff --git a/apps/web/res/css/views/settings/tabs/_SettingsTab.pcss b/apps/web/res/css/views/settings/tabs/_SettingsTab.pcss index c92e454e05..9e43079364 100644 --- a/apps/web/res/css/views/settings/tabs/_SettingsTab.pcss +++ b/apps/web/res/css/views/settings/tabs/_SettingsTab.pcss @@ -10,7 +10,8 @@ Please see LICENSE files in the repository root for full details. color: $primary-content; - a { + /* Compound links carry data-kind and provide their own colour. */ + a:not([data-kind]) { color: $links; } diff --git a/apps/web/res/themes/dark-custom/css/dark-custom.pcss b/apps/web/res/themes/dark-custom/css/dark-custom.pcss index 7eaffcee0c..1b1b30f33d 100644 --- a/apps/web/res/themes/dark-custom/css/dark-custom.pcss +++ b/apps/web/res/themes/dark-custom/css/dark-custom.pcss @@ -1,9 +1,11 @@ -@import "../../../../res/css/_font-sizes.pcss"; -@import "../../legacy-light/css/_fonts.pcss"; -@import "../../legacy-light/css/_legacy-light.pcss"; -@import "../../legacy-dark/css/_legacy-dark.pcss"; -@import "../../light-custom/css/_custom.pcss"; -@import "../../../../res/css/_components.pcss"; +@layer compound-tokens, compound-web, shared-components, app-web; + +@import "../../../../res/css/_font-sizes.pcss" layer(app-web); +@import "../../legacy-light/css/_fonts.pcss" layer(app-web); +@import "../../legacy-light/css/_legacy-light.pcss" layer(app-web); +@import "../../legacy-dark/css/_legacy-dark.pcss" layer(app-web); +@import "../../light-custom/css/_custom.pcss" layer(app-web); +@import "../../../../res/css/_components.pcss" layer(app-web); @import "../../../../res/css/_compound.pcss"; -@import url("highlight.js/styles/atom-one-light.min.css"); -@import url("github-markdown-css/github-markdown-dark.css"); +@import url("highlight.js/styles/atom-one-light.min.css") layer(app-web); +@import url("github-markdown-css/github-markdown-dark.css") layer(app-web); diff --git a/apps/web/res/themes/dark/css/dark.pcss b/apps/web/res/themes/dark/css/dark.pcss index 05cc9dfb6a..abb4fc04ee 100644 --- a/apps/web/res/themes/dark/css/dark.pcss +++ b/apps/web/res/themes/dark/css/dark.pcss @@ -1,9 +1,11 @@ -@import "../../../../res/css/_font-sizes.pcss"; -@import "../../light/css/_fonts.pcss"; -@import "../../light/css/_light.pcss"; -@import "_dark.pcss"; -@import "../../light/css/_mods.pcss"; -@import "../../../../res/css/_components.pcss"; +@layer compound-tokens, compound-web, shared-components, app-web; + +@import "../../../../res/css/_font-sizes.pcss" layer(app-web); +@import "../../light/css/_fonts.pcss" layer(app-web); +@import "../../light/css/_light.pcss" layer(app-web); +@import "_dark.pcss" layer(app-web); +@import "../../light/css/_mods.pcss" layer(app-web); +@import "../../../../res/css/_components.pcss" layer(app-web); @import "../../../../res/css/_compound.pcss"; -@import url("highlight.js/styles/atom-one-dark.min.css"); -@import url("github-markdown-css/github-markdown-dark.css"); +@import url("highlight.js/styles/atom-one-dark.min.css") layer(app-web); +@import url("github-markdown-css/github-markdown-dark.css") layer(app-web); diff --git a/apps/web/res/themes/legacy-dark/css/legacy-dark.pcss b/apps/web/res/themes/legacy-dark/css/legacy-dark.pcss index d816c8c24d..99dadd6eab 100644 --- a/apps/web/res/themes/legacy-dark/css/legacy-dark.pcss +++ b/apps/web/res/themes/legacy-dark/css/legacy-dark.pcss @@ -1,8 +1,10 @@ -@import "../../../../res/css/_font-sizes.pcss"; -@import "../../legacy-light/css/_fonts.pcss"; -@import "../../legacy-light/css/_legacy-light.pcss"; -@import "_legacy-dark.pcss"; -@import "../../../../res/css/_components.pcss"; +@layer compound-tokens, compound-web, shared-components, app-web; + +@import "../../../../res/css/_font-sizes.pcss" layer(app-web); +@import "../../legacy-light/css/_fonts.pcss" layer(app-web); +@import "../../legacy-light/css/_legacy-light.pcss" layer(app-web); +@import "_legacy-dark.pcss" layer(app-web); +@import "../../../../res/css/_components.pcss" layer(app-web); @import "../../../../res/css/_compound.pcss"; -@import url("highlight.js/styles/atom-one-dark.min.css"); -@import url("github-markdown-css/github-markdown-dark.css"); +@import url("highlight.js/styles/atom-one-dark.min.css") layer(app-web); +@import url("github-markdown-css/github-markdown-dark.css") layer(app-web); diff --git a/apps/web/res/themes/legacy-light/css/legacy-light.pcss b/apps/web/res/themes/legacy-light/css/legacy-light.pcss index d196b2850c..dcc7b1a82a 100644 --- a/apps/web/res/themes/legacy-light/css/legacy-light.pcss +++ b/apps/web/res/themes/legacy-light/css/legacy-light.pcss @@ -1,7 +1,9 @@ -@import "../../../../res/css/_font-sizes.pcss"; -@import "_fonts.pcss"; -@import "_legacy-light.pcss"; -@import "../../../../res/css/_components.pcss"; +@layer compound-tokens, compound-web, shared-components, app-web; + +@import "../../../../res/css/_font-sizes.pcss" layer(app-web); +@import "_fonts.pcss" layer(app-web); +@import "_legacy-light.pcss" layer(app-web); +@import "../../../../res/css/_components.pcss" layer(app-web); @import "../../../../res/css/_compound.pcss"; -@import url("highlight.js/styles/atom-one-light.min.css"); -@import url("github-markdown-css/github-markdown-light.css"); +@import url("highlight.js/styles/atom-one-light.min.css") layer(app-web); +@import url("github-markdown-css/github-markdown-light.css") layer(app-web); diff --git a/apps/web/res/themes/light-custom/css/_custom.pcss b/apps/web/res/themes/light-custom/css/_custom.pcss index 5f0210ed10..1bf761a7e7 100644 --- a/apps/web/res/themes/light-custom/css/_custom.pcss +++ b/apps/web/res/themes/light-custom/css/_custom.pcss @@ -25,16 +25,17 @@ $panels: var(--panels, var(--cpd-color-gray-600)); $panel-actions: var(--panels-actions, var(--cpd-color-gray-300)); /* --timeline-background-color */ -$button-secondary-bg-color: var(--timeline-background-color); -$lightbox-border-color: var(--timeline-background-color); -$menu-bg-color: var(--timeline-background-color); -$message-action-bar-bg-color: var(--timeline-background-color); -$background: var(--timeline-background-color); +$custom-theme-background: var(--timeline-background-color, var(--cpd-color-bg-canvas-default)); +$button-secondary-bg-color: $custom-theme-background; +$lightbox-border-color: $custom-theme-background; +$menu-bg-color: $custom-theme-background; +$message-action-bar-bg-color: $custom-theme-background; +$background: $custom-theme-background; $togglesw-ball-color: var(--cpd-color-bg-action-primary-rest); $togglesw-off-color: var(--togglesw-off-color); $droptarget-bg-color: var(--timeline-background-color-50pct); /* still needs alpha at .5 */ $authpage-modal-bg-color: var(--timeline-background-color-50pct); /* still needs alpha at .59 */ -$roomheader-bg-color: var(--timeline-background-color); +$roomheader-bg-color: $custom-theme-background; /* --roomlist-highlights-color */ $panel-actions: var(--roomlist-highlights-color); diff --git a/apps/web/res/themes/light-custom/css/light-custom.pcss b/apps/web/res/themes/light-custom/css/light-custom.pcss index d02c0eeef4..ce575e0518 100644 --- a/apps/web/res/themes/light-custom/css/light-custom.pcss +++ b/apps/web/res/themes/light-custom/css/light-custom.pcss @@ -1,8 +1,10 @@ -@import "../../../../res/css/_font-sizes.pcss"; -@import "../../legacy-light/css/_fonts.pcss"; -@import "../../legacy-light/css/_legacy-light.pcss"; -@import "_custom.pcss"; -@import "../../../../res/css/_components.pcss"; +@layer compound-tokens, compound-web, shared-components, app-web; + +@import "../../../../res/css/_font-sizes.pcss" layer(app-web); +@import "../../legacy-light/css/_fonts.pcss" layer(app-web); +@import "../../legacy-light/css/_legacy-light.pcss" layer(app-web); +@import "_custom.pcss" layer(app-web); +@import "../../../../res/css/_components.pcss" layer(app-web); @import "../../../../res/css/_compound.pcss"; -@import url("highlight.js/styles/atom-one-light.min.css"); -@import url("github-markdown-css/github-markdown-light.css"); +@import url("highlight.js/styles/atom-one-light.min.css") layer(app-web); +@import url("github-markdown-css/github-markdown-light.css") layer(app-web); diff --git a/apps/web/res/themes/light-high-contrast/css/light-high-contrast.pcss b/apps/web/res/themes/light-high-contrast/css/light-high-contrast.pcss index c794d07499..b1c35e414e 100644 --- a/apps/web/res/themes/light-high-contrast/css/light-high-contrast.pcss +++ b/apps/web/res/themes/light-high-contrast/css/light-high-contrast.pcss @@ -1,9 +1,11 @@ -@import "../../../../res/css/_font-sizes.pcss"; -@import "../../light/css/_fonts.pcss"; -@import "../../light/css/_light.pcss"; -@import "_light-high-contrast.pcss"; -@import "../../light/css/_mods.pcss"; -@import "../../../../res/css/_components.pcss"; +@layer compound-tokens, compound-web, shared-components, app-web; + +@import "../../../../res/css/_font-sizes.pcss" layer(app-web); +@import "../../light/css/_fonts.pcss" layer(app-web); +@import "../../light/css/_light.pcss" layer(app-web); +@import "_light-high-contrast.pcss" layer(app-web); +@import "../../light/css/_mods.pcss" layer(app-web); +@import "../../../../res/css/_components.pcss" layer(app-web); @import "../../../../res/css/_compound.pcss"; -@import url("highlight.js/styles/atom-one-light.min.css"); -@import url("github-markdown-css/github-markdown-light.css"); +@import url("highlight.js/styles/atom-one-light.min.css") layer(app-web); +@import url("github-markdown-css/github-markdown-light.css") layer(app-web); diff --git a/apps/web/res/themes/light/css/light.pcss b/apps/web/res/themes/light/css/light.pcss index d3abfe669c..10e5e05d78 100644 --- a/apps/web/res/themes/light/css/light.pcss +++ b/apps/web/res/themes/light/css/light.pcss @@ -1,8 +1,10 @@ -@import "../../../../res/css/_font-sizes.pcss"; -@import "_fonts.pcss"; -@import "_light.pcss"; -@import "_mods.pcss"; -@import "../../../../res/css/_components.pcss"; +@layer compound-tokens, compound-web, shared-components, app-web; + +@import "../../../../res/css/_font-sizes.pcss" layer(app-web); +@import "_fonts.pcss" layer(app-web); +@import "_light.pcss" layer(app-web); +@import "_mods.pcss" layer(app-web); +@import "../../../../res/css/_components.pcss" layer(app-web); @import "../../../../res/css/_compound.pcss"; -@import url("highlight.js/styles/atom-one-light.min.css"); -@import url("github-markdown-css/github-markdown-light.css"); +@import url("highlight.js/styles/atom-one-light.min.css") layer(app-web); +@import url("github-markdown-css/github-markdown-light.css") layer(app-web); diff --git a/apps/web/src/components/views/messages/TextualBodyFactory.tsx b/apps/web/src/components/views/messages/TextualBodyFactory.tsx index d947bb982e..b51eebe78b 100644 --- a/apps/web/src/components/views/messages/TextualBodyFactory.tsx +++ b/apps/web/src/components/views/messages/TextualBodyFactory.tsx @@ -210,7 +210,7 @@ export function TextualBodyFactory(props: Readonly): JSX.Element { vm={textualBodyVm} body={} bodyRef={contentRef} - urlPreviews={} + urlPreviews={} className={getTextualBodyClassName(content.msgtype as MsgType | undefined)} /> ); diff --git a/apps/web/src/theme.ts b/apps/web/src/theme.ts index a0a527c28a..4bd3ea322d 100644 --- a/apps/web/src/theme.ts +++ b/apps/web/src/theme.ts @@ -207,9 +207,10 @@ function generateCustomCompoundCSS(theme: CompoundTheme): 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(" ")} } }`; + // Insert the design token overrides into the existing Compound tokens + // layer so custom themes win over the imported default tokens by source + // order without creating a lower-priority nested layer. + return `@layer compound-tokens { :root, [class*="cpd-theme-"] { ${properties.join(" ")} } }`; } /** diff --git a/apps/web/src/utils/exportUtils/exportCSS.ts b/apps/web/src/utils/exportUtils/exportCSS.ts index 8c470aaa0d..7e55b0cebe 100644 --- a/apps/web/src/utils/exportUtils/exportCSS.ts +++ b/apps/web/src/utils/exportUtils/exportCSS.ts @@ -6,11 +6,12 @@ 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 type { Rule, StyleSheet } from "css-tree"; +import type { CssNode, Rule, StyleSheet } from "css-tree"; import customCSS from "!!raw-loader!./exportCustomCSS.css"; const cssSelectorTextClassesRegex = /\.[\w-]+/g; +const appLayerName = "app-web"; function mutateCssText(css: string): string { // replace used fonts so that we don't have to bundle Inter & Fira Code @@ -41,6 +42,21 @@ function includeRule(rule: Rule, usedClasses: Set): boolean { return true; } +function includeNode(node: CssNode, usedClasses: Set): boolean { + if (node.type === "Atrule") { + if (node.name === "font-face") { + return false; + } + + if (node.block) { + node.block.children = node.block.children.filter((child) => includeNode(child, usedClasses)); + return !node.block.children.isEmpty; + } + } + + return node.type !== "Rule" || includeRule(node, usedClasses); +} + // naively culls unused css rules based on which classes are present in the html, // doesn't cull rules which won't apply due to the full selector not matching but gets rid of a LOT of cruft anyway. // We cannot use document.styleSheets as it does not handle variables in shorthand properties sanely, @@ -69,13 +85,7 @@ const getExportCSS = async (usedClasses: Set): Promise => { }) as StyleSheet; for (const rule of ast.children) { - if (rule.type === "Atrule") { - if (rule.name === "font-face") { - continue; - } - } - - if (rule.type === "Rule" && !includeRule(rule, usedClasses)) { + if (!includeNode(rule, usedClasses)) { continue; } @@ -83,7 +93,7 @@ const getExportCSS = async (usedClasses: Set): Promise => { } } - return css + customCSS; + return `${css}@layer ${appLayerName} {${customCSS}}`; }; export default getExportCSS; diff --git a/apps/web/src/utils/exportUtils/exportCustomCSS.css b/apps/web/src/utils/exportUtils/exportCustomCSS.css index 117eff6b23..fe8f7c7269 100644 --- a/apps/web/src/utils/exportUtils/exportCustomCSS.css +++ b/apps/web/src/utils/exportUtils/exportCustomCSS.css @@ -60,11 +60,6 @@ body { a.mx_reply_anchor { cursor: pointer; - color: #238cf5; -} - -a.mx_reply_anchor:hover { - text-decoration: underline; } @-webkit-keyframes mx_snackbar_fadein { diff --git a/apps/web/test/unit-tests/__snapshots__/theme-test.ts.snap b/apps/web/test/unit-tests/__snapshots__/theme-test.ts.snap index af61bc7151..83b0c90a7c 100644 --- a/apps/web/test/unit-tests/__snapshots__/theme-test.ts.snap +++ b/apps/web/test/unit-tests/__snapshots__/theme-test.ts.snap @@ -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); } }"`; diff --git a/apps/web/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap b/apps/web/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap index 745d84f52b..0a6b8d1acc 100644 --- a/apps/web/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/apps/web/test/unit-tests/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -237,7 +237,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`] class="_content_n7ud0_38" >

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" >

00:00
Message #49
  • @user48:example.com
    00:00
    Message #48
  • @user47:example.com
    00:00
    Message #47
  • @user46:example.com
    00:00
    Message #46
  • @user45:example.com
    00:00
    Message #45
  • @user44:example.com
    00:00
    Message #44
  • @user43:example.com
    00:00
    Message #43
  • @user42:example.com
    00:00
    Message #42
  • @user41:example.com
    00:00
    Message #41
  • @user40:example.com
    00:00
    Message #40
  • @user39:example.com
    00:00
    Message #39
  • @user38:example.com
    00:00
    Message #38
  • @user37:example.com
    00:00
    Message #37
  • @user36:example.com
    00:00
    Message #36
  • @user35:example.com
    00:00
    Message #35
  • @user34:example.com
    00:00
    Message #34
  • @user33:example.com
    00:00
    Message #33
  • @user32:example.com
    00:00
    Message #32
  • @user31:example.com
    00:00
    Message #31
  • @user30:example.com
    00:00
    Message #30
  • @user29:example.com
    00:00
    Message #29
  • @user28:example.com
    00:00
    Message #28
  • @user27:example.com
    00:00
    Message #27
  • @user26:example.com
    00:00
    Message #26
  • @user25:example.com
    00:00
    Message #25
  • @user24:example.com
    00:00
    Message #24
  • @user23:example.com
    00:00
    Message #23
  • @user22:example.com
    00:00
    Message #22
  • @user21:example.com
    00:00
    Message #21
  • @user20:example.com
    00:00
    Message #20
  • @user19:example.com
    00:00
    Message #19
  • @user18:example.com
    00:00
    Message #18
  • @user17:example.com
    00:00
    Message #17
  • @user16:example.com
    00:00
    Message #16
  • @user15:example.com
    00:00
    Message #15
  • @user14:example.com
    00:00
    Message #14
  • @user13:example.com
    00:00
    Message #13
  • @user12:example.com
    00:00
    Message #12
  • @user11:example.com
    00:00
    Message #11
  • @user10:example.com
    00:00
    Message #10
  • @user9:example.com
    00:00
    Message #9
  • @user8:example.com
    00:00
    Message #8
  • @user7:example.com
    00:00
    Message #7
  • @user6:example.com
    00:00
    Message #6
  • @user5:example.com
    00:00
    Message #5
  • @user4:example.com
    00:00
    Message #4
  • @user3:example.com
    00:00
    Message #3
  • @user2:example.com
    00:00
    Message #2
  • @user1:example.com
    00:00
    Message #1
  • @user0:example.com
    00:00
    Message #0
  • +
  • @user49:example.com
    00:00
    Message #49
  • @user48:example.com
    00:00
    Message #48
  • @user47:example.com
    00:00
    Message #47
  • @user46:example.com
    00:00
    Message #46
  • @user45:example.com
    00:00
    Message #45
  • @user44:example.com
    00:00
    Message #44
  • @user43:example.com
    00:00
    Message #43
  • @user42:example.com
    00:00
    Message #42
  • @user41:example.com
    00:00
    Message #41
  • @user40:example.com
    00:00
    Message #40
  • @user39:example.com
    00:00
    Message #39
  • @user38:example.com
    00:00
    Message #38
  • @user37:example.com
    00:00
    Message #37
  • @user36:example.com
    00:00
    Message #36
  • @user35:example.com
    00:00
    Message #35
  • @user34:example.com
    00:00
    Message #34
  • @user33:example.com
    00:00
    Message #33
  • @user32:example.com
    00:00
    Message #32
  • @user31:example.com
    00:00
    Message #31
  • @user30:example.com
    00:00
    Message #30
  • @user29:example.com
    00:00
    Message #29
  • @user28:example.com
    00:00
    Message #28
  • @user27:example.com
    00:00
    Message #27
  • @user26:example.com
    00:00
    Message #26
  • @user25:example.com
    00:00
    Message #25
  • @user24:example.com
    00:00
    Message #24
  • @user23:example.com
    00:00
    Message #23
  • @user22:example.com
    00:00
    Message #22
  • @user21:example.com
    00:00
    Message #21
  • @user20:example.com
    00:00
    Message #20
  • @user19:example.com
    00:00
    Message #19
  • @user18:example.com
    00:00
    Message #18
  • @user17:example.com
    00:00
    Message #17
  • @user16:example.com
    00:00
    Message #16
  • @user15:example.com
    00:00
    Message #15
  • @user14:example.com
    00:00
    Message #14
  • @user13:example.com
    00:00
    Message #13
  • @user12:example.com
    00:00
    Message #12
  • @user11:example.com
    00:00
    Message #11
  • @user10:example.com
    00:00
    Message #10
  • @user9:example.com
    00:00
    Message #9
  • @user8:example.com
    00:00
    Message #8
  • @user7:example.com
    00:00
    Message #7
  • @user6:example.com
    00:00
    Message #6
  • @user5:example.com
    00:00
    Message #5
  • @user4:example.com
    00:00
    Message #4
  • @user3:example.com
    00:00
    Message #3
  • @user2:example.com
    00:00
    Message #2
  • @user1:example.com
    00:00
    Message #1
  • @user0:example.com
    00:00
    Message #0
  • diff --git a/apps/web/test/unit-tests/utils/exportUtils/exportCSS-test.ts b/apps/web/test/unit-tests/utils/exportUtils/exportCSS-test.ts index 0cf154e9be..dc37a022e8 100644 --- a/apps/web/test/unit-tests/utils/exportUtils/exportCSS-test.ts +++ b/apps/web/test/unit-tests/utils/exportUtils/exportCSS-test.ts @@ -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 = ` + + + + `; + + 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 = ` + + `; + + 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}"); + }); }); }); diff --git a/knip.ts b/knip.ts index 53718f2e15..9d4fab1d49 100644 --- a/knip.ts +++ b/knip.ts @@ -65,13 +65,8 @@ export default { ignoreExportsUsedInFile: true, compilers: { pcss: (text: string) => - [...text.matchAll(/(?<=@)import[^;]+/g)] - .map(([line]) => { - if (line.startsWith("import url(")) { - return line.replace("url(", "").slice(0, -1); - } - return line; - }) + [...text.matchAll(/@import\s+(?:url\()?["']([^"']+)["']\)?[^;]*;/g)] + .map(([, specifier]) => `import "${specifier}";`) .join("\n"), }, nx: { diff --git a/packages/shared-components/.storybook/compound.css b/packages/shared-components/.storybook/compound.css index 42f8393666..dda8ccf13e 100644 --- a/packages/shared-components/.storybook/compound.css +++ b/packages/shared-components/.storybook/compound.css @@ -5,5 +5,8 @@ * Please see LICENSE files in the repository root for full details. */ -@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"); +/* Shared cascade order: Compound tokens, Compound Web, shared components, then app overrides. */ +@layer compound-tokens, compound-web, shared-components, app-web; + +@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css") layer(compound-tokens); +@import url("@vector-im/compound-web/dist/style.css") layer(compound-web); diff --git a/packages/shared-components/.storybook/main.ts b/packages/shared-components/.storybook/main.ts index 72cb55aea5..60fb388507 100644 --- a/packages/shared-components/.storybook/main.ts +++ b/packages/shared-components/.storybook/main.ts @@ -8,11 +8,13 @@ Please see LICENSE files in the repository root for full details. import type { StorybookConfig } from "@storybook/react-vite"; import fs from "node:fs"; import { nodePolyfills } from "vite-plugin-node-polyfills"; -import { mergeConfig } from "vite"; +import { mergeConfig, normalizePath, type Plugin } from "vite"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); +const srcRoot = normalizePath(join(__dirname, "..", "src")); +const sharedComponentsLayer = "shared-components"; // Get a list of available languages so the language selector can display them at runtime const languageFiles = fs.readdirSync(join(__dirname, "..", "src", "i18n", "strings")).map((f) => f.slice(0, -5)); @@ -36,6 +38,24 @@ function getAbsolutePath(value: string): any { return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`))); } +function layerSharedComponentCssModules(): Plugin { + return { + name: "element-web-shared-components-storybook-css-layer", + enforce: "pre", + transform(code, id) { + const cssPath = normalizePath(id.split("?")[0]); + if (!cssPath.startsWith(srcRoot) || !cssPath.endsWith(".module.css")) { + return; + } + + return { + code: `@layer ${sharedComponentsLayer} {\n${code}\n}\n`, + map: null, + }; + }, + }; +} + const config: StorybookConfig = { stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], addons: [ @@ -61,6 +81,7 @@ const config: StorybookConfig = { async viteFinal(config) { return mergeConfig(config, { plugins: [ + layerSharedComponentCssModules(), // Needed for counterpart to work nodePolyfills({ include: ["util"], globals: { global: false } }), { diff --git a/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.module.css b/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.module.css index 859f84bba9..74315673f1 100644 --- a/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.module.css +++ b/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.module.css @@ -6,6 +6,6 @@ */ .button { - border-radius: 32px !important; - background-color: var(--cpd-color-bg-subtle-primary) !important; + border-radius: 32px; + background-color: var(--cpd-color-bg-subtle-primary); } diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.module.css b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.module.css index 11a81da947..255babced3 100644 --- a/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.module.css +++ b/packages/shared-components/src/room-list/RoomListHeaderView/menu/OptionMenuView.module.css @@ -7,5 +7,5 @@ .title { /* For first title, there is already enough space at the top */ - margin-top: 0 !important; + margin-top: 0; } diff --git a/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.module.css b/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.module.css index 9b7097373c..20b71a90bf 100644 --- a/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.module.css +++ b/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.module.css @@ -16,9 +16,8 @@ .search { /* The search button should take all the remaining space */ flex: 1; - /* !important is needed to override compound button in EW */ - font: var(--cpd-font-body-md-regular) !important; - color: var(--cpd-color-text-secondary) !important; + font: var(--cpd-font-body-md-regular); + color: var(--cpd-color-text-secondary); min-width: 0; svg { diff --git a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.module.css b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.module.css index c3832e2e9f..78300df3f9 100644 --- a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.module.css +++ b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.module.css @@ -7,18 +7,18 @@ .container { color: var(--cpd-color-text-primary); - svg { - /* Ensure button icons are primary too */ - color: var(--cpd-color-text-primary) !important; - } } -.secondaryAction svg { - color: var(--cpd-color-text-secondary) !important; +.container svg { + color: var(--cpd-color-text-primary); } -.primaryAction svg { - color: var(--cpd-color-text-on-solid-primary) !important; +.secondaryAction.secondaryAction[data-kind="secondary"] > svg { + color: var(--cpd-color-text-secondary); +} + +.primaryAction.primaryAction[data-kind="primary"] > svg { + color: var(--cpd-color-text-on-solid-primary); } .title { diff --git a/packages/shared-components/src/room/timeline/DateSeparatorView/DateSeparatorContextMenuView.module.css b/packages/shared-components/src/room/timeline/DateSeparatorView/DateSeparatorContextMenuView.module.css index fff772c4bd..75aa130936 100644 --- a/packages/shared-components/src/room/timeline/DateSeparatorView/DateSeparatorContextMenuView.module.css +++ b/packages/shared-components/src/room/timeline/DateSeparatorView/DateSeparatorContextMenuView.module.css @@ -6,15 +6,15 @@ */ .picker_menu { - max-inline-size: none !important; - padding: 0 !important; - gap: 0 !important; + max-inline-size: none; + padding: 0; + gap: 0; } .picker_menu_item { - padding: var(--cpd-space-3x) var(--cpd-space-5x) !important; + padding: var(--cpd-space-3x) var(--cpd-space-5x); } .picker_separator { - margin-inline: 0 !important; + margin-inline: 0; } diff --git a/packages/shared-components/src/room/timeline/DateSeparatorView/DateSeparatorDatePickerView.module.css b/packages/shared-components/src/room/timeline/DateSeparatorView/DateSeparatorDatePickerView.module.css index e664b1f5eb..f8e9894c38 100644 --- a/packages/shared-components/src/room/timeline/DateSeparatorView/DateSeparatorDatePickerView.module.css +++ b/packages/shared-components/src/room/timeline/DateSeparatorView/DateSeparatorDatePickerView.module.css @@ -6,15 +6,15 @@ */ .picker_menu_item { - padding: var(--cpd-space-3x) var(--cpd-space-5x) !important; + padding: var(--cpd-space-3x) var(--cpd-space-5x); } .picker_form { display: flex; - flex-direction: row !important; - flex-wrap: wrap !important; - padding: 0 !important; - gap: var(--cpd-space-2x) !important; + flex-direction: row; + flex-wrap: wrap; + padding: 0; + gap: var(--cpd-space-2x); color: var(--cpd-color-text-primary); font: var(--cpd-font-body-md-medium); } @@ -28,7 +28,7 @@ .picker_input_date { font: inherit; color-scheme: light; - padding: var(--cpd-space-2x) !important; + padding: var(--cpd-space-2x); :global(.cpd-theme-dark) &, :global(.cpd-theme-dark-hc) & { diff --git a/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/UrlPreviewGroupView.tsx b/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/UrlPreviewGroupView.tsx index 79f0a96f93..01c7a07441 100644 --- a/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/UrlPreviewGroupView.tsx +++ b/packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/UrlPreviewGroupView.tsx @@ -25,7 +25,14 @@ export interface UrlPreviewGroupViewSnapshot { } export interface UrlPreviewGroupViewProps { + /** + * The view model for the component. + */ vm: ViewModel & UrlPreviewGroupViewActions; + /** + * Extra CSS classes to apply to the component. + */ + className?: string; } export interface UrlPreviewGroupViewActions { @@ -42,7 +49,7 @@ export type UrlPreviewGroupViewModel = ViewModel +
    {previews.map((preview) => ( vm.onImageClick(preview)} {...preview} /> diff --git a/packages/shared-components/src/room/timeline/event-tile/actions/ActionBarView/ActionBarView.module.css b/packages/shared-components/src/room/timeline/event-tile/actions/ActionBarView/ActionBarView.module.css index 50cbb3f0d6..6b08fa635e 100644 --- a/packages/shared-components/src/room/timeline/event-tile/actions/ActionBarView/ActionBarView.module.css +++ b/packages/shared-components/src/room/timeline/event-tile/actions/ActionBarView/ActionBarView.module.css @@ -14,31 +14,31 @@ .toolbar_item { align-items: center; justify-content: center; - padding: 0 !important; - min-block-size: 0 !important; + padding: 0; + min-block-size: 0; - margin: 3px !important; - border-radius: 6px !important; + margin: 3px; + border-radius: 6px; &:hover { - background-color: var(--cpd-color-bg-subtle-secondary) !important; + background-color: var(--cpd-color-bg-subtle-secondary); z-index: 1; } } .toolbar_item[data-presentation="icon"] { - block-size: 28px !important; - inline-size: 28px !important; + block-size: 28px; + inline-size: 28px; - color: var(--cpd-color-icon-secondary) !important; + color: var(--cpd-color-icon-secondary); } .toolbar_item[data-presentation="label"] { - padding-inline-start: var(--cpd-space-2x) !important; - padding-inline-end: var(--cpd-space-2x) !important; + padding-inline-start: var(--cpd-space-2x); + padding-inline-end: var(--cpd-space-2x); font: var(--cpd-font-body-md-regular); - text-decoration: none !important; + text-decoration: none; white-space: nowrap; } } diff --git a/packages/shared-components/src/room/timeline/event-tile/body/AudioPlayerView/AudioPlayerView.module.css b/packages/shared-components/src/room/timeline/event-tile/body/AudioPlayerView/AudioPlayerView.module.css index f6bfec3969..1842cc90a1 100644 --- a/packages/shared-components/src/room/timeline/event-tile/body/AudioPlayerView/AudioPlayerView.module.css +++ b/packages/shared-components/src/room/timeline/event-tile/body/AudioPlayerView/AudioPlayerView.module.css @@ -6,8 +6,8 @@ */ .audioPlayer { - padding: var(--cpd-space-4x) var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-3x) !important; - border-radius: 8px !important; + padding: var(--cpd-space-4x) var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-3x); + border-radius: 8px; } .mediaInfo { diff --git a/packages/shared-components/src/room/timeline/event-tile/body/MFileBodyView/FileBodyView.module.css b/packages/shared-components/src/room/timeline/event-tile/body/MFileBodyView/FileBodyView.module.css index bf71413b93..52434c133d 100644 --- a/packages/shared-components/src/room/timeline/event-tile/body/MFileBodyView/FileBodyView.module.css +++ b/packages/shared-components/src/room/timeline/event-tile/body/MFileBodyView/FileBodyView.module.css @@ -26,8 +26,8 @@ & button { display: flex; - margin: 0 !important; - padding: 0 !important; + margin: 0; + padding: 0; border: none; width: 100%; min-block-size: unset; @@ -57,7 +57,6 @@ .content [data-type="download"] { align-items: center; - color: var(--cpd-color-text-action-accent); & iframe { margin: 0; diff --git a/packages/shared-components/src/room/timeline/event-tile/timestamp/MessageTimestampView/MessageTimestampView.module.css b/packages/shared-components/src/room/timeline/event-tile/timestamp/MessageTimestampView/MessageTimestampView.module.css index 6ee9d62aa1..e00b9f0678 100644 --- a/packages/shared-components/src/room/timeline/event-tile/timestamp/MessageTimestampView/MessageTimestampView.module.css +++ b/packages/shared-components/src/room/timeline/event-tile/timestamp/MessageTimestampView/MessageTimestampView.module.css @@ -6,7 +6,7 @@ */ .content { - color: var(--cpd-color-text-secondary) !important; /* override anchor color */ + color: var(--cpd-color-text-secondary); /* override anchor color */ font-size: var(--cpd-font-size-body-xs); font-variant-numeric: tabular-nums; display: inline-block; diff --git a/packages/shared-components/vite.config.ts b/packages/shared-components/vite.config.ts index 0a27742816..116689e645 100644 --- a/packages/shared-components/vite.config.ts +++ b/packages/shared-components/vite.config.ts @@ -6,12 +6,36 @@ * */ +import { readFileSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { defineConfig, esmExternalRequirePlugin } from "vite"; +import { defineConfig, esmExternalRequirePlugin, type Plugin } from "vite"; import dts from "unplugin-dts/vite"; const __dirname = dirname(fileURLToPath(import.meta.url)); +const cssLayerOrder = "@layer compound-tokens, compound-web, shared-components, app-web;"; +const sharedComponentsLayer = "shared-components"; + +function layerCssAssets(): Plugin { + return { + name: "element-web-shared-components-css-layer", + writeBundle(_options, bundle): void { + for (const asset of Object.values(bundle)) { + if (asset.type !== "asset" || asset.fileName !== "element-web-shared-components.css") { + continue; + } + + const cssPath = resolve(__dirname, "dist", asset.fileName); + const source = readFileSync(cssPath, "utf-8"); + if (source.startsWith(cssLayerOrder)) { + continue; + } + + writeFileSync(cssPath, `${cssLayerOrder}\n@layer ${sharedComponentsLayer} {\n${source}\n}\n`); + } + }, + }; +} export default defineConfig({ build: { @@ -50,6 +74,7 @@ export default defineConfig({ }, }, plugins: [ + layerCssAssets(), dts({ bundleTypes: true, include: ["src/**/*.{ts,tsx}"],