Custom layout and refactor (#146)

* Fixes #145

* custom layout support

* Some css fix

* Documentation update

* Doc update

* Update TOC

* Localization support
This commit is contained in:
Jérôme W 2019-05-09 16:13:06 +02:00 committed by GitHub
parent 880e7e37e1
commit c0f50ff675
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 383 additions and 159 deletions

123
README.md
View File

@ -1,4 +1,4 @@
# Button Card
# Button Card <!-- omit in toc -->
[![GitHub Release][releases-shield]][releases]
[![License][license-shield]](LICENSE.md)
@ -13,28 +13,58 @@ Lovelace Button card for your entities.
![all](examples/all.gif)
## TOC <!-- omit in toc -->
- [Features](#features)
- [Configuration](#configuration)
- [Main Options](#main-options)
- [Action](#action)
- [State](#state)
- [Available operators](#available-operators)
- [Layout](#layout)
- [Templates](#templates)
- [Styles](#styles)
- [Easy styling options](#easy-styling-options)
- [ADVANCED styling options](#advanced-styling-options)
- [Installation](#installation)
- [Manual Installation](#manual-installation)
- [Installation and tracking with `custom_updater`](#installation-and-tracking-with-custom_updater)
- [Examples](#examples)
- [Configuration with states](#configuration-with-states)
- [Default behavior](#default-behavior)
- [With Operator on state](#with-operator-on-state)
- [`tap_action` Navigate](#tap_action-navigate)
- [blink](#blink)
- [Play with width, height and icon size](#play-with-width-height-and-icon-size)
- [Templates Support](#templates-support)
- [Playing with label templates](#playing-with-label-templates)
- [State Templates](#state-templates)
- [Styling](#styling)
- [Lock](#lock)
- [Credits](#credits)
## Features
- works with any toggleable entity
- 6 available actions on **tap** and/or **hold**: `none`, `toggle`, `more-info`, `navigate`, `url` and `call-service`
- state display (optional)
- custom color (optional), or based on light rgb value
- custom color (optional), or based on light rgb value/temperature
- custom state definition with customizable color, icon and style (optional)
- [custom size of the icon, width and height](#Play-with-width-height-and-icon-size) (optional)
- Support for [templates](#templates) in some fields
- custom icon (optional)
- custom css style (optional)
- multiple [layout](#Layout) support
- units can be redefined or hidden
- multiple [layout](#Layout) support and [custom layout](#advanced-styling-options) support
- units for sensors can be redefined or hidden
- 2 color types
- `icon` : apply color settings to the icon only
- `card` : apply color settings to the card only
- automatic font color if color_type is set to `card`
- support unit of measurement
- blank card and label card (for organization)
- [blink](#blink) animation support
- rotating animation support
- confirmation popup for sensitive items (optional)
- confirmation popup for sensitive items (optional) or [locking mecanism](#lock)
- haptic support for the [Beta IOS App](http://home-assistant.io/ios/beta)
- support for [custom_updater](https://github.com/custom-components/custom_updater)
@ -170,6 +200,8 @@ See [here](#templates-support) for some examples.
### Styles
#### Easy styling options
For each element in the card, styles can be defined in 2 places:
* in the main part of the config
* in each state
@ -222,6 +254,85 @@ This will render:
See [styling](#styling) for a complete example.
#### ADVANCED styling options
For advanced styling, there are 2 other options in the `styles` config object:
* `grid`: mainly layout for the grid
* `img_cell`: mainly how you position your icon in it's cell
This is how the button is constructed (HTML elements):
![elements in the button](examples/button-card-elements.png)
The `grid` element uses CSS grids to design the layout of the card:
* `img_cell` element is going to the `grid-area: i` by default
* `name` element is going to the `grid-area: n` by default
* `state` element is going to the `grid-area: s` by default
* `label` element is going to the `grid-area: l` by default
You can see how the default layouts are constructed [here](./src/styles.ts#L152) and inspire yourself with it. We'll not support advanced layout questions here, please use [home-assitant's community forum][forum] for that.
To learn more, please use Google and this [excellent guide about CSS Grids](https://css-tricks.com/snippets/css/complete-guide-grid/) :)
Some examples:
* label on top:
```yaml
styles:
grid:
- grid-template-areas: '"l" "i" "n" "s"'
- grid-template-rows: min-content 1fr min-content min-content
- grid-template-columns: 1fr
```
* icon on the right side (by overloading an existing layout):
```yaml
- type: "custom:button-card"
entity: sensor.sensor1
layout: icon_state_name2nd
show_state: true
show_name: true
show_label: true
label: label
styles:
grid:
- grid-template-areas: '"n i" "s i" "l i"'
- grid-template-columns: 1fr 40%
```
* Apple Homekit-like buttons:
![apple-like-buttons](examples/apple_style.gif)
```yaml
- type: custom:button-card
entity: switch.skylight
name: <3 Apple
icon: mdi:fire
show_state: true
styles:
card:
- width: 100px
- height: 100px
grid:
- grid-template-areas: '"i" "n" "s"'
- grid-template-columns: 1fr
- grid-template-rows: 1fr min-content min-content
img_cell:
- align-self: start
- text-align: start
name:
- justify-self: start
- padding-left: 10px
- font-weight: bold
- text-transform: lowercase
state:
- justify-self: start
- padding-left: 10px
state:
- value: 'off'
styles:
card:
- filter: opacity(50%)
icon:
- filter: grayscale(100%)
```
## Installation
### Manual Installation

196
dist/button-card.js vendored
View File

@ -3699,6 +3699,9 @@ const styles = css`
overflow: hidden;
box-sizing: border-box;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
ha-card.disabled {
pointer-events: none;
@ -3718,7 +3721,7 @@ const styles = css`
letter-spacing: normal;
width: 100%;
}
div {
.ellipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
@ -3792,34 +3795,36 @@ const styles = css`
animation: rotating 2s linear infinite;
}
.container {
#container {
display: grid;
max-height: 100%;
text-align: center;
height: 100%;
align-items: center;
}
.img-cell {
#img-cell {
/* display: flex; */
grid-area: i;
height: 100%;
width: 100%;
max-width: 100%;
align-self: center;
}
.icon {
ha-icon#icon, img#icon {
height: 100%;
max-width: 100%;
object-fit: scale;
object-fit: contain;
overflow: hidden;
vertical-align: middle;
}
.name {
#name {
grid-area: n;
max-width: 100%;
align-self: center;
justify-self: center;
/* margin: auto; */
}
.state {
#state {
grid-area: s;
max-width: 100%;
align-self: center;
@ -3827,211 +3832,243 @@ const styles = css`
/* margin: auto; */
}
.label {
#label {
grid-area: l;
max-width: 100%;
align-self: center;
justify-self: center;
}
.container.vertical {
#container {
width: 100%;
}
#container.vertical {
grid-template-areas: "i" "n" "s" "l";
grid-template-columns: 1fr;
grid-template-rows: 1fr min-content min-content min-content;
}
/* Vertical No Icon */
.container.vertical.no-icon {
#container.vertical.no-icon {
grid-template-areas: "n" "s" "l";
grid-template-columns: 1fr;
grid-template-rows: 1fr min-content 1fr;
}
.container.vertical.no-icon .state {
#container.vertical.no-icon #state {
align-self: center;
}
.container.vertical.no-icon .name {
#container.vertical.no-icon #name {
align-self: end;
}
.container.vertical.no-icon .label {
#container.vertical.no-icon #label {
align-self: start;
}
/* Vertical No Icon No Name */
.container.vertical.no-icon.no-name {
#container.vertical.no-icon.no-name {
grid-template-areas: "s" "l";
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
.container.vertical.no-icon.no-name .state {
#container.vertical.no-icon.no-name #state {
align-self: end;
}
.container.vertical.no-icon.no-name .label {
#container.vertical.no-icon.no-name #label {
align-self: start;
}
/* Vertical No Icon No State */
.container.vertical.no-icon.no-state {
#container.vertical.no-icon.no-state {
grid-template-areas: "n" "l";
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
.container.vertical.no-icon.no-state .name {
#container.vertical.no-icon.no-state #name {
align-self: end;
}
.container.vertical.no-icon.no-state .label {
#container.vertical.no-icon.no-state #label {
align-self: start;
}
/* Vertical No Icon No Label */
.container.vertical.no-icon.no-label {
#container.vertical.no-icon.no-label {
grid-template-areas: "n" "s";
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
.container.vertical.no-icon.no-label .name {
#container.vertical.no-icon.no-label #name {
align-self: end;
}
.container.vertical.no-icon.no-label .state {
#container.vertical.no-icon.no-label #state {
align-self: start;
}
/* Vertical No Icon No Label No Name */
.container.vertical.no-icon.no-label.no-name {
#container.vertical.no-icon.no-label.no-name {
grid-template-areas: "s";
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.container.vertical.no-icon.no-label.no-name .state {
#container.vertical.no-icon.no-label.no-name #state {
align-self: center;
}
/* Vertical No Icon No Label No State */
.container.vertical.no-icon.no-label.no-state {
#container.vertical.no-icon.no-label.no-state {
grid-template-areas: "n";
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.container.vertical.no-icon.no-label.no-state .name {
#container.vertical.no-icon.no-label.no-state #name {
align-self: center;
}
/* Vertical No Icon No Name No State */
.container.vertical.no-icon.no-name.no-state {
#container.vertical.no-icon.no-name.no-state {
grid-template-areas: "l";
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.container.vertical.no-icon.no-name.no-state .label {
#container.vertical.no-icon.no-name.no-state #label {
align-self: center;
}
.container.icon_name_state {
#container.icon_name_state {
grid-template-areas: "i n" "l l";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr min-content;
}
.container.icon_name {
#container.icon_name {
grid-template-areas: "i n" "s s" "l l";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr min-content min-content;
}
.container.icon_state {
#container.icon_state {
grid-template-areas: "i s" "n n" "l l";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr min-content min-content;
}
.container.name_state {
#container.name_state {
grid-template-areas: "i" "n" "l";
grid-template-columns: 1fr;
grid-template-rows: 1fr min-content min-content;
}
.container.name_state.no-icon {
#container.name_state.no-icon {
grid-template-areas: "n" "l";
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
.container.name_state.no-icon .name {
#container.name_state.no-icon #name {
align-self: end
}
.container.name_state.no-icon .label {
#container.name_state.no-icon #label {
align-self: start
}
.container.name_state.no-icon.no-label {
#container.name_state.no-icon.no-label {
grid-template-areas: "n";
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.container.name_state.no-icon.no-label .name {
#container.name_state.no-icon.no-label #name {
align-self: center
}
/* icon_name_state2nd default */
.container.icon_name_state2nd {
#container.icon_name_state2nd {
grid-template-areas: "i n" "i s" "i l";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr min-content 1fr;
}
.container.icon_name_state2nd .name {
#container.icon_name_state2nd #name {
align-self: end;
}
.container.icon_name_state2nd .state {
#container.icon_name_state2nd #state {
align-self: center;
}
.container.icon_name_state2nd .label {
#container.icon_name_state2nd #label {
align-self: start;
}
/* icon_name_state2nd No Label */
.container.icon_name_state2nd.no-label {
#container.icon_name_state2nd.no-label {
grid-template-areas: "i n" "i s";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr 1fr;
}
.container.icon_name_state2nd .name {
#container.icon_name_state2nd #name {
align-self: end;
}
.container.icon_name_state2nd .state {
#container.icon_name_state2nd #state {
align-self: start;
}
/* icon_state_name2nd Default */
.container.icon_state_name2nd {
#container.icon_state_name2nd {
grid-template-areas: "i s" "i n" "i l";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr min-content 1fr;
}
.container.icon_state_name2nd .state {
#container.icon_state_name2nd #state {
align-self: end;
}
.container.icon_state_name2nd .name {
#container.icon_state_name2nd #name {
align-self: center;
}
.container.icon_state_name2nd .state {
#container.icon_state_name2nd #state {
align-self: start;
}
/* icon_state_name2nd No Label */
.container.icon_state_name2nd.no-label {
#container.icon_state_name2nd.no-label {
grid-template-areas: "i s" "i n";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr 1fr;
}
.container.icon_state_name2nd .state {
#container.icon_state_name2nd #state {
align-self: end;
}
.container.icon_state_name2nd .name {
#container.icon_state_name2nd #name {
align-self: start;
}
.container.icon_label {
#container.icon_label {
grid-template-areas: "i l" "n n" "s s";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr min-content min-content;
}
`;
var computeStateDisplay = (localize, stateObj) => {
let display;
const domain = computeDomain(stateObj.entity_id);
if (domain === 'binary_sensor') {
// Try device class translation, then default binary sensor translation
if (stateObj.attributes.device_class) {
display = localize(`state.${domain}.${stateObj.attributes.device_class}.${stateObj.state}`);
}
if (!display) {
display = localize(`state.${domain}.default.${stateObj.state}`);
}
} else if (stateObj.attributes.unit_of_measurement && !['unknown', 'unavailable'].includes(stateObj.state)) {
display = stateObj.state;
} else if (domain === 'zwave') {
if (['initializing', 'dead'].includes(stateObj.state)) {
display = localize(`state.zwave.query_stage.${stateObj.state}`, 'query_stage', stateObj.attributes.query_stage);
} else {
display = localize(`state.zwave.default.${stateObj.state}`);
}
} else {
display = localize(`state.${domain}.${stateObj.state}`);
}
// Fall back to default, component backend translation, or raw state if nothing else matches.
if (!display) {
display = localize(`state.default.${stateObj.state}`) || localize(`component.${domain}.state.${stateObj.state}`) || stateObj.state;
}
return display;
};
let ButtonCard = class ButtonCard extends LitElement {
static get styles() {
return styles;
@ -4049,7 +4086,11 @@ let ButtonCard = class ButtonCard extends LitElement {
return hasConfigOrEntityChanged(this, changedProps, forceUpdate);
}
_getMatchingConfigState(state) {
if (!state || !this.config.state) {
if (!this.config.state) {
return undefined;
}
const hasTemplate = this.config.state.find(elt => elt.operator === 'template');
if (!state && !hasTemplate) {
return undefined;
}
let def;
@ -4058,21 +4099,21 @@ let ButtonCard = class ButtonCard extends LitElement {
switch (elt.operator) {
case '==':
/* eslint eqeqeq: 0 */
return state.state == elt.value;
return state && state.state == elt.value;
case '<=':
return state.state <= elt.value;
return state && state.state <= elt.value;
case '<':
return state.state < elt.value;
return state && state.state < elt.value;
case '>=':
return state.state >= elt.value;
return state && state.state >= elt.value;
case '>':
return state.state > elt.value;
return state && state.state > elt.value;
case '!=':
return state.state != elt.value;
return state && state.state != elt.value;
case 'regex':
{
/* eslint no-unneeded-ternary: 0 */
const matches = state.state.match(elt.value) ? true : false;
const matches = state && state.state.match(elt.value) ? true : false;
return matches;
}
case 'template':
@ -4086,7 +4127,7 @@ let ButtonCard = class ButtonCard extends LitElement {
return false;
}
} else {
return elt.value == state.state;
return state && elt.value == state.state;
}
});
if (!retval && def) {
@ -4200,11 +4241,12 @@ let ButtonCard = class ButtonCard extends LitElement {
_buildStateString(state) {
let stateString;
if (this.config.show_state && state && state.state) {
const localizedState = computeStateDisplay(this.hass.localize, state);
const units = this._buildUnits(state);
if (units) {
stateString = `${state.state} ${units}`;
} else {
stateString = state.state;
stateString = localizedState;
}
}
return stateString;
@ -4223,7 +4265,7 @@ let ButtonCard = class ButtonCard extends LitElement {
return units;
}
_buildLastChanged(state, style) {
return state ? html`<ha-relative-time .hass="${this.hass}" .datetime="${state.last_changed}" class="label" style=${styleMap(style)}></ha-relative-time>` : html``;
return this.config.show_last_changed && state ? html`<ha-relative-time id="label" class="ellipsis" .hass="${this.hass}" .datetime="${state.last_changed}" style=${styleMap(style)}></ha-relative-time>` : undefined;
}
_buildLabel(state, configState) {
if (!this.config.show_label) {
@ -4316,7 +4358,7 @@ let ButtonCard = class ButtonCard extends LitElement {
<ha-card class="button-card-main ${this._isClickable(state) ? '' : 'disabled'}" style=${styleMap(cardStyle)} @ha-click="${this._handleTap}" @ha-hold="${this._handleHold}" .longpress="${longPress()}" .config="${this.config}">
${this._getLock(lockStyle)}
${this._buttonContent(state, configState, buttonColor)}
${this.config.lock ? '' : html`<paper-ripple id="ripple"></paper-ripple>`}
${this.config.lock ? '' : html`<mwc-ripple id="ripple"></mwc-ripple>`}
</ha-card>
`;
}
@ -4344,23 +4386,24 @@ let ButtonCard = class ButtonCard extends LitElement {
}
_gridHtml(state, configState, containerClass, color, name, stateString) {
const iconTemplate = this._getIconHtml(state, configState, color);
const itemClass = ['container', containerClass];
const itemClass = [containerClass];
const label = this._buildLabel(state, configState);
const nameStyleFromConfig = this._buildStyleGeneric(configState, 'name');
const stateStyleFromConfig = this._buildStyleGeneric(configState, 'state');
const labelStyleFromConfig = this._buildStyleGeneric(configState, 'label');
const lastChangedTemplate = this._buildLastChanged(state, labelStyleFromConfig);
const gridStyleFromConfig = this._buildStyleGeneric(configState, 'grid');
if (!iconTemplate) itemClass.push('no-icon');
if (!name) itemClass.push('no-name');
if (!stateString) itemClass.push('no-state');
if (!label) itemClass.push('no-label');
if (!label && !lastChangedTemplate) itemClass.push('no-label');
return html`
<div class=${itemClass.join(' ')}>
<div id="container" class=${itemClass.join(' ')} style=${styleMap(gridStyleFromConfig)}>
${iconTemplate ? iconTemplate : ''}
${name ? html`<div class="name" style=${styleMap(nameStyleFromConfig)}>${name}</div>` : ''}
${stateString ? html`<div class="state" style=${styleMap(stateStyleFromConfig)}>${stateString}</div>` : ''}
${label && !this.config.show_last_changed ? html`<div class="label" style=${styleMap(labelStyleFromConfig)}>${unsafeHTML(label)}</div>` : ''}
${this.config.show_last_changed ? lastChangedTemplate : ''}
${name ? html`<div id="name" class="ellipsis" style=${styleMap(nameStyleFromConfig)}>${name}</div>` : ''}
${stateString ? html`<div id="state" class="ellipsis" style=${styleMap(stateStyleFromConfig)}>${stateString}</div>` : ''}
${label && !lastChangedTemplate ? html`<div id="label" class="ellipsis" style=${styleMap(labelStyleFromConfig)}>${unsafeHTML(label)}</div>` : ''}
${lastChangedTemplate ? lastChangedTemplate : ''}
</div>
`;
}
@ -4369,15 +4412,16 @@ let ButtonCard = class ButtonCard extends LitElement {
const entityPicture = this._buildEntityPicture(state, configState);
const entityPictureStyleFromConfig = this._buildStyleGeneric(configState, 'entity_picture');
const haIconStyleFromConfig = this._buildStyleGeneric(configState, 'icon');
const haIconStyle = Object.assign({ color, width: this.config.size, 'min-width': this.config.size }, haIconStyleFromConfig);
const imgCellStyleFromConfig = this._buildStyleGeneric(configState, 'img_cell');
const haIconStyle = Object.assign({ color, width: this.config.size }, haIconStyleFromConfig);
const entityPictureStyle = Object.assign({}, haIconStyle, entityPictureStyleFromConfig);
if (icon || entityPicture) {
return html`
<div class="img-cell">
<div id="img-cell" style=${styleMap(imgCellStyleFromConfig)}>
${icon && !entityPicture ? html`<ha-icon style=${styleMap(haIconStyle)}
.icon="${icon}" class="icon" ?rotating=${this._rotate(configState)}></ha-icon>` : ''}
.icon="${icon}" id="icon" ?rotating=${this._rotate(configState)}></ha-icon>` : ''}
${entityPicture ? html`<img src="${entityPicture}" style=${styleMap(entityPictureStyle)}
class="icon" ?rotating=${this._rotate(configState)} />` : ''}
id="icon" ?rotating=${this._rotate(configState)} />` : ''}
</div>
`;
} else {

BIN
examples/apple_style.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -31,6 +31,7 @@ import {
import { handleClick } from './handle-click';
import { longPress } from './long-press';
import { styles } from './styles';
import computeStateDisplay from './compute_state_display';
@customElement('button-card')
class ButtonCard extends LitElement {
@ -64,7 +65,13 @@ class ButtonCard extends LitElement {
}
private _getMatchingConfigState(state: HassEntity | undefined): StateConfig | undefined {
if (!state || !this.config!.state) {
if (!this.config!.state) {
return undefined;
}
const hasTemplate = this.config!.state.find(
elt => elt.operator === 'template',
);
if (!state && !hasTemplate) {
return undefined;
}
let def: StateConfig | undefined;
@ -73,20 +80,20 @@ class ButtonCard extends LitElement {
switch (elt.operator) {
case '==':
/* eslint eqeqeq: 0 */
return (state!.state == elt.value);
return (state && state.state == elt.value);
case '<=':
return (state!.state <= elt.value);
return (state && state.state <= elt.value);
case '<':
return (state!.state < elt.value);
return (state && state.state < elt.value);
case '>=':
return (state!.state >= elt.value);
return (state && state.state >= elt.value);
case '>':
return (state.state > elt.value);
return (state && state.state > elt.value);
case '!=':
return (state.state != elt.value);
return (state && state.state != elt.value);
case 'regex': {
/* eslint no-unneeded-ternary: 0 */
const matches = state.state.match(elt.value) ? true : false;
const matches = state && state.state.match(elt.value) ? true : false;
return matches;
}
case 'template': {
@ -101,7 +108,7 @@ class ButtonCard extends LitElement {
return false;
}
} else {
return (elt.value == state.state);
return state && (elt.value == state.state);
}
});
if (!retval && def) {
@ -249,11 +256,12 @@ class ButtonCard extends LitElement {
private _buildStateString(state: HassEntity | undefined): string | undefined {
let stateString: string | undefined;
if (this.config!.show_state && state && state.state) {
const localizedState = computeStateDisplay(this.hass!.localize, state);
const units = this._buildUnits(state);
if (units) {
stateString = `${state.state} ${units}`;
} else {
stateString = state.state;
stateString = localizedState;
}
}
return stateString;
@ -276,8 +284,8 @@ class ButtonCard extends LitElement {
private _buildLastChanged(
state: HassEntity | undefined,
style: StyleInfo,
): TemplateResult {
return state ? html`<ha-relative-time .hass="${this.hass}" .datetime="${state.last_changed}" class="label" style=${styleMap(style)}></ha-relative-time>` : html``;
): TemplateResult | undefined {
return this.config!.show_last_changed && state ? html`<ha-relative-time id="label" class="ellipsis" .hass="${this.hass}" .datetime="${state.last_changed}" style=${styleMap(style)}></ha-relative-time>` : undefined;
}
private _buildLabel(
@ -391,7 +399,7 @@ class ButtonCard extends LitElement {
<ha-card class="button-card-main ${this._isClickable(state) ? '' : 'disabled'}" style=${styleMap(cardStyle)} @ha-click="${this._handleTap}" @ha-hold="${this._handleHold}" .longpress="${longPress()}" .config="${this.config}">
${this._getLock(lockStyle)}
${this._buttonContent(state, configState, buttonColor)}
${this.config!.lock ? '' : html`<paper-ripple id="ripple"></paper-ripple>`}
${this.config!.lock ? '' : html`<mwc-ripple id="ripple"></mwc-ripple>`}
</ha-card>
`;
}
@ -436,24 +444,25 @@ class ButtonCard extends LitElement {
stateString: string | undefined,
): TemplateResult {
const iconTemplate = this._getIconHtml(state, configState, color);
const itemClass: string[] = ['container', containerClass];
const itemClass: string[] = [containerClass];
const label = this._buildLabel(state, configState);
const nameStyleFromConfig = this._buildStyleGeneric(configState, 'name');
const stateStyleFromConfig = this._buildStyleGeneric(configState, 'state');
const labelStyleFromConfig = this._buildStyleGeneric(configState, 'label');
const lastChangedTemplate = this._buildLastChanged(state, labelStyleFromConfig);
const gridStyleFromConfig = this._buildStyleGeneric(configState, 'grid');
if (!iconTemplate) itemClass.push('no-icon');
if (!name) itemClass.push('no-name');
if (!stateString) itemClass.push('no-state');
if (!label) itemClass.push('no-label');
if (!label && !lastChangedTemplate) itemClass.push('no-label');
return html`
<div class=${itemClass.join(' ')}>
<div id="container" class=${itemClass.join(' ')} style=${styleMap(gridStyleFromConfig)}>
${iconTemplate ? iconTemplate : ''}
${name ? html`<div class="name" style=${styleMap(nameStyleFromConfig)}>${name}</div>` : ''}
${stateString ? html`<div class="state" style=${styleMap(stateStyleFromConfig)}>${stateString}</div>` : ''}
${label && !this.config!.show_last_changed ? html`<div class="label" style=${styleMap(labelStyleFromConfig)}>${unsafeHTML(label)}</div>` : ''}
${this.config!.show_last_changed ? lastChangedTemplate : ''}
${name ? html`<div id="name" class="ellipsis" style=${styleMap(nameStyleFromConfig)}>${name}</div>` : ''}
${stateString ? html`<div id="state" class="ellipsis" style=${styleMap(stateStyleFromConfig)}>${stateString}</div>` : ''}
${label && !lastChangedTemplate ? html`<div id="label" class="ellipsis" style=${styleMap(labelStyleFromConfig)}>${unsafeHTML(label)}</div>` : ''}
${lastChangedTemplate ? lastChangedTemplate : ''}
</div>
`;
}
@ -467,11 +476,11 @@ class ButtonCard extends LitElement {
const entityPicture = this._buildEntityPicture(state, configState);
const entityPictureStyleFromConfig = this._buildStyleGeneric(configState, 'entity_picture');
const haIconStyleFromConfig = this._buildStyleGeneric(configState, 'icon');
const imgCellStyleFromConfig = this._buildStyleGeneric(configState, 'img_cell');
const haIconStyle = {
color,
width: this.config!.size,
'min-width': this.config!.size,
...haIconStyleFromConfig,
};
const entityPictureStyle = {
@ -481,11 +490,11 @@ class ButtonCard extends LitElement {
if (icon || entityPicture) {
return html`
<div class="img-cell">
<div id="img-cell" style=${styleMap(imgCellStyleFromConfig)}>
${icon && !entityPicture ? html`<ha-icon style=${styleMap(haIconStyle)}
.icon="${icon}" class="icon" ?rotating=${this._rotate(configState)}></ha-icon>` : ''}
.icon="${icon}" id="icon" ?rotating=${this._rotate(configState)}></ha-icon>` : ''}
${entityPicture ? html`<img src="${entityPicture}" style=${styleMap(entityPictureStyle)}
class="icon" ?rotating=${this._rotate(configState)} />` : ''}
id="icon" ?rotating=${this._rotate(configState)} />` : ''}
</div>
`;
} else {

View File

@ -0,0 +1,50 @@
import { HassEntity } from 'home-assistant-js-websocket';
import { computeDomain } from './helpers';
import { LocalizeFunc } from './types';
export default (
localize: LocalizeFunc,
stateObj: HassEntity,
): string => {
let display: string | undefined;
const domain = computeDomain(stateObj.entity_id);
if (domain === 'binary_sensor') {
// Try device class translation, then default binary sensor translation
if (stateObj.attributes.device_class) {
display = localize(
`state.${domain}.${stateObj.attributes.device_class}.${stateObj.state}`,
);
}
if (!display) {
display = localize(`state.${domain}.default.${stateObj.state}`);
}
} else if (
stateObj.attributes.unit_of_measurement
&& !['unknown', 'unavailable'].includes(stateObj.state)
) {
display = stateObj.state;
} else if (domain === 'zwave') {
if (['initializing', 'dead'].includes(stateObj.state)) {
display = localize(
`state.zwave.query_stage.${stateObj.state}`,
'query_stage',
stateObj.attributes.query_stage,
);
} else {
display = localize(`state.zwave.default.${stateObj.state}`);
}
} else {
display = localize(`state.${domain}.${stateObj.state}`);
}
// Fall back to default, component backend translation, or raw state if nothing else matches.
if (!display) {
display = localize(`state.default.${stateObj.state}`)
|| localize(`component.${domain}.state.${stateObj.state}`)
|| stateObj.state;
}
return display;
};

View File

@ -6,6 +6,9 @@ export const styles = css`
overflow: hidden;
box-sizing: border-box;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
ha-card.disabled {
pointer-events: none;
@ -25,7 +28,7 @@ export const styles = css`
letter-spacing: normal;
width: 100%;
}
div {
.ellipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
@ -99,34 +102,36 @@ export const styles = css`
animation: rotating 2s linear infinite;
}
.container {
#container {
display: grid;
max-height: 100%;
text-align: center;
height: 100%;
align-items: center;
}
.img-cell {
#img-cell {
/* display: flex; */
grid-area: i;
height: 100%;
width: 100%;
max-width: 100%;
align-self: center;
}
.icon {
ha-icon#icon, img#icon {
height: 100%;
max-width: 100%;
object-fit: scale;
object-fit: contain;
overflow: hidden;
vertical-align: middle;
}
.name {
#name {
grid-area: n;
max-width: 100%;
align-self: center;
justify-self: center;
/* margin: auto; */
}
.state {
#state {
grid-area: s;
max-width: 100%;
align-self: center;
@ -134,205 +139,208 @@ export const styles = css`
/* margin: auto; */
}
.label {
#label {
grid-area: l;
max-width: 100%;
align-self: center;
justify-self: center;
}
.container.vertical {
#container {
width: 100%;
}
#container.vertical {
grid-template-areas: "i" "n" "s" "l";
grid-template-columns: 1fr;
grid-template-rows: 1fr min-content min-content min-content;
}
/* Vertical No Icon */
.container.vertical.no-icon {
#container.vertical.no-icon {
grid-template-areas: "n" "s" "l";
grid-template-columns: 1fr;
grid-template-rows: 1fr min-content 1fr;
}
.container.vertical.no-icon .state {
#container.vertical.no-icon #state {
align-self: center;
}
.container.vertical.no-icon .name {
#container.vertical.no-icon #name {
align-self: end;
}
.container.vertical.no-icon .label {
#container.vertical.no-icon #label {
align-self: start;
}
/* Vertical No Icon No Name */
.container.vertical.no-icon.no-name {
#container.vertical.no-icon.no-name {
grid-template-areas: "s" "l";
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
.container.vertical.no-icon.no-name .state {
#container.vertical.no-icon.no-name #state {
align-self: end;
}
.container.vertical.no-icon.no-name .label {
#container.vertical.no-icon.no-name #label {
align-self: start;
}
/* Vertical No Icon No State */
.container.vertical.no-icon.no-state {
#container.vertical.no-icon.no-state {
grid-template-areas: "n" "l";
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
.container.vertical.no-icon.no-state .name {
#container.vertical.no-icon.no-state #name {
align-self: end;
}
.container.vertical.no-icon.no-state .label {
#container.vertical.no-icon.no-state #label {
align-self: start;
}
/* Vertical No Icon No Label */
.container.vertical.no-icon.no-label {
#container.vertical.no-icon.no-label {
grid-template-areas: "n" "s";
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
.container.vertical.no-icon.no-label .name {
#container.vertical.no-icon.no-label #name {
align-self: end;
}
.container.vertical.no-icon.no-label .state {
#container.vertical.no-icon.no-label #state {
align-self: start;
}
/* Vertical No Icon No Label No Name */
.container.vertical.no-icon.no-label.no-name {
#container.vertical.no-icon.no-label.no-name {
grid-template-areas: "s";
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.container.vertical.no-icon.no-label.no-name .state {
#container.vertical.no-icon.no-label.no-name #state {
align-self: center;
}
/* Vertical No Icon No Label No State */
.container.vertical.no-icon.no-label.no-state {
#container.vertical.no-icon.no-label.no-state {
grid-template-areas: "n";
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.container.vertical.no-icon.no-label.no-state .name {
#container.vertical.no-icon.no-label.no-state #name {
align-self: center;
}
/* Vertical No Icon No Name No State */
.container.vertical.no-icon.no-name.no-state {
#container.vertical.no-icon.no-name.no-state {
grid-template-areas: "l";
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.container.vertical.no-icon.no-name.no-state .label {
#container.vertical.no-icon.no-name.no-state #label {
align-self: center;
}
.container.icon_name_state {
#container.icon_name_state {
grid-template-areas: "i n" "l l";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr min-content;
}
.container.icon_name {
#container.icon_name {
grid-template-areas: "i n" "s s" "l l";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr min-content min-content;
}
.container.icon_state {
#container.icon_state {
grid-template-areas: "i s" "n n" "l l";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr min-content min-content;
}
.container.name_state {
#container.name_state {
grid-template-areas: "i" "n" "l";
grid-template-columns: 1fr;
grid-template-rows: 1fr min-content min-content;
}
.container.name_state.no-icon {
#container.name_state.no-icon {
grid-template-areas: "n" "l";
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
.container.name_state.no-icon .name {
#container.name_state.no-icon #name {
align-self: end
}
.container.name_state.no-icon .label {
#container.name_state.no-icon #label {
align-self: start
}
.container.name_state.no-icon.no-label {
#container.name_state.no-icon.no-label {
grid-template-areas: "n";
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.container.name_state.no-icon.no-label .name {
#container.name_state.no-icon.no-label #name {
align-self: center
}
/* icon_name_state2nd default */
.container.icon_name_state2nd {
#container.icon_name_state2nd {
grid-template-areas: "i n" "i s" "i l";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr min-content 1fr;
}
.container.icon_name_state2nd .name {
#container.icon_name_state2nd #name {
align-self: end;
}
.container.icon_name_state2nd .state {
#container.icon_name_state2nd #state {
align-self: center;
}
.container.icon_name_state2nd .label {
#container.icon_name_state2nd #label {
align-self: start;
}
/* icon_name_state2nd No Label */
.container.icon_name_state2nd.no-label {
#container.icon_name_state2nd.no-label {
grid-template-areas: "i n" "i s";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr 1fr;
}
.container.icon_name_state2nd .name {
#container.icon_name_state2nd #name {
align-self: end;
}
.container.icon_name_state2nd .state {
#container.icon_name_state2nd #state {
align-self: start;
}
/* icon_state_name2nd Default */
.container.icon_state_name2nd {
#container.icon_state_name2nd {
grid-template-areas: "i s" "i n" "i l";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr min-content 1fr;
}
.container.icon_state_name2nd .state {
#container.icon_state_name2nd #state {
align-self: end;
}
.container.icon_state_name2nd .name {
#container.icon_state_name2nd #name {
align-self: center;
}
.container.icon_state_name2nd .state {
#container.icon_state_name2nd #state {
align-self: start;
}
/* icon_state_name2nd No Label */
.container.icon_state_name2nd.no-label {
#container.icon_state_name2nd.no-label {
grid-template-areas: "i s" "i n";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr 1fr;
}
.container.icon_state_name2nd .state {
#container.icon_state_name2nd #state {
align-self: end;
}
.container.icon_state_name2nd .name {
#container.icon_state_name2nd #name {
align-self: start;
}
.container.icon_label {
#container.icon_label {
grid-template-areas: "i l" "n n" "s s";
grid-template-columns: 40% 1fr;
grid-template-rows: 1fr min-content min-content;

View File

@ -73,6 +73,8 @@ export interface StylesConfig {
name?: CssStyleConfig[];
state?: CssStyleConfig[];
label?: CssStyleConfig[];
grid?: CssStyleConfig[];
img_cell?: CssStyleConfig[];
}
export interface CssStyleConfig {