@ui5/webcomponents-tools 2.27.2 → 2.28.0
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 +19 -2
- package/lib/cem/custom-elements-manifest.config.mjs +31 -9
- package/lib/cem/schema-internal.json +7 -0
- package/lib/cem/types-internal.d.ts +4 -0
- package/lib/cem/utils.mjs +26 -9
- package/lib/css-processors/css-processor-themes.mjs +8 -45
- package/lib/css-processors/merge-light-dark.mjs +131 -66
- package/package.json +2 -2
- package/lib/css-processors/postcss-plugin.mjs +0 -153
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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
|
-
|
|
6
|
+
# [2.28.0](https://github.com/UI5/webcomponents/compare/v2.28.0-rc.0...v2.28.0) (2026-10-21)
|
|
7
7
|
|
|
8
8
|
**Note:** Version bump only for package @ui5/webcomponents-tools
|
|
9
9
|
|
|
@@ -11,7 +11,24 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# [2.28.0-rc.0](https://github.com/UI5/webcomponents/compare/v2.27.1-rc.0...v2.28.0-rc.0) (2026-10-15)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **tools:** enable CEM generation for tsx files ([#14114](https://github.com/UI5/webcomponents/issues/14114)) ([cc73f73](https://github.com/UI5/webcomponents/commit/cc73f736d010fb8494e5ee56f31749e3a9baedc7)), closes [#11145](https://github.com/UI5/webcomponents/issues/11145)
|
|
20
|
+
* **tools:** use correct CSS_VARIABLES_TARGET in auto theme selector ([#14136](https://github.com/UI5/webcomponents/issues/14136)) ([06e6369](https://github.com/UI5/webcomponents/commit/06e6369d1f2fa42b97c02b64a37167d3db42f881))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* **base:** allow components to provide their own slot accessor ([#14128](https://github.com/UI5/webcomponents/issues/14128)) ([2196121](https://github.com/UI5/webcomponents/commit/2196121936bfef06989b4b04be7d07ece415ea9c))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## [2.27.1-rc.0](https://github.com/UI5/webcomponents/compare/v2.27.0...v2.27.1-rc.0) (2026-09-25)
|
|
15
32
|
|
|
16
33
|
**Note:** Version bump only for package @ui5/webcomponents-tools
|
|
17
34
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getDeprecatedStatus,
|
|
7
7
|
getExperimentalStatus,
|
|
8
8
|
getSinceStatus,
|
|
9
|
+
getSubcomponentStatus,
|
|
9
10
|
getPrivacyStatus,
|
|
10
11
|
getReference,
|
|
11
12
|
validateJSDocComment,
|
|
@@ -105,9 +106,10 @@ function processClass(ts, classNode, moduleDoc) {
|
|
|
105
106
|
currClass.kind = "class";
|
|
106
107
|
currClass.deprecated = getDeprecatedStatus(classParsedJsDoc);
|
|
107
108
|
currClass._ui5experimental = getExperimentalStatus(classParsedJsDoc);
|
|
108
|
-
currClass._ui5since = getSinceStatus(classParsedJsDoc);
|
|
109
|
+
currClass._ui5since = getSinceStatus(classParsedJsDoc, moduleDoc.path);
|
|
109
110
|
currClass._ui5privacy = getPrivacyStatus(classParsedJsDoc);
|
|
110
111
|
currClass._ui5abstract = hasTag(classParsedJsDoc, "abstract") ? true : undefined;
|
|
112
|
+
currClass._ui5subcomponents = getSubcomponentStatus(classParsedJsDoc);
|
|
111
113
|
currClass.description = normalizeDescription(classParsedJsDoc.description || findTag(classParsedJsDoc, "class")?.description);
|
|
112
114
|
currClass._ui5implements = findAllTags(classParsedJsDoc, "implements")
|
|
113
115
|
.map(tag => {
|
|
@@ -210,7 +212,7 @@ function processClass(ts, classNode, moduleDoc) {
|
|
|
210
212
|
|
|
211
213
|
const memberParsedJsDoc = parse(classNodeMemberJSdoc?.getText())[0];
|
|
212
214
|
|
|
213
|
-
member._ui5since = getSinceStatus(memberParsedJsDoc);
|
|
215
|
+
member._ui5since = getSinceStatus(memberParsedJsDoc, moduleDoc.path);
|
|
214
216
|
member.deprecated === "true" && (member.deprecated = true)
|
|
215
217
|
|
|
216
218
|
// Slots with accessors are treated like fields by the tool, so we have to convert them into slots.
|
|
@@ -248,6 +250,7 @@ function processClass(ts, classNode, moduleDoc) {
|
|
|
248
250
|
delete member.type;
|
|
249
251
|
delete member.privacy;
|
|
250
252
|
delete slot.kind;
|
|
253
|
+
delete slot.readonly;
|
|
251
254
|
|
|
252
255
|
currClass.slots.push(slot);
|
|
253
256
|
i--;
|
|
@@ -386,7 +389,7 @@ function processInterface(ts, interfaceNode, moduleDoc) {
|
|
|
386
389
|
description: normalizeDescription(interfaceParsedJsDoc?.description),
|
|
387
390
|
_ui5experimental: getExperimentalStatus(interfaceParsedJsDoc),
|
|
388
391
|
_ui5privacy: getPrivacyStatus(interfaceParsedJsDoc),
|
|
389
|
-
_ui5since: getSinceStatus(interfaceParsedJsDoc),
|
|
392
|
+
_ui5since: getSinceStatus(interfaceParsedJsDoc, moduleDoc.path),
|
|
390
393
|
deprecated: getDeprecatedStatus(interfaceParsedJsDoc),
|
|
391
394
|
});
|
|
392
395
|
}
|
|
@@ -407,7 +410,7 @@ function processEnum(ts, enumNode, moduleDoc) {
|
|
|
407
410
|
description: normalizeDescription(enumJSdoc?.comment),
|
|
408
411
|
_ui5experimental: getExperimentalStatus(enumParsedJsDoc),
|
|
409
412
|
_ui5privacy: getPrivacyStatus(enumParsedJsDoc),
|
|
410
|
-
_ui5since: getSinceStatus(enumParsedJsDoc),
|
|
413
|
+
_ui5since: getSinceStatus(enumParsedJsDoc, moduleDoc.path),
|
|
411
414
|
deprecated: getDeprecatedStatus(enumParsedJsDoc) || undefined,
|
|
412
415
|
members: (enumNode?.members || []).map(member => {
|
|
413
416
|
const memberJSdoc = member?.jsDoc?.[0];
|
|
@@ -422,7 +425,7 @@ function processEnum(ts, enumNode, moduleDoc) {
|
|
|
422
425
|
kind: "field",
|
|
423
426
|
static: true,
|
|
424
427
|
privacy: getPrivacyStatus(memberParsedJsDoc),
|
|
425
|
-
_ui5since: getSinceStatus(memberParsedJsDoc),
|
|
428
|
+
_ui5since: getSinceStatus(memberParsedJsDoc, moduleDoc.path),
|
|
426
429
|
description: memberJSdoc?.comment,
|
|
427
430
|
default: member.initializer?.text,
|
|
428
431
|
deprecated: getDeprecatedStatus(memberParsedJsDoc),
|
|
@@ -447,7 +450,7 @@ const processPublicAPI = object => {
|
|
|
447
450
|
if (((key === "privacy" && object[key] !== "public") || (key === "_ui5privacy" && object[key] !== "public")) && !object.customElement) {
|
|
448
451
|
return true;
|
|
449
452
|
} else if (typeof object[key] === "object") {
|
|
450
|
-
if (key === "cssParts" || key === "cssStates" || key === "attributes" || key === "_ui5implements") {
|
|
453
|
+
if (key === "cssParts" || key === "cssStates" || key === "attributes" || key === "_ui5implements" || key === "_ui5subcomponents") {
|
|
451
454
|
continue;
|
|
452
455
|
}
|
|
453
456
|
|
|
@@ -472,7 +475,7 @@ let typeChecker;
|
|
|
472
475
|
let typeProgram;
|
|
473
476
|
|
|
474
477
|
export default {
|
|
475
|
-
globs: ["src/!(*generated)/*.ts", "src/!(*bundle)*.ts"],
|
|
478
|
+
globs: ["src/!(*generated)/*.{ts,tsx}", "src/!(*bundle)*.{ts,tsx}"],
|
|
476
479
|
outdir: 'dist',
|
|
477
480
|
overrideModuleCreation: ({ ts, globs }) => {
|
|
478
481
|
typeProgram = ts.createProgram(globs, {
|
|
@@ -518,7 +521,7 @@ export default {
|
|
|
518
521
|
}
|
|
519
522
|
},
|
|
520
523
|
moduleLinkPhase({ moduleDoc }) {
|
|
521
|
-
moduleDoc.path = moduleDoc.path?.replace(/^src/, "dist").replace(/\.
|
|
524
|
+
moduleDoc.path = moduleDoc.path?.replace(/^src/, "dist").replace(/\.tsx?$/, ".js");
|
|
522
525
|
|
|
523
526
|
moduleDoc.exports = moduleDoc.exports.
|
|
524
527
|
filter(e => !(e.kind === "custom-element-definition" && !moduleDoc.declarations?.find(d => d.name === e.name)?.tagName))
|
|
@@ -527,7 +530,7 @@ export default {
|
|
|
527
530
|
const classNode = moduleDoc.declarations.find(c => c.name === e.declaration.name);
|
|
528
531
|
|
|
529
532
|
if (e.declaration && e.declaration.module) {
|
|
530
|
-
e.declaration.module = e.declaration.module.replace(/^src/, "dist").replace(/\.
|
|
533
|
+
e.declaration.module = e.declaration.module.replace(/^src/, "dist").replace(/\.tsx?$/, ".js");
|
|
531
534
|
}
|
|
532
535
|
|
|
533
536
|
if (classNode?.customElement && classNode.tagName && e.kind !== "custom-element-definition") {
|
|
@@ -591,6 +594,25 @@ export default {
|
|
|
591
594
|
})
|
|
592
595
|
});
|
|
593
596
|
|
|
597
|
+
const publicCustomElements = new Set(
|
|
598
|
+
customElementsManifest.modules.flatMap(m => m.declarations)
|
|
599
|
+
.filter(d => d.customElement && d.tagName && d._ui5privacy === "public")
|
|
600
|
+
.map(d => d.name)
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
customElementsManifest.modules.forEach(moduleDoc => {
|
|
604
|
+
moduleDoc.declarations.forEach(declaration => {
|
|
605
|
+
declaration._ui5subcomponents?.forEach(name => {
|
|
606
|
+
if (!publicCustomElements.has(name)) {
|
|
607
|
+
logDocumentationError(
|
|
608
|
+
moduleDoc.path?.replace(/^dist/, "src").replace(/\.js$/, ".ts"),
|
|
609
|
+
`@ui5subcomponent '${name}' is not a public custom element with a tag name in this package.`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
594
616
|
if (devMode) {
|
|
595
617
|
displayDocumentationErrors();
|
|
596
618
|
}
|
|
@@ -523,6 +523,13 @@
|
|
|
523
523
|
"_ui5abstract": {
|
|
524
524
|
"type": "boolean"
|
|
525
525
|
},
|
|
526
|
+
"_ui5subcomponents": {
|
|
527
|
+
"description": "Names of sub-components that belong to this component's documentation group",
|
|
528
|
+
"items": {
|
|
529
|
+
"type": "string"
|
|
530
|
+
},
|
|
531
|
+
"type": "array"
|
|
532
|
+
},
|
|
526
533
|
"_ui5privacy": {
|
|
527
534
|
"$ref": "#/definitions/Privacy"
|
|
528
535
|
},
|
|
@@ -224,6 +224,10 @@ export interface CustomElementDeclaration
|
|
|
224
224
|
* Marks when the field was introduced
|
|
225
225
|
*/
|
|
226
226
|
_ui5since?: string
|
|
227
|
+
/**
|
|
228
|
+
* Names of sub-components that belong to this component's documentation group
|
|
229
|
+
*/
|
|
230
|
+
_ui5subcomponents?: string[]
|
|
227
231
|
}
|
|
228
232
|
|
|
229
233
|
/**
|
package/lib/cem/utils.mjs
CHANGED
|
@@ -67,13 +67,27 @@ const getTypeRefs = (ts, node, member) => {
|
|
|
67
67
|
return typeRefs?.length ? typeRefs : undefined;
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
-
const
|
|
70
|
+
const sinceVersionRegExp = /^\d+\.\d+\.\d+$/;
|
|
71
|
+
|
|
72
|
+
const getSinceStatus = (jsdocComment, modulePath) => {
|
|
71
73
|
const sinceTag = findTag(jsdocComment, "since");
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
:
|
|
74
|
+
if (!sinceTag) return undefined;
|
|
75
|
+
|
|
76
|
+
const value = sinceTag.description
|
|
77
|
+
? `${sinceTag.name} ${sinceTag.description}`
|
|
78
|
+
: sinceTag.name;
|
|
79
|
+
|
|
80
|
+
if (modulePath && !sinceVersionRegExp.test(value)) {
|
|
81
|
+
logDocumentationError(modulePath, `@since value "${value}" must follow the pattern number.number.number (e.g. 2.0.0)`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return value;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getSubcomponentStatus = (jsdocComment) => {
|
|
88
|
+
const tags = findAllTags(jsdocComment, "ui5subcomponent");
|
|
89
|
+
if (!tags.length) return undefined;
|
|
90
|
+
return tags.map(tag => tag.name).filter(Boolean);
|
|
77
91
|
};
|
|
78
92
|
|
|
79
93
|
const getPrivacyStatus = (jsdocComment) => {
|
|
@@ -148,7 +162,7 @@ const findImportPath = (ts, sourceFile, typeName, modulePath) => {
|
|
|
148
162
|
|
|
149
163
|
if (isLocalDeclared) {
|
|
150
164
|
return (
|
|
151
|
-
modulePath?.replace("src", "dist")?.replace(
|
|
165
|
+
modulePath?.replace("src", "dist")?.replace(/\.tsx?$/, ".js") || undefined
|
|
152
166
|
);
|
|
153
167
|
} else {
|
|
154
168
|
const importStatements = sourceFile.statements?.filter(
|
|
@@ -167,7 +181,7 @@ const findImportPath = (ts, sourceFile, typeName, modulePath) => {
|
|
|
167
181
|
if (currentModuleSpecifier?.text?.startsWith(".")) {
|
|
168
182
|
return (
|
|
169
183
|
path.join(path.dirname(modulePath), currentModuleSpecifier.text)
|
|
170
|
-
?.replace("src", "dist")?.replace(
|
|
184
|
+
?.replace("src", "dist")?.replace(/\.tsx?$/, ".js") || undefined
|
|
171
185
|
);
|
|
172
186
|
} else {
|
|
173
187
|
let packageName = currentModuleSpecifier?.text?.replace(packageRegex, "") || undefined;
|
|
@@ -240,7 +254,7 @@ const allowedTags = {
|
|
|
240
254
|
event: [...commonTags, "param", "native", "allowPreventDefault"],
|
|
241
255
|
eventParam: [...commonTags],
|
|
242
256
|
method: [...commonTags, "param", "returns", "override"],
|
|
243
|
-
class: [...commonTags, "constructor", "class", "abstract", "experimental", "implements", "extends", "slot", "csspart", "cssstate", "cssState"],
|
|
257
|
+
class: [...commonTags, "constructor", "class", "abstract", "experimental", "implements", "extends", "slot", "csspart", "cssstate", "cssState", "ui5subcomponent"],
|
|
244
258
|
enum: [...commonTags, "experimental",],
|
|
245
259
|
enumMember: [...commonTags, "experimental",],
|
|
246
260
|
interface: [...commonTags, "experimental",],
|
|
@@ -335,6 +349,8 @@ const validateJSDocTag = (tag) => {
|
|
|
335
349
|
return !tag.type && tag.name && tag.description;
|
|
336
350
|
case "since":
|
|
337
351
|
return !tag.type && tag.name;
|
|
352
|
+
case "ui5subcomponent":
|
|
353
|
+
return !tag.type && tag.name && !tag.description;
|
|
338
354
|
case "returns":
|
|
339
355
|
return !tag.type && tag.name;
|
|
340
356
|
case "default":
|
|
@@ -411,6 +427,7 @@ const formatSlotTypes = (typeText) => {
|
|
|
411
427
|
export {
|
|
412
428
|
getPrivacyStatus,
|
|
413
429
|
getSinceStatus,
|
|
430
|
+
getSubcomponentStatus,
|
|
414
431
|
getDeprecatedStatus,
|
|
415
432
|
getExperimentalStatus,
|
|
416
433
|
getType,
|
|
@@ -5,7 +5,6 @@ import * as path from "path";
|
|
|
5
5
|
import { writeFile, mkdir } from "fs/promises";
|
|
6
6
|
import postcss from "postcss";
|
|
7
7
|
import combineDuplicatedSelectors from "../postcss-combine-duplicated-selectors/index.js"
|
|
8
|
-
import postcssPlugin from "./postcss-plugin.mjs";
|
|
9
8
|
import { writeFileIfChanged, getFileContent } from "./shared.mjs";
|
|
10
9
|
import scopeVariables from "./scope-variables.mjs";
|
|
11
10
|
import { mergeLightDark } from "./merge-light-dark.mjs";
|
|
@@ -75,8 +74,7 @@ const generate = async (argv) => {
|
|
|
75
74
|
const processComponentPackageFile = async (f) => {
|
|
76
75
|
if (CSS_VARIABLES_TARGET) {
|
|
77
76
|
const result = await postcss([
|
|
78
|
-
combineDuplicatedSelectors
|
|
79
|
-
postcssPlugin
|
|
77
|
+
combineDuplicatedSelectors
|
|
80
78
|
]).process(f.text, { from: undefined });
|
|
81
79
|
|
|
82
80
|
return { css: result.css };
|
|
@@ -92,8 +90,8 @@ const generate = async (argv) => {
|
|
|
92
90
|
|
|
93
91
|
/**
|
|
94
92
|
* Generate auto theme CSS by merging light and dark theme outputs.
|
|
95
|
-
* For the theming package
|
|
96
|
-
* For component packages
|
|
93
|
+
* For the theming package adds color-scheme.
|
|
94
|
+
* For component packages merges the variables.
|
|
97
95
|
*/
|
|
98
96
|
const generateAutoThemes = async (processedCSS) => {
|
|
99
97
|
for (const { light, dark, auto } of AUTO_THEME_PAIRS) {
|
|
@@ -104,13 +102,12 @@ const generate = async (argv) => {
|
|
|
104
102
|
|
|
105
103
|
const darkPath = lightPath.replace(lightPattern, (match) => match.replace(light, dark));
|
|
106
104
|
const darkCSS = processedCSS.get(darkPath);
|
|
107
|
-
if (
|
|
105
|
+
if (darkCSS === undefined) continue;
|
|
108
106
|
|
|
109
|
-
|
|
110
|
-
let autoCSS = mergeLightDark(lightCSS, darkCSS, selector);
|
|
107
|
+
let autoCSS = mergeLightDark(lightCSS, darkCSS);
|
|
111
108
|
|
|
112
109
|
// For the theming package, add color-scheme and toggle setup
|
|
113
|
-
if (
|
|
110
|
+
if (isThemingPackage(lightPath)) {
|
|
114
111
|
autoCSS = addAutoThemeSetup(autoCSS);
|
|
115
112
|
}
|
|
116
113
|
|
|
@@ -163,51 +160,17 @@ const generate = async (argv) => {
|
|
|
163
160
|
await esbuild.build(config);
|
|
164
161
|
}
|
|
165
162
|
}
|
|
166
|
-
|
|
167
163
|
/**
|
|
168
|
-
* Adds auto theme setup CSS: color-scheme declaration
|
|
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
|
-
*
|
|
164
|
+
* Adds auto theme setup CSS: color-scheme declaration.
|
|
179
165
|
* @param {string} mergedCSS - The merged :root CSS from mergeLightDark()
|
|
180
166
|
* @returns {string} CSS with toggle setup and @media rule appended
|
|
181
167
|
*/
|
|
182
168
|
const addAutoThemeSetup = (mergedCSS) => {
|
|
183
169
|
const root = postcss.parse(mergedCSS);
|
|
184
170
|
|
|
185
|
-
// Add color-scheme and toggle defaults to the :root rule
|
|
186
171
|
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)",
|
|
172
|
+
rule.prepend(postcss.decl({ prop: "color-scheme", value: "light dark" }));
|
|
202
173
|
});
|
|
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
174
|
|
|
212
175
|
return root.toString();
|
|
213
176
|
};
|
|
@@ -1,91 +1,156 @@
|
|
|
1
1
|
import postcss from "postcss";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Merges two CSS theme files (light and dark) into a single file
|
|
5
|
-
* CSS light-dark() for color values and space toggles for non-color values.
|
|
4
|
+
* Merges two CSS theme files (light and dark) into a single auto-theme file.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Walks the entire CSS tree of both files without targeting a specific selector.
|
|
7
|
+
* Every rule found (under any at-rule context) is merged by (atRuleContext, selector):
|
|
8
|
+
* - identical values → base selector block
|
|
9
|
+
* - differing colors → light-dark() in base selector block
|
|
10
|
+
* - differing other → split into @media (prefers-color-scheme: light/dark) blocks
|
|
9
11
|
*
|
|
10
|
-
* @param {string} lightCSS
|
|
11
|
-
* @param {string} darkCSS
|
|
12
|
-
* @
|
|
13
|
-
* @returns {string} Merged CSS string with light-dark() and space toggle values
|
|
12
|
+
* @param {string} lightCSS
|
|
13
|
+
* @param {string} darkCSS
|
|
14
|
+
* @returns {string} Merged CSS string
|
|
14
15
|
*/
|
|
15
|
-
const mergeLightDark = (lightCSS, darkCSS
|
|
16
|
-
const lightRoot = postcss.parse(lightCSS);
|
|
17
|
-
const darkRoot = postcss.parse(darkCSS);
|
|
18
|
-
|
|
16
|
+
const mergeLightDark = (lightCSS, darkCSS) => {
|
|
19
17
|
// Extract all custom property declarations from each theme
|
|
20
|
-
const
|
|
21
|
-
const
|
|
18
|
+
const lightGroups = extractVarGroups(postcss.parse(lightCSS));
|
|
19
|
+
const darkGroups = extractVarGroups(postcss.parse(darkCSS));
|
|
20
|
+
|
|
21
|
+
const allContexts = new Set([...lightGroups.keys(), ...darkGroups.keys()]);
|
|
22
|
+
const root = postcss.root();
|
|
23
|
+
|
|
24
|
+
for (const context of allContexts) {
|
|
25
|
+
const lightSelectors = lightGroups.get(context) ?? new Map();
|
|
26
|
+
const darkSelectors = darkGroups.get(context) ?? new Map();
|
|
27
|
+
|
|
28
|
+
const allSelectors = new Set([...lightSelectors.keys(), ...darkSelectors.keys()]);
|
|
29
|
+
|
|
30
|
+
const sharedNodes = [];
|
|
31
|
+
const lightNodes = [];
|
|
32
|
+
const darkNodes = [];
|
|
33
|
+
|
|
34
|
+
for (const selector of allSelectors) {
|
|
35
|
+
const lightVars = lightSelectors.get(selector) ?? new Map();
|
|
36
|
+
const darkVars = darkSelectors.get(selector) ?? new Map();
|
|
22
37
|
|
|
23
|
-
|
|
24
|
-
const allProps = new Set([...lightVars.keys(), ...darkVars.keys()]);
|
|
25
|
-
const mergedRule = postcss.rule({ selector });
|
|
38
|
+
const allProps = new Set([...lightVars.keys(), ...darkVars.keys()]);
|
|
26
39
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
40
|
+
const sharedRule = postcss.rule({ selector });
|
|
41
|
+
const lightRule = postcss.rule({ selector });
|
|
42
|
+
const darkRule = postcss.rule({ selector });
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
for (const prop of allProps) {
|
|
45
|
+
const lightVal = lightVars.get(prop);
|
|
46
|
+
const darkVal = darkVars.get(prop);
|
|
47
|
+
|
|
48
|
+
if (prop.startsWith("--sapThemeMetaData")) {
|
|
49
|
+
if (lightVal !== undefined) {
|
|
50
|
+
sharedRule.append(postcss.decl({ prop, value: lightVal }));
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (lightVal !== undefined && darkVal !== undefined) {
|
|
56
|
+
if (lightVal === darkVal) {
|
|
57
|
+
// Identical values - output once
|
|
58
|
+
sharedRule.append(postcss.decl({ prop, value: lightVal }));
|
|
59
|
+
} else if (isColorValue(lightVal) && isColorValue(darkVal)) {
|
|
60
|
+
// Different color values - use light-dark()
|
|
61
|
+
sharedRule.append(postcss.decl({ prop, value: `light-dark(${lightVal}, ${darkVal})` }));
|
|
62
|
+
} else {
|
|
63
|
+
// Different non-color values - use space toggle
|
|
64
|
+
lightRule.append(postcss.decl({ prop, value: lightVal }));
|
|
65
|
+
darkRule.append(postcss.decl({ prop, value: darkVal }));
|
|
66
|
+
}
|
|
67
|
+
} else if (lightVal !== undefined) {
|
|
68
|
+
// Only in light theme - use space toggle with guaranteed-invalid for dark
|
|
69
|
+
lightRule.append(postcss.decl({ prop, value: lightVal }));
|
|
70
|
+
} else {
|
|
71
|
+
// Only in dark theme - use space toggle with guaranteed-invalid for light
|
|
72
|
+
darkRule.append(postcss.decl({ prop, value: darkVal }));
|
|
73
|
+
}
|
|
35
74
|
}
|
|
36
|
-
|
|
75
|
+
|
|
76
|
+
if (sharedRule.nodes.length > 0) sharedNodes.push(sharedRule);
|
|
77
|
+
if (lightRule.nodes.length > 0) lightNodes.push(lightRule);
|
|
78
|
+
if (darkRule.nodes.length > 0) darkNodes.push(darkRule);
|
|
37
79
|
}
|
|
38
80
|
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
81
|
+
const appendGroup = (nodes, mediaParams) => {
|
|
82
|
+
if (nodes.length === 0) return;
|
|
83
|
+
const wrapped = context ? wrapInAtRule(context, nodes) : nodes;
|
|
84
|
+
if (mediaParams) {
|
|
85
|
+
const media = postcss.atRule({ name: "media", params: mediaParams });
|
|
86
|
+
if (Array.isArray(wrapped)) {
|
|
87
|
+
wrapped.forEach(n => media.append(n));
|
|
88
|
+
} else {
|
|
89
|
+
media.append(wrapped);
|
|
90
|
+
}
|
|
91
|
+
root.append(media);
|
|
46
92
|
} else {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
93
|
+
if (Array.isArray(wrapped)) {
|
|
94
|
+
wrapped.forEach(n => root.append(n));
|
|
95
|
+
} else {
|
|
96
|
+
root.append(wrapped);
|
|
97
|
+
}
|
|
52
98
|
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
appendGroup(sharedNodes, null);
|
|
102
|
+
appendGroup(lightNodes, "(prefers-color-scheme: light)");
|
|
103
|
+
appendGroup(darkNodes, "(prefers-color-scheme: dark)");
|
|
66
104
|
}
|
|
67
105
|
|
|
68
|
-
return
|
|
106
|
+
return root.toString();
|
|
69
107
|
};
|
|
70
108
|
|
|
71
109
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* @param {string} selector
|
|
75
|
-
* @returns {Map<string, string>}
|
|
110
|
+
* Wraps an array of rules inside a clone of an at-rule identified by its signature.
|
|
111
|
+
* The signature is "{name} {params}", e.g. "container style(--foo: bar)".
|
|
76
112
|
*/
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
113
|
+
const wrapInAtRule = (signature, rules) => {
|
|
114
|
+
const spaceIdx = signature.indexOf(" ");
|
|
115
|
+
const name = signature.slice(0, spaceIdx);
|
|
116
|
+
const params = signature.slice(spaceIdx + 1);
|
|
117
|
+
const atRule = postcss.atRule({ name, params });
|
|
118
|
+
rules.forEach(r => atRule.append(r));
|
|
119
|
+
return atRule;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Extract custom property declarations grouped by (atRuleContext, selector).
|
|
124
|
+
*
|
|
125
|
+
* Returns: Map<atRuleContext, Map<selector, Map<prop, value>>>
|
|
126
|
+
* atRuleContext — "" for top-level rules, "{name} {params}" for rules inside an at-rule
|
|
127
|
+
*/
|
|
128
|
+
const extractVarGroups = (root) => {
|
|
129
|
+
const groups = new Map();
|
|
130
|
+
|
|
131
|
+
const addVar = (context, selector, prop, value) => {
|
|
132
|
+
if (!groups.has(context)) groups.set(context, new Map());
|
|
133
|
+
const bySelector = groups.get(context);
|
|
134
|
+
if (!bySelector.has(selector)) bySelector.set(selector, new Map());
|
|
135
|
+
bySelector.get(selector).set(prop, value);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
root.walk(node => {
|
|
139
|
+
if (node.type !== "rule") return;
|
|
140
|
+
|
|
141
|
+
const parent = node.parent;
|
|
142
|
+
const context = (parent && parent.type === "atrule")
|
|
143
|
+
? `${parent.name} ${parent.params}`
|
|
144
|
+
: "";
|
|
145
|
+
|
|
146
|
+
node.walkDecls(decl => {
|
|
147
|
+
if (decl.prop.startsWith("--")) {
|
|
148
|
+
addVar(context, node.selector, decl.prop, decl.value);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
87
151
|
});
|
|
88
|
-
|
|
152
|
+
|
|
153
|
+
return groups;
|
|
89
154
|
};
|
|
90
155
|
|
|
91
156
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ui5/webcomponents-tools",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.28.0",
|
|
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": "8fdaadeb2c7cbf0eb3a42e6dbdbdca1f27bb6797"
|
|
86
86
|
}
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import postcss from "postcss";
|
|
3
|
-
|
|
4
|
-
const hostVariables = new Map();
|
|
5
|
-
|
|
6
|
-
const SELECTOR = ":host";
|
|
7
|
-
|
|
8
|
-
const saveVariable = (variable, value, density) => {
|
|
9
|
-
if (!hostVariables.has(variable)) {
|
|
10
|
-
hostVariables.set(variable, {});
|
|
11
|
-
}
|
|
12
|
-
hostVariables.get(variable)[density] = value;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* PostCSS plugin for handling CSS variables across density modes (cozy and compact).
|
|
17
|
-
*
|
|
18
|
-
* The plugin scans CSS for `:host` rules that define density-specific custom properties.
|
|
19
|
-
* Variables declared in a root-level `:host` rule are treated as **cozy** values, while
|
|
20
|
-
* variables declared inside `@container style(--ui5_content_density: compact)` `:host` rules
|
|
21
|
-
* are treated as **compact** values.
|
|
22
|
-
*
|
|
23
|
-
* All discovered variables are merged into a single root `:host` rule. For variables that
|
|
24
|
-
* exist in both modes, the plugin generates a value that uses CSS variable fallbacks to
|
|
25
|
-
* dynamically switch between compact and cozy values based on the active density.
|
|
26
|
-
*
|
|
27
|
-
* Variables that exist in only one mode are preserved, with appropriate fallbacks added
|
|
28
|
-
* when needed.
|
|
29
|
-
*
|
|
30
|
-
* Example input:
|
|
31
|
-
*
|
|
32
|
-
* ```css
|
|
33
|
-
* :host {
|
|
34
|
-
* --my-variable: cozy-value;
|
|
35
|
-
* --cozy-only-variable: cozy-only-value;
|
|
36
|
-
* }
|
|
37
|
-
*
|
|
38
|
-
* @container style(--ui5_content_density: compact) {
|
|
39
|
-
* :host {
|
|
40
|
-
* --my-variable: compact-value;
|
|
41
|
-
* --compact-only-variable: compact-only;
|
|
42
|
-
* }
|
|
43
|
-
* }
|
|
44
|
-
* ```
|
|
45
|
-
*
|
|
46
|
-
* Output:
|
|
47
|
-
*
|
|
48
|
-
* ```css
|
|
49
|
-
* :host {
|
|
50
|
-
* --my-variable: var(--_ui5-compact-size, compact-value) var(--_ui5-cozy-size, cozy-value);
|
|
51
|
-
* --compact-only-variable: var(--_ui5-compact-size, compact-only) var(--_ui5-cozy-size, initial);
|
|
52
|
-
* --cozy-only-variable: cozy-only-value;
|
|
53
|
-
* }
|
|
54
|
-
* ```
|
|
55
|
-
*
|
|
56
|
-
* This enables seamless runtime switching between density modes using CSS variables alone.
|
|
57
|
-
*/
|
|
58
|
-
|
|
59
|
-
export default function postcssPlugin() {
|
|
60
|
-
return {
|
|
61
|
-
postcssPlugin: 'postcss-content-density-variables',
|
|
62
|
-
Once(root) {
|
|
63
|
-
let hasRootHost = false;
|
|
64
|
-
let hostRule;
|
|
65
|
-
|
|
66
|
-
root.walkRules((rule) => {
|
|
67
|
-
// Only process :host rules
|
|
68
|
-
if (rule.selector !== SELECTOR) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Handle root-level :host rules (cozy)
|
|
73
|
-
if (rule.parent.type === "root") {
|
|
74
|
-
if (!hasRootHost) {
|
|
75
|
-
hasRootHost = true;
|
|
76
|
-
hostRule = rule;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
rule.walkDecls((decl) => {
|
|
80
|
-
if (decl.prop.startsWith('--')) {
|
|
81
|
-
saveVariable(decl.prop, decl.value, 'cozy');
|
|
82
|
-
decl.remove();
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Handle :host rules inside @container (compact)
|
|
88
|
-
if (rule.parent.type === "atrule") {
|
|
89
|
-
if (rule.parent.params.replaceAll("\s", "") === 'style(--ui5_content_density: compact)'.replaceAll("\s", "")) {
|
|
90
|
-
rule.walkDecls((decl) => {
|
|
91
|
-
if (decl.prop.startsWith('--')) {
|
|
92
|
-
saveVariable(decl.prop, decl.value, 'compact');
|
|
93
|
-
decl.remove();
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
let current = rule;
|
|
100
|
-
// Remove up empty rules
|
|
101
|
-
while (current) {
|
|
102
|
-
if (current.nodes.length === 0) {
|
|
103
|
-
const parent = current.parent;
|
|
104
|
-
current.remove();
|
|
105
|
-
current = parent;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (current.type === "root") {
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
if (!hasRootHost) {
|
|
115
|
-
hostRule = postcss.rule({ selector: SELECTOR });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Construct merged variable declarations depending on available modes
|
|
119
|
-
for (const [variable, variableData] of hostVariables) {
|
|
120
|
-
if (variableData.cozy && variableData.compact) {
|
|
121
|
-
hostRule.append({
|
|
122
|
-
prop: variable,
|
|
123
|
-
value: `var(--_ui5-compact-size, ${variableData.compact}) var(--_ui5-cozy-size, ${variableData.cozy})`,
|
|
124
|
-
});
|
|
125
|
-
} else if (variableData.compact) {
|
|
126
|
-
|
|
127
|
-
// Use a non-existent variable to always trigger the fallback.
|
|
128
|
-
// Because this variable is never defined, the fallback is used
|
|
129
|
-
// in all cases.
|
|
130
|
-
|
|
131
|
-
// Using `initial` as a fallback can work only for properties
|
|
132
|
-
// that treat it as an invalid value. To avoid this inconsistency,
|
|
133
|
-
// we intentionally reference an undefined variable, which is
|
|
134
|
-
// always considered invalid and undefined.
|
|
135
|
-
hostRule.append({
|
|
136
|
-
prop: variable,
|
|
137
|
-
value: `var(--_ui5-compact-size, ${variableData.compact}) var(--_ui5-cozy-size, var(--_ui5-f2d95f8))`,
|
|
138
|
-
});
|
|
139
|
-
} else {
|
|
140
|
-
hostRule.append({
|
|
141
|
-
prop: variable,
|
|
142
|
-
value: variableData.cozy,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
root.prepend(hostRule);
|
|
148
|
-
hostVariables.clear();
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
postcssPlugin.postcss = true;
|