mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 11:16:35 +00:00
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:
parent
cbce6f633f
commit
813feff12e
@ -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%;
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -509,6 +509,7 @@ export interface HeadingEntityConfig {
|
||||
icon?: string;
|
||||
show_state?: boolean;
|
||||
show_icon?: boolean;
|
||||
color?: string;
|
||||
tap_action?: ActionConfig;
|
||||
visibility?: Condition[];
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user