@skbkontur/colors 2.0.0 → 2.0.1

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.
@@ -0,0 +1,424 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ import { camelCaseToKebabCase, kebabCaseToCamelCase } from '../lib/utils/format-variable.js';
5
+ import { getColors } from '../lib/get-colors.js';
6
+ import * as DEFAULT_SWATCH from '../lib/consts/default-swatch.js';
7
+ import type { ColorObject, ColorValue } from '../lib/types/tokens.js';
8
+ import type { ConfigOptions } from '../lib/get-colors-base.js';
9
+
10
+ type ColorFormat = ConfigOptions['format'];
11
+
12
+ const DEFAULT_BRAND = 'red';
13
+ const DEFAULT_ACCENT = 'gray';
14
+
15
+ interface SaveTokensOptions {
16
+ colorBrand: string;
17
+ colorAccent: string;
18
+ colorFormat?: ColorFormat;
19
+ tokens: any;
20
+ tokensIsFlat?: boolean;
21
+ tokensCSSPrefix?: string;
22
+ tokensJSVariableName?: string;
23
+ fileSingleOutputName?: string;
24
+ fileOutputDir: string;
25
+ fileFormat: 'json' | 'css' | 'less' | 'scss' | 'js' | 'js-css-vars' | 'js-css-vars-fallback';
26
+ removePressedAndHover?: boolean;
27
+ }
28
+
29
+ const TOKENS_OUTPUT = path.join(import.meta.dirname, '..');
30
+
31
+ for (const accentVariant of ['brand', 'gray']) {
32
+ for (const brandColorKey in DEFAULT_SWATCH.brand) {
33
+ if (accentVariant === 'brand' && (brandColorKey === 'red' || brandColorKey === 'orange')) {
34
+ continue;
35
+ }
36
+
37
+ const tokens = {
38
+ light: getColors({
39
+ brand: brandColorKey,
40
+ accent: accentVariant,
41
+ theme: 'light',
42
+ }),
43
+ dark: getColors({
44
+ brand: brandColorKey,
45
+ accent: accentVariant,
46
+ theme: 'dark',
47
+ }),
48
+ };
49
+
50
+ const tokensMobile = {
51
+ light: getColors({
52
+ brand: brandColorKey,
53
+ accent: accentVariant,
54
+ theme: 'light',
55
+ format: 'hex-aarrggbb',
56
+ }),
57
+ dark: getColors({
58
+ brand: brandColorKey,
59
+ accent: accentVariant,
60
+ theme: 'dark',
61
+ format: 'hex-aarrggbb',
62
+ }),
63
+ };
64
+
65
+ const brandFileName = camelCaseToKebabCase(brandColorKey);
66
+
67
+ saveTokens({
68
+ tokens,
69
+ colorBrand: brandFileName,
70
+ colorAccent: accentVariant,
71
+ fileOutputDir: path.join(TOKENS_OUTPUT, 'tokens'),
72
+ fileFormat: 'css',
73
+ tokensCSSPrefix: 'k-color',
74
+ });
75
+
76
+ saveTokens({
77
+ tokens: tokensMobile,
78
+ colorBrand: brandFileName,
79
+ colorAccent: accentVariant,
80
+ fileOutputDir: path.join(TOKENS_OUTPUT, 'tokens-mobile'),
81
+ fileFormat: 'json',
82
+ tokensIsFlat: true,
83
+ removePressedAndHover: true,
84
+ });
85
+ }
86
+ }
87
+
88
+ const tokensDefault = {
89
+ light: getColors({
90
+ brand: DEFAULT_BRAND,
91
+ accent: DEFAULT_ACCENT,
92
+ theme: 'light',
93
+ }),
94
+ dark: getColors({
95
+ brand: DEFAULT_BRAND,
96
+ accent: DEFAULT_ACCENT,
97
+ theme: 'dark',
98
+ }),
99
+ };
100
+
101
+ const defaultBrandFileName = camelCaseToKebabCase(DEFAULT_BRAND);
102
+
103
+ saveTokens({
104
+ tokens: tokensDefault,
105
+ colorBrand: defaultBrandFileName,
106
+ colorAccent: DEFAULT_ACCENT,
107
+ fileOutputDir: '',
108
+ fileFormat: 'scss',
109
+ tokensIsFlat: true,
110
+ tokensCSSPrefix: 'k-color',
111
+ fileSingleOutputName: path.join(TOKENS_OUTPUT, 'colors.scss'),
112
+ });
113
+
114
+ saveTokens({
115
+ tokens: tokensDefault,
116
+ colorBrand: defaultBrandFileName,
117
+ colorAccent: DEFAULT_ACCENT,
118
+ fileOutputDir: '',
119
+ fileFormat: 'less',
120
+ tokensIsFlat: true,
121
+ tokensCSSPrefix: 'k-color',
122
+ fileSingleOutputName: path.join(TOKENS_OUTPUT, 'colors.less'),
123
+ });
124
+
125
+ saveTokens({
126
+ tokens: tokensDefault,
127
+ colorBrand: defaultBrandFileName,
128
+ colorAccent: DEFAULT_ACCENT,
129
+ fileOutputDir: '',
130
+ fileFormat: 'js-css-vars',
131
+ tokensIsFlat: true,
132
+ tokensCSSPrefix: 'k-color',
133
+ fileSingleOutputName: path.join(TOKENS_OUTPUT, 'colors.ts'),
134
+ });
135
+
136
+ saveTokens({
137
+ tokens: { light: tokensDefault.light },
138
+ colorBrand: defaultBrandFileName,
139
+ colorAccent: DEFAULT_ACCENT,
140
+ fileOutputDir: '',
141
+ fileFormat: 'js-css-vars-fallback',
142
+ tokensIsFlat: true,
143
+ tokensCSSPrefix: 'k-color',
144
+ fileSingleOutputName: path.join(TOKENS_OUTPUT, 'colors-default-light.ts'),
145
+ });
146
+
147
+ saveTokens({
148
+ tokens: { dark: tokensDefault.dark },
149
+ colorBrand: defaultBrandFileName,
150
+ colorAccent: DEFAULT_ACCENT,
151
+ fileOutputDir: '',
152
+ fileFormat: 'js-css-vars-fallback',
153
+ tokensIsFlat: true,
154
+ tokensCSSPrefix: 'k-color',
155
+ fileSingleOutputName: path.join(TOKENS_OUTPUT, 'colors-default-dark.ts'),
156
+ });
157
+
158
+ function saveTokens({
159
+ tokens,
160
+ colorBrand,
161
+ colorAccent,
162
+ tokensIsFlat,
163
+ tokensCSSPrefix,
164
+ tokensJSVariableName,
165
+ fileSingleOutputName,
166
+ fileOutputDir,
167
+ fileFormat,
168
+ removePressedAndHover,
169
+ }: SaveTokensOptions) {
170
+ const isFlat = tokensIsFlat ?? false;
171
+ const cssPrefix = tokensCSSPrefix ?? '';
172
+
173
+ const brandFileName = colorBrand;
174
+ const accentVariant = colorAccent;
175
+ const outputDir = fileOutputDir;
176
+ const format = fileFormat;
177
+ const singleOutputFile = fileSingleOutputName;
178
+ const jsVariableName = tokensJSVariableName;
179
+
180
+ if (!singleOutputFile) {
181
+ fs.mkdirSync(outputDir, { recursive: true });
182
+ }
183
+
184
+ const baseFileName = `brand-${brandFileName}_accent-${accentVariant.toLowerCase()}`;
185
+ let fileName = baseFileName;
186
+
187
+ if (!isFlat && (format === 'json' || format === 'js')) {
188
+ fileName += '.tree';
189
+ }
190
+
191
+ const finalOutputFile = singleOutputFile || path.join(outputDir, `${fileName}.${format}`);
192
+
193
+ switch (format) {
194
+ case 'json': {
195
+ const tokensToWrite = isFlat ? JSON.parse(JSON.stringify(tokens)) : tokens;
196
+
197
+ if (removePressedAndHover) {
198
+ if (tokensToWrite.light) {
199
+ removeStateTokens(tokensToWrite.light);
200
+ }
201
+ if (tokensToWrite.dark) {
202
+ removeStateTokens(tokensToWrite.dark);
203
+ }
204
+ }
205
+
206
+ const toCamelCaseFlat = (tokens: any) =>
207
+ Object.entries(flattenHybridCase(tokens)).reduce((acc: any, [key, value]) => {
208
+ acc[kebabCaseToCamelCase(key)] = value;
209
+ return acc;
210
+ }, {});
211
+
212
+ const flatLight = toCamelCaseFlat(tokensToWrite.light);
213
+ const flatDark = toCamelCaseFlat(tokensToWrite.dark);
214
+
215
+ const themedTokens: any = {};
216
+ const allKeys = new Set([...Object.keys(flatLight), ...Object.keys(flatDark)]);
217
+
218
+ // @ts-ignore
219
+ for (const key of allKeys) {
220
+ themedTokens[key] = {
221
+ light: flatLight[key],
222
+ dark: flatDark[key],
223
+ };
224
+ }
225
+
226
+ fs.writeFileSync(finalOutputFile, JSON.stringify(themedTokens, null, 2));
227
+ break;
228
+ }
229
+
230
+ case 'css': {
231
+ const brandDataSelector = `[data-k-brand="${brandFileName}"][data-k-accent="${accentVariant.toLowerCase()}"]`;
232
+ const baseSelector = brandDataSelector;
233
+
234
+ let cssContent = '';
235
+
236
+ const hasThemes = tokens.light || tokens.dark;
237
+
238
+ if (hasThemes) {
239
+ const lightTokens = tokens.light;
240
+ const flattenedLightTokens = flattenHybridCase(lightTokens);
241
+ const lightVars = Object.entries(flattenedLightTokens)
242
+ .map(([key, value]) => ` --${cssPrefix}-${camelCaseToKebabCase(key)}: ${value};`)
243
+ .join('\n');
244
+
245
+ cssContent += `${baseSelector} {\n${lightVars}\n}\n\n`;
246
+
247
+ if (tokens.dark) {
248
+ const darkTokens = tokens.dark;
249
+ const flattenedDarkTokens = flattenHybridCase(darkTokens);
250
+ const darkVars = Object.entries(flattenedDarkTokens)
251
+ .map(([key, value]) => ` --${cssPrefix}-${camelCaseToKebabCase(key)}: ${value};`)
252
+ .join('\n');
253
+
254
+ const darkSelector = `${baseSelector}[data-k-theme="dark"]`;
255
+ cssContent += `${darkSelector} {\n${darkVars}\n}\n\n`;
256
+ }
257
+ }
258
+
259
+ fs.writeFileSync(finalOutputFile, cssContent.trim());
260
+ break;
261
+ }
262
+
263
+ case 'less':
264
+ case 'scss': {
265
+ const varPrefix = format === 'less' ? '@color' : '$color';
266
+
267
+ const lessScssVars: string[] = [];
268
+ const hasThemes = tokens.light || tokens.dark;
269
+ const themeTokens = hasThemes ? tokens.light : tokens;
270
+
271
+ if (themeTokens) {
272
+ const flattenedTokens = flattenHybridCase(themeTokens);
273
+
274
+ const themeVars = Object.entries(flattenedTokens).map(([key]) => {
275
+ const cssVarName = camelCaseToKebabCase(key);
276
+
277
+ return `${varPrefix}${cssVarName}: var(--${cssPrefix}-${cssVarName});`;
278
+ });
279
+
280
+ lessScssVars.push(...themeVars);
281
+ }
282
+
283
+ const content = lessScssVars.join('\n');
284
+
285
+ fs.writeFileSync(finalOutputFile, content.trim() + '\n');
286
+ break;
287
+ }
288
+
289
+ case 'js-css-vars': {
290
+ const jsCssVars: string[] = [];
291
+ const hasThemes = tokens.light || tokens.dark;
292
+ const themeTokens = hasThemes ? tokens.light : tokens;
293
+
294
+ if (themeTokens) {
295
+ const flattenedTokens = flattenHybridCase(themeTokens);
296
+
297
+ const jsVars = Object.entries(flattenedTokens).map(([key]) => {
298
+ const cssVarName = camelCaseToKebabCase(key);
299
+ const jsVarNameCamel = kebabCaseToCamelCase(cssVarName);
300
+ const cssVarReference = `var(--${cssPrefix}-${cssVarName})`;
301
+
302
+ return `export const ${jsVarNameCamel} = "${cssVarReference}";`;
303
+ });
304
+
305
+ jsCssVars.push(...jsVars);
306
+ }
307
+
308
+ const content = jsCssVars.join('\n');
309
+
310
+ fs.writeFileSync(finalOutputFile, content.trim() + '\n');
311
+ break;
312
+ }
313
+
314
+ case 'js-css-vars-fallback': {
315
+ const jsCssVars: string[] = [];
316
+
317
+ const themeKey = tokens.light ? 'light' : 'dark';
318
+ const themeTokens = tokens[themeKey];
319
+
320
+ if (themeTokens) {
321
+ const flattenedTokens = flattenHybridCase(themeTokens);
322
+
323
+ const jsVars = Object.entries(flattenedTokens).map(([key, value]) => {
324
+ const cssVarName = camelCaseToKebabCase(key);
325
+ const jsVarNameCamel = kebabCaseToCamelCase(cssVarName);
326
+
327
+ const cssVarReference = `var(--${cssPrefix}-${cssVarName}, ${value})`;
328
+
329
+ return `export const ${jsVarNameCamel} = "${cssVarReference}";`;
330
+ });
331
+
332
+ jsCssVars.push(...jsVars);
333
+ }
334
+
335
+ const content = jsCssVars.join('\n');
336
+ fs.writeFileSync(finalOutputFile, content.trim() + '\n');
337
+ break;
338
+ }
339
+ case 'js': {
340
+ let jsTokens;
341
+
342
+ const toCamelCaseFlat = (tokens: any) =>
343
+ Object.entries(flattenObject(tokens)).reduce((acc: any, [key, value]) => {
344
+ acc[kebabCaseToCamelCase(key)] = value;
345
+ return acc;
346
+ }, {});
347
+
348
+ if (isFlat) {
349
+ jsTokens = {
350
+ light: toCamelCaseFlat(tokens.light),
351
+ dark: toCamelCaseFlat(tokens.dark),
352
+ };
353
+
354
+ const jsContent: string = `export const ${jsVariableName} = ${JSON.stringify(jsTokens, null, 2)};`;
355
+ fs.writeFileSync(finalOutputFile, jsContent);
356
+ } else {
357
+ jsTokens = tokens;
358
+ let jsContent = `export const ${jsVariableName} = ${JSON.stringify(jsTokens, null, 2)};`;
359
+ jsContent = unquoteJsKeys(jsContent);
360
+ fs.writeFileSync(finalOutputFile, jsContent);
361
+ }
362
+ break;
363
+ }
364
+
365
+ default: {
366
+ console.error(`Unsupported format: ${format}`);
367
+ break;
368
+ }
369
+ }
370
+ }
371
+
372
+ function unquoteJsKeys(objString: string): string {
373
+ return objString.replace(/"([^"]+)":/g, '$1:');
374
+ }
375
+
376
+ function removeStateTokens(obj: any): any {
377
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
378
+ return obj;
379
+ }
380
+
381
+ for (const key in obj) {
382
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
383
+ const camelKey = kebabCaseToCamelCase(key);
384
+ if (camelKey.includes('Pressed') || camelKey.includes('Hover')) {
385
+ delete obj[key];
386
+ } else if (typeof obj[key] === 'object' && obj[key] !== null) {
387
+ removeStateTokens(obj[key]);
388
+ }
389
+ }
390
+ }
391
+ return obj;
392
+ }
393
+
394
+ function flattenHybridCase(obj: any, prefix = ''): any {
395
+ return Object.keys(obj).reduce((acc, key) => {
396
+ const newKey = prefix + (prefix ? '-' : '') + key;
397
+ if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
398
+ Object.assign(acc, flattenHybridCase(obj[key], newKey));
399
+ } else {
400
+ // @ts-ignore
401
+ acc[newKey] = obj[key];
402
+ }
403
+ return acc;
404
+ }, {});
405
+ }
406
+
407
+ function flattenObject(obj: ColorObject, prefix = ''): { [key: string]: string | ColorValue } {
408
+ let result: { [key: string]: string | ColorValue } = {};
409
+ for (const key in obj) {
410
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
411
+ const newKey = prefix ? `${prefix}-${camelCaseToKebabCase(key)}` : key;
412
+ if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
413
+ result = {
414
+ ...result,
415
+ // @ts-ignore
416
+ ...flattenObject(obj[key], newKey),
417
+ };
418
+ } else {
419
+ result[newKey] = obj[key];
420
+ }
421
+ }
422
+ }
423
+ return result;
424
+ }