From 3da4a9e873a1907fee09da033a86a20b22e9d616 Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Wed, 15 Oct 2025 13:31:44 +0300 Subject: [PATCH 1/6] chore: WIP scope theming css variables with packages --- packages/base/lib/generate-styles/index.js | 2 +- packages/base/src/theming/applyTheme.ts | 18 +++++------------- .../css-processors/css-processor-themes.mjs | 14 +++++++++++++- .../lib/css-processors/scope-variables.mjs | 16 ++++++++-------- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/base/lib/generate-styles/index.js b/packages/base/lib/generate-styles/index.js index 6c00d978ba7a..4867c66b5c22 100644 --- a/packages/base/lib/generate-styles/index.js +++ b/packages/base/lib/generate-styles/index.js @@ -9,7 +9,7 @@ const generate = async () => { const filesPromises = files.map(async file => { let content = await fs.readFile(path.join("src/css/", file)); const res = new CleanCSS().minify(`${content}`); - content = `export default \`${res.styles}\`;`; + content = `export default \`${res.styles.replaceAll("--sap", "--ui5-sap")}\`;`; return fs.writeFile(path.join("src/generated/css/", `${file}.ts`), content); }); diff --git a/packages/base/src/theming/applyTheme.ts b/packages/base/src/theming/applyTheme.ts index c8f7d729c156..cd4008b1a492 100644 --- a/packages/base/src/theming/applyTheme.ts +++ b/packages/base/src/theming/applyTheme.ts @@ -1,5 +1,5 @@ import { getThemeProperties, getRegisteredPackages, isThemeRegistered } from "../asset-registries/Themes.js"; -import { removeStyle, createOrUpdateStyle } from "../ManagedStyles.js"; +import { createOrUpdateStyle } from "../ManagedStyles.js"; import getThemeDesignerTheme from "./getThemeDesignerTheme.js"; import { fireThemeLoaded } from "./ThemeLoaded.js"; import { getFeature } from "../FeaturesRegistry.js"; @@ -31,10 +31,6 @@ const loadThemeBase = async (theme: string) => { } }; -const deleteThemeBase = () => { - removeStyle("data-ui5-theme-properties", BASE_THEME_PACKAGE); -}; - const loadComponentPackages = async (theme: string, externalThemeName?: string) => { const registeredPackages = getRegisteredPackages(); @@ -79,16 +75,12 @@ const detectExternalTheme = async (theme: string) => { const applyTheme = async (theme: string) => { const extTheme = await detectExternalTheme(theme); - // Only load theme_base properties if there is no externally loaded theme, or there is, but it is not being loaded - if (!extTheme || theme !== extTheme.themeName) { - await loadThemeBase(theme); - } else { - deleteThemeBase(); - } - // Always load component packages properties. For non-registered themes, try with the base theme, if any const packagesTheme = isThemeRegistered(theme) ? theme : extTheme && extTheme.baseThemeName; - await loadComponentPackages(packagesTheme || DEFAULT_THEME, extTheme && extTheme.themeName === theme ? theme : undefined); + const effectiveTheme = packagesTheme || DEFAULT_THEME; + + await loadThemeBase(effectiveTheme); + await loadComponentPackages(effectiveTheme, extTheme && extTheme.themeName === theme ? theme : undefined); fireThemeLoaded(theme); }; diff --git a/packages/tools/lib/css-processors/css-processor-themes.mjs b/packages/tools/lib/css-processors/css-processor-themes.mjs index 7fd4f818470b..6c179fbc43e6 100644 --- a/packages/tools/lib/css-processors/css-processor-themes.mjs +++ b/packages/tools/lib/css-processors/css-processor-themes.mjs @@ -8,6 +8,17 @@ import combineDuplicatedSelectors from "../postcss-combine-duplicated-selectors/ import { writeFileIfChanged, getFileContent } from "./shared.mjs"; import scopeVariables from "./scope-variables.mjs"; +const cloneThemingDeclaration = (decl) => { + if (!decl.prop.startsWith('--sap')) { + return decl.clone(); + } + + const originalProp = decl.prop; + const originalValue = decl.value; + + return decl.clone({ prop: originalProp.replace("--sap", "--ui5-sap"), value: `var(${originalProp}, ${originalValue})` }); +} + const generate = async (argv) => { const tsMode = process.env.UI5_TS === "true"; const extension = tsMode ? ".css.ts" : ".css.js"; @@ -28,7 +39,8 @@ const generate = async (argv) => { result.root.walkRules(selector, rule => { rule.walkDecls(decl => { if (!decl.prop.startsWith('--sapFontUrl')) { - newRule.append(decl.clone()); + + newRule.append(cloneThemingDeclaration(decl)); } }); }); diff --git a/packages/tools/lib/css-processors/scope-variables.mjs b/packages/tools/lib/css-processors/scope-variables.mjs index c824e6ba9d3e..9f896a271f29 100644 --- a/packages/tools/lib/css-processors/scope-variables.mjs +++ b/packages/tools/lib/css-processors/scope-variables.mjs @@ -9,9 +9,9 @@ const require = createRequire(import.meta.url); * @returns */ const getOverrideVersion = filePath => { - if (!filePath) { - return; - } + if (!filePath) { + return; + } if (!filePath.includes(`overrides${path.sep}`)) { return; // The "overrides/" directory is the marker @@ -37,12 +37,12 @@ const getOverrideVersion = filePath => { } const scopeVariables = (cssText, packageJSON, inputFile) => { - const escapeVersion = version => "v" + version?.replaceAll(/[^0-9A-Za-z\-_]/g, "-"); - const versionStr = escapeVersion(getOverrideVersion(inputFile) || packageJSON.version); - - const expr = /(--_?ui5)([^\,\:\)\s]+)/g; + const escapeVersion = version => "v" + version?.replaceAll(/[^0-9A-Za-z\-_]/g, "-"); + const versionStr = escapeVersion(getOverrideVersion(inputFile) || packageJSON.version); + const expr = /(--_?ui5)([^\,\:\)\s]+)/g; + let newText = cssText.replaceAll(expr, `$1-${versionStr}$2`); - return cssText.replaceAll(expr, `$1-${versionStr}$2`); + return newText.replaceAll("--sap", `--ui5-sap`); } export default scopeVariables; From 036ce9ae2a9e93847d2d8581ac3f415e2bd9d1d8 Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Mon, 20 Oct 2025 13:43:33 +0300 Subject: [PATCH 2/6] chore: reuse scope logic --- packages/base/lib/generate-styles/index.js | 7 +- .../css-processor-components.mjs | 4 +- .../css-processors/css-processor-themes.mjs | 70 ++++++++++--------- .../lib/css-processors/scope-variables.mjs | 11 ++- 4 files changed, 53 insertions(+), 39 deletions(-) diff --git a/packages/base/lib/generate-styles/index.js b/packages/base/lib/generate-styles/index.js index 4867c66b5c22..65b8d4dcff3f 100644 --- a/packages/base/lib/generate-styles/index.js +++ b/packages/base/lib/generate-styles/index.js @@ -9,7 +9,12 @@ const generate = async () => { const filesPromises = files.map(async file => { let content = await fs.readFile(path.join("src/css/", file)); const res = new CleanCSS().minify(`${content}`); - content = `export default \`${res.styles.replaceAll("--sap", "--ui5-sap")}\`;`; + + // Scope used variables + content = await processComponentPackageFile(res.styles); + + content = `export default \`${content}\`;`; + return fs.writeFile(path.join("src/generated/css/", `${file}.ts`), content); }); diff --git a/packages/tools/lib/css-processors/css-processor-components.mjs b/packages/tools/lib/css-processors/css-processor-components.mjs index 8315259e4499..5d6266ace177 100644 --- a/packages/tools/lib/css-processors/css-processor-components.mjs +++ b/packages/tools/lib/css-processors/css-processor-components.mjs @@ -4,7 +4,7 @@ import * as fs from "fs"; import * as path from "path"; import { writeFile, mkdir } from "fs/promises"; import chokidar from "chokidar"; -import scopeVariables from "./scope-variables.mjs"; +import {scopeUi5Variables} from "./scope-variables.mjs"; import { writeFileIfChanged, getFileContent } from "./shared.mjs"; const generate = async (argv) => { @@ -23,7 +23,7 @@ const generate = async (argv) => { build.onEnd(result => { result.outputFiles.forEach(async f => { // scoping - let newText = scopeVariables(f.text, packageJSON); + let newText = scopeUi5Variables(f.text, packageJSON); newText = newText.replaceAll(/\\/g, "\\\\"); // Escape backslashes as they might appear in css rules await mkdir(path.dirname(f.path), { recursive: true }); writeFile(f.path, newText); diff --git a/packages/tools/lib/css-processors/css-processor-themes.mjs b/packages/tools/lib/css-processors/css-processor-themes.mjs index 6c179fbc43e6..181400ff07b4 100644 --- a/packages/tools/lib/css-processors/css-processor-themes.mjs +++ b/packages/tools/lib/css-processors/css-processor-themes.mjs @@ -6,20 +6,42 @@ import { writeFile, mkdir } from "fs/promises"; import postcss from "postcss"; import combineDuplicatedSelectors from "../postcss-combine-duplicated-selectors/index.js" import { writeFileIfChanged, getFileContent } from "./shared.mjs"; -import scopeVariables from "./scope-variables.mjs"; +import { scopeUi5Variables, scopeThemingVariables } from "./scope-variables.mjs"; -const cloneThemingDeclaration = (decl) => { - if (!decl.prop.startsWith('--sap')) { - return decl.clone(); - } +async function processThemingPackageFile(f) { + const selector = ':root'; + const newRule = postcss.rule({ selector }); + const result = await postcss().process(f.text); + + result.root.walkRules(selector, rule => { + for (const decl of rule.nodes) { + if (decl.type !== 'decl' || !decl.prop.startsWith('--sapFontUrl')) { + continue; + } else if (!decl.prop.startsWith('--sap')) { + newRule.append(decl.clone()); + } else { + const originalProp = decl.prop; + const originalValue = decl.value; + + newRule.append(decl.clone({ prop: originalProp.replace("--sap", "--ui5-sap"), value: `var(${originalProp}, ${originalValue})` })); + } + } + }); + + return newRule.toString(); +}; + +async function processComponentPackageFile(f, packageJSON) { + let result = await postcss(combineDuplicatedSelectors).process(f.text); - const originalProp = decl.prop; - const originalValue = decl.value; + result = scopeUi5Variables(result.css, packageJSON, f.path); - return decl.clone({ prop: originalProp.replace("--sap", "--ui5-sap"), value: `var(${originalProp}, ${originalValue})` }); + result = scopeThemingVariables(result); + + return result; } -const generate = async (argv) => { +async function generate(argv) { const tsMode = process.env.UI5_TS === "true"; const extension = tsMode ? ".css.ts" : ".css.js"; @@ -30,30 +52,6 @@ const generate = async (argv) => { ]); const restArgs = argv.slice(2); - const processThemingPackageFile = async (f) => { - const selector = ':root'; - const result = await postcss().process(f.text); - - const newRule = postcss.rule({ selector }); - - result.root.walkRules(selector, rule => { - rule.walkDecls(decl => { - if (!decl.prop.startsWith('--sapFontUrl')) { - - newRule.append(cloneThemingDeclaration(decl)); - } - }); - }); - - return newRule.toString(); - }; - - const processComponentPackageFile = async (f) => { - const result = await postcss(combineDuplicatedSelectors).process(f.text); - - return scopeVariables(result.css, packageJSON, f.path); - } - let scopingPlugin = { name: 'scoping', setup(build) { @@ -61,7 +59,7 @@ const generate = async (argv) => { build.onEnd(result => { result.outputFiles.forEach(async f => { - let newText = f.path.includes("packages/theming") ? await processThemingPackageFile(f) : await processComponentPackageFile(f); + let newText = f.path.includes("packages/theming") ? await processThemingPackageFile(f) : await processComponentPackageFile(f, packageJSON); await mkdir(path.dirname(f.path), { recursive: true }); writeFile(f.path, newText); @@ -107,4 +105,8 @@ if (import.meta.url === `file://${process.argv[1]}`) { export default { _ui5mainFn: generate +} + +export { + processComponentPackageFile } \ No newline at end of file diff --git a/packages/tools/lib/css-processors/scope-variables.mjs b/packages/tools/lib/css-processors/scope-variables.mjs index 9f896a271f29..dea8968e4e70 100644 --- a/packages/tools/lib/css-processors/scope-variables.mjs +++ b/packages/tools/lib/css-processors/scope-variables.mjs @@ -36,7 +36,7 @@ const getOverrideVersion = filePath => { return overrideVersion; } -const scopeVariables = (cssText, packageJSON, inputFile) => { +const scopeUi5Variables = (cssText, packageJSON, inputFile) => { const escapeVersion = version => "v" + version?.replaceAll(/[^0-9A-Za-z\-_]/g, "-"); const versionStr = escapeVersion(getOverrideVersion(inputFile) || packageJSON.version); const expr = /(--_?ui5)([^\,\:\)\s]+)/g; @@ -45,5 +45,12 @@ const scopeVariables = (cssText, packageJSON, inputFile) => { return newText.replaceAll("--sap", `--ui5-sap`); } -export default scopeVariables; +const scopeThemingVariables = (cssText) => { + return cssText.replaceAll("--sap", `--ui5-sap`); +} + +export { + scopeUi5Variables, + scopeThemingVariables, +}; From 75ecaeb0bd89c0e857716b7a7b1e6367bcfa46ec Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Mon, 20 Oct 2025 13:53:32 +0300 Subject: [PATCH 3/6] chore: fix build --- packages/base/lib/generate-styles/index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/base/lib/generate-styles/index.js b/packages/base/lib/generate-styles/index.js index 65b8d4dcff3f..6c1faec21764 100644 --- a/packages/base/lib/generate-styles/index.js +++ b/packages/base/lib/generate-styles/index.js @@ -1,17 +1,21 @@ import fs from 'fs/promises'; import path from "path"; import CleanCSS from "clean-css"; +import { processComponentPackageFile } from '@ui5/webcomponents-tools/lib/css-processors/css-processor-themes.mjs'; + const generate = async () => { + const packageJSON = JSON.parse(await fs.readFile("./package.json")) await fs.mkdir("src/generated/css/", { recursive: true }); const files = (await fs.readdir("src/css/")).filter(file => file.endsWith(".css")); const filesPromises = files.map(async file => { - let content = await fs.readFile(path.join("src/css/", file)); + const filePath = path.join("src/css/", file); + let content = await fs.readFile(filePath); const res = new CleanCSS().minify(`${content}`); // Scope used variables - content = await processComponentPackageFile(res.styles); + content = await processComponentPackageFile({ text: res.styles, path: filePath }, packageJSON); content = `export default \`${content}\`;`; From 1503235f3d66c20e4ae26abffdc645f89273de96 Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Mon, 27 Oct 2025 14:58:44 +0200 Subject: [PATCH 4/6] chore: add test pages --- packages/main/test/pages/theming/Themes.html | 20 ++++++++++++ packages/main/test/pages/theming/Themes2.html | 25 +++++++++++++++ packages/main/test/pages/theming/Themes3.html | 22 +++++++++++++ packages/main/test/pages/theming/Themes4.html | 28 +++++++++++++++++ packages/main/test/pages/theming/Themes5.html | 27 ++++++++++++++++ packages/main/test/pages/theming/Themes6.html | 31 +++++++++++++++++++ .../css-processors/css-processor-themes.mjs | 4 ++- 7 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 packages/main/test/pages/theming/Themes.html create mode 100644 packages/main/test/pages/theming/Themes2.html create mode 100644 packages/main/test/pages/theming/Themes3.html create mode 100644 packages/main/test/pages/theming/Themes4.html create mode 100644 packages/main/test/pages/theming/Themes5.html create mode 100644 packages/main/test/pages/theming/Themes6.html diff --git a/packages/main/test/pages/theming/Themes.html b/packages/main/test/pages/theming/Themes.html new file mode 100644 index 000000000000..38045c1b8609 --- /dev/null +++ b/packages/main/test/pages/theming/Themes.html @@ -0,0 +1,20 @@ + + + + + + Theming + + + + + + +

Test Page 1: Default theming - Tests the component with default theme settings without any + external styles or theme changes.

+

Expected theme sap_horizon

+ + Some button + + + \ No newline at end of file diff --git a/packages/main/test/pages/theming/Themes2.html b/packages/main/test/pages/theming/Themes2.html new file mode 100644 index 000000000000..b85ec04d28c9 --- /dev/null +++ b/packages/main/test/pages/theming/Themes2.html @@ -0,0 +1,25 @@ + + + + + + Theming + + + + + + +

Test Page 6: Theme change without external styles - Tests programmatic theme switching + behavior without any external CSS interference to verify pure theme transition functionality.

+

Expected theme sap_horizon_hcb

+ Some button + + + + + \ No newline at end of file diff --git a/packages/main/test/pages/theming/Themes3.html b/packages/main/test/pages/theming/Themes3.html new file mode 100644 index 000000000000..d7ff5ae80ced --- /dev/null +++ b/packages/main/test/pages/theming/Themes3.html @@ -0,0 +1,22 @@ + + + + + + Theming + + + + + + + +

Test Page 2: Default theming with preloaded external styles - Tests how components behave when + external CSS is loaded before component initialization.

+

Expected theme sap_belize

+ + Some button + + + \ No newline at end of file diff --git a/packages/main/test/pages/theming/Themes4.html b/packages/main/test/pages/theming/Themes4.html new file mode 100644 index 000000000000..7285b09aa7fc --- /dev/null +++ b/packages/main/test/pages/theming/Themes4.html @@ -0,0 +1,28 @@ + + + + + + Theming + + + + + + +

Test Page 3: Default theming with external styles loaded later - Tests the impact of external + CSS loaded after component initialization on styling.

+

Expected theme sap_belize

+ + Some button + + + + \ No newline at end of file diff --git a/packages/main/test/pages/theming/Themes5.html b/packages/main/test/pages/theming/Themes5.html new file mode 100644 index 000000000000..ad596f7b1424 --- /dev/null +++ b/packages/main/test/pages/theming/Themes5.html @@ -0,0 +1,27 @@ + + + + + + Theming + + + + + + + +

Test Page 4: Default theming with theme change and preloaded external styles - Tests theme + switching behavior when external CSS is already present in the DOM.

+

Expected theme sap_belize

+ + Some button + + + + \ No newline at end of file diff --git a/packages/main/test/pages/theming/Themes6.html b/packages/main/test/pages/theming/Themes6.html new file mode 100644 index 000000000000..93a61a022769 --- /dev/null +++ b/packages/main/test/pages/theming/Themes6.html @@ -0,0 +1,31 @@ + + + + + + Theming + + + + + + +

Test Page 5: Default theming with theme change and external styles loaded later - Tests theme + switching followed by external CSS injection to verify style resolution order.

+

Expected theme sap_belize

+ Some button + + + + + \ No newline at end of file diff --git a/packages/tools/lib/css-processors/css-processor-themes.mjs b/packages/tools/lib/css-processors/css-processor-themes.mjs index 181400ff07b4..d9cd91106b68 100644 --- a/packages/tools/lib/css-processors/css-processor-themes.mjs +++ b/packages/tools/lib/css-processors/css-processor-themes.mjs @@ -15,7 +15,9 @@ async function processThemingPackageFile(f) { result.root.walkRules(selector, rule => { for (const decl of rule.nodes) { - if (decl.type !== 'decl' || !decl.prop.startsWith('--sapFontUrl')) { + if (decl.type !== 'decl' ) { + continue; + } else if (decl.prop.startsWith('--sapFontUrl')) { continue; } else if (!decl.prop.startsWith('--sap')) { newRule.append(decl.clone()); From 150f5f5fb3cadd7c189fe85fd32fbe3084e3bc49 Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Tue, 28 Oct 2025 10:22:41 +0200 Subject: [PATCH 5/6] fix: docs --- packages/base/src/theming/applyTheme.ts | 30 +++++++++++-------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/base/src/theming/applyTheme.ts b/packages/base/src/theming/applyTheme.ts index cd4008b1a492..9fd384ca66ef 100644 --- a/packages/base/src/theming/applyTheme.ts +++ b/packages/base/src/theming/applyTheme.ts @@ -49,37 +49,33 @@ const loadComponentPackages = async (theme: string, externalThemeName?: string) }; const detectExternalTheme = async (theme: string) => { + if (getThemeRoot()) { + await attachCustomThemeStylesToHead(theme); + } + // If theme designer theme is detected, use this const extTheme = getThemeDesignerTheme(); if (extTheme) { return extTheme; } - - // If OpenUI5Support is enabled, try to find out if it loaded variables - const openUI5Support = getFeature("OpenUI5Support"); - if (openUI5Support && openUI5Support.isOpenUI5Detected()) { - const varsLoaded = openUI5Support.cssVariablesLoaded(); - if (varsLoaded) { - return { - themeName: openUI5Support.getConfigurationSettingsObject()?.theme, // just themeName - baseThemeName: "", // baseThemeName is only relevant for custom themes - }; - } - } else if (getThemeRoot()) { - await attachCustomThemeStylesToHead(theme); - - return getThemeDesignerTheme(); - } }; const applyTheme = async (theme: string) => { + // Detect external theme if available (e.g., from theme designer or custom theme root) const extTheme = await detectExternalTheme(theme); - // Always load component packages properties. For non-registered themes, try with the base theme, if any + // Determine which theme to use for component packages: + // 1. If the requested theme is registered, use it directly + // 2. If external theme exists, use its base theme (e.g., "my_custom_theme" extends "sap_fiori_3") + // 3. Otherwise, fallback to the default theme const packagesTheme = isThemeRegistered(theme) ? theme : extTheme && extTheme.baseThemeName; const effectiveTheme = packagesTheme || DEFAULT_THEME; + // Load base theme properties await loadThemeBase(effectiveTheme); + + // Load component-specific theme properties + // Pass external theme name only if it matches the requested theme to avoid conflicts await loadComponentPackages(effectiveTheme, extTheme && extTheme.themeName === theme ? theme : undefined); fireThemeLoaded(theme); From 8cbb172cedb5fc8e53108e1730fcd876e01face4 Mon Sep 17 00:00:00 2001 From: Nayden Naydenov Date: Tue, 28 Oct 2025 11:05:13 +0200 Subject: [PATCH 6/6] chore: lint --- packages/base/src/theming/applyTheme.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/base/src/theming/applyTheme.ts b/packages/base/src/theming/applyTheme.ts index 9fd384ca66ef..dfc0e799c4c2 100644 --- a/packages/base/src/theming/applyTheme.ts +++ b/packages/base/src/theming/applyTheme.ts @@ -2,9 +2,7 @@ import { getThemeProperties, getRegisteredPackages, isThemeRegistered } from ".. import { createOrUpdateStyle } from "../ManagedStyles.js"; import getThemeDesignerTheme from "./getThemeDesignerTheme.js"; import { fireThemeLoaded } from "./ThemeLoaded.js"; -import { getFeature } from "../FeaturesRegistry.js"; import { attachCustomThemeStylesToHead, getThemeRoot } from "../config/ThemeRoot.js"; -import type OpenUI5Support from "../features/OpenUI5Support.js"; import { DEFAULT_THEME } from "../generated/AssetParameters.js"; import { getCurrentRuntimeIndex } from "../Runtimes.js";