diff --git a/package.json b/package.json index 372c447c3b..8a07e6a60e 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "comlink": "4.4.2", "core-js": "3.44.0", "cropperjs": "1.6.2", + "culori": "4.0.2", "date-fns": "4.1.0", "date-fns-tz": "3.2.0", "deep-clone-simple": "1.1.1", @@ -164,6 +165,7 @@ "@types/chromecast-caf-receiver": "6.0.22", "@types/chromecast-caf-sender": "1.0.11", "@types/color-name": "2.0.0", + "@types/culori": "4", "@types/html-minifier-terser": "7.0.2", "@types/js-yaml": "4.0.9", "@types/leaflet": "1.9.20", diff --git a/src/common/color/palette.ts b/src/common/color/palette.ts new file mode 100644 index 0000000000..6e657922cb --- /dev/null +++ b/src/common/color/palette.ts @@ -0,0 +1,115 @@ +import { formatHex, oklch, wcagLuminance, type Oklch } from "culori"; + +const MIN_LUMINANCE = 0.3; +const MAX_LUMINANCE = 0.6; + +/** + * Normalizes the luminance of a given color to ensure it falls within the specified minimum and maximum luminance range. + * This helps to keep everything readable and accessible, especially for text and UI elements. + * + * This function converts the input color to the OKLCH color space, calculates its luminance, + * and adjusts the lightness component if the luminance is outside the allowed range. + * The adjustment is performed using a binary search to find the appropriate lightness value. + * If the color is already within the range, it is returned unchanged. + * + * @param color - css color string + * @returns The normalized color as a hex string, or the original color if normalization is not needed. + * @throws If the provided color is invalid or cannot be parsed. + */ +export const normalizeLuminance = (color: string): string => { + const baseOklch = oklch(color); + + if (baseOklch === undefined) { + throw new Error("Invalid color provided"); + } + + const luminance = wcagLuminance(baseOklch); + + if (luminance >= MIN_LUMINANCE && luminance <= MAX_LUMINANCE) { + return color; + } + const targetLuminance = + luminance < MIN_LUMINANCE ? MIN_LUMINANCE : MAX_LUMINANCE; + + function findLightness(lowL = 0, highL = 1, iterations = 10) { + if (iterations <= 0) { + return (lowL + highL) / 2; + } + + const midL = (lowL + highL) / 2; + const testColor = { ...baseOklch, l: midL } as Oklch; + const testLuminance = wcagLuminance(testColor); + + if (Math.abs(testLuminance - targetLuminance) < 0.01) { + return midL; + } + if (testLuminance < targetLuminance) { + return findLightness(midL, highL, iterations--); + } + return findLightness(lowL, midL, iterations--); + } + + baseOklch.l = findLightness(); + + return formatHex(baseOklch) || color; +}; + +/** + * Generates a color palette based on a base color using the OKLCH color space. + * + * The palette consists of multiple shades, both lighter and darker than the base color, + * calculated by adjusting the lightness and chroma values. Each shade is labeled and + * returned as a tuple containing the shade name and its hexadecimal color value. + * + * @param baseColor - The base color in a HEX format. + * @param label - A string label used to name each color variant in the palette. + * @param steps - An array of numbers representing the percentage steps for generating shades (default: [5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95]). + * @returns An array of tuples, each containing the shade name and its corresponding hex color value. + * @throws If the provided base color is invalid or cannot be parsed by the `oklch` function. + */ +export const generateColorPalette = ( + baseColor: string, + label: string, + steps = [5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95] +) => { + const baseOklch = oklch(baseColor); + + if (baseOklch === undefined) { + throw new Error("Invalid base color provided"); + } + + return steps.map((step) => { + const name = `color-${label}-${step}`; + + // Base color at 50% + if (step === 50) { + return [name, formatHex(baseOklch)]; + } + + // For darker shades (below 50%) + if (step < 50) { + const darkFactor = step / 50; + + // Adjust lightness and chroma to create darker variants + const darker = { + ...baseOklch, + l: baseOklch.l * darkFactor, // darkening + c: baseOklch.c * (0.9 + 0.1 * darkFactor), // Slightly adjust chroma + }; + + return [name, formatHex(darker)]; + } + + // For lighter shades (above 50%) + const lightFactor = (step - 50) / 45; // Normalized from 0 to 1 + + // Adjust lightness and reduce chroma for lighter variants + const lighter = { + ...baseOklch, + l: Math.min(1, baseOklch.l + (1 - baseOklch.l) * lightFactor), // Increase lightness + c: baseOklch.c * Math.max(0, 1 - lightFactor * 0.7), // Gradually reduce chroma + }; + + return [name, formatHex(lighter)]; + }); +}; diff --git a/src/common/dom/apply_themes_on_element.ts b/src/common/dom/apply_themes_on_element.ts index b986e053a8..fd6dffe3dd 100644 --- a/src/common/dom/apply_themes_on_element.ts +++ b/src/common/dom/apply_themes_on_element.ts @@ -1,5 +1,5 @@ import type { ThemeVars } from "../../data/ws-themes"; -import { darkColorVariables } from "../../resources/theme/color.globals"; +import { darkColorVariables } from "../../resources/theme/color"; import { derivedStyles } from "../../resources/theme/theme"; import type { HomeAssistant } from "../../types"; import { @@ -11,6 +11,7 @@ import { } from "../color/convert-color"; import { hexBlend } from "../color/hex"; import { labBrighten, labDarken } from "../color/lab"; +import { generateColorPalette } from "../color/palette"; import { rgbContrast } from "../color/rgb"; interface ProcessedTheme { @@ -75,6 +76,11 @@ export const applyThemesOnElement = ( const labPrimaryColor = rgb2lab(rgbPrimaryColor); themeRules["primary-color"] = primaryColor; const rgbLightPrimaryColor = lab2rgb(labBrighten(labPrimaryColor)); + + generateColorPalette(primaryColor, "primary").forEach(([key, color]) => { + themeRules[key] = color; + }); + themeRules["light-primary-color"] = rgb2hex(rgbLightPrimaryColor); themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor)); themeRules["text-primary-color"] = diff --git a/src/common/style/derived-css-vars.ts b/src/common/style/derived-css-vars.ts index 3b2add7e1b..14fe45ae78 100644 --- a/src/common/style/derived-css-vars.ts +++ b/src/common/style/derived-css-vars.ts @@ -18,7 +18,43 @@ const _extractCssVars = ( return variables; }; -export const extractVar = (css: CSSResult, varName: string) => { +/** + * Recursively resolves a CSS variable reference from a base variable map. + * + * If the value of the specified variable in `baseVars` is itself a CSS variable reference + * (i.e., starts with `var(`), this function will recursively resolve the reference until + * it finds a concrete value or reaches an undefined variable. + * + * @param varName - The name of the CSS variable to resolve. + * @param baseVars - A record mapping variable names to their values or references. + * @returns The resolved value of the variable, or `undefined` if not found. + */ +const extractVarFromBase = ( + varName: string, + baseVars: Record +): string | undefined => { + if (baseVars[varName] && baseVars[varName].startsWith("var(")) { + const baseVarName = baseVars[varName] + .substring(6, baseVars[varName].length - 1) + .trim(); + return extractVarFromBase(baseVarName, baseVars); + } + return baseVars[varName]; +}; + +/** + * Extracts the value of a CSS custom property (CSS variable) from a given CSSResult object. + * + * @param css - The CSSResult object containing the CSS string to search. + * @param varName - The name of the CSS variable (without the leading '--') to extract. + * @param baseVars - (Optional) A record of base variable names and their values, used to resolve variables that reference other variables via `var()`. + * @returns The value of the CSS variable if found, otherwise an empty string. If the variable references another variable and `baseVars` is provided, attempts to resolve it from `baseVars`. + */ +export const extractVar = ( + css: CSSResult, + varName: string, + baseVars?: Record +) => { const cssString = css.toString(); const search = `--${varName}:`; const startIndex = cssString.indexOf(search); @@ -27,10 +63,17 @@ export const extractVar = (css: CSSResult, varName: string) => { } const endIndex = cssString.indexOf(";", startIndex + search.length); - return cssString + const value = cssString .substring(startIndex + search.length, endIndex) .replaceAll("}", "") .trim(); + + if (baseVars && value.startsWith("var(")) { + const baseVarName = value.substring(6, value.length - 1).trim(); + return extractVarFromBase(baseVarName, baseVars) || value; + } + + return value; }; export const extractVars = (css: CSSResult) => { diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index e6a0bef350..22249c6889 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -29,7 +29,7 @@ import { formatTimeLabel } from "./axis-label"; import { ensureArray } from "../../common/array/ensure-array"; import "../chips/ha-assist-chip"; import { downSampleLineData } from "./down-sample"; -import { colorVariables } from "../../resources/theme/color.globals"; +import { colorVariables } from "../../resources/theme/color/color.globals"; export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; const LEGEND_OVERFLOW_LIMIT = 10; diff --git a/src/panels/config/hardware/ha-config-hardware.ts b/src/panels/config/hardware/ha-config-hardware.ts index 0b1beadf79..0f5547723f 100644 --- a/src/panels/config/hardware/ha-config-hardware.ts +++ b/src/panels/config/hardware/ha-config-hardware.ts @@ -1,9 +1,9 @@ import { mdiPower } from "@mdi/js"; +import type { SeriesOption } from "echarts/types/dist/shared"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import type { SeriesOption } from "echarts/types/dist/shared"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { round } from "../../../common/number/round"; @@ -12,9 +12,9 @@ import "../../../components/buttons/ha-progress-button"; import "../../../components/chart/ha-chart-base"; import "../../../components/ha-alert"; import "../../../components/ha-card"; -import "../../../components/ha-md-list-item"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-next"; +import "../../../components/ha-md-list-item"; import "../../../components/ha-settings-row"; import type { ConfigEntry } from "../../../data/config_entries"; import { subscribeConfigEntries } from "../../../data/config_entries"; @@ -23,6 +23,7 @@ import type { SystemStatusStreamMessage, } from "../../../data/hardware"; import { BOARD_NAMES } from "../../../data/hardware"; +import { extractApiErrorMessage } from "../../../data/hassio/common"; import type { HassioHassOSInfo } from "../../../data/hassio/host"; import { fetchHassioHassOsInfo } from "../../../data/hassio/host"; import { scanUSBDevices } from "../../../data/usb"; @@ -30,13 +31,12 @@ import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog- import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart"; import "../../../layouts/hass-subpage"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; -import { DefaultPrimaryColor } from "../../../resources/theme/color.globals"; +import type { ECOption } from "../../../resources/echarts"; import { haStyle } from "../../../resources/styles"; +import { DefaultPrimaryColor } from "../../../resources/theme/color/color.globals"; import type { HomeAssistant } from "../../../types"; import { hardwareBrandsUrl } from "../../../util/brands-url"; import { showhardwareAvailableDialog } from "./show-dialog-hardware-available"; -import { extractApiErrorMessage } from "../../../data/hassio/common"; -import type { ECOption } from "../../../resources/echarts"; const DATASAMPLES = 60; diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts index ee4e2bb8fd..562e6553a8 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-network-visualization.ts @@ -1,18 +1,20 @@ -import { html, LitElement, css } from "lit"; -import type { CSSResultGroup } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CallbackDataParams, TopLevelFormatterParams, } from "echarts/types/dist/shared"; +import type { UnsubscribeFunc } from "home-assistant-js-websocket"; +import type { CSSResultGroup } from "lit"; +import { css, html, LitElement } from "lit"; +import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import type { HomeAssistant, Route } from "../../../../../types"; +import { relativeTime } from "../../../../../common/datetime/relative_time"; +import { navigate } from "../../../../../common/navigate"; +import { throttle } from "../../../../../common/util/throttle"; import "../../../../../components/chart/ha-network-graph"; import type { NetworkData, - NetworkNode, NetworkLink, + NetworkNode, } from "../../../../../components/chart/ha-network-graph"; import type { BluetoothDeviceData, @@ -24,11 +26,9 @@ import { } from "../../../../../data/bluetooth"; import type { DeviceRegistryEntry } from "../../../../../data/device_registry"; import "../../../../../layouts/hass-subpage"; -import { colorVariables } from "../../../../../resources/theme/color.globals"; -import { navigate } from "../../../../../common/navigate"; +import { colorVariables } from "../../../../../resources/theme/color/color.globals"; +import type { HomeAssistant, Route } from "../../../../../types"; import { bluetoothAdvertisementMonitorTabs } from "./bluetooth-advertisement-monitor"; -import { relativeTime } from "../../../../../common/datetime/relative_time"; -import { throttle } from "../../../../../common/util/throttle"; const UPDATE_THROTTLE_TIME = 10000; diff --git a/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts b/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts index 657f8a8209..5a5481c837 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts @@ -1,26 +1,26 @@ import "@material/mwc-button"; -import type { CSSResultGroup, PropertyValues } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { mdiRefresh } from "@mdi/js"; import type { CallbackDataParams, TopLevelFormatterParams, } from "echarts/types/dist/shared"; -import { mdiRefresh } from "@mdi/js"; +import type { CSSResultGroup, PropertyValues } from "lit"; +import { css, html, LitElement } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { navigate } from "../../../../../common/navigate"; import "../../../../../components/chart/ha-network-graph"; import type { NetworkData, - NetworkNode, NetworkLink, + NetworkNode, } from "../../../../../components/chart/ha-network-graph"; import type { ZHADevice } from "../../../../../data/zha"; import { fetchDevices, refreshTopology } from "../../../../../data/zha"; import "../../../../../layouts/hass-tabs-subpage"; +import { colorVariables } from "../../../../../resources/theme/color/color.globals"; import type { HomeAssistant, Route } from "../../../../../types"; import { formatAsPaddedHex } from "./functions"; import { zhaTabs } from "./zha-config-dashboard"; -import { colorVariables } from "../../../../../resources/theme/color.globals"; -import { navigate } from "../../../../../common/navigate"; @customElement("zha-network-visualization-page") export class ZHANetworkVisualizationPage extends LitElement { diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-network-visualization.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-network-visualization.ts index 444b2eb0a8..74e08d335d 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-network-visualization.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-network-visualization.ts @@ -1,33 +1,33 @@ -import { customElement, property, state } from "lit/decorators"; -import { css, html, LitElement } from "lit"; -import memoizeOne from "memoize-one"; import type { CallbackDataParams, TopLevelFormatterParams, } from "echarts/types/dist/shared"; -import type { HomeAssistant, Route } from "../../../../../types"; -import { configTabs } from "./zwave_js-config-router"; -import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin"; +import { css, html, LitElement } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { navigate } from "../../../../../common/navigate"; +import { debounce } from "../../../../../common/util/debounce"; +import "../../../../../components/chart/ha-network-graph"; import type { NetworkData, NetworkLink, NetworkNode, } from "../../../../../components/chart/ha-network-graph"; -import "../../../../../components/chart/ha-network-graph"; -import "../../../../../layouts/hass-tabs-subpage"; +import type { DeviceRegistryEntry } from "../../../../../data/device_registry"; +import type { + ZWaveJSNodeStatisticsUpdatedMessage, + ZWaveJSNodeStatus, +} from "../../../../../data/zwave_js"; import { fetchZwaveNetworkStatus, NodeStatus, subscribeZwaveNodeStatistics, } from "../../../../../data/zwave_js"; -import type { - ZWaveJSNodeStatisticsUpdatedMessage, - ZWaveJSNodeStatus, -} from "../../../../../data/zwave_js"; -import { colorVariables } from "../../../../../resources/theme/color.globals"; -import type { DeviceRegistryEntry } from "../../../../../data/device_registry"; -import { debounce } from "../../../../../common/util/debounce"; -import { navigate } from "../../../../../common/navigate"; +import "../../../../../layouts/hass-tabs-subpage"; +import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin"; +import { colorVariables } from "../../../../../resources/theme/color/color.globals"; +import type { HomeAssistant, Route } from "../../../../../types"; +import { configTabs } from "./zwave_js-config-router"; @customElement("zwave_js-network-visualization") export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) { diff --git a/src/panels/profile/ha-pick-theme-row.ts b/src/panels/profile/ha-pick-theme-row.ts index edd4cece83..3ec2abccd9 100644 --- a/src/panels/profile/ha-pick-theme-row.ts +++ b/src/panels/profile/ha-pick-theme-row.ts @@ -3,6 +3,7 @@ import "@material/mwc-button/mwc-button"; import type { PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { normalizeLuminance } from "../../common/color/palette"; import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-formfield"; import "../../components/ha-list-item"; @@ -14,7 +15,7 @@ import "../../components/ha-textfield"; import { DefaultAccentColor, DefaultPrimaryColor, -} from "../../resources/theme/color.globals"; +} from "../../resources/theme/color/color.globals"; import type { HomeAssistant } from "../../types"; import { documentationUrl } from "../../util/documentation-url"; @@ -171,6 +172,12 @@ export class HaPickThemeRow extends LitElement { private _handleColorChange(ev: CustomEvent) { const target = ev.target as any; + + // normalize primary color if needed for contrast + if (target.name === "primaryColor") { + target.value = normalizeLuminance(target.value); + } + fireEvent(this, "settheme", { [target.name]: target.value }); } diff --git a/src/resources/particles.ts b/src/resources/particles.ts index a2ae5912ab..6574be4fef 100644 --- a/src/resources/particles.ts +++ b/src/resources/particles.ts @@ -1,6 +1,6 @@ import { tsParticles } from "@tsparticles/engine"; import { loadLinksPreset } from "@tsparticles/preset-links"; -import { DefaultPrimaryColor } from "./theme/color.globals"; +import { DefaultPrimaryColor } from "./theme/color/color.globals"; loadLinksPreset(tsParticles).then(() => { tsParticles.load({ diff --git a/src/resources/theme/color.globals.ts b/src/resources/theme/color/color.globals.ts similarity index 97% rename from src/resources/theme/color.globals.ts rename to src/resources/theme/color/color.globals.ts index 56ceb72acb..163129d34b 100644 --- a/src/resources/theme/color.globals.ts +++ b/src/resources/theme/color/color.globals.ts @@ -1,21 +1,21 @@ import { css } from "lit"; import { - extractDerivedVars, extractVar, extractVars, -} from "../../common/style/derived-css-vars"; +} from "../../../common/style/derived-css-vars"; +import { coreColorVariables } from "./core.globals"; export const colorStyles = css` html { /* text */ - --primary-text-color: #212121; - --secondary-text-color: #727272; + --primary-text-color: var(--color-text-primary); + --secondary-text-color: var(--color-text-secondary); --text-primary-color: #ffffff; --text-light-primary-color: #212121; --disabled-text-color: #bdbdbd; /* main interface colors */ - --primary-color: #03a9f4; + --primary-color: var(--color-primary-40); --dark-primary-color: #0288d1; --light-primary-color: #b3e5fc; --accent-color: #ff9800; @@ -24,7 +24,7 @@ export const colorStyles = css` --outline-hover-color: rgba(0, 0, 0, 0.24); /* rgb */ - --rgb-primary-color: 3, 169, 244; + --rgb-primary-color: 0, 154, 199; --rgb-accent-color: 255, 152, 0; --rgb-primary-text-color: 33, 33, 33; --rgb-secondary-text-color: 114, 114, 114; @@ -303,7 +303,7 @@ export const colorStyles = css` } `; -const darkColorStyles = css` +export const darkColorStyles = css` html { --primary-background-color: #111111; --card-background-color: #1c1c1c; @@ -359,9 +359,11 @@ const darkColorStyles = css` --disabled-color: #464646; } `; -export const colorDerivedVariables = extractDerivedVars(colorStyles); export const colorVariables = extractVars(colorStyles); -export const darkColorVariables = extractVars(darkColorStyles); -export const DefaultPrimaryColor = extractVar(colorStyles, "primary-color"); +export const DefaultPrimaryColor = extractVar( + colorStyles, + "primary-color", + coreColorVariables +); export const DefaultAccentColor = extractVar(colorStyles, "accent-color"); diff --git a/src/resources/theme/color/core.globals.ts b/src/resources/theme/color/core.globals.ts new file mode 100644 index 0000000000..791a2cf968 --- /dev/null +++ b/src/resources/theme/color/core.globals.ts @@ -0,0 +1,77 @@ +import { css } from "lit"; +import { extractVars } from "../../../common/style/derived-css-vars"; + +export const coreColorStyles = css` + html { + --white: #ffffff; + --black: #000000; + --transparent-none: rgba(255, 255, 255, 0); + + /* primary */ + --color-primary-05: #001721; + --color-primary-10: #002e3e; + --color-primary-20: #004156; + --color-primary-30: #006787; + --color-primary-40: #009ac7; + --color-primary-50: #18bcf2; + --color-primary-60: #37c8fd; + --color-primary-70: #7bd4fb; + --color-primary-80: #b9e6fc; + --color-primary-90: #b9e6fc; + --color-primary-95: #eff9fe; + + /* neutral */ + --color-neutral-05: #101219; + --color-neutral-10: #1b1d26; + --color-neutral-20: #2f323f; + --color-neutral-30: #424554; + --color-neutral-40: #545868; + --color-neutral-50: #717584; + --color-neutral-60: #9194a2; + --color-neutral-70: #abaeb9; + --color-neutral-80: #c7c9d0; + --color-neutral-90: #e4e5e9; + --color-neutral-95: #f1f2f3; + + /* orange */ + --color-orange-05: #280700; + --color-orange-10: #3b0f00; + --color-orange-20: #5e1c00; + --color-orange-30: #7e2900; + --color-orange-40: #9d3800; + --color-orange-50: #c94e00; + --color-orange-60: #f36d00; + --color-orange-70: #ff9342; + --color-orange-80: #ffbb89; + --color-orange-90: #ffe0c8; + --color-orange-95: #fff0e4; + + /* red */ + --color-red-05: #2a040b; + --color-red-10: #3e0913; + --color-red-20: #631323; + --color-red-30: #8a132c; + --color-red-40: #b30532; + --color-red-50: #dc3146; + --color-red-60: #f3676c; + --color-red-70: #fd8f90; + --color-red-80: #ffb8b6; + --color-red-90: #ffdedc; + --color-red-95: #fff0ef; + + /* green */ + --color-green-05: #031608; + --color-green-10: #052310; + --color-green-20: #0a3a1d; + --color-green-30: #0a5027; + --color-green-40: #036730; + --color-green-50: #00883c; + --color-green-60: #00ac49; + --color-green-70: #5dc36f; + --color-green-80: #93da98; + --color-green-90: #c2f2c1; + --color-green-95: #e3f9e3; + } +`; + +export const coreColorVariables = extractVars(coreColorStyles); diff --git a/src/resources/theme/color/index.ts b/src/resources/theme/color/index.ts new file mode 100644 index 0000000000..c67135235b --- /dev/null +++ b/src/resources/theme/color/index.ts @@ -0,0 +1,26 @@ +import { + extractDerivedVars, + extractVars, +} from "../../../common/style/derived-css-vars"; +import { colorStyles, darkColorStyles } from "./color.globals"; +import { coreColorStyles } from "./core.globals"; +import { + darkSemanticColorStyles, + semanticColorStyles, +} from "./semantic.globals"; + +export const darkColorVariables = { + ...extractVars(darkColorStyles), + ...extractVars(darkSemanticColorStyles), +}; + +export const colorDerivedVariables = { + ...extractDerivedVars(colorStyles), + ...extractDerivedVars(semanticColorStyles), +}; + +export const colorStylesCollection = [ + coreColorStyles.toString(), + semanticColorStyles.toString(), + colorStyles.toString(), +]; diff --git a/src/resources/theme/color/semantic.globals.ts b/src/resources/theme/color/semantic.globals.ts new file mode 100644 index 0000000000..f271901e89 --- /dev/null +++ b/src/resources/theme/color/semantic.globals.ts @@ -0,0 +1,293 @@ +import { css } from "lit"; + +export const semanticColorStyles = css` + html { + --color-overlay-modal: rgba(0, 0, 0, 0.25); + --color-focus: var(--color-orange-60); + + /* surface */ + --color-surface-lower: var(--color-neutral-90); + --color-surface-low: var(--color-neutral-95); + --color-surface-default: var(--white); + + /* text */ + --color-text-primary: var(--color-neutral-05); + --color-text-secondary: var(--color-neutral-40); + --color-text-disabled: var(--color-neutral-60); + --color-text-link: var(--color-primary-40); + /* border primary */ + --color-border-quiet: var(--color-primary-80); + --color-border-normal: var(--color-primary-70); + --color-border-loud: var(--color-primary-40); + + /* border neutral */ + --color-border-neutral-quiet: var(--color-neutral-80); + --color-border-neutral-normal: var(--color-neutral-60); + --color-border-neutral-loud: var(--color-neutral-40); + + /* border danger */ + --color-border-danger-quiet: var(--color-red-80); + --color-border-danger-normal: var(--color-red-70); + --color-border-danger-loud: var(--color-red-40); + + /* border warning */ + --color-border-warning-quiet: var(--color-orange-80); + --color-border-warning-normal: var(--color-orange-70); + --color-border-warning-loud: var(--color-orange-40); + + /* border success */ + --color-border-success-quiet: var(--color-green-80); + --color-border-success-normal: var(--color-green-70); + --color-border-success-loud: var(--color-green-40); + + /* fill primary quiet */ + --color-fill-primary-quiet-resting: var(--color-primary-95); + --color-fill-primary-quiet-hover: var(--color-primary-90); + --color-fill-primary-quiet-active: var(--color-primary-95); + + /* fill primary normal */ + --color-fill-primary-normal-resting: var(--color-primary-90); + --color-fill-primary-normal-hover: var(--color-primary-80); + --color-fill-primary-normal-active: var(--color-primary-90); + + /* fill primary loud */ + --color-fill-primary-loud-resting: var(--color-primary-40); + --color-fill-primary-loud-hover: var(--color-primary-30); + --color-fill-primary-loud-active: var(--color-primary-40); + + /* fill neutral quiet */ + --color-fill-neutral-quiet-resting: var(--color-neutral-95); + --color-fill-neutral-quiet-hover: var(--color-neutral-90); + --color-fill-neutral-quiet-active: var(--color-neutral-95); + + /* fill neutral normal */ + --color-fill-neutral-normal-resting: var(--color-neutral-90); + --color-fill-neutral-normal-hover: var(--color-neutral-80); + --color-fill-neutral-normal-active: var(--color-neutral-90); + + /* fill neutral loud */ + --color-fill-neutral-loud-resting: var(--color-neutral-40); + --color-fill-neutral-loud-hover: var(--color-neutral-30); + --color-fill-neutral-loud-active: var(--color-neutral-40); + + /* fill disabled quiet */ + --color-fill-disabled-quiet-resting: var(--color-neutral-95); + + /* fill disabled normal */ + --color-fill-disabled-normal-resting: var(--color-neutral-95); + + /* fill disabled loud */ + --color-fill-disabled-loud-resting: var(--color-neutral-80); + + /* fill danger quiet */ + --color-fill-danger-quiet-resting: var(--color-red-95); + --color-fill-danger-quiet-hover: var(--color-red-90); + --color-fill-danger-quiet-active: var(--color-red-95); + + /* fill danger normal */ + --color-fill-danger-normal-resting: var(--color-red-90); + --color-fill-danger-normal-hover: var(--color-red-80); + --color-fill-danger-normal-active: var(--color-red-90); + + /* fill danger loud */ + --color-fill-danger-loud-resting: var(--color-red-50); + --color-fill-danger-loud-hover: var(--color-red-40); + --color-fill-danger-loud-active: var(--color-red-50); + + /* fill warning quiet */ + --color-fill-warning-quiet-resting: var(--color-orange-95); + --color-fill-warning-quiet-hover: var(--color-orange-90); + --color-fill-warning-quiet-active: var(--color-orange-95); + + /* fill warning normal */ + --color-fill-warning-normal-resting: var(--color-orange-90); + --color-fill-warning-normal-hover: var(--color-orange-80); + --color-fill-warning-normal-active: var(--color-orange-90); + + /* fill warning loud */ + --color-fill-warning-loud-resting: var(--color-orange-70); + --color-fill-warning-loud-hover: var(--color-orange-50); + --color-fill-warning-loud-active: var(--color-orange-70); + + /* fill success quiet */ + --color-fill-success-quiet-resting: var(--color-green-95); + --color-fill-success-quiet-hover: var(--color-green-90); + --color-fill-success-quiet-active: var(--color-green-95); + + /* fill success normal */ + --color-fill-success-normal-resting: var(--color-green-90); + --color-fill-success-normal-hover: var(--color-green-80); + --color-fill-success-normal-active: var(--color-green-90); + + /* fill success loud */ + --color-fill-success-loud-resting: var(--color-green-50); + --color-fill-success-loud-hover: var(--color-green-40); + --color-fill-success-loud-active: var(--color-green-50); + + /* on primary */ + --color-on-primary-quiet: var(--color-primary-50); + --color-on-primary-normal: var(--color-primary-40); + --color-on-primary-loud: var(--white); + + /* on neutral */ + --color-on-neutral-quiet: var(--color-neutral-50); + --color-on-neutral-normal: var(--color-neutral-40); + --color-on-neutral-loud: var(--white); + + /* on disabled */ + --color-on-disabled-quiet: var(--color-neutral-80); + --color-on-disabled-normal: var(--color-neutral-70); + --color-on-disabled-loud: var(--color-neutral-95); + + /* on danger */ + --color-on-danger-quiet: var(--color-red-50); + --color-on-danger-normal: var(--color-red-40); + --color-on-danger-loud: var(--white); + + /* on warning */ + --color-on-warning-quiet: var(--color-orange-50); + --color-on-warning-normal: var(--color-orange-40); + --color-on-warning-loud: var(--white); + + /* on success */ + --color-on-success-quiet: var(--color-green-50); + --color-on-success-normal: var(--color-green-40); + --color-on-success-loud: var(--white); + + /* logo */ + --color-logo-primary: var(--color-primary-50); + } +`; + +export const darkSemanticColorStyles = css` + html { + /* surface */ + --color-surface-lower: var(--black); + --color-surface-low: var(--color-neutral-05); + --color-surface-default: var(--color-neutral-10); + + /* text */ + --color-text-primary: var(--white); + --color-text-secondary: var(--color-neutral-80); + --color-text-link: var(--color-primary-60); + + /* border primary */ + --color-border-normal: var(--color-primary-50); + + /* border neutral */ + --color-border-neutral-quiet: var(--color-neutral-40); + --color-border-neutral-normal: var(--color-neutral-50); + --color-border-neutral-loud: var(--color-neutral-70); + + /* border danger */ + --color-border-danger-normal: var(--color-red-50); + --color-border-danger-loud: var(--color-red-50); + + /* border warning */ + --color-border-warning-normal: var(--color-orange-50); + --color-border-warning-loud: var(--color-orange-50); + + /* fill primary quiet */ + --color-fill-primary-quiet-resting: var(--color-primary-05); + --color-fill-primary-quiet-hover: var(--color-primary-10); + --color-fill-primary-quiet-active: var(--color-primary-05); + + /* fill primary normal */ + --color-fill-primary-normal-resting: var(--color-primary-10); + --color-fill-primary-normal-hover: var(--color-primary-20); + --color-fill-primary-normal-active: var(--color-primary-10); + + /* fill neutral quiet */ + --color-fill-neutral-quiet-resting: var(--color-neutral-05); + --color-fill-neutral-quiet-hover: var(--color-neutral-10); + --color-fill-neutral-quiet-active: var(--color-neutral-00); + + /* fill neutral normal */ + --color-fill-neutral-normal-resting: var(--color-neutral-10); + --color-fill-neutral-normal-hover: var(--color-neutral-20); + --color-fill-neutral-normal-active: var(--color-neutral-10); + + /* fill disabled quiet */ + --color-fill-disabled-quiet-resting: var(--color-neutral-10); + + /* fill disabled normal */ + --color-fill-disabled-normal-resting: var(--color-neutral-20); + + /* fill disabled loud */ + --color-fill-disabled-loud-resting: var(--color-neutral-30); + + /* fill danger quiet */ + --color-fill-danger-quiet-resting: var(--color-red-05); + --color-fill-danger-quiet-hover: var(--color-red-10); + --color-fill-danger-quiet-active: var(--color-red-05); + + /* fill danger normal */ + --color-fill-danger-normal-resting: var(--color-red-10); + --color-fill-danger-normal-hover: var(--color-red-20); + --color-fill-danger-normal-active: var(--color-red-10); + + /* fill danger loud */ + --color-fill-danger-loud-resting: var(--color-red-40); + --color-fill-danger-loud-hover: var(--color-red-30); + --color-fill-danger-loud-active: var(--color-red-40); + + /* fill warning quiet */ + --color-fill-warning-quiet-resting: var(--color-orange-05); + --color-fill-warning-quiet-hover: var(--color-orange-10); + --color-fill-warning-quiet-active: var(--color-orange-05); + + /* fill warning normal */ + --color-fill-warning-normal-resting: var(--color-orange-10); + --color-fill-warning-normal-hover: var(--color-orange-20); + --color-fill-warning-normal-active: var(--color-orange-10); + + /* fill warning loud */ + --color-fill-warning-loud-resting: var(--color-orange-40); + --color-fill-warning-loud-hover: var(--color-orange-30); + --color-fill-warning-loud-active: var(--color-orange-40); + + /* fill success quiet */ + --color-fill-success-quiet-resting: var(--color-green-05); + --color-fill-success-quiet-hover: var(--color-green-10); + --color-fill-success-quiet-active: var(--color-green-05); + + /* fill success normal */ + --color-fill-success-normal-resting: var(--color-green-10); + --color-fill-success-normal-hover: var(--color-green-20); + --color-fill-success-normal-active: var(--color-green-10); + + /* fill success loud */ + --color-fill-success-loud-resting: var(--color-green-40); + --color-fill-success-loud-hover: var(--color-green-30); + --color-fill-success-loud-active: var(--color-green-40); + + /* on primary */ + --color-on-primary-quiet: var(--color-primary-70); + --color-on-primary-normal: var(--color-primary-80); + + /* on neutral */ + --color-on-neutral-quiet: var(--color-neutral-70); + --color-on-neutral-normal: var(--color-neutral-60); + --color-on-neutral-loud: var(--white); + + /* on disabled */ + --color-on-disabled-quiet: var(--color-neutral-40); + --color-on-disabled-normal: var(--color-neutral-50); + --color-on-disabled-loud: var(--color-neutral-50); + + /* on danger */ + --color-on-danger-quiet: var(--color-red-70); + --color-on-danger-normal: var(--color-red-60); + --color-on-danger-loud: var(--white); + + /* on warning */ + --color-on-warning-quiet: var(--color-orange-70); + --color-on-warning-normal: var(--color-orange-60); + --color-on-warning-loud: var(--white); + + /* on success */ + --color-on-success-quiet: var(--color-green-70); + --color-on-success-normal: var(--color-green-60); + --color-on-success-loud: var(--white); + } +`; diff --git a/src/resources/theme/theme.ts b/src/resources/theme/theme.ts index defc01a2d7..3890144d5d 100644 --- a/src/resources/theme/theme.ts +++ b/src/resources/theme/theme.ts @@ -1,5 +1,5 @@ import { fontStyles } from "../roboto"; -import { colorDerivedVariables, colorStyles } from "./color.globals"; +import { colorDerivedVariables, colorStylesCollection } from "./color"; import { mainDerivedVariables, mainStyles } from "./main.globals"; import { typographyDerivedVariables, @@ -9,7 +9,7 @@ import { export const themeStyles = [ mainStyles.toString(), typographyStyles.toString(), - colorStyles.toString(), + ...colorStylesCollection, fontStyles.toString(), ].join(""); diff --git a/yarn.lock b/yarn.lock index f7b5907ec1..fc6736f595 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4474,6 +4474,13 @@ __metadata: languageName: node linkType: hard +"@types/culori@npm:4": + version: 4.0.0 + resolution: "@types/culori@npm:4.0.0" + checksum: 10/62a9058d6125fe489ca1e7df27ac9837ea7a34c772b8bed8e5e00177b141574830efaa0c93363e9532878490d3245a9c9c8183ebee181a450097584af0cfefc1 + languageName: node + linkType: hard + "@types/deep-eql@npm:*": version: 4.0.2 resolution: "@types/deep-eql@npm:4.0.2" @@ -7040,6 +7047,13 @@ __metadata: languageName: node linkType: hard +"culori@npm:4.0.2": + version: 4.0.2 + resolution: "culori@npm:4.0.2" + checksum: 10/9d297ca5c6fc86b2637200e1d9edb5a7d1015a2e978a6748781ef53c9577269ce1df96dc0925b0002239c85665ba881a3d580564dfd2807c65ee65dc2e829849 + languageName: node + linkType: hard + "data-urls@npm:^5.0.0": version: 5.0.0 resolution: "data-urls@npm:5.0.0" @@ -9388,6 +9402,7 @@ __metadata: "@types/chromecast-caf-receiver": "npm:6.0.22" "@types/chromecast-caf-sender": "npm:1.0.11" "@types/color-name": "npm:2.0.0" + "@types/culori": "npm:4" "@types/html-minifier-terser": "npm:7.0.2" "@types/js-yaml": "npm:4.0.9" "@types/leaflet": "npm:1.9.20" @@ -9417,6 +9432,7 @@ __metadata: comlink: "npm:4.4.2" core-js: "npm:3.44.0" cropperjs: "npm:1.6.2" + culori: "npm:4.0.2" date-fns: "npm:4.1.0" date-fns-tz: "npm:3.2.0" deep-clone-simple: "npm:1.1.1"