From 5f43e68f166912cbbddc5deff1e47168e80d9672 Mon Sep 17 00:00:00 2001 From: Ice Lam Date: Mon, 31 Aug 2020 02:28:21 +0800 Subject: [PATCH] feat: add color input box --- .eslintrc | 3 +- src/main.ts | 13 +- src/preload.ts | 22 +++- src/renderer/app.ts | 122 ++++++++++++++++- src/renderer/assets/images/icons/arrow.svg | 11 ++ src/renderer/assets/scss/_colors.scss | 20 +++ .../components/Button/button.styles.ts | 2 +- src/renderer/components/Colors/ColorInput.ts | 123 ++++++++++++++++++ .../components/Colors/ColorRandomize.ts | 26 ++-- src/renderer/components/Input/InputText.ts | 63 +++++++++ src/renderer/components/Input/input.styles.ts | 76 +++++++++++ .../components/Tooltip/tooltip.styles.ts | 2 +- src/renderer/pages/TintsShadesGenerator.ts | 32 ++++- src/storage/storage.ts | 49 ++++++- src/types.ts | 7 + src/utils/color.ts | 44 +++++-- 16 files changed, 579 insertions(+), 36 deletions(-) create mode 100644 src/renderer/assets/images/icons/arrow.svg create mode 100644 src/renderer/components/Colors/ColorInput.ts create mode 100644 src/renderer/components/Input/InputText.ts create mode 100644 src/renderer/components/Input/input.styles.ts diff --git a/.eslintrc b/.eslintrc index 6c365b3..2b942bb 100644 --- a/.eslintrc +++ b/.eslintrc @@ -63,7 +63,8 @@ "devDependencies": true, "optionalDependencies": false, "peerDependencies": false - }] + }], + "no-unused-expressions": ["off"] }, "overrides": [ { diff --git a/src/main.ts b/src/main.ts index 6741ca7..2b576eb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,7 +5,12 @@ import { } from 'electron'; import { debounce, listenToSystemThemeChange, getUserPreferedTheme } from '@utils'; import { - getWindowLocation, saveWindowPosition, getWindowPinStatus, saveWindowPinStatus, saveSelectedColor + getWindowLocation, + saveWindowPosition, + getWindowPinStatus, + saveWindowPinStatus, + saveSelectedColor, + saveColorInputMode } from '@storage'; import { applicationMenu, settingMenu } from '@menus'; import { @@ -16,7 +21,7 @@ import { MAIN_WINDOW_WIDTH, MAIN_WINDOW_HEIGHT } from '@constants'; -import { Position, AppThemeOptions } from '@types'; +import { Position, AppThemeOptions, ColorInputMode } from '@types'; let mainWindow: BrowserWindow; @@ -127,6 +132,10 @@ ipcMain.on('SAVE_SELECTED_COLOR', (_, value: string) => { saveSelectedColor(value); }); +ipcMain.on('SAVE_COLOR_INPUT_MODE', (_, value: ColorInputMode) => { + saveColorInputMode(value); +}); + ipcMain.on('COPY_COLOR_TO_CLIPBOARD', (_, value: string) => { clipboard.writeText(value); }); diff --git a/src/preload.ts b/src/preload.ts index d50b11d..50e569c 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -1,7 +1,8 @@ import { ipcRenderer } from 'electron'; import { getUserPreferedTheme, getOsTheme } from '@utils/getThemes'; -import { getWindowPinStatus, getSelectedColor } from '@storage'; +import { getWindowPinStatus, getSelectedColor, getColorInputMode } from '@storage'; import { AppTheme } from '@types'; +import { convertColorHexToRgbString } from '@utils/color'; const setInitialAppTheme = async () => { const userPreference = await getUserPreferedTheme(); @@ -25,12 +26,27 @@ const restoreWindowPinStatus = async (): Promise => { window.document.getElementById('app')?.setAttribute('shouldPinFrame', isPinned ? 'true' : 'false'); }; -const restoreLastSelectedColor = async (): Promise => { +const restoreSelectedColorAndInputMode = async (): Promise => { + const { inputMode } = await getColorInputMode(); + if (inputMode) { + window.document.getElementById('app')?.setAttribute('colorInputMode', inputMode); + } + const { selectedColor } = await getSelectedColor(); if (selectedColor) { window.document.getElementById('app')?.setAttribute('selectedColor', selectedColor); + + // Format input value according to input mode + let inputValue = ''; + if (inputMode === 'rgb') { + inputValue = convertColorHexToRgbString(selectedColor); + } else { + inputValue = selectedColor.replace('#', ''); + } + + inputValue && window.document.getElementById('app')?.setAttribute('colorInputValue', inputValue); } }; restoreWindowPinStatus(); -restoreLastSelectedColor(); +restoreSelectedColorAndInputMode(); diff --git a/src/renderer/app.ts b/src/renderer/app.ts index 5dad371..afc62c1 100644 --- a/src/renderer/app.ts +++ b/src/renderer/app.ts @@ -3,7 +3,15 @@ import { } from 'lit-element'; import '@components/Header/FrameHeader'; import '@pages/TintsShadesGenerator'; -import { randomHexColor } from '@utils/color'; +import { + randomHexColor, + isValidHexColor, + isValidRgbColor, + convertColorRgbToHex, + convertColorHexToRgbString +} from '@utils/color'; +import debounce from '@utils/debounce'; +import { ColorInputMode } from '@types'; /** * Enrty point of the app @@ -36,10 +44,25 @@ class GeneratorApp extends LitElement { shouldPinFrame = false; /** - * Current selectedColor from user + * Current color selected by user */ @property({ type: String }) selectedColor = '#46beb9'; + /** + * Current color value input by user + */ + @property({ type: String }) colorInputValue = '46beb9'; + + /** + * Current input mode of color input text box + */ + @property({ type: String }) colorInputMode: ColorInputMode = 'hex'; + + /** + * Indicates if value of color input text box has error + */ + @property({ type: Boolean }) colorInputHasError = false; + render(): TemplateResult { return html` `; } + /** + * Pass signal to main process to close application window + */ private closeFrame(): void { window.ipcRenderer.send('QUIT_APP'); } + /** + * Pass signal to main process to open setting menu + */ private openSettingMenu(event: MouseEvent): void { window.ipcRenderer.send('OPEN_SETTING_MENU', { x: event.clientX + 10, @@ -71,34 +106,117 @@ class GeneratorApp extends LitElement { }); } + /** + * Pass signal to main process to minimize application window + */ private minimizeFrame(): void { window.ipcRenderer.send('MINIMIZE_APP'); } + /** + * Pass signal to main process to set application window on top + */ private pinFrame(): void { const newPinState = !this.shouldPinFrame; this.shouldPinFrame = newPinState; window.ipcRenderer.send('PIN_APP', newPinState); } + /** + * Handle change of colour through color picker + */ private onColorPickerChange(event: Event): void { const newColor = (event.target as HTMLInputElement).value; this.selectedColor = newColor; + + // Format input value according to input mode + let inputValue = ''; + if (this.colorInputMode === 'rgb') { + inputValue = convertColorHexToRgbString(newColor); + } else { + inputValue = newColor.replace('#', ''); + } + + this.colorInputValue = inputValue; window.ipcRenderer.send('SAVE_SELECTED_COLOR', newColor); } + /** + * Generate a random color + */ private onRandomizeColor(): void { const newColor = randomHexColor() ?? this.selectedColor; this.selectedColor = newColor; window.ipcRenderer.send('SAVE_SELECTED_COLOR', newColor); } + /** + * Pass signal to main process to copy specified color to clipboard + */ private copyColorToClipboard(event: MouseEvent): void { const colorToCopy = (event?.target as HTMLButtonElement)?.value; if (colorToCopy) { window.ipcRenderer.send('COPY_COLOR_TO_CLIPBOARD', colorToCopy); } } + + /** + * Handle change of colour through input box + */ + private onColorInputChange(value: string): void { + const isCurrentlyInHexMode = this.colorInputMode === 'hex'; + const isColorValid = isCurrentlyInHexMode + ? isValidHexColor + : isValidRgbColor; + + const inputHasError = !isColorValid(value); + this.colorInputHasError = inputHasError; + this.colorInputValue = value; + + if (!inputHasError) { + this.selectedColor = isCurrentlyInHexMode + ? `#${value}` + : convertColorRgbToHex(value) as string; + } + } + + /** + * Change input mode from hex to rgb, or rgb to hex and apply formatting to input value + * Input mode will be saved to storage and restore t next app launch + */ + private onColorInputModeClick(): void { + const isCurrentlyInHexMode = this.colorInputMode === 'hex'; + const newColorInputMode = isCurrentlyInHexMode ? 'rgb' : 'hex'; + this.colorInputMode = newColorInputMode; + + // Convert input value to desired format + const isColorValid = isCurrentlyInHexMode + ? isValidHexColor + : isValidRgbColor; + + if (!isColorValid(this.colorInputValue)) { + this.colorInputValue = ''; + } else if (isCurrentlyInHexMode) { + this.colorInputValue = convertColorHexToRgbString(this.colorInputValue); + } else { + const hexValue = convertColorRgbToHex(this.colorInputValue) as string; + this.colorInputValue = hexValue.replace('#', ''); // TODO: make a reusable utils for removing # + } + + window.ipcRenderer.send('SAVE_COLOR_INPUT_MODE', newColorInputMode); + } + + /** + * Prevent input of unwanted characters to color input field + */ + private onColorInputKeypress(event: KeyboardEvent): void { + const ALLOWED_CHARS = this.colorInputMode === 'hex' + ? /[0-9A-Fa-f]+/ + : /[0-9, ]+/; + if (!ALLOWED_CHARS.test(event.key)) { + event.preventDefault(); + } + } } declare global { diff --git a/src/renderer/assets/images/icons/arrow.svg b/src/renderer/assets/images/icons/arrow.svg new file mode 100644 index 0000000..381217b --- /dev/null +++ b/src/renderer/assets/images/icons/arrow.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/src/renderer/assets/scss/_colors.scss b/src/renderer/assets/scss/_colors.scss index 2135db3..0e463ab 100644 --- a/src/renderer/assets/scss/_colors.scss +++ b/src/renderer/assets/scss/_colors.scss @@ -40,12 +40,22 @@ --color-frame-header-icon-hover: var(--color-grey-40); --color-input-border: var(--color-grey-91); --color-body-icon: var(--color-grey-54); + --color-body-icon-hover: var(--color-grey-40); --color-tooltip-background: var(--color-grey-40); --color-tooltip-text: var(--color-white); --color-color-steps-border: var(--color-grey-91); --color-copy-button-background: var(--color-white); --color-copy-button-text: var(--color-grey-54); --color-copy-button-shadow: var(--color-light-shadow); + --color-input-background: var(--color-white); + --color-input-text: var(--color-grey-40); + --color-input-placeholder: var(--color-grey-67); + --color-input-border: var(--color-grey-91); + --color-input-border-error: var(--color-light-red); + --color-input-label-background: var(--color-grey-96); + --color-input-label-text: var(--color-grey-40); + --color-input-mode-icon: var(--color-grey-67); + --color-input-mode-icon-hover: var(--color-grey-54); } [data-theme^='dark'] { @@ -61,10 +71,20 @@ --color-frame-header-icon-hover: var(--color-grey-60); --color-input-border: var(--color-grey-28); --color-body-icon: var(--color-grey-40); + --color-body-icon-hover: var(--color-grey-60); --color-tooltip-background: var(--color-grey-28); --color-tooltip-text: var(--color-grey-68); --color-color-steps-border: var(--color-grey-28); --color-copy-button-background: var(--color-grey-28); --color-copy-button-text: var(--color-grey-54); --color-copy-button-shadow: var(--color-dark-shadow); + --color-input-background: var(--color--grey-18); + --color-input-text: var(--color-grey-68); + --color-input-placeholder: var(--color-grey-28); + --color-input-border: var(--color-grey-28); + --color-input-border-error: var(--color-light-red); + --color-input-label-background: var(--color-grey-23); + --color-input-label-text: var(--color-grey-68); + --color-input-mode-icon: var(--color-grey-40); + --color-input-mode-icon-hover: var(--color-grey-60); } diff --git a/src/renderer/components/Button/button.styles.ts b/src/renderer/components/Button/button.styles.ts index 2dcc772..605f3b5 100644 --- a/src/renderer/components/Button/button.styles.ts +++ b/src/renderer/components/Button/button.styles.ts @@ -1,7 +1,7 @@ import { css } from 'lit-element'; /** - * Shared styles for buttons. + * Shared styles for buttons */ const commonButtonStyles = css` .button { diff --git a/src/renderer/components/Colors/ColorInput.ts b/src/renderer/components/Colors/ColorInput.ts new file mode 100644 index 0000000..d9c8e62 --- /dev/null +++ b/src/renderer/components/Colors/ColorInput.ts @@ -0,0 +1,123 @@ +import { + LitElement, html, css, CSSResult, property, customElement, TemplateResult, svg +} from 'lit-element'; +import { unsafeHTML } from 'lit-html/directives/unsafe-html'; +import { classMap } from 'lit-html/directives/class-map'; +import arrowIcon from '@images/icons/arrow.svg'; +import '@components/Input/InputText'; +import commonButtonStyles from '@components/Button/button.styles'; +import { ColorInputMode } from '@types'; + +/** + * Click to generate random color + */ +@customElement('color-input') +class ColorInput extends LitElement { + static get styles(): CSSResult[] { + return [ + commonButtonStyles, + css` + :host { + display: block; + } + + .color-input-group { + border: 0.0625rem solid var(--color-input-border); + border-radius: 0.3125rem; + overflow: hidden; + display: flex; + } + + .color-input-group--error { + border-color: var(--color-input-border-error); + } + + .color-input-label { + text-align: center; + line-height: 1.5rem; + padding: 0.4375rem; + background-color:var(--color-input-label-background); + color: var(--color-input-label-text); + flex: 1 0 3rem; + } + + .color-input-text { + flex: 0 1 auto; + } + + .color-input-mode { + flex: 1 0 9px; + padding: 0.6875rem; + line-height: 1rem; + } + + .color-input-mode .icon-button svg { + display: block; + height: 1rem; + width: auto; + fill: var(--color-input-mode-icon); + transition: fill 0.1s linear; + } + + .color-input-mode .icon-button:hover svg { + fill: var(--color-input-mode-icon-hover); + } + ` + ]; + } + + @property() colorInputMode: ColorInputMode = 'hex'; + + @property({ type: String }) colorInputValue = ''; + + @property() onColorInputChange?: (value: string) => void; + + @property() onColorInputKeypress?: (event: KeyboardEvent) => void; + + @property() onColorInputModeClick?: () => void; + + @property({ type: Boolean }) colorInputHasError = false; + + render(): TemplateResult { + return html` +
+
+ ${this.colorInputMode === 'hex' ? '#' : 'RGB'} +
+
+ + +
+
+ +
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'color-input': ColorInput; + } +} + +export default ColorInput; diff --git a/src/renderer/components/Colors/ColorRandomize.ts b/src/renderer/components/Colors/ColorRandomize.ts index 9387209..844dcff 100644 --- a/src/renderer/components/Colors/ColorRandomize.ts +++ b/src/renderer/components/Colors/ColorRandomize.ts @@ -14,18 +14,22 @@ class ColorRandomize extends LitElement { return [ commonButtonStyles, css` - :host { - display: block; - } + :host { + display: block; + } - .icon-button svg { - height: 1.25rem; - width: 1.25rem; - fill: var(--color-body-icon); - display: block; - transition: fill 0.1s linear; - } - ` + .icon-button svg { + height: 1.25rem; + width: 1.25rem; + fill: var(--color-body-icon); + display: block; + transition: fill 0.1s linear; + } + + .icon-button:hover svg { + fill: var(--color-body-icon-hover); + } + ` ]; } diff --git a/src/renderer/components/Input/InputText.ts b/src/renderer/components/Input/InputText.ts new file mode 100644 index 0000000..47a5976 --- /dev/null +++ b/src/renderer/components/Input/InputText.ts @@ -0,0 +1,63 @@ +import { + LitElement, html, css, CSSResult, property, customElement, TemplateResult +} from 'lit-element'; +import { classMap } from 'lit-html/directives/class-map'; +import { ifDefined } from 'lit-html/directives/if-defined'; +import commonInputStyles from './input.styles'; + +/** + * Text input component + */ +@customElement('input-text') +class InputText extends LitElement { + static get styles(): CSSResult[] { + return [ + commonInputStyles, + css` + :host { + display: block; + } + ` + ]; + } + + @property() onInput?: (value: string) => void; + + @property() onKeyPress?: (event: KeyboardEvent) => void; + + @property({ type: Number }) maxlength?: number; + + @property({ type: String }) autoComplete: 'on' | 'off' = 'off'; + + @property({ type: String }) textAlign: 'left' | 'center' | 'right' = 'left'; + + @property({ type: String }) inputValue = ''; + + render(): TemplateResult { + return html` + { + const { value } = event.target as HTMLInputElement; + this.onInput && this.onInput(value); + }} + @keypress=${this.onKeyPress} + .value=${this.inputValue} + > + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'input-text': InputText; + } +} + +export default InputText; diff --git a/src/renderer/components/Input/input.styles.ts b/src/renderer/components/Input/input.styles.ts new file mode 100644 index 0000000..793c0fa --- /dev/null +++ b/src/renderer/components/Input/input.styles.ts @@ -0,0 +1,76 @@ +import { css } from 'lit-element'; + +/** + * Shared styles for input elements + */ +const commonInputStyles = css` + .input { + background-color: var(--color-input-background); + border-radius: 0; + border: none; + padding: 0.4375rem; + font-family: inherit; + font-size: 1em; + line-height: 1.5em; + color: var(--color-input-text); + outline: none; + width: 100%; + box-sizing: border-box; + } + + .input:read-only { + background-color: var(--color-input-background); + border: none; + color: var(--color-input-text); + /* cursor: pointer; */ + } + + /* Placeholder */ + .input::placeholder { + color: var(--color-input-placeholder); + } + + .input::-webkit-input-placeholder { + color: var(--color-input-placeholder); + } + + .input::-moz-placeholder { + color: var(--color-input-placeholder); + } + + .input:-moz-placeholder { + color: var(--color-input-placeholder); + } + + .input:-ms-input-placeholder { + color: var(--color-input-placeholder); + } + + .input::-ms-input-placeholder { + color: var(--color-input-placeholder); + } + + /* Auto complete styles */ + .input:-webkit-autofill, + .input:-webkit-autofill:hover, + .input:-webkit-autofill:focus, + .input:-webkit-autofill:active { + background-clip: content-box !important; + -webkit-box-shadow: 0 0 0 50px var(--color-input-background) inset !important; + -webkit-text-fill-color: var(--color-input-text) !important; + } + + .input--align-left { + text-align: left; + } + + .input--align-center { + text-align: center; + } + + .input--align-right { + text-align: right; + } +`; + +export default commonInputStyles; diff --git a/src/renderer/components/Tooltip/tooltip.styles.ts b/src/renderer/components/Tooltip/tooltip.styles.ts index 58e5275..60ad81a 100644 --- a/src/renderer/components/Tooltip/tooltip.styles.ts +++ b/src/renderer/components/Tooltip/tooltip.styles.ts @@ -1,7 +1,7 @@ import { css } from 'lit-element'; /** - * Shared styles for buttons. + * Shared styles for tooltips */ const commonTooltipStyles = css` [data-tooltip] { diff --git a/src/renderer/pages/TintsShadesGenerator.ts b/src/renderer/pages/TintsShadesGenerator.ts index 70e213d..0a812eb 100644 --- a/src/renderer/pages/TintsShadesGenerator.ts +++ b/src/renderer/pages/TintsShadesGenerator.ts @@ -2,8 +2,10 @@ import { LitElement, html, css, CSSResult, customElement, TemplateResult, property } from 'lit-element'; import '@components/Colors/ColorPicker'; +import '@components/Colors/ColorInput'; import '@components/Colors/ColorRandomize'; import '@components/Colors/ColorSteps'; +import { ColorInputMode } from '@types'; /** * Wrapper of tints and shades generation UI @@ -24,16 +26,16 @@ class TintsShadesGenerator extends LitElement { } .color-input-set color-picker { - flex: 0 1 20%; + flex: 1 0 20%; } - .color-input-set .color-input { - flex: 1 0 auto; + .color-input-set color-input { + flex: 0 1 auto; padding: 0 0.6875rem; } .color-input-set color-randomize { - flex: 0 1 20px; + flex: 1 0 20px; } color-steps { @@ -44,12 +46,24 @@ class TintsShadesGenerator extends LitElement { @property({ type: String }) selectedColor = ''; + @property({ type: String }) colorInputValue = ''; + @property() onColorPickerChange?: (event: Event) => void; @property() onRandomizeColor?: (event: Event) => void; @property() copyColorToClipboard?: (event: MouseEvent) => void; + @property() colorInputMode: ColorInputMode = 'hex'; + + @property() onColorInputChange?: (value: string) => void; + + @property() onColorInputModeClick?: () => void; + + @property() onColorInputKeypress?: (event: KeyboardEvent) => void; + + @property({ type: Boolean }) colorInputHasError = false; + render(): TemplateResult { return html`
@@ -58,7 +72,15 @@ class TintsShadesGenerator extends LitElement { .onColorPickerChange=${this.onColorPickerChange} > -
+ + diff --git a/src/storage/storage.ts b/src/storage/storage.ts index 8720d25..8ada7e5 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -1,13 +1,20 @@ import * as storage from 'electron-json-storage'; import log from 'electron-log'; import { - Position, AppThemeOptions, AppThemeStorage, PinStatusStorage, SelectedColorStorage + Position, + AppThemeOptions, + AppThemeStorage, + PinStatusStorage, + SelectedColorStorage, + ColorInputModeStorage, + ColorInputMode } from '@types'; const WINDOW_POSITION_STORAGE_PATH = 'windowPosition'; const THEME_STORAGE_PATH = 'appTheme'; const WINDOW_PIN_STATUS_STORAGE_PATH = 'windowPinStatus'; const WINDOW_SELECTED_COLOR_STORAGE_PATH = 'selectedColor'; +const WINDOW_COLOR_INPUT_MODE_STORAGE_PATH = 'colorInputMode'; /** * Get user defined theme preference stored in storage @@ -164,6 +171,46 @@ export const saveSelectedColor = (color?: string): void => { } }; +/** + * Get color input mode stored in storage + * @returns {ColorInputModeStorage} JSON that contains user's last selected color input mode + */ +export const getColorInputMode = async (): Promise => { + try { + const colorInputMode = await new Promise((resolve, reject) => { + storage.get(WINDOW_COLOR_INPUT_MODE_STORAGE_PATH, ( + error: Error, data: ColorInputModeStorage + ) => { + if (error) { reject(error); } + resolve(data); + }); + }); + return colorInputMode as ColorInputModeStorage; + } catch (error) { + log.error(error?.message ?? 'Unknown error from getColorInputMode()'); + return {}; + } +}; + +/** + * Save selected color input mode to storage + * @param {ColorInputMode} user selected color input mode + */ +export const saveColorInputMode = (mode?: ColorInputMode): void => { + try { + if (typeof mode !== 'string') { + throw new Error('Mode is not defined when trying to save selected color'); + } + + const colorInputMode: ColorInputModeStorage = { inputMode: mode }; + storage.set(WINDOW_COLOR_INPUT_MODE_STORAGE_PATH, colorInputMode, (error) => { + if (error) { throw error; } + }); + } catch (error) { + log.error(error?.message ?? 'Unknown error from saveColorInputMode()'); + } +}; + /** * Get storage path * @returns {string} folder path which contains all the stored json diff --git a/src/types.ts b/src/types.ts index 40a8058..6338b92 100644 --- a/src/types.ts +++ b/src/types.ts @@ -18,6 +18,12 @@ export type AppThemeStorage = { theme?: AppThemeOptions; } +export type ColorInputMode = 'hex' | 'rgb'; + +export type ColorInputModeStorage = { + inputMode?: ColorInputMode; +} + export type RgbColor = { red: number; green: number; @@ -30,3 +36,4 @@ export type TintsOrShadesItem = { hex: string; rgb: RgbColor; }; + diff --git a/src/utils/color.ts b/src/utils/color.ts index 82b9948..7ce55a3 100644 --- a/src/utils/color.ts +++ b/src/utils/color.ts @@ -1,10 +1,30 @@ import { RgbColor, TintsOrShadesItem, TintsOrShadesMode } from '@types'; +const VALID_HEX_COLOR_REGEX = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; +const VALID_RGB_COLOR_REGEX = /^(\d{1,3}),\s?(\d{1,3}),\s?(\d{1,3})$/; + +export const isValidHexColor = (value: string): boolean => VALID_HEX_COLOR_REGEX.test(value); + +export const isValidRgbColor = (value: string): boolean => { + const rgbParts = VALID_RGB_COLOR_REGEX.exec(value.toLowerCase()); + + if (!rgbParts) { + return false; + } + + const red = parseInt(rgbParts[1], 10); + const green = parseInt(rgbParts[2], 10); + const blue = parseInt(rgbParts[3], 10); + + return (red >= 0 && red <= 255) + && (green >= 0 && green <= 255) + && (blue >= 0 && blue <= 255); +}; + const randomRgbNumber = (): number => Math.floor(Math.random() * 256); // 0 - 255 -export const colorHexToRgb = (color: string): RgbColor | null => { - const re = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; - const hexParts = re.exec(color.toLowerCase()); +export const convertColorHexToRgb = (color: string): RgbColor | null => { + const hexParts = VALID_HEX_COLOR_REGEX.exec(color.toLowerCase()); if (!hexParts) { return null; @@ -19,11 +39,17 @@ export const colorHexToRgb = (color: string): RgbColor | null => { }; }; -export const colorRgbToHex = (color: string | RgbColor): string | null => { +export const convertColorHexToRgbString = (color: string): string => { + const rgbObject = convertColorHexToRgb(color); + return rgbObject + ? `${rgbObject.red}, ${rgbObject.green}, ${rgbObject.blue}` + : ''; +}; + +export const convertColorRgbToHex = (color: string | RgbColor): string | null => { let colorParts: number[] = []; if (typeof color === 'string') { - const re = /^(\d{1,3}),\s?(\d{1,3}),\s?(\d{1,3})$/; - const rgbParts = re.exec(color); + const rgbParts = VALID_RGB_COLOR_REGEX.exec(color); if (!rgbParts) { return null; @@ -55,7 +81,7 @@ export const randomHexColor = (): string | null => { blue: randomRgbNumber() }; - return colorRgbToHex(randomRgbNumbers); + return convertColorRgbToHex(randomRgbNumbers); }; const calculateTints = ( @@ -71,7 +97,7 @@ export const generateTintsOrShades = ( color: string, mode: TintsOrShadesMode ): TintsOrShadesItem[] => { - const rgb = colorHexToRgb(color); + const rgb = convertColorHexToRgb(color); if (!rgb) { return []; @@ -89,7 +115,7 @@ export const generateTintsOrShades = ( green: calculateNewValue(rgb.green, i), blue: calculateNewValue(rgb.blue, i) }; - const nexStepHex = colorRgbToHex(nextStepRgb) ?? ''; + const nexStepHex = convertColorRgbToHex(nextStepRgb) ?? ''; result.push({ hex: nexStepHex,