diff --git a/packages/code-studio/src/styleguide/index.tsx b/packages/code-studio/src/styleguide/index.tsx index a08f6c21a0..e2bb431cce 100644 --- a/packages/code-studio/src/styleguide/index.tsx +++ b/packages/code-studio/src/styleguide/index.tsx @@ -1,12 +1,22 @@ import React, { Suspense } from 'react'; import ReactDOM from 'react-dom'; import '@deephaven/components/scss/BaseStyleSheet.scss'; -import { LoadingOverlay } from '@deephaven/components'; +import { + LoadingOverlay, + preloadTheme, + ThemeData, + ThemeProvider, +} from '@deephaven/components'; import { ApiBootstrap } from '@deephaven/jsapi-bootstrap'; import logInit from '../log/LogInit'; logInit(); +preloadTheme(); + +// Provide a non-null array to ThemeProvider to tell it to initialize +const customThemes: ThemeData[] = []; + // eslint-disable-next-line react-refresh/only-export-components const StyleGuideRoot = React.lazy(() => import('./StyleGuideRoot')); @@ -24,9 +34,11 @@ const apiURL = new URL( ReactDOM.render( }> - - - + + + + + , document.getElementById('root') diff --git a/packages/components/scss/BaseStyleSheet.scss b/packages/components/scss/BaseStyleSheet.scss index 08033a28b1..aef83108ef 100644 --- a/packages/components/scss/BaseStyleSheet.scss +++ b/packages/components/scss/BaseStyleSheet.scss @@ -20,7 +20,7 @@ html { body { min-height: 100%; - background-color: var(--dh-background-color, $background); + background-color: var(--dh-color-background, $background); color: $foreground; margin: 0; padding: 0; @@ -30,7 +30,7 @@ body { } #root { - background-color: var(--dh-background-color, $background); + background-color: var(--dh-color-background, $background); .app { height: 100vh; diff --git a/packages/components/src/LoadingSpinner.scss b/packages/components/src/LoadingSpinner.scss index cb46e63c87..6409f733ad 100644 --- a/packages/components/src/LoadingSpinner.scss +++ b/packages/components/src/LoadingSpinner.scss @@ -2,7 +2,7 @@ .loading-spinner { --primary-color: var( --dh-loading-spinner-primary-color, - var(--dh-accent-color, #4c7dee) + var(--dh-color-accent, #4c7dee) ); --secondary-color: var( --dh-loading-spinner-secondary-color, diff --git a/packages/components/src/theme/ThemeModel.ts b/packages/components/src/theme/ThemeModel.ts index 489eb5354b..54c0526c42 100644 --- a/packages/components/src/theme/ThemeModel.ts +++ b/packages/components/src/theme/ThemeModel.ts @@ -7,8 +7,8 @@ export const DEFAULT_LIGHT_THEME_KEY = 'default-light' satisfies BaseThemeKey; // Css properties that are used in preload data with default values. export const DEFAULT_PRELOAD_DATA_VARIABLES = { - '--dh-accent-color': '#4c7dee', // dark theme --dh-color-blue-700 - '--dh-background-color': '#1a171a', // dark theme --dh-color-gray-50 + '--dh-color-accent': '#4c7dee', // dark theme --dh-color-blue-700 + '--dh-color-background': '#1a171a', // dark theme --dh-color-gray-50 } satisfies Record<`--dh-${string}`, string>; export const THEME_CACHE_LOCAL_STORAGE_KEY = 'deephaven.themeCache'; diff --git a/packages/components/src/theme/ThemeProvider.tsx b/packages/components/src/theme/ThemeProvider.tsx index 677b99d7ec..d3ff14d0ec 100644 --- a/packages/components/src/theme/ThemeProvider.tsx +++ b/packages/components/src/theme/ThemeProvider.tsx @@ -36,7 +36,7 @@ export function ThemeProvider({ const activeThemes = useMemo( () => - // Themes remain inactive until a non-null themes value is provided. This + // Themes remain inactive until a non-null themes array is provided. This // avoids the default base theme overriding the preload if we are waiting // on additional themes to be available. themes == null diff --git a/packages/components/src/theme/ThemeUtils.test.ts b/packages/components/src/theme/ThemeUtils.test.ts index 1647f2ba3e..ba34a9b329 100644 --- a/packages/components/src/theme/ThemeUtils.test.ts +++ b/packages/components/src/theme/ThemeUtils.test.ts @@ -1,4 +1,6 @@ +import { ColorUtils, TestUtils } from '@deephaven/utils'; import { + DEFAULT_DARK_THEME_KEY, DEFAULT_PRELOAD_DATA_VARIABLES, ThemeData, ThemeRegistrationData, @@ -6,14 +8,23 @@ import { } from './ThemeModel'; import { calculatePreloadStyleContent, + extractDistinctCssVariableExpressions, getActiveThemes, + getCssVariableRanges, getDefaultBaseThemes, getThemeKey, getThemePreloadData, preloadTheme, + resolveCssVariablesInRecord, + resolveCssVariablesInString, setThemePreloadData, + TMP_CSS_PROP_PREFIX, } from './ThemeUtils'; +jest.mock('shortid'); + +const { asMock, createMockProxy } = TestUtils; + beforeEach(() => { document.body.removeAttribute('style'); document.head.innerHTML = ''; @@ -26,31 +37,61 @@ beforeEach(() => { describe('calculatePreloadStyleContent', () => { it('should set defaults if css variables are not defined', () => { expect(calculatePreloadStyleContent()).toEqual( - `:root{--dh-accent-color:${DEFAULT_PRELOAD_DATA_VARIABLES['--dh-accent-color']};--dh-background-color:${DEFAULT_PRELOAD_DATA_VARIABLES['--dh-background-color']}}` + `:root{--dh-color-accent:${DEFAULT_PRELOAD_DATA_VARIABLES['--dh-color-accent']};--dh-color-background:${DEFAULT_PRELOAD_DATA_VARIABLES['--dh-color-background']}}` ); }); it('should resolve css variables', () => { - document.body.style.setProperty('--dh-accent-color', 'pink'); - document.body.style.setProperty('--dh-background-color', 'orange'); + document.body.style.setProperty('--dh-color-accent', 'pink'); + document.body.style.setProperty('--dh-color-background', 'orange'); expect(calculatePreloadStyleContent()).toEqual( - ':root{--dh-accent-color:pink;--dh-background-color:orange}' + ':root{--dh-color-accent:pink;--dh-color-background:orange}' + ); + }); +}); + +describe('extractDistinctCssVariableExpressions', () => { + it('should extract distinct css variable expressions', () => { + const given = { + aaa: 'var(--aaa-aa)', + bbb: 'var(--bbb-bb)', + ccc: 'var(--ccc-cc)', + ddd: 'var(--aaa-aa)', + eee: 'var(--bbb-bb)', + fff: 'xxx', + ggg: 'xxx var(--gg-ggg) yyy', + }; + + const actual = extractDistinctCssVariableExpressions(given); + expect(actual).toEqual( + new Set([ + 'var(--aaa-aa)', + 'var(--bbb-bb)', + 'var(--ccc-cc)', + 'var(--gg-ggg)', + ]) ); }); }); describe('getActiveThemes', () => { const mockTheme = { + default: { + name: 'Default Theme', + baseThemeKey: undefined, + themeKey: DEFAULT_DARK_THEME_KEY, + styleContent: '', + }, base: { name: 'Base Theme', baseThemeKey: undefined, - themeKey: 'default-dark', + themeKey: 'default-light', styleContent: '', }, custom: { name: 'Custom Theme', - baseThemeKey: 'default-dark', + baseThemeKey: 'default-light', themeKey: 'customTheme', styleContent: '', }, @@ -62,10 +103,15 @@ describe('getActiveThemes', () => { } satisfies Record; const themeRegistration: ThemeRegistrationData = { - base: [mockTheme.base], + base: [mockTheme.default, mockTheme.base], custom: [mockTheme.custom], }; + it('should use default dark theme if no base theme is matched', () => { + const actual = getActiveThemes('somekey', themeRegistration); + expect(actual).toEqual([mockTheme.default]); + }); + it.each([null, mockTheme.customInvalid])( 'should throw if base theme not found', customTheme => { @@ -92,6 +138,52 @@ describe('getActiveThemes', () => { }); }); +describe('getCssVariableRanges', () => { + const t = [ + ['Single var', 'var(--aaa-aa)', [[0, 12]]], + [ + 'Multiple vars', + 'var(--aaa-aa) var(--bbb-bb)', + [ + [0, 12], + [14, 26], + ], + ], + [ + 'Nested vars - level 2', + 'var(--ccc-cc, var(--aaa-aa, green)) var(--bbb-bb)', + [ + [0, 34], + [36, 48], + ], + ], + ['Nested vars - level 3', 'var(--a, var(--b, var(--c, red)))', [[0, 32]]], + [ + 'Nested vars - level 4', + 'var(--a, var(--b, var(--c, var(--d, red)))) var(--e, var(--f, var(--g, var(--h, red))))', + [ + [0, 42], + [44, 86], + ], + ], + ['Nested calc - level 3', 'var(--a, calc(calc(1px + 2px)))', [[0, 30]]], + [ + 'Nested calc - level 4', + 'var(--a, calc(calc(calc(1px + 2px) + 3px)))', + [[0, 42]], + ], + ['Unbalanced', 'var(--a', []], + ] as const; + + it.each(t)( + 'should return the css variable ranges - %s: %s, %s', + (_label, given, expected) => { + const actual = getCssVariableRanges(given); + expect(actual).toEqual(expected); + } + ); +}); + describe('getDefaultBaseThemes', () => { it('should return default base themes', () => { const actual = getDefaultBaseThemes(); @@ -99,7 +191,8 @@ describe('getDefaultBaseThemes', () => { { name: 'Default Dark', themeKey: 'default-dark', - styleContent: 'test-file-stub', + styleContent: + 'test-file-stub\ntest-file-stub\ntest-file-stub\ntest-file-stub', }, { name: 'Default Light', @@ -175,6 +268,152 @@ describe('preloadTheme', () => { }); }); +describe.each([undefined, document.createElement('div')])( + 'resolveCssVariablesInRecord', + targetElement => { + const computedStyle = createMockProxy(); + const expectedTargetEl = targetElement ?? document.body; + const tmpPropEl = document.createElement('div'); + + beforeEach(() => { + asMock(computedStyle.getPropertyValue) + .mockName('getPropertyValue') + .mockImplementation(key => `resolved:${key}`); + + jest.spyOn(expectedTargetEl, 'appendChild').mockName('appendChild'); + + jest + .spyOn(document, 'createElement') + .mockName('createElement') + .mockReturnValue(tmpPropEl); + + jest.spyOn(tmpPropEl.style, 'setProperty').mockName('setProperty'); + jest.spyOn(tmpPropEl.style, 'removeProperty').mockName('removeProperty'); + jest.spyOn(tmpPropEl, 'remove').mockName('remove'); + + jest + .spyOn(ColorUtils, 'normalizeCssColor') + .mockName('normalizeCssColor') + .mockImplementation(key => `normalized:${key}`); + jest + .spyOn(window, 'getComputedStyle') + .mockName('getComputedStyle') + .mockReturnValue(computedStyle); + }); + + it('should map non-css variable values verbatim', () => { + const given = { + aaa: 'aaa', + bbb: 'bbb', + }; + + const actual = resolveCssVariablesInRecord(given, targetElement); + + expect(computedStyle.getPropertyValue).not.toHaveBeenCalled(); + expect(ColorUtils.normalizeCssColor).not.toHaveBeenCalled(); + expect(actual).toEqual(given); + }); + + it('should replace css variables with resolved values', () => { + const given = { + aaa: 'var(--aaa)', + bbb: 'var(--bbb1) var(--bbb2)', + }; + + const expected = { + aaa: 'normalized:resolved:--dh-tmp-0', + bbb: 'normalized:resolved:--dh-tmp-1 normalized:resolved:--dh-tmp-2', + }; + + const actual = resolveCssVariablesInRecord(given, targetElement); + + expect(expectedTargetEl.appendChild).toHaveBeenCalledWith(tmpPropEl); + expect(tmpPropEl.remove).toHaveBeenCalled(); + expect(actual).toEqual(expected); + + let i = 0; + + Object.keys(given).forEach(key => { + const varExpressions = given[key].split(' '); + varExpressions.forEach(value => { + const tmpPropKey = `--${TMP_CSS_PROP_PREFIX}-${i}`; + i += 1; + + expect(tmpPropEl.style.setProperty).toHaveBeenCalledWith( + tmpPropKey, + value + ); + expect(computedStyle.getPropertyValue).toHaveBeenCalledWith( + tmpPropKey + ); + expect(ColorUtils.normalizeCssColor).toHaveBeenCalledWith( + `resolved:${tmpPropKey}` + ); + }); + }); + }); + } +); + +describe('resolveCssVariablesInString', () => { + const mockResolver = jest.fn(); + + beforeEach(() => { + mockResolver + .mockName('mockResolver') + .mockImplementation(varExpression => `R[${varExpression}]`); + }); + + it.each([ + ['No vars', 'red', 'red'], + ['Single var', 'var(--aaa-aa)', 'R[var(--aaa-aa)]'], + [ + 'Multiple vars', + 'var(--aaa-aa) var(--bbb-bb)', + 'R[var(--aaa-aa)] R[var(--bbb-bb)]', + ], + [ + 'Nested vars - level 2', + 'var(--ccc-cc, var(--aaa-aa, green)) var(--bbb-bb)', + 'R[var(--ccc-cc, var(--aaa-aa, green))] R[var(--bbb-bb)]', + ], + [ + 'Nested vars - level 3', + 'var(--a, var(--b, var(--c, red)))', + 'R[var(--a, var(--b, var(--c, red)))]', + ], + [ + 'Nested vars - level 4', + 'var(--a, var(--b, var(--c, var(--d, red))))', + 'R[var(--a, var(--b, var(--c, var(--d, red))))]', + ], + [ + 'Nested calc - level 3', + 'var(--a, calc(calc(1px + 2px)))', + 'R[var(--a, calc(calc(1px + 2px)))]', + ], + [ + 'Nested calc - level 4', + 'var(--a, calc(calc(calc(1px + 2px) + 3px)))', + 'R[var(--a, calc(calc(calc(1px + 2px) + 3px)))]', + ], + [ + 'Nested calc - level 4', + 'var(--a, calc(calc(calc(1px + 2px) + 3px)))', + 'R[var(--a, calc(calc(calc(1px + 2px) + 3px)))]', + ], + [ + 'Non top-level var', + 'calc(var(--a, calc(calc(calc(1px + 2px) + 3px)))) var(--b)', + 'calc(R[var(--a, calc(calc(calc(1px + 2px) + 3px)))]) R[var(--b)]', + ], + ])('should replace css variables - %s: %s, %s', (_label, given, expected) => { + const actual = resolveCssVariablesInString(mockResolver, given); + + expect(actual).toEqual(expected); + }); +}); + describe('setThemePreloadData', () => { it('should set the theme preload data', () => { const preloadData = { diff --git a/packages/components/src/theme/ThemeUtils.ts b/packages/components/src/theme/ThemeUtils.ts index 04768f736e..f1bb6fd6b2 100644 --- a/packages/components/src/theme/ThemeUtils.ts +++ b/packages/components/src/theme/ThemeUtils.ts @@ -1,5 +1,5 @@ import Log from '@deephaven/log'; -import { assertNotNull } from '@deephaven/utils'; +import { assertNotNull, ColorUtils } from '@deephaven/utils'; // Note that ?inline imports are natively supported by Vite, but consumers of // @deephaven/components using Webpack will need to add a rule to their module // config. @@ -12,8 +12,8 @@ import { assertNotNull } from '@deephaven/utils'; // }, // ], // }, -import darkTheme from './theme_default_dark.css?inline'; -import lightTheme from './theme_default_light.css?inline'; +import { themeDark } from './theme-dark'; +import { themeLight } from './theme-light'; import { DEFAULT_DARK_THEME_KEY, DEFAULT_LIGHT_THEME_KEY, @@ -27,6 +27,10 @@ import { const log = Log.module('ThemeUtils'); +export const TMP_CSS_PROP_PREFIX = 'dh-tmp'; + +export type VarExpressionResolver = (varExpression: string) => string; + /** * Creates a string containing preload style content for the current theme. * This resolves the current values of a few CSS variables that can be used @@ -45,6 +49,25 @@ export function calculatePreloadStyleContent(): ThemePreloadStyleContent { return `:root{${pairs.join(';')}}`; } +/** + * Extracts all css variable expressions from the given record and returns + * a set of unique expressions. + * @param record The record to extract css variable expressions from + */ +export function extractDistinctCssVariableExpressions( + record: Record +): Set { + const set = new Set(); + + Object.values(record).forEach(value => { + getCssVariableRanges(value).forEach(([start, end]) => { + set.add(value.substring(start, end + 1)); + }); + }); + + return set; +} + /** * Returns an array of the active themes. The first item will always be one * of the base themes. Optionally, the second item will be a custom theme. @@ -93,12 +116,12 @@ export function getDefaultBaseThemes(): ThemeData[] { { name: 'Default Dark', themeKey: DEFAULT_DARK_THEME_KEY, - styleContent: darkTheme, + styleContent: themeDark, }, { name: 'Default Light', themeKey: DEFAULT_LIGHT_THEME_KEY, - styleContent: lightTheme, + styleContent: themeLight, }, ]; } @@ -119,6 +142,162 @@ export function getThemePreloadData(): ThemePreloadData | null { return null; } +/** + * Identifies start and end indices of any css variable expressions in the given + * string. + * + * e.g. + * getCssVariableRanges('var(--aaa-aa) var(--bbb-bb)') + * yields: + * [ + * [0, 12], + * [14, 26], + * ] + * + * In cases where there are nested expressions, only the indices of the outermost + * expression will be included. + * + * e.g. + * getCssVariableRanges('var(--ccc-cc, var(--aaa-aa, green)) var(--bbb-bb)') + * yields: + * [ + * [0, 34], // range for --ccc-cc expression + * [36, 48], // range for --bbb-bb expression + * ] + * @param value The string to search for css variable expressions + * @returns An array of [start, end] index pairs for each css variable expression + */ +export function getCssVariableRanges(value: string): [number, number][] { + const ranges: [number, number][] = []; + + const cssVarPrefix = 'var(--'; + let start = value.indexOf(cssVarPrefix); + let parenLevel = 0; + + while (start > -1) { + parenLevel = 1; + let i = start + cssVarPrefix.length; + for (; i < value.length; i += 1) { + if (value[i] === '(') { + parenLevel += 1; + } else if (value[i] === ')') { + parenLevel -= 1; + } + + if (parenLevel === 0) { + ranges.push([start, i]); + break; + } + } + + if (parenLevel !== 0) { + log.error('Unbalanced parentheses in css var expression', value); + return []; + } + + start = value.indexOf(cssVarPrefix, i + 1); + } + + return ranges; +} + +/** + * Make a copy of the given object replacing any css variable expressions + * contained in its prop values with values resolved from the given HTML element. + * Variables that resolve to color strings will also be normalized to rgb or + * rgba color strings. + * + * Note that the browser will force a reflow when calling `getComputedStyle` if + * css properties have changed. In order to avoid a reflow for every property + * check we use distinct setup, resolve / normalize, and cleanup passes: + * 1. Setup - Create a tmp element and set all css props we want to evaluate + * 2. Resolve / Normalize - Evaluate all css props via `getPropertyValue` calls + * and replace the original expressions with resolved values. Also normalize + * css colors to rgb/a. + * 3. Cleanup - Remove the tmp element + * @param record An object whose values may contain css var expressions + * @param targetElement The element to resolve css variables against. Defaults + * to document.body + */ +export function resolveCssVariablesInRecord>( + record: T, + targetElement: HTMLElement = document.body +): T { + const perfStart = performance.now(); + + // Add a temporary div to attach temp css variables to + const tmpPropEl = document.createElement('div'); + targetElement.appendChild(tmpPropEl); + + const varExpressions = [...extractDistinctCssVariableExpressions(record)]; + + // Set temporary css variables for resolving var expressions + varExpressions.forEach((varExpression, i) => { + const tmpPropKey = `--${TMP_CSS_PROP_PREFIX}-${i}`; + tmpPropEl.style.setProperty(tmpPropKey, varExpression); + }); + + const result = {} as T; + + const computedStyle = window.getComputedStyle(tmpPropEl); + + const resolver = (varExpression: string): string => { + const tmpPropKey = `--${TMP_CSS_PROP_PREFIX}-${varExpressions.indexOf( + varExpression + )}`; + + const resolved = computedStyle.getPropertyValue(tmpPropKey); + + return ColorUtils.normalizeCssColor(resolved); + }; + + // Resolve the temporary css variables + Object.entries(record).forEach(([key, value]) => { + result[key as keyof T] = resolveCssVariablesInString( + resolver, + value + ) as T[keyof T]; + }); + + // Remove the temporary css variables + tmpPropEl.remove(); + + log.debug('Resolved css variables', performance.now() - perfStart, 'ms'); + + return result; +} + +/** + * Resolve css variable expressions in the given string using the + * given resolver and replace the original expressions with the resolved values. + * + * @param resolver Function that can resolve a css variable expression + * @param value Value that may contain css variable expressions + */ +export function resolveCssVariablesInString( + resolver: VarExpressionResolver, + value: string +): string { + const result: string[] = []; + let i = 0; + getCssVariableRanges(value).forEach(([start, end]) => { + if (i < start) { + result.push(value.substring(i, start)); + i += start - i; + } + + result.push(resolver(value.substring(start, end + 1))); + + i += end - start + 1; + }); + + if (result.length === 0) { + return value; + } + + return result.join(''); +} + /** * Store theme preload data in local storage. * @param preloadData The preload data to set diff --git a/packages/components/src/theme/__snapshots__/ThemeProvider.test.tsx.snap b/packages/components/src/theme/__snapshots__/ThemeProvider.test.tsx.snap index 245e385a12..33d1ae08e2 100644 --- a/packages/components/src/theme/__snapshots__/ThemeProvider.test.tsx.snap +++ b/packages/components/src/theme/__snapshots__/ThemeProvider.test.tsx.snap @@ -22,6 +22,9 @@ exports[`ThemeProvider setSelectedThemeKey: [ [Object] ] should change selected data-theme-key="default-dark" > test-file-stub +test-file-stub +test-file-stub +test-file-stub
Child diff --git a/packages/components/src/theme/theme-dark/index.ts b/packages/components/src/theme/theme-dark/index.ts new file mode 100644 index 0000000000..04f5f09c2b --- /dev/null +++ b/packages/components/src/theme/theme-dark/index.ts @@ -0,0 +1,13 @@ +import themeDarkPalette from './theme-dark-palette.css?inline'; +import themeDarkSemantic from './theme-dark-semantic.css?inline'; +import themeDarkSemanticEditor from './theme-dark-semantic-editor.css?inline'; +import themeDarkSemanticGrid from './theme-dark-semantic-grid.css?inline'; + +export const themeDark = [ + themeDarkPalette, + themeDarkSemantic, + themeDarkSemanticEditor, + themeDarkSemanticGrid, +].join('\n'); + +export default themeDark; diff --git a/packages/components/src/theme/theme-dark/theme-dark-palette.css b/packages/components/src/theme/theme-dark/theme-dark-palette.css new file mode 100644 index 0000000000..e5c60aa08e --- /dev/null +++ b/packages/components/src/theme/theme-dark/theme-dark-palette.css @@ -0,0 +1,302 @@ +:root { + /* Gray */ + --dh-color-gray-hue: 0deg; + --dh-color-gray-50: hsl(var(--dh-color-gray-hue) 6% 10%); + --dh-color-gray-75: hsl(var(--dh-color-gray-hue) 5% 13%); + --dh-color-gray-100: hsl(var(--dh-color-gray-hue), 5%, 17%); + --dh-color-gray-200: hsl(var(--dh-color-gray-hue) 4% 19%); + --dh-color-gray-300: hsl(var(--dh-color-gray-hue) 4% 21%); + --dh-color-gray-400: hsl(var(--dh-color-gray-hue) 2% 25%); + --dh-color-gray-500: hsl(var(--dh-color-gray-hue) 1% 36%); + --dh-color-gray-600: hsl(var(--dh-color-gray-hue) 0% 57%); + --dh-color-gray-700: hsl(var(--dh-color-gray-hue) 1% 75%); + --dh-color-gray-800: hsl(var(--dh-color-gray-hue) 6% 94%); + --dh-color-gray-900: hsl(var(--dh-color-gray-hue) 25% 98%); + + /** Black & White */ + --dh-color-black: var(--dh-color-gray-50); + --dh-color-white: var(--dh-color-gray-800); + + /* Blue */ + --dh-color-blue-hue: 222deg; + --dh-color-blue-100: hsl(var(--dh-color-blue-hue) 65% 19%); + --dh-color-blue-200: hsl(var(--dh-color-blue-hue) 66% 25%); + --dh-color-blue-300: hsl(var(--dh-color-blue-hue) 65% 32%); + --dh-color-blue-400: hsl(var(--dh-color-blue-hue) 63% 39%); + --dh-color-blue-500: hsl(var(--dh-color-blue-hue) 61% 47%); + --dh-color-blue-600: hsl(var(--dh-color-blue-hue) 68% 54%); + --dh-color-blue-700: hsl(var(--dh-color-blue-hue) 83% 62%); + --dh-color-blue-800: hsl(var(--dh-color-blue-hue) 94% 68%); + --dh-color-blue-900: hsl(var(--dh-color-blue-hue) 100% 74%); + --dh-color-blue-1000: hsl(var(--dh-color-blue-hue) 100% 80%); + --dh-color-blue-1100: hsl(calc(var(--dh-color-blue-hue) - 1deg) 100% 84%); + --dh-color-blue-1200: hsl(calc(var(--dh-color-blue-hue) - 1deg) 100% 89%); + --dh-color-blue-1300: hsl(var(--dh-color-blue-hue) 100% 93%); + --dh-color-blue-1400: hsl(calc(var(--dh-color-blue-hue) + 2deg) 100% 96%); + + /* Red */ + --dh-color-red-hue: 345deg; + --dh-color-red-100: hsl(calc(var(--dh-color-red-hue) + 1deg) 54% 18%); + --dh-color-red-200: hsl(var(--dh-color-red-hue) 55% 24%); + --dh-color-red-300: hsl(calc(var(--dh-color-red-hue) + 1deg) 54% 30%); + --dh-color-red-400: hsl(var(--dh-color-red-hue) 54% 37%); + --dh-color-red-500: hsl(var(--dh-color-red-hue) 53% 44%); + --dh-color-red-600: hsl(var(--dh-color-red-hue) 55% 51%); + --dh-color-red-700: hsl(var(--dh-color-red-hue) 71% 59%); + --dh-color-red-800: hsl(var(--dh-color-red-hue) 92% 67%); + --dh-color-red-900: hsl(calc(var(--dh-color-red-hue) - 1deg) 100% 74%); + --dh-color-red-1000: hsl(calc(var(--dh-color-red-hue) - 2deg) 100% 80%); + --dh-color-red-1100: hsl(calc(var(--dh-color-red-hue) - 2deg) 100% 85%); + --dh-color-red-1200: hsl(calc(var(--dh-color-red-hue) - 1deg) 100% 90%); + --dh-color-red-1300: hsl(calc(var(--dh-color-red-hue) - 1deg) 100% 94%); + --dh-color-red-1400: hsl(calc(var(--dh-color-red-hue) - 1deg) 100% 96%); + + /* Orange */ + --dh-color-orange-hue: 22deg; + --dh-color-orange-100: hsl(calc(var(--dh-color-orange-hue) - 2deg) 100% 14%); + --dh-color-orange-200: hsl(var(--dh-color-orange-hue) 96% 18%); + --dh-color-orange-300: hsl(calc(var(--dh-color-orange-hue) + 1deg) 90% 23%); + --dh-color-orange-400: hsl(var(--dh-color-orange-hue) 84% 29%); + --dh-color-orange-500: hsl(var(--dh-color-orange-hue) 80% 35%); + --dh-color-orange-600: hsl(var(--dh-color-orange-hue) 74% 41%); + --dh-color-orange-700: hsl(var(--dh-color-orange-hue) 70% 48%); + --dh-color-orange-800: hsl(var(--dh-color-orange-hue) 78% 55%); + --dh-color-orange-900: hsl(calc(var(--dh-color-orange-hue) - 1deg) 95% 63%); + --dh-color-orange-1000: hsl(var(--dh-color-orange-hue) 100% 71%); + --dh-color-orange-1100: hsl(calc(var(--dh-color-orange-hue) + 1deg) 100% 78%); + --dh-color-orange-1200: hsl(calc(var(--dh-color-orange-hue) + 2deg) 100% 84%); + --dh-color-orange-1300: hsl(calc(var(--dh-color-orange-hue) + 2deg) 100% 90%); + --dh-color-orange-1400: hsl(calc(var(--dh-color-orange-hue) + 3deg) 100% 94%); + + /* Yellow */ + --dh-color-yellow-hue: 49deg; + --dh-color-yellow-100: hsl(calc(var(--dh-color-yellow-hue) + 2deg) 100% 9%); + --dh-color-yellow-200: hsl(calc(var(--dh-color-yellow-hue) + 1deg) 100% 12%); + --dh-color-yellow-300: hsl(calc(var(--dh-color-yellow-hue) + 2deg) 100% 15%); + --dh-color-yellow-400: hsl(calc(var(--dh-color-yellow-hue) + 1deg) 90% 20%); + --dh-color-yellow-500: hsl(var(--dh-color-yellow-hue) 82% 25%); + --dh-color-yellow-600: hsl(calc(var(--dh-color-yellow-hue) - 1deg) 75% 30%); + --dh-color-yellow-700: hsl(calc(var(--dh-color-yellow-hue) - 1deg) 70% 35%); + --dh-color-yellow-800: hsl(calc(var(--dh-color-yellow-hue) - 2deg) 66% 41%); + --dh-color-yellow-900: hsl(calc(var(--dh-color-yellow-hue) - 2deg) 61% 47%); + --dh-color-yellow-1000: hsl(calc(var(--dh-color-yellow-hue) - 2deg) 66% 54%); + --dh-color-yellow-1100: hsl(calc(var(--dh-color-yellow-hue) - 3deg) 79% 60%); + --dh-color-yellow-1200: hsl(calc(var(--dh-color-yellow-hue) - 3deg) 94% 66%); + --dh-color-yellow-1300: hsl(var(--dh-color-yellow-hue) 100% 74%); + --dh-color-yellow-1400: hsl(calc(var(--dh-color-yellow-hue) + 2deg) 100% 84%); + + /* Chartreuse */ + --dh-color-chartreuse-hue: 70deg; + --dh-color-chartreuse-100: hsl( + calc(var(--dh-color-chartreuse-hue) + 7deg) 100% 8% + ); + --dh-color-chartreuse-200: hsl( + calc(var(--dh-color-chartreuse-hue) + 5deg) 84% 12% + ); + --dh-color-chartreuse-300: hsl( + calc(var(--dh-color-chartreuse-hue) + 4deg) 73% 16% + ); + --dh-color-chartreuse-400: hsl( + calc(var(--dh-color-chartreuse-hue) + 1deg) 67% 20% + ); + --dh-color-chartreuse-500: hsl( + calc(var(--dh-color-chartreuse-hue) + 1deg) 62% 25% + ); + --dh-color-chartreuse-600: hsl(var(--dh-color-chartreuse-hue) 59% 30%); + --dh-color-chartreuse-700: hsl( + calc(var(--dh-color-chartreuse-hue) - 1deg) 56% 35% + ); + --dh-color-chartreuse-800: hsl( + calc(var(--dh-color-chartreuse-hue) - 2deg) 53% 40% + ); + --dh-color-chartreuse-900: hsl( + calc(var(--dh-color-chartreuse-hue) - 3deg) 50% 45% + ); + --dh-color-chartreuse-1000: hsl( + calc(var(--dh-color-chartreuse-hue) - 3deg) 49% 51% + ); + --dh-color-chartreuse-1100: hsl( + calc(var(--dh-color-chartreuse-hue) - 3deg) 58% 57% + ); + --dh-color-chartreuse-1200: hsl( + calc(var(--dh-color-chartreuse-hue) - 4deg) 67% 64% + ); + --dh-color-chartreuse-1300: hsl( + calc(var(--dh-color-chartreuse-hue) - 4deg) 76% 73% + ); + --dh-color-chartreuse-1400: hsl( + calc(var(--dh-color-chartreuse-hue) - 4deg) 82% 83% + ); + + /* Celery */ + --dh-color-celery-hue: 126deg; + --dh-color-celery-100: hsl(calc(var(--dh-color-celery-hue) + 1deg) 43% 12%); + --dh-color-celery-200: hsl(calc(var(--dh-color-celery-hue) - 1deg) 43% 16%); + --dh-color-celery-300: hsl(calc(var(--dh-color-celery-hue) - 1deg) 43% 21%); + --dh-color-celery-400: hsl(calc(var(--dh-color-celery-hue) - 1deg) 43% 25%); + --dh-color-celery-500: hsl(calc(var(--dh-color-celery-hue) - 1deg) 42% 30%); + --dh-color-celery-600: hsl(var(--dh-color-celery-hue) 41% 36%); + --dh-color-celery-700: hsl(calc(var(--dh-color-celery-hue) - 1deg) 39% 41%); + --dh-color-celery-800: hsl(calc(var(--dh-color-celery-hue) - 1deg) 37% 48%); + --dh-color-celery-900: hsl(calc(var(--dh-color-celery-hue) - 1deg) 39% 55%); + --dh-color-celery-1000: hsl(calc(var(--dh-color-celery-hue) - 1deg) 44% 63%); + --dh-color-celery-1100: hsl(calc(var(--dh-color-celery-hue) - 1deg) 48% 71%); + --dh-color-celery-1200: hsl(var(--dh-color-celery-hue) 50% 80%); + --dh-color-celery-1300: hsl(var(--dh-color-celery-hue) 48% 87%); + --dh-color-celery-1400: hsl(calc(var(--dh-color-celery-hue) + 1deg) 50% 93%); + + /* Green */ + --dh-color-green-hue: 94deg; + --dh-color-green-100: hsl(calc(var(--dh-color-green-hue) + 1deg) 17% 14%); + --dh-color-green-200: hsl(calc(var(--dh-color-green-hue) - 2deg) 19% 18%); + --dh-color-green-300: hsl(var(--dh-color-green-hue) 22% 23%); + --dh-color-green-400: hsl(var(--dh-color-green-hue) 23% 27%); + --dh-color-green-500: hsl(calc(var(--dh-color-green-hue) - 1deg) 26% 32%); + --dh-color-green-600: hsl(calc(var(--dh-color-green-hue) + 1deg) 28% 37%); + --dh-color-green-700: hsl(calc(var(--dh-color-green-hue) + 1deg) 30% 42%); + --dh-color-green-800: hsl(var(--dh-color-green-hue) 32% 48%); + --dh-color-green-900: hsl(var(--dh-color-green-hue) 38% 53%); + --dh-color-green-1000: hsl(var(--dh-color-green-hue) 47% 58%); + --dh-color-green-1100: hsl(var(--dh-color-green-hue) 59% 64%); + --dh-color-green-1200: hsl(var(--dh-color-green-hue) 71% 70%); + --dh-color-green-1300: hsl(var(--dh-color-green-hue) 81% 79%); + --dh-color-green-1400: hsl(calc(var(--dh-color-green-hue) - 1deg) 88% 87%); + + /* Seafoam */ + --dh-color-seafoam-hue: 159deg; + --dh-color-seafoam-100: hsl(calc(var(--dh-color-seafoam-hue) - 3deg) 14% 14%); + --dh-color-seafoam-200: hsl(calc(var(--dh-color-seafoam-hue) - 1deg) 17% 18%); + --dh-color-seafoam-300: hsl(var(--dh-color-seafoam-hue) 20% 23%); + --dh-color-seafoam-400: hsl(calc(var(--dh-color-seafoam-hue) - 1deg) 23% 28%); + --dh-color-seafoam-500: hsl(calc(var(--dh-color-seafoam-hue) - 1deg) 26% 33%); + --dh-color-seafoam-600: hsl(var(--dh-color-seafoam-hue) 29% 37%); + --dh-color-seafoam-700: hsl(calc(var(--dh-color-seafoam-hue) + 2deg) 33% 42%); + --dh-color-seafoam-800: hsl(calc(var(--dh-color-seafoam-hue) + 2deg) 36% 47%); + --dh-color-seafoam-900: hsl(calc(var(--dh-color-seafoam-hue) + 3deg) 42% 51%); + --dh-color-seafoam-1000: hsl( + calc(var(--dh-color-seafoam-hue) + 2deg) 53% 56% + ); + --dh-color-seafoam-1100: hsl( + calc(var(--dh-color-seafoam-hue) + 3deg) 66% 62% + ); + --dh-color-seafoam-1200: hsl( + calc(var(--dh-color-seafoam-hue) + 2deg) 76% 70% + ); + --dh-color-seafoam-1300: hsl( + calc(var(--dh-color-seafoam-hue) - 1deg) 87% 80% + ); + --dh-color-seafoam-1400: hsl( + calc(var(--dh-color-seafoam-hue) - 3deg) 89% 89% + ); + + /* Cyan */ + --dh-color-cyan-hue: 186deg; + --dh-color-cyan-100: hsl(calc(var(--dh-color-cyan-hue) - 1deg) 38% 13%); + --dh-color-cyan-200: hsl(var(--dh-color-cyan-hue) 38% 16%); + --dh-color-cyan-300: hsl(var(--dh-color-cyan-hue) 39% 21%); + --dh-color-cyan-400: hsl(calc(var(--dh-color-cyan-hue) - 1deg) 38% 26%); + --dh-color-cyan-500: hsl(calc(var(--dh-color-cyan-hue) - 1deg) 38% 31%); + --dh-color-cyan-600: hsl(var(--dh-color-cyan-hue) 38% 37%); + --dh-color-cyan-700: hsl(calc(var(--dh-color-cyan-hue) - 1deg) 38% 42%); + --dh-color-cyan-800: hsl(calc(var(--dh-color-cyan-hue) - 1deg) 37% 48%); + --dh-color-cyan-900: hsl(calc(var(--dh-color-cyan-hue) - 1deg) 42% 54%); + --dh-color-cyan-1000: hsl(calc(var(--dh-color-cyan-hue) - 1deg) 52% 60%); + --dh-color-cyan-1100: hsl(var(--dh-color-cyan-hue) 64% 67%); + --dh-color-cyan-1200: hsl(var(--dh-color-cyan-hue) 79% 74%); + --dh-color-cyan-1300: hsl(var(--dh-color-cyan-hue) 92% 81%); + --dh-color-cyan-1400: hsl(var(--dh-color-cyan-hue) 100% 90%); + + /* Indigo */ + --dh-color-indigo-hue: 235deg; + --dh-color-indigo-100: hsl(calc(var(--dh-color-indigo-hue) + 2deg) 58% 24%); + --dh-color-indigo-200: hsl(calc(var(--dh-color-indigo-hue) + 1deg) 52% 30%); + --dh-color-indigo-300: hsl(calc(var(--dh-color-indigo-hue) + 1deg) 47% 37%); + --dh-color-indigo-400: hsl(var(--dh-color-indigo-hue) 43% 44%); + --dh-color-indigo-500: hsl(var(--dh-color-indigo-hue) 40% 51%); + --dh-color-indigo-600: hsl(var(--dh-color-indigo-hue) 49% 58%); + --dh-color-indigo-700: hsl(calc(var(--dh-color-indigo-hue) - 1deg) 60% 65%); + --dh-color-indigo-800: hsl(calc(var(--dh-color-indigo-hue) - 1deg) 75% 72%); + --dh-color-indigo-900: hsl(calc(var(--dh-color-indigo-hue) - 1deg) 91% 78%); + --dh-color-indigo-1000: hsl(calc(var(--dh-color-indigo-hue) - 1deg) 100% 83%); + --dh-color-indigo-1100: hsl(calc(var(--dh-color-indigo-hue) - 1deg) 100% 87%); + --dh-color-indigo-1200: hsl(var(--dh-color-indigo-hue) 100% 91%); + --dh-color-indigo-1300: hsl(calc(var(--dh-color-indigo-hue) - 3deg) 100% 94%); + --dh-color-indigo-1400: hsl(calc(var(--dh-color-indigo-hue) + 1deg) 100% 97%); + + /* Purple */ + --dh-color-purple-hue: 254deg; + --dh-color-purple-100: hsl(calc(var(--dh-color-purple-hue) + 9deg) 73% 23%); + --dh-color-purple-200: hsl(calc(var(--dh-color-purple-hue) + 6deg) 59% 30%); + --dh-color-purple-300: hsl(calc(var(--dh-color-purple-hue) + 4deg) 50% 37%); + --dh-color-purple-400: hsl(calc(var(--dh-color-purple-hue) + 3deg) 43% 44%); + --dh-color-purple-500: hsl(calc(var(--dh-color-purple-hue) + 1deg) 38% 50%); + --dh-color-purple-600: hsl(var(--dh-color-purple-hue) 45% 57%); + --dh-color-purple-700: hsl(calc(var(--dh-color-purple-hue) - 1deg) 54% 64%); + --dh-color-purple-800: hsl(calc(var(--dh-color-purple-hue) - 3deg) 64% 71%); + --dh-color-purple-900: hsl(calc(var(--dh-color-purple-hue) - 4deg) 79% 78%); + --dh-color-purple-1000: hsl(calc(var(--dh-color-purple-hue) - 5deg) 91% 83%); + --dh-color-purple-1100: hsl(calc(var(--dh-color-purple-hue) - 5deg) 97% 88%); + --dh-color-purple-1200: hsl(calc(var(--dh-color-purple-hue) - 4deg) 100% 91%); + --dh-color-purple-1300: hsl(calc(var(--dh-color-purple-hue) - 3deg) 100% 95%); + --dh-color-purple-1400: hsl(calc(var(--dh-color-purple-hue) + 6deg) 88% 97%); + + /* Fuchsia */ + --dh-color-fuchsia-hue: 286deg; + --dh-color-fuchsia-100: hsl( + calc(var(--dh-color-fuchsia-hue) + 15deg) 64% 16% + ); + --dh-color-fuchsia-200: hsl( + calc(var(--dh-color-fuchsia-hue) + 14deg) 53% 22% + ); + --dh-color-fuchsia-300: hsl( + calc(var(--dh-color-fuchsia-hue) + 11deg) 44% 29% + ); + --dh-color-fuchsia-400: hsl(calc(var(--dh-color-fuchsia-hue) + 8deg) 39% 35%); + --dh-color-fuchsia-500: hsl(calc(var(--dh-color-fuchsia-hue) + 5deg) 35% 43%); + --dh-color-fuchsia-600: hsl(calc(var(--dh-color-fuchsia-hue) + 1deg) 33% 51%); + --dh-color-fuchsia-700: hsl(calc(var(--dh-color-fuchsia-hue) - 3deg) 40% 58%); + --dh-color-fuchsia-800: hsl(calc(var(--dh-color-fuchsia-hue) - 7deg) 50% 66%); + --dh-color-fuchsia-900: hsl(calc(var(--dh-color-fuchsia-hue) - 9deg) 61% 74%); + --dh-color-fuchsia-1000: hsl( + calc(var(--dh-color-fuchsia-hue) - 11deg) 71% 80% + ); + --dh-color-fuchsia-1100: hsl( + calc(var(--dh-color-fuchsia-hue) - 11deg) 76% 85% + ); + --dh-color-fuchsia-1200: hsl( + calc(var(--dh-color-fuchsia-hue) - 9deg) 78% 89% + ); + --dh-color-fuchsia-1300: hsl( + calc(var(--dh-color-fuchsia-hue) - 6deg) 77% 93% + ); + --dh-color-fuchsia-1400: hsl( + calc(var(--dh-color-fuchsia-hue) + 2deg) 71% 96% + ); + + /* Magenta */ + --dh-color-magenta-hue: 330deg; + --dh-color-magenta-100: hsl(calc(var(--dh-color-magenta-hue) + 3deg) 91% 17%); + --dh-color-magenta-200: hsl(calc(var(--dh-color-magenta-hue) + 4deg) 72% 23%); + --dh-color-magenta-300: hsl(calc(var(--dh-color-magenta-hue) + 4deg) 60% 30%); + --dh-color-magenta-400: hsl(calc(var(--dh-color-magenta-hue) + 4deg) 52% 36%); + --dh-color-magenta-500: hsl(calc(var(--dh-color-magenta-hue) + 3deg) 46% 43%); + --dh-color-magenta-600: hsl(calc(var(--dh-color-magenta-hue) + 2deg) 42% 50%); + --dh-color-magenta-700: hsl(var(--dh-color-magenta-hue) 51% 58%); + --dh-color-magenta-800: hsl(calc(var(--dh-color-magenta-hue) - 1deg) 61% 65%); + --dh-color-magenta-900: hsl(calc(var(--dh-color-magenta-hue) - 1deg) 76% 72%); + --dh-color-magenta-1000: hsl( + calc(var(--dh-color-magenta-hue) - 2deg) 92% 79% + ); + --dh-color-magenta-1100: hsl( + calc(var(--dh-color-magenta-hue) - 3deg) 100% 85% + ); + --dh-color-magenta-1200: hsl( + calc(var(--dh-color-magenta-hue) - 3deg) 100% 89% + ); + --dh-color-magenta-1300: hsl( + calc(var(--dh-color-magenta-hue) - 4deg) 100% 93% + ); + --dh-color-magenta-1400: hsl( + calc(var(--dh-color-magenta-hue) - 2deg) 100% 96% + ); +} diff --git a/packages/components/src/theme/theme-dark/theme-dark-semantic-editor.css b/packages/components/src/theme/theme-dark/theme-dark-semantic-editor.css new file mode 100644 index 0000000000..41ee75dcc5 --- /dev/null +++ b/packages/components/src/theme/theme-dark/theme-dark-semantic-editor.css @@ -0,0 +1,67 @@ +:root { + /* Editor */ + --dh-color-editor-background: var(--dh-color-content-background); + --dh-color-editor-foreground: var(--dh-color-gray-900); + --dh-color-editor-error-foreground: var(--dh-color-visual-red); + --dh-color-editor-line-number-foreground: var(--dh-color-gray-700); + --dh-color-editor-line-highlight-bg: var(--dh-color-gray-200); + --dh-color-editor-selection-background: var(--dh-color-text-highlight); + + /* Code rules */ + --dh-color-editor-string: var(--dh-color-visual-yellow); + --dh-color-editor-string-delim: var(--dh-color-gray-700); + --dh-color-editor-delimiter: var(--dh-color-gray-700); + --dh-color-editor-predefined: var(--dh-color-visual-green); + --dh-color-editor-keyword: var(--dh-color-visual-cyan); + --dh-color-editor-storage: var(--dh-color-visual-red); + --dh-color-editor-number: var(--dh-color-visual-purple); + --dh-color-editor-operator: var(--dh-color-visual-red); + --dh-color-editor-identifier: var(--dh-color-gray-900); + --dh-color-editor-identifier-namespace: var(--dh-color-visual-red); + --dh-color-editor-identifier-js: var(--dh-color-visual-yellow); + --dh-color-editor-comment: var(--dh-color-gray-700); + + /* Input */ + --dh-color-editor-focus-border: var(--dh-color-focus-border); + --dh-color-editor-input-option-active-border: var(--dh-color-focus-ring); + --dh-color-editor-input-background: var(--dh-color-background); + --dh-color-editor-input-foreground: var(--dh-color-text); + --dh-color-editor-input-border: var(--dh-color-border); + + /* Menus */ + --dh-color-editor-context-menu-background: var(--dh-color-gray-300); + --dh-color-editor-context-menu-foreground: var(--dh-color-gray-900); + --dh-color-editor-menu-selection-background: var(--dh-color-highlight-hover); + + /* Logging */ + --dh-color-editor-log-date: var(--dh-color-gray-700); + --dh-color-editor-log-error: var(--dh-color-visual-red); + --dh-color-editor-log-info: var(--dh-color-visual-cyan); + --dh-color-editor-log-stdout: var(--dh-color-gray-900); + --dh-color-editor-log-warn: var(--dh-color-visual-yellow); + --dh-color-editor-log-debug: var(--dh-color-visual-purple); + --dh-color-editor-log-trace: var(--dh-color-visual-green); + + /* Find */ + --dh-color-editor-find-background: var(--dh-color-gray-200); + --dh-color-editor-find-match-background: var(--dh-color-highlight-selected); + --dh-color-editor-find-match-highlight-background: var( + --dh-color-highlight-selected-hover + ); + --dh-color-editor-find-option-active-background: var(--dh-color-accent-700); + --dh-color-editor-find-option-active-foreground: var(--dh-color-gray-900); + + /* Suggest */ + --dh-color-editor-suggest-background: var(--dh-color-gray-200); + --dh-color-editor-suggest-border: var(--dh-color-gray-400); + --dh-color-editor-suggest-foreground: var(--dh-color-gray-100); + --dh-color-editor-suggest-selected-background: var( + --dh-color-highlight-selected + ); + --dh-color-editor-suggest-highlight-foreground: var(--dh-color-accent-700); + --dh-color-editor-suggest-hover-background: var(--dh-color-highlight-hover); + + /* Links */ + --dh-color-editor-link-foreground: var(--dh-color-accent-1000); + --dh-color-editor-link-active-foreground: var(--dh-color-accent-1100); +} diff --git a/packages/components/src/theme/theme-dark/theme-dark-semantic-grid.css b/packages/components/src/theme/theme-dark/theme-dark-semantic-grid.css new file mode 100644 index 0000000000..20cdf034de --- /dev/null +++ b/packages/components/src/theme/theme-dark/theme-dark-semantic-grid.css @@ -0,0 +1,3 @@ +:root { + --dh-color-grid-background: var(--dh-color-background); +} diff --git a/packages/components/src/theme/theme-dark/theme-dark-semantic.css b/packages/components/src/theme/theme-dark/theme-dark-semantic.css new file mode 100644 index 0000000000..34bea973d8 --- /dev/null +++ b/packages/components/src/theme/theme-dark/theme-dark-semantic.css @@ -0,0 +1,74 @@ +/* stylelint-disable alpha-value-notation */ +:root { + /* General */ + --dh-color-accent: var(--dh-color-blue-700); + --dh-color-border: var(--dh-color-gray-500); + --dh-color-background: var(--dh-color-black); + --dh-color-foreground: var(--dh-color-white); + --dh-color-content-background: var(--dh-color-gray-100); + + /* Text */ + --dh-color-text: var(--dh-color-gray-800); + --dh-color-text-highlight: hsla(var(--dh-color-blue-hue), 83%, 62%, 0.3); + + /* Focus */ + --dh-color-focus: var(--dh-color-blue-800); + --dh-color-focus-border: var(--dh-color-blue-800); + --dh-color-focus-ring: var(--dh-color-focus); + + /* Highlight */ + --dh-color-highlight-active: hsla(var(--dh-color-gray-hue), 0%, 94%, 0.15); + --dh-color-highlight-hover: hsla(var(--dh-color-gray-hue), 0%, 100%, 0.08); + --dh-color-highlight-invalid: hsla(var(--dh-color-red-hue), 80%, 48%, 0.15); + --dh-color-highlight-selected: hsla(var(--dh-color-blue-hue), 83%, 62%, 0.13); + --dh-color-highlight-selected-hover: hsla( + var(--dh-color-blue-hue), + 83%, + 62%, + 0.2 + ); + + /* Visual Colors */ + --dh-color-visual-blue: var(--dh-color-blue-700); + --dh-color-visual-celery: var(--dh-color-celery-1000); + --dh-color-visual-chartreuse: var(--dh-color-chartreuse-1100); + --dh-color-visual-cyan: var(--dh-color-cyan-1100); + --dh-color-visual-fuchsia: var(--dh-color-fuchsia-900); + --dh-color-visual-gray: var(--dh-color-gray-600); + --dh-color-visual-green: var(--dh-color-green-1100); + --dh-color-visual-indigo: var(--dh-color-indigo-900); + --dh-color-visual-magenta: var(--dh-color-magenta-900); + --dh-color-visual-orange: var(--dh-color-orange-900); + --dh-color-visual-purple: var(--dh-color-purple-900); + --dh-color-visual-red: var(--dh-color-red-800); + --dh-color-visual-seafoam: var(--dh-color-seafoam-1100); + --dh-color-visual-yellow: var(--dh-color-yellow-1200); + + /** Accent Colors */ + --dh-color-accent-100: var(--dh-color-blue-100); + --dh-color-accent-200: var(--dh-color-blue-200); + --dh-color-accent-300: var(--dh-color-blue-300); + --dh-color-accent-400: var(--dh-color-blue-400); + --dh-color-accent-500: var(--dh-color-blue-500); + --dh-color-accent-600: var(--dh-color-blue-600); + --dh-color-accent-700: var(--dh-color-blue-700); + --dh-color-accent-800: var(--dh-color-blue-800); + --dh-color-accent-900: var(--dh-color-blue-900); + --dh-color-accent-1000: var(--dh-color-blue-1000); + --dh-color-accent-1100: var(--dh-color-blue-1100); + --dh-color-accent-1200: var(--dh-color-blue-1200); + --dh-color-accent-1300: var(--dh-color-blue-1300); + --dh-color-accent-1400: var(--dh-color-blue-1400); + + /* Accent Background */ + --dh-color-accent-background-default: var(--dh-color-accent-600); + --dh-color-accent-background-hover: var(--dh-color-accent-500); + --dh-color-accent-background-down: var(--dh-color-accent-400); + --dh-color-accent-background-key-focus: var(--dh-color-accent-500); + + /* Negative Background */ + --dh-color-negative-background-default: var(--dh-color-red-600); + --dh-color-negative-background-hover: var(--dh-color-red-500); + --dh-color-negative-background-down: var(--dh-color-red-400); + --dh-color-negative-background-key-focus: var(--dh-color-red-500); +} diff --git a/packages/components/src/theme/theme-light/index.ts b/packages/components/src/theme/theme-light/index.ts new file mode 100644 index 0000000000..b6c7d42258 --- /dev/null +++ b/packages/components/src/theme/theme-light/index.ts @@ -0,0 +1,5 @@ +import themeLightPalette from './theme-light-palette.css?inline'; + +export const themeLight = themeLightPalette; + +export default themeLight; diff --git a/packages/components/src/theme/theme_default_light.css b/packages/components/src/theme/theme-light/theme-light-palette.css similarity index 87% rename from packages/components/src/theme/theme_default_light.css rename to packages/components/src/theme/theme-light/theme-light-palette.css index 20fb5c30f2..ce7c31acc7 100644 --- a/packages/components/src/theme/theme_default_light.css +++ b/packages/components/src/theme/theme-light/theme-light-palette.css @@ -47,8 +47,8 @@ /* Semantic */ --dh-color-black: var(--dh-color-gray-50); --dh-color-white: var(--dh-color-gray-75); - --dh-accent-color: var(--dh-color-blue-700); - --dh-background-color: var(--dh-color-white); - --dh-foreground-color: var(--dh-color-black); - --dh-grid-background-color: var(--dh-background-color); + --dh-color-accent: var(--dh-color-blue-700); + --dh-color-background: var(--dh-color-white); + --dh-color-foreground: var(--dh-color-black); + --dh-color-grid-background: var(--dh-color-background); } diff --git a/packages/components/src/theme/theme_default_dark.css b/packages/components/src/theme/theme_default_dark.css deleted file mode 100644 index 7e2c6d7cfc..0000000000 --- a/packages/components/src/theme/theme_default_dark.css +++ /dev/null @@ -1,54 +0,0 @@ -:root { - /* Grays */ - --dh-color-gray-900: #fcfcfa; - --dh-color-gray-800: #f0f0ee; - --dh-color-gray-700: #c0bfbf; - --dh-color-gray-600: #929192; - --dh-color-gray-500: #5b5a5c; - --dh-color-gray-400: #403e41; - --dh-color-gray-300: #373438; - --dh-color-gray-200: #322f33; - --dh-color-gray-100: #2d2a2e; - --dh-color-gray-75: #211f22; - --dh-color-gray-50: #1a171a; - - /* Blues */ - --dh-color-blue-100: #112451; - --dh-color-blue-200: #16306c; - --dh-color-blue-300: #1d3d88; - --dh-color-blue-400: #254ba4; - --dh-color-blue-500: #2f5bc0; - --dh-color-blue-600: #3b6bda; - --dh-color-blue-700: #4c7dee; - --dh-color-blue-800: #6390fa; - --dh-color-blue-900: #7ca4ff; - --dh-color-blue-1000: #97b7ff; - --dh-color-blue-1100: #afc9ff; - --dh-color-blue-1200: #c7d9ff; - --dh-color-blue-1300: #dbe6ff; - --dh-color-blue-1400: #ecf1ff; - - /* Seafoam */ - --dh-color-seafoam-100: #1f2925; - --dh-color-seafoam-200: #263630; - --dh-color-seafoam-300: #2f463e; - --dh-color-seafoam-400: #37574b; - --dh-color-seafoam-500: #3e6959; - --dh-color-seafoam-600: #447b68; - --dh-color-seafoam-700: #488f78; - --dh-color-seafoam-800: #4ca387; - --dh-color-seafoam-900: #4fb797; - --dh-color-seafoam-1000: #54cba6; - --dh-color-seafoam-1100: #5edeb7; - --dh-color-seafoam-1200: #78edc7; - --dh-color-seafoam-1300: #9ef8d7; - --dh-color-seafoam-1400: #cbfce8; - - /* Semantic */ - --dh-color-black: var(--dh-color-gray-50); - --dh-color-white: var(--dh-color-gray-800); - --dh-accent-color: var(--dh-color-blue-700); - --dh-background-color: var(--dh-color-black); - --dh-foreground-color: var(--dh-color-white); - --dh-grid-background-color: var(--dh-background-color); -} diff --git a/packages/console/src/monaco/MonacoTheme.module.scss b/packages/console/src/monaco/MonacoTheme.module.scss index 24d242ebf0..7185cb8386 100644 --- a/packages/console/src/monaco/MonacoTheme.module.scss +++ b/packages/console/src/monaco/MonacoTheme.module.scss @@ -3,71 +3,80 @@ :export { // iris dark theme - error-foreground: $red; - background: $content-bg; - foreground: $gray-100; + error-foreground: var(--dh-color-editor-error-foreground); + background: var(--dh-color-editor-background); + foreground: var(--dh-color-editor-foreground); line-height: 19px; // 19 is the line height in the default monaco theme //code rules - //more nuanced grays required in a few spots - string: $yellow; - string-delim: $gray-400; - delimiter: mix($gray-400, $gray-300, 30%); - predefined: $green; - keyword: $blue; - storage: $red; - number: $purple; - operator: $red; - identifier: $gray-100; - namespace-identifier: $red; - identifier-js: $yellow; - comment: mix($gray-500, $gray-400, 40%); + string: var(--dh-color-editor-string); + string-delim: var(--dh-color-editor-string-delim); + delimiter: var(--dh-color-editor-delimiter); + predefined: var(--dh-color-editor-predefined); + keyword: var(--dh-color-editor-keyword); + storage: var(--dh-color-editor-storage); + number: var(--dh-color-editor-number); + operator: var(--dh-color-editor-operator); + identifier: var(--dh-color-editor-identifier); + namespace-identifier: var(--dh-color-editor-identifier-namespace); + identifier-js: var(--dh-color-editor-identifier-js); + comment: var(--dh-color-editor-comment); //input - input-option-active-border: $input-border-color; - focus-border: $input-focus-border-color; - input-background: $input-bg; - input-foreground: $input-color; - input-border: $input-border-color; + input-option-active-border: var(--dh-color-editor-input-option-active-border); + focus-border: var(--dh-color-editor-focus-border); + input-background: var(--dh-color-editor-input-background); + input-foreground: var(--dh-color-editor-input-foreground); + input-border: var(--dh-color-editor-input-border); //editor - editor-line-number-foreground: $gray-400; - editor-selection-background: $text-select-color-editor; - editor-line-highlight-bg: $gray-800; + editor-line-number-foreground: var(--dh-color-editor-line-number-foreground); + editor-selection-background: var(--dh-color-editor-selection-background); + editor-line-highlight-bg: var(--dh-color-editor-line-highlight-bg); //context menu - context-menu-background: $contextmenu-bg; - context-menu-foreground: $contextmenu-color; - menu-selection-background: $contextmenu-selected-bg; + context-menu-background: var(--dh-color-editor-context-menu-background); + context-menu-foreground: var(--dh-color-editor-context-menu-foreground); + menu-selection-background: var(--dh-color-editor-menu-selection-background); //log items - log-date: $gray-400; - log-error: $danger; - log-info: $blue; - log-stdout: $foreground; - log-warn: $yellow; - log-debug: $purple; - log-trace: $green; + log-date: var(--dh-color-editor-log-date); + log-error: var(--dh-color-editor-log-error); + log-info: var(--dh-color-editor-log-info); + log-stdout: var(--dh-color-editor-log-stdout); + log-warn: var(--dh-color-editor-log-warn); + log-debug: var(--dh-color-editor-log-debug); + log-trace: var(--dh-color-editor-log-trace); // find matches - editor-find-match-background: mix($primary, $content-bg, 55%); - editor-find-match-highlight-background: mix($primary, $content-bg, 25%); + editor-find-match-background: var(--dh-color-editor-find-match-background); + editor-find-match-highlight-background: var( + --dh-color-editor-find-match-highlight-background + ); // find widget - editor-widget-background: $gray-700; - input-option-active-background: $primary; - input-option-active-foreground: $foreground; + editor-widget-background: var(--dh-color-editor-find-background); + input-option-active-background: var( + --dh-color-editor-find-option-active-background + ); + input-option-active-foreground: var( + --dh-color-editor-find-option-active-foreground + ); // suggest widget - editor-suggest-widget-background: $gray-700; - editor-suggest-widget-border: $gray-500; - editor-suggest-widget-foreground: $white; - editor-suggest-widget-selected-background: mix($primary, $gray-700, 15%); - editor-suggest-widget-highlightForeground: $primary; - list-hover-background: $gray-600; + editor-suggest-widget-background: var(--dh-color-editor-suggest-background); + editor-suggest-widget-border: var(--dh-color-editor-suggest-border); + editor-suggest-widget-foreground: var(--dh-color-editor-suggest-foreground); + editor-suggest-widget-selected-background: var( + --dh-color-editor-suggest-selected-background + ); + editor-suggest-widget-highlightForeground: var( + --dh-color-editor-suggest-highlight-foreground + ); + list-hover-background: var(--dh-color-editor-suggest-hover-background); // links - text-link-foreground: $link-color; - text-link-active-foreground: $link-hover-color; - editor-link-active-foreground: $link-hover-color; + text-link-foreground: var(--dh-color-editor-link-foreground); + text-link-active-foreground: var(--dh-color-editor-link-active-foreground); + editor-link-active-foreground: var(--dh-color-editor-link-active-foreground); } diff --git a/packages/console/src/monaco/MonacoUtils.ts b/packages/console/src/monaco/MonacoUtils.ts index 26a9fb54ee..49f39e2127 100644 --- a/packages/console/src/monaco/MonacoUtils.ts +++ b/packages/console/src/monaco/MonacoUtils.ts @@ -3,7 +3,7 @@ import shortid from 'shortid'; /** * Exports a function for initializing monaco with the deephaven theme/config */ -import { Shortcut } from '@deephaven/components'; +import { resolveCssVariablesInRecord, Shortcut } from '@deephaven/components'; import type { IdeSession } from '@deephaven/jsapi-types'; import { assertNotNull } from '@deephaven/utils'; import { find as linkifyFind } from 'linkifyjs'; @@ -12,7 +12,7 @@ import type { Environment } from 'monaco-editor'; // @ts-ignore import { KeyCodeUtils } from 'monaco-editor/esm/vs/base/common/keyCodes.js'; import Log from '@deephaven/log'; -import MonacoTheme from './MonacoTheme.module.scss'; +import MonacoThemeRaw from './MonacoTheme.module.scss'; import PyLang from './lang/python'; import GroovyLang from './lang/groovy'; import ScalaLang from './lang/scala'; @@ -45,6 +45,10 @@ class MonacoUtils { const { registerLanguages, removeHashtag } = MonacoUtils; + const MonacoTheme = resolveCssVariablesInRecord(MonacoThemeRaw); + log.debug2('Monaco theme:', MonacoThemeRaw); + log.debug2('Monaco theme derived:', MonacoTheme); + const dhDarkRules = [ { token: '', foreground: removeHashtag(MonacoTheme.foreground) }, { token: 'string', foreground: removeHashtag(MonacoTheme.string) }, @@ -154,7 +158,7 @@ class MonacoUtils { rules: dhDarkRules, colors: dhDarkColors, }); - log.debug2('monaco theme: ', MonacoTheme); + monaco.editor.setTheme('dh-dark'); registerLanguages([DbLang, PyLang, GroovyLang, LogLang, ScalaLang]); diff --git a/packages/golden-layout/scss/goldenlayout-dark-theme.scss b/packages/golden-layout/scss/goldenlayout-dark-theme.scss index d80d933f19..8bf2c972a7 100644 --- a/packages/golden-layout/scss/goldenlayout-dark-theme.scss +++ b/packages/golden-layout/scss/goldenlayout-dark-theme.scss @@ -63,7 +63,7 @@ body:not(.lm_dragging) .lm_header .lm_tab .lm_close_tab:hover { // Entire GoldenLayout Container, if a background is set, it is visible as color of "pane header" and "splitters" (if these latest has opacity very low) .lm_goldenlayout { - background: var(--dh-background-color, $background); + background: var(--dh-color-background, $background); position: absolute; } diff --git a/packages/utils/src/ColorUtils.test.ts b/packages/utils/src/ColorUtils.test.ts index 3616722903..5cc3166e82 100644 --- a/packages/utils/src/ColorUtils.test.ts +++ b/packages/utils/src/ColorUtils.test.ts @@ -1,4 +1,104 @@ import ColorUtils from './ColorUtils'; +import TestUtils from './TestUtils'; + +const { createMockProxy } = TestUtils; + +const getBackgroundColor = jest.fn(); +const setBackgroundColor = jest.fn(); + +const mockDivEl = createMockProxy({ + style: { + get backgroundColor(): string { + return getBackgroundColor(); + }, + set backgroundColor(value: string) { + setBackgroundColor(value); + }, + } as HTMLDivElement['style'], +}); + +const colorMap = [ + { + rgb: { r: 255, g: 0, b: 0 }, + hex: '#ff0000ff', + }, + { + rgb: { r: 255, g: 128, b: 0 }, + hex: '#ff8000ff', + }, + { + rgb: { r: 255, g: 255, b: 0 }, + hex: '#ffff00ff', + }, + { + rgb: { r: 128, g: 255, b: 0 }, + hex: '#80ff00ff', + }, + { + rgb: { r: 0, g: 255, b: 0 }, + hex: '#00ff00ff', + }, + { + rgb: { r: 0, g: 255, b: 128 }, + hex: '#00ff80ff', + }, + { + rgb: { r: 0, g: 255, b: 255 }, + hex: '#00ffffff', + }, + { + rgb: { r: 0, g: 128, b: 255 }, + hex: '#0080ffff', + }, + { + rgb: { r: 0, g: 0, b: 255 }, + hex: '#0000ffff', + }, + { + rgb: { r: 128, g: 0, b: 255 }, + hex: '#8000ffff', + }, + { + rgb: { r: 255, g: 0, b: 255 }, + hex: '#ff00ffff', + }, + { + rgb: { r: 255, g: 0, b: 128 }, + hex: '#ff0080ff', + }, +]; + +beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + expect.hasAssertions(); + + getBackgroundColor.mockName('getBackgroundColor'); + setBackgroundColor.mockName('setBackgroundColor'); +}); + +describe('asRgbOrRgbaString', () => { + beforeEach(() => { + jest + .spyOn(document, 'createElement') + .mockName('createElement') + .mockReturnValue(mockDivEl); + }); + + it('should return resolved backgroundColor value', () => { + getBackgroundColor.mockReturnValue('get backgroundColor'); + + const actual = ColorUtils.asRgbOrRgbaString('red'); + expect(actual).toEqual('get backgroundColor'); + }); + + it('should return null if backgroundColor resolves to empty string', () => { + getBackgroundColor.mockReturnValue(''); + + const actual = ColorUtils.asRgbOrRgbaString('red'); + expect(actual).toBeNull(); + }); +}); describe('isDark', () => { it('returns true if the background is dark', () => { @@ -21,3 +121,83 @@ describe('isDark', () => { expect(() => ColorUtils.isDark('')).toThrowError(/Invalid color received/); }); }); + +describe('normalizeCssColor', () => { + beforeEach(() => { + jest + .spyOn(document, 'createElement') + .mockName('createElement') + .mockReturnValue(mockDivEl); + }); + + it.each([ + 'rgb(0, 128, 255)', + 'rgba(0, 128, 255, 64)', + 'rgb(0 128 255)', + 'rgba(0 128 255 64)', + ])( + 'should normalize a resolved rgb/a color to 8 character hex value', + rgbOrRgbaColor => { + getBackgroundColor.mockReturnValue(rgbOrRgbaColor); + + const actual = ColorUtils.normalizeCssColor('some.color'); + expect(actual).toEqual( + ColorUtils.rgbaToHex8(ColorUtils.parseRgba(rgbOrRgbaColor)!) + ); + } + ); + + it('should return original color if backgroundColor resolves to empty string', () => { + getBackgroundColor.mockReturnValue(''); + + const actual = ColorUtils.normalizeCssColor('red'); + expect(actual).toEqual('red'); + }); + + it('should return original color if backgroundColor resolves to non rgb/a', () => { + getBackgroundColor.mockReturnValue('xxx'); + + const actual = ColorUtils.normalizeCssColor('red'); + expect(actual).toEqual('red'); + }); +}); + +describe('parseRgba', () => { + it.each([ + ['rgb(255, 255, 255)', { r: 255, g: 255, b: 255, a: 1 }], + ['rgb(0,0,0)', { r: 0, g: 0, b: 0, a: 1 }], + ['rgb(255 255 255)', { r: 255, g: 255, b: 255, a: 1 }], + ['rgb(0 0 0)', { r: 0, g: 0, b: 0, a: 1 }], + ['rgb(0 128 255)', { r: 0, g: 128, b: 255, a: 1 }], + ['rgb(0 128 255 / .5)', { r: 0, g: 128, b: 255, a: 0.5 }], + ])('should parse rgb: %s, %s', (rgb, hex) => { + expect(ColorUtils.parseRgba(rgb)).toEqual(hex); + }); + + it.each([ + ['rgba(255, 255, 255, 1)', { r: 255, g: 255, b: 255, a: 1 }], + ['rgba(0,0,0,0)', { r: 0, g: 0, b: 0, a: 0 }], + ['rgba(255 255 255 1)', { r: 255, g: 255, b: 255, a: 1 }], + ['rgba(0 0 0 0)', { r: 0, g: 0, b: 0, a: 0 }], + ['rgba(0 128 255 .5)', { r: 0, g: 128, b: 255, a: 0.5 }], + ])('should parse rgba: %s, %s', (rgba, hex) => { + expect(ColorUtils.parseRgba(rgba)).toEqual(hex); + }); + + it('should return null if not rgb or rgba', () => { + expect(ColorUtils.parseRgba('xxx')).toBeNull(); + }); + + it.each(['rgb(0 128)', 'rgba(0 128)', 'rgb(0, 128)', 'rgba(0, 128)'])( + 'should return null if given < 3 args', + value => { + expect(ColorUtils.parseRgba(value)).toBeNull(); + } + ); +}); + +describe('rgbaToHex8', () => { + it.each(colorMap)('should convert rgb to hex: %s, %s', ({ rgb, hex }) => { + expect(ColorUtils.rgbaToHex8(rgb)).toEqual(hex); + }); +}); diff --git a/packages/utils/src/ColorUtils.ts b/packages/utils/src/ColorUtils.ts index 6531fedec0..560b9fd6aa 100644 --- a/packages/utils/src/ColorUtils.ts +++ b/packages/utils/src/ColorUtils.ts @@ -1,4 +1,20 @@ class ColorUtils { + /** + * Attempt to get the rgb or rgba string for a color string. If the color string + * can't be resolved to a valid color, null is returned. + * @param colorString The color string to resolve + */ + static asRgbOrRgbaString(colorString: string): string | null { + const divEl = document.createElement('div'); + divEl.style.backgroundColor = colorString; + + if (divEl.style.backgroundColor === '') { + return null; + } + + return divEl.style.backgroundColor; + } + /** * THIS HAS POOR PERFORMANCE DUE TO DOM MANIPULATION * DO NOT USE HEAVILY @@ -32,5 +48,93 @@ class ColorUtils { (color[0] * 299 + color[1] * 587 + color[2] * 114) / 1000 ); } + + /** + * Normalize a css color to 8 character hex value. If the color can't be resolved, + * return the original color string. + * @param colorString The color string to normalize + */ + static normalizeCssColor(colorString: string): string { + const maybeRgbOrRgba = ColorUtils.asRgbOrRgbaString(colorString); + if (maybeRgbOrRgba == null) { + return colorString; + } + + const rgba = ColorUtils.parseRgba(maybeRgbOrRgba); + if (rgba === null) { + return colorString; + } + + return ColorUtils.rgbaToHex8(rgba); + } + + /** + * Parse a given `rgb` or `rgba` css expression into its constituent r, g, b, a + * values. If the expression cannot be parsed, it will return null. + * Note that this parser is more permissive than the CSS spec and shouldn't be + * relied on as a full validation mechanism. For the most part, it assumes that + * the input is already a valid rgb or rgba expression. + * + * e.g. `rgb(255, 255, 255)` -> `{ r: 255, g: 255, b: 255, a: 1 }` + * e.g. `rgba(255, 255, 255, 0.5)` -> `{ r: 255, g: 255, b: 255, a: 0.5 }` + * @param rgbOrRgbaString The rgb or rgba string to parse + */ + static parseRgba( + rgbOrRgbaString: string + ): { r: number; g: number; b: number; a: number } | null { + const [, name, args] = /^(rgba?)\((.*?)\)$/.exec(rgbOrRgbaString) ?? []; + if (name == null) { + return null; + } + + // Split on spaces, commas, and slashes. Note that this more permissive than + // the CSS spec in that slashes should only be used to delimit the alpha value + // (e.g. r g b / a), but this would match r/g/b/a. It also would match a mixed + // delimiter case (e.g. r,g b,a). This seems like a reasonable tradeoff for the + // complexity that would be added to enforce the full spec. + const tokens = args.split(/[ ,/]/).filter(Boolean); + + if (tokens.length < 3) { + return null; + } + + const [r, g, b, a = 1] = tokens.map(Number); + + return { + r, + g, + b, + a, + }; + } + + /** + * Convert an rgba object to an 8 character hex color string. + * @param r The red value + * @param g The green value + * @param b The blue value + * @param a The alpha value (defaults to 1) + * @returns The a character hex string with # prefix + */ + static rgbaToHex8({ + r, + g, + b, + a = 1, + }: { + r: number; + g: number; + b: number; + a?: number; + }): string { + // eslint-disable-next-line no-param-reassign + a = Math.round(a * 255); + + const [rh, gh, bh, ah] = [r, g, b, a].map(v => + v.toString(16).padStart(2, '0') + ); + + return `#${rh}${gh}${bh}${ah}`; + } } export default ColorUtils;