1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-02-15 07:25:54 +00:00

Create reusable ha tile container component. (#29038)

* Create reusable component for tile based card

* Fix icon interaction

* Add icon and iconPath props

* Migrate discovered devices card

* Refactor

* Share card style
This commit is contained in:
Paul Bottein
2026-01-20 07:49:10 +01:00
committed by GitHub
parent 07aa8706ce
commit c5ad074dfb
7 changed files with 473 additions and 645 deletions

View File

@@ -0,0 +1,155 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import type { ActionHandlerOptions } from "../../data/lovelace/action_handler";
import { actionHandler } from "../../panels/lovelace/common/directives/action-handler-directive";
import "../ha-ripple";
@customElement("ha-tile-container")
export class HaTileContainer extends LitElement {
@property({ attribute: false })
public featurePosition: "bottom" | "inline" = "bottom";
@property({ type: Boolean })
public vertical = false;
@property({ type: Boolean, attribute: false })
public interactive = false;
@property({ attribute: false })
public actionHandlerOptions?: ActionHandlerOptions;
private _handleFocus(ev: FocusEvent) {
if ((ev.target as HTMLElement).matches(":focus-visible")) {
this.setAttribute("focused", "");
}
}
private _handleBlur() {
this.removeAttribute("focused");
}
protected render() {
const containerOrientationClass =
this.featurePosition === "inline" ? "horizontal" : "";
const contentClasses = { vertical: this.vertical };
return html`
<div
class="background"
role=${ifDefined(this.interactive ? "button" : undefined)}
tabindex=${ifDefined(this.interactive ? "0" : undefined)}
aria-labelledby="info"
.actionHandler=${actionHandler(this.actionHandlerOptions)}
@focus=${this._handleFocus}
@blur=${this._handleBlur}
>
<ha-ripple .disabled=${!this.interactive}></ha-ripple>
</div>
<div class="container ${containerOrientationClass}">
<div class="content ${classMap(contentClasses)}">
<slot name="icon"></slot>
<slot name="info" id="info"></slot>
</div>
<slot name="features"></slot>
</div>
`;
}
static styles = css`
:host {
-webkit-tap-highlight-color: transparent;
--ha-ripple-color: var(--tile-color);
--ha-ripple-hover-opacity: 0.04;
--ha-ripple-pressed-opacity: 0.12;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.background {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
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([slot="info"]) {
width: 100%;
flex: none;
}
::slotted([slot="icon"]) {
position: relative;
padding: 6px;
margin: -6px;
}
::slotted([slot="icon"]:focus) {
outline: none;
}
::slotted([slot="info"]) {
position: relative;
min-width: 0;
transition: background-color 180ms ease-in-out;
box-sizing: border-box;
}
::slotted([slot="features"]) {
padding: 0 var(--ha-space-3) var(--ha-space-3) var(--ha-space-3);
}
.container.horizontal ::slotted([slot="features"]) {
width: calc(50% - var(--column-gap, 0px) / 2 - var(--ha-space-3));
flex: none;
--feature-height: var(--ha-space-9);
padding: 0 var(--ha-space-3);
padding-inline-start: 0;
}
[role="button"] {
cursor: pointer;
pointer-events: auto;
}
[role="button"]:focus {
outline: none;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-tile-container": HaTileContainer;
}
}

View File

@@ -1,6 +1,9 @@
import type { TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import type { ActionHandlerOptions } from "../../data/lovelace/action_handler";
import { actionHandler } from "../../panels/lovelace/common/directives/action-handler-directive";
import "../ha-icon";
import "../ha-svg-icon";
@@ -13,34 +16,53 @@ import "../ha-svg-icon";
* A tile icon component, used in tile card in Home Assistant to display an icon or image.
*
* @slot - Additional content (for example, a badge).
* @slot icon - The icon container (usually for icons).
* @slot icon - The icon container (usually for custom icons like ha-state-icon).
*
* @cssprop --ha-tile-icon-border-radius - The border radius of the tile icon. defaults to `var(--ha-border-radius-pill)`.
*
* @attr {boolean} interactive - Whether the icon is interactive (hover and focus styles).
* @attr {string} image-url - The URL of the image to display instead of an icon.
*/
@customElement("ha-tile-icon")
export class HaTileIcon extends LitElement {
@property({ type: Boolean, reflect: true })
@property({ type: Boolean, reflect: true, attribute: "interactive" })
public interactive = false;
@property({ attribute: "image-url", type: String })
public imageUrl?: string;
protected render(): TemplateResult {
@property({ type: String })
public icon?: string;
@property({ type: String, attribute: "icon-path" })
public iconPath?: string;
@property({ attribute: false })
public actionHandlerOptions?: ActionHandlerOptions;
private _renderIcon() {
if (this.imageUrl) {
return html`
<div class="container">
<img alt="" src=${this.imageUrl} />
</div>
<slot></slot>
`;
return html`<img alt="" src=${this.imageUrl} />`;
}
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
}
if (this.iconPath) {
return html`<ha-svg-icon .path=${this.iconPath}></ha-svg-icon>`;
}
return nothing;
}
protected render(): TemplateResult {
const hasImage = Boolean(this.imageUrl);
return html`
<div class="container ${this.interactive ? "background" : ""}">
<slot name="icon"></slot>
<div
class="container ${this.interactive && !hasImage ? "background" : ""}"
role=${ifDefined(this.interactive ? "button" : undefined)}
tabindex=${ifDefined(this.interactive ? "0" : undefined)}
.actionHandler=${actionHandler(this.actionHandlerOptions)}
>
<slot name="icon">${this._renderIcon()}</slot>
</div>
<slot></slot>
`;
@@ -60,6 +82,11 @@ export class HaTileIcon extends LitElement {
position: relative;
user-select: none;
transition: transform 180ms ease-in-out;
pointer-events: none;
}
:host([interactive]) {
-webkit-tap-highlight-color: transparent;
pointer-events: auto;
}
:host([interactive]:active) {
transform: scale(1.2);
@@ -78,9 +105,16 @@ export class HaTileIcon extends LitElement {
overflow: hidden;
transition: box-shadow 180ms ease-in-out;
}
:host([interactive]:focus-visible) .container {
.container:focus-visible {
box-shadow: 0 0 0 2px var(--tile-icon-color);
}
.container:focus {
outline: none;
}
[role="button"] {
cursor: pointer;
pointer-events: auto;
}
.container.background::before {
content: "";
position: absolute;
@@ -94,7 +128,9 @@ export class HaTileIcon extends LitElement {
opacity 180ms ease-in-out;
opacity: var(--tile-icon-opacity);
}
.container ::slotted([slot="icon"]) {
.container ::slotted([slot="icon"]),
.container ha-icon,
.container ha-svg-icon {
display: flex;
color: var(--tile-icon-color);
transition: color 180ms ease-in-out;

View File

@@ -9,8 +9,6 @@ import {
type TemplateResult,
} 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";
@@ -30,21 +28,20 @@ import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/ha-domain-icon";
import "../../../components/ha-icon";
import "../../../components/ha-ripple";
import "../../../components/ha-svg-icon";
import "../../../components/tile/ha-tile-badge";
import "../../../components/tile/ha-tile-container";
import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-info";
import { isUnavailableState } from "../../../data/entity/entity";
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 type {
LovelaceCard,
LovelaceCardEditor,
LovelaceGridOptions,
} from "../types";
import { tileCardStyle } from "./tile/tile-card-style";
import type { AreaCardConfig } from "./types";
export const DEFAULT_ASPECT_RATIO = "16:9";
@@ -548,9 +545,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
`;
}
const contentClasses = { vertical: Boolean(this._config.vertical) };
const icon = area.icon;
const icon = area.icon || undefined;
const name = this._config.name || computeAreaName(area);
@@ -560,9 +555,6 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
const featurePosition = this._featurePosition(this._config);
const features = this._displayedFeatures(this._config);
const containerOrientationClass =
featurePosition === "inline" ? "horizontal" : "";
const displayType = this._config.display_type || "picture";
const cameraEntityId =
@@ -582,16 +574,6 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
return html`
<ha-card style=${styleMap(style)}>
<div
class="background"
@action=${this._handleAction}
.actionHandler=${actionHandler()}
role=${ifDefined(this._hasCardAction ? "button" : undefined)}
tabindex=${ifDefined(this._hasCardAction ? "0" : undefined)}
aria-labelledby="info"
>
<ha-ripple .disabled=${!this._hasCardAction}></ha-ripple>
</div>
${displayType === "compact"
? nothing
: html`
@@ -628,30 +610,30 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
${this._renderAlertSensors()}
</div>
`}
<div class="container ${containerOrientationClass}">
<div class="content ${classMap(contentClasses)}">
<ha-tile-icon>
${displayType === "compact"
? this._renderAlertSensorBadge()
: nothing}
${icon
? html`<ha-icon slot="icon" .icon=${icon}></ha-icon>`
: html`
<ha-svg-icon
slot="icon"
.path=${mdiTextureBox}
></ha-svg-icon>
`}
</ha-tile-icon>
<ha-tile-info
id="info"
.primary=${primary}
.secondary=${secondary}
></ha-tile-info>
</div>
<ha-tile-container
.featurePosition=${featurePosition}
.vertical=${Boolean(this._config.vertical)}
.interactive=${Boolean(this._hasCardAction)}
@action=${this._handleAction}
>
<ha-tile-icon
slot="icon"
.icon=${icon}
.iconPath=${icon ? undefined : mdiTextureBox}
>
${displayType === "compact"
? this._renderAlertSensorBadge()
: nothing}
</ha-tile-icon>
<ha-tile-info
slot="info"
.primary=${primary}
.secondary=${secondary}
></ha-tile-info>
${features.length > 0
? html`
<hui-card-features
slot="features"
.hass=${this.hass}
.context=${this._featureContext}
.color=${this._config.color}
@@ -660,181 +642,100 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
></hui-card-features>
`
: nothing}
</div>
</ha-tile-container>
</ha-card>
`;
}
static styles = css`
:host {
--tile-color: var(--state-icon-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;
}
[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, var(--ha-border-radius-lg));
margin: calc(-1 * var(--ha-card-border-width, 1px));
overflow: hidden;
}
.header {
flex: 1;
overflow: hidden;
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
border-end-end-radius: 0;
border-end-start-radius: 0;
pointer-events: none;
}
.picture {
height: 100%;
width: 100%;
background-size: cover;
background-position: center;
position: relative;
}
.picture hui-image {
height: 100%;
}
.picture .icon-container {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
--mdc-icon-size: var(--ha-space-12);
color: var(--tile-color);
}
.picture .icon-container::before {
position: absolute;
content: "";
width: 100%;
height: 100%;
background-color: var(--tile-color);
opacity: 0.12;
}
.container {
margin: calc(-1 * var(--ha-card-border-width, 1px));
display: flex;
flex-direction: column;
flex: 1;
}
.header + .container {
height: auto;
flex: none;
}
.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;
right: 3px;
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 var(--ha-space-3) var(--ha-space-3) var(--ha-space-3);
}
.container.horizontal hui-card-features {
width: calc(50% - var(--column-gap, 0px) / 2 - var(--ha-space-3));
flex: none;
--feature-height: var(--ha-space-9);
padding: 0 var(--ha-space-3);
padding-inline-start: 0;
}
.alert-badge {
--tile-badge-background-color: var(--orange-color);
}
.alerts {
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: row;
gap: var(--ha-space-2);
padding: var(--ha-space-2);
pointer-events: none;
z-index: 1;
}
.alert {
background-color: var(--orange-color);
border-radius: var(--ha-border-radius-lg);
width: var(--ha-space-6);
height: var(--ha-space-6);
padding: 2px;
box-sizing: border-box;
--mdc-icon-size: var(--ha-space-4);
display: flex;
align-items: center;
justify-content: center;
color: white;
}
`;
static styles = [
tileCardStyle,
css`
:host {
--tile-color: var(--state-icon-color);
}
ha-card {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.header {
flex: 1;
overflow: hidden;
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
border-end-end-radius: 0;
border-end-start-radius: 0;
pointer-events: none;
}
.picture {
height: 100%;
width: 100%;
background-size: cover;
background-position: center;
position: relative;
}
.picture hui-image {
height: 100%;
}
.picture .icon-container {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
--mdc-icon-size: var(--ha-space-12);
color: var(--tile-color);
}
.picture .icon-container::before {
position: absolute;
content: "";
width: 100%;
height: 100%;
background-color: var(--tile-color);
opacity: 0.12;
}
.header + ha-tile-container {
height: auto;
flex: none;
}
ha-tile-badge {
position: absolute;
top: 3px;
right: 3px;
inset-inline-end: 3px;
inset-inline-start: initial;
}
hui-card-features {
--feature-color: var(--tile-color);
}
.alert-badge {
--tile-badge-background-color: var(--orange-color);
}
.alerts {
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: row;
gap: var(--ha-space-2);
padding: var(--ha-space-2);
pointer-events: none;
z-index: 1;
}
.alert {
background-color: var(--orange-color);
border-radius: var(--ha-border-radius-lg);
width: var(--ha-space-6);
height: var(--ha-space-6);
padding: 2px;
box-sizing: border-box;
--mdc-icon-size: var(--ha-space-4);
display: flex;
align-items: center;
justify-content: center;
color: white;
}
`,
];
}
declare global {

View File

@@ -1,14 +1,11 @@
import { mdiDevices } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { css, 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 { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-card";
import "../../../components/ha-ripple";
import "../../../components/ha-svg-icon";
import "../../../components/tile/ha-tile-container";
import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-info";
import {
@@ -20,14 +17,12 @@ import type { DataEntryFlowProgress } from "../../../data/data_entry_flow";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
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 type { LovelaceCard, LovelaceGridOptions } from "../types";
import { tileCardStyle } from "./tile/tile-card-style";
import type { DiscoveredDevicesCardConfig } from "./types";
const ICON = mdiDevices;
@customElement("hui-discovered-devices-card")
export class HuiDiscoveredDevicesCard
extends SubscribeMixin(LitElement)
@@ -159,121 +154,36 @@ export class HuiDiscoveredDevicesCard
})
: this.hass.localize("ui.card.discovered-devices.no_devices");
const contentClasses = { vertical: Boolean(this._config.vertical) };
return html`
<ha-card>
<div
class="background"
@action=${this._handleAction}
.actionHandler=${actionHandler({
<ha-tile-container
.vertical=${Boolean(this._config.vertical)}
.interactive=${this._hasCardAction}
.actionHandlerOptions=${{
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"
}}
@action=${this._handleAction}
>
<ha-ripple .disabled=${!this._hasCardAction}></ha-ripple>
</div>
<div class="container">
<div class="content ${classMap(contentClasses)}">
<ha-tile-icon>
<ha-svg-icon slot="icon" .path=${ICON}></ha-svg-icon>
</ha-tile-icon>
<ha-tile-info
id="info"
.primary=${label}
.secondary=${secondary}
></ha-tile-info>
</div>
</div>
<ha-tile-icon slot="icon" .iconPath=${mdiDevices}></ha-tile-icon>
<ha-tile-info
slot="info"
.primary=${label}
.secondary=${secondary}
></ha-tile-info>
</ha-tile-container>
</ha-card>
`;
}
static styles = css`
:host {
--tile-color: var(--primary-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;
}
[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, var(--ha-border-radius-lg));
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;
}
.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;
}
`;
static styles = [
tileCardStyle,
css`
:host {
--tile-color: var(--primary-color);
}
`,
];
}
declare global {

View File

@@ -2,8 +2,6 @@ import { endOfDay, startOfDay } from "date-fns";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, 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 { calcDate } from "../../../common/datetime/calc_date";
@@ -14,8 +12,7 @@ import {
} 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-container";
import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-info";
import type { EnergyData } from "../../../data/energy";
@@ -28,7 +25,6 @@ import {
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
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 {
@@ -38,6 +34,7 @@ import {
type HomeSummary,
} from "../strategies/home/helpers/home-summaries";
import type { LovelaceCard, LovelaceGridOptions } from "../types";
import { tileCardStyle } from "./tile/tile-card-style";
import type { HomeSummaryCard } from "./types";
const COLORS: Record<HomeSummary, string> = {
@@ -269,8 +266,6 @@ export class HuiHomeSummaryCard
return nothing;
}
const contentClasses = { vertical: Boolean(this._config.vertical) };
const color = computeCssColor(COLORS[this._config.summary]);
const style = {
@@ -284,125 +279,34 @@ export class HuiHomeSummaryCard
return html`
<ha-card style=${styleMap(style)}>
<div
class="background"
@action=${this._handleAction}
.actionHandler=${actionHandler({
<ha-tile-container
.vertical=${Boolean(this._config.vertical)}
.interactive=${this._hasCardAction}
.actionHandlerOptions=${{
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"
}}
@action=${this._handleAction}
>
<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-tile-icon slot="icon" .icon=${icon}></ha-tile-icon>
<ha-tile-info
slot="info"
.primary=${label}
.secondary=${secondary}
></ha-tile-info>
</ha-tile-container>
</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, var(--ha-border-radius-lg));
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;
}
`;
static styles = [
tileCardStyle,
css`
:host {
--tile-color: var(--state-inactive-color);
}
`,
];
}
declare global {

View File

@@ -12,9 +12,8 @@ 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-ripple";
import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon";
import "../../../components/tile/ha-tile-container";
import "../../../components/tile/ha-tile-badge";
import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-info";
@@ -24,12 +23,12 @@ 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 { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
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 { tileCardStyle } from "./tile/tile-card-style";
import type {
LovelaceCard,
LovelaceCardEditor,
@@ -253,8 +252,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
`;
}
const contentClasses = { vertical: Boolean(this._config.vertical) };
const name = computeLovelaceEntityName(
this.hass,
stateObj,
@@ -287,58 +284,49 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
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}
.actionHandler=${actionHandler({
<ha-tile-container
.featurePosition=${featurePosition}
.vertical=${Boolean(this._config.vertical)}
.interactive=${this._hasCardAction}
.actionHandlerOptions=${{
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"
}}
@action=${this._handleAction}
>
<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>
<ha-tile-icon
slot="icon"
@action=${this._handleIconAction}
.actionHandlerOptions=${{
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 slot="info">
<span slot="primary" class="primary">${name}</span>
${stateDisplay
? html`<span slot="secondary">${stateDisplay}</span>`
: nothing}
</ha-tile-info>
${features.length > 0
? html`
<hui-card-features
slot="features"
.hass=${this.hass}
.context=${this._featureContext}
.color=${this._config.color}
@@ -346,148 +334,63 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
></hui-card-features>
`
: nothing}
</div>
</ha-tile-container>
</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, var(--ha-border-radius-lg));
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;
right: 3px;
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 var(--ha-space-3) var(--ha-space-3) var(--ha-space-3);
}
.container.horizontal hui-card-features {
width: calc(50% - var(--column-gap, 0px) / 2 - var(--ha-space-3));
flex: none;
--feature-height: var(--ha-space-9);
padding: 0 var(--ha-space-3);
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: var(--ha-border-radius-square);
}
/* 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;
static styles = [
tileCardStyle,
css`
:host {
--tile-color: var(--state-inactive-color);
}
50% {
opacity: 0;
ha-card.active {
--tile-color: var(--state-icon-color);
}
100% {
opacity: 1;
ha-tile-badge {
position: absolute;
top: 3px;
right: 3px;
inset-inline-end: 3px;
inset-inline-start: initial;
}
}
`;
hui-card-features {
--feature-color: var(--tile-color);
}
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: var(--ha-border-radius-square);
}
/* 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;
}
}
`,
];
}
declare global {

View File

@@ -0,0 +1,19 @@
import { css } from "lit";
export const tileCardStyle = css`
ha-card:has(ha-tile-container[focused]) {
--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 {
height: 100%;
transition:
box-shadow 180ms ease-in-out,
border-color 180ms ease-in-out;
}
ha-tile-icon {
--tile-icon-color: var(--tile-color);
}
`;