diff --git a/packages/base/lib/generate-styles/index.js b/packages/base/lib/generate-styles/index.js index dfa76e645fe8..266f5681a6fe 100644 --- a/packages/base/lib/generate-styles/index.js +++ b/packages/base/lib/generate-styles/index.js @@ -1,16 +1,24 @@ 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'; import { pathToFileURL } from "url"; 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}`); - content = `export default \`${res.styles}\`;`; + + // Scope used variables + content = await processComponentPackageFile({ text: res.styles, path: filePath }, packageJSON); + + content = `export default \`${content}\`;`; + 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..dfc0e799c4c2 100644 --- a/packages/base/src/theming/applyTheme.ts +++ b/packages/base/src/theming/applyTheme.ts @@ -1,10 +1,8 @@ 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"; 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"; @@ -31,10 +29,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(); @@ -53,42 +47,34 @@ 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); - // 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 + // 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; - await loadComponentPackages(packagesTheme || DEFAULT_THEME, extTheme && extTheme.themeName === theme ? theme : undefined); + 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); }; 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-components.mjs b/packages/tools/lib/css-processors/css-processor-components.mjs index a305013079b9..35137567ca37 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"; import { pathToFileURL } from "url"; @@ -24,7 +24,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 a334e560274a..656402979911 100644 --- a/packages/tools/lib/css-processors/css-processor-themes.mjs +++ b/packages/tools/lib/css-processors/css-processor-themes.mjs @@ -6,10 +6,45 @@ 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"; import { pathToFileURL } from "url"; -const generate = async (argv) => { +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' ) { + continue; + } else if (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); + + result = scopeUi5Variables(result.css, packageJSON, f.path); + + result = scopeThemingVariables(result); + + return result; +} + +async function generate(argv) { const tsMode = process.env.UI5_TS === "true"; const extension = tsMode ? ".css.ts" : ".css.js"; @@ -20,29 +55,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(decl.clone()); - } - }); - }); - - 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) { @@ -50,7 +62,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); @@ -99,4 +111,8 @@ if (import.meta.url === fileUrl) { 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 c824e6ba9d3e..dea8968e4e70 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 @@ -36,14 +36,21 @@ const getOverrideVersion = filePath => { return overrideVersion; } -const scopeVariables = (cssText, packageJSON, inputFile) => { - const escapeVersion = version => "v" + version?.replaceAll(/[^0-9A-Za-z\-_]/g, "-"); - const versionStr = escapeVersion(getOverrideVersion(inputFile) || packageJSON.version); +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; + let newText = cssText.replaceAll(expr, `$1-${versionStr}$2`); - const expr = /(--_?ui5)([^\,\:\)\s]+)/g; + return newText.replaceAll("--sap", `--ui5-sap`); +} - return cssText.replaceAll(expr, `$1-${versionStr}$2`); +const scopeThemingVariables = (cssText) => { + return cssText.replaceAll("--sap", `--ui5-sap`); } -export default scopeVariables; +export { + scopeUi5Variables, + scopeThemingVariables, +};