Skip to content

Commit

Permalink
Add color option to heading entities (#22068)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
piitaya authored Sep 24, 2024
1 parent cbce6f6 commit 813feff
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 27 deletions.
77 changes: 58 additions & 19 deletions src/components/ha-color-picker.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -20,50 +20,88 @@ 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`
<ha-list-item .value=${color} graphic="icon">
${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>
`
Expand All @@ -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%;
Expand Down
2 changes: 2 additions & 0 deletions src/components/ha-selector/ha-selector-ui-color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
6 changes: 5 additions & 1 deletion src/data/selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
57 changes: 57 additions & 0 deletions src/panels/lovelace/cards/heading/hui-heading-entity.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HassEntity } from "home-assistant-js-websocket";
import {
CSSResultGroup,
LitElement,
Expand All @@ -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";
Expand Down Expand Up @@ -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);

Expand All @@ -125,15 +173,22 @@ 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"
@action=${this._handleAction}
.actionHandler=${actionHandler()}
role=${ifDefined(actionable ? "button" : undefined)}
tabindex=${ifDefined(actionable ? "0" : undefined)}
style=${styleMap(style)}
>
${config.show_icon
? html`
Expand Down Expand Up @@ -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);
}
`;
}
Expand Down
1 change: 1 addition & 0 deletions src/panels/lovelace/cards/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ export interface HeadingEntityConfig {
icon?: string;
show_state?: boolean;
show_icon?: boolean;
color?: string;
tap_action?: ActionConfig;
visibility?: Condition[];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ export class HuiEntityBadgeEditor
{
name: "color",
selector: {
ui_color: { default_color: true },
ui_color: {
include_state: true,
},
},
},
{
Expand Down Expand Up @@ -203,6 +205,7 @@ export class HuiEntityBadgeEditor
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ export class HuiTileCardEditor
{
name: "color",
selector: {
ui_color: { default_color: true },
ui_color: {
default_color: "state",
include_state: true,
},
},
},
{
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 813feff

Please sign in to comment.