@skbkontur/colors 2.0.0-alpha.6 → 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.
Files changed (43) hide show
  1. package/.gitignore +10 -0
  2. package/.npmignore +10 -0
  3. package/CHANGELOG.md +117 -0
  4. package/__docs__/Colors.docs.stories.tsx +1578 -0
  5. package/__docs__/Colors.mdx +228 -0
  6. package/__docs__/ColorsAPI.docs.stories.tsx +954 -0
  7. package/__docs__/ColorsAPI.mdx +133 -0
  8. package/__stories__/colors.stories.tsx +452 -0
  9. package/__tests__/convert-color.test.ts +23 -0
  10. package/__tests__/create-tokens-from-figma.test.ts +162 -0
  11. package/__tests__/format-variable.test.ts +16 -0
  12. package/__tests__/get-colors-base.test.ts +55 -0
  13. package/__tests__/get-colors.test.ts +75 -0
  14. package/__tests__/get-interactions.test.ts +37 -0
  15. package/__tests__/get-logo.test.ts +24 -0
  16. package/__tests__/get-palette.test.ts +43 -0
  17. package/__tests__/get-promo.test.ts +32 -0
  18. package/colors-default-dark.d.ts +319 -0
  19. package/colors-default-dark.js +319 -0
  20. package/colors-default-dark.ts +332 -0
  21. package/colors-default-light.d.ts +319 -0
  22. package/colors-default-light.js +319 -0
  23. package/colors-default-light.ts +336 -0
  24. package/package.json +25 -28
  25. package/scripts/create-tokens-files.ts +424 -0
  26. package/scripts/create-tokens-from-figma.ts +376 -0
  27. package/scripts/figma-tokens-base.json +3499 -0
  28. package/scripts/figma-tokens.json +710 -0
  29. package/tokens/brand-blue-deep_accent-brand.css +1 -1
  30. package/tokens/brand-blue-deep_accent-gray.css +1 -1
  31. package/tokens/brand-blue_accent-brand.css +1 -1
  32. package/tokens/brand-blue_accent-gray.css +1 -1
  33. package/tokens/brand-green_accent-brand.css +1 -1
  34. package/tokens/brand-green_accent-gray.css +1 -1
  35. package/tokens/brand-mint_accent-brand.css +1 -1
  36. package/tokens/brand-mint_accent-gray.css +1 -1
  37. package/tokens/brand-orange_accent-gray.css +1 -1
  38. package/tokens/brand-purple_accent-brand.css +1 -1
  39. package/tokens/brand-purple_accent-gray.css +1 -1
  40. package/tokens/brand-red_accent-gray.css +1 -1
  41. package/tokens/brand-violet_accent-brand.css +1 -1
  42. package/tokens/brand-violet_accent-gray.css +1 -1
  43. package/tsconfig.json +8 -0
@@ -0,0 +1,162 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { slashToCamelCase, transformations, extractTokensFromFigma } from '../scripts/create-tokens-from-figma';
3
+
4
+ const [
5
+ { fn: filterFn },
6
+ { fn: groupByThemeFn },
7
+ { fn: reorderStatesFn },
8
+ { fn: applyNamingFn },
9
+ { fn: mapToBaseTokensFn },
10
+ { fn: sortKeysFn },
11
+ { fn: generateCodeFn },
12
+ ] = transformations;
13
+
14
+ describe('extractTokensFromFigma (Integration)', () => {
15
+ test('should transform raw input to final TypeScript code', () => {
16
+ const rawInput = {
17
+ 'Text/Primary / Light': 'Gray/20',
18
+ 'Text/Primary / Dark': 'Gray/80',
19
+ 'Shape/Onbrand/Default / Light': 'Brand/Normal/100',
20
+ 'Shape/Onbrand/Default / Dark': 'Brand/Normal/0',
21
+ 'Line/Soft / Light': 'Light/Line/Soft',
22
+ 'Line/Soft / Dark': 'Dark/Line/Soft',
23
+ 'Effect/Drop Shadow / Light': 'rgba(0,0,0,0.1)',
24
+ 'Incomplete/Token / Dark': 'Value',
25
+ };
26
+
27
+ const expectedCode = `import type { TokensBase } from './types/tokens-base.js';
28
+
29
+ export const getColorsDefaultTokens = (base: TokensBase) => ({
30
+ light: {
31
+ textPrimary: base.gray[20],
32
+ shapeOnBrandDefault: base.brand.original,
33
+ lineSoft: base.accent?.palette?.dim[76] || base.blackAlpha[48]
34
+ },
35
+ dark: {
36
+ textPrimary: base.gray[80],
37
+ shapeOnBrandDefault: base.brand.promo,
38
+ lineSoft: base.accent?.palette?.dim[48] || base.whiteAlpha[48]
39
+ }
40
+ });
41
+ `;
42
+ const result = extractTokensFromFigma(rawInput);
43
+
44
+ expect(result.trim()).toBe(expectedCode.trim());
45
+ });
46
+ });
47
+
48
+ describe('Pipeline Steps', () => {
49
+ test('should convert path segments to camelCase and preserve custom casing (slashToCamelCase)', () => {
50
+ expect(slashToCamelCase('Text/Primary')).toBe('textPrimary');
51
+ expect(slashToCamelCase('Surface/Base/Hover')).toBe('surfaceBaseHover');
52
+ expect(slashToCamelCase('Text/OnBrand/Primary')).toBe('textOnBrandPrimary');
53
+ expect(slashToCamelCase('Line Soft')).toBe('lineSoft');
54
+ expect(slashToCamelCase('red-500')).toBe('red500');
55
+ });
56
+
57
+ test('should exclude Effect tokens and values containing rgba/hex (Filter)', () => {
58
+ const input = {
59
+ 'Valid/Token / Light': 'Gray/100',
60
+ 'Effect/Shadow / Light': 'rgba(0, 0, 0, 0.1)',
61
+ 'Another/Effect / Dark': '#FFFFFF',
62
+ 'Keep/This / Dark': 'Brand/200',
63
+ };
64
+ const expected = {
65
+ 'Valid/Token / Light': 'Gray/100',
66
+ 'Keep/This / Dark': 'Brand/200',
67
+ };
68
+ expect(filterFn(input)).toEqual(expected);
69
+ });
70
+
71
+ test('should group light/dark pairs and exclude incomplete tokens (Group by Theme)', () => {
72
+ const input = {
73
+ 'Surface/Base / Light': 'v1',
74
+ 'Surface/Base / Dark': 'v2',
75
+ 'Incomplete/Token / Light': 'v3',
76
+ };
77
+ const expected = {
78
+ 'Surface/Base': { light: 'v1', dark: 'v2' },
79
+ };
80
+ expect(groupByThemeFn(input)).toEqual(expected);
81
+ });
82
+
83
+ test('should move state suffixes (Hover, Pressed, Disabled) to the end (Reorder States)', () => {
84
+ const input = {
85
+ 'Hover/Shape/Other/Low': { light: 'v1', dark: 'v2' },
86
+ 'Shape/Base/Pressed': { light: 'v3', dark: 'v4' },
87
+ 'Simple/Token': { light: 'v5', dark: 'v6' },
88
+ };
89
+ const expected = {
90
+ 'Shape/Other/Low/Hover': { light: 'v1', dark: 'v2' },
91
+ 'Shape/Base/Pressed': { light: 'v3', dark: 'v4' },
92
+ 'Simple/Token': { light: 'v5', dark: 'v6' },
93
+ };
94
+ expect(reorderStatesFn(input)).toEqual(expected);
95
+ });
96
+
97
+ test('should convert to camelCase and fix "Onbrand" casing (Apply Naming)', () => {
98
+ const input = {
99
+ 'Text/Onbrand/Primary': { light: 'v1', dark: 'v2' },
100
+ 'Surface/Base/Hover': { light: 'v3', dark: 'v4' },
101
+ };
102
+ const expected = {
103
+ textOnBrandPrimary: { light: 'v1', dark: 'v2' },
104
+ surfaceBaseHover: { light: 'v3', dark: 'v4' },
105
+ };
106
+ expect(applyNamingFn(input)).toEqual(expected);
107
+ });
108
+
109
+ test('should convert raw values to JS expressions and handle special cases (Map to BaseTokens)', () => {
110
+ const input = {
111
+ standardToken: { light: 'Gray/100', dark: 'Blue/50' },
112
+ brandOriginal: { light: 'Brand/Normal/100', dark: 'Brand/Normal/100' },
113
+ brandPromo: { light: 'Brand/Normal/0', dark: 'Brand/Normal/0' },
114
+ customizableColor: { light: 'Violet/100', dark: 'Violet/100' },
115
+
116
+ onAccentScale: { light: 'OnAccent/40', dark: 'OnAccent/40' },
117
+ };
118
+ const expected = {
119
+ standardToken: { light: 'base.gray[100]', dark: 'base.customizable.blue[50]' },
120
+ brandOriginal: { light: 'base.brand.original', dark: 'base.brand.original' },
121
+ brandPromo: { light: 'base.brand.promo', dark: 'base.brand.promo' },
122
+ customizableColor: { light: 'base.customizable.violet[100]', dark: 'base.customizable.violet[100]' },
123
+
124
+ onAccentScale: { light: 'base.onAccent?.[40]', dark: 'base.onAccent?.[40]' },
125
+ };
126
+ expect(mapToBaseTokensFn(input)).toEqual(expected);
127
+ });
128
+
129
+ test('should sort keys according to defined SORT_ORDER (Text > Shape > Surface) (Sort Keys)', () => {
130
+ const input = {
131
+ surfaceBase: { light: 'v1', dark: 'v1' },
132
+ shapeFaint: { light: 'v2', dark: 'v2' },
133
+ textPrimary: { light: 'v3', dark: 'v3' },
134
+ };
135
+
136
+ const resultKeys = (sortKeysFn(input) as any[]).map(([key]) => key);
137
+
138
+ expect(resultKeys).toEqual(['textPrimary', 'shapeFaint', 'surfaceBase']);
139
+ });
140
+
141
+ test('should produce the final TS function with correct formatting (Generate Code)', () => {
142
+ const input = [
143
+ ['tokenOne', { light: 'l1', dark: 'd1' }],
144
+ ['tokenTwo', { light: 'l2', dark: 'd2' }],
145
+ ];
146
+
147
+ const expected = `import type { TokensBase } from './types/tokens-base.js';
148
+
149
+ export const getColorsDefaultTokens = (base: TokensBase) => ({
150
+ light: {
151
+ tokenOne: l1,
152
+ tokenTwo: l2
153
+ },
154
+ dark: {
155
+ tokenOne: d1,
156
+ tokenTwo: d2
157
+ }
158
+ });
159
+ `;
160
+ expect(generateCodeFn(input).trim()).toBe(expected.trim());
161
+ });
162
+ });
@@ -0,0 +1,16 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { camelCaseToKebabCase, kebabCaseToCamelCase } from '../lib/utils/format-variable';
3
+
4
+ describe('format-variable', () => {
5
+ test('camelCaseToKebabCase should convert correctly', () => {
6
+ expect(camelCaseToKebabCase('someVariableName')).toBe('some-variable-name');
7
+ expect(camelCaseToKebabCase('button')).toBe('button');
8
+ expect(camelCaseToKebabCase('onBrandPrimary')).toBe('on-brand-primary');
9
+ });
10
+
11
+ test('kebabCaseToCamelCase should convert correctly', () => {
12
+ expect(kebabCaseToCamelCase('some-variable-name')).toBe('someVariableName');
13
+ expect(kebabCaseToCamelCase('button')).toBe('button');
14
+ expect(kebabCaseToCamelCase('on-brand-primary')).toBe('onBrandPrimary');
15
+ });
16
+ });
@@ -0,0 +1,55 @@
1
+ import { test, expect } from 'vitest';
2
+ import { getColorsBase } from '../lib/get-colors-base';
3
+
4
+ test('should return full TokensBase structure by default', () => {
5
+ const res = getColorsBase({ brand: 'blue', accent: 'brand' });
6
+ expect(res.brand.original).toBeDefined();
7
+ expect(res.brand.palette).toBeDefined();
8
+ });
9
+
10
+ test('should use preset color when brand name is provided', () => {
11
+ const res = getColorsBase({ brand: 'blue', accent: 'brand' });
12
+ expect(res.brand.original.toLowerCase()).toBe('#2291ff');
13
+ });
14
+
15
+ test('should use custom hex when brand is a hex string', () => {
16
+ const res = getColorsBase({ brand: '#FF5500', accent: 'brand' });
17
+ expect(res.brand.original).toBe('#FF5500');
18
+ });
19
+
20
+ test('should link accent to brand when accent is "brand"', () => {
21
+ const res = getColorsBase({ brand: 'blue', accent: 'brand' });
22
+ expect(res.accent?.original.light).toBe(res.brand.original);
23
+ });
24
+
25
+ test('should not generate accent palette when accent is "gray"', () => {
26
+ const res = getColorsBase({ brand: 'blue', accent: 'gray' });
27
+ expect(res.accent).toBeUndefined();
28
+ });
29
+
30
+ test('should apply custom system colors to palettes', () => {
31
+ const system = { warning: '#FF00FF', error: '#00FFFF', success: '#FFFF00' };
32
+ const res = getColorsBase({ brand: 'blue', accent: 'brand', system });
33
+ expect(Object.values(res.warning.normal)[0]).toMatch(/^#[0-9a-f]{6}$/i);
34
+ });
35
+
36
+ test('should return oklch values when format is "oklch"', () => {
37
+ const res = getColorsBase({ brand: 'blue', accent: 'brand', format: 'oklch' });
38
+ expect(res.brand.promo).toContain('oklch(');
39
+ });
40
+
41
+ test('should return hex values when format is "hex-aarrggbb"', () => {
42
+ const res = getColorsBase({ brand: 'blue', accent: 'brand', format: 'hex-aarrggbb' });
43
+ expect(res.brand.promo).toMatch(/^#[0-9A-F]{6,8}$/);
44
+ });
45
+
46
+ test('should generate onBrand tokens', () => {
47
+ const res = getColorsBase({ brand: '#ffffff', accent: 'brand' });
48
+ expect(res.onBrand[100]).toContain('0, 0, 0');
49
+ });
50
+
51
+ test('should provide all customizable palettes', () => {
52
+ const res = getColorsBase({ brand: 'blue', accent: 'brand' });
53
+ expect(res.customizable.red).toBeDefined();
54
+ expect(res.customizable.green).toBeDefined();
55
+ });
@@ -0,0 +1,75 @@
1
+ import { test, expect } from 'vitest';
2
+ import { getColors } from '../lib/get-colors';
3
+
4
+ test('should return both themes with HEX values by default', () => {
5
+ const res = getColors({ brand: 'blue', accent: 'brand', theme: 'light' });
6
+ const firstToken = Object.values(res)[0] as string;
7
+ expect(firstToken).toMatch(/^#[0-9a-f]{6}$/i);
8
+ });
9
+
10
+ test('should return flat object when theme is "light"', () => {
11
+ const res = getColors({ brand: 'blue', accent: 'brand', theme: 'light' });
12
+ const firstToken = Object.values(res)[0] as string;
13
+ expect(firstToken).toMatch(/^#[0-9a-f]{6}$/i);
14
+ });
15
+
16
+ test('should use custom hex for brand tokens', () => {
17
+ const res = getColors({ brand: '#FF5500', accent: 'brand', theme: 'light' });
18
+ const firstToken = Object.values(res)[0] as string;
19
+ expect(firstToken).toMatch(/^#[0-9a-f]{6}$/i);
20
+ });
21
+
22
+ test('should apply custom system palette', () => {
23
+ const system = { warning: '#000000', error: '#000000', success: '#000000' };
24
+ const res = getColors({ brand: 'blue', accent: 'brand', system, theme: 'light' });
25
+ expect(res).toBeDefined();
26
+ });
27
+
28
+ test('should output OKLCH strings when format is "oklch"', () => {
29
+ const res = getColors({ brand: 'blue', accent: 'brand', theme: 'light', format: 'oklch' });
30
+ const firstToken = Object.values(res)[0] as string;
31
+ expect(firstToken).toContain('oklch(');
32
+ });
33
+
34
+ test('should output ARGB hex for "hex-aarrggbb" format', () => {
35
+ const res = getColors({ brand: 'blue', accent: 'brand', theme: 'light', format: 'hex-aarrggbb' });
36
+ const firstToken = Object.values(res)[0] as string;
37
+ expect(firstToken).toMatch(/^#[0-9A-F]{6,8}$/);
38
+ });
39
+
40
+ test('should merge custom tokens via overrides', () => {
41
+ const res = getColors({
42
+ brand: 'blue',
43
+ accent: 'brand',
44
+ theme: 'light',
45
+ overrides: (base) => ({ light: { customBrand: base?.gray[20] } } as any),
46
+ }) as any;
47
+ expect(res.customBrand).toBe('#161616');
48
+ });
49
+
50
+ test('should apply format to override tokens', () => {
51
+ const res = getColors({
52
+ brand: 'blue',
53
+ accent: 'brand',
54
+ theme: 'light',
55
+ format: 'oklch',
56
+ overrides: () => ({
57
+ light: { testToken: '#ffffff' },
58
+ dark: { testToken: '#000000' },
59
+ }),
60
+ }) as any;
61
+ expect(res.testToken).toContain('oklch(');
62
+ });
63
+
64
+ test('should process themed values in overrides', () => {
65
+ const res = getColors({
66
+ brand: 'blue',
67
+ accent: 'brand',
68
+ theme: 'light',
69
+ overrides: () => ({
70
+ light: { testToken: '#ffffff' },
71
+ dark: { testToken: '#000000' },
72
+ }),
73
+ }) as any;
74
+ expect(res.testToken).toBe('#ffffff');
75
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { getHover, getPressed } from '../lib/helpers/get-interactions';
3
+
4
+ describe('getHover', () => {
5
+ test('should return oklch strings for valid hex', () => {
6
+ const result = getHover('#0070FF');
7
+
8
+ expect(result.light).toContain('oklch(');
9
+ expect(result.dark).toContain('oklch(');
10
+ });
11
+
12
+ test('should return original hex for invalid input', () => {
13
+ const invalid = 'invalid-color';
14
+ const result = getHover(invalid);
15
+
16
+ expect(result.light).toBe(invalid);
17
+ expect(result.dark).toBe(invalid);
18
+ });
19
+ });
20
+
21
+ describe('getPressed', () => {
22
+ test('should return oklch strings for valid hex', () => {
23
+ const result = getPressed('#0070FF');
24
+
25
+ expect(result.light).toContain('oklch(');
26
+ expect(result.dark).toContain('oklch(');
27
+ });
28
+
29
+ test('should apply different deltas than hover', () => {
30
+ const hex = '#0070FF';
31
+ const hover = getHover(hex);
32
+ const pressed = getPressed(hex);
33
+
34
+ expect(pressed.light).not.toBe(hover.light);
35
+ expect(pressed.dark).not.toBe(hover.dark);
36
+ });
37
+ });
@@ -0,0 +1,24 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { getLogo } from '../lib/helpers/get-logo';
3
+ import { LOGO_LIGHTNESS_MIN } from '../lib/consts/params/logo-lightness';
4
+
5
+ describe('getLogo', () => {
6
+ test('should return hex for light and oklch for dark theme', () => {
7
+ const hex = '#0070FF';
8
+ const result = getLogo(hex);
9
+ expect(result.light).toBe(hex);
10
+ expect(result.dark).toContain('oklch(');
11
+ });
12
+
13
+ test('should clamp lightness to LOGO_LIGHTNESS_MIN', () => {
14
+ const result = getLogo('#000000');
15
+ const expectedL = (LOGO_LIGHTNESS_MIN / 100).toFixed(3);
16
+ expect(result.dark).toContain(`oklch(${expectedL}`);
17
+ });
18
+
19
+ test('should return fallback for invalid input', () => {
20
+ const result = getLogo('invalid');
21
+ expect(result.light).toBe('invalid');
22
+ expect(result.dark).toBe(`oklch(${LOGO_LIGHTNESS_MIN}% 0 0)`);
23
+ });
24
+ });
@@ -0,0 +1,43 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { getPalette } from '../lib/helpers/get-palette';
3
+
4
+ describe('getPalette', () => {
5
+ const testColor = '#2291FF';
6
+
7
+ test('should return a full palette object with correct scales', () => {
8
+ const palette = getPalette({ color: testColor });
9
+
10
+ expect(palette).toHaveProperty('vivid');
11
+ expect(palette).toHaveProperty('normal');
12
+ expect(palette).toHaveProperty('dim');
13
+
14
+ expect(palette.normal[52]).toMatch(/^oklch\(/);
15
+ });
16
+
17
+ test('should generate different colors for "default" and "warning" types', () => {
18
+ const defaultPalette = getPalette({ color: testColor, type: 'default' });
19
+ const warningPalette = getPalette({ color: testColor, type: 'warning' });
20
+
21
+ expect(defaultPalette.normal[64]).not.toBe(warningPalette.normal[64]);
22
+ });
23
+
24
+ test('should respect custom settings if provided', () => {
25
+ const customSettings = {
26
+ promoHueShifts: { 0: 100 },
27
+ };
28
+
29
+ const palette = getPalette({
30
+ color: '#FF0000',
31
+ settings: customSettings as any,
32
+ });
33
+
34
+ expect(palette.vivid[52]).toBeDefined();
35
+ });
36
+
37
+ test('should handle edge cases with lightness and chroma clamping', () => {
38
+ const palette = getPalette({ color: '#FFFFFF' });
39
+
40
+ expect(palette.vivid[20]).toContain('oklch(');
41
+ expect(palette.dim[96]).toContain('oklch(');
42
+ });
43
+ });
@@ -0,0 +1,32 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { getPromo, getPromoHueShift } from '../lib/helpers/get-promo';
3
+ import { PROMO_HUE_SHIFTS } from '../lib/consts/params/promo-hue-shift';
4
+
5
+ describe('getPromo', () => {
6
+ test('should transform color to promo hex', () => {
7
+ const result = getPromo('#0070FF');
8
+
9
+ expect(result).toMatch(/^#[0-9a-f]{6}$/i);
10
+ expect(result).not.toBe('#0070ff');
11
+ });
12
+
13
+ test('should throw on invalid input', () => {
14
+ expect(() => getPromo('invalid')).toThrow('Invalid color string: invalid');
15
+ });
16
+ });
17
+
18
+ describe('getPromoHueShift', () => {
19
+ test('should find correct shift for hue ranges', () => {
20
+ const shifts = { 0: -10, 100: 20, 200: 30 };
21
+
22
+ expect(getPromoHueShift(50, shifts)).toBe(-10);
23
+ expect(getPromoHueShift(150, shifts)).toBe(20);
24
+ expect(getPromoHueShift(250, shifts)).toBe(30);
25
+ expect(getPromoHueShift(350, shifts)).toBe(30);
26
+ });
27
+
28
+ test('should use default shifts from constants', () => {
29
+ expect(getPromoHueShift(0, PROMO_HUE_SHIFTS)).toBe(-24);
30
+ expect(getPromoHueShift(319, PROMO_HUE_SHIFTS)).toBe(-24);
31
+ });
32
+ });