@ui5/webcomponents-tools 2.21.0-rc.1 → 2.21.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/assets-meta.js +6 -4
- package/lib/css-processors/css-processor-themes.mjs +114 -10
- package/lib/css-processors/merge-light-dark.mjs +131 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [2.21.0-rc.2](https://github.com/UI5/webcomponents/compare/v2.21.0-rc.1...v2.21.0-rc.2) (2026-03-19)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **theming:** add OS-adaptive auto themes (sap_horizon_auto, sap_horizon_hc_auto) ([#13300](https://github.com/UI5/webcomponents/issues/13300)) ([f6ae2b5](https://github.com/UI5/webcomponents/commit/f6ae2b53ff86d0b4e1c420863535d8e43bae4ed4))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
# [2.21.0-rc.1](https://github.com/UI5/webcomponents/compare/v2.21.0-rc.0...v2.21.0-rc.1) (2026-03-19)
|
|
7
18
|
|
|
8
19
|
**Note:** Version bump only for package @ui5/webcomponents-tools
|
package/assets-meta.js
CHANGED
|
@@ -6,10 +6,12 @@ const assetsMeta = {
|
|
|
6
6
|
"sap_fiori_3_dark",
|
|
7
7
|
"sap_fiori_3_hcb",
|
|
8
8
|
"sap_fiori_3_hcw",
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
"sap_horizon",
|
|
10
|
+
"sap_horizon_auto", // os-based auto theme, not generated by merging light + dark
|
|
11
|
+
"sap_horizon_dark",
|
|
12
|
+
"sap_horizon_hc_auto", // os-based auto theme, merging hcb + hcw
|
|
13
|
+
"sap_horizon_hcb",
|
|
14
|
+
"sap_horizon_hcw",
|
|
13
15
|
],
|
|
14
16
|
},
|
|
15
17
|
"languages": {
|
|
@@ -8,8 +8,18 @@ import combineDuplicatedSelectors from "../postcss-combine-duplicated-selectors/
|
|
|
8
8
|
import postcssPlugin from "./postcss-plugin.mjs";
|
|
9
9
|
import { writeFileIfChanged, getFileContent } from "./shared.mjs";
|
|
10
10
|
import scopeVariables from "./scope-variables.mjs";
|
|
11
|
+
import { mergeLightDark } from "./merge-light-dark.mjs";
|
|
11
12
|
import { pathToFileURL } from "url";
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Auto theme pairs: each entry defines a light theme, its dark counterpart,
|
|
16
|
+
* and the auto theme name that will be generated by merging them.
|
|
17
|
+
*/
|
|
18
|
+
const AUTO_THEME_PAIRS = [
|
|
19
|
+
{ light: "sap_horizon", dark: "sap_horizon_dark", auto: "sap_horizon_auto" },
|
|
20
|
+
{ light: "sap_horizon_hcw", dark: "sap_horizon_hcb", auto: "sap_horizon_hc_auto" },
|
|
21
|
+
];
|
|
22
|
+
|
|
13
23
|
const generate = async (argv) => {
|
|
14
24
|
const CSS_VARIABLES_TARGET = process.env.CSS_VARIABLES_TARGET === "host";
|
|
15
25
|
const tsMode = process.env.UI5_TS === "true";
|
|
@@ -18,9 +28,14 @@ const generate = async (argv) => {
|
|
|
18
28
|
const packageJSON = JSON.parse(fs.readFileSync("./package.json"));
|
|
19
29
|
const basePackageJSON = (await import("@ui5/webcomponents-base/package.json", { with: { type: "json" } })).default;
|
|
20
30
|
|
|
21
|
-
const
|
|
31
|
+
const allInputFiles = await globby([
|
|
22
32
|
"src/**/parameters-bundle.css",
|
|
23
33
|
]);
|
|
34
|
+
|
|
35
|
+
// Filter out auto theme placeholders - they will be generated from merging light + dark
|
|
36
|
+
const autoThemeNames = AUTO_THEME_PAIRS.map(p => p.auto);
|
|
37
|
+
const inputFiles = allInputFiles.filter(f => !autoThemeNames.some(name => f.includes(`/${name}/`)));
|
|
38
|
+
|
|
24
39
|
const restArgs = argv.slice(2);
|
|
25
40
|
|
|
26
41
|
const saveFiles = async (distPath, css, suffix = "") => {
|
|
@@ -38,6 +53,8 @@ const generate = async (argv) => {
|
|
|
38
53
|
writeFileIfChanged(jsPath, jsContent);
|
|
39
54
|
}
|
|
40
55
|
|
|
56
|
+
const isThemingPackage = (filePath) => filePath.includes("packages/theming") || filePath.includes("packages\\theming");
|
|
57
|
+
|
|
41
58
|
const processThemingPackageFile = async (f) => {
|
|
42
59
|
const selector = ':root';
|
|
43
60
|
const result = await postcss().process(f.text, { from: undefined });
|
|
@@ -73,17 +90,54 @@ const generate = async (argv) => {
|
|
|
73
90
|
return { css: scopeVariables(combined.css, basePackageJSON, f.path) };
|
|
74
91
|
}
|
|
75
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Generate auto theme CSS by merging light and dark theme outputs.
|
|
95
|
+
* For the theming package (:root), adds color-scheme and toggle variables.
|
|
96
|
+
* For component packages (:host), just merges the variables.
|
|
97
|
+
*/
|
|
98
|
+
const generateAutoThemes = async (processedCSS) => {
|
|
99
|
+
for (const { light, dark, auto } of AUTO_THEME_PAIRS) {
|
|
100
|
+
for (const [lightPath, lightCSS] of processedCSS) {
|
|
101
|
+
// Match paths containing the light theme folder
|
|
102
|
+
const lightPattern = new RegExp(`[/\\\\]${light}[/\\\\]`);
|
|
103
|
+
if (!lightPattern.test(lightPath)) continue;
|
|
104
|
+
|
|
105
|
+
const darkPath = lightPath.replace(lightPattern, (match) => match.replace(light, dark));
|
|
106
|
+
const darkCSS = processedCSS.get(darkPath);
|
|
107
|
+
if (!darkCSS) continue;
|
|
108
|
+
|
|
109
|
+
const selector = isThemingPackage(lightPath) ? ":root" : ":host";
|
|
110
|
+
let autoCSS = mergeLightDark(lightCSS, darkCSS, selector);
|
|
111
|
+
|
|
112
|
+
// For the theming package, add color-scheme and toggle setup
|
|
113
|
+
if (selector === ":root") {
|
|
114
|
+
autoCSS = addAutoThemeSetup(autoCSS);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const autoPath = lightPath.replace(lightPattern, (match) => match.replace(light, auto));
|
|
118
|
+
await saveFiles(autoPath, autoCSS);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
76
123
|
let scopingPlugin = {
|
|
77
124
|
name: 'scoping',
|
|
78
125
|
setup(build) {
|
|
79
126
|
build.initialOptions.write = false;
|
|
80
127
|
|
|
81
|
-
build.onEnd(result => {
|
|
82
|
-
|
|
83
|
-
|
|
128
|
+
build.onEnd(async (result) => {
|
|
129
|
+
const processedCSS = new Map();
|
|
130
|
+
|
|
131
|
+
// Process all real theme files
|
|
132
|
+
await Promise.all(result.outputFiles.map(async f => {
|
|
133
|
+
let { css } = isThemingPackage(f.path) ? await processThemingPackageFile(f) : await processComponentPackageFile(f);
|
|
134
|
+
|
|
135
|
+
await saveFiles(f.path, css);
|
|
136
|
+
processedCSS.set(f.path, css);
|
|
137
|
+
}));
|
|
84
138
|
|
|
85
|
-
|
|
86
|
-
|
|
139
|
+
// Generate auto themes by merging light + dark
|
|
140
|
+
await generateAutoThemes(processedCSS);
|
|
87
141
|
})
|
|
88
142
|
},
|
|
89
143
|
}
|
|
@@ -110,13 +164,63 @@ const generate = async (argv) => {
|
|
|
110
164
|
}
|
|
111
165
|
}
|
|
112
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Adds auto theme setup CSS: color-scheme declaration and
|
|
169
|
+
* light/dark toggle variables with @media switching.
|
|
170
|
+
*
|
|
171
|
+
* In light mode (default):
|
|
172
|
+
* --_ui5-light-scheme is guaranteed-invalid → var() fallbacks resolve to light values
|
|
173
|
+
* --_ui5-dark-scheme is valid empty → var() fallbacks are NOT used
|
|
174
|
+
*
|
|
175
|
+
* In dark mode (@media prefers-color-scheme: dark):
|
|
176
|
+
* --_ui5-light-scheme is valid empty → var() fallbacks are NOT used
|
|
177
|
+
* --_ui5-dark-scheme is guaranteed-invalid → var() fallbacks resolve to dark values
|
|
178
|
+
*
|
|
179
|
+
* @param {string} mergedCSS - The merged :root CSS from mergeLightDark()
|
|
180
|
+
* @returns {string} CSS with toggle setup and @media rule appended
|
|
181
|
+
*/
|
|
182
|
+
const addAutoThemeSetup = (mergedCSS) => {
|
|
183
|
+
const root = postcss.parse(mergedCSS);
|
|
184
|
+
|
|
185
|
+
// Add color-scheme and toggle defaults to the :root rule
|
|
186
|
+
root.walkRules(":root", rule => {
|
|
187
|
+
rule.prepend(
|
|
188
|
+
postcss.decl({ prop: "--_ui5-dark-scheme", value: " " }),
|
|
189
|
+
);
|
|
190
|
+
rule.prepend(
|
|
191
|
+
postcss.decl({ prop: "--_ui5-light-scheme", value: "var(--_ui5-f2d95f8)" }),
|
|
192
|
+
);
|
|
193
|
+
rule.prepend(
|
|
194
|
+
postcss.decl({ prop: "color-scheme", value: "light dark" }),
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Add @media rule for dark mode - flips the toggles
|
|
199
|
+
const mediaRule = postcss.atRule({
|
|
200
|
+
name: "media",
|
|
201
|
+
params: "(prefers-color-scheme: dark)",
|
|
202
|
+
});
|
|
203
|
+
const darkRoot = postcss.rule({ selector: ":root" });
|
|
204
|
+
darkRoot.append(
|
|
205
|
+
postcss.decl({ prop: "--_ui5-light-scheme", value: " " }),
|
|
206
|
+
postcss.decl({ prop: "--_ui5-dark-scheme", value: "var(--_ui5-f2d95f8)" }),
|
|
207
|
+
postcss.decl({ prop: "color-scheme", value: "dark" }),
|
|
208
|
+
);
|
|
209
|
+
mediaRule.append(darkRoot);
|
|
210
|
+
root.append(mediaRule);
|
|
211
|
+
|
|
212
|
+
return root.toString();
|
|
213
|
+
};
|
|
214
|
+
|
|
113
215
|
const filePath = process.argv[1];
|
|
114
|
-
const fileUrl = pathToFileURL(filePath).href;
|
|
115
216
|
|
|
116
|
-
if (
|
|
117
|
-
|
|
217
|
+
if (filePath) {
|
|
218
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
219
|
+
if (import.meta.url === fileUrl) {
|
|
220
|
+
generate(process.argv)
|
|
221
|
+
}
|
|
118
222
|
}
|
|
119
223
|
|
|
120
224
|
export default {
|
|
121
225
|
_ui5mainFn: generate
|
|
122
|
-
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import postcss from "postcss";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Merges two CSS theme files (light and dark) into a single file using
|
|
5
|
+
* CSS light-dark() for color values and space toggles for non-color values.
|
|
6
|
+
*
|
|
7
|
+
* The space toggle pattern uses --_ui5-dark-scheme / --_ui5-light-scheme,
|
|
8
|
+
* following the same approach as the compact/cozy density toggles.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} lightCSS - The light theme CSS string
|
|
11
|
+
* @param {string} darkCSS - The dark theme CSS string
|
|
12
|
+
* @param {string} selector - The selector to match (":root" for theming package, ":host" for component packages)
|
|
13
|
+
* @returns {string} Merged CSS string with light-dark() and space toggle values
|
|
14
|
+
*/
|
|
15
|
+
const mergeLightDark = (lightCSS, darkCSS, selector = ":root") => {
|
|
16
|
+
const lightRoot = postcss.parse(lightCSS);
|
|
17
|
+
const darkRoot = postcss.parse(darkCSS);
|
|
18
|
+
|
|
19
|
+
// Extract all custom property declarations from each theme
|
|
20
|
+
const lightVars = extractVars(lightRoot, selector);
|
|
21
|
+
const darkVars = extractVars(darkRoot, selector);
|
|
22
|
+
|
|
23
|
+
// Build merged declarations
|
|
24
|
+
const allProps = new Set([...lightVars.keys(), ...darkVars.keys()]);
|
|
25
|
+
const mergedRule = postcss.rule({ selector });
|
|
26
|
+
|
|
27
|
+
for (const prop of allProps) {
|
|
28
|
+
const lightVal = lightVars.get(prop);
|
|
29
|
+
const darkVal = darkVars.get(prop);
|
|
30
|
+
|
|
31
|
+
if (prop.startsWith("--sapThemeMetaData")) {
|
|
32
|
+
// Keep light version only for metadata
|
|
33
|
+
if (lightVal !== undefined) {
|
|
34
|
+
mergedRule.append(postcss.decl({ prop, value: lightVal }));
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (lightVal !== undefined && darkVal !== undefined) {
|
|
40
|
+
if (lightVal === darkVal) {
|
|
41
|
+
// Identical values - output once
|
|
42
|
+
mergedRule.append(postcss.decl({ prop, value: lightVal }));
|
|
43
|
+
} else if (isColorValue(lightVal) && isColorValue(darkVal)) {
|
|
44
|
+
// Different color values - use light-dark()
|
|
45
|
+
mergedRule.append(postcss.decl({ prop, value: `light-dark(${lightVal}, ${darkVal})` }));
|
|
46
|
+
} else {
|
|
47
|
+
// Different non-color values - use space toggle
|
|
48
|
+
mergedRule.append(postcss.decl({
|
|
49
|
+
prop,
|
|
50
|
+
value: `var(--_ui5-light-scheme, ${lightVal}) var(--_ui5-dark-scheme, ${darkVal})`,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
} else if (lightVal !== undefined) {
|
|
54
|
+
// Only in light theme - use space toggle with guaranteed-invalid for dark
|
|
55
|
+
mergedRule.append(postcss.decl({
|
|
56
|
+
prop,
|
|
57
|
+
value: `var(--_ui5-light-scheme, ${lightVal}) var(--_ui5-dark-scheme, var(--_ui5-f2d95f8))`,
|
|
58
|
+
}));
|
|
59
|
+
} else {
|
|
60
|
+
// Only in dark theme - use space toggle with guaranteed-invalid for light
|
|
61
|
+
mergedRule.append(postcss.decl({
|
|
62
|
+
prop,
|
|
63
|
+
value: `var(--_ui5-dark-scheme, ${darkVal}) var(--_ui5-light-scheme, var(--_ui5-f2d95f8))`,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return mergedRule.toString();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Extract custom property declarations from a parsed CSS root for a given selector.
|
|
73
|
+
* @param {postcss.Root} root
|
|
74
|
+
* @param {string} selector
|
|
75
|
+
* @returns {Map<string, string>}
|
|
76
|
+
*/
|
|
77
|
+
const extractVars = (root, selector) => {
|
|
78
|
+
const vars = new Map();
|
|
79
|
+
root.walkRules(rule => {
|
|
80
|
+
if (rule.selector === selector) {
|
|
81
|
+
rule.walkDecls(decl => {
|
|
82
|
+
if (decl.prop.startsWith("--")) {
|
|
83
|
+
vars.set(decl.prop, decl.value);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return vars;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Heuristic to determine if a CSS value is a color value.
|
|
93
|
+
* light-dark() only works with <color> values.
|
|
94
|
+
*/
|
|
95
|
+
const isColorValue = (value) => {
|
|
96
|
+
const v = value.trim().toLowerCase();
|
|
97
|
+
|
|
98
|
+
// hex colors
|
|
99
|
+
if (/^#[0-9a-f]{3,8}$/.test(v)) return true;
|
|
100
|
+
|
|
101
|
+
// rgb/rgba/hsl/hsla functions
|
|
102
|
+
if (/^(rgba?|hsla?|oklch|oklab|lab|lch|color)\s*\(/.test(v)) return true;
|
|
103
|
+
|
|
104
|
+
// transparent and common CSS color keywords
|
|
105
|
+
if (v === "transparent" || v === "currentcolor") return true;
|
|
106
|
+
|
|
107
|
+
// Named CSS colors - check for common ones used in SAP themes
|
|
108
|
+
const namedColors = new Set([
|
|
109
|
+
"black", "white", "red", "green", "blue", "yellow", "orange", "purple",
|
|
110
|
+
"pink", "gray", "grey", "brown", "cyan", "magenta", "lime", "navy",
|
|
111
|
+
"teal", "aqua", "silver", "maroon", "olive", "fuchsia",
|
|
112
|
+
]);
|
|
113
|
+
if (namedColors.has(v)) return true;
|
|
114
|
+
|
|
115
|
+
// var() references to SAP color variables
|
|
116
|
+
// Match patterns like var(--sapXxxColor), var(--sapXxx_Background), etc.
|
|
117
|
+
if (/^var\(--sap\w*(Color|Background|BorderColor|TextColor|IconColor|ForegroundColor|HoverColor|ActiveColor|SelectedColor|Shadow)\w*\)$/.test(value.trim())) return true;
|
|
118
|
+
|
|
119
|
+
// Compound values containing color-related var() references
|
|
120
|
+
// e.g., "0.0625rem solid var(--sapContent_FocusColor)"
|
|
121
|
+
if (/var\(--sap\w*(Color|BorderColor|TextColor|IconColor|ForegroundColor|SelectedColor)\w*\)/.test(value)) {
|
|
122
|
+
// If the value also contains dimensional tokens, it's likely a shorthand (border, box-shadow)
|
|
123
|
+
// These are NOT pure color values - light-dark() won't work
|
|
124
|
+
if (/\d+(\.\d+)?(rem|px|em|%)/.test(value)) return false;
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return false;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export { mergeLightDark, isColorValue };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ui5/webcomponents-tools",
|
|
3
|
-
"version": "2.21.0-rc.
|
|
3
|
+
"version": "2.21.0-rc.2",
|
|
4
4
|
"description": "UI5 Web Components: webcomponents.tools",
|
|
5
5
|
"author": "SAP SE (https://www.sap.com)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -82,5 +82,5 @@
|
|
|
82
82
|
"esbuild": "^0.25.0",
|
|
83
83
|
"yargs": "^17.5.1"
|
|
84
84
|
},
|
|
85
|
-
"gitHead": "
|
|
85
|
+
"gitHead": "41f7af366ca45a15b31f7f74d349420dcd87fe1f"
|
|
86
86
|
}
|