From 813feff12e481816f47fa575674c376a0b10a351 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 24 Sep 2024 20:14:03 +0200 Subject: [PATCH] Add color option to heading entities (#22068) * Add uncolored option * Allow to color icon based on state or custom color * Use text color for inactive color * Rename uncolored to none * Add helper * Update wording --- src/components/ha-color-picker.ts | 77 ++++++++++++++----- .../ha-selector/ha-selector-ui-color.ts | 2 + src/data/selector.ts | 6 +- .../cards/heading/hui-heading-entity.ts | 57 ++++++++++++++ src/panels/lovelace/cards/types.ts | 1 + .../hui-entity-badge-editor.ts | 18 ++++- .../config-elements/hui-tile-card-editor.ts | 19 ++++- .../hui-heading-entity-editor.ts | 38 ++++++++- src/translations/en.json | 10 ++- 9 files changed, 201 insertions(+), 27 deletions(-) diff --git a/src/components/ha-color-picker.ts b/src/components/ha-color-picker.ts index 2acd53aa14..f89c8fc5ca 100644 --- a/src/components/ha-color-picker.ts +++ b/src/components/ha-color-picker.ts @@ -1,14 +1,14 @@ -import "@material/mwc-list/mwc-list-item"; +import { mdiInvertColorsOff, mdiPalette } from "@mdi/js"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import { computeCssColor, THEME_COLORS } from "../common/color/compute-color"; import { fireEvent } from "../common/dom/fire_event"; import { stopPropagation } from "../common/dom/stop_propagation"; -import "./ha-select"; -import "./ha-list-item"; -import { HomeAssistant } from "../types"; import { LocalizeKeys } from "../common/translations/localize"; +import { HomeAssistant } from "../types"; +import "./ha-list-item"; +import "./ha-select"; @customElement("ha-color-picker") export class HaColorPicker extends LitElement { @@ -20,43 +20,78 @@ export class HaColorPicker extends LitElement { @property() public value?: string; - @property({ type: Boolean }) public defaultColor = false; + @property({ type: String, attribute: "default_color" }) + public defaultColor?: string; + + @property({ type: Boolean, attribute: "include_state" }) + public includeState = false; + + @property({ type: Boolean, attribute: "include_none" }) + public includeNone = false; @property({ type: Boolean }) public disabled = false; _valueSelected(ev) { const value = ev.target.value; - if (value) { - fireEvent(this, "value-changed", { - value: value !== "default" ? value : undefined, - }); - } + this.value = value === this.defaultColor ? undefined : value; + fireEvent(this, "value-changed", { + value: this.value, + }); } render() { + const value = this.value || this.defaultColor; + return html` - ${this.value + ${value ? html` - ${this.renderColorCircle(this.value || "grey")} + ${value === "none" + ? html` + + ` + : value === "state" + ? html`` + : this.renderColorCircle(value || "grey")} ` : nothing} - ${this.defaultColor - ? html` - ${this.hass.localize(`ui.components.color-picker.default_color`)} - ` + ${this.includeNone + ? html` + + ${this.hass.localize("ui.components.color-picker.none")} + ${this.defaultColor === "none" + ? ` (${this.hass.localize("ui.components.color-picker.default")})` + : nothing} + + + ` + : nothing} + ${this.includeState + ? html` + + ${this.hass.localize("ui.components.color-picker.state")} + ${this.defaultColor === "state" + ? ` (${this.hass.localize("ui.components.color-picker.default")})` + : nothing} + + + ` : nothing} ${Array.from(THEME_COLORS).map( (color) => html` @@ -64,6 +99,9 @@ export class HaColorPicker extends LitElement { ${this.hass.localize( `ui.components.color-picker.colors.${color}` as LocalizeKeys ) || color} + ${this.defaultColor === color + ? ` (${this.hass.localize("ui.components.color-picker.default")})` + : nothing} ${this.renderColorCircle(color)} ` @@ -87,10 +125,11 @@ export class HaColorPicker extends LitElement { return css` .circle-color { display: block; - background-color: var(--circle-color); + background-color: var(--circle-color, var(--divider-color)); border-radius: 10px; width: 20px; height: 20px; + box-sizing: border-box; } ha-select { width: 100%; diff --git a/src/components/ha-selector/ha-selector-ui-color.ts b/src/components/ha-selector/ha-selector-ui-color.ts index 56379f7fef..7dfc864836 100644 --- a/src/components/ha-selector/ha-selector-ui-color.ts +++ b/src/components/ha-selector/ha-selector-ui-color.ts @@ -24,6 +24,8 @@ export class HaSelectorUiColor extends LitElement { .hass=${this.hass} .value=${this.value} .helper=${this.helper} + .includeNone=${this.selector.ui_color?.include_none} + .includeState=${this.selector.ui_color?.include_state} .defaultColor=${this.selector.ui_color?.default_color} @value-changed=${this._valueChanged} > diff --git a/src/data/selector.ts b/src/data/selector.ts index 036fa0bc86..6ed4517170 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -454,7 +454,11 @@ export interface UiActionSelector { export interface UiColorSelector { // eslint-disable-next-line @typescript-eslint/ban-types - ui_color: { default_color?: boolean } | null; + ui_color: { + default_color?: string; + include_none?: boolean; + include_state?: boolean; + } | null; } export interface UiStateContentSelector { diff --git a/src/panels/lovelace/cards/heading/hui-heading-entity.ts b/src/panels/lovelace/cards/heading/hui-heading-entity.ts index d91f24fe91..8b12950e7f 100644 --- a/src/panels/lovelace/cards/heading/hui-heading-entity.ts +++ b/src/panels/lovelace/cards/heading/hui-heading-entity.ts @@ -1,3 +1,4 @@ +import { HassEntity } from "home-assistant-js-websocket"; import { CSSResultGroup, LitElement, @@ -8,8 +9,18 @@ import { } from "lit"; import { customElement, property } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; +import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; +import { computeCssColor } from "../../../../common/color/compute-color"; +import { + hsv2rgb, + rgb2hex, + rgb2hsv, +} from "../../../../common/color/convert-color"; import { MediaQueriesListener } from "../../../../common/dom/media_query"; +import { computeDomain } from "../../../../common/entity/compute_domain"; +import { stateActive } from "../../../../common/entity/state_active"; +import { stateColorCss } from "../../../../common/entity/state_color"; import "../../../../components/ha-card"; import "../../../../components/ha-icon"; import "../../../../components/ha-icon-next"; @@ -116,6 +127,43 @@ export class HuiHeadingEntity extends LitElement { ); } + private _computeStateColor = memoizeOne( + (entity: HassEntity, color?: string) => { + if (!color || color === "none") { + return undefined; + } + + if (color === "state") { + // Use light color if the light support rgb + if ( + computeDomain(entity.entity_id) === "light" && + entity.attributes.rgb_color + ) { + const hsvColor = rgb2hsv(entity.attributes.rgb_color); + + // Modify the real rgb color for better contrast + if (hsvColor[1] < 0.4) { + // Special case for very light color (e.g: white) + if (hsvColor[1] < 0.1) { + hsvColor[2] = 225; + } else { + hsvColor[1] = 0.4; + } + } + return rgb2hex(hsv2rgb(hsvColor)); + } + // Fallback to state color + return stateColorCss(entity); + } + + if (color) { + // Use custom color if active + return stateActive(entity) ? computeCssColor(color) : undefined; + } + return color; + } + ); + protected render() { const config = this._config(this.config); @@ -125,8 +173,14 @@ export class HuiHeadingEntity extends LitElement { return nothing; } + const color = this._computeStateColor(stateObj, config.color); + const actionable = hasAction(config.tap_action); + const style = { + "--color": color, + }; + return html`
${config.show_icon ? html` @@ -176,9 +231,11 @@ export class HuiHeadingEntity extends LitElement { line-height: 20px; /* 142.857% */ letter-spacing: 0.1px; --mdc-icon-size: 14px; + --state-inactive-color: initial; } .entity ha-state-icon { --ha-icon-display: block; + color: var(--color); } `; } diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 6fb0a6fd90..e3d3257346 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -509,6 +509,7 @@ export interface HeadingEntityConfig { icon?: string; show_state?: boolean; show_icon?: boolean; + color?: string; tap_action?: ActionConfig; visibility?: Condition[]; } diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts index 2458596fa4..926c012774 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts @@ -92,7 +92,9 @@ export class HuiEntityBadgeEditor { name: "color", selector: { - ui_color: { default_color: true }, + ui_color: { + include_state: true, + }, }, }, { @@ -203,6 +205,7 @@ export class HuiEntityBadgeEditor .data=${data} .schema=${schema} .computeLabel=${this._computeLabelCallback} + .computeHelper=${this._computeHelperCallback} @value-changed=${this._valueChanged} > `; @@ -250,6 +253,19 @@ export class HuiEntityBadgeEditor } }; + private _computeHelperCallback = ( + schema: SchemaUnion> + ) => { + switch (schema.name) { + case "color": + return this.hass!.localize( + `ui.panel.lovelace.editor.badge.entity.${schema.name}_helper` + ); + default: + return undefined; + } + }; + static get styles() { return [ configElementStyle, diff --git a/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts index 743a006aea..b7e95b0179 100644 --- a/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts @@ -95,7 +95,10 @@ export class HuiTileCardEditor { name: "color", selector: { - ui_color: { default_color: true }, + ui_color: { + default_color: "state", + include_state: true, + }, }, }, { @@ -205,6 +208,7 @@ export class HuiTileCardEditor .data=${data} .schema=${schema} .computeLabel=${this._computeLabelCallback} + .computeHelper=${this._computeHelperCallback} @value-changed=${this._valueChanged} > @@ -329,6 +333,19 @@ export class HuiTileCardEditor } }; + private _computeHelperCallback = ( + schema: SchemaUnion> + ) => { + switch (schema.name) { + case "color": + return this.hass!.localize( + `ui.panel.lovelace.editor.card.tile.${schema.name}_helper` + ); + default: + return undefined; + } + }; + static get styles() { return [ configElementStyle, diff --git a/src/panels/lovelace/editor/heading-entity/hui-heading-entity-editor.ts b/src/panels/lovelace/editor/heading-entity/hui-heading-entity-editor.ts index 5673ab2dce..6d0c4f3d51 100644 --- a/src/panels/lovelace/editor/heading-entity/hui-heading-entity-editor.ts +++ b/src/panels/lovelace/editor/heading-entity/hui-heading-entity-editor.ts @@ -39,6 +39,7 @@ const entityConfigStruct = object({ state_content: optional(union([string(), array(string())])), show_state: optional(boolean()), show_icon: optional(boolean()), + color: optional(string()), tap_action: optional(actionConfigStruct), visibility: optional(array(any())), }); @@ -80,9 +81,25 @@ export class HuiHeadingEntityEditor iconPath: mdiPalette, schema: [ { - name: "icon", - selector: { icon: {} }, - context: { icon_entity: "entity" }, + name: "", + type: "grid", + schema: [ + { + name: "icon", + selector: { icon: {} }, + context: { icon_entity: "entity" }, + }, + { + name: "color", + selector: { + ui_color: { + default_color: "none", + include_state: true, + include_none: true, + }, + }, + }, + ], }, { name: "displayed_elements", @@ -159,6 +176,7 @@ export class HuiHeadingEntityEditor .data=${data} .schema=${schema} .computeLabel=${this._computeLabelCallback} + .computeHelper=${this._computeHelperCallback} @value-changed=${this._valueChanged} > @@ -228,6 +246,7 @@ export class HuiHeadingEntityEditor case "state_content": case "displayed_elements": case "appearance": + case "color": return this.hass!.localize( `ui.panel.lovelace.editor.card.heading.entity_config.${schema.name}` ); @@ -238,6 +257,19 @@ export class HuiHeadingEntityEditor } }; + private _computeHelperCallback = ( + schema: SchemaUnion> + ) => { + switch (schema.name) { + case "color": + return this.hass!.localize( + `ui.panel.lovelace.editor.card.heading.entity_config.${schema.name}_helper` + ); + default: + return undefined; + } + }; + static get styles() { return [ configElementStyle, diff --git a/src/translations/en.json b/src/translations/en.json index b9d4e7f24d..e460303633 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -699,7 +699,9 @@ "unsupported_format": "Unsupported format, please choose a JPEG, PNG, or GIF image." }, "color-picker": { - "default_color": "Default color (state)", + "default": "default", + "state": "State color", + "none": "No color", "colors": { "primary": "Primary", "accent": "Accent", @@ -6007,6 +6009,8 @@ }, "entities": "Entities", "entity_config": { + "color": "[%key:ui::panel::lovelace::editor::card::tile::color%]", + "color_helper": "[%key:ui::panel::lovelace::editor::card::tile::color_helper%]", "visibility": "Visibility", "visibility_explanation": "The entity will be shown when ALL conditions below are fulfilled. If no conditions are set, the entity will always be shown.", "appearance": "Appearance", @@ -6101,6 +6105,7 @@ "name": "Tile", "description": "The tile card gives you a quick overview of your entity. The card allow you to toggle the entity, show the more info dialog or custom actions.", "color": "Color", + "color_helper": "Inactive state (e.g. off, closed) will not be colored.", "icon_tap_action": "Icon tap behavior", "interactions": "Interactions", "appearance": "Appearance", @@ -6140,7 +6145,8 @@ "entity": { "name": "Entity", "description": "The Entity badge gives you a quick overview of your entity.", - "color": "Color", + "color": "[%key:ui::panel::lovelace::editor::card::tile::color%]", + "color_helper": "[%key:ui::panel::lovelace::editor::card::tile::color_helper%]", "interactions": "Interactions", "appearance": "Appearance", "show_entity_picture": "Show entity picture",