Compare commits

...

5 Commits

Author SHA1 Message Date
Petar Petrov
1a4a6e60fb clean up 2025-09-30 09:35:36 +03:00
Petar Petrov
d377275bff clean up 2025-09-30 08:50:41 +03:00
Petar Petrov
65d15da469 slots 2025-09-30 08:44:59 +03:00
Petar Petrov
903ab67604 Use hui-tile in hui-home-summary-card 2025-09-29 20:55:37 +03:00
Petar Petrov
948b020b7c Reusable hui-tile component 2025-09-29 20:49:54 +03:00
3 changed files with 253 additions and 308 deletions

View File

@@ -1,23 +1,17 @@
import { css, html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } 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 { computeCssColor } from "../../../common/color/compute-color"; import { computeCssColor } from "../../../common/color/compute-color";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { generateEntityFilter } from "../../../common/entity/entity_filter"; import { generateEntityFilter } from "../../../common/entity/entity_filter";
import { formatNumber } from "../../../common/number/format_number"; import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-card";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import "../../../components/ha-ripple";
import "../../../components/tile/ha-tile-icon"; import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-info"; import "../../../components/tile/ha-tile-info";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import "../../../state-display/state-display";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action"; import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action"; import { hasAction } from "../common/has-action";
import "../components/hui-tile";
import { import {
findEntities, findEntities,
getSummaryLabel, getSummaryLabel,
@@ -229,140 +223,32 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
return nothing; return nothing;
} }
const contentClasses = { vertical: Boolean(this._config.vertical) };
const color = computeCssColor(COLORS[this._config.summary]);
const style = {
"--tile-color": color,
};
const secondary = this._computeSummaryState();
const label = getSummaryLabel(this.hass.localize, this._config.summary); const label = getSummaryLabel(this.hass.localize, this._config.summary);
const icon = HOME_SUMMARIES_ICONS[this._config.summary]; const icon = HOME_SUMMARIES_ICONS[this._config.summary];
const color = computeCssColor(COLORS[this._config.summary]);
const secondary = this._computeSummaryState();
return html` return html`
<ha-card style=${styleMap(style)}> <hui-tile
<div name=${label}
class="background" ?vertical=${this._config.vertical}
@action=${this._handleAction} color=${color}
.actionHandler=${actionHandler({ .hasCardAction=${this._hasCardAction}
hasHold: hasAction(this._config!.hold_action), .onAction=${this._handleAction}
hasDoubleClick: hasAction(this._config!.double_tap_action), .tapAction=${this._config.tap_action}
})} .holdAction=${this._config.hold_action}
role=${ifDefined(this._hasCardAction ? "button" : undefined)} .doubleTapAction=${this._config.double_tap_action}
tabindex=${ifDefined(this._hasCardAction ? "0" : undefined)}
aria-labelledby="info"
> >
<ha-ripple .disabled=${!this._hasCardAction}></ha-ripple> <ha-tile-icon slot="icon">
</div>
<div class="container">
<div class="content ${classMap(contentClasses)}">
<ha-tile-icon>
<ha-icon slot="icon" .icon=${icon}></ha-icon> <ha-icon slot="icon" .icon=${icon}></ha-icon>
</ha-tile-icon> </ha-tile-icon>
<ha-tile-info <ha-tile-info slot="info" id="info">
id="info" <span slot="primary" class="primary">${label}</span>
.primary=${label} <span slot="secondary">${secondary}</span>
.secondary=${secondary} </ha-tile-info>
></ha-tile-info> </hui-tile>
</div>
</div>
</ha-card>
`; `;
} }
static styles = css`
:host {
--tile-color: var(--state-inactive-color);
-webkit-tap-highlight-color: transparent;
}
ha-card:has(.background:focus-visible) {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--tile-color);
border-color: var(--tile-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
ha-card {
--ha-ripple-color: var(--tile-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;
height: 100%;
transition:
box-shadow 180ms ease-in-out,
border-color 180ms ease-in-out;
display: flex;
flex-direction: column;
justify-content: space-between;
}
ha-card.active {
--tile-color: var(--state-icon-color);
}
[role="button"] {
cursor: pointer;
pointer-events: auto;
}
[role="button"]:focus {
outline: none;
}
.background {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
border-radius: var(--ha-card-border-radius, 12px);
margin: calc(-1 * var(--ha-card-border-width, 1px));
overflow: hidden;
}
.container {
margin: calc(-1 * var(--ha-card-border-width, 1px));
display: flex;
flex-direction: column;
flex: 1;
}
.container.horizontal {
flex-direction: row;
}
.content {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
padding: 10px;
flex: 1;
min-width: 0;
box-sizing: border-box;
pointer-events: none;
gap: 10px;
}
.vertical {
flex-direction: column;
text-align: center;
justify-content: center;
}
.vertical ha-tile-info {
width: 100%;
flex: none;
}
ha-tile-icon {
--tile-icon-color: var(--tile-color);
position: relative;
padding: 6px;
margin: -6px;
}
ha-tile-info {
position: relative;
min-width: 0;
transition: background-color 180ms ease-in-out;
box-sizing: border-box;
}
`;
} }
declare global { declare global {

View File

@@ -3,7 +3,6 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color"; import { computeCssColor } from "../../../common/color/compute-color";
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color"; import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
@@ -12,24 +11,15 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateActive } from "../../../common/entity/state_active"; import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color"; import { stateColorCss } from "../../../common/entity/state_color";
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 { cameraUrlWithWidthHeight } from "../../../data/camera"; import { cameraUrlWithWidthHeight } from "../../../data/camera";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import "../../../state-display/state-display";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features";
import type { LovelaceCardFeatureContext } from "../card-features/types"; import type { LovelaceCardFeatureContext } from "../card-features/types";
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 { hasAction } from "../common/has-action"; import { hasAction } from "../common/has-action";
import { createEntityNotFoundWarning } from "../components/hui-warning"; import { createEntityNotFoundWarning } from "../components/hui-warning";
import "../components/hui-tile";
import type { import type {
LovelaceCard, LovelaceCard,
LovelaceCardEditor, LovelaceCardEditor,
@@ -37,6 +27,7 @@ import type {
} from "../types"; } from "../types";
import { renderTileBadge } from "./tile/badges/tile-badge"; import { renderTileBadge } from "./tile/badges/tile-badge";
import type { TileCardConfig } from "./types"; import type { TileCardConfig } from "./types";
import { actionHandler } from "../common/directives/action-handler-directive";
export const getEntityDefaultTileIconAction = (entityId: string) => { export const getEntityDefaultTileIconAction = (entityId: string) => {
const domain = computeDomain(entityId); const domain = computeDomain(entityId);
@@ -253,10 +244,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
`; `;
} }
const contentClasses = { vertical: Boolean(this._config.vertical) };
const name = this._config.name || computeStateName(stateObj); const name = this._config.name || computeStateName(stateObj);
const active = stateActive(stateObj);
const color = this._computeStateColor(stateObj, this._config.color); const color = this._computeStateColor(stateObj, this._config.color);
const domain = computeDomain(stateObj.entity_id); const domain = computeDomain(stateObj.entity_id);
@@ -272,38 +260,26 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
</state-display> </state-display>
`; `;
const style = {
"--tile-color": color,
};
const imageUrl = this._config.show_entity_picture const imageUrl = this._config.show_entity_picture
? this._getImageUrl(stateObj) ? this._getImageUrl(stateObj)
: undefined; : undefined;
const featurePosition = this._featurePosition(this._config);
const features = this._displayedFeatures(this._config); const features = this._displayedFeatures(this._config);
const containerOrientationClass =
featurePosition === "inline" ? "horizontal" : "";
return html` return html`
<ha-card style=${styleMap(style)} class=${classMap({ active })}> <hui-tile
<div ?vertical=${this._config.vertical}
class="background" color=${ifDefined(color)}
@action=${this._handleAction} .hasCardAction=${this._hasCardAction}
.actionHandler=${actionHandler({ .hasIconAction=${this._hasIconAction}
hasHold: hasAction(this._config!.hold_action), .onAction=${this._handleAction}
hasDoubleClick: hasAction(this._config!.double_tap_action), .tapAction=${this._config.tap_action}
})} .holdAction=${this._config.hold_action}
role=${ifDefined(this._hasCardAction ? "button" : undefined)} .doubleTapAction=${this._config.double_tap_action}
tabindex=${ifDefined(this._hasCardAction ? "0" : undefined)} .featurePosition=${this._featurePosition(this._config)}
aria-labelledby="info"
> >
<ha-ripple .disabled=${!this._hasCardAction}></ha-ripple>
</div>
<div class="container ${containerOrientationClass}">
<div class="content ${classMap(contentClasses)}">
<ha-tile-icon <ha-tile-icon
slot="icon"
role=${ifDefined(this._hasIconAction ? "button" : undefined)} role=${ifDefined(this._hasIconAction ? "button" : undefined)}
tabindex=${ifDefined(this._hasIconAction ? "0" : undefined)} tabindex=${ifDefined(this._hasIconAction ? "0" : undefined)}
@action=${this._handleIconAction} @action=${this._handleIconAction}
@@ -325,16 +301,16 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
></ha-state-icon> ></ha-state-icon>
${renderTileBadge(stateObj, this.hass)} ${renderTileBadge(stateObj, this.hass)}
</ha-tile-icon> </ha-tile-icon>
<ha-tile-info id="info"> <ha-tile-info slot="info" id="info">
<span slot="primary" class="primary">${name}</span> <span slot="primary" class="primary">${name}</span>
${stateDisplay ${stateDisplay
? html`<span slot="secondary">${stateDisplay}</span>` ? html`<span slot="secondary">${stateDisplay}</span>`
: nothing} : nothing}
</ha-tile-info> </ha-tile-info>
</div> ${features.length
${features.length > 0
? html` ? html`
<hui-card-features <hui-card-features
slot="features"
.hass=${this.hass} .hass=${this.hass}
.context=${this._featureContext} .context=${this._featureContext}
.color=${this._config.color} .color=${this._config.color}
@@ -342,92 +318,11 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
></hui-card-features> ></hui-card-features>
` `
: nothing} : nothing}
</div> </hui-tile>
</ha-card>
`; `;
} }
static styles = css` static styles = css`
:host {
--tile-color: var(--state-inactive-color);
-webkit-tap-highlight-color: transparent;
}
ha-card:has(.background:focus-visible) {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--tile-color);
border-color: var(--tile-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
ha-card {
--ha-ripple-color: var(--tile-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;
height: 100%;
transition:
box-shadow 180ms ease-in-out,
border-color 180ms ease-in-out;
display: flex;
flex-direction: column;
justify-content: space-between;
}
ha-card.active {
--tile-color: var(--state-icon-color);
}
[role="button"] {
cursor: pointer;
pointer-events: auto;
}
[role="button"]:focus {
outline: none;
}
.background {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
border-radius: var(--ha-card-border-radius, 12px);
margin: calc(-1 * var(--ha-card-border-width, 1px));
overflow: hidden;
}
.container {
margin: calc(-1 * var(--ha-card-border-width, 1px));
display: flex;
flex-direction: column;
flex: 1;
}
.container.horizontal {
flex-direction: row;
}
.content {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
padding: 10px;
flex: 1;
min-width: 0;
box-sizing: border-box;
pointer-events: none;
gap: 10px;
}
.vertical {
flex-direction: column;
text-align: center;
justify-content: center;
}
.vertical ha-tile-info {
width: 100%;
flex: none;
}
ha-tile-icon {
--tile-icon-color: var(--tile-color);
position: relative;
padding: 6px;
margin: -6px;
}
ha-tile-badge { ha-tile-badge {
position: absolute; position: absolute;
top: 3px; top: 3px;
@@ -435,23 +330,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
inset-inline-end: 3px; inset-inline-end: 3px;
inset-inline-start: initial; inset-inline-start: initial;
} }
ha-tile-info {
position: relative;
min-width: 0;
transition: background-color 180ms ease-in-out;
box-sizing: border-box;
}
hui-card-features {
--feature-color: var(--tile-color);
padding: 0 12px 12px 12px;
}
.container.horizontal 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="pending"],
ha-tile-icon[data-domain="alarm_control_panel"][data-state="arming"], ha-tile-icon[data-domain="alarm_control_panel"][data-state="arming"],

View File

@@ -0,0 +1,181 @@
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 type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import type { LovelaceCardFeaturePosition } from "../card-features/types";
@customElement("hui-tile")
export class HuiTile extends LitElement {
@property({ type: Boolean }) public vertical = false;
@property() public color?: string;
@property({ attribute: false, type: Boolean }) public hasCardAction = false;
@property({ attribute: false, type: Boolean }) public hasIconAction = false;
@property({ attribute: false }) public onAction?: (
ev: ActionHandlerEvent
) => void;
@property({ attribute: false }) public tapAction?: any;
@property({ attribute: false }) public holdAction?: any;
@property({ attribute: false }) public doubleTapAction?: any;
@property({ attribute: false })
public featurePosition?: LovelaceCardFeaturePosition;
private _handleAction(ev: ActionHandlerEvent) {
this.onAction?.(ev);
}
protected render() {
const contentClasses = { vertical: Boolean(this.vertical) };
const style = {
"--tile-color": this.color,
};
const containerOrientationClass =
this.featurePosition === "inline" ? "horizontal" : "";
return html`
<ha-card style=${styleMap(style)}>
<div
class="background"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this.holdAction),
hasDoubleClick: hasAction(this.doubleTapAction),
})}
role=${ifDefined(this.hasCardAction ? "button" : undefined)}
tabindex=${ifDefined(this.hasCardAction ? "0" : undefined)}
aria-labelledby="info"
>
<ha-ripple .disabled=${!this.hasCardAction}></ha-ripple>
</div>
<div class="container ${containerOrientationClass}">
<div class="content ${classMap(contentClasses)}">
<slot name="icon"></slot>
<slot name="info"></slot>
</div>
<slot name="features"></slot>
</div>
</ha-card>
`;
}
static styles = css`
:host {
--tile-color: var(--state-inactive-color);
-webkit-tap-highlight-color: transparent;
}
ha-card:has(.background:focus-visible) {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--tile-color);
border-color: var(--tile-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
ha-card {
--ha-ripple-color: var(--tile-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;
height: 100%;
transition:
box-shadow 180ms ease-in-out,
border-color 180ms ease-in-out;
display: flex;
flex-direction: column;
justify-content: space-between;
}
ha-card.active {
--tile-color: var(--state-icon-color);
}
[role="button"] {
cursor: pointer;
pointer-events: auto;
}
[role="button"]:focus {
outline: none;
}
.background {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
border-radius: var(--ha-card-border-radius, 12px);
margin: calc(-1 * var(--ha-card-border-width, 1px));
overflow: hidden;
}
.container {
margin: calc(-1 * var(--ha-card-border-width, 1px));
display: flex;
flex-direction: column;
flex: 1;
}
.container.horizontal {
flex-direction: row;
}
.content {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
padding: 10px;
flex: 1;
min-width: 0;
box-sizing: border-box;
pointer-events: none;
gap: 10px;
}
.vertical {
flex-direction: column;
text-align: center;
justify-content: center;
}
.vertical ::slotted(ha-tile-info) {
width: 100%;
flex: none;
}
::slotted(ha-tile-icon) {
--tile-icon-color: var(--tile-color);
position: relative;
padding: 6px;
margin: -6px;
}
::slotted(ha-tile-info) {
position: relative;
min-width: 0;
transition: background-color 180ms ease-in-out;
box-sizing: border-box;
}
::slotted(features) {
--feature-color: var(--tile-color);
padding: 0 12px 12px 12px;
}
.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;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hui-tile": HuiTile;
}
}