@atlaskit/tokens 1.11.3 → 1.12.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 +14 -0
- package/dist/cjs/artifacts/atlassian-dark-token-value-for-contrast-check.js +24 -0
- package/dist/cjs/artifacts/atlassian-light-token-value-for-contrast-check.js +24 -0
- package/dist/cjs/constants.js +3 -1
- package/dist/cjs/custom-theme.js +108 -0
- package/dist/cjs/get-token-value.js +1 -1
- package/dist/cjs/get-token.js +1 -1
- package/dist/cjs/set-global-theme.js +156 -59
- package/dist/cjs/utils/color-utils.js +178 -0
- package/dist/cjs/utils/custom-theme-loading-utils.js +47 -0
- package/dist/cjs/utils/custom-theme-token-contrast-check.js +74 -0
- package/dist/cjs/utils/generate-custom-color-ramp.js +213 -0
- package/dist/cjs/utils/hash.js +17 -0
- package/dist/cjs/utils/hct-color-utils/color-utils.js +310 -0
- package/dist/cjs/utils/hct-color-utils/contrast.js +188 -0
- package/dist/cjs/utils/hct-color-utils/hct.js +1036 -0
- package/dist/cjs/utils/hct-color-utils/index.js +32 -0
- package/dist/cjs/utils/hct-color-utils/math-utils.js +159 -0
- package/dist/cjs/utils/theme-loading.js +1 -1
- package/dist/cjs/utils/theme-state-transformer.js +1 -1
- package/dist/cjs/version.json +1 -1
- package/dist/es2019/artifacts/atlassian-dark-token-value-for-contrast-check.js +17 -0
- package/dist/es2019/artifacts/atlassian-light-token-value-for-contrast-check.js +17 -0
- package/dist/es2019/constants.js +1 -0
- package/dist/es2019/custom-theme.js +77 -0
- package/dist/es2019/get-token-value.js +1 -1
- package/dist/es2019/get-token.js +1 -1
- package/dist/es2019/set-global-theme.js +67 -13
- package/dist/es2019/utils/color-utils.js +154 -0
- package/dist/es2019/utils/custom-theme-loading-utils.js +31 -0
- package/dist/es2019/utils/custom-theme-token-contrast-check.js +68 -0
- package/dist/es2019/utils/generate-custom-color-ramp.js +187 -0
- package/dist/es2019/utils/hash.js +10 -0
- package/dist/es2019/utils/hct-color-utils/color-utils.js +286 -0
- package/dist/es2019/utils/hct-color-utils/contrast.js +161 -0
- package/dist/es2019/utils/hct-color-utils/hct.js +931 -0
- package/dist/es2019/utils/hct-color-utils/index.js +3 -0
- package/dist/es2019/utils/hct-color-utils/math-utils.js +145 -0
- package/dist/es2019/utils/theme-loading.js +2 -2
- package/dist/es2019/utils/theme-state-transformer.js +3 -1
- package/dist/es2019/version.json +1 -1
- package/dist/esm/artifacts/atlassian-dark-token-value-for-contrast-check.js +17 -0
- package/dist/esm/artifacts/atlassian-light-token-value-for-contrast-check.js +17 -0
- package/dist/esm/constants.js +1 -0
- package/dist/esm/custom-theme.js +98 -0
- package/dist/esm/get-token-value.js +1 -1
- package/dist/esm/get-token.js +1 -1
- package/dist/esm/set-global-theme.js +149 -60
- package/dist/esm/utils/color-utils.js +162 -0
- package/dist/esm/utils/custom-theme-loading-utils.js +38 -0
- package/dist/esm/utils/custom-theme-token-contrast-check.js +65 -0
- package/dist/esm/utils/generate-custom-color-ramp.js +202 -0
- package/dist/esm/utils/hash.js +10 -0
- package/dist/esm/utils/hct-color-utils/color-utils.js +285 -0
- package/dist/esm/utils/hct-color-utils/contrast.js +181 -0
- package/dist/esm/utils/hct-color-utils/hct.js +1029 -0
- package/dist/esm/utils/hct-color-utils/index.js +3 -0
- package/dist/esm/utils/hct-color-utils/math-utils.js +145 -0
- package/dist/esm/utils/theme-loading.js +2 -2
- package/dist/esm/utils/theme-state-transformer.js +1 -1
- package/dist/esm/version.json +1 -1
- package/dist/types/artifacts/atlassian-dark-token-value-for-contrast-check.d.ts +17 -0
- package/dist/types/artifacts/atlassian-light-token-value-for-contrast-check.d.ts +17 -0
- package/dist/types/constants.d.ts +1 -0
- package/dist/types/custom-theme.d.ts +30 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/set-global-theme.d.ts +9 -3
- package/dist/types/tokens/atlassian-dark/utility/utility.d.ts +1 -1
- package/dist/types/tokens/atlassian-light/utility/utility.d.ts +1 -1
- package/dist/types/tokens/default/utility/utility.d.ts +1 -1
- package/dist/types/utils/color-utils.d.ts +10 -0
- package/dist/types/utils/custom-theme-loading-utils.d.ts +11 -0
- package/dist/types/utils/custom-theme-token-contrast-check.d.ts +20 -0
- package/dist/types/utils/generate-custom-color-ramp.d.ts +19 -0
- package/dist/types/utils/hash.d.ts +1 -0
- package/dist/types/utils/hct-color-utils/color-utils.d.ts +131 -0
- package/dist/types/utils/hct-color-utils/contrast.d.ts +78 -0
- package/dist/types/utils/hct-color-utils/hct.d.ts +137 -0
- package/dist/types/utils/hct-color-utils/index.d.ts +3 -0
- package/dist/types/utils/hct-color-utils/math-utils.d.ts +86 -0
- package/dist/types-ts4.5/artifacts/atlassian-dark-token-value-for-contrast-check.d.ts +17 -0
- package/dist/types-ts4.5/artifacts/atlassian-light-token-value-for-contrast-check.d.ts +17 -0
- package/dist/types-ts4.5/constants.d.ts +1 -0
- package/dist/types-ts4.5/custom-theme.d.ts +30 -0
- package/dist/types-ts4.5/index.d.ts +1 -0
- package/dist/types-ts4.5/set-global-theme.d.ts +9 -3
- package/dist/types-ts4.5/tokens/atlassian-dark/utility/utility.d.ts +1 -1
- package/dist/types-ts4.5/tokens/atlassian-light/utility/utility.d.ts +1 -1
- package/dist/types-ts4.5/tokens/default/utility/utility.d.ts +1 -1
- package/dist/types-ts4.5/utils/color-utils.d.ts +27 -0
- package/dist/types-ts4.5/utils/custom-theme-loading-utils.d.ts +11 -0
- package/dist/types-ts4.5/utils/custom-theme-token-contrast-check.d.ts +20 -0
- package/dist/types-ts4.5/utils/generate-custom-color-ramp.d.ts +19 -0
- package/dist/types-ts4.5/utils/hash.d.ts +1 -0
- package/dist/types-ts4.5/utils/hct-color-utils/color-utils.d.ts +131 -0
- package/dist/types-ts4.5/utils/hct-color-utils/contrast.d.ts +78 -0
- package/dist/types-ts4.5/utils/hct-color-utils/hct.d.ts +137 -0
- package/dist/types-ts4.5/utils/hct-color-utils/index.d.ts +3 -0
- package/dist/types-ts4.5/utils/hct-color-utils/math-utils.d.ts +86 -0
- package/package.json +35 -36
- package/report.api.md +24 -1
- package/tmp/api-report-tmp.d.ts +0 -1132
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// valid hex color with 6 digits
|
|
2
|
+
export const isValidBrandHex = hex => /^#[0-9A-F]{6}$/i.test(hex);
|
|
3
|
+
|
|
4
|
+
// valid hex color with 4, 6 or 8 digits
|
|
5
|
+
const isValidHex = hex => /^#([A-Fa-f0-9]{3,4}){1,2}$/.test(hex);
|
|
6
|
+
export function rgbToHex(r, g, b) {
|
|
7
|
+
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
|
8
|
+
}
|
|
9
|
+
export function getAlpha(hex) {
|
|
10
|
+
if (hex.length === 9) {
|
|
11
|
+
const int = parseInt(hex.slice(7, 9), 16) / 255;
|
|
12
|
+
return Number(parseFloat(int.toString()).toFixed(2));
|
|
13
|
+
}
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
export function hexToRgbA(hex) {
|
|
17
|
+
if (!isValidHex(hex)) {
|
|
18
|
+
throw new Error('Invalid HEX');
|
|
19
|
+
}
|
|
20
|
+
let c;
|
|
21
|
+
c = hex.substring(1).split('');
|
|
22
|
+
if (c.length === 3) {
|
|
23
|
+
c = [c[0], c[0], c[1], c[1], c[2], c[2]];
|
|
24
|
+
}
|
|
25
|
+
c = '0x' + c.join('');
|
|
26
|
+
return [c >> 16 & 255, c >> 8 & 255, c & 255, getAlpha(hex)];
|
|
27
|
+
}
|
|
28
|
+
export function hexToRgb(hex) {
|
|
29
|
+
if (!isValidHex(hex)) {
|
|
30
|
+
throw new Error('Invalid HEX');
|
|
31
|
+
}
|
|
32
|
+
let c;
|
|
33
|
+
c = hex.substring(1).split('');
|
|
34
|
+
if (c.length === 3) {
|
|
35
|
+
c = [c[0], c[0], c[1], c[1], c[2], c[2]];
|
|
36
|
+
}
|
|
37
|
+
c = '0x' + c.join('');
|
|
38
|
+
return [c >> 16 & 255, c >> 8 & 255, c & 255];
|
|
39
|
+
}
|
|
40
|
+
export function hexToHSL(hex) {
|
|
41
|
+
if (!isValidHex(hex)) {
|
|
42
|
+
throw new Error('Invalid HEX');
|
|
43
|
+
}
|
|
44
|
+
let r = 0,
|
|
45
|
+
g = 0,
|
|
46
|
+
b = 0;
|
|
47
|
+
if (hex.length === 4) {
|
|
48
|
+
r = '0x' + hex[1] + hex[1];
|
|
49
|
+
g = '0x' + hex[2] + hex[2];
|
|
50
|
+
b = '0x' + hex[3] + hex[3];
|
|
51
|
+
} else if (hex.length === 7) {
|
|
52
|
+
r = '0x' + hex[1] + hex[2];
|
|
53
|
+
g = '0x' + hex[3] + hex[4];
|
|
54
|
+
b = '0x' + hex[5] + hex[6];
|
|
55
|
+
}
|
|
56
|
+
// Then to HSL
|
|
57
|
+
r /= 255;
|
|
58
|
+
g /= 255;
|
|
59
|
+
b /= 255;
|
|
60
|
+
let cmin = Math.min(r, g, b),
|
|
61
|
+
cmax = Math.max(r, g, b),
|
|
62
|
+
delta = cmax - cmin,
|
|
63
|
+
h = 0,
|
|
64
|
+
s = 0,
|
|
65
|
+
l = 0;
|
|
66
|
+
if (delta === 0) {
|
|
67
|
+
h = 0;
|
|
68
|
+
} else if (cmax === r) {
|
|
69
|
+
h = (g - b) / delta % 6;
|
|
70
|
+
} else if (cmax === g) {
|
|
71
|
+
h = (b - r) / delta + 2;
|
|
72
|
+
} else {
|
|
73
|
+
h = (r - g) / delta + 4;
|
|
74
|
+
}
|
|
75
|
+
h = Math.round(h * 60);
|
|
76
|
+
if (h < 0) {
|
|
77
|
+
h += 360;
|
|
78
|
+
}
|
|
79
|
+
l = (cmax + cmin) / 2;
|
|
80
|
+
s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
|
|
81
|
+
s = +(s * 100).toFixed(1);
|
|
82
|
+
l = +(l * 100).toFixed(1);
|
|
83
|
+
return [h, s, l];
|
|
84
|
+
}
|
|
85
|
+
export function HSLToRGB(h, s, l) {
|
|
86
|
+
s /= 100;
|
|
87
|
+
l /= 100;
|
|
88
|
+
const k = n => (n + h / 30) % 12;
|
|
89
|
+
const a = s * Math.min(l, 1 - l);
|
|
90
|
+
const f = n => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
91
|
+
return [255 * f(0), 255 * f(8), 255 * f(4)];
|
|
92
|
+
}
|
|
93
|
+
export function relativeLuminanceW3C(r, g, b) {
|
|
94
|
+
const RsRGB = r / 255;
|
|
95
|
+
const GsRGB = g / 255;
|
|
96
|
+
const BsRGB = b / 255;
|
|
97
|
+
const R = RsRGB <= 0.03928 ? RsRGB / 12.92 : Math.pow((RsRGB + 0.055) / 1.055, 2.4);
|
|
98
|
+
const G = GsRGB <= 0.03928 ? GsRGB / 12.92 : Math.pow((GsRGB + 0.055) / 1.055, 2.4);
|
|
99
|
+
const B = BsRGB <= 0.03928 ? BsRGB / 12.92 : Math.pow((BsRGB + 0.055) / 1.055, 2.4);
|
|
100
|
+
|
|
101
|
+
// For the sRGB colorspace, the relative luminance of a color is defined as:
|
|
102
|
+
const L = 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
|
103
|
+
return L;
|
|
104
|
+
}
|
|
105
|
+
export function getContrastRatio(foreground, background) {
|
|
106
|
+
if (!isValidHex(foreground) || !isValidHex(background)) {
|
|
107
|
+
throw new Error('Invalid HEX');
|
|
108
|
+
}
|
|
109
|
+
const foregroundRgb = hexToRgb(foreground);
|
|
110
|
+
const backgroundRgb = hexToRgb(background);
|
|
111
|
+
const foregroundLuminance = relativeLuminanceW3C(foregroundRgb[0], foregroundRgb[1], foregroundRgb[2]);
|
|
112
|
+
const backgroundLuminance = relativeLuminanceW3C(backgroundRgb[0], backgroundRgb[1], backgroundRgb[2]);
|
|
113
|
+
// calculate the color contrast ratio
|
|
114
|
+
var brightest = Math.max(foregroundLuminance, backgroundLuminance);
|
|
115
|
+
var darkest = Math.min(foregroundLuminance, backgroundLuminance);
|
|
116
|
+
return (brightest + 0.05) / (darkest + 0.05);
|
|
117
|
+
}
|
|
118
|
+
export function deltaE(rgbA, rgbB) {
|
|
119
|
+
let labA = rgbToLab(rgbA);
|
|
120
|
+
let labB = rgbToLab(rgbB);
|
|
121
|
+
let deltaL = labA[0] - labB[0];
|
|
122
|
+
let deltaA = labA[1] - labB[1];
|
|
123
|
+
let deltaB = labA[2] - labB[2];
|
|
124
|
+
let c1 = Math.sqrt(labA[1] * labA[1] + labA[2] * labA[2]);
|
|
125
|
+
let c2 = Math.sqrt(labB[1] * labB[1] + labB[2] * labB[2]);
|
|
126
|
+
let deltaC = c1 - c2;
|
|
127
|
+
let deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC;
|
|
128
|
+
deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH);
|
|
129
|
+
let sc = 1.0 + 0.045 * c1;
|
|
130
|
+
let sh = 1.0 + 0.015 * c1;
|
|
131
|
+
let deltaLKlsl = deltaL / 1.0;
|
|
132
|
+
let deltaCkcsc = deltaC / sc;
|
|
133
|
+
let deltaHkhsh = deltaH / sh;
|
|
134
|
+
let i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh;
|
|
135
|
+
return i < 0 ? 0 : Math.sqrt(i);
|
|
136
|
+
}
|
|
137
|
+
function rgbToLab(rgb) {
|
|
138
|
+
let r = rgb[0] / 255,
|
|
139
|
+
g = rgb[1] / 255,
|
|
140
|
+
b = rgb[2] / 255,
|
|
141
|
+
x,
|
|
142
|
+
y,
|
|
143
|
+
z;
|
|
144
|
+
r = r > 0.04045 ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
|
|
145
|
+
g = g > 0.04045 ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
|
|
146
|
+
b = b > 0.04045 ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
|
|
147
|
+
x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
|
|
148
|
+
y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.0;
|
|
149
|
+
z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
|
|
150
|
+
x = x > 0.008856 ? Math.pow(x, 1 / 3) : 7.787 * x + 16 / 116;
|
|
151
|
+
y = y > 0.008856 ? Math.pow(y, 1 / 3) : 7.787 * y + 16 / 116;
|
|
152
|
+
z = z > 0.008856 ? Math.pow(z, 1 / 3) : 7.787 * z + 16 / 116;
|
|
153
|
+
return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
|
|
154
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import tokens from '../artifacts/token-names';
|
|
2
|
+
import { CUSTOM_THEME_ATTRIBUTE, THEME_DATA_ATTRIBUTE } from '../constants';
|
|
3
|
+
import { hash } from './hash';
|
|
4
|
+
export function findMissingCustomStyleElements(UNSAFE_themeOptions, mode) {
|
|
5
|
+
const optionString = JSON.stringify(UNSAFE_themeOptions);
|
|
6
|
+
const uniqueId = hash(optionString);
|
|
7
|
+
const attrOfMissingCustomStyles = [];
|
|
8
|
+
(mode === 'auto' ? ['light', 'dark'] : [mode]).forEach(themeId => {
|
|
9
|
+
const element = document.head.querySelector(`style[${CUSTOM_THEME_ATTRIBUTE}="${uniqueId}"][${THEME_DATA_ATTRIBUTE}="${themeId}"]`);
|
|
10
|
+
if (element) {
|
|
11
|
+
// Append the existing custom styles to take precedence over others
|
|
12
|
+
document.head.appendChild(element);
|
|
13
|
+
} else {
|
|
14
|
+
attrOfMissingCustomStyles.push(themeId);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
return attrOfMissingCustomStyles;
|
|
18
|
+
}
|
|
19
|
+
export function limitSizeOfCustomStyleElements(sizeThreshold) {
|
|
20
|
+
const styleTags = [...document.head.querySelectorAll(`style[${CUSTOM_THEME_ATTRIBUTE}][${THEME_DATA_ATTRIBUTE}]`)];
|
|
21
|
+
if (styleTags.length < sizeThreshold) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
styleTags.slice(0, styleTags.length - (sizeThreshold - 1)).forEach(element => element.remove());
|
|
25
|
+
}
|
|
26
|
+
export function reduceTokenMap(tokenMap, themeRamp) {
|
|
27
|
+
return Object.entries(tokenMap).reduce((acc, [key, value]) => {
|
|
28
|
+
const cssVar = tokens[key];
|
|
29
|
+
return cssVar ? `${acc}\n ${cssVar}: ${themeRamp[value]};` : acc;
|
|
30
|
+
}, '');
|
|
31
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import tokenValuesDark from '../artifacts/atlassian-dark-token-value-for-contrast-check';
|
|
2
|
+
import tokenValuesLight from '../artifacts/atlassian-light-token-value-for-contrast-check';
|
|
3
|
+
import { getContrastRatio } from './color-utils';
|
|
4
|
+
export const additionalChecks = [{
|
|
5
|
+
foreground: 'color.text.brand',
|
|
6
|
+
backgroundLight: 'elevation.surface.sunken',
|
|
7
|
+
backgroundDark: 'elevation.surface.overlay',
|
|
8
|
+
desiredContrast: 4.5,
|
|
9
|
+
updatedTokens: [
|
|
10
|
+
// In light mode: darken the following tokens by one base token
|
|
11
|
+
// In dark mode: lighten the following tokens by one base token
|
|
12
|
+
'color.text.brand', 'color.text.selected', 'color.link', 'color.link.pressed', 'color.icon.brand', 'color.icon.selected']
|
|
13
|
+
}, {
|
|
14
|
+
foreground: 'color.text.selected',
|
|
15
|
+
backgroundLight: 'color.background.selected',
|
|
16
|
+
backgroundDark: 'color.background.selected',
|
|
17
|
+
desiredContrast: 4.5,
|
|
18
|
+
// In light mode: darken the following tokens by one base token
|
|
19
|
+
// In dark mode: lighten the following tokens by one base token
|
|
20
|
+
updatedTokens: ['color.text.selected', 'color.icon.selected']
|
|
21
|
+
}, {
|
|
22
|
+
foreground: 'color.border.brand',
|
|
23
|
+
backgroundLight: 'elevation.surface.sunken',
|
|
24
|
+
backgroundDark: 'elevation.surface.overlay',
|
|
25
|
+
desiredContrast: 3,
|
|
26
|
+
// In light mode: darken the following tokens by one base token
|
|
27
|
+
// In dark mode: lighten the following tokens by one base toke
|
|
28
|
+
updatedTokens: ['color.border.brand', 'color.border.selected']
|
|
29
|
+
}, {
|
|
30
|
+
foreground: 'color.chart.brand',
|
|
31
|
+
backgroundLight: 'elevation.surface.sunken',
|
|
32
|
+
backgroundDark: 'elevation.surface.overlay',
|
|
33
|
+
desiredContrast: 3,
|
|
34
|
+
// In light mode: darken the following tokens by one base token
|
|
35
|
+
// In dark mode: lighten the following tokens by one base token
|
|
36
|
+
updatedTokens: ['color.chart.brand', 'color.chart.brand.hovered']
|
|
37
|
+
}];
|
|
38
|
+
const getColorFromTokenRaw = (tokenName, mode) => {
|
|
39
|
+
return mode === 'light' ? tokenValuesLight[tokenName] : tokenValuesDark[tokenName];
|
|
40
|
+
};
|
|
41
|
+
export const additionalContrastChecker = ({
|
|
42
|
+
customThemeTokenMap,
|
|
43
|
+
mode,
|
|
44
|
+
themeRamp
|
|
45
|
+
}) => {
|
|
46
|
+
const updatedCustomThemeTokenMap = {};
|
|
47
|
+
const brandTokens = Object.keys(customThemeTokenMap);
|
|
48
|
+
additionalChecks.forEach(pairing => {
|
|
49
|
+
const {
|
|
50
|
+
backgroundLight,
|
|
51
|
+
backgroundDark,
|
|
52
|
+
foreground,
|
|
53
|
+
desiredContrast,
|
|
54
|
+
updatedTokens
|
|
55
|
+
} = pairing;
|
|
56
|
+
const background = mode === 'light' ? backgroundLight : backgroundDark;
|
|
57
|
+
const foregroundColor = brandTokens.includes(foreground) ? themeRamp[customThemeTokenMap[foreground]] : getColorFromTokenRaw(foreground, mode);
|
|
58
|
+
const backgroundColor = brandTokens.includes(background) ? themeRamp[customThemeTokenMap[background]] : getColorFromTokenRaw(background, mode);
|
|
59
|
+
const contrast = getContrastRatio(foregroundColor, backgroundColor);
|
|
60
|
+
if (contrast <= desiredContrast) {
|
|
61
|
+
updatedTokens.forEach(token => {
|
|
62
|
+
const rampValue = customThemeTokenMap[token];
|
|
63
|
+
updatedCustomThemeTokenMap[token] = mode === 'light' ? rampValue + 1 : rampValue - 1;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return updatedCustomThemeTokenMap;
|
|
68
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import rawTokensDark from '../artifacts/tokens-raw/atlassian-dark';
|
|
2
|
+
import { deltaE, getContrastRatio, hexToHSL, hexToRgb, hexToRgbA, HSLToRGB, relativeLuminanceW3C, rgbToHex } from './color-utils';
|
|
3
|
+
import { additionalContrastChecker } from './custom-theme-token-contrast-check';
|
|
4
|
+
import { argbFromRgba, Contrast, Hct, rgbaFromArgb } from './hct-color-utils';
|
|
5
|
+
const lowLuminanceContrastRatios = [1.12, 1.33, 2.03, 2.73, 3.33, 4.27, 5.2, 6.62, 12.46, 15.98];
|
|
6
|
+
const highLuminanceContrastRatios = [1.08, 1.24, 1.55, 1.99, 2.45, 3.34, 4.64, 6.1, 10.19, 13.43];
|
|
7
|
+
export const getClosestColorIndex = (themeRamp, brandColor) => {
|
|
8
|
+
// Iterate over themeRamp and find whichever color is closest to brandColor
|
|
9
|
+
let closestColorIndex = 0;
|
|
10
|
+
let closestColorDistance = null;
|
|
11
|
+
themeRamp.forEach((value, index) => {
|
|
12
|
+
const distance = deltaE(hexToRgb(value), hexToRgb(brandColor));
|
|
13
|
+
if (closestColorDistance === null || distance < closestColorDistance) {
|
|
14
|
+
closestColorIndex = index;
|
|
15
|
+
closestColorDistance = distance;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return closestColorIndex;
|
|
19
|
+
};
|
|
20
|
+
export const generateColors = brandColor => {
|
|
21
|
+
// Determine luminance
|
|
22
|
+
const HSLBrandColorHue = hexToHSL(brandColor)[0];
|
|
23
|
+
const baseRgb = HSLToRGB(HSLBrandColorHue, 100, 60);
|
|
24
|
+
const isLowLuminance = relativeLuminanceW3C(baseRgb[0], baseRgb[1], baseRgb[2]) < 0.4;
|
|
25
|
+
// Choose right palette
|
|
26
|
+
const themeRatios = isLowLuminance ? lowLuminanceContrastRatios : highLuminanceContrastRatios;
|
|
27
|
+
const brandRgba = hexToRgbA(brandColor);
|
|
28
|
+
const hctColor = Hct.fromInt(argbFromRgba({
|
|
29
|
+
r: brandRgba[0],
|
|
30
|
+
g: brandRgba[1],
|
|
31
|
+
b: brandRgba[2],
|
|
32
|
+
a: brandRgba[3]
|
|
33
|
+
}));
|
|
34
|
+
const themeRamp = themeRatios.map(contrast => {
|
|
35
|
+
const rgbaColor = rgbaFromArgb(Hct.from(hctColor.hue, hctColor.chroma, Contrast.darker(100, contrast) + 0.25 // Material's utils provide an offset
|
|
36
|
+
).toInt());
|
|
37
|
+
return rgbToHex(rgbaColor.r, rgbaColor.g, rgbaColor.b);
|
|
38
|
+
});
|
|
39
|
+
const closestColorIndex = getClosestColorIndex(themeRamp, brandColor);
|
|
40
|
+
|
|
41
|
+
// Replace closet color with brandColor
|
|
42
|
+
const updatedThemeRamp = [...themeRamp];
|
|
43
|
+
updatedThemeRamp[closestColorIndex] = brandColor;
|
|
44
|
+
return updatedThemeRamp;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Return the interaction tokens for a color, given its ramp position and the number of
|
|
49
|
+
* needed interaction states. Use higher-indexed colors (i.e. darker colors) if possible;
|
|
50
|
+
* if there's not enough room to shift up for the required number of interaction tokens,
|
|
51
|
+
* it goes as far as it can, then returns lighter colors lower down the ramp instead.
|
|
52
|
+
*
|
|
53
|
+
* Returns an array of the resulting colors
|
|
54
|
+
*/
|
|
55
|
+
function getInteractionStates(rampPosition, number, colors) {
|
|
56
|
+
const result = [];
|
|
57
|
+
for (let i = 1; i <= number; i++) {
|
|
58
|
+
if (rampPosition + i < colors.length) {
|
|
59
|
+
result.push(rampPosition + i);
|
|
60
|
+
} else {
|
|
61
|
+
result.push(rampPosition - (i - (colors.length - 1 - rampPosition)));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
export const generateTokenMap = (brandColor, mode, themeRamp) => {
|
|
67
|
+
const colors = themeRamp || generateColors(brandColor);
|
|
68
|
+
const closestColorIndex = getClosestColorIndex(colors, brandColor);
|
|
69
|
+
let customThemeTokenMapLight = {};
|
|
70
|
+
let customThemeTokenMapDark = {};
|
|
71
|
+
const inputContrast = getContrastRatio(brandColor, '#FFFFFF');
|
|
72
|
+
// Branch based on brandColor's contrast against white
|
|
73
|
+
if (inputContrast >= 4.5) {
|
|
74
|
+
/**
|
|
75
|
+
* Generate interaction tokens for
|
|
76
|
+
* - color.background.brand.bold
|
|
77
|
+
* - color.background.selected.bold
|
|
78
|
+
*/
|
|
79
|
+
const [brandBoldSelectedHoveredIndex, brandBoldSelectedPressedIndex] = getInteractionStates(closestColorIndex, 2, colors);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Generate interaction token for color.link:
|
|
83
|
+
* If inputted color replaces X1000
|
|
84
|
+
* - Pressed = X900
|
|
85
|
+
*
|
|
86
|
+
* If inputted color replaces X700-X900
|
|
87
|
+
* - Shift one 1 step darker
|
|
88
|
+
*/
|
|
89
|
+
const [linkPressed] = getInteractionStates(closestColorIndex, 1, colors);
|
|
90
|
+
customThemeTokenMapLight = {
|
|
91
|
+
'color.text.brand': closestColorIndex,
|
|
92
|
+
'color.icon.brand': closestColorIndex,
|
|
93
|
+
'color.background.brand.bold': closestColorIndex,
|
|
94
|
+
'color.background.brand.bold.hovered': brandBoldSelectedHoveredIndex,
|
|
95
|
+
'color.background.brand.bold.pressed': brandBoldSelectedPressedIndex,
|
|
96
|
+
'color.border.brand': closestColorIndex,
|
|
97
|
+
'color.text.selected': closestColorIndex,
|
|
98
|
+
'color.icon.selected': closestColorIndex,
|
|
99
|
+
'color.background.selected.bold': closestColorIndex,
|
|
100
|
+
'color.background.selected.bold.hovered': brandBoldSelectedHoveredIndex,
|
|
101
|
+
'color.background.selected.bold.pressed': brandBoldSelectedPressedIndex,
|
|
102
|
+
'color.border.selected': closestColorIndex,
|
|
103
|
+
'color.link': closestColorIndex,
|
|
104
|
+
'color.link.pressed': linkPressed,
|
|
105
|
+
'color.chart.brand': 5,
|
|
106
|
+
'color.chart.brand.hovered': 6,
|
|
107
|
+
'color.background.selected': 0,
|
|
108
|
+
'color.background.selected.hovered': 1,
|
|
109
|
+
'color.background.selected.pressed': 2
|
|
110
|
+
};
|
|
111
|
+
} else {
|
|
112
|
+
customThemeTokenMapLight = {
|
|
113
|
+
'color.background.brand.bold': 6,
|
|
114
|
+
'color.background.brand.bold.hovered': 7,
|
|
115
|
+
'color.background.brand.bold.pressed': 8,
|
|
116
|
+
'color.border.brand': 6,
|
|
117
|
+
'color.background.selected.bold': 6,
|
|
118
|
+
'color.background.selected.bold.hovered': 7,
|
|
119
|
+
'color.background.selected.bold.pressed': 8,
|
|
120
|
+
'color.text.brand': 6,
|
|
121
|
+
'color.icon.brand': 6,
|
|
122
|
+
'color.chart.brand': 5,
|
|
123
|
+
'color.chart.brand.hovered': 6,
|
|
124
|
+
'color.text.selected': 6,
|
|
125
|
+
'color.icon.selected': 6,
|
|
126
|
+
'color.border.selected': 6,
|
|
127
|
+
'color.background.selected': 0,
|
|
128
|
+
'color.background.selected.hovered': 1,
|
|
129
|
+
'color.background.selected.pressed': 2,
|
|
130
|
+
'color.link': 6,
|
|
131
|
+
'color.link.pressed': 7
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (mode === 'light') {
|
|
135
|
+
return {
|
|
136
|
+
light: customThemeTokenMapLight
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Generate dark mode values using rule of symmetry
|
|
142
|
+
*/
|
|
143
|
+
Object.entries(customThemeTokenMapLight).forEach(([key, value]) => {
|
|
144
|
+
customThemeTokenMapDark[key] = 9 - value;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* If the input brand color < 4.5, and it meets 4.5 contrast again inverse text color
|
|
149
|
+
* in dark mode, shift color.background.brand.bold to the brand color
|
|
150
|
+
*/
|
|
151
|
+
if (inputContrast < 4.5) {
|
|
152
|
+
var _rawTokensDark$find;
|
|
153
|
+
const inverseTextColor = (_rawTokensDark$find = rawTokensDark.find(token => token.cleanName === 'color.text.inverse')) === null || _rawTokensDark$find === void 0 ? void 0 : _rawTokensDark$find.value;
|
|
154
|
+
if (getContrastRatio(inverseTextColor, brandColor) >= 4.5 && closestColorIndex >= 2) {
|
|
155
|
+
customThemeTokenMapDark['color.background.brand.bold'] = closestColorIndex;
|
|
156
|
+
customThemeTokenMapDark['color.background.brand.bold.hovered'] = closestColorIndex - 1;
|
|
157
|
+
customThemeTokenMapDark['color.background.brand.bold.pressed'] = closestColorIndex - 2;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (mode === 'dark') {
|
|
161
|
+
return {
|
|
162
|
+
dark: customThemeTokenMapDark
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
light: customThemeTokenMapLight,
|
|
167
|
+
dark: customThemeTokenMapDark
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
export const generateTokenMapWithContrastCheck = (brandColor, mode, themeRamp) => {
|
|
171
|
+
const colors = themeRamp || generateColors(brandColor);
|
|
172
|
+
const tokenMaps = generateTokenMap(brandColor, mode, colors);
|
|
173
|
+
const result = {};
|
|
174
|
+
Object.entries(tokenMaps).forEach(([mode, map]) => {
|
|
175
|
+
if (mode === 'light' || mode === 'dark') {
|
|
176
|
+
result[mode] = {
|
|
177
|
+
...map,
|
|
178
|
+
...additionalContrastChecker({
|
|
179
|
+
customThemeTokenMap: map,
|
|
180
|
+
mode,
|
|
181
|
+
themeRamp: colors
|
|
182
|
+
})
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
return result;
|
|
187
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const hash = str => {
|
|
2
|
+
let hash = 0;
|
|
3
|
+
for (let i = 0; i < str.length; i++) {
|
|
4
|
+
const char = str.charCodeAt(i);
|
|
5
|
+
hash = (hash << 5) - hash + char;
|
|
6
|
+
hash &= hash; // Convert to 32bit integer
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return new Uint32Array([hash])[0].toString(36);
|
|
10
|
+
};
|