-
Notifications
You must be signed in to change notification settings - Fork 5
/
applyTheme.ts
196 lines (179 loc) · 4.97 KB
/
applyTheme.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import {
ColorHSL,
hslToRGB,
parseColor,
rgbToHSL
} from '@microsoft/fast-colors';
import { DesignToken } from '@microsoft/fast-foundation';
import {
Palette,
PaletteRGB,
StandardLuminance,
Swatch,
SwatchRGB
} from '../../color';
import {
accentFillHoverDelta,
accentPalette,
baseLayerLuminance,
bodyFont,
neutralPalette,
strokeWidth,
typeRampBaseFontSize
} from '../../design-tokens';
const THEME_NAME_BODY_ATTRIBUTE = 'data-jp-theme-name';
const THEME_MODE_BODY_ATTRIBUTE = 'data-jp-theme-light';
/**
* Flag to initialized only one listener
*/
let isThemeChangeInitialized = false;
/**
* Configures a MutationObserver to watch for Jupyter theme changes and
* applies the current Jupyter theme to the toolkit components.
*/
export function addJupyterLabThemeChangeListener(): void {
if (!isThemeChangeInitialized) {
isThemeChangeInitialized = true;
initThemeChangeListener();
}
}
function initThemeChangeListener(): void {
const addObserver = () => {
const observer = new MutationObserver(() => {
applyCurrentTheme();
});
observer.observe(document.body, {
attributes: true,
attributeFilter: [THEME_NAME_BODY_ATTRIBUTE],
childList: false,
characterData: false
});
applyCurrentTheme();
};
if (document.readyState === 'complete') {
addObserver();
} else {
window.addEventListener('load', addObserver);
}
}
/**
* JupyterLab CSS variable to Design Token converter
*/
interface IConverter<T> {
/**
* Convert the CSS variable value to design token value
*/
converter?: (value: string, isDark: boolean) => T | null;
/**
* Design token to update
*/
token: DesignToken<T>;
}
/**
* Convert a string to an integer.
*
* @param value String to convert
* @returns Extracted integer or null
*/
const intConverter = (value: string): number | null => {
const parsedValue = parseInt(value, 10);
return isNaN(parsedValue) ? null : parsedValue;
};
/**
* Mapping JupyterLab CSS variables to FAST design tokens
*/
const tokenMappings: { [key: string]: IConverter<any> } = {
'--jp-border-width': {
converter: intConverter,
token: strokeWidth
},
'--jp-layout-color1': {
converter: (value: string, isDark: boolean): Palette<Swatch> | null => {
const parsedColor = parseColor(value);
if (parsedColor) {
const hsl = rgbToHSL(parsedColor);
// Neutral luminance should be about 50%
const correctedHSL = ColorHSL.fromObject({
h: hsl.h,
s: hsl.s,
l: 0.5
});
const correctedRGB = hslToRGB(correctedHSL!);
return PaletteRGB.from(
SwatchRGB.create(correctedRGB.r, correctedRGB.g, correctedRGB.b)
);
} else {
return null;
}
},
token: neutralPalette
},
'--jp-brand-color1': {
converter: (value: string, isDark: boolean): Palette<Swatch> | null => {
const parsedColor = parseColor(value);
if (parsedColor) {
const hsl = rgbToHSL(parsedColor);
// Correct luminance to get accent fill closer to brand color 1
const direction = isDark ? 1 : -1;
const correctedHSL = ColorHSL.fromObject({
h: hsl.h,
s: hsl.s,
l:
hsl.l +
(direction * accentFillHoverDelta.getValueFor(document.body)) / 94.0
});
const correctedRGB = hslToRGB(correctedHSL!);
return PaletteRGB.from(
SwatchRGB.create(correctedRGB.r, correctedRGB.g, correctedRGB.b)
);
} else {
return null;
}
},
token: accentPalette
},
'--jp-ui-font-family': {
token: bodyFont
},
'--jp-ui-font-size1': {
token: typeRampBaseFontSize
}
};
/**
* Applies the current Jupyter theme to the toolkit components.
*/
function applyCurrentTheme() {
if (!document.body.getAttribute(THEME_NAME_BODY_ATTRIBUTE)) {
return;
}
// Get all the styles applied to the <body> tag in the webview HTML
// Importantly this includes all the CSS variables associated with the
// current Jupyter theme
const styles = getComputedStyle(document.body);
// Set mode
const isDark =
document.body.getAttribute(THEME_MODE_BODY_ATTRIBUTE) === 'false';
baseLayerLuminance.setValueFor(
document.body,
isDark ? StandardLuminance.DarkMode : StandardLuminance.LightMode
);
for (const jpTokenName in tokenMappings) {
const toolkitTokenName = tokenMappings[jpTokenName];
const value = styles.getPropertyValue(jpTokenName).toString();
if (document.body && value !== '') {
const parsedValue = (toolkitTokenName.converter ?? ((v: string) => v))(
value.trim(),
isDark
);
if (parsedValue !== null) {
toolkitTokenName.token.setValueFor(document.body, parsedValue);
} else {
console.error(
`Fail to parse value '${value}' for '${jpTokenName}' as FAST design token.`
);
}
}
}
}