Refactor color handling to use CSS variables (#28021)

This commit is contained in:
Petar Petrov 2025-11-26 10:42:50 +02:00 committed by GitHub
parent 7fe9ae22f0
commit eb6191ab3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 127 additions and 79 deletions

View File

@ -1,64 +1,16 @@
import memoizeOne from "memoize-one";
import { theme2hex } from "./convert-color";
export const COLORS = [
"#4269d0",
"#f4bd4a",
"#ff725c",
"#6cc5b0",
"#a463f2",
"#ff8ab7",
"#9c6b4e",
"#97bbf5",
"#01ab63",
"#094bad",
"#c99000",
"#d84f3e",
"#49a28f",
"#048732",
"#d96895",
"#8043ce",
"#7599d1",
"#7a4c31",
"#6989f4",
"#ffd444",
"#ff957c",
"#8fe9d3",
"#62cc71",
"#ffadda",
"#c884ff",
"#badeff",
"#bf8b6d",
"#927acc",
"#97ee3f",
"#bf3947",
"#9f5b00",
"#f48758",
"#8caed6",
"#f2b94f",
"#eff26e",
"#e43872",
"#d9b100",
"#9d7a00",
"#698cff",
"#00d27e",
"#d06800",
"#009f82",
"#c49200",
"#cbe8ff",
"#fecddf",
"#c27eb6",
"#8cd2ce",
"#c4b8d9",
"#f883b0",
"#a49100",
"#f48800",
"#27d0df",
"#a04a9b",
];
// Total number of colors defined in CSS variables (--color-1 through --color-54)
export const COLORS_COUNT = 54;
export function getColorByIndex(index: number) {
return COLORS[index % COLORS.length];
export function getColorByIndex(
index: number,
style: CSSStyleDeclaration
): string {
// Wrap around using modulo to support unlimited indices
const colorIndex = (index % COLORS_COUNT) + 1;
return style.getPropertyValue(`--color-${colorIndex}`);
}
export function getGraphColorByIndex(
@ -68,15 +20,19 @@ export function getGraphColorByIndex(
// The CSS vars for the colors use range 1..n, so we need to adjust the index from the internal 0..n color index range.
const themeColor =
style.getPropertyValue(`--graph-color-${index + 1}`) ||
getColorByIndex(index);
getColorByIndex(index, style);
return theme2hex(themeColor);
}
export const getAllGraphColors = memoizeOne(
(style: CSSStyleDeclaration) =>
COLORS.map((_color, index) => getGraphColorByIndex(index, style)),
Array.from({ length: COLORS_COUNT }, (_, index) =>
getGraphColorByIndex(index, style)
),
(newArgs: [CSSStyleDeclaration], lastArgs: [CSSStyleDeclaration]) =>
// this is not ideal, but we need to memoize the colors
newArgs[0].getPropertyValue("--graph-color-1") ===
lastArgs[0].getPropertyValue("--graph-color-1")
lastArgs[0].getPropertyValue("--graph-color-1") &&
newArgs[0].getPropertyValue("--color-1") ===
lastArgs[0].getPropertyValue("--color-1")
);

View File

@ -137,8 +137,12 @@ const getCalendarDate = (dateObj: any): string | undefined => {
return undefined;
};
export const getCalendars = (hass: HomeAssistant): Calendar[] =>
Object.keys(hass.states)
export const getCalendars = (
hass: HomeAssistant,
element: Element
): Calendar[] => {
const computedStyles = getComputedStyle(element);
return Object.keys(hass.states)
.filter(
(eid) =>
computeDomain(eid) === "calendar" &&
@ -149,8 +153,9 @@ export const getCalendars = (hass: HomeAssistant): Calendar[] =>
.map((eid, idx) => ({
...hass.states[eid],
name: computeStateName(hass.states[eid]),
backgroundColor: getColorByIndex(idx),
backgroundColor: getColorByIndex(idx, computedStyles),
}));
};
export const createCalendarEvent = (
hass: HomeAssistant,

View File

@ -87,7 +87,7 @@ class PanelCalendar extends LitElement {
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._calendars = getCalendars(this.hass);
this._calendars = getCalendars(this.hass, this);
}
}
@ -243,7 +243,7 @@ class PanelCalendar extends LitElement {
manifest: await fetchIntegrationManifest(this.hass, "local_calendar"),
dialogClosedCallback: ({ flowFinished }) => {
if (flowFinished) {
this._calendars = getCalendars(this.hass);
this._calendars = getCalendars(this.hass, this);
}
},
});

View File

@ -80,9 +80,10 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
throw new Error("Entities need to be an array");
}
const computedStyles = getComputedStyle(this);
this._calendars = config!.entities.map((entity, idx) => ({
entity_id: entity,
backgroundColor: getColorByIndex(idx),
backgroundColor: getColorByIndex(idx, computedStyles),
}));
if (this._config?.entities !== config.entities) {

View File

@ -394,7 +394,8 @@ class HuiMapCard extends LitElement implements LovelaceCard {
if (color) {
return color;
}
color = getColorByIndex(this._colorIndex);
const computedStyles = getComputedStyle(this);
color = getColorByIndex(this._colorIndex, computedStyles);
this._colorIndex++;
this._colorDict[entityId] = color;
return color;

View File

@ -92,6 +92,62 @@ export const colorStyles = css`
--black-color: #000000;
--white-color: #ffffff;
/* colors - used for graphs, calendars, maps, etc */
--color-1: #4269d0;
--color-2: #f4bd4a;
--color-3: #ff725c;
--color-4: #6cc5b0;
--color-5: #a463f2;
--color-6: #ff8ab7;
--color-7: #9c6b4e;
--color-8: #97bbf5;
--color-9: #01ab63;
--color-10: #094bad;
--color-11: #c99000;
--color-12: #d84f3e;
--color-13: #49a28f;
--color-14: #048732;
--color-15: #d96895;
--color-16: #8043ce;
--color-17: #7599d1;
--color-18: #7a4c31;
--color-19: #6989f4;
--color-20: #ffd444;
--color-21: #ff957c;
--color-22: #8fe9d3;
--color-23: #62cc71;
--color-24: #ffadda;
--color-25: #c884ff;
--color-26: #badeff;
--color-27: #bf8b6d;
--color-28: #927acc;
--color-29: #97ee3f;
--color-30: #bf3947;
--color-31: #9f5b00;
--color-32: #f48758;
--color-33: #8caed6;
--color-34: #f2b94f;
--color-35: #eff26e;
--color-36: #e43872;
--color-37: #d9b100;
--color-38: #9d7a00;
--color-39: #698cff;
--color-40: #00d27e;
--color-41: #d06800;
--color-42: #009f82;
--color-43: #c49200;
--color-44: #cbe8ff;
--color-45: #fecddf;
--color-46: #c27eb6;
--color-47: #8cd2ce;
--color-48: #c4b8d9;
--color-49: #f883b0;
--color-50: #a49100;
--color-51: #f48800;
--color-52: #27d0df;
--color-53: #a04a9b;
--color-54: #4269d0;
/* history colors */
--history-unavailable-color: transparent;

View File

@ -2,34 +2,63 @@ import { describe, test, expect } from "vitest";
import {
getColorByIndex,
getGraphColorByIndex,
COLORS,
COLORS_COUNT,
} from "../../../src/common/color/colors";
import { theme2hex } from "../../../src/common/color/convert-color";
describe("getColorByIndex", () => {
test("return the correct color for a given index", () => {
expect(getColorByIndex(0)).toBe(COLORS[0]);
expect(getColorByIndex(10)).toBe(COLORS[10]);
test("return the correct color from CSS variable", () => {
const style = {
getPropertyValue: (prop) => {
if (prop === "--color-1") return "#4269d0";
if (prop === "--color-11") return "#c99000";
return "";
},
} as CSSStyleDeclaration;
expect(getColorByIndex(0, style)).toBe(theme2hex("#4269d0"));
expect(getColorByIndex(10, style)).toBe(theme2hex("#c99000"));
});
test("wrap around if the index is greater than the length of COLORS", () => {
expect(getColorByIndex(COLORS.length)).toBe(COLORS[0]);
expect(getColorByIndex(COLORS.length + 4)).toBe(COLORS[4]);
test("wrap around if the index is greater than the total count", () => {
const style = {
getPropertyValue: (prop) => {
if (prop === "--color-1") return "#4269d0";
if (prop === "--color-5") return "#a463f2";
return "";
},
} as CSSStyleDeclaration;
// Index 54 should wrap to color 1
expect(getColorByIndex(COLORS_COUNT, style)).toBe(theme2hex("#4269d0"));
// Index 58 should wrap to color 5
expect(getColorByIndex(COLORS_COUNT + 4, style)).toBe(theme2hex("#a463f2"));
});
});
describe("getGraphColorByIndex", () => {
test("return the correct theme color if it exists", () => {
test("return color from --graph-color variable when it exists", () => {
const style = {
getPropertyValue: (prop) => (prop === "--graph-color-1" ? "#123456" : ""),
} as CSSStyleDeclaration;
expect(getGraphColorByIndex(0, style)).toBe(theme2hex("#123456"));
});
test("return the default color if the theme color does not exist", () => {
test("fallback to --color variable when --graph-color does not exist", () => {
const style = {
getPropertyValue: () => "",
} as unknown as CSSStyleDeclaration;
expect(getGraphColorByIndex(0, style)).toBe(theme2hex(COLORS[0]));
getPropertyValue: (prop) => (prop === "--color-5" ? "#abcdef" : ""),
} as CSSStyleDeclaration;
// Index 4 should try --graph-color-5, then fallback to --color-5
expect(getGraphColorByIndex(4, style)).toBe(theme2hex("#abcdef"));
});
test("prefer --graph-color over --color when both exist", () => {
const style = {
getPropertyValue: (prop) => {
if (prop === "--graph-color-1") return "#111111";
if (prop === "--color-1") return "#222222";
return "";
},
} as CSSStyleDeclaration;
// Should prefer --graph-color-1
expect(getGraphColorByIndex(0, style)).toBe(theme2hex("#111111"));
});
});