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
This commit is contained in:
Paul Bottein 2024-09-24 20:14:03 +02:00 committed by GitHub
parent cbce6f633f
commit 813feff12e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 201 additions and 27 deletions

View File

@ -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 { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color"; import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation"; 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 { LocalizeKeys } from "../common/translations/localize";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
@customElement("ha-color-picker") @customElement("ha-color-picker")
export class HaColorPicker extends LitElement { export class HaColorPicker extends LitElement {
@ -20,43 +20,78 @@ export class HaColorPicker extends LitElement {
@property() public value?: string; @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; @property({ type: Boolean }) public disabled = false;
_valueSelected(ev) { _valueSelected(ev) {
const value = ev.target.value; const value = ev.target.value;
if (value) { this.value = value === this.defaultColor ? undefined : value;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: value !== "default" ? value : undefined, value: this.value,
}); });
} }
}
render() { render() {
const value = this.value || this.defaultColor;
return html` return html`
<ha-select <ha-select
.icon=${Boolean(this.value)} .icon=${Boolean(value)}
.label=${this.label} .label=${this.label}
.value=${this.value || "default"} .value=${value}
.helper=${this.helper} .helper=${this.helper}
.disabled=${this.disabled} .disabled=${this.disabled}
@closed=${stopPropagation} @closed=${stopPropagation}
@selected=${this._valueSelected} @selected=${this._valueSelected}
fixedMenuPosition fixedMenuPosition
naturalMenuWidth naturalMenuWidth
.clearable=${!this.defaultColor}
> >
${this.value ${value
? html` ? html`
<span slot="icon"> <span slot="icon">
${this.renderColorCircle(this.value || "grey")} ${value === "none"
? html`
<ha-svg-icon path=${mdiInvertColorsOff}></ha-svg-icon>
`
: value === "state"
? html`<ha-svg-icon path=${mdiPalette}></ha-svg-icon>`
: this.renderColorCircle(value || "grey")}
</span> </span>
` `
: nothing} : nothing}
${this.defaultColor ${this.includeNone
? html` <ha-list-item value="default"> ? html`
${this.hass.localize(`ui.components.color-picker.default_color`)} <ha-list-item value="none" graphic="icon">
</ha-list-item>` ${this.hass.localize("ui.components.color-picker.none")}
${this.defaultColor === "none"
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<ha-svg-icon
slot="graphic"
path=${mdiInvertColorsOff}
></ha-svg-icon>
</ha-list-item>
`
: nothing}
${this.includeState
? html`
<ha-list-item value="state" graphic="icon">
${this.hass.localize("ui.components.color-picker.state")}
${this.defaultColor === "state"
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<ha-svg-icon slot="graphic" path=${mdiPalette}></ha-svg-icon>
</ha-list-item>
`
: nothing} : nothing}
${Array.from(THEME_COLORS).map( ${Array.from(THEME_COLORS).map(
(color) => html` (color) => html`
@ -64,6 +99,9 @@ export class HaColorPicker extends LitElement {
${this.hass.localize( ${this.hass.localize(
`ui.components.color-picker.colors.${color}` as LocalizeKeys `ui.components.color-picker.colors.${color}` as LocalizeKeys
) || color} ) || color}
${this.defaultColor === color
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<span slot="graphic">${this.renderColorCircle(color)}</span> <span slot="graphic">${this.renderColorCircle(color)}</span>
</ha-list-item> </ha-list-item>
` `
@ -87,10 +125,11 @@ export class HaColorPicker extends LitElement {
return css` return css`
.circle-color { .circle-color {
display: block; display: block;
background-color: var(--circle-color); background-color: var(--circle-color, var(--divider-color));
border-radius: 10px; border-radius: 10px;
width: 20px; width: 20px;
height: 20px; height: 20px;
box-sizing: border-box;
} }
ha-select { ha-select {
width: 100%; width: 100%;

View File

@ -24,6 +24,8 @@ export class HaSelectorUiColor extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.value=${this.value} .value=${this.value}
.helper=${this.helper} .helper=${this.helper}
.includeNone=${this.selector.ui_color?.include_none}
.includeState=${this.selector.ui_color?.include_state}
.defaultColor=${this.selector.ui_color?.default_color} .defaultColor=${this.selector.ui_color?.default_color}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-color-picker> ></ha-color-picker>

View File

@ -454,7 +454,11 @@ export interface UiActionSelector {
export interface UiColorSelector { export interface UiColorSelector {
// eslint-disable-next-line @typescript-eslint/ban-types // 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 { export interface UiStateContentSelector {

View File

@ -1,3 +1,4 @@
import { HassEntity } from "home-assistant-js-websocket";
import { import {
CSSResultGroup, CSSResultGroup,
LitElement, LitElement,
@ -8,8 +9,18 @@ import {
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; 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 { 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-card";
import "../../../../components/ha-icon"; import "../../../../components/ha-icon";
import "../../../../components/ha-icon-next"; 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() { protected render() {
const config = this._config(this.config); const config = this._config(this.config);
@ -125,8 +173,14 @@ export class HuiHeadingEntity extends LitElement {
return nothing; return nothing;
} }
const color = this._computeStateColor(stateObj, config.color);
const actionable = hasAction(config.tap_action); const actionable = hasAction(config.tap_action);
const style = {
"--color": color,
};
return html` return html`
<div <div
class="entity" class="entity"
@ -134,6 +188,7 @@ export class HuiHeadingEntity extends LitElement {
.actionHandler=${actionHandler()} .actionHandler=${actionHandler()}
role=${ifDefined(actionable ? "button" : undefined)} role=${ifDefined(actionable ? "button" : undefined)}
tabindex=${ifDefined(actionable ? "0" : undefined)} tabindex=${ifDefined(actionable ? "0" : undefined)}
style=${styleMap(style)}
> >
${config.show_icon ${config.show_icon
? html` ? html`
@ -176,9 +231,11 @@ export class HuiHeadingEntity extends LitElement {
line-height: 20px; /* 142.857% */ line-height: 20px; /* 142.857% */
letter-spacing: 0.1px; letter-spacing: 0.1px;
--mdc-icon-size: 14px; --mdc-icon-size: 14px;
--state-inactive-color: initial;
} }
.entity ha-state-icon { .entity ha-state-icon {
--ha-icon-display: block; --ha-icon-display: block;
color: var(--color);
} }
`; `;
} }

View File

@ -509,6 +509,7 @@ export interface HeadingEntityConfig {
icon?: string; icon?: string;
show_state?: boolean; show_state?: boolean;
show_icon?: boolean; show_icon?: boolean;
color?: string;
tap_action?: ActionConfig; tap_action?: ActionConfig;
visibility?: Condition[]; visibility?: Condition[];
} }

View File

@ -92,7 +92,9 @@ export class HuiEntityBadgeEditor
{ {
name: "color", name: "color",
selector: { selector: {
ui_color: { default_color: true }, ui_color: {
include_state: true,
},
}, },
}, },
{ {
@ -203,6 +205,7 @@ export class HuiEntityBadgeEditor
.data=${data} .data=${data}
.schema=${schema} .schema=${schema}
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-form> ></ha-form>
`; `;
@ -250,6 +253,19 @@ export class HuiEntityBadgeEditor
} }
}; };
private _computeHelperCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "color":
return this.hass!.localize(
`ui.panel.lovelace.editor.badge.entity.${schema.name}_helper`
);
default:
return undefined;
}
};
static get styles() { static get styles() {
return [ return [
configElementStyle, configElementStyle,

View File

@ -95,7 +95,10 @@ export class HuiTileCardEditor
{ {
name: "color", name: "color",
selector: { selector: {
ui_color: { default_color: true }, ui_color: {
default_color: "state",
include_state: true,
},
}, },
}, },
{ {
@ -205,6 +208,7 @@ export class HuiTileCardEditor
.data=${data} .data=${data}
.schema=${schema} .schema=${schema}
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-form> ></ha-form>
<ha-expansion-panel outlined> <ha-expansion-panel outlined>
@ -329,6 +333,19 @@ export class HuiTileCardEditor
} }
}; };
private _computeHelperCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "color":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.tile.${schema.name}_helper`
);
default:
return undefined;
}
};
static get styles() { static get styles() {
return [ return [
configElementStyle, configElementStyle,

View File

@ -39,6 +39,7 @@ const entityConfigStruct = object({
state_content: optional(union([string(), array(string())])), state_content: optional(union([string(), array(string())])),
show_state: optional(boolean()), show_state: optional(boolean()),
show_icon: optional(boolean()), show_icon: optional(boolean()),
color: optional(string()),
tap_action: optional(actionConfigStruct), tap_action: optional(actionConfigStruct),
visibility: optional(array(any())), visibility: optional(array(any())),
}); });
@ -78,12 +79,28 @@ export class HuiHeadingEntityEditor
type: "expandable", type: "expandable",
flatten: true, flatten: true,
iconPath: mdiPalette, iconPath: mdiPalette,
schema: [
{
name: "",
type: "grid",
schema: [ schema: [
{ {
name: "icon", name: "icon",
selector: { icon: {} }, selector: { icon: {} },
context: { icon_entity: "entity" }, context: { icon_entity: "entity" },
}, },
{
name: "color",
selector: {
ui_color: {
default_color: "none",
include_state: true,
include_none: true,
},
},
},
],
},
{ {
name: "displayed_elements", name: "displayed_elements",
selector: { selector: {
@ -159,6 +176,7 @@ export class HuiHeadingEntityEditor
.data=${data} .data=${data}
.schema=${schema} .schema=${schema}
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-form> ></ha-form>
<ha-expansion-panel outlined> <ha-expansion-panel outlined>
@ -228,6 +246,7 @@ export class HuiHeadingEntityEditor
case "state_content": case "state_content":
case "displayed_elements": case "displayed_elements":
case "appearance": case "appearance":
case "color":
return this.hass!.localize( return this.hass!.localize(
`ui.panel.lovelace.editor.card.heading.entity_config.${schema.name}` `ui.panel.lovelace.editor.card.heading.entity_config.${schema.name}`
); );
@ -238,6 +257,19 @@ export class HuiHeadingEntityEditor
} }
}; };
private _computeHelperCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
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() { static get styles() {
return [ return [
configElementStyle, configElementStyle,

View File

@ -699,7 +699,9 @@
"unsupported_format": "Unsupported format, please choose a JPEG, PNG, or GIF image." "unsupported_format": "Unsupported format, please choose a JPEG, PNG, or GIF image."
}, },
"color-picker": { "color-picker": {
"default_color": "Default color (state)", "default": "default",
"state": "State color",
"none": "No color",
"colors": { "colors": {
"primary": "Primary", "primary": "Primary",
"accent": "Accent", "accent": "Accent",
@ -6007,6 +6009,8 @@
}, },
"entities": "Entities", "entities": "Entities",
"entity_config": { "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": "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.", "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", "appearance": "Appearance",
@ -6101,6 +6105,7 @@
"name": "Tile", "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.", "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": "Color",
"color_helper": "Inactive state (e.g. off, closed) will not be colored.",
"icon_tap_action": "Icon tap behavior", "icon_tap_action": "Icon tap behavior",
"interactions": "Interactions", "interactions": "Interactions",
"appearance": "Appearance", "appearance": "Appearance",
@ -6140,7 +6145,8 @@
"entity": { "entity": {
"name": "Entity", "name": "Entity",
"description": "The Entity badge gives you a quick overview of your 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", "interactions": "Interactions",
"appearance": "Appearance", "appearance": "Appearance",
"show_entity_picture": "Show entity picture", "show_entity_picture": "Show entity picture",