diff --git a/samples/sampler/.storybook/main.js b/samples/sampler/.storybook/main.js index fc4e5a9732..fdc7360a06 100644 --- a/samples/sampler/.storybook/main.js +++ b/samples/sampler/.storybook/main.js @@ -17,7 +17,8 @@ module.exports = { '@enact/storybook-utils/addons/actions', '@enact/storybook-utils/addons/controls', '@enact/storybook-utils/addons/docs', - '@enact/storybook-utils/addons/toolbars' + '@enact/storybook-utils/addons/toolbars', + '../colors-toolbar/manager.js' ], webpackFinal: async (config, {configType}) => { return webpack(config, configType, __dirname); diff --git a/samples/sampler/.storybook/preview.js b/samples/sampler/.storybook/preview.js index 8dca7e3ef4..0e6431bff0 100644 --- a/samples/sampler/.storybook/preview.js +++ b/samples/sampler/.storybook/preview.js @@ -7,6 +7,28 @@ import {themes} from '@storybook/theming'; import ThemeEnvironment from '../src/ThemeEnvironment'; +// custom decorator imports +import {addDecorator} from '@storybook/react'; +import {platform} from '@enact/webos/platform'; + +import {CustomStoryDecorator} from '../colors-toolbar/CustomStoryDecorator'; + +// if running in webos environment, render a list of color pickers in every story +export const GlobalDecorator = (Story) => { + if (platform.tv) { + return ( +
+ + +
+ ) + } else { + return ; + } +}; + +addDecorator(GlobalDecorator); + // NOTE: Locales taken from strawman. Might need to add more in the future. const locales = { 'local': '', @@ -96,7 +118,13 @@ export const globalTypes = { 'debug aria': getBooleanType('debug aria'), 'debug layout': getBooleanType('debug layout'), 'debug spotlight': getBooleanType('debug spotlight'), - 'debug sprites': getBooleanType('debug sprites') + 'debug sprites': getBooleanType('debug sprites'), + // custom theme globalTypes + 'componentBackgroundColor': 'string', + 'focusBackgroundColor': 'string', + 'popupBackgroundColor': 'string', + 'subtitleTextColor': 'string', + 'textColor': 'string' }; export const decorators = [ThemeEnvironment]; diff --git a/samples/sampler/colors-toolbar/ColorPicker.js b/samples/sampler/colors-toolbar/ColorPicker.js new file mode 100644 index 0000000000..be51f0bb95 --- /dev/null +++ b/samples/sampler/colors-toolbar/ColorPicker.js @@ -0,0 +1,52 @@ +import PropTypes from 'prop-types'; +import React, {useCallback} from 'react'; // eslint-disable-line +import {useGlobals} from '@storybook/api'; + +import { + BACKGROUNDCOLOR_ADDON_ID, + BACKGROUNDCOLOR_DEFAULT_VALUE, + FOCUSBGCOLOR_ADDON_ID, + FOCUSBGCOLOR_DEFAULT_VALUE, + POPUPBGCOLOR_ADDON_ID, + POPUPBGCOLOR_DEFAULT_VALUE, + SUBTEXTCOLOR_DEFAULT_VALUE, + TEXT_ADDON_ID, + TEXT_DEFAULT_VALUE +} from './constants'; + +const ColorPicker = ({colorPickerType}) => { + const [globals, updateGlobals] = useGlobals(); + + const getDefaultColor = () => { + switch (colorPickerType) { + case BACKGROUNDCOLOR_ADDON_ID: + return BACKGROUNDCOLOR_DEFAULT_VALUE; + case FOCUSBGCOLOR_ADDON_ID: + return FOCUSBGCOLOR_DEFAULT_VALUE; + case POPUPBGCOLOR_ADDON_ID: + return POPUPBGCOLOR_DEFAULT_VALUE; + case TEXT_ADDON_ID: + return TEXT_DEFAULT_VALUE; + default: + return SUBTEXTCOLOR_DEFAULT_VALUE; + } + }; + + const handleColorChange = useCallback((ev) => { + updateGlobals({[colorPickerType]: ev.target.value}); + }, [colorPickerType, updateGlobals]); + + return ( + + ); +}; + +ColorPicker.propTypes = { + colorPickerType: PropTypes.string +}; + +export default ColorPicker; diff --git a/samples/sampler/colors-toolbar/CustomStoryDecorator.js b/samples/sampler/colors-toolbar/CustomStoryDecorator.js new file mode 100644 index 0000000000..1caab84baf --- /dev/null +++ b/samples/sampler/colors-toolbar/CustomStoryDecorator.js @@ -0,0 +1,137 @@ +// this component has been added as an alternative of built-in HTML color picker which does not work in webos environment +import Button from '@enact/sandstone/Button'; +import {ColorPicker as SandstoneColorPicker} from '@enact/sandstone/ColorPicker'; +import Scroller from '@enact/sandstone/Scroller'; +import LS2Request from '@enact/webos/LS2Request'; +import {useCallback, useContext} from 'react'; + +import {AppContext} from './constants'; +import {generateStylesheet} from '../utils/generateStylesheet'; + +import css from './CustomStoryDecorator.module.less'; + +const request = new LS2Request(); + +export const CustomStoryDecorator = () => { + const {context, setContext} = useContext(AppContext); + const {componentBackgroundColor, focusBackgroundColor, popupBackgroundColor, subtitleTextColor, textColor} = context; + + const onColorChange = useCallback((color, newColor) => { + // a copy of the context object is created + const newContext = Object.assign({}, context); + // update the color value on the newly created object with what gets received from `event` (handleBackgroundColor, handleFocusBgColor...) + newContext[color] = newColor; + // generate the new stylesheet based on the updated color + newContext.colors = generateStylesheet( + newContext.componentBackgroundColor, + newContext.focusBackgroundColor, + newContext.popupBackgroundColor, + newContext.subtitleTextColor, + newContext.textColor + ); + setContext(newContext); + + request.send({ + service: 'luna://com.webos.service.settings/', + method: 'setSystemSettings', + parameters: { + category: 'customUi', + settings: { + theme: JSON.stringify(newContext) + } + }, + onSuccess: () => { + console.log('setSystemSettings onSuccess'); // eslint-disable-line no-console + } + }); + }, [context, setContext]); + + const handleComponentBgColor = useCallback((ev) => { + onColorChange('componentBackgroundColor', ev); + }, [onColorChange]); + const handleFocusBgColor = useCallback((ev) => { + onColorChange('focusBackgroundColor', ev); + }, [onColorChange]); + const handlePopupBgColor = useCallback((ev) => { + onColorChange('popupBackgroundColor', ev); + }, [onColorChange]); + const handleTextColor = useCallback((ev) => { + onColorChange('textColor', ev); + }, [onColorChange]); + const handleSubTextColor = useCallback((ev) => { + onColorChange('subtitleTextColor', ev); + }, [onColorChange]); + + const handleResetButton = useCallback(() => { + // a copy of the context object is created + const newContext = Object.assign({}, context); + // generate the new stylesheet with default sandstone colors + newContext.colors = generateStylesheet( + newContext.componentBackgroundColor, + newContext.focusBackgroundColor, + newContext.popupBackgroundColor, + newContext.subtitleTextColor, + newContext.textColor + ); + setContext(newContext); + + request.send({ + service: 'luna://com.webos.service.settings/', + method: 'setSystemSettings', + parameters: { + category: 'customUi', + settings: { + theme: JSON.stringify(newContext) + } + }, + onSuccess: () => { + console.log('setSystemSettings onSuccess'); // eslint-disable-line no-console + } + }); + }, []); // eslint-disable-line + + return ( +
+ + + + + + +
+ +
+
+
+ ); +}; diff --git a/samples/sampler/colors-toolbar/CustomStoryDecorator.module.less b/samples/sampler/colors-toolbar/CustomStoryDecorator.module.less new file mode 100644 index 0000000000..37f86185b8 --- /dev/null +++ b/samples/sampler/colors-toolbar/CustomStoryDecorator.module.less @@ -0,0 +1,61 @@ +// CustomStoryDecorator styles + +@import '~@enact/sandstone/styles/mixins.less'; + +// CSS variables needed to override generated custom colors +// Note: These variables need to be updated when corresponding value from sandstone/styles/colors.less is changed +@sand-text-color: #e6e6e6; +@sand-focus-bg-color-rgb: 230, 230, 230; +@sand-overlay-bg-color-rgb: 87, 94, 102; +@sand-text-color-rgb: 230, 230, 230; +@sand-progress-color-rgb: 230, 230, 230; +@sand-component-bg-color: #7d848c; +@sand-component-text-color-rgb: 230, 230, 230; + +.colorPickersBlock { + width: 30%; + height: 65%; + position: absolute; + right: 51px; + bottom: 51px; + background: #2e3239; + border-radius: 30px; + padding: 12px; +} + +.colorPicker { + color: @sand-text-color; + + .focus({ + --sand-focus-bg-color-rgb: @sand-focus-bg-color-rgb; + }) +} + +.colorPopup { + --sand-overlay-bg-color-rgb: @sand-overlay-bg-color-rgb; + --sand-text-color-rgb: @sand-text-color-rgb; +} + +.colorPickerSliders { + .colorSlider { + --sand-focus-bg-color-rgb: @sand-focus-bg-color-rgb; + } +} + +.scrollerColors { + --sand-progress-color-rgb: @sand-progress-color-rgb; +} + +.resetButtonBlock { + border-top: 3px solid #e6e6e6; + padding-top: 9px; +} + +.resetButton { + --sand-component-bg-color: @sand-component-bg-color; + --sand-component-text-color-rgb: @sand-component-text-color-rgb; + + .focus({ + --sand-focus-bg-color-rgb: @sand-focus-bg-color-rgb; + }) +} diff --git a/samples/sampler/colors-toolbar/ToolbarButton.js b/samples/sampler/colors-toolbar/ToolbarButton.js new file mode 100644 index 0000000000..1230c41d43 --- /dev/null +++ b/samples/sampler/colors-toolbar/ToolbarButton.js @@ -0,0 +1,38 @@ +import {IconButton, Icons, TooltipLinkList, WithTooltip} from '@storybook/components'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import ColorPicker from './ColorPicker'; + +const ToolbarButton = React.memo(({active = true, buttonName, colorPickerType, tooltipName}) => { + const tooltipLink = { + center: , + id: colorPickerType, + key: colorPickerType, + left: {tooltipName || colorPickerType}, + title: true + }; + + const tooltip = ; + + return ( + + + {buttonName || colorPickerType} + + + ); +}); + +ToolbarButton.prototypes = { + active: PropTypes.boolean, + buttonName: PropTypes.string, + colorPickerType: PropTypes.string.isRequired, + tooltipName: PropTypes.string +}; + +export default ToolbarButton; diff --git a/samples/sampler/colors-toolbar/constants.js b/samples/sampler/colors-toolbar/constants.js new file mode 100644 index 0000000000..7de2020d6e --- /dev/null +++ b/samples/sampler/colors-toolbar/constants.js @@ -0,0 +1,39 @@ +import {createContext} from 'react'; + +// Object containing the default CSS values of Sandstone used for generating custom theme +// NOTE: These values need to be updated when corresponding value from sandstone/styles/colors.less is changed +export const defaultSandstoneColors = { + componentBackgroundColor: '#7d848c', // equivalent of @sand-component-bg-color - styles/colors.less + focusBackgroundColor: '#e6e6e6', // equivalent of @sand-focus-bg-color-rgb - styles/colors.less + popupBackgroundColor: '#575e66', // equivalent of @sand-overlay-bg-color-rgb - styles/colors.less + subtitleTextColor: '#abaeb3', // equivalent of @sand-text-sub-color - styles/colors.less + textColor: '#e6e6e6' // equivalent of @sand-text-color-rgb - styles/colors.less +}; + +export const customColorsContext = { + activeTheme: 'defaultTheme', + componentBackgroundColor: defaultSandstoneColors.componentBackgroundColor, + focusBackgroundColor: defaultSandstoneColors.focusBackgroundColor, + popupBackgroundColor: defaultSandstoneColors.popupBackgroundColor, + subtitleTextColor: defaultSandstoneColors.subtitleTextColor, + textColor: defaultSandstoneColors.textColor +}; + +export const AppContext = createContext(null); + +export const BACKGROUNDCOLOR_ADDON_ID = "componentBackgroundColor"; +export const BACKGROUNDCOLOR_DEFAULT_VALUE = defaultSandstoneColors.componentBackgroundColor; + +export const FOCUSBGCOLOR_ADDON_ID = "focusBackgroundColor"; +export const FOCUSBGCOLOR_DEFAULT_VALUE = defaultSandstoneColors.focusBackgroundColor; + +export const POPUPBGCOLOR_ADDON_ID = "popupBackgroundColor"; +export const POPUPBGCOLOR_DEFAULT_VALUE = defaultSandstoneColors.popupBackgroundColor; + +export const SUBTEXTCOLOR_ADDON_ID = "subtitleTextColor"; +export const SUBTEXTCOLOR_DEFAULT_VALUE = defaultSandstoneColors.subtitleTextColor; + +export const TEXT_ADDON_ID = "textColor"; +export const TEXT_DEFAULT_VALUE = defaultSandstoneColors.textColor; + +export const TOOLBAR_ADDON_ID = "toolbar-colors"; diff --git a/samples/sampler/colors-toolbar/manager.js b/samples/sampler/colors-toolbar/manager.js new file mode 100644 index 0000000000..6908b78ccd --- /dev/null +++ b/samples/sampler/colors-toolbar/manager.js @@ -0,0 +1,59 @@ +import {platform} from '@enact/webos/platform'; +import {addons, types} from '@storybook/addons'; +import React from 'react'; // eslint-disable-line + +import { + BACKGROUNDCOLOR_ADDON_ID, + FOCUSBGCOLOR_ADDON_ID, + POPUPBGCOLOR_ADDON_ID, + TEXT_ADDON_ID, + SUBTEXTCOLOR_ADDON_ID, + TOOLBAR_ADDON_ID +} from './constants'; +import ToolbarButton from './ToolbarButton'; + +// render colors Globals only when running in non-webos environment +if (!platform.tv) { + addons.register(TOOLBAR_ADDON_ID, () => { + const renderBackgroundColorButton = () => ; + const renderFocusBgColorButton = () => ; + const renderPopupBgColor = () => ; + const renderTextColorButton = () => ; + const renderSubTextColorButton = () => ; + + addons.add(BACKGROUNDCOLOR_ADDON_ID, { + title: BACKGROUNDCOLOR_ADDON_ID, + type: types.TOOL, + match: ({viewMode}) => !!(viewMode && viewMode.match(/^(story|docs)$/)), + render: renderBackgroundColorButton + }); + + addons.add(FOCUSBGCOLOR_ADDON_ID, { + title: FOCUSBGCOLOR_ADDON_ID, + type: types.TOOL, + match: ({viewMode}) => !!(viewMode && viewMode.match(/^(story|docs)$/)), + render: renderFocusBgColorButton + }); + + addons.add(POPUPBGCOLOR_ADDON_ID, { + title: POPUPBGCOLOR_ADDON_ID, + type: types.TOOL, + match: ({viewMode}) => !!(viewMode && viewMode.match(/^(story|docs)$/)), + render: renderPopupBgColor + }); + + addons.add(TEXT_ADDON_ID, { + title: TEXT_ADDON_ID, + type: types.TOOL, + match: ({viewMode}) => !!(viewMode && viewMode.match(/^(story|docs)$/)), + render: renderTextColorButton + }); + + addons.add(SUBTEXTCOLOR_ADDON_ID, { + title: SUBTEXTCOLOR_ADDON_ID, + type: types.TOOL, + match: ({viewMode}) => !!(viewMode && viewMode.match(/^(story|docs)$/)), + render: renderSubTextColorButton + }); + }); +} diff --git a/samples/sampler/npm-shrinkwrap.json b/samples/sampler/npm-shrinkwrap.json index 1d95a35aac..c4635a7012 100644 --- a/samples/sampler/npm-shrinkwrap.json +++ b/samples/sampler/npm-shrinkwrap.json @@ -15,6 +15,7 @@ "@enact/spotlight": "^4.9.0-beta.1", "@enact/storybook-utils": "^5.1.4", "@enact/ui": "^4.9.0-beta.1", + "@enact/webos": "^4.9.0-beta.1", "@storybook/addons": "^6.5.16", "@storybook/builder-webpack5": "^6.5.16", "@storybook/manager-webpack5": "^6.5.16", @@ -62779,6 +62780,17 @@ "warning": "^4.0.3" } }, + "node_modules/@enact/webos": { + "version": "4.9.0-beta.1", + "resolved": "https://registry.npmjs.org/@enact/webos/-/webos-4.9.0-beta.1.tgz", + "integrity": "sha512-L65lJtE43C5UwQNEMwVITOOdOYj+nnTzKwaU/3HLC/tIvaeibbmePipQxgbBCkJacacas/XTKkMQkdMgVApA+A==", + "dependencies": { + "@enact/core": "^4.9.0-beta.1", + "prop-types": "^15.8.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -126287,6 +126299,17 @@ "warning": "^4.0.3" } }, + "@enact/webos": { + "version": "4.9.0-beta.1", + "resolved": "https://registry.npmjs.org/@enact/webos/-/webos-4.9.0-beta.1.tgz", + "integrity": "sha512-L65lJtE43C5UwQNEMwVITOOdOYj+nnTzKwaU/3HLC/tIvaeibbmePipQxgbBCkJacacas/XTKkMQkdMgVApA+A==", + "requires": { + "@enact/core": "^4.9.0-beta.1", + "prop-types": "^15.8.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" + } + }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", diff --git a/samples/sampler/package.json b/samples/sampler/package.json index 79e49c96a8..0ccbc2d0fb 100644 --- a/samples/sampler/package.json +++ b/samples/sampler/package.json @@ -26,6 +26,7 @@ "@enact/spotlight": "^4.9.0-beta.1", "@enact/storybook-utils": "^5.1.4", "@enact/ui": "^4.9.0-beta.1", + "@enact/webos": "^4.9.0-beta.1", "@storybook/addons": "^6.5.16", "@storybook/builder-webpack5": "^6.5.16", "@storybook/manager-webpack5": "^6.5.16", diff --git a/samples/sampler/src/ThemeEnvironment/ThemeEnvironment.js b/samples/sampler/src/ThemeEnvironment/ThemeEnvironment.js index 85b9461aa9..25c0c6709e 100644 --- a/samples/sampler/src/ThemeEnvironment/ThemeEnvironment.js +++ b/samples/sampler/src/ThemeEnvironment/ThemeEnvironment.js @@ -5,9 +5,17 @@ import kind from '@enact/core/kind'; import {Panels, Panel, Header} from '@enact/sandstone/Panels'; import ThemeDecorator from '@enact/sandstone/ThemeDecorator'; import PropTypes from 'prop-types'; +import {useEffect, useState} from 'react'; import css from './ThemeEnvironment.module.less'; +// custom theme imports +import LS2Request from '@enact/webos/LS2Request'; +import {platform} from '@enact/webos/platform'; + +import {AppContext, customColorsContext, defaultSandstoneColors} from '../../colors-toolbar/constants'; +import {generateStylesheet} from '../../utils/generateStylesheet'; + const reloadPage = () => { const {protocol, host, pathname} = window.parent.location; window.parent.location.href = protocol + '//' + host + pathname; @@ -65,23 +73,92 @@ const StorybookDecorator = (story, config = {}) => { classes.debug = true; } + // ---> beginning of custom theme code + const request = new LS2Request(); + const [context, setContext] = useState(customColorsContext); + + useEffect(() => { + if (platform.tv) { + request.send({ + service: 'luna://com.webos.service.settings/', + method: 'getSystemSettings', + parameters: { + category: 'customUi', + keys: ['theme'] + }, + subscribe: true, + onSuccess: (res) => { + // update context with received data from `theme` key + if (res.settings.theme !== '' && res) { + const parsedKeyData = JSON.parse(res.settings.theme); + setContext({...parsedKeyData}); + } + } + }); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const [localColors, setLocalColors] = useState({ + componentBackgroundColor: defaultSandstoneColors.componentBackgroundColor, + focusBackgroundColor: defaultSandstoneColors.focusBackgroundColor, + popupBackgroundColor: defaultSandstoneColors.popupBackgroundColor, + subtitleTextColor: defaultSandstoneColors.subtitleTextColor, + textColor: defaultSandstoneColors.textColor + }); + + const { + componentBackgroundColor, + focusBackgroundColor, + popupBackgroundColor, + subtitleTextColor, + textColor + } = platform.tv ? context : localColors; + + // merge `generatedColors` with `background` image(global type) into the style object + const generatedColors = generateStylesheet(componentBackgroundColor, focusBackgroundColor, popupBackgroundColor, subtitleTextColor, textColor); + const background = {'--sand-env-background': globals.background === 'default' ? '' : globals.background}; + const mergedStyles = {...generatedColors, ...background}; + + // update custom colors when a new color is picked from color picker + useEffect(() => { + setLocalColors({ + componentBackgroundColor: globals.componentBackgroundColor || defaultSandstoneColors.componentBackgroundColor, + focusBackgroundColor: globals.focusBackgroundColor || defaultSandstoneColors.focusBackgroundColor, + popupBackgroundColor: globals.popupBackgroundColor || defaultSandstoneColors.popupBackgroundColor, + subtitleTextColor: globals.subtitleTextColor || defaultSandstoneColors.subtitleTextColor, + textColor: globals.textColor || defaultSandstoneColors.textColor + }); + }, [globals]); + + // some components render on a `floatingLayer` which is a sibling of `Theme`, so we need to apply colors to as well + useEffect(() => { + const floatLayerElement = document.getElementById("floatLayer"); + // Apply the generated styles to the element + for (const property in generatedColors) { + if (generatedColors.hasOwnProperty(property)) { // eslint-disable-line + floatLayerElement.style.setProperty(property, generatedColors[property]); + } + } + }, [generatedColors]); + // <--- end of custom theme code + return ( - - {sample} - + + + {sample} + + ); }; diff --git a/samples/sampler/utils/generateStylesheet.js b/samples/sampler/utils/generateStylesheet.js new file mode 100644 index 0000000000..def9ba708b --- /dev/null +++ b/samples/sampler/utils/generateStylesheet.js @@ -0,0 +1,28 @@ +import {hexToRGB} from './hexToRGB'; + +export const generateStylesheet = (componentBackgroundColor, focusBackgroundColor, popupBackgroundColor, subTextColor, textColor) => { + const textColorRGB = hexToRGB(textColor); + const subTextColorRGB = hexToRGB(subTextColor); + const focusBgColorRGB = hexToRGB(focusBackgroundColor); + const popupBgColorRGB = hexToRGB(popupBackgroundColor); + + // return stylesheet based on received colors + return { + '--sand-text-color-rgb': `${textColorRGB}`, + '--sand-text-sub-color': `${subTextColor}`, + '--sand-component-text-color-rgb': `${textColorRGB}`, + '--sand-component-text-sub-color-rgb': `${subTextColorRGB}`, + '--sand-component-bg-color': `${componentBackgroundColor}`, + '--sand-component-active-indicator-bg-color': `${focusBackgroundColor}`, + '--sand-focus-bg-color-rgb': `${focusBgColorRGB}`, + '--sand-selected-color-rgb': `${textColorRGB}`, + '--sand-disabled-focus-bg-color': `${subTextColor}`, + '--sand-overlay-bg-color-rgb': `${popupBgColorRGB}`, + '--sand-toggle-on-color': `${focusBackgroundColor}`, + '--sand-progress-color-rgb': `${textColorRGB}`, + '--sand-checkbox-color': `${focusBackgroundColor}`, + '--sand-alert-overlay-text-color-rgb': `${textColorRGB}`, + '--sand-alert-overlay-focus-text-color': `${subTextColorRGB}`, + '--sand-alert-overlay-disabled-selected-focus-color': `${focusBackgroundColor}` + }; +}; diff --git a/samples/sampler/utils/hexToRGB.js b/samples/sampler/utils/hexToRGB.js new file mode 100644 index 0000000000..50bd736e16 --- /dev/null +++ b/samples/sampler/utils/hexToRGB.js @@ -0,0 +1,13 @@ +export const hexToRGB = (color) => { + const hexColor = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color); + + if (hexColor) { + const red = parseInt(hexColor[1], 16); + const green = parseInt(hexColor[2], 16); + const blue = parseInt(hexColor[3], 16); + + return `${red}, ${green}, ${blue}`; + } + + return null; +};