Skip to content

Commit 0333f74

Browse files
authored
chore: enable build for derived css variables (#308)
Add postcss plugin to calculate all derived css variables Details (applies for each theme): * global-parameters-new.css (would be renamed later) is the new version of global-parameters.css * base-parameters-new.css (would be renamed later) is the new version of base-parameters.css * global-derived-colors.js - include all base and global derived css variables * component-derived-colors.js - include all component derived css variables
1 parent 3978327 commit 0333f74

21 files changed

+2056
-16
lines changed

packages/main/.eslintignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ bundle.esm.js
88
bundle.es5.js
99
rollup.config*.js
1010
wdio.conf.js
11-
postcss.config.js
11+
postcss.config.js
12+
global-derived-colors.js
13+
component-derived-colors.js
Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
const postcssImport = require('postcss-import');
2-
const postcssAddFallback = require('../../lib/postcss-add-fallback/index.js');
32
const combineSelectors = require('postcss-combine-duplicated-selectors');
43
const postcssCSStoESM = require('../../lib/postcss-css-to-esm/index.js');
4+
const postcssDerivedColors = require('../../lib/postcss-process-derived-colors/index');
55
const cssnano = require('cssnano');
6+
const postcssAddFallback = require('../../lib/postcss-add-fallback/index.js');
67

78
module.exports = {
8-
plugins: [
9-
postcssImport(),
10-
postcssAddFallback(),
11-
combineSelectors({removeDuplicatedProperties: true}),
12-
cssnano(),
13-
postcssCSStoESM(),
14-
]
9+
plugins: [
10+
postcssImport(),
11+
combineSelectors({
12+
removeDuplicatedProperties: true
13+
}),
14+
postcssDerivedColors(),
15+
postcssAddFallback(),
16+
cssnano({preset: [
17+
'default', {
18+
mergeLonghand: false, // https://github.com/cssnano/cssnano/issues/675
19+
},
20+
]}, ),
21+
postcssCSStoESM(),
22+
]
1523
};

packages/main/config/postcss.components/postcss.config.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ module.exports = {
77
plugins: [
88
postcssNesting(),
99
postcssAddFallback({importFrom: "./dist/css/themes-next/sap_fiori_3/parameters-bundle.css"}),
10-
cssnano(),
10+
cssnano({preset: [
11+
'default', {
12+
mergeLonghand: false, // https://github.com/cssnano/cssnano/issues/675
13+
},
14+
]}, ),
1115
postcssCSStoESM(),
1216
]
1317
};
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
const postcss = require("postcss");
2+
const lessFunctionsFactory = require("./less-functions");
3+
4+
const isCalcUsed = value => {
5+
return value.includes("calc(");
6+
};
7+
8+
const isStatic = value => {
9+
return !value.includes("var(--");
10+
};
11+
12+
const getNormalizedColorValue = colorValue => {
13+
return colorValue.toCSS ? colorValue.toCSS() : colorValue;
14+
}
15+
16+
const getColorValue = async (col) => {
17+
let colorValue = col instanceof Promise ? await col : await getPromiseFor(col);
18+
colorValue = getNormalizedColorValue(colorValue);
19+
20+
return colorValue;
21+
}
22+
23+
const lessFunctions = lessFunctionsFactory(getColorValue);
24+
25+
const varPromises = new Map();
26+
const unresolvedNames = new Set();
27+
const outputVars = new Map();
28+
const originalRefs = new Map();
29+
30+
const extractName = varRef => {
31+
const result = varRef.match(/var\((--\w+),?.*\)/);
32+
return result[1];
33+
};
34+
35+
const getPromiseFor = varName => {
36+
if (varPromises.has(varName)) {
37+
return varPromises.get(varName);
38+
}
39+
40+
let promiseResolveFn;
41+
42+
const refPromise = new Promise((resolve, reject) => {
43+
promiseResolveFn = resolve;
44+
});
45+
46+
refPromise.resolveFn = promiseResolveFn;
47+
varPromises.set(varName, refPromise);
48+
49+
unresolvedNames.add(varName);
50+
51+
return refPromise;
52+
}
53+
54+
const resolvePromiseFor = (varName, varValue) => {
55+
if (varPromises.has(varName)) {
56+
varPromises.get(varName).resolveFn(varValue);
57+
delete varPromises.get(varName).resolveFn;
58+
} else {
59+
varPromises.set(varName, Promise.resolve(varValue));
60+
}
61+
62+
outputVars.set(varName, varValue);
63+
unresolvedNames.delete(varName);
64+
}
65+
66+
const findCSSVars = styleString => {
67+
const couples = styleString.match(/--[^:)]+:\s*[^;}]+/g) || [];
68+
69+
couples.forEach(couple => {
70+
const [varName, varValue] = couple.split(/:\s*/);
71+
72+
if (isStatic(varValue) || isCalcUsed(varValue)) {
73+
resolvePromiseFor(varName, varValue);
74+
} else { // Case: variable, that reference another variable, e.g. --a: var(--b);
75+
originalRefs.set(varName, varValue);
76+
77+
let refName = extractName(varValue); // extract the variable that we depend on, e.g. --b
78+
let refPromise = getPromiseFor(refName);
79+
80+
refPromise.then((refValue) => {
81+
resolvePromiseFor(varName, refValue);
82+
});
83+
}
84+
});
85+
};
86+
87+
const processDerivations = async (derivations) => {
88+
const transformations = Object.keys(derivations).map(async newParam => {
89+
const transform = derivations[newParam];
90+
const derivedColor = await transform();
91+
const resultValue = getNormalizedColorValue(derivedColor);
92+
93+
resolvePromiseFor(newParam, resultValue);
94+
});
95+
96+
const timeoutPromise = new Promise(resolve => {
97+
setTimeout(resolve, 500);
98+
});
99+
100+
await Promise.race([
101+
Promise.all(transformations),
102+
timeoutPromise
103+
]);
104+
}
105+
106+
const restoreRefs = () => {
107+
Array.from(originalRefs.entries()).forEach(([key, value]) => {
108+
outputVars.set(key, value);
109+
})
110+
};
111+
112+
const clearMaps = () => {
113+
varPromises.clear();
114+
unresolvedNames.clear();
115+
outputVars.clear();
116+
originalRefs.clear();
117+
}
118+
119+
const pluginQueue = [];
120+
121+
module.exports = postcss.plugin('process derived colors', function (opts) {
122+
opts = opts || {};
123+
124+
return async function (root) {
125+
const theme = root.source.input.from.match(/themes-next\/(\w+)\//)[1];
126+
127+
const prevPlugins = [...pluginQueue];
128+
129+
let pluginFinishedResolve;
130+
pluginFinished = new Promise(resolve => {
131+
pluginFinishedResolve = resolve;
132+
});
133+
pluginQueue.push(pluginFinished);
134+
135+
await Promise.all(prevPlugins);
136+
console.log('plugin start:', theme);
137+
138+
clearMaps();
139+
const result = [];
140+
141+
// Step 1: Read entry params files
142+
let allParameters = root.toString();
143+
144+
// Step2: Collect derivation functions
145+
const derivationFactories = require(`../../src/themes-next/${theme}/derived-colors`);
146+
let derivations = {};
147+
derivationFactories.forEach(factory => {
148+
Object.assign(derivations, factory(lessFunctions));
149+
});
150+
151+
// Step 3: Find all vars and which are unresolved (has not calculated value)
152+
findCSSVars(allParameters);
153+
154+
// Step 4: Process derivations
155+
await processDerivations(derivations);
156+
console.log({unresolvedNames});
157+
158+
// Step 5: Restore refs
159+
restoreRefs();
160+
161+
// Step 6: Output the result
162+
Array.from(outputVars.entries()).forEach(([key, value]) => {
163+
result.push(`${key}: ${value};`)
164+
});
165+
166+
const newRoot = postcss.parse(`:root { ${result.join("\n") }}`);
167+
168+
root.removeAll();
169+
root.append(...newRoot.nodes);
170+
console.log('plugin end:', theme);
171+
172+
pluginFinishedResolve();
173+
return pluginFinished;
174+
}
175+
});
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
const registry = require("less/lib/less/functions/function-registry");
2+
require("less/lib/less/functions/color");
3+
const Color = require("less/lib/less/tree/color");
4+
const Dimension = require("less/lib/less/tree/dimension");
5+
const lessDarken = registry.get("darken");
6+
const lessLighten = registry.get("lighten");
7+
const lessContrast = registry.get("contrast");
8+
const lessFade = registry.get("fade");
9+
const lessSaturate = registry.get("saturate");
10+
const lessDesaturate = registry.get("desaturate");
11+
const lessMix = registry.get("mix");
12+
const lessSpin = registry.get("spin");
13+
14+
const factory = (getColorValue) => {
15+
const getColorInstance = colorValue => {
16+
return new Color(colorValue.replace("#", ""));
17+
};
18+
19+
const darken = async (col, value) => {
20+
const colorValue = await getColorValue(col);
21+
return lessDarken(getColorInstance(colorValue), { value });
22+
}
23+
24+
const lighten = async (col, value) => {
25+
const colorValue = await getColorValue(col);
26+
return lessLighten(getColorInstance(colorValue), { value });
27+
}
28+
29+
const contrast = async (color, dark, light, threshold) => {
30+
const colorValue = await getColorValue(color);
31+
const darkValue = await getColorValue(dark);
32+
const lightValue = await getColorValue(light);
33+
const col1 = getColorInstance(colorValue);
34+
const col2 = getColorInstance(darkValue);
35+
const col3 = getColorInstance(lightValue);
36+
37+
let thresholdValue;
38+
39+
if (threshold) {
40+
thresholdValue = await getColorValue(threshold);
41+
thresholdValue = new Dimension(thresholdValue)
42+
}
43+
44+
45+
return lessContrast(col1, col2, col3, thresholdValue);
46+
}
47+
48+
const fade = async (col, value) => {
49+
const colorValue = await getColorValue(col);
50+
return lessFade(getColorInstance(colorValue), {
51+
value
52+
});
53+
}
54+
55+
const saturate = async (col, value) => {
56+
const colorValue = await getColorValue(col);
57+
return lessSaturate(getColorInstance(colorValue), {
58+
value
59+
});
60+
}
61+
62+
const desaturate = async (col, value) => {
63+
const colorValue = await getColorValue(col);
64+
return lessDesaturate(getColorInstance(colorValue), {
65+
value
66+
});
67+
}
68+
69+
const mix = async (color1, color2, value) => {
70+
const color1Value = await getColorValue(color1);
71+
const color2Value = await getColorValue(color2);
72+
const col1 = getColorInstance(color1Value);
73+
const col2 = getColorInstance(color2Value);
74+
return lessMix(col1, col2, {
75+
value
76+
});
77+
}
78+
79+
const spin = async (col, value) => {
80+
const colorValue = await getColorValue(col);
81+
return lessSpin(getColorInstance(colorValue), {
82+
value
83+
});
84+
}
85+
86+
const concat = async (...derivations) => {
87+
let result = "";
88+
89+
const derivedPromises = derivations.map(derivation => getColorValue(derivation.var));
90+
91+
await Promise.all(derivedPromises).then((values) => {
92+
93+
values.forEach((value, i) => {
94+
if (i > 0) {
95+
result += `, `;
96+
}
97+
result += `${derivations[i].static} ${value}`;
98+
})
99+
});
100+
101+
return result;
102+
}
103+
104+
return { darken, lighten, contrast, fade, saturate, desaturate, mix, spin, concat };
105+
}
106+
107+
module.exports = factory;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
const derivationsFactory = ({ darken, lighten, contrast, fade, saturate, desaturate, mix, spin, concat }) => {
3+
const derivations = {
4+
"--sapUiFieldWarningColorDarken100": () => darken("--sapUiFieldWarningColor", 100), // #000000;
5+
"--sapUiListBackgroundDarken3": () => darken("--sapUiListBackground", 3), // #f7f7f7;
6+
"--sapUiListBackgroundDarken10": () => darken("--sapUiListBackground", 10), // #e6e6e6;
7+
"--sapUiListBackgroundDarken13": () => darken("--sapUiListBackground", 13), // #dedede;
8+
"--sapUiListBackgroundDarken15": () => darken("--sapUiListBackground", 15), // #d9d9d9;
9+
"--sapUiListBackgroundDarken20": () => darken("--sapUiListBackground", 20), // #cccccc;
10+
"--sapUiTileBackgroundDarken20": () => darken("--sapUiTileBackground", 20), // #000000;
11+
"--sapUiListBorderColorLighten10": () => lighten("--sapUiListBorderColor", 10), // #ffffff;
12+
"--sapUiActiveLighten3": () => lighten("--sapUiActive", 3), // #085caf;
13+
"--sapUiLinkDarken15": () => darken("--sapUiLink", 15), // #004065;
14+
"--sapUiSelectedDarken10": () => darken("--sapUiSelected", 10), // #346187;
15+
"--sapUiShellBorderColorLighten30": () => lighten("--sapUiShellBorderColor", 30), // rgba(77, 77, 77, 0);
16+
"--sapUiToggleButtonPressedBackgroundLighten50Desaturate47": () => lighten(desaturate("--sapUiToggleButtonPressedBackground", 47), 50), // #dddddd;
17+
"--sapUiToggleButtonPressedBorderColorLighten19Desaturate46": () => lighten(desaturate("--sapUiToggleButtonPressedBorderColor", 46), 19), // #818181;
18+
"--sapUiSuccessBGLighten5": () => lighten("--sapUiSuccessBG", 5), // #f6fcf6;
19+
"--sapUiErrorBGLighten4": () => lighten("--sapUiErrorBG", 4), // #fff8f8;
20+
"--sapUiButtonBackgroundDarken7": () => darken("--sapUiButtonBackground", 7), // #e5e5e5;
21+
"--sapUiButtonBackgroundDarken2": () => darken("--sapUiButtonBackground", 2), // #f2f2f2;
22+
"--sapUiButtonHoverBackgroundDarken2": () => darken("--sapUiButtonHoverBackground", 2), // #e5e5e5;
23+
"--sapUiButtonRejectActiveBackgroundDarken5": () => darken("--sapUiButtonRejectActiveBackground", 5), // #a20000;
24+
"--sapUiButtonAcceptActiveBackgroundDarken5": () => darken("--sapUiButtonAcceptActiveBackground", 5), // #246924;
25+
"--sapUiContentForegroundColorLighten5": () => lighten("--sapUiContentForegroundColor", 5), // #f2f2f2;
26+
"--sapUiContentForegroundColorLighten7": () => lighten("--sapUiContentForegroundColor", 7), // #f7f7f7;
27+
"--sapUiContentForegroundColorDarken3": () => darken("--sapUiContentForegroundColor", 3), // #dedede;
28+
"--sapUiContentForegroundColorDarken5": () => darken("--sapUiContentForegroundColor", 5), // #d9d9d9;
29+
"--sapUiContentForegroundColorDarken10": () => darken("--sapUiContentForegroundColor", 10), // #cccccc;
30+
"--sapUiButtonRejectActiveBackgroundLighten5": () => lighten("--sapUiButtonRejectActiveBackground", 5),
31+
"--sapUiButtonAcceptActiveBackgroundLighten5": () => lighten("--sapUiButtonAcceptActiveBackground", 5),
32+
};
33+
34+
return derivations;
35+
};
36+
37+
module.exports = derivationsFactory;

0 commit comments

Comments
 (0)