-
Notifications
You must be signed in to change notification settings - Fork 815
Expand file tree
/
Copy pathapp-theme.js
More file actions
279 lines (247 loc) · 9.82 KB
/
app-theme.js
File metadata and controls
279 lines (247 loc) · 9.82 KB
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
import {
accentBaseColor,
baseLayerLuminance,
SwatchRGB,
fillColor,
neutralLayerL2,
neutralPalette,
DesignToken,
neutralFillLayerRestDelta
} from "/_content/Microsoft.FluentUI.AspNetCore.Components/Microsoft.FluentUI.AspNetCore.Components.lib.module.js";
const currentThemeCookieName = "currentTheme";
const themeSettingDark = "Dark";
const themeSettingLight = "Light";
const darkThemeLuminance = 0.19;
const lightThemeLuminance = 1.0;
const darknessLuminanceTarget = (-0.1 + Math.sqrt(0.21)) / 2;
/**
* Updates the current theme on the site based on the specified theme
* @param {string} specifiedTheme
*/
export function updateTheme(specifiedTheme) {
const effectiveTheme = getEffectiveTheme(specifiedTheme);
applyTheme(effectiveTheme);
setThemeCookie(specifiedTheme);
return effectiveTheme;
}
/**
* Returns the value of the currentTheme cookie.
* @returns {string}
*/
export function getThemeCookieValue() {
return getCookieValue(currentThemeCookieName);
}
export function getCurrentTheme() {
return getEffectiveTheme(getThemeCookieValue());
}
/**
* Returns the current system theme (Light or Dark)
* @returns {string}
*/
function getSystemTheme() {
let matched = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (matched) {
return themeSettingDark;
} else {
return themeSettingLight;
}
}
/**
* Sets the currentTheme cookie to the specified value.
* @param {string} theme
*/
function setThemeCookie(theme) {
if (theme == themeSettingDark || theme == themeSettingLight) {
// Cookie will expire after 1 year. Using a much larger value won't have an impact because
// Chrome limits expiration to 400 days: https://developer.chrome.com/blog/cookie-max-age-expires
// The cookie is reset when the dashboard loads to creating a sliding expiration.
document.cookie = `${currentThemeCookieName}=${theme}; Path=/; expires=${new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 365).toGMTString()}`;
} else {
// Delete cookie for other values (e.g. System)
document.cookie = `${currentThemeCookieName}=; Path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;`;
}
}
/**
* Sets the document data-theme attribute to the specified value.
* @param {string} theme The theme to set. Should be Light or Dark.
*/
function setThemeOnDocument(theme) {
if (theme === themeSettingDark) {
document.documentElement.setAttribute('data-theme', 'dark');
} else /* Light */ {
document.documentElement.setAttribute('data-theme', 'light');
}
}
/**
*
* @param {string} theme The theme to use. Should be Light or Dark.
*/
function setBaseLayerLuminance(theme) {
const baseLayerLuminanceValue = getBaseLayerLuminanceForTheme(theme);
baseLayerLuminance.withDefault(baseLayerLuminanceValue);
}
/**
* Returns the value of the specified cookie, or the empty string if the cookie is not present
* @param {string} cookieName
* @returns {string}
*/
function getCookieValue(cookieName) {
const cookiePieces = document.cookie.split(';');
for (let index = 0; index < cookiePieces.length; index++) {
if (cookiePieces[index].trim().startsWith(cookieName)) {
const cookieKeyValue = cookiePieces[index].split('=');
if (cookieKeyValue.length > 1) {
return cookieKeyValue[1];
}
}
}
return "";
}
/**
* Converts a setting value for the theme (Light, Dark, System or null/empty) into the effective theme that should be applied
* @param {string} specifiedTheme The setting value to use to determine the effective theme. Anything other than Light or Dark will be treated as System
* @returns {string} The actual theme to use based on the supplied setting. Will be either Light or Dark.
*/
function getEffectiveTheme(specifiedTheme) {
if (specifiedTheme === themeSettingLight ||
specifiedTheme === themeSettingDark) {
return specifiedTheme;
} else {
return getSystemTheme();
}
}
/**
*
* @param {string} theme The theme to use. Should be Light or Dark
* @returns {string}
*/
function getBaseLayerLuminanceForTheme(theme) {
if (theme === themeSettingDark) {
return darkThemeLuminance;
} else /* Light */ {
return lightThemeLuminance;
}
}
/**
* Configures the accent color palette based on the .NET purple
*/
function setAccentColor() {
// Convert the base color ourselves to avoid pulling in the
// @microsoft/fast-colors library just for one call to parseColorHexRGB
const baseColor = { // #512BD4
r: 0x51 / 255.0,
g: 0x2B / 255.0,
b: 0xD4 / 255.0
};
const accentBase = SwatchRGB.create(baseColor.r, baseColor.g, baseColor.b);
accentBaseColor.withDefault(accentBase);
}
/**
* Configures the default background color to use for the body
*/
function setFillColor() {
// Design specs say we should use --neutral-layer-2 as the fill color
// for the body. Most of the web components use --fill-color as their
// background color, so we need to make sure they get --neutral-layer-2
// when they request --fill-color.
fillColor.setValueFor(document.body, neutralLayerL2);
}
/**
* Applies the Light or Dark theme to the entire site
* @param {string} theme The theme to use. Should be Light or Dark
*/
function applyTheme(theme) {
setBaseLayerLuminance(theme);
setAccentColor();
setFillColor();
setThemeOnDocument(theme);
}
/**
*
* @param {Palette} palette
* @param {number} baseLayerLuminance
* @returns {number}
*/
function neutralLayer1Index(palette, baseLayerLuminance) {
return palette.closestIndexOf(SwatchRGB.create(baseLayerLuminance, baseLayerLuminance, baseLayerLuminance));
}
/**
*
* @param {Palette} palette
* @param {Swatch} reference
* @param {number} baseLayerLuminance
* @param {number} layerDelta
* @param {number} hoverDeltaLight
* @param {number} hoverDeltaDark
* @returns {Swatch}
*/
function neutralLayerHoverAlgorithm(palette, reference, baseLayerLuminance, layerDelta, hoverDeltaLight, hoverDeltaDark) {
const baseIndex = neutralLayer1Index(palette, baseLayerLuminance);
// Determine both the size of the delta (from the value passed in) and the direction (if the current color is dark,
// the hover color will be a lower index (lighter); if the current color is light, the hover color will be a higher index (darker))
const hoverDelta = isDark(reference) ? hoverDeltaDark * -1 : hoverDeltaLight;
return palette.get(baseIndex + (layerDelta * -1) + hoverDelta);
}
/**
*
* @param {Swatch} color
* @returns {boolean}
*/
function isDark(color) {
return color.relativeLuminance <= darknessLuminanceTarget;
}
/**
* Creates additional design tokens that are used to define the hover colors for the neutral layers
* used in the site theme (neutral-layer-1 and neutral-layer-2, specifically). Unlike other -hover
* variants, these are not created by the design system by default so we need to create them ourselves.
* This is a lightly tweaked variant of other hover recipes used in the design system.
*/
function createAdditionalDesignTokens() {
const neutralLayer1HoverLightDelta = DesignToken.create({ name: 'neutral-layer-1-hover-light-delta', cssCustomPropertyName: null }).withDefault(3);
const neutralLayer1HoverDarkDelta = DesignToken.create({ name: 'neutral-layer-1-hover-dark-delta', cssCustomPropertyName: null }).withDefault(2);
const neutralLayer2HoverLightDelta = DesignToken.create({ name: 'neutral-layer-2-hover-light-delta', cssCustomPropertyName: null }).withDefault(2);
const neutralLayer2HoverDarkDelta = DesignToken.create({ name: 'neutral-layer-2-hover-dark-delta', cssCustomPropertyName: null }).withDefault(2);
const neutralLayer1HoverRecipe = DesignToken.create({ name: 'neutral-layer-1-hover-recipe', cssCustomPropertyName: null }).withDefault({
evaluate: (element, reference) =>
neutralLayerHoverAlgorithm(
neutralPalette.getValueFor(element),
reference || fillColor.getValueFor(element),
baseLayerLuminance.getValueFor(element),
0, // No layer delta since this is for neutral-layer-1
neutralLayer1HoverLightDelta.getValueFor(element),
neutralLayer1HoverDarkDelta.getValueFor(element)
),
});
const neutralLayer2HoverRecipe = DesignToken.create({ name: 'neutral-layer-2-hover-recipe', cssCustomPropertyName: null }).withDefault({
evaluate: (element, reference) =>
neutralLayerHoverAlgorithm(
neutralPalette.getValueFor(element),
reference || fillColor.getValueFor(element),
baseLayerLuminance.getValueFor(element),
// Use the same layer delta used by the base recipe to calculate layer 2
neutralFillLayerRestDelta.getValueFor(element),
neutralLayer2HoverLightDelta.getValueFor(element),
neutralLayer2HoverDarkDelta.getValueFor(element)
),
});
// Creates the --neutral-layer-1-hover custom CSS property
DesignToken.create('neutral-layer-1-hover').withDefault((element) =>
neutralLayer1HoverRecipe.getValueFor(element).evaluate(element),
);
// Creates the --neutral-layer-2-hover custom CSS property
DesignToken.create('neutral-layer-2-hover').withDefault((element) =>
neutralLayer2HoverRecipe.getValueFor(element).evaluate(element),
);
}
function initializeTheme() {
const themeCookieValue = getThemeCookieValue();
const effectiveTheme = getEffectiveTheme(themeCookieValue);
applyTheme(effectiveTheme);
// If a theme cookie has been set then set it again on page load.
// This updates the cookie expiration date and creates a sliding expiration.
if (themeCookieValue) {
setThemeCookie(themeCookieValue);
}
}
createAdditionalDesignTokens();
initializeTheme();