From 56a8fac0fa927dd0de0778971a03ea164ffd2ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20W?= Date: Mon, 29 Apr 2019 18:44:04 +0200 Subject: [PATCH] Label and templates support (#134) * Label and templates support * Fix blank card with fixed width * Fix some layouts --- README.md | 97 +++++++++++++++++-- dist/button-card.js | 222 +++++++++++++++++++++++++++++++++++++------- examples/labels.png | Bin 0 -> 8022 bytes src/button-card.ts | 68 ++++++++++++-- src/helpers.ts | 3 +- src/styles.ts | 166 ++++++++++++++++++++++++++++----- src/types.ts | 18 +++- 7 files changed, 504 insertions(+), 70 deletions(-) create mode 100644 examples/labels.png diff --git a/README.md b/README.md index d14ec97..e295138 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Lovelace Button card for your entities. - custom color (optional), or based on light rgb value - 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 @@ -52,10 +53,13 @@ Lovelace Button card for your entities. | `tap_action` | object | optional | See [Action](#Action) | Define the type of action on click, if undefined, toggle will be used. | | `hold_action` | object | optional | See [Action](#Action) | Define the type of action on hold, if undefined, nothing happens. | | `name` | string | optional | `Air conditioner` | Define an optional text to show below the icon | +| `label` | string | optional | Any string that you want | Display a label below the card. See [Layouts](#layout) for more information. | +| `label_template` | string | optional | `states['light.mylight'].attributes.brightness` | See [templates](#templates). Any javascript code which returns a string. Overrides `label` | | `show_name` | boolean | `true` | `true` \| `false` | Wether to show the name or not. Will pick entity_id's name by default, unless redefined in the `name` property or in any state `name` property | | `show_state` | boolean | `false` | `true` \| `false` | Show the state on the card. defaults to false if not set | | `show_icon` | boolean | `true` | `true` \| `false` | Wether to show the icon or not. Unless redefined in `icon`, uses the default entity icon from hass | | `show_units` | boolean | `true` | `true` \| `false` | Display or hide the units of a sensor, if any. | +| `show_label` | boolean | `false` | `true` \| `false` | Display or hide the `label`/`label_template` | `show_entity_picture` | boolean | `false` | `true` \| `false` | Replace the icon by the entity picture (if any) or the custom picture (if any). Falls back to using the icon if both are undefined | | `entity_picture` | string | optional | Can be any of `/local/*` file or a URL | Will override the icon/the default entity_picture with your own image. Best is to use a square image. You can also define one per state | | `units` | string | optional | `Kb/s`, `lux`, ... | Override or define the units to display after the state of the entity. If omitted, it's using the entity's units | @@ -89,9 +93,13 @@ Lovelace Button card for your entities. | `spin` | boolean | `false` | `true` \| `false` | Should the icon spin for this state? | | `entity_picture` | string | optional | Can be any of `/local/*` file or a URL | Will override the icon/the default entity_picture with your own image for this state. Best is to use a square image | | `entity_picture_style` | object list | optional | `- border-radius: 50%`, `- filter: grayscale(100%)`, ... | Style applied to the entity picture for this state | +| `label` | string | optional | Any string that you want | Display a label below the card. See [Layouts](#layout) for more information. | +| `label_template` | string | optional | `states['light.mylight'].attributes.brightness` | See [templates](#templates). Any javascript code which returns a string. Overrides `label` | ### Available operators +The order of your elements in the `state` object matters. The first one which is `true` will match. + | Operator | `value` example | Description | | :-------: | --------------- | -------------------------------------------------------------------------------------------------------- | | `<` | `5` | Current state is inferior to `value` | @@ -101,6 +109,7 @@ Lovelace Button card for your entities. | `>` | `12` | Current state is superior to `value` | | `!=` | `'normal'` | Current state is not equal (`!=` javascript) to `value` | | `regex` | `'^norm.*$'` | `value` regex applied to current state does match | +| `template` | `return states['input_select.light_mode'].state === 'night_mode'` | See [here](#state-templates) for examples. `value` needs to be a javascript expression which returns a boolean. If the boolean is true, it will match this state | | `default` | N/A | If nothing matches, this is used | ### Layout @@ -111,15 +120,29 @@ It is fully compatible with every `show_*` option. Make sure you set `show_state Multiple values are possible, see the image below for examples: * `vertical` (default value if nothing is provided): Everything is centered vertically on top of each other -* `icon_name_state`: Everything is aligned horizontally, name and state are concatenated -* `name_state`: Icon sits on top of name and state concatenated on one line -* `icon_name`: Icon and name are horizontally aligned, state is centered below -* `icon_state`: Icon and state are horizontally aligned, name is centered below -* `icon_name_state2nd`: Icon, name and state are horizontally aligned, name is above state -* `icon_state_name2nd`: Icon, name and state are horizontally aligned, state is above name +* `icon_name_state`: Everything is aligned horizontally, name and state are concatenated, label is centered below +* `name_state`: Icon sits on top of name and state concatenated on one line, label below +* `icon_name`: Icon and name are horizontally aligned, state and label are centered below +* `icon_state`: Icon and state are horizontally aligned, name and label are centered below +* `icon_label`: Icon and label are horizontally aligned, name and state are centered below +* `icon_name_state2nd`: Icon, name and state are horizontally aligned, name is above state, label below name and state +* `icon_state_name2nd`: Icon, name and state are horizontally aligned, state is above name, label below name and state ![layout_image](examples/layout.png) +### Templates + +`label_template` supports templating. +It will be interpreted as javascript code and the code should return a value. + +Inside the javascript code, you'll have access to those variables: +* `entity`: The current entity object, if the entity is defined in the card +* `states`: An object with all the states of all the entities (equivalent to `hass.states`) +* `user`: The user object (equivalent to `hass.user`) +* `hass`: The complete `hass` object + +See [here](#playing-with-label-templates) for some examples. + ## Installation ### Manual Installation @@ -466,6 +489,68 @@ If you specify a width for the card, it has to be in `px`. All the cards without style: - height: 300px ``` +### Templates Support + +#### Playing with label templates + +![label_template](examples/labels.png) + +```yaml +- type: "custom:button-card" + color_type: icon + entity: light.test_light + label_template: > + var bri = states['light.test_light'].attributes.brightness; + return 'Brightness: ' + (bri ? bri : '0') + '%'; + show_label: true + size: 15% + style: + - height: 100px +- type: "custom:button-card" + color_type: icon + entity: light.test_light + layout: icon_label + label_template: > + return 'Other State: ' + states['switch.skylight'].state; + show_label: true + show_name: false + style: + - height: 100px +``` + +#### State Templates + +The javascript code inside `value` needs to return `true` of `false`. + +Example with `template`: +```yaml +- type: "custom:button-card" + color_type: icon + entity: switch.skylight + show_state: true + show_label: true + state: + - operator: template + value: > + return states['light.test_light'].attributes + && (states['light.test_light'].attributes.brightness <= 100) + icon: mdi:alert + - operator: default + icon: mdi:lightbulb +- type: "custom:button-card" + color_type: icon + entity: light.test_light + show_label: true + state: + - operator: template + value: > + return states['input_select.light_mode'].state === 'night_mode' + icon: mdi:weather-night + label: Night Mode + - operator: default + icon: mdi:white-balance-sunny + label: Day Mode +``` ## Credits diff --git a/dist/button-card.js b/dist/button-card.js index 10297a8..c562dec 100644 --- a/dist/button-card.js +++ b/dist/button-card.js @@ -3336,8 +3336,8 @@ function applyBrightnessToColor(color, brightness) { return color; } // Check if config or Entity changed -function hasConfigOrEntityChanged(element, changedProps) { - if (changedProps.has('config')) { +function hasConfigOrEntityChanged(element, changedProps, forceUpdate) { + if (changedProps.has('config') || forceUpdate) { return true; } if (element.config.entity) { @@ -3711,8 +3711,9 @@ const styles = css` } .img-cell { grid-area: i; - min-height: 0; - min-width: 0; + height: 100%; + width: 100%; + max-width: 100%; } .icon { @@ -3736,64 +3737,164 @@ const styles = css` /* margin: auto; */ } - .container.vertical { - grid-template-areas: "i" "n" "s"; - grid-template-columns: 1fr; - grid-template-rows: 1fr min-content min-content; + .label { + grid-area: l; + max-width: 100%; + align-self: center; + justify-self: center; } - .container.vertical.no-icon { - grid-template-areas: "n" "s"; + + .container.vertical { + grid-template-areas: "i" "n" "s" "l"; grid-template-columns: 1fr; - grid-template-rows: 1fr 1fr; + grid-template-rows: 1fr min-content min-content min-content; + } + /* 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 { - align-self: start; + align-self: center; } .container.vertical.no-icon .name { align-self: end; } + .container.vertical.no-icon .label { + align-self: start; + } + + /* 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 { + align-self: end; + } + .container.vertical.no-icon.no-name .label { + align-self: start; + } + + /* 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 { + align-self: end; + } + .container.vertical.no-icon.no-state .label { + align-self: start; + } + + /* 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 { + align-self: end; + } + .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 { grid-template-areas: "s"; grid-template-columns: 1fr; grid-template-rows: 1fr; } - .container.vertical.no-icon.no-name .state { + .container.vertical.no-icon.no-label.no-name .state { align-self: center; } - .container.vertical.no-icon.no-state { + /* 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-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 { + grid-template-areas: "l"; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + } + .container.vertical.no-icon.no-name.no-state .label { align-self: center; } .container.icon_name_state { - grid-template-areas: "i n"; + grid-template-areas: "i n" "l l"; grid-template-columns: 40% 1fr; - grid-template-rows: 1fr; + grid-template-rows: 1fr min-content; } .container.icon_name { - grid-template-areas: "i n" "s s"; + grid-template-areas: "i n" "s s" "l l"; grid-template-columns: 40% 1fr; - grid-template-rows: 1fr min-content; + grid-template-rows: 1fr min-content min-content; } .container.icon_state { - grid-template-areas: "i s" "n n"; + grid-template-areas: "i s" "n n" "l l"; grid-template-columns: 40% 1fr; - grid-template-rows: 1fr min-content; + grid-template-rows: 1fr min-content min-content; } .container.name_state { - grid-template-areas: "i" "n"; + grid-template-areas: "i" "n" "l"; grid-template-columns: 1fr; - grid-template-rows: 1fr min-content; + grid-template-rows: 1fr min-content min-content; + } + .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 { + align-self: end + } + .container.name_state.no-icon .label { + align-self: start } + .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 { + align-self: center + } + + /* icon_name_state2nd default */ .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 { + align-self: end; + } + .container.icon_name_state2nd .state { + align-self: center; + } + .container.icon_name_state2nd .label { + align-self: start; + } + + /* 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; @@ -3805,7 +3906,24 @@ const styles = css` align-self: start; } + /* icon_state_name2nd Default */ .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 { + align-self: end; + } + .container.icon_state_name2nd .name { + align-self: center; + } + .container.icon_state_name2nd .state { + align-self: start; + } + + /* 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; @@ -3816,6 +3934,12 @@ const styles = css` .container.icon_state_name2nd .name { align-self: start; } + + .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; + } `; let ButtonCard = class ButtonCard extends LitElement { @@ -3829,7 +3953,12 @@ let ButtonCard = class ButtonCard extends LitElement { return this._cardHtml(); } shouldUpdate(changedProps) { - return hasConfigOrEntityChanged(this, changedProps); + const state = this.config.entity ? this.hass.states[this.config.entity] : undefined; + const configState = this._getMatchingConfigState(state); + const forceUpdate = this.config.show_label && (configState && configState.label_template || this.config.label_template) || this.config.state && this.config.state.find(elt => { + return elt.operator === 'template'; + }) ? true : false; + return hasConfigOrEntityChanged(this, changedProps, forceUpdate); } _getMatchingConfigState(state) { if (!state || !this.config.state) { @@ -3858,6 +3987,10 @@ let ButtonCard = class ButtonCard extends LitElement { const matches = state.state.match(elt.value) ? true : false; return matches; } + case 'template': + { + return new Function('states', 'entity', 'user', 'hass', `'use strict'; ${elt.value}`).call(this, this.hass.states, state, this.hass.user, this.hass); + } case 'default': def = elt; return false; @@ -4018,6 +4151,28 @@ let ButtonCard = class ButtonCard extends LitElement { } return units; } + _buildLabel(state, configState) { + if (!this.config.show_label) { + return undefined; + } + let label; + let matchingLabelTemplate; + if (configState && configState.label_template) { + matchingLabelTemplate = configState.label_template; + } else { + matchingLabelTemplate = this.config.label_template; + } + if (!matchingLabelTemplate) { + if (configState && configState.label) { + label = configState.label; + } else { + label = this.config.label; + } + return label; + } + /* eslint no-new-func: 0 */ + return new Function('states', 'entity', 'user', 'hass', `'use strict'; ${matchingLabelTemplate}`).call(this, this.hass.states, state, this.hass.user, this.hass); + } _isClickable(state) { let clickable = true; if (this.config.tap_action.action === 'toggle' && this.config.hold_action.action === 'none' || this.config.hold_action.action === 'toggle' && this.config.tap_action.action === 'none') { @@ -4045,11 +4200,11 @@ let ButtonCard = class ButtonCard extends LitElement { _rotate(configState) { return configState && configState.spin ? true : false; } - _blankCardColoredHtml(state) { + _blankCardColoredHtml(state, cardStyle) { const color = this._buildCssColorAttribute(state, undefined); const fontColor = getFontColorBasedOnBackgroundColor(color); return html` - +
`; @@ -4061,9 +4216,13 @@ let ButtonCard = class ButtonCard extends LitElement { let buttonColor = color; let cardStyle = {}; const configCardStyle = this._buildStyle(state, configState); + if (configCardStyle.width) { + this.style.setProperty('flex', '0 0 auto'); + this.style.setProperty('max-width', 'fit-content'); + } switch (this.config.color_type) { case 'blank-card': - return this._blankCardColoredHtml(state); + return this._blankCardColoredHtml(state, configCardStyle); case 'card': case 'label-card': { @@ -4078,10 +4237,6 @@ let ButtonCard = class ButtonCard extends LitElement { cardStyle = configCardStyle; break; } - if (configCardStyle.width) { - this.style.setProperty('flex', '0 0 auto'); - this.style.setProperty('max-width', 'fit-content'); - } return html` ${this._buttonContent(state, configState, buttonColor)} @@ -4104,14 +4259,17 @@ 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 label = this._buildLabel(state, configState); if (!iconTemplate) itemClass.push('no-icon'); if (!name) itemClass.push('no-name'); if (!stateString) itemClass.push('no-state'); + if (!label) itemClass.push('no-label'); return html`
${iconTemplate ? iconTemplate : ''} ${name ? html`
${name}
` : ''} ${stateString ? html`
${stateString}
` : ''} + ${label ? html`
${label}
` : ''}
`; } @@ -4142,7 +4300,7 @@ let ButtonCard = class ButtonCard extends LitElement { if (!config) { throw new Error('Invalid configuration'); } - this.config = Object.assign({ tap_action: { action: 'toggle' }, hold_action: { action: 'none' }, layout: 'vertical', size: '40%', color_type: 'icon', show_name: true, show_state: false, show_icon: true, show_units: true, show_entity_picture: false }, config); + this.config = Object.assign({ tap_action: { action: 'toggle' }, hold_action: { action: 'none' }, layout: 'vertical', size: '40%', color_type: 'icon', show_name: true, show_state: false, show_icon: true, show_units: true, show_label: false, show_entity_picture: false }, config); this.config.default_color = 'var(--primary-text-color)'; if (this.config.color_type !== 'icon') { this.config.color_off = 'var(--paper-card-background-color)'; diff --git a/examples/labels.png b/examples/labels.png new file mode 100644 index 0000000000000000000000000000000000000000..e490e185c226850baed51d4fc9b421a963a39b24 GIT binary patch literal 8022 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 BK&b!# literal 0 HcmV?d00001 diff --git a/src/button-card.ts b/src/button-card.ts index 185a3cd..68f54ca 100644 --- a/src/button-card.ts +++ b/src/button-card.ts @@ -48,7 +48,17 @@ class ButtonCard extends LitElement { } protected shouldUpdate(changedProps: PropertyValues): boolean { - return hasConfigOrEntityChanged(this, changedProps); + const state = this.config!.entity ? this.hass!.states[this.config!.entity] : undefined; + const configState = this._getMatchingConfigState(state); + const forceUpdate = (this.config!.show_label + && (configState + && configState.label_template + || this.config!.label_template) + ) + || this.config!.state + && this.config!.state.find((elt) => { return elt.operator === 'template'; }) + ? true : false; + return hasConfigOrEntityChanged(this, changedProps, forceUpdate); } private _getMatchingConfigState(state: HassEntity | undefined): StateConfig | undefined { @@ -77,6 +87,11 @@ class ButtonCard extends LitElement { const matches = state.state.match(elt.value) ? true : false; return matches; } + case 'template': { + return new Function('states', 'entity', 'user', 'hass', + `'use strict'; ${elt.value}`) + .call(this, this.hass!.states, state, this.hass!.user, this.hass); + } case 'default': def = elt; return false; @@ -266,6 +281,36 @@ class ButtonCard extends LitElement { return units; } + private _buildLabel( + state: HassEntity | undefined, + configState: StateConfig | undefined, + ): string | undefined { + if (!this.config!.show_label) { + return undefined; + } + let label: string | undefined; + let matchingLabelTemplate: string | undefined; + + if (configState && configState.label_template) { + matchingLabelTemplate = configState.label_template; + } else { + matchingLabelTemplate = this.config!.label_template; + } + if (!matchingLabelTemplate) { + if (configState && configState.label) { + label = configState.label; + } else { + label = this.config!.label; + } + return label; + } + + /* eslint no-new-func: 0 */ + return new Function('states', 'entity', 'user', 'hass', + `'use strict'; ${matchingLabelTemplate}`) + .call(this, this.hass!.states, state, this.hass!.user, this.hass); + } + private _isClickable(state: HassEntity | undefined): boolean { let clickable = true; if (this.config!.tap_action!.action === 'toggle' && this.config!.hold_action!.action === 'none' @@ -297,11 +342,14 @@ class ButtonCard extends LitElement { return configState && configState.spin ? true : false; } - private _blankCardColoredHtml(state: HassEntity | undefined): TemplateResult { + private _blankCardColoredHtml( + state: HassEntity | undefined, + cardStyle: StyleInfo, + ): TemplateResult { const color = this._buildCssColorAttribute(state, undefined); const fontColor = getFontColorBasedOnBackgroundColor(color); return html` - +
`; @@ -315,9 +363,13 @@ class ButtonCard extends LitElement { let cardStyle: StyleInfo = {}; const configCardStyle = this._buildStyle(state, configState); + if (configCardStyle.width) { + this.style.setProperty('flex', '0 0 auto'); + this.style.setProperty('max-width', 'fit-content'); + } switch (this.config!.color_type) { case 'blank-card': - return this._blankCardColoredHtml(state); + return this._blankCardColoredHtml(state, configCardStyle); case 'card': case 'label-card': { const fontColor = getFontColorBasedOnBackgroundColor(color); @@ -331,10 +383,6 @@ class ButtonCard extends LitElement { cardStyle = configCardStyle; break; } - if (configCardStyle.width) { - this.style.setProperty('flex', '0 0 auto'); - this.style.setProperty('max-width', 'fit-content'); - } return html` @@ -374,15 +422,18 @@ class ButtonCard extends LitElement { ): TemplateResult { const iconTemplate = this._getIconHtml(state, configState, color); const itemClass: string[] = ['container', containerClass]; + const label = this._buildLabel(state, configState); if (!iconTemplate) itemClass.push('no-icon'); if (!name) itemClass.push('no-name'); if (!stateString) itemClass.push('no-state'); + if (!label) itemClass.push('no-label'); return html`
${iconTemplate ? iconTemplate : ''} ${name ? html`
${name}
` : ''} ${stateString ? html`
${stateString}
` : ''} + ${label ? html`
${label}
` : ''}
`; } @@ -435,6 +486,7 @@ class ButtonCard extends LitElement { show_state: false, show_icon: true, show_units: true, + show_label: false, show_entity_picture: false, ...config, }; diff --git a/src/helpers.ts b/src/helpers.ts index 25647f4..3c9110c 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -61,8 +61,9 @@ export function applyBrightnessToColor( export function hasConfigOrEntityChanged( element: any, changedProps: PropertyValues, + forceUpdate: Boolean, ): boolean { - if (changedProps.has('config')) { + if (changedProps.has('config') || forceUpdate) { return true; } diff --git a/src/styles.ts b/src/styles.ts index eb82ce1..4b77411 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -79,8 +79,9 @@ export const styles = css` } .img-cell { grid-area: i; - min-height: 0; - min-width: 0; + height: 100%; + width: 100%; + max-width: 100%; } .icon { @@ -104,64 +105,164 @@ export const styles = css` /* margin: auto; */ } - .container.vertical { - grid-template-areas: "i" "n" "s"; - grid-template-columns: 1fr; - grid-template-rows: 1fr min-content min-content; + .label { + grid-area: l; + max-width: 100%; + align-self: center; + justify-self: center; } - .container.vertical.no-icon { - grid-template-areas: "n" "s"; + + .container.vertical { + grid-template-areas: "i" "n" "s" "l"; grid-template-columns: 1fr; - grid-template-rows: 1fr 1fr; + grid-template-rows: 1fr min-content min-content min-content; + } + /* 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 { - align-self: start; + align-self: center; } .container.vertical.no-icon .name { align-self: end; } + .container.vertical.no-icon .label { + align-self: start; + } + + /* 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 { + align-self: end; + } + .container.vertical.no-icon.no-name .label { + align-self: start; + } + + /* 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 { + align-self: end; + } + .container.vertical.no-icon.no-state .label { + align-self: start; + } + + /* 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 { + align-self: end; + } + .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 { grid-template-areas: "s"; grid-template-columns: 1fr; grid-template-rows: 1fr; } - .container.vertical.no-icon.no-name .state { + .container.vertical.no-icon.no-label.no-name .state { align-self: center; } - .container.vertical.no-icon.no-state { + /* 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-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 { + grid-template-areas: "l"; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + } + .container.vertical.no-icon.no-name.no-state .label { align-self: center; } .container.icon_name_state { - grid-template-areas: "i n"; + grid-template-areas: "i n" "l l"; grid-template-columns: 40% 1fr; - grid-template-rows: 1fr; + grid-template-rows: 1fr min-content; } .container.icon_name { - grid-template-areas: "i n" "s s"; + grid-template-areas: "i n" "s s" "l l"; grid-template-columns: 40% 1fr; - grid-template-rows: 1fr min-content; + grid-template-rows: 1fr min-content min-content; } .container.icon_state { - grid-template-areas: "i s" "n n"; + grid-template-areas: "i s" "n n" "l l"; grid-template-columns: 40% 1fr; - grid-template-rows: 1fr min-content; + grid-template-rows: 1fr min-content min-content; } .container.name_state { - grid-template-areas: "i" "n"; + grid-template-areas: "i" "n" "l"; grid-template-columns: 1fr; - grid-template-rows: 1fr min-content; + grid-template-rows: 1fr min-content min-content; + } + .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 { + align-self: end + } + .container.name_state.no-icon .label { + align-self: start } + .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 { + align-self: center + } + + /* icon_name_state2nd default */ .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 { + align-self: end; + } + .container.icon_name_state2nd .state { + align-self: center; + } + .container.icon_name_state2nd .label { + align-self: start; + } + + /* 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; @@ -173,7 +274,24 @@ export const styles = css` align-self: start; } + /* icon_state_name2nd Default */ .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 { + align-self: end; + } + .container.icon_state_name2nd .name { + align-self: center; + } + .container.icon_state_name2nd .state { + align-self: start; + } + + /* 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; @@ -184,6 +302,12 @@ export const styles = css` .container.icon_state_name2nd .name { align-self: start; } + + .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; + } `; export default styles; diff --git a/src/types.ts b/src/types.ts index 71523f0..45d0fff 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,12 +23,15 @@ export interface ButtonCardConfig { show_icon?: boolean; show_units?: boolean; show_entity_picture?: boolean; + show_label?: boolean; + label?: string; + label_template?: string; entity_picture?: string; units?: string; style?: CssStyleConfig[]; state?: StateConfig[]; confirmation?: string; - layout: 'vertical' | 'icon_name_state' | 'name_state' | 'icon_name' | 'icon_state' | 'icon_name_state2nd' | 'icon_state_name2nd'; + layout: Layout; entity_picture_style?: CssStyleConfig[]; default_color: string; @@ -36,8 +39,17 @@ export interface ButtonCardConfig { color_off: string; } +export type Layout = 'vertical' + | 'icon_name_state' + | 'name_state' + | 'icon_name' + | 'icon_state' + | 'icon_name_state2nd' + | 'icon_state_name2nd' + | 'icon_label'; + export interface StateConfig { - operator?: '<' | '<=' | '==' | '>=' | '>' | '!=' | 'regex' | 'default'; + operator?: '<' | '<=' | '==' | '>=' | '>' | '!=' | 'regex' | 'template' | 'default'; value?: any; name?: string; icon?: string; @@ -46,6 +58,8 @@ export interface StateConfig { entity_picture_style?: CssStyleConfig[]; entity_picture?: string; spin?: boolean; + label?: string; + label_template?: string; } export interface CssStyleConfig {