mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 03:06:41 +00:00
Add badges for tile card (#14231)
This commit is contained in:
parent
ebc0edac10
commit
9778c0731c
51
src/components/tile/ha-tile-badge.ts
Normal file
51
src/components/tile/ha-tile-badge.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import "../ha-icon";
|
||||||
|
|
||||||
|
@customElement("ha-tile-badge")
|
||||||
|
export class HaTileBadge extends LitElement {
|
||||||
|
@property() public iconPath?: string;
|
||||||
|
|
||||||
|
@property() public icon?: string;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<div class="badge">
|
||||||
|
${this.icon
|
||||||
|
? html`<ha-icon .icon=${this.icon}></ha-icon>`
|
||||||
|
: html`<ha-svg-icon .path=${this.iconPath}></ha-svg-icon>`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
--tile-badge-background-color: rgb(var(--rgb-primary-color));
|
||||||
|
--tile-badge-icon-color: rgb(var(--rgb-white-color));
|
||||||
|
--mdc-icon-size: 12px;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--tile-badge-background-color);
|
||||||
|
transition: background-color 280ms ease-in-out;
|
||||||
|
}
|
||||||
|
.badge ha-icon,
|
||||||
|
.badge ha-svg-icon {
|
||||||
|
color: var(--tile-badge-icon-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-tile-badge": HaTileBadge;
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ import { stateActive } from "../../../common/entity/state_active";
|
|||||||
import { stateColorCss } from "../../../common/entity/state_color";
|
import { stateColorCss } from "../../../common/entity/state_color";
|
||||||
import { stateIconPath } from "../../../common/entity/state_icon_path";
|
import { stateIconPath } from "../../../common/entity/state_icon_path";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/tile/ha-tile-badge";
|
||||||
import "../../../components/tile/ha-tile-icon";
|
import "../../../components/tile/ha-tile-icon";
|
||||||
import "../../../components/tile/ha-tile-image";
|
import "../../../components/tile/ha-tile-image";
|
||||||
import "../../../components/tile/ha-tile-info";
|
import "../../../components/tile/ha-tile-info";
|
||||||
@ -21,6 +22,7 @@ import { actionHandler } from "../common/directives/action-handler-directive";
|
|||||||
import { findEntities } from "../common/find-entities";
|
import { findEntities } from "../common/find-entities";
|
||||||
import { handleAction } from "../common/handle-action";
|
import { handleAction } from "../common/handle-action";
|
||||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||||
|
import { computeTileBadge } from "./tile/badges/tile-badge";
|
||||||
import { ThermostatCardConfig, TileCardConfig } from "./types";
|
import { ThermostatCardConfig, TileCardConfig } from "./types";
|
||||||
|
|
||||||
@customElement("hui-tile-card")
|
@customElement("hui-tile-card")
|
||||||
@ -116,7 +118,9 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||||||
return html`
|
return html`
|
||||||
<ha-card class="disabled">
|
<ha-card class="disabled">
|
||||||
<div class="tile">
|
<div class="tile">
|
||||||
|
<div class="icon-container">
|
||||||
<ha-tile-icon .iconPath=${mdiHelp}></ha-tile-icon>
|
<ha-tile-icon .iconPath=${mdiHelp}></ha-tile-icon>
|
||||||
|
</div>
|
||||||
<ha-tile-info
|
<ha-tile-info
|
||||||
.primary=${entityId}
|
.primary=${entityId}
|
||||||
secondary=${this.hass.localize("ui.card.tile.not_found")}
|
secondary=${this.hass.localize("ui.card.tile.not_found")}
|
||||||
@ -147,19 +151,23 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||||||
const imageUrl = this._config.show_entity_picture
|
const imageUrl = this._config.show_entity_picture
|
||||||
? this._getImageUrl(entity)
|
? this._getImageUrl(entity)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const badge = computeTileBadge(entity, this.hass);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card style=${styleMap(style)}>
|
<ha-card style=${styleMap(style)}>
|
||||||
<div class="tile">
|
<div class="tile">
|
||||||
|
<div
|
||||||
|
class="icon-container"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
@action=${this._handleIconAction}
|
||||||
|
.actionHandler=${actionHandler()}
|
||||||
|
>
|
||||||
${imageUrl
|
${imageUrl
|
||||||
? html`
|
? html`
|
||||||
<ha-tile-image
|
<ha-tile-image
|
||||||
class="icon"
|
class="icon"
|
||||||
.imageUrl=${imageUrl}
|
.imageUrl=${imageUrl}
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
@action=${this._handleIconAction}
|
|
||||||
.actionHandler=${actionHandler()}
|
|
||||||
></ha-tile-image>
|
></ha-tile-image>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
@ -167,12 +175,21 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||||||
class="icon"
|
class="icon"
|
||||||
.icon=${icon}
|
.icon=${icon}
|
||||||
.iconPath=${iconPath}
|
.iconPath=${iconPath}
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
@action=${this._handleIconAction}
|
|
||||||
.actionHandler=${actionHandler()}
|
|
||||||
></ha-tile-icon>
|
></ha-tile-icon>
|
||||||
`}
|
`}
|
||||||
|
${badge
|
||||||
|
? html`
|
||||||
|
<ha-tile-badge
|
||||||
|
class="badge"
|
||||||
|
.icon=${badge.icon}
|
||||||
|
.iconPath=${badge.iconPath}
|
||||||
|
style=${styleMap({
|
||||||
|
"--tile-badge-background-color": `rgb(${badge.color})`,
|
||||||
|
})}
|
||||||
|
></ha-tile-badge>
|
||||||
|
`
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
<ha-tile-info
|
<ha-tile-info
|
||||||
.primary=${name}
|
.primary=${name}
|
||||||
.secondary=${stateDisplay}
|
.secondary=${stateDisplay}
|
||||||
@ -198,32 +215,39 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||||||
ha-card.disabled {
|
ha-card.disabled {
|
||||||
background: rgba(var(--rgb-disabled-color), 0.1);
|
background: rgba(var(--rgb-disabled-color), 0.1);
|
||||||
}
|
}
|
||||||
|
[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
[role="button"]:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
.tile {
|
.tile {
|
||||||
padding: calc(12px - var(--tile-tap-padding));
|
padding: calc(12px - var(--tile-tap-padding));
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.icon {
|
.icon-container {
|
||||||
|
position: relative;
|
||||||
padding: var(--tile-tap-padding);
|
padding: var(--tile-tap-padding);
|
||||||
flex: none;
|
flex: none;
|
||||||
margin-right: calc(12px - 2 * var(--tile-tap-padding));
|
margin-right: calc(12px - 2 * var(--tile-tap-padding));
|
||||||
margin-inline-end: calc(12px - 2 * var(--tile-tap-padding));
|
margin-inline-end: calc(12px - 2 * var(--tile-tap-padding));
|
||||||
margin-inline-start: initial;
|
margin-inline-start: initial;
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
--color: var(--tile-color);
|
|
||||||
transition: transform 180ms ease-in-out;
|
transition: transform 180ms ease-in-out;
|
||||||
}
|
}
|
||||||
[role="button"] {
|
.icon-container .icon {
|
||||||
cursor: pointer;
|
--icon-color: rgb(var(--tile-color));
|
||||||
|
--shape-color: rgba(var(--tile-color), 0.2);
|
||||||
}
|
}
|
||||||
.icon[role="button"]:focus {
|
.icon-container .badge {
|
||||||
outline: none;
|
position: absolute;
|
||||||
|
top: calc(-3px + var(--tile-tap-padding));
|
||||||
|
right: calc(-3px + var(--tile-tap-padding));
|
||||||
}
|
}
|
||||||
.icon[role="button"]:focus-visible {
|
.icon-container[role="button"]:focus-visible,
|
||||||
transform: scale(1.2);
|
.icon-container[role="button"]:active {
|
||||||
}
|
|
||||||
.icon[role="button"]:active {
|
|
||||||
transform: scale(1.2);
|
transform: scale(1.2);
|
||||||
}
|
}
|
||||||
ha-tile-info {
|
ha-tile-info {
|
||||||
@ -234,9 +258,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||||||
border-radius: calc(var(--ha-card-border-radius, 10px) - 2px);
|
border-radius: calc(var(--ha-card-border-radius, 10px) - 2px);
|
||||||
transition: background-color 180ms ease-in-out;
|
transition: background-color 180ms ease-in-out;
|
||||||
}
|
}
|
||||||
ha-tile-info:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
ha-tile-info:focus-visible {
|
ha-tile-info:focus-visible {
|
||||||
background-color: rgba(var(--tile-color), 0.1);
|
background-color: rgba(var(--tile-color), 0.1);
|
||||||
}
|
}
|
||||||
|
38
src/panels/lovelace/cards/tile/badges/tile-badge-climate.ts
Normal file
38
src/panels/lovelace/cards/tile/badges/tile-badge-climate.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
mdiClockOutline,
|
||||||
|
mdiFire,
|
||||||
|
mdiPower,
|
||||||
|
mdiSnowflake,
|
||||||
|
mdiWaterPercent,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import { ClimateEntity, HvacAction } from "../../../../../data/climate";
|
||||||
|
import { ComputeBadgeFunction } from "./tile-badge";
|
||||||
|
|
||||||
|
export const CLIMATE_HVAC_ACTION_COLORS: Record<HvacAction, string> = {
|
||||||
|
cooling: "var(--rgb-state-climate-cool-color)",
|
||||||
|
drying: "var(--rgb-state-climate-dry-color)",
|
||||||
|
heating: "var(--rgb-state-climate-heat-color)",
|
||||||
|
idle: "var(--rgb-state-climate-idle-color)",
|
||||||
|
off: "var(--rgb-state-climate-off-color)",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CLIMATE_HVAC_ACTION_ICONS: Record<HvacAction, string> = {
|
||||||
|
cooling: mdiSnowflake,
|
||||||
|
drying: mdiWaterPercent,
|
||||||
|
heating: mdiFire,
|
||||||
|
idle: mdiClockOutline,
|
||||||
|
off: mdiPower,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const computeClimateBadge: ComputeBadgeFunction = (stateObj) => {
|
||||||
|
const hvacAction = (stateObj as ClimateEntity).attributes.hvac_action;
|
||||||
|
|
||||||
|
if (!hvacAction || hvacAction === "off") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
iconPath: CLIMATE_HVAC_ACTION_ICONS[hvacAction],
|
||||||
|
color: CLIMATE_HVAC_ACTION_COLORS[hvacAction],
|
||||||
|
};
|
||||||
|
};
|
44
src/panels/lovelace/cards/tile/badges/tile-badge-person.ts
Normal file
44
src/panels/lovelace/cards/tile/badges/tile-badge-person.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { mdiHelp, mdiHome, mdiHomeExportOutline } from "@mdi/js";
|
||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { UNAVAILABLE_STATES } from "../../../../../data/entity";
|
||||||
|
import { HomeAssistant } from "../../../../../types";
|
||||||
|
import { ComputeBadgeFunction } from "./tile-badge";
|
||||||
|
|
||||||
|
function getZone(entity: HassEntity, hass: HomeAssistant) {
|
||||||
|
const state = entity.state;
|
||||||
|
if (state === "home" || state === "not_home") return undefined;
|
||||||
|
|
||||||
|
const zones = Object.values(hass.states).filter((stateObj) =>
|
||||||
|
stateObj.entity_id.startsWith("zone.")
|
||||||
|
);
|
||||||
|
|
||||||
|
return zones.find((z) => state === z.attributes.friendly_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function personBadgeIcon(entity: HassEntity) {
|
||||||
|
const state = entity.state;
|
||||||
|
if (UNAVAILABLE_STATES.includes(state)) {
|
||||||
|
return mdiHelp;
|
||||||
|
}
|
||||||
|
return state === "not_home" ? mdiHomeExportOutline : mdiHome;
|
||||||
|
}
|
||||||
|
|
||||||
|
function personBadgeColor(entity: HassEntity, inZone?: boolean) {
|
||||||
|
if (inZone) {
|
||||||
|
return "var(--rgb-state-person-zone-color)";
|
||||||
|
}
|
||||||
|
const state = entity.state;
|
||||||
|
return state === "not_home"
|
||||||
|
? "var(--rgb-state-person-not-home-color)"
|
||||||
|
: "var(--rgb-state-person-home-color)";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const computePersonBadge: ComputeBadgeFunction = (stateObj, hass) => {
|
||||||
|
const zone = getZone(stateObj, hass);
|
||||||
|
|
||||||
|
return {
|
||||||
|
iconPath: personBadgeIcon(stateObj),
|
||||||
|
icon: zone?.attributes.icon,
|
||||||
|
color: personBadgeColor(stateObj, Boolean(zone)),
|
||||||
|
};
|
||||||
|
};
|
29
src/panels/lovelace/cards/tile/badges/tile-badge.ts
Normal file
29
src/panels/lovelace/cards/tile/badges/tile-badge.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||||
|
import { HomeAssistant } from "../../../../../types";
|
||||||
|
import { computeClimateBadge } from "./tile-badge-climate";
|
||||||
|
import { computePersonBadge } from "./tile-badge-person";
|
||||||
|
|
||||||
|
export type TileBadge = {
|
||||||
|
color: string;
|
||||||
|
icon?: string;
|
||||||
|
iconPath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ComputeBadgeFunction = (
|
||||||
|
stateObj: HassEntity,
|
||||||
|
hass: HomeAssistant
|
||||||
|
) => TileBadge | undefined;
|
||||||
|
|
||||||
|
export const computeTileBadge: ComputeBadgeFunction = (stateObj, hass) => {
|
||||||
|
const domain = computeDomain(stateObj.entity_id);
|
||||||
|
switch (domain) {
|
||||||
|
case "person":
|
||||||
|
case "device_tracker":
|
||||||
|
return computePersonBadge(stateObj, hass);
|
||||||
|
case "climate":
|
||||||
|
return computeClimateBadge(stateObj, hass);
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
@ -152,6 +152,9 @@ documentContainer.innerHTML = `<custom-style>
|
|||||||
--rgb-state-lock-unlocked-color: var(--rgb-red-color);
|
--rgb-state-lock-unlocked-color: var(--rgb-red-color);
|
||||||
--rgb-state-media-player-color: var(--rgb-indigo-color);
|
--rgb-state-media-player-color: var(--rgb-indigo-color);
|
||||||
--rgb-state-person-color: var(--rgb-blue-grey-color);
|
--rgb-state-person-color: var(--rgb-blue-grey-color);
|
||||||
|
--rgb-state-person-home-color: var(--rgb-green-color);
|
||||||
|
--rgb-state-person-not-home-color: var(--rgb-red-color);
|
||||||
|
--rgb-state-person-zone-color: var(--rgb-blue-color);
|
||||||
--rgb-state-sensor-battery-high-color: var(--rgb-green-color);
|
--rgb-state-sensor-battery-high-color: var(--rgb-green-color);
|
||||||
--rgb-state-sensor-battery-low-color: var(--rgb-red-color);
|
--rgb-state-sensor-battery-low-color: var(--rgb-red-color);
|
||||||
--rgb-state-sensor-battery-medium-color: var(--rgb-orange-color);
|
--rgb-state-sensor-battery-medium-color: var(--rgb-orange-color);
|
||||||
@ -173,6 +176,7 @@ documentContainer.innerHTML = `<custom-style>
|
|||||||
--rgb-state-climate-fan-only-color: var(--rgb-teal-color);
|
--rgb-state-climate-fan-only-color: var(--rgb-teal-color);
|
||||||
--rgb-state-climate-heat-color: var(--rgb-deep-orange-color);
|
--rgb-state-climate-heat-color: var(--rgb-deep-orange-color);
|
||||||
--rgb-state-climate-heat-cool-color: var(--rgb-green-color);
|
--rgb-state-climate-heat-cool-color: var(--rgb-green-color);
|
||||||
|
--rgb-state-climate-idle-color: var(--rgb-disabled-color);
|
||||||
|
|
||||||
/* input components */
|
/* input components */
|
||||||
--input-idle-line-color: rgba(0, 0, 0, 0.42);
|
--input-idle-line-color: rgba(0, 0, 0, 0.42);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user