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 { 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`
<ha-select
.icon=${Boolean(this.value)}
.icon=${Boolean(value)}
.label=${this.label}
.value=${this.value || "default"}
.value=${value}
.helper=${this.helper}
.disabled=${this.disabled}
@closed=${stopPropagation}
@selected=${this._valueSelected}
fixedMenuPosition
naturalMenuWidth
.clearable=${!this.defaultColor}
>
${this.value
${value
? html`
<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>
`
: nothing}
${this.defaultColor
? html` <ha-list-item value="default">
${this.hass.localize(`ui.components.color-picker.default_color`)}
</ha-list-item>`
${this.includeNone
? html`
<ha-list-item value="none" graphic="icon">
${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}
${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}
<span slot="graphic">${this.renderColorCircle(color)}</span>
</ha-list-item>
`
@ -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%;

View File

@ -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}
></ha-color-picker>

View File

@ -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 {

View File

@ -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`
<div
class="entity"
@ -134,6 +188,7 @@ export class HuiHeadingEntity extends LitElement {
.actionHandler=${actionHandler()}
role=${ifDefined(actionable ? "button" : undefined)}
tabindex=${ifDefined(actionable ? "0" : undefined)}
style=${styleMap(style)}
>
${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);
}
`;
}

View File

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

View File

@ -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}
></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() {
return [
configElementStyle,

View File

@ -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}
></ha-form>
<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() {
return [
configElementStyle,

View File

@ -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}
></ha-form>
<ha-expansion-panel outlined>
@ -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<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() {
return [
configElementStyle,

View File

@ -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",