UNPKG

@czi-sds/data-viz

Version:
1,415 lines (1,379 loc) 884 kB
"use client"; 'use strict'; var jsxRuntime = require('react/jsx-runtime'); var echarts = require('echarts'); var React = require('react'); var lodash = require('lodash'); var styled = require('@emotion/styled'); var react = require('@emotion/react'); var material = require('@mui/material'); var iconsMaterial = require('@mui/icons-material'); var css$1 = require('@emotion/css'); var Button$2 = require('@mui/material/Button'); var styles = require('@mui/material/styles'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var React__namespace = /*#__PURE__*/_interopNamespace(React); var styled__default = /*#__PURE__*/_interopDefault(styled); var Button$2__default = /*#__PURE__*/_interopDefault(Button$2); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ var __assign$1 = function() { __assign$1 = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign$1.apply(this, arguments); }; function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } function __spreadArray(to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); } function __makeTemplateObject(cooked, raw) { if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } return cooked; } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; var EMPTY_OBJECT$1 = {}; var DEFAULT_ITEM_STYLE = { color: function () { return "rgb(0, 0, 0)"; }, }; function createChartOptions(props) { var axisPointer = props.axisPointer, camera = props.camera, data = props.data, dataZoom = props.dataZoom, emphasis = props.emphasis, encode = props.encode, gridProp = props.grid, _a = props.itemStyle, itemStyle = _a === void 0 ? DEFAULT_ITEM_STYLE : _a, options = props.options, symbolSize = props.symbolSize, _b = props.symbol, symbol = _b === void 0 ? "rect" : _b; var _c = generateDefaultValues(props), defaultAxisPointer = _c.defaultAxisPointer, defaultDataZoom = _c.defaultDataZoom, defaultEmphasis = _c.defaultEmphasis, defaultGrid = _c.defaultGrid, defaultXAxis = _c.defaultXAxis, defaultYAxis = _c.defaultYAxis; var customGrid = typeof gridProp === "function" ? gridProp(defaultGrid) : gridProp; var _d = options || {}, optionsAxisPointer = _d.axisPointer, optionsDataZoom = _d.dataZoom, optionsSeries = _d.series, optionsXAxis = _d.xAxis, optionsYAxis = _d.yAxis, optionsRest = __rest(_d, ["axisPointer", "dataZoom", "series", "xAxis", "yAxis"]); return __assign$1({ animation: false, axisPointer: mergeAxisPointer(defaultAxisPointer, axisPointer, optionsAxisPointer), dataZoom: (camera === null || camera === void 0 ? void 0 : camera.active) ? mergeDataZoom(defaultDataZoom, dataZoom, optionsDataZoom) : [], dataset: { source: data, }, grid: customGrid || defaultGrid, series: [ Object.assign({ emphasis: Object.assign(defaultEmphasis, emphasis), encode: encode, itemStyle: itemStyle, legendHoverLink: false, symbol: symbol, symbolSize: symbolSize, }, optionsSeries ? Array.isArray(optionsSeries) ? optionsSeries[0] : optionsSeries : [], { symbol: symbol, type: "scatter" }), ], xAxis: [ Object.assign(defaultXAxis, optionsXAxis ? Array.isArray(optionsXAxis) ? optionsXAxis[0] : optionsXAxis : {}), ], yAxis: [ Object.assign(defaultYAxis, optionsYAxis ? Array.isArray(optionsYAxis) ? optionsYAxis[0] : optionsYAxis : {}), ] }, optionsRest); } function mergeAxisPointer(defaultAxisPointer, axisPointer, optionsAxisPointer) { var finalAxisPointer = Array.isArray(axisPointer) ? axisPointer // if it's an array, assume that the user has [{...propsToChangeOnX}] OR [ , {...propsToChangeOnY}] OR [{...propsToChangeOnX}, {...propsToChangeOnY}] : [axisPointer, axisPointer]; // else if the user only supplies one AxisPointer object, we assume they want the props to apply to both x and y axisPointer objects var finalOptionsAxisPointer = Array.isArray(optionsAxisPointer) ? optionsAxisPointer : [optionsAxisPointer, optionsAxisPointer]; // merge x axisPointer options var x = __assign$1(__assign$1(__assign$1({}, defaultAxisPointer), finalAxisPointer[0]), finalOptionsAxisPointer === null || finalOptionsAxisPointer === void 0 ? void 0 : finalOptionsAxisPointer[0]); // merge y axisPointer options var y = __assign$1(__assign$1(__assign$1({}, defaultAxisPointer), finalAxisPointer[1]), finalOptionsAxisPointer[1]); return [x, y]; } function mergeDataZoom(defaultDataZoom, dataZoom, optionsDataZoom) { var finalDataZoom = Array.isArray(dataZoom) ? dataZoom // if it's an array, assume that the user has [{...propsToChangeOnX}] OR [ , {...propsToChangeOnY}] OR [{...propsToChangeOnX}, {...propsToChangeOnY}] : [dataZoom, dataZoom]; // else if the user only supplies one dataZoom object, we assume they want the props to apply to both x and y zoom objects var finalOptionsDataZoom = Array.isArray(optionsDataZoom) ? optionsDataZoom : [optionsDataZoom, optionsDataZoom]; // merge x dataZoom options var x = __assign$1(__assign$1(__assign$1({}, defaultDataZoom[0]), finalDataZoom[0]), finalOptionsDataZoom === null || finalOptionsDataZoom === void 0 ? void 0 : finalOptionsDataZoom[0]); // merge y dataZoom options var y = __assign$1(__assign$1(__assign$1({}, defaultDataZoom[1]), finalDataZoom[1]), finalOptionsDataZoom[1]); return [x, y]; } function generateDefaultValues(props) { var camera = props.camera, height = props.height, symbol = props.symbol, width = props.width, xAxisData = props.xAxisData, yAxisData = props.yAxisData; var defaultGrid = { height: "".concat(height, "px"), left: 0, top: 0, // (atarashansky): this is the key change to align x and y axis // labels to fixed spaces width: "".concat(width, "px"), }; var defaultAxisPointer = { label: { show: false }, show: false, triggerOn: "mousemove", }; var defaultXAxis = { axisLabel: { fontSize: 0, rotate: 90 }, axisLine: { show: false, }, axisTick: { show: false, }, boundaryGap: true, data: xAxisData, splitLine: { show: false, }, type: "category", }; var defaultYAxis = { axisLabel: { fontSize: 0 }, axisLine: { show: false, }, axisTick: { show: false, }, boundaryGap: true, data: yAxisData, splitLine: { show: false, }, }; var defaultEmphasis = { itemStyle: { borderColor: symbol === "circle" ? "black" : "white", borderType: "solid", borderWidth: symbol === "circle" ? 2 : 4, opacity: 1, }, scale: false, }; var defaultCamera = { height: camera && camera.height ? camera.height : 20, width: camera && camera.width ? camera.width : 40, }; var defaultDataZoom = [ { // end index of the x axis window endValue: defaultCamera.width - 1, filterMode: "filter", moveOnMouseMove: true, // There's a PR to allow touchpad panning // https://github.com/apache/echarts/pull/19352 moveOnMouseWheel: false, orient: "horizontal", preventDefaultMouseMove: true, // start index of the x axis window startValue: 0, throttle: 0, type: "inside", xAxisIndex: 0, zoomOnMouseWheel: false, }, { // end index of the y axis window endValue: defaultCamera.height - 1, filterMode: "filter", moveOnMouseMove: true, moveOnMouseWheel: true, orient: "vertical", preventDefaultMouseMove: true, // start index of the y axis window startValue: 0, throttle: 0, type: "inside", yAxisIndex: 0, zoomOnMouseWheel: false, }, ]; return { defaultAxisPointer: defaultAxisPointer, defaultDataZoom: defaultDataZoom, defaultEmphasis: defaultEmphasis, defaultGrid: defaultGrid, defaultXAxis: defaultXAxis, defaultYAxis: defaultYAxis, }; } var UPDATE_THROTTLE_MS = 1 * 100; function useUpdateChart(_a) { var axisPointer = _a.axisPointer, camera = _a.camera, chart = _a.chart, data = _a.data, emphasis = _a.emphasis, xAxisData = _a.xAxisData, yAxisData = _a.yAxisData, width = _a.width, height = _a.height, encode = _a.encode, itemStyle = _a.itemStyle, symbol = _a.symbol, symbolSize = _a.symbolSize, grid = _a.grid, options = _a.options, onEvents = _a.onEvents; var throttledUpdateChart = React.useMemo(function () { return lodash.throttle(function () { if (!chart || !data || !xAxisData || !yAxisData) { return; } // (thuang): resize() needs to be called before setOption() to prevent // TypeError: Cannot read properties of undefined (reading 'shouldBePainted') chart.resize(); var chartOptions = createChartOptions({ axisPointer: axisPointer, camera: camera, data: data, emphasis: emphasis, encode: encode, grid: grid, height: height, itemStyle: itemStyle, options: options, symbol: symbol, symbolSize: symbolSize, width: width, xAxisData: xAxisData, yAxisData: yAxisData, }); chart.setOption(chartOptions, { replaceMerge: ["dataZoom", "tooltip"], }); /** * We need to remove old event listeners and bind new ones to * make sure that the event listeners are updated when the props change. */ if (onEvents) { var _loop_1 = function (eventName) { if (Object.prototype.hasOwnProperty.call(onEvents, eventName) && typeof eventName === "string" && typeof onEvents[eventName] === "function") { // Remove old event listener chart.off(eventName); // Add new event listener chart.on(eventName, function (event) { onEvents[eventName](event, chart); }); } }; for (var eventName in onEvents) { _loop_1(eventName); } } }, UPDATE_THROTTLE_MS, // (thuang): Trailing guarantees that the last call to the function will // be executed { trailing: true }); }, [ axisPointer, camera, chart, data, emphasis, xAxisData, yAxisData, width, height, encode, itemStyle, symbol, symbolSize, grid, options, onEvents, ]); React.useEffect(function () { return function () { return throttledUpdateChart.cancel(); }; }, [throttledUpdateChart]); // Update the charts React.useEffect(function () { throttledUpdateChart(); }, [ axisPointer, camera, chart, data, emphasis, xAxisData, yAxisData, throttledUpdateChart, width, height, encode, itemStyle, symbol, symbolSize, grid, options, onEvents, ]); } var ChartContainer = styled__default.default("div")(templateObject_1$1 || (templateObject_1$1 = __makeTemplateObject(["\n ", "\n"], ["\n ", "\n"])), getWidthAndHeight); function getWidthAndHeight(_a) { var width = _a.width, height = _a.height; return "\n width: ".concat(width, "px;\n height: ").concat(height, "px;\n "); } var templateObject_1$1; var HeatmapChart = React.forwardRef(function (props, ref // eslint-disable-next-line sonarjs/cognitive-complexity ) { var axisPointer = props.axisPointer, width = props.width, height = props.height, _a = props.echartsRendererMode, echartsRendererMode = _a === void 0 ? "svg" : _a, camera = props.camera, onEvents = props.onEvents, xAxisData = props.xAxisData, yAxisData = props.yAxisData, data = props.data, encode = props.encode, emphasis = props.emphasis, itemStyle = props.itemStyle, symbol = props.symbol, _b = props.symbolSize, symbolSize = _b === void 0 ? 5 : _b, grid = props.grid, options = props.options, rest = __rest(props, ["axisPointer", "width", "height", "echartsRendererMode", "camera", "onEvents", "xAxisData", "yAxisData", "data", "encode", "emphasis", "itemStyle", "symbol", "symbolSize", "grid", "options"]); // Validate width and height if (!width || !height) { throw Error("Heatmap must have width and height bigger than Zero!"); } // Ref for the chart container var innerRef = React.useRef(null); /** * (thuang): We need both a state and a ref to store the chart instance, so * some hooks can opt out of re-rendering when the chart instance changes. */ var _c = React.useState(null), chart = _c[0], setChart = _c[1]; var chartRef = React.useRef(chart); /** * (thuang): Use this ref to store the onEvents prop to prevent * unnecessary re-renders when the onEvents prop changes. * NOTE: This implies that `onEvents` prop changes alone from the parent * won't re-render the chart */ var onEventsRef = React.useRef(onEvents); /** * (thuang): Use this function to dispose the chart instance for both * the state and the ref. This is to prevent memory leaks. */ var disposeChart = React.useCallback(function () { var _a; (_a = chartRef.current) === null || _a === void 0 ? void 0 : _a.dispose(); chartRef.current = null; setChart(null); }, []); // Function to initialize the chart var initChart = React.useCallback(function () { var onEventsCurrent = onEventsRef.current; var current = innerRef.current; if (!current || chartRef.current || // (thuang): echart's `init()` will throw error if the container has 0 width or height (current === null || current === void 0 ? void 0 : current.getAttribute("height")) === "0" || (current === null || current === void 0 ? void 0 : current.getAttribute("width")) === "0") { return; } // Initialize ECharts instance var rawChart = echarts.init(current, EMPTY_OBJECT$1, { renderer: echartsRendererMode, useDirtyRect: true, }); // Bind events if provided if (onEventsCurrent) { bindEvents(rawChart, onEventsCurrent); } setChart(rawChart); chartRef.current = rawChart; // Cleanup function return function () { disposeChart(); // Unbind events if provided if (onEventsCurrent) { for (var eventName in onEventsCurrent) { if (Object.prototype.hasOwnProperty.call(onEventsCurrent, eventName) && typeof eventName === "string" && typeof onEventsCurrent[eventName] === "function") { rawChart.off(eventName, onEventsCurrent[eventName]); } } } }; }, [echartsRendererMode, disposeChart]); // Initialize charts on component mount React.useEffect(function () { disposeChart(); initChart(); }, [initChart, disposeChart]); // Hook to update chart data and options useUpdateChart({ axisPointer: axisPointer, camera: camera, chart: chart, data: data, emphasis: emphasis, encode: encode, grid: grid, height: height, itemStyle: itemStyle, onEvents: onEvents, options: options, symbol: symbol, symbolSize: symbolSize, width: width, xAxisData: xAxisData, yAxisData: yAxisData, }); // Render the chart container return (jsxRuntime.jsx(ChartContainer, __assign$1({ height: height, width: width, ref: handleRef }, rest))); // Function to bind events to the ECharts instance function bindEvents(instance, events) { function innerBindEvent(eventName, func) { // Ignore invalid event configurations if (typeof eventName === "string" && typeof func === "function") { // Bind event instance.on(eventName, function (param) { func(param, instance); }); } } // Loop through events and bind them for (var eventName in events) { if (Object.prototype.hasOwnProperty.call(events, eventName)) { innerBindEvent(eventName, events[eventName]); } } } // Function to handle the ref of the chart container function handleRef(element) { innerRef.current = element; if (!ref) return; // (thuang): `ref` from `forwardRef` can be a function or a ref object if (typeof ref === "function") { ref(element); } else { ref.current = element; } } }); var index$1 = React.memo(HeatmapChart); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /* eslint-disable sort-keys */ const FontSizeValues = { wide: { body: { xxxxs: 10, xxxs: 11, xxs: 12, xs: 13, s: 14, m: 16, l: 18 }, header: { xxxs: 11, xxs: 12, xs: 13, s: 14, m: 16, l: 18, xl: 22, xxl: 26 }, code: { xs: 13, s: 14 }, caps: { xxs: 12, xxxs: 11, xxxxs: 10 }, tabular: { xxxxs: 10, xxxs: 11, xxs: 12, xs: 13, s: 14 }, title: { s: 32, m: 40, l: 52 } }, narrow: { body: { xxxxs: 10, xxxs: 12, xxs: 12, xs: 13, s: 14, m: 14, l: 16 }, header: { xxxs: 12, xxs: 12, xs: 13, s: 14, m: 14, l: 16, xl: 18, xxl: 22 }, code: { xs: 13, s: 14 }, caps: { xxs: 12, xxxs: 11, xxxxs: 11 }, tabular: { xxxxs: 10, xxxs: 11, xxs: 12, xs: 13, s: 14 }, title: { s: 26, m: 32, l: 40 } } }; const LineHeightValues = { wide: { body: { xxxxs: 14, xxxs: 16, xxs: 18, xs: 20, s: 24, m: 26, l: 28 }, header: { xxxs: 16, xxs: 18, xs: 18, s: 20, m: 22, l: 24, xl: 30, xxl: 34 }, code: { xs: 20, s: 24 }, caps: { xxs: 18, xxxs: 16, xxxxs: 14 }, tabular: { xxxxs: 14, xxxs: 16, xxs: 18, xs: 20, s: 24 }, title: { s: 40, m: 50, l: 64 } }, narrow: { body: { xxxxs: 14, xxxs: 18, xxs: 18, xs: 20, s: 24, m: 24, l: 26 }, header: { xxxs: 18, xxs: 18, xs: 18, s: 20, m: 20, l: 22, xl: 24, xxl: 30 }, code: { xs: 20, s: 24 }, caps: { xxs: 18, xxxs: 16, xxxxs: 16 }, tabular: { xxxxs: 14, xxxs: 16, xxs: 18, xs: 20, s: 24 }, title: { s: 34, m: 40, l: 50 } } }; const LetterSpacingValues = { wide: { body: { xxxxs: "0.06px", xxxs: "0.06px", xxs: "0.06px", xs: "0px", s: "0px", m: "0px", l: "0px" }, header: { xxxs: "0.1px", xxs: "0.1px", xs: "0.08px", s: "0.08px", m: "0px", l: "0px", xl: "0px", xxl: "0px" }, code: { xs: "0px", s: "0px" }, caps: { xxs: "0.4px", xxxs: "0.4px", xxxxs: "0.4px" }, tabular: { xxxxs: "-0.25px", xxxs: "-0.25px", xxs: "-0.25px", xs: "-0.3px", s: "-0.3px" }, title: { s: "0px", m: "0px", l: "0px" } }, narrow: { body: { xxxxs: "0.06px", xxxs: "0.06px", xxs: "0.06px", xs: "0px", s: "0px", m: "0px", l: "0px" }, header: { xxxs: "0.1px", xxs: "0.1px", xs: "0.08px", s: "0.08px", m: "0.08px", l: "0px", xl: "0px", xxl: "0px" }, code: { xs: "0px", s: "0px" }, caps: { xxs: "0.4px", xxxs: "0.4px", xxxxs: "0.4px" }, tabular: { xxxxs: "-0.25px", xxxs: "-0.25px", xxs: "-0.25px", xs: "-0.3px", s: "-0.3px" }, title: { s: "0px", m: "0px", l: "0px" } } }; const TypographyCategories = { body: { weights: ["regular", "medium", "semibold"], sizes: ["l", "m", "s", "xs", "xxs", "xxxs", "xxxxs"] }, header: { weights: ["semibold"], sizes: ["xxl", "xl", "l", "m", "s", "xs", "xxs", "xxxs"] }, code: { weights: ["regular", "medium", "semibold"], sizes: ["s", "xs"] }, caps: { weights: ["semibold"], sizes: ["xxs", "xxxs", "xxxxs"] }, tabular: { weights: ["regular", "medium", "semibold"], sizes: ["s", "xs", "xxs", "xxxs", "xxxxs"] }, title: { weights: ["bold"], sizes: ["s", "m", "l"] }, link: { weights: ["regular", "medium", "semibold"], sizes: ["l", "m", "s", "xs", "xxs", "xxxs", "xxxxs"] } }; const FontWeight = { light: 300, regular: 400, medium: 500, semibold: 600, bold: 700 }; const DEFAULT_FONT_SIZE = 14; const DEFAULT_LINE_HEIGHT = 24; const DEFAULT_LETTER_SPACING = "0px"; function generateTypographyStyle(category, size, weight, isNarrow = false, tabularNums) { const screenType = isNarrow ? "narrow" : "wide"; // Use body values for link category const sourceCategory = category === "link" ? "body" : category; const baseFontSize = FontSizeValues[screenType]?.[sourceCategory]?.[size] || DEFAULT_FONT_SIZE; const fontSize = baseFontSize; const lineHeight = LineHeightValues[screenType]?.[sourceCategory]?.[size] || DEFAULT_LINE_HEIGHT; const letterSpacing = LetterSpacingValues[screenType]?.[sourceCategory]?.[size] || DEFAULT_LETTER_SPACING; const fontWeight = FontWeight[weight] || FontWeight.regular; const baseStyle = { fontSize, fontWeight, letterSpacing, lineHeight: `${lineHeight}px`, textTransform: category === "caps" ? "uppercase" : "none" }; if (category === "tabular" && tabularNums) { baseStyle.fontVariantNumeric = tabularNums; } if (category === "link") { baseStyle.textDecoration = "underline"; } return baseStyle; } const generateCategoryStyles = (category, isNarrow = false, tabularNums) => { const config = TypographyCategories[category]; const categoryStyles = {}; config.weights.forEach((weight) => { categoryStyles[weight] = {}; config.sizes.forEach((size) => { categoryStyles[weight][size] = generateTypographyStyle(category, size, weight, isNarrow, tabularNums); }); }); return categoryStyles; }; const generateTypographyStyles = (isNarrow = false, tabularNums) => { return { body: generateCategoryStyles("body", isNarrow), caps: generateCategoryStyles("caps", isNarrow), code: generateCategoryStyles("code", isNarrow), header: generateCategoryStyles("header", isNarrow), link: generateCategoryStyles("link", isNarrow), tabular: generateCategoryStyles("tabular", isNarrow, tabularNums), title: generateCategoryStyles("title", isNarrow) }; }; const generateTypographyTheme = (fontFamily, fontFamilyCode, tabularNums) => { return { fontFamily: { body: fontFamily, caps: fontFamily, code: fontFamilyCode, header: fontFamily, link: fontFamily, tabular: fontFamily, title: fontFamily }, narrowStyles: generateTypographyStyles(true, tabularNums), wideStyles: generateTypographyStyles(false, tabularNums) }; }; // (masoudmanson): We need to define borders separately for light and dark themes // because the border colors are different for each theme. const BorderThickness = { default: 1, extraThick: 4, thick: 2 }; const createAppThemeBorders = (colors, isDarkMode) => { const createBorder = (color, level, style, thickness) => `${BorderThickness[thickness]}px ${style} ${colors[color][level]}`; return { accent: { default: createBorder("indigo", isDarkMode ? 600 : 500, "solid", "default"), focused: createBorder("indigo", isDarkMode ? 600 : 500, "solid", "default"), hover: createBorder("indigo", isDarkMode ? 700 : 600, "solid", "default"), open: createBorder("indigo", isDarkMode ? 600 : 500, "solid", "default"), pressed: createBorder("indigo", isDarkMode ? 800 : 700, "solid", "default"), selected: createBorder("indigo", isDarkMode ? 600 : 500, "solid", "default") }, base: { default: createBorder("gray", 500, "solid", "default"), disabled: createBorder("gray", 300, "solid", "default"), divider: createBorder("gray", 200, "solid", "default"), dividerOnDark: createBorder("gray", isDarkMode ? 200 : 700, "solid", "default"), dividerInverse: createBorder("gray", 600, "solid", "default"), hover: createBorder("gray", 900, "solid", "default"), inverse: createBorder("gray", 50, "solid", "default"), pressed: createBorder("gray", 900, "solid", "default"), table: createBorder("gray", 300, "solid", "default") }, beta: { default: createBorder("purple", 600, "solid", "default"), extraThick: createBorder("purple", 600, "solid", "extraThick"), thick: createBorder("purple", 600, "solid", "thick") }, info: { default: createBorder("blue", 600, "solid", "default"), extraThick: createBorder("blue", 600, "solid", "extraThick"), thick: createBorder("blue", 600, "solid", "thick") }, link: { dashed: createBorder("gray", 900, "dashed", "default"), solid: createBorder("gray", 900, "solid", "default") }, negative: { default: createBorder("red", 600, "solid", "default"), extraThick: createBorder("red", 600, "solid", "extraThick"), thick: createBorder("red", 600, "solid", "thick") }, neutral: { default: createBorder("gray", 500, "solid", "default"), extraThick: createBorder("gray", 500, "solid", "extraThick"), thick: createBorder("gray", 500, "solid", "thick") }, none: "none", notice: { default: createBorder("yellow", 600, "solid", "default"), extraThick: createBorder("yellow", 600, "solid", "extraThick"), thick: createBorder("yellow", 600, "solid", "thick") }, positive: { default: createBorder("green", 600, "solid", "default"), extraThick: createBorder("green", 600, "solid", "extraThick"), thick: createBorder("green", 600, "solid", "thick") } }; }; /* eslint-disable sort-keys */ const Corners = { rounded: 20, xl: 8, l: 6, m: 4, s: 2, none: 0 }; const IconSizes = { // for use with input icons only (radio and checkbox) xxs: { height: 10, width: 10 }, xs: { height: 12, width: 12 }, s: { height: 16, width: 16 }, input: { height: 16, width: 16 }, l: { height: 24, width: 24 }, xl: { height: 32, width: 32 } }; const Shadows = { l: "0 2px 12px 0 rgba(0,0,0, 0.3)", m: "0 2px 4px 0 rgba(0,0,0, 0.15), 0 2px 10px 0 rgba(0,0,0, 0.15)", none: "none", s: "0 2px 4px 0 rgba(0,0,0, 0.25)" }; const Spaces = { xxxs: 2, xxs: 4, xs: 6, s: 8, m: 12, default: 8, l: 16, xl: 24, xxl: 32, xxxl: 40 }; const SDSLightThemeColors = { blue: { "100": "#edf3fd", "200": "#cce1ff", "300": "#9dc6ff", "400": "#6ca6ff", "500": "#1a6cef", "600": "#0041b9", "700": "#002d90", "800": "#00114a" }, gray: { "100": "#ededed", "200": "#dfdfdf", "300": "#c3c3c3", "400": "#a5a5a5", "50": "#ffffff", "500": "#969696", "600": "#767676", "700": "#3b3b3b", "75": "#f8f8f8", "800": "#1b1b1b", "900": "#000000" }, indigo: { "100": "#f1f0ff", "200": "#dcd9ff", "300": "#beb5ff", "400": "#9d8bff", "500": "#6e4ff9", "600": "#4e18cc", "700": "#330296", "800": "#1d004d" }, green: { "100": "#ebf9ed", "200": "#b9ecc3", "300": "#7fd693", "400": "#50b96d", "500": "#238444", "600": "#105b2b", "700": "#07431d", "800": "#001f00" }, purple: { "100": "#efeafe", "200": "#e4dbfc", "300": "#cbbaf8", "400": "#b296f2", "500": "#8b54e2", "600": "#6526b5", "700": "#490092", "800": "#26004c" }, red: { "100": "#ffe8e6", "200": "#ffd6d2", "300": "#ffafa8", "400": "#ff7e78", "500": "#db2131", "600": "#b80017", "700": "#6f0008", "800": "#340000" }, yellow: { "100": "#fff3db", "200": "#ffdb97", "300": "#ffca5c", "400": "#fab700", "500": "#da9900", "600": "#b07300", "700": "#7c3e00", "800": "#541700" } }; const SDSDarkThemeColors = { blue: { "100": "#0f1d4a", "200": "#0a216e", "300": "#0647b8", "400": "#2876f9", "500": "#5b9aff", "600": "#a2c9ff", "700": "#cde3ff", "800": "#e2eeff" }, gray: { "100": "#333333", "200": "#494949", "300": "#696969", "400": "#949494", "50": "#000000", "500": "#aaaaaa", "600": "#cdcdcd", "700": "#ededed", "75": "#101010", "800": "#fafafa", "900": "#ffffff" }, indigo: { "100": "#1d004d", "200": "#330296", "300": "#521ec9", "400": "#6e4ff9", "500": "#9785ff", "600": "#beb5ff", "700": "#dcd9ff", "800": "#f1f0ff" }, green: { "100": "#082608", "200": "#063617", "300": "#10632e", "400": "#2b8e4b", "500": "#4bae68", "600": "#85d898", "700": "#bcecc5", "800": "#daf4de" }, purple: { "100": "#331252", "200": "#410f70", "300": "#6429b2", "400": "#9261e6", "500": "#aa89ef", "600": "#cebef8", "700": "#e4dcfc", "800": "#f0ebfe" }, red: { "100": "#330603", "200": "#660a12", "300": "#9e1c1c", "400": "#df453c", "500": "#e06257", "600": "#ff988a", "700": "#ffbdb3", "800": "#ffd8d1" }, yellow: { "100": "#361b07", "200": "#52270a", "300": "#965a0b", "400": "#bd8804", "500": "#d9a943", "600": "#e5bc63", "700": "#f5d789", "800": "#ffe6a8" } }; const fontFamily = '"Inter", sans-serif'; const fontFamilyCode = '"IBM Plex Mono", monospace'; const tabularNums = "tabular-nums"; /** * Base app theme for properties shared between light and dark mode. Generally, if a theme * property doesn't deal with colors it belongs here, otherwise it'll have its specific * theme variant defined in `lightAppTheme` or `darkAppTheme`. * * `colors` and `mode` are omitted because they must be defined by the `lightAppTheme` and * `darkAppTheme` objects before use in the `makeThemeOptions` function. */ const sharedAppTheme = { corners: Corners, fontWeights: FontWeight, iconSizes: IconSizes, shadows: Shadows, spacing: Spaces, typography: generateTypographyTheme(fontFamily, fontFamilyCode, tabularNums) }; /** * Create a SDS App Theme with custom colors that follows the SDS color model. */ const makeSdsSemanticAppTheme = (colors, isDarkTheme = false) => ({ ...sharedAppTheme, borders: createAppThemeBorders(colors, isDarkTheme), colors }); const SDSLightAppTheme = makeSdsSemanticAppTheme(SDSLightThemeColors); makeSdsSemanticAppTheme(SDSDarkThemeColors, true); /** * Helper function to select the appropriate theme settings. * * @param theme The theme to choose from. Currently supports a light and dark variant. * @returns The appropriate app theme for the variant. */ const SDSChooseTheme = (theme) => { return SDSLightAppTheme; }; const Breakpoints = { lg: 1024, md: 512, sm: 0 }; /** * Converts a percentage opacity value to its hexadecimal representation * that can be appended to hex color codes. * * @param percentOpacity - Opacity as a percentage (0-100) * @returns Hex string representation of the opacity (e.g., "33" for 20%) * * @example * ```ts * percentToHex(20) // "33" (20% opacity) * percentToHex(58) // "94" (58% opacity) * percentToHex(50) // "80" (50% opacity) * percentToHex(100) // "FF" (100% opacity) * percentToHex(0) // "00" (0% opacity) * ``` */ const percentToHex = (percentOpacity) => { // Clamp the value between 0 and 100 const clampedPercent = Math.max(0, Math.min(100, percentOpacity)); // Convert percentage to 0-255 range const alphaValue = Math.round((clampedPercent / 100) * 255); // Convert to hex and pad with leading zero if needed return alphaValue.toString(16).padStart(2, "0").toLowerCase(); }; /** * Adds opacity to a hex color by appending the hex opacity value. * * @param hexColor - Base hex color (with or without #) * @param percentOpacity - Opacity as a percentage (0-100) * @returns Hex color with opacity appended * * @example * ```ts * addOpacityToHex("#FF0000", 20) // "#FF000033" * addOpacityToHex("FF0000", 58) // "FF000094" * ``` */ const addOpacityToHex = (hexColor, percentOpacity) => { const opacityHex = percentToHex(percentOpacity); return `${hexColor}${opacityHex}`; }; const SDSPaletteLight = (appTheme) => { return { accent: { border: appTheme.colors.indigo[500], borderFocus: appTheme.colors.indigo[500], borderHover: appTheme.colors.indigo[600], borderOpen: appTheme.colors.indigo[500], borderPressed: appTheme.colors.indigo[700], borderSelected: appTheme.colors.indigo[500], fillHover: appTheme.colors.indigo[600], fillPressed: appTheme.colors.indigo[700], fillPrimary: appTheme.colors.indigo[500], fillInteraction: appTheme.colors.indigo[600], foreground: appTheme.colors.indigo[500], foregroundOnDark: appTheme.colors.indigo[400], foregroundInteraction: appTheme.colors.indigo[600], foregroundInteractionOnDark: appTheme.colors.indigo[300], foregroundPressed: appTheme.colors.indigo[700], foregroundPressedOnDark: appTheme.colors.indigo[200], foregroundActive: appTheme.colors.indigo[500], ornament: appTheme.colors.indigo[500], ornamentFocus: appTheme.colors.indigo[500], ornamentHover: appTheme.colors.indigo[600], ornamentOpen: appTheme.colors.indigo[500], ornamentPressed: appTheme.colors.indigo[700], ornamentSelected: appTheme.colors.indigo[500], surfacePrimary: appTheme.colors.indigo[500], surfaceSecondary: appTheme.colors.indigo[100], surfaceSecondaryOnDark: appTheme.colors.indigo[800], textAction: appTheme.colors.indigo[500], textActionHover: appTheme.colors.indigo[600], textActionPressed: appTheme.colors.indigo[700] }, base: { backgroundPrimary: appTheme.colors.gray[50], backgroundPrimaryDark: appTheme.colors.gray[900], backgroundPrimaryInverse: appTheme.colors.gray[900], backgroundSecondary: addOpacityToHex(appTheme.colors.gray[100], 40), backgroundSecondaryDark: addOpacityToHex(appTheme.colors.gray[700], 62), backgroundSecondaryInverse: appTheme.colors.gray[700], backgroundTertiary: appTheme.colors.gray[100], borderOnFill: appTheme.colors.gray[50], borderPrimary: appTheme.colors.gray[600], borderPrimaryDisabled: appTheme.colors.gray[300], borderPrimaryDisabledOnDark: appTheme.colors.gray[600], borderPrimaryDisabledInverse: appTheme.colors.gray[600], borderPrimaryInteraction: appTheme.colors.gray[900], borderPrimaryInteractionOnDark: appTheme.colors.gray[50], borderPrimaryHoverInverse: appTheme.colors.gray[50], borderPrimaryOnDark: appTheme.colors.gray[300], borderPrimaryInverse: appTheme.colors.gray[300], borderPrimaryPressedInverse: appTheme.colors.gray[50], borderSecondary: appTheme.colors.gray[300], borderSecondaryOnDark: appTheme.colors.gray[600], divider: appTheme.colors.gray[200], dividerOnDark: appTheme.colors.gray[700], dividerInverse: appTheme.colors.gray[700], fillDisabled: appTheme.colors.gray[100], fillPrimaryInteraction: addOpacityToHex(appTheme.colors.gray[300], 20), fillPrimaryInteractionOnDark: addOpacityToHex(appTheme.colors.gray[300], 20), fillHoverInverse: addOpacityToHex(appTheme.colors.gray[300], 20), fillOpenInverse: addOpacityToHex(appTheme.colors.gray[300], 34), fillPrimaryPressed: addOpacityToHex(appTheme.colors.gray[300], 30), fillPrimaryPressedOnDark: addOpacityToHex(appTheme.colors.gray[300], 24), fillPressedInverse: addOpacityToHex(appTheme.colors.gray[300], 34), fillPrimary: appTheme.colors.gray[50], fillSelected: appTheme.colors.gray[900], fillSecondary: appTheme.colors.gray[200], fillSecondaryInteraction: addOpacityToHex(appTheme.colors.gray[400], 34), fillSecondaryPressed: addOpacityToHex(appTheme.colors.gray[400], 60), fillSecondaryOnDark: appTheme.colors.gray[700], fillSecondaryInteractionOnDark: addOpacityToHex(appTheme.colors.gray[400], 32), fillSecondaryPressedOnDark: addOpacityToHex(appTheme.colors.gray[400], 46), fillDisabledOnDark: appTheme.colors.gray[700], ornamentDisabled: appTheme.colors.gray[300], ornamentDisabledOnDark: appTheme.colors.gray[600], ornamentDisabledInverse: appTheme.colors.gray[600], ornamentOnFill: appTheme.colors.gray[50], ornamentPrimary: appTheme.colors.gray[900], ornamentPrimaryOnDark: appTheme.colors.gray[50], ornamentPrimaryInverse: appTheme.colors.gray[50], ornamentSecondary: appTheme.colors.gray[600], ornamentSecondaryInteraction: appTheme.colors.gray[900], ornamentSecondaryInteractionOnDark: appTheme.colors.gray[50], ornamentSecondaryHoverInverse: appTheme.colors.gray[50], ornamentSecondaryOnDark: appTheme.colors.gray[300], ornamentSecondaryInverse: appTheme.colors.gray[300], ornamentSecondaryPressedInverse: appTheme.colors.gray[50], surfacePrimary: appTheme.colors.gray[50], surfaceSecondary: addOpacityToHex(appTheme.colors.gray[300], 20), surfacePrimaryDark: appTheme.colors.gray[900], surfaceInverse: appTheme.colors.gray[900], textDisabled: appTheme.colors.gray[300], textDisabledOnDark: appTheme.colors.gray[600], textDisabledInverse: appTheme.colors.gray[600], textOnFill: appTheme.colors.gray[50], textPrimary: appTheme.colors.gray[900], textPrimaryOnDark: appTheme.colors.gray[50], textPrimaryInverse: appTheme.colors.gray[50], textSecondary: appTheme.colors.gray[600], textSecondaryOnDark: appTheme.colors.gray[300], textSecondaryInverse: appTheme.colors.gray[300], textTertiary: appTheme.colors.gray[400], textTertiaryOnDark: appTheme.colors.gray[500], textTertiaryInverse: appTheme.colors.gray[500] }, beta: { border: appTheme.colors.purple[600], fillHover: appTheme.colors.purple[600], fillPressed: appTheme.colors.purple[700], fillPrimary: appTheme.colors.purple[500], fillSecondary: appTheme.colors.purple[100], ornament: appTheme.colors.purple[600], surfacePrimary: appTheme.colors.purple[500], surfaceSecondary: appTheme.colors.purple[100], text: appTheme.colors.purple[600], fillInteraction: appTheme.colors.purple[600], foreground: appTheme.colors.purple[600], foregroundOnDark: appTheme.colors.purple[300], foregroundInteraction: appTheme.colors.purple[700], foregroundInteractionOnDark: appTheme.colors.purple[200], foregroundPressed: appTheme.colors.purple[800], foregroundPressedOnDark: appTheme.colors.purple[100] }, info: { border: appTheme.colors.blue[600], fillHover: appTheme.colors.blue[600], fillPressed: appTheme.colors.blue[700], fillPrimary: appTheme.colors.blue[500], fillSecondary: appTheme.colors.blue[100], ornament: appTheme.colors.blue[600], surfacePrimary: appTheme.colors.blue[500], surfaceSecondary: appTheme.colors.blue[100], text: appTheme.colors.blue[600], fillInteraction: appTheme.colors.blue[600], foreground: appTheme.colors.blue[600], foregroundOnDark: appTheme.colors.blue[300], foregroundInteraction: appTheme.colors.blue[700], foregroundInteractionOnDark: appTheme.colors.blue[200], foregroundPressed: appTheme.colors.blue[800], foregroundPressedOnDark: appTheme.colors.blue[100] }, negative: { border: appTheme.colors.red[600], fillHover: appTheme.colors.red[600], fillPressed: appTheme.colors.red[700], fillPrimary: appTheme.colors.red[500], fillSecondary: appTheme.colors.red[100], ornament: appTheme.colors.red[600], surfacePrimary: appTheme.colors.red[500], surfaceSecondary: appTheme.colors.red[100], text: appTheme.colors.red[600], fillInteraction: appTheme.colors.red[600], foreground: appTheme.colors.red[600], foregroundOnDark: appTheme.colors.red[300], foregroundInteraction: appTheme.colors.red[700], foregroundInteractionOnDark: appTheme.colors.red[200], foregroundPressed: appTheme.colors.red[800], foregroundPressedOnDark: appTheme.colors.red[100] }, neutral: { border: appTheme.colors.gray[700], fillHover: appTheme.colors.gray[700], fillPressed: appTheme.colors.gray[800], fillPrimary: appTheme.colors.gray[600], fillSecondary: appTheme.colors.gray[200], ornament: appTheme.colors.gray[700], surfacePrimary: appTheme.colors.gray[600], surfaceSecondary: appTheme.colors.gray[200],