UNPKG

@jupyter/web-components

Version:

A component library for building extensions in Jupyter frontends.

180 lines (179 loc) 6.67 kB
// 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.`); } } } }