@jupyter/web-components
Version:
A component library for building extensions in Jupyter frontends.
180 lines (179 loc) • 6.67 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { ColorHSL, hslToRGB, parseColor, rgbToHSL } from '@microsoft/fast-colors';
import { SwatchRGB } from '../../color/swatch.js';
import { StandardLuminance } from '../../color/utilities/base-layer-luminance.js';
import { isDark } from '../../color/utilities/is-dark.js';
import { accentColor, accentFillHoverDelta, baseLayerLuminance, bodyFont, controlCornerRadius, errorColor, neutralColor, strokeWidth, typeRampBaseFontSize } from '../../design-tokens.js';
const THEME_NAME_BODY_ATTRIBUTE = 'data-jp-theme-name';
const THEME_MODE_BODY_ATTRIBUTE = 'data-jp-theme-light';
// Use to determine the neutral color and possibly if theme is dark
const BASE_LAYOUT_COLOR = '--jp-layout-color1';
/**
* 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() {
if (!isThemeChangeInitialized) {
isThemeChangeInitialized = true;
initThemeChangeListener();
}
}
function initThemeChangeListener() {
const addObserver = () => {
const observer = new MutationObserver(() => {
applyJupyterTheme();
});
observer.observe(document.body, {
attributes: true,
attributeFilter: [THEME_NAME_BODY_ATTRIBUTE],
childList: false,
characterData: false
});
applyJupyterTheme();
};
if (document.readyState === 'complete') {
addObserver();
}
else {
window.addEventListener('load', addObserver);
}
}
/**
* Convert a string to an integer.
*
* @param value String to convert
* @returns Extracted integer or null
*/
const intConverter = (value) => {
const parsedValue = parseInt(value, 10);
return isNaN(parsedValue) ? null : parsedValue;
};
/**
* Mapping JupyterLab CSS variables to FAST design tokens
*/
const tokenMappings = {
'--jp-border-width': {
converter: intConverter,
token: strokeWidth
},
'--jp-border-radius': {
converter: intConverter,
token: controlCornerRadius
},
[BASE_LAYOUT_COLOR]: {
converter: (value, isDark) => {
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 SwatchRGB.create(correctedRGB.r, correctedRGB.g, correctedRGB.b);
}
else {
return null;
}
},
token: neutralColor
},
'--jp-brand-color1': {
converter: (value, isDark) => {
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 SwatchRGB.create(correctedRGB.r, correctedRGB.g, correctedRGB.b);
}
else {
return null;
}
},
token: accentColor
},
'--jp-error-color1': {
converter: (value, isDark) => {
const parsedColor = parseColor(value);
if (parsedColor) {
const hsl = rgbToHSL(parsedColor);
// Correct luminance to get error fill closer
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 SwatchRGB.create(correctedRGB.r, correctedRGB.g, correctedRGB.b);
}
else {
return null;
}
},
token: errorColor
},
'--jp-ui-font-family': {
token: bodyFont
},
'--jp-ui-font-size1': {
token: typeRampBaseFontSize
}
};
/**
* Applies the current Jupyter theme to the toolkit components.
*/
export function applyJupyterTheme() {
var _a;
// 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
// It will look at the body attribute or try to extrapolate
const themeMode = document.body.getAttribute(THEME_MODE_BODY_ATTRIBUTE);
let isDark_ = false;
if (themeMode) {
isDark_ = themeMode === 'false';
}
else {
const layoutColor = styles.getPropertyValue(BASE_LAYOUT_COLOR).toString();
if (layoutColor) {
const parsedColor = parseColor(layoutColor);
if (parsedColor) {
isDark_ = isDark(SwatchRGB.create(parsedColor.r, parsedColor.g, parsedColor.b));
console.debug(`Theme is ${isDark_ ? 'dark' : 'light'} based on '${BASE_LAYOUT_COLOR}' value: ${layoutColor}.`);
}
}
}
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 = ((_a = toolkitTokenName.converter) !== null && _a !== void 0 ? _a : ((v) => 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.`);
}
}
}
}