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 { 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 { computeDomain } from "../../../common/entity/compute_domain";
import { generateEntityFilter } from "../../../common/entity/entity_filter";
import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import "../../../components/ha-ripple";
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 { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import "../components/hui-tile";
import {
findEntities,
getSummaryLabel,
@@ -229,140 +223,32 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
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 icon = HOME_SUMMARIES_ICONS[this._config.summary];
const color = computeCssColor(COLORS[this._config.summary]);
const secondary = this._computeSummaryState();
return html`
<ha-card style=${styleMap(style)}>
<div
class="background"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
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">
<div class="content ${classMap(contentClasses)}">
<ha-tile-icon>
<ha-icon slot="icon" .icon=${icon}></ha-icon>
</ha-tile-icon>
<ha-tile-info
id="info"
.primary=${label}
.secondary=${secondary}
></ha-tile-info>
</div>
</div>
</ha-card>
<hui-tile
name=${label}
?vertical=${this._config.vertical}
color=${color}
.hasCardAction=${this._hasCardAction}
.onAction=${this._handleAction}
.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>
`;
}
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 {

View File

@@ -3,7 +3,6 @@ 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 { 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";
@@ -12,24 +11,15 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateActive } from "../../../common/entity/state_active";
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 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 { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import "../components/hui-tile";
import type {
LovelaceCard,
LovelaceCardEditor,
@@ -37,6 +27,7 @@ import type {
} from "../types";
import { renderTileBadge } from "./tile/badges/tile-badge";
import type { TileCardConfig } from "./types";
import { actionHandler } from "../common/directives/action-handler-directive";
export const getEntityDefaultTileIconAction = (entityId: string) => {
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 active = stateActive(stateObj);
const color = this._computeStateColor(stateObj, this._config.color);
const domain = computeDomain(stateObj.entity_id);
@@ -272,162 +260,69 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
</state-display>
`;
const style = {
"--tile-color": color,
};
const imageUrl = this._config.show_entity_picture
? this._getImageUrl(stateObj)
: undefined;
const featurePosition = this._featurePosition(this._config);
const features = this._displayedFeatures(this._config);
const containerOrientationClass =
featurePosition === "inline" ? "horizontal" : "";
return html`
<ha-card style=${styleMap(style)} class=${classMap({ active })}>
<div
class="background"
@action=${this._handleAction}
<hui-tile
?vertical=${this._config.vertical}
color=${ifDefined(color)}
.hasCardAction=${this._hasCardAction}
.hasIconAction=${this._hasIconAction}
.onAction=${this._handleAction}
.tapAction=${this._config.tap_action}
.holdAction=${this._config.hold_action}
.doubleTapAction=${this._config.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)}
@action=${this._handleIconAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
hasHold: hasAction(this._config!.icon_hold_action),
hasDoubleClick: hasAction(this._config!.icon_double_tap_action),
})}
role=${ifDefined(this._hasCardAction ? "button" : undefined)}
tabindex=${ifDefined(this._hasCardAction ? "0" : undefined)}
aria-labelledby="info"
.interactive=${this._hasIconAction}
.imageUrl=${imageUrl}
data-domain=${ifDefined(domain)}
data-state=${ifDefined(stateObj?.state)}
class=${classMap({ image: Boolean(imageUrl) })}
>
<ha-ripple .disabled=${!this._hasCardAction}></ha-ripple>
</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!.icon_hold_action),
hasDoubleClick: hasAction(this._config!.icon_double_tap_action),
})}
.interactive=${this._hasIconAction}
.imageUrl=${imageUrl}
data-domain=${ifDefined(domain)}
data-state=${ifDefined(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 id="info">
<span slot="primary" class="primary">${name}</span>
${stateDisplay
? html`<span slot="secondary">${stateDisplay}</span>`
: nothing}
</ha-tile-info>
</div>
${features.length > 0
? html`
<hui-card-features
.hass=${this.hass}
.context=${this._featureContext}
.color=${this._config.color}
.features=${features}
></hui-card-features>
`
<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}
</div>
</ha-card>
</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`
: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 {
position: absolute;
top: 3px;
@@ -435,23 +330,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
inset-inline-end: 3px;
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="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;
}
}