This commit is contained in:
Petar Petrov
2025-09-30 08:44:59 +03:00
parent 903ab67604
commit 65d15da469
3 changed files with 157 additions and 225 deletions

View File

@@ -4,6 +4,9 @@ import { computeCssColor } from "../../../common/color/compute-color";
import { computeDomain } from "../../../common/entity/compute_domain";
import { generateEntityFilter } from "../../../common/entity/entity_filter";
import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-icon";
import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-info";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import type { HomeAssistant } from "../../../types";
import { handleAction } from "../common/handle-action";
@@ -229,29 +232,25 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
const color = computeCssColor(COLORS[this._config.summary]);
const secondary = this._computeSummaryState();
const tileConfig = {
name: label,
icon,
vertical: this._config.vertical,
tapAction: this._config.tap_action,
holdAction: this._config.hold_action,
doubleTapAction: this._config.double_tap_action,
};
const tileState = {
active: false, // Home summary cards don't have active state
color,
stateDisplay: html`<span>${secondary}</span>`,
};
return html`
<hui-tile
.hass=${this.hass}
.config=${tileConfig}
.state=${tileState}
name=${label}
?vertical=${this._config.vertical}
color=${color}
.hasCardAction=${this._hasCardAction}
.onAction=${this._onAction}
></hui-tile>
.tapAction=${this._config.tap_action}
.holdAction=${this._config.hold_action}
.doubleTapAction=${this._config.double_tap_action}
>
<ha-tile-icon slot="icon">
<ha-icon slot="icon" .icon=${icon}></ha-icon>
</ha-tile-icon>
<ha-tile-info slot="info" id="info">
<span slot="primary" class="primary">${label}</span>
<span slot="secondary">${secondary}</span>
</ha-tile-info>
</hui-tile>
`;
}
}

View File

@@ -1,6 +1,9 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, html, nothing } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
import { DOMAINS_TOGGLE } from "../../../common/const";
@@ -22,6 +25,7 @@ import type {
LovelaceCardEditor,
LovelaceGridOptions,
} from "../types";
import { renderTileBadge } from "./tile/badges/tile-badge";
import type { TileCardConfig } from "./types";
export const getEntityDefaultTileIconAction = (entityId: string) => {
@@ -216,6 +220,23 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
);
}
private _featurePosition = memoizeOne((config: TileCardConfig) => {
if (config.vertical) {
return "bottom";
}
return config.features_position || "bottom";
});
private _displayedFeatures = memoizeOne((config: TileCardConfig) => {
const features = config.features || [];
const featurePosition = this._featurePosition(config);
if (featurePosition === "inline") {
return features.slice(0, 1);
}
return features;
});
protected render() {
if (!this._config || !this.hass) {
return nothing;
@@ -232,7 +253,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
}
const name = this._config.name || computeStateName(stateObj);
const active = stateActive(stateObj);
const color = this._computeStateColor(stateObj, this._config.color);
const domain = computeDomain(stateObj.entity_id);
@@ -252,46 +272,102 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
? this._getImageUrl(stateObj)
: undefined;
const tileConfig = {
name,
stateContent: this._config.state_content,
hideState: this._config.hide_state,
icon: this._config.icon,
color: this._config.color,
showEntityPicture: this._config.show_entity_picture,
vertical: this._config.vertical,
features: this._config.features,
featuresPosition: this._config.features_position,
tapAction: this._config.tap_action,
holdAction: this._config.hold_action,
doubleTapAction: this._config.double_tap_action,
iconTapAction: this._config.icon_tap_action,
iconHoldAction: this._config.icon_hold_action,
iconDoubleTapAction: this._config.icon_double_tap_action,
};
const tileState = {
active,
color,
imageUrl,
stateDisplay,
};
const features = this._displayedFeatures(this._config);
return html`
<hui-tile
.hass=${this.hass}
.config=${tileConfig}
.state=${tileState}
.featureContext=${this._featureContext}
.domain=${domain}
.entityId=${entityId}
?vertical=${this._config.vertical}
color=${ifDefined(color)}
.hasCardAction=${this._hasCardAction}
.hasIconAction=${this._hasIconAction}
.onAction=${this._onAction}
.onIconAction=${this._onIconAction}
></hui-tile>
.tapAction=${this._config.tap_action}
.holdAction=${this._config.hold_action}
.doubleTapAction=${this._config.double_tap_action}
.iconTapAction=${this._config.icon_tap_action}
.iconHoldAction=${this._config.icon_hold_action}
.iconDoubleTapAction=${this._config.icon_double_tap_action}
.featurePosition=${this._featurePosition(this._config)}
>
<ha-tile-icon
slot="icon"
role=${ifDefined(this._hasIconAction ? "button" : undefined)}
tabindex=${ifDefined(this._hasIconAction ? "0" : undefined)}
.imageUrl=${imageUrl}
data-domain=${domain}
data-state=${stateObj.state}
class=${classMap({ image: Boolean(imageUrl) })}
>
<ha-state-icon
slot="icon"
.icon=${this._config.icon}
.stateObj=${stateObj}
.hass=${this.hass}
></ha-state-icon>
${renderTileBadge(stateObj, this.hass)}
</ha-tile-icon>
<ha-tile-info slot="info" id="info">
<span slot="primary" class="primary">${name}</span>
${stateDisplay
? html`<span slot="secondary">${stateDisplay}</span>`
: nothing}
</ha-tile-info>
${features.length
? html`
<hui-card-features
slot="features"
.hass=${this.hass}
.context=${this._featureContext}
.color=${this._config.color}
.features=${features}
></hui-card-features>
`
: nothing}
</hui-tile>
`;
}
static styles = css`
ha-tile-icon[data-domain="alarm_control_panel"][data-state="pending"],
ha-tile-icon[data-domain="alarm_control_panel"][data-state="arming"],
ha-tile-icon[data-domain="alarm_control_panel"][data-state="triggered"],
ha-tile-icon[data-domain="lock"][data-state="jammed"] {
animation: pulse 1s infinite;
}
/* Make sure we display the whole image */
ha-tile-icon.image[data-domain="update"] {
--tile-icon-border-radius: 0;
}
/* Make sure we display the almost the whole image but it often use text */
ha-tile-icon.image[data-domain="media_player"] {
--tile-icon-border-radius: min(
var(--ha-tile-icon-border-radius, var(--ha-border-radius-sm)),
var(--ha-border-radius-sm)
);
}
ha-tile-badge {
position: absolute;
top: 3px;
right: 3px;
inset-inline-end: 3px;
inset-inline-start: initial;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
`;
}
declare global {

View File

@@ -1,63 +1,20 @@
import { LitElement, css, html, nothing } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import "../../../components/ha-card";
import "../../../components/ha-ripple";
import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon";
import "../../../components/tile/ha-tile-badge";
import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-info";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import "../../../state-display/state-display";
import type { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features";
import type { LovelaceCardFeatureContext } from "../card-features/types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { renderTileBadge } from "../cards/tile/badges/tile-badge";
export interface TileConfig {
name: string;
stateContent?: any;
hideState?: boolean;
icon?: string;
color?: string;
showEntityPicture?: boolean;
vertical?: boolean;
features?: any[];
featuresPosition?: string;
tapAction?: any;
holdAction?: any;
doubleTapAction?: any;
iconTapAction?: any;
iconHoldAction?: any;
iconDoubleTapAction?: any;
}
export interface TileState {
active: boolean;
color?: string;
imageUrl?: string;
stateDisplay?: any;
}
import type { LovelaceCardFeaturePosition } from "../card-features/types";
@customElement("hui-tile")
export class HuiTile extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public vertical = false;
@property({ attribute: false }) public config?: TileConfig;
@property({ attribute: false }) public state?: TileState;
@property({ attribute: false })
public featureContext?: LovelaceCardFeatureContext;
@property({ attribute: false }) public domain?: string;
@property({ attribute: false }) public entityId?: string;
@property() public color?: string;
@property({ attribute: false, type: Boolean }) public hasCardAction = false;
@@ -71,62 +28,43 @@ export class HuiTile extends LitElement {
ev: CustomEvent
) => void;
private get _featurePosition() {
if (this.config?.vertical) {
return "bottom";
}
return this.config?.featuresPosition || "bottom";
}
@property({ attribute: false }) public tapAction?: any;
private get _displayedFeatures() {
const features = this.config?.features || [];
const featurePosition = this._featurePosition;
@property({ attribute: false }) public holdAction?: any;
if (featurePosition === "inline") {
return features.slice(0, 1);
}
return features;
}
@property({ attribute: false }) public doubleTapAction?: any;
@property({ attribute: false }) public iconTapAction?: any;
@property({ attribute: false }) public iconHoldAction?: any;
@property({ attribute: false }) public iconDoubleTapAction?: any;
@property({ attribute: false })
public featurePosition?: LovelaceCardFeaturePosition;
private _handleAction(ev: ActionHandlerEvent) {
this.onAction?.(ev);
}
private _handleIconAction(ev: CustomEvent) {
this.onIconAction?.(ev);
}
protected render() {
if (!this.config || !this.state || !this.hass) {
return nothing;
}
const contentClasses = { vertical: Boolean(this.config.vertical) };
const name = this.config.name;
const active = this.state.active;
const color = this.state.color;
const stateDisplay = this.config.hideState
? nothing
: this.state.stateDisplay;
const contentClasses = { vertical: Boolean(this.vertical) };
const style = {
"--tile-color": color,
"--tile-color": this.color,
};
const imageUrl = this.state.imageUrl;
const featurePosition = this._featurePosition;
const features = this._displayedFeatures;
const containerOrientationClass =
featurePosition === "inline" ? "horizontal" : "";
this.featurePosition === "inline" ? "horizontal" : "";
return html`
<ha-card style=${styleMap(style)} class=${classMap({ active })}>
<ha-card style=${styleMap(style)}>
<div
class="background"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this.config!.holdAction),
hasDoubleClick: hasAction(this.config!.doubleTapAction),
hasHold: hasAction(this.holdAction),
hasDoubleClick: hasAction(this.doubleTapAction),
})}
role=${ifDefined(this.hasCardAction ? "button" : undefined)}
tabindex=${ifDefined(this.hasCardAction ? "0" : undefined)}
@@ -136,53 +74,10 @@ export class HuiTile extends LitElement {
</div>
<div class="container ${containerOrientationClass}">
<div class="content ${classMap(contentClasses)}">
<ha-tile-icon
role=${ifDefined(this.hasIconAction ? "button" : undefined)}
tabindex=${ifDefined(this.hasIconAction ? "0" : undefined)}
@action=${this._handleIconAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this.config!.iconHoldAction),
hasDoubleClick: hasAction(this.config!.iconDoubleTapAction),
})}
.interactive=${this.hasIconAction}
.imageUrl=${imageUrl}
data-domain=${ifDefined(this.domain)}
data-state=${ifDefined(
this.entityId
? this.hass.states[this.entityId]?.state
: undefined
)}
class=${classMap({ image: Boolean(imageUrl) })}
>
<ha-state-icon
slot="icon"
.icon=${this.config.icon}
.stateObj=${this.entityId
? this.hass.states[this.entityId]
: undefined}
.hass=${this.hass}
></ha-state-icon>
${this.entityId
? renderTileBadge(this.hass.states[this.entityId], this.hass)
: nothing}
</ha-tile-icon>
<ha-tile-info id="info">
<span slot="primary" class="primary">${name}</span>
${stateDisplay
? html`<span slot="secondary">${stateDisplay}</span>`
: nothing}
</ha-tile-info>
<slot name="icon"></slot>
<slot name="info"></slot>
</div>
${features.length > 0
? html`
<hui-card-features
.hass=${this.hass}
.context=${this.featureContext || {}}
.color=${this.config.color}
.features=${features}
></hui-card-features>
`
: nothing}
<slot name="features"></slot>
</div>
</ha-card>
`;
@@ -259,71 +154,33 @@ export class HuiTile extends LitElement {
text-align: center;
justify-content: center;
}
.vertical ha-tile-info {
.vertical ::slotted(ha-tile-info) {
width: 100%;
flex: none;
}
ha-tile-icon {
::slotted(ha-tile-icon) {
--tile-icon-color: var(--tile-color);
position: relative;
padding: 6px;
margin: -6px;
}
ha-tile-badge {
position: absolute;
top: 3px;
right: 3px;
inset-inline-end: 3px;
inset-inline-start: initial;
}
ha-tile-info {
::slotted(ha-tile-info) {
position: relative;
min-width: 0;
transition: background-color 180ms ease-in-out;
box-sizing: border-box;
}
hui-card-features {
::slotted(features) {
--feature-color: var(--tile-color);
padding: 0 12px 12px 12px;
}
.container.horizontal hui-card-features {
.container.horizontal ::slotted(hui-card-features) {
width: calc(50% - var(--column-gap, 0px) / 2 - 12px);
flex: none;
--feature-height: 36px;
padding: 0 12px;
padding-inline-start: 0;
}
ha-tile-icon[data-domain="alarm_control_panel"][data-state="pending"],
ha-tile-icon[data-domain="alarm_control_panel"][data-state="arming"],
ha-tile-icon[data-domain="alarm_control_panel"][data-state="triggered"],
ha-tile-icon[data-domain="lock"][data-state="jammed"] {
animation: pulse 1s infinite;
}
/* Make sure we display the whole image */
ha-tile-icon.image[data-domain="update"] {
--tile-icon-border-radius: 0;
}
/* Make sure we display the almost the whole image but it often use text */
ha-tile-icon.image[data-domain="media_player"] {
--tile-icon-border-radius: min(
var(--ha-tile-icon-border-radius, var(--ha-border-radius-sm)),
var(--ha-border-radius-sm)
);
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
`;
}