Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / colors / gen-themes.ts
blob21ed23a07b94460d18f0e559a2b1967efc280ba4
1 import cssTree from 'css-tree';
2 import fs from 'fs';
3 import prettier from 'prettier';
4 import tiny from 'tinycolor2';
6 import genButtonShades from './gen-button-shades';
7 import type { ThemeConfig, ThemeFileType } from './themes.config';
9 function generateTheme({ source, type }: { source: string; type: ThemeFileType }) {
10     const buttonBases = [
11         'primary',
12         'signal-danger',
13         'signal-warning',
14         'signal-info',
15         'signal-success',
16         'interaction-norm',
17         'interaction-weak',
18     ];
20     const buttonShadeNames = ['-minor-2', '-minor-1', '', '-major-1', '-major-2', '-major-3', '-contrast'];
22     const ast = cssTree.parse(source);
24     cssTree.walk(ast, (node, item, list) => {
25         if (node.type !== 'Declaration') {
26             return;
27         }
29         if (node.value.type !== 'Raw') {
30             return;
31         }
33         const baseName = node.property.substring(2);
35         if (!buttonBases.includes(baseName)) {
36             return;
37         }
39         /*
40          * make sure we don't visit the same base name again
41          * by removing it from the array of button base names
42          */
43         buttonBases.splice(buttonBases.indexOf(baseName), 1);
45         const isLight = type === 'light';
47         const base = tiny(node.value.value);
49         const buttonShades = genButtonShades(base, isLight);
51         /* here we don't use tiny.mostReadable to prioritize white against black color. */
52         const buttonContrast = tiny(tiny.isReadable(base, 'white', { level: 'AA', size: 'large' }) ? 'white' : 'black');
54         // use original input when color contains alpha channel (opacity, eg. rgba)
55         const declarations = [...buttonShades, buttonContrast].map((color, i) =>
56             list.createItem({
57                 type: 'Declaration',
58                 important: false,
59                 property: '--' + baseName + buttonShadeNames[i],
60                 value: { type: 'Raw', value: color.getAlpha() == 1 ? color.toHexString() : color.toString() },
61             })
62         );
64         if (!item.next) {
65             for (const declaration of declarations) {
66                 list.append(declaration);
67             }
68         } else {
69             /* list.insert() inserts after the next element so we reverse insertion order */
70             for (let i = declarations.length - 1; i >= 0; i--) {
71                 list.insert(declarations[i], item.next);
72             }
73         }
75         /* base is consumed, we don't need it any more and we don't want to re-visit */
76         list.remove(item);
77     });
79     return cssTree.generate(ast);
82 export async function main({ output, files }: ThemeConfig) {
83     const sources = files.map(({ path, type }) => ({
84         source: fs.readFileSync(path, { encoding: 'utf-8' }),
85         type,
86     }));
88     const generatedCssFiles = sources.map(generateTheme);
90     const autoGenerateDisclaimer = [
91         '/*',
92         ' * This file is automatically generated.',
93         ' * Manual changes will be lost.',
94         ' */',
95     ].join('\n');
97     const cssFile = [autoGenerateDisclaimer, ...generatedCssFiles].join('\n\n');
99     const prettierCssFile = await prettier.format(cssFile, { parser: 'css' });
101     fs.writeFileSync(output, prettierCssFile);
103     return output;