Skip to content
12 changes: 10 additions & 2 deletions packages/base/lib/generate-styles/index.js
Original file line number Diff line number Diff line change
@@ -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);
});

Expand Down
50 changes: 18 additions & 32 deletions packages/base/src/theming/applyTheme.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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();

Expand All @@ -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<typeof OpenUI5Support>("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);
};
Expand Down
20 changes: 20 additions & 0 deletions packages/main/test/pages/theming/Themes.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<title>Theming</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<script src="../%VITE_BUNDLE_PATH%" type="module"></script>
</head>

<body>
<p><strong>Test Page 1:</strong> Default theming - Tests the component with default theme settings without any
external styles or theme changes.</p>
<p>Expected theme <strong>sap_horizon</strong></p>

<ui5-button>Some button</ui5-button>
</body>

</html>
25 changes: 25 additions & 0 deletions packages/main/test/pages/theming/Themes2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<title>Theming</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<script src="../%VITE_BUNDLE_PATH%" type="module"></script>
</head>

<body>
<p><strong>Test Page 6:</strong> Theme change without external styles - Tests programmatic theme switching
behavior without any external CSS interference to verify pure theme transition functionality.</p>
<p>Expected theme <strong>sap_horizon_hcb</strong></p>
<ui5-button>Some button</ui5-button>

<script type="module">
setTimeout(() => {
window["sap-ui-webcomponents-bundle"].configuration.setTheme("sap_horizon_hcb");
}, 1000);
</script>
</body>

</html>
22 changes: 22 additions & 0 deletions packages/main/test/pages/theming/Themes3.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<title>Theming</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@sap-theming/theming-base-content/content/Base/baseLib/sap_belize/css_variables.css">
<script src="../%VITE_BUNDLE_PATH%" type="module"></script>
</head>

<body>
<p><strong>Test Page 2:</strong> Default theming with preloaded external styles - Tests how components behave when
external CSS is loaded before component initialization.</p>
<p>Expected theme <strong>sap_belize</strong></p>

<ui5-button>Some button</ui5-button>
</body>

</html>
28 changes: 28 additions & 0 deletions packages/main/test/pages/theming/Themes4.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<title>Theming</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<script src="../%VITE_BUNDLE_PATH%" type="module"></script>
</head>

<body>
<p><strong>Test Page 3:</strong> Default theming with external styles loaded later - Tests the impact of external
CSS loaded after component initialization on styling.</p>
<p>Expected theme <strong>sap_belize</strong></p>

<ui5-button>Some button</ui5-button>
<script>
setTimeout(() => {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "https://cdn.jsdelivr.net/npm/@sap-theming/theming-base-content/content/Base/baseLib/sap_belize/css_variables.css";
document.head.appendChild(link);
}, 3000);
</script>
</body>

</html>
27 changes: 27 additions & 0 deletions packages/main/test/pages/theming/Themes5.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<title>Theming</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@sap-theming/theming-base-content/content/Base/baseLib/sap_belize/css_variables.css">
<script src="../%VITE_BUNDLE_PATH%" type="module"></script>
</head>

<body>
<p><strong>Test Page 4:</strong> Default theming with theme change and preloaded external styles - Tests theme
switching behavior when external CSS is already present in the DOM.</p>
<p>Expected theme <strong>sap_belize</strong></p>

<ui5-button>Some button</ui5-button>
<script type="module">
setTimeout(() => {
window["sap-ui-webcomponents-bundle"].configuration.setTheme("sap_horizon_hcb");
}, 1000);
</script>
</body>

</html>
31 changes: 31 additions & 0 deletions packages/main/test/pages/theming/Themes6.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<title>Theming</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta charset="utf-8">
<script src="../%VITE_BUNDLE_PATH%" type="module"></script>
</head>

<body>
<p><strong>Test Page 5:</strong> Default theming with theme change and external styles loaded later - Tests theme
switching followed by external CSS injection to verify style resolution order.</p>
<p>Expected theme <strong>sap_belize</strong></p>
<ui5-button>Some button</ui5-button>

<script type="module">
setTimeout(() => {
window["sap-ui-webcomponents-bundle"].configuration.setTheme("sap_horizon_hcb");
setTimeout(() => {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "https://cdn.jsdelivr.net/npm/@sap-theming/theming-base-content/content/Base/baseLib/sap_belize/css_variables.css";
document.head.appendChild(link);
}, 1000);
}, 1000);
</script>
</body>

</html>
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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);
Expand Down
68 changes: 42 additions & 26 deletions packages/tools/lib/css-processors/css-processor-themes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -20,37 +55,14 @@ 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) {
build.initialOptions.write = false;

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);
Expand Down Expand Up @@ -99,4 +111,8 @@ if (import.meta.url === fileUrl) {

export default {
_ui5mainFn: generate
}

export {
processComponentPackageFile
}
Loading
Loading