mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-31 14:39:38 +00:00 
			
		
		
		
	Compare commits
	
		
			7 Commits
		
	
	
		
			20251001.1
			...
			card_featu
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 0dd290cd85 | ||
|   | fb499f09fb | ||
|   | d3c83e0157 | ||
|   | 60c7a0e545 | ||
|   | d0f3eed49e | ||
|   | c2d4873ac3 | ||
|   | c3507bac0b | 
| @@ -83,7 +83,7 @@ export class HaControlSelectMenu extends SelectBase { | |||||||
|     if (!this.showArrow) return nothing; |     if (!this.showArrow) return nothing; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="icon"> |       <div class="icon arrow"> | ||||||
|         <ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon> |         <ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon> | ||||||
|       </div> |       </div> | ||||||
|     `; |     `; | ||||||
| @@ -179,7 +179,8 @@ export class HaControlSelectMenu extends SelectBase { | |||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|         align-items: flex-start; |         align-items: flex-start; | ||||||
|         justify-content: center; |         justify-content: center; | ||||||
|         flex: 1; |         width: 0; | ||||||
|  |         flex-grow: 1; | ||||||
|         overflow: hidden; |         overflow: hidden; | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -192,6 +193,13 @@ export class HaControlSelectMenu extends SelectBase { | |||||||
|         margin: auto; |         margin: auto; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       .arrow { | ||||||
|  |         margin-left: -10px; | ||||||
|  |         margin-inline-end: initial; | ||||||
|  |         margin-inline-start: -10px; | ||||||
|  |         direction: var(--direction); | ||||||
|  |       } | ||||||
|  |  | ||||||
|       .label { |       .label { | ||||||
|         font-size: 0.85em; |         font-size: 0.85em; | ||||||
|         letter-spacing: 0.4px; |         letter-spacing: 0.4px; | ||||||
|   | |||||||
| @@ -184,23 +184,21 @@ class HuiAlarmModeCardFeature | |||||||
|       `; |       `; | ||||||
|     } |     } | ||||||
|     return html` |     return html` | ||||||
|       <div class="container"> |       <ha-control-select | ||||||
|         <ha-control-select |         .options=${options} | ||||||
|           .options=${options} |         .value=${this._currentMode} | ||||||
|           .value=${this._currentMode} |         @value-changed=${this._valueChanged} | ||||||
|           @value-changed=${this._valueChanged} |         hide-label | ||||||
|           hide-label |         .ariaLabel=${this.hass.localize( | ||||||
|           .ariaLabel=${this.hass.localize( |           "ui.card.alarm_control_panel.modes_label" | ||||||
|             "ui.card.alarm_control_panel.modes_label" |         )} | ||||||
|           )} |         style=${styleMap({ | ||||||
|           style=${styleMap({ |           "--control-select-color": color, | ||||||
|             "--control-select-color": color, |           "--modes-count": options.length.toString(), | ||||||
|             "--modes-count": options.length.toString(), |         })} | ||||||
|           })} |         .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|           .disabled=${this.stateObj!.state === UNAVAILABLE} |       > | ||||||
|         > |       </ha-control-select> | ||||||
|         </ha-control-select> |  | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -214,13 +212,8 @@ class HuiAlarmModeCardFeature | |||||||
|         --control-select-button-border-radius: 10px; |         --control-select-button-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       ha-control-button-group { |       ha-control-button-group { | ||||||
|         margin: 0 12px 12px 12px; |  | ||||||
|         --control-button-group-spacing: 12px; |         --control-button-group-spacing: 12px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import { mdiSwapHorizontal } from "@mdi/js"; | ||||||
| import type { HassEntity } from "home-assistant-js-websocket"; | import type { HassEntity } from "home-assistant-js-websocket"; | ||||||
| import { | import { | ||||||
|   CSSResultGroup, |   CSSResultGroup, | ||||||
| @@ -7,9 +8,12 @@ import { | |||||||
|   html, |   html, | ||||||
|   nothing, |   nothing, | ||||||
| } from "lit"; | } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
|  | import "../../../components/ha-control-button"; | ||||||
|  | import "../../../components/ha-svg-icon"; | ||||||
| import { HomeAssistant } from "../../../types"; | import { HomeAssistant } from "../../../types"; | ||||||
| import type { HuiErrorCard } from "../cards/hui-error-card"; | import type { HuiErrorCard } from "../cards/hui-error-card"; | ||||||
|  | import { LovelaceCardFeatureLayout } from "../cards/types"; | ||||||
| import { createCardFeatureElement } from "../create-element/create-card-feature-element"; | import { createCardFeatureElement } from "../create-element/create-card-feature-element"; | ||||||
| import type { LovelaceCardFeature } from "../types"; | import type { LovelaceCardFeature } from "../types"; | ||||||
| import type { LovelaceCardFeatureConfig } from "./types"; | import type { LovelaceCardFeatureConfig } from "./types"; | ||||||
| @@ -22,8 +26,12 @@ export class HuiCardFeatures extends LitElement { | |||||||
|  |  | ||||||
|   @property({ attribute: false }) public features?: LovelaceCardFeatureConfig[]; |   @property({ attribute: false }) public features?: LovelaceCardFeatureConfig[]; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) public layout?: LovelaceCardFeatureLayout; | ||||||
|  |  | ||||||
|   @property({ attribute: false }) public color?: string; |   @property({ attribute: false }) public color?: string; | ||||||
|  |  | ||||||
|  |   @state() private _currentFeatureIndex = 0; | ||||||
|  |  | ||||||
|   private _featuresElements = new WeakMap< |   private _featuresElements = new WeakMap< | ||||||
|     LovelaceCardFeatureConfig, |     LovelaceCardFeatureConfig, | ||||||
|     LovelaceCardFeature | HuiErrorCard |     LovelaceCardFeature | HuiErrorCard | ||||||
| @@ -54,14 +62,47 @@ export class HuiCardFeatures extends LitElement { | |||||||
|     return html`${element}`; |     return html`${element}`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _next() { | ||||||
|  |     let newIndex = this._currentFeatureIndex + 1; | ||||||
|  |     if (this.features?.length && newIndex >= this.features.length) { | ||||||
|  |       newIndex = 0; | ||||||
|  |     } | ||||||
|  |     this._currentFeatureIndex = newIndex; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
|     if (!this.features) { |     if (!this.features) { | ||||||
|       return nothing; |       return nothing; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (this.layout === "compact") { | ||||||
|  |       const currentFeature = this.features[this._currentFeatureIndex]; | ||||||
|  |       return html` | ||||||
|  |         <div class="container horizontal"> | ||||||
|  |           ${this.renderFeature(currentFeature, this.stateObj)} | ||||||
|  |           ${this.features.length > 1 | ||||||
|  |             ? html` | ||||||
|  |                 <ha-control-button | ||||||
|  |                   class="next" | ||||||
|  |                   @click=${this._next} | ||||||
|  |                   .label=${"Next"} | ||||||
|  |                 > | ||||||
|  |                   <ha-svg-icon .path=${mdiSwapHorizontal}></ha-svg-icon> | ||||||
|  |                 </ha-control-button> | ||||||
|  |               ` | ||||||
|  |             : nothing} | ||||||
|  |         </div> | ||||||
|  |       `; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const containerClass = this.layout ? ` ${this.layout}` : ""; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       ${this.features.map((featureConf) => |       <div class="container${containerClass}"> | ||||||
|         this.renderFeature(featureConf, this.stateObj) |         ${this.features.map((featureConf) => | ||||||
|       )} |           this.renderFeature(featureConf, this.stateObj) | ||||||
|  |         )} | ||||||
|  |       </div> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -69,8 +110,32 @@ export class HuiCardFeatures extends LitElement { | |||||||
|     return css` |     return css` | ||||||
|       :host { |       :host { | ||||||
|         --feature-color: var(--state-icon-color); |         --feature-color: var(--state-icon-color); | ||||||
|  |         --feature-padding: 12px; | ||||||
|  |         position: relative; | ||||||
|  |         width: 100%; | ||||||
|  |       } | ||||||
|  |       .container { | ||||||
|  |         position: relative; | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|  |         padding: var(--feature-padding); | ||||||
|  |         padding-top: 0px; | ||||||
|  |         gap: var(--feature-padding); | ||||||
|  |         width: 100%; | ||||||
|  |         box-sizing: border-box; | ||||||
|  |       } | ||||||
|  |       .container.horizontal { | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: row; | ||||||
|  |       } | ||||||
|  |       .container.horizontal > * { | ||||||
|  |         flex: 1; | ||||||
|  |       } | ||||||
|  |       .next { | ||||||
|  |         flex: none !important; | ||||||
|  |         --control-button-background-opacity: 0; | ||||||
|  |         --control-button-padding: 0; | ||||||
|  |         width: 32px; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -140,54 +140,50 @@ class HuiClimateFanModesCardFeature | |||||||
|  |  | ||||||
|     if (this._config.style === "icons") { |     if (this._config.style === "icons") { | ||||||
|       return html` |       return html` | ||||||
|         <div class="container"> |         <ha-control-select | ||||||
|           <ha-control-select |           .options=${options} | ||||||
|             .options=${options} |           .value=${this._currentFanMode} | ||||||
|             .value=${this._currentFanMode} |           @value-changed=${this._valueChanged} | ||||||
|             @value-changed=${this._valueChanged} |           hide-label | ||||||
|             hide-label |           .ariaLabel=${this.hass!.formatEntityAttributeName( | ||||||
|             .ariaLabel=${this.hass!.formatEntityAttributeName( |             stateObj, | ||||||
|               stateObj, |             "fan_mode" | ||||||
|               "fan_mode" |           )} | ||||||
|             )} |           .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|             .disabled=${this.stateObj!.state === UNAVAILABLE} |         > | ||||||
|           > |         </ha-control-select> | ||||||
|           </ha-control-select> |  | ||||||
|         </div> |  | ||||||
|       `; |       `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="container"> |       <ha-control-select-menu | ||||||
|         <ha-control-select-menu |         show-arrow | ||||||
|           show-arrow |         hide-label | ||||||
|           hide-label |         .label=${this.hass!.formatEntityAttributeName(stateObj, "fan_mode")} | ||||||
|           .label=${this.hass!.formatEntityAttributeName(stateObj, "fan_mode")} |         .value=${this._currentFanMode} | ||||||
|           .value=${this._currentFanMode} |         .disabled=${this.stateObj.state === UNAVAILABLE} | ||||||
|           .disabled=${this.stateObj.state === UNAVAILABLE} |         fixedMenuPosition | ||||||
|           fixedMenuPosition |         naturalMenuWidth | ||||||
|           naturalMenuWidth |         @selected=${this._valueChanged} | ||||||
|           @selected=${this._valueChanged} |         @closed=${stopPropagation} | ||||||
|           @closed=${stopPropagation} |       > | ||||||
|         > |         ${this._currentFanMode | ||||||
|           ${this._currentFanMode |           ? html`<ha-attribute-icon | ||||||
|             ? html`<ha-attribute-icon |               slot="icon" | ||||||
|                 slot="icon" |               .hass=${this.hass} | ||||||
|                 .hass=${this.hass} |               .stateObj=${stateObj} | ||||||
|                 .stateObj=${stateObj} |               attribute="fan_mode" | ||||||
|                 attribute="fan_mode" |               .attributeValue=${this._currentFanMode} | ||||||
|                 .attributeValue=${this._currentFanMode} |             ></ha-attribute-icon>` | ||||||
|               ></ha-attribute-icon>` |           : html` <ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>`} | ||||||
|             : html` <ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>`} |         ${options.map( | ||||||
|           ${options.map( |           (option) => html` | ||||||
|             (option) => html` |             <ha-list-item .value=${option.value} graphic="icon"> | ||||||
|               <ha-list-item .value=${option.value} graphic="icon"> |               ${option.icon}${option.label} | ||||||
|                 ${option.icon}${option.label} |             </ha-list-item> | ||||||
|               </ha-list-item> |           ` | ||||||
|             ` |         )} | ||||||
|           )} |       </ha-control-select-menu> | ||||||
|         </ha-control-select-menu> |  | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -208,10 +204,6 @@ class HuiClimateFanModesCardFeature | |||||||
|         --control-select-border-radius: 10px; |         --control-select-border-radius: 10px; | ||||||
|         --control-select-button-border-radius: 10px; |         --control-select-button-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -139,55 +139,51 @@ class HuiClimateHvacModesCardFeature | |||||||
|  |  | ||||||
|     if (this._config.style === "dropdown") { |     if (this._config.style === "dropdown") { | ||||||
|       return html` |       return html` | ||||||
|         <div class="container"> |         <ha-control-select-menu | ||||||
|           <ha-control-select-menu |           show-arrow | ||||||
|             show-arrow |           hide-label | ||||||
|             hide-label |           .label=${this.hass.localize("ui.card.climate.mode")} | ||||||
|             .label=${this.hass.localize("ui.card.climate.mode")} |           .value=${this._currentHvacMode} | ||||||
|             .value=${this._currentHvacMode} |           .disabled=${this.stateObj.state === UNAVAILABLE} | ||||||
|             .disabled=${this.stateObj.state === UNAVAILABLE} |           fixedMenuPosition | ||||||
|             fixedMenuPosition |           naturalMenuWidth | ||||||
|             naturalMenuWidth |           @selected=${this._valueChanged} | ||||||
|             @selected=${this._valueChanged} |           @closed=${stopPropagation} | ||||||
|             @closed=${stopPropagation} |         > | ||||||
|           > |           ${this._currentHvacMode | ||||||
|             ${this._currentHvacMode |             ? html` | ||||||
|               ? html` |                 <ha-svg-icon | ||||||
|                   <ha-svg-icon |                   slot="icon" | ||||||
|                     slot="icon" |                   .path=${climateHvacModeIcon(this._currentHvacMode)} | ||||||
|                     .path=${climateHvacModeIcon(this._currentHvacMode)} |                 ></ha-svg-icon> | ||||||
|                   ></ha-svg-icon> |  | ||||||
|                 ` |  | ||||||
|               : html` |  | ||||||
|                   <ha-svg-icon slot="icon" .path=${mdiThermostat}></ha-svg-icon> |  | ||||||
|                 `} |  | ||||||
|             ${options.map( |  | ||||||
|               (option) => html` |  | ||||||
|                 <ha-list-item .value=${option.value} graphic="icon"> |  | ||||||
|                   ${option.icon}${option.label} |  | ||||||
|                 </ha-list-item> |  | ||||||
|               ` |               ` | ||||||
|             )} |             : html` | ||||||
|           </ha-control-select-menu> |                 <ha-svg-icon slot="icon" .path=${mdiThermostat}></ha-svg-icon> | ||||||
|         </div> |               `} | ||||||
|  |           ${options.map( | ||||||
|  |             (option) => html` | ||||||
|  |               <ha-list-item .value=${option.value} graphic="icon"> | ||||||
|  |                 ${option.icon}${option.label} | ||||||
|  |               </ha-list-item> | ||||||
|  |             ` | ||||||
|  |           )} | ||||||
|  |         </ha-control-select-menu> | ||||||
|       `; |       `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="container"> |       <ha-control-select | ||||||
|         <ha-control-select |         .options=${options} | ||||||
|           .options=${options} |         .value=${this._currentHvacMode} | ||||||
|           .value=${this._currentHvacMode} |         @value-changed=${this._valueChanged} | ||||||
|           @value-changed=${this._valueChanged} |         hide-label | ||||||
|           hide-label |         .ariaLabel=${this.hass.localize("ui.card.climate.mode")} | ||||||
|           .ariaLabel=${this.hass.localize("ui.card.climate.mode")} |         style=${styleMap({ | ||||||
|           style=${styleMap({ |           "--control-select-color": color, | ||||||
|             "--control-select-color": color, |         })} | ||||||
|           })} |         .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|           .disabled=${this.stateObj!.state === UNAVAILABLE} |       > | ||||||
|         > |       </ha-control-select> | ||||||
|         </ha-control-select> |  | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -207,10 +203,6 @@ class HuiClimateHvacModesCardFeature | |||||||
|         --control-select-border-radius: 10px; |         --control-select-border-radius: 10px; | ||||||
|         --control-select-button-border-radius: 10px; |         --control-select-button-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -142,59 +142,52 @@ class HuiClimatePresetModesCardFeature | |||||||
|  |  | ||||||
|     if (this._config.style === "icons") { |     if (this._config.style === "icons") { | ||||||
|       return html` |       return html` | ||||||
|         <div class="container"> |         <ha-control-select | ||||||
|           <ha-control-select |           .options=${options} | ||||||
|             .options=${options} |           .value=${this._currentPresetMode} | ||||||
|             .value=${this._currentPresetMode} |           @value-changed=${this._valueChanged} | ||||||
|             @value-changed=${this._valueChanged} |           hide-label | ||||||
|             hide-label |           .ariaLabel=${this.hass!.formatEntityAttributeName( | ||||||
|             .ariaLabel=${this.hass!.formatEntityAttributeName( |             stateObj, | ||||||
|               stateObj, |             "preset_mode" | ||||||
|               "preset_mode" |           )} | ||||||
|             )} |           .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|             .disabled=${this.stateObj!.state === UNAVAILABLE} |         > | ||||||
|           > |         </ha-control-select> | ||||||
|           </ha-control-select> |  | ||||||
|         </div> |  | ||||||
|       `; |       `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="container"> |       <ha-control-select-menu | ||||||
|         <ha-control-select-menu |         show-arrow | ||||||
|           show-arrow |         hide-label | ||||||
|           hide-label |         .label=${this.hass!.formatEntityAttributeName(stateObj, "preset_mode")} | ||||||
|           .label=${this.hass!.formatEntityAttributeName( |         .value=${this._currentPresetMode} | ||||||
|             stateObj, |         .disabled=${this.stateObj.state === UNAVAILABLE} | ||||||
|             "preset_mode" |         fixedMenuPosition | ||||||
|           )} |         naturalMenuWidth | ||||||
|           .value=${this._currentPresetMode} |         @selected=${this._valueChanged} | ||||||
|           .disabled=${this.stateObj.state === UNAVAILABLE} |         @closed=${stopPropagation} | ||||||
|           fixedMenuPosition |       > | ||||||
|           naturalMenuWidth |         ${this._currentPresetMode | ||||||
|           @selected=${this._valueChanged} |           ? html`<ha-attribute-icon | ||||||
|           @closed=${stopPropagation} |               slot="icon" | ||||||
|         > |               .hass=${this.hass} | ||||||
|           ${this._currentPresetMode |               .stateObj=${stateObj} | ||||||
|             ? html`<ha-attribute-icon |               attribute="preset_mode" | ||||||
|                 slot="icon" |               .attributeValue=${this._currentPresetMode} | ||||||
|                 .hass=${this.hass} |             ></ha-attribute-icon>` | ||||||
|                 .stateObj=${stateObj} |           : html` | ||||||
|                 attribute="preset_mode" |               <ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon> | ||||||
|                 .attributeValue=${this._currentPresetMode} |             `} | ||||||
|               ></ha-attribute-icon>` |         ${options.map( | ||||||
|             : html` |           (option) => html` | ||||||
|                 <ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon> |             <ha-list-item .value=${option.value} graphic="icon"> | ||||||
|               `} |               ${option.icon}${option.label} | ||||||
|           ${options.map( |             </ha-list-item> | ||||||
|             (option) => html` |           ` | ||||||
|               <ha-list-item .value=${option.value} graphic="icon"> |         )} | ||||||
|                 ${option.icon}${option.label} |       </ha-control-select-menu> | ||||||
|               </ha-list-item> |  | ||||||
|             ` |  | ||||||
|           )} |  | ||||||
|         </ha-control-select-menu> |  | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -215,10 +208,6 @@ class HuiClimatePresetModesCardFeature | |||||||
|         --control-select-border-radius: 10px; |         --control-select-border-radius: 10px; | ||||||
|         --control-select-button-border-radius: 10px; |         --control-select-button-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -142,57 +142,53 @@ class HuiClimateSwingModesCardFeature | |||||||
|  |  | ||||||
|     if (this._config.style === "icons") { |     if (this._config.style === "icons") { | ||||||
|       return html` |       return html` | ||||||
|         <div class="container"> |         <ha-control-select | ||||||
|           <ha-control-select |           .options=${options} | ||||||
|             .options=${options} |           .value=${this._currentSwingMode} | ||||||
|             .value=${this._currentSwingMode} |           @value-changed=${this._valueChanged} | ||||||
|             @value-changed=${this._valueChanged} |           hide-label | ||||||
|             hide-label |           .ariaLabel=${this.hass!.formatEntityAttributeName( | ||||||
|             .ariaLabel=${this.hass!.formatEntityAttributeName( |             stateObj, | ||||||
|               stateObj, |             "swing_mode" | ||||||
|               "swing_mode" |           )} | ||||||
|             )} |           .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|             .disabled=${this.stateObj!.state === UNAVAILABLE} |         > | ||||||
|           > |         </ha-control-select> | ||||||
|           </ha-control-select> |  | ||||||
|         </div> |  | ||||||
|       `; |       `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="container"> |       <ha-control-select-menu | ||||||
|         <ha-control-select-menu |         show-arrow | ||||||
|           show-arrow |         hide-label | ||||||
|           hide-label |         .label=${this.hass!.formatEntityAttributeName(stateObj, "swing_mode")} | ||||||
|           .label=${this.hass!.formatEntityAttributeName(stateObj, "swing_mode")} |         .value=${this._currentSwingMode} | ||||||
|           .value=${this._currentSwingMode} |         .disabled=${this.stateObj.state === UNAVAILABLE} | ||||||
|           .disabled=${this.stateObj.state === UNAVAILABLE} |         fixedMenuPosition | ||||||
|           fixedMenuPosition |         naturalMenuWidth | ||||||
|           naturalMenuWidth |         @selected=${this._valueChanged} | ||||||
|           @selected=${this._valueChanged} |         @closed=${stopPropagation} | ||||||
|           @closed=${stopPropagation} |       > | ||||||
|         > |         ${this._currentSwingMode | ||||||
|           ${this._currentSwingMode |           ? html`<ha-attribute-icon | ||||||
|             ? html`<ha-attribute-icon |               slot="icon" | ||||||
|                 slot="icon" |               .hass=${this.hass} | ||||||
|                 .hass=${this.hass} |               .stateObj=${stateObj} | ||||||
|                 .stateObj=${stateObj} |               attribute="swing_mode" | ||||||
|                 attribute="swing_mode" |               .attributeValue=${this._currentSwingMode} | ||||||
|                 .attributeValue=${this._currentSwingMode} |             ></ha-attribute-icon>` | ||||||
|               ></ha-attribute-icon>` |           : html` <ha-svg-icon | ||||||
|             : html` <ha-svg-icon |               slot="icon" | ||||||
|                 slot="icon" |               .path=${mdiArrowOscillating} | ||||||
|                 .path=${mdiArrowOscillating} |             ></ha-svg-icon>`} | ||||||
|               ></ha-svg-icon>`} |         ${options.map( | ||||||
|           ${options.map( |           (option) => html` | ||||||
|             (option) => html` |             <ha-list-item .value=${option.value} graphic="icon"> | ||||||
|               <ha-list-item .value=${option.value} graphic="icon"> |               ${option.icon}${option.label} | ||||||
|                 ${option.icon}${option.label} |             </ha-list-item> | ||||||
|               </ha-list-item> |           ` | ||||||
|             ` |         )} | ||||||
|           )} |       </ha-control-select-menu> | ||||||
|         </ha-control-select-menu> |  | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -213,10 +209,6 @@ class HuiClimateSwingModesCardFeature | |||||||
|         --control-select-border-radius: 10px; |         --control-select-border-radius: 10px; | ||||||
|         --control-select-button-border-radius: 10px; |         --control-select-button-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -130,7 +130,6 @@ class HuiCoverOpenCloseCardFeature | |||||||
|   static get styles() { |   static get styles() { | ||||||
|     return css` |     return css` | ||||||
|       ha-control-button-group { |       ha-control-button-group { | ||||||
|         margin: 0 12px 12px 12px; |  | ||||||
|         --control-button-group-spacing: 12px; |         --control-button-group-spacing: 12px; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -78,26 +78,25 @@ class HuiCoverPositionCardFeature | |||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="container" style=${styleMap(style)}> |       <ha-control-slider | ||||||
|         <ha-control-slider |         style=${styleMap(style)} | ||||||
|           .value=${value} |         .value=${value} | ||||||
|           min="0" |         min="0" | ||||||
|           max="100" |         max="100" | ||||||
|           step="1" |         step="1" | ||||||
|           inverted |         inverted | ||||||
|           show-handle |         show-handle | ||||||
|           @value-changed=${this._valueChanged} |         @value-changed=${this._valueChanged} | ||||||
|           .ariaLabel=${computeAttributeNameDisplay( |         .ariaLabel=${computeAttributeNameDisplay( | ||||||
|             this.hass.localize, |           this.hass.localize, | ||||||
|             this.stateObj, |           this.stateObj, | ||||||
|             this.hass.entities, |           this.hass.entities, | ||||||
|             "current_position" |           "current_position" | ||||||
|           )} |         )} | ||||||
|           .disabled=${this.stateObj!.state === UNAVAILABLE} |         .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|           .unit=${DOMAIN_ATTRIBUTES_UNITS.cover.current_position} |         .unit=${DOMAIN_ATTRIBUTES_UNITS.cover.current_position} | ||||||
|           .locale=${this.hass.locale} |         .locale=${this.hass.locale} | ||||||
|         ></ha-control-slider> |       ></ha-control-slider> | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -120,10 +119,6 @@ class HuiCoverPositionCardFeature | |||||||
|         --control-slider-thickness: 40px; |         --control-slider-thickness: 40px; | ||||||
|         --control-slider-border-radius: 10px; |         --control-slider-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -122,7 +122,6 @@ class HuiCoverTiltCardFeature | |||||||
|   static get styles() { |   static get styles() { | ||||||
|     return css` |     return css` | ||||||
|       ha-control-button-group { |       ha-control-button-group { | ||||||
|         margin: 0 12px 12px 12px; |  | ||||||
|         --control-button-group-spacing: 12px; |         --control-button-group-spacing: 12px; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -78,27 +78,26 @@ class HuiCoverTiltPositionCardFeature | |||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="container" style=${styleMap(style)}> |       <ha-control-slider | ||||||
|         <ha-control-slider |         style=${styleMap(style)} | ||||||
|           .value=${value} |         .value=${value} | ||||||
|           min="0" |         min="0" | ||||||
|           max="100" |         max="100" | ||||||
|           mode="cursor" |         mode="cursor" | ||||||
|           inverted |         inverted | ||||||
|           @value-changed=${this._valueChanged} |         @value-changed=${this._valueChanged} | ||||||
|           .ariaLabel=${computeAttributeNameDisplay( |         .ariaLabel=${computeAttributeNameDisplay( | ||||||
|             this.hass.localize, |           this.hass.localize, | ||||||
|             this.stateObj, |           this.stateObj, | ||||||
|             this.hass.entities, |           this.hass.entities, | ||||||
|             "current_tilt_position" |           "current_tilt_position" | ||||||
|           )} |         )} | ||||||
|           .disabled=${this.stateObj!.state === UNAVAILABLE} |         .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|           .unit=${DOMAIN_ATTRIBUTES_UNITS.cover.current_tilt_position} |         .unit=${DOMAIN_ATTRIBUTES_UNITS.cover.current_tilt_position} | ||||||
|           .locale=${this.hass.locale} |         .locale=${this.hass.locale} | ||||||
|         > |       > | ||||||
|           <div slot="background" class="gradient"></div |         <div slot="background" class="gradient"></div | ||||||
|         ></ha-control-slider> |       ></ha-control-slider> | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -122,10 +121,6 @@ class HuiCoverTiltPositionCardFeature | |||||||
|         --control-slider-thickness: 40px; |         --control-slider-thickness: 40px; | ||||||
|         --control-slider-border-radius: 10px; |         --control-slider-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|       .gradient { |       .gradient { | ||||||
|         background: -webkit-linear-gradient(left, ${GRADIENT}); |         background: -webkit-linear-gradient(left, ${GRADIENT}); | ||||||
|         opacity: 0.6; |         opacity: 0.6; | ||||||
|   | |||||||
| @@ -139,59 +139,52 @@ class HuiFanPresetModesCardFeature | |||||||
|  |  | ||||||
|     if (this._config.style === "icons") { |     if (this._config.style === "icons") { | ||||||
|       return html` |       return html` | ||||||
|         <div class="container"> |         <ha-control-select | ||||||
|           <ha-control-select |           .options=${options} | ||||||
|             .options=${options} |           .value=${this._currentPresetMode} | ||||||
|             .value=${this._currentPresetMode} |           @value-changed=${this._valueChanged} | ||||||
|             @value-changed=${this._valueChanged} |           hide-label | ||||||
|             hide-label |           .ariaLabel=${this.hass!.formatEntityAttributeName( | ||||||
|             .ariaLabel=${this.hass!.formatEntityAttributeName( |             stateObj, | ||||||
|               stateObj, |             "preset_mode" | ||||||
|               "preset_mode" |           )} | ||||||
|             )} |           .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|             .disabled=${this.stateObj!.state === UNAVAILABLE} |         > | ||||||
|           > |         </ha-control-select> | ||||||
|           </ha-control-select> |  | ||||||
|         </div> |  | ||||||
|       `; |       `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="container"> |       <ha-control-select-menu | ||||||
|         <ha-control-select-menu |         show-arrow | ||||||
|           show-arrow |         hide-label | ||||||
|           hide-label |         .label=${this.hass!.formatEntityAttributeName(stateObj, "preset_mode")} | ||||||
|           .label=${this.hass!.formatEntityAttributeName( |         .value=${this._currentPresetMode} | ||||||
|             stateObj, |         .disabled=${this.stateObj.state === UNAVAILABLE} | ||||||
|             "preset_mode" |         fixedMenuPosition | ||||||
|           )} |         naturalMenuWidth | ||||||
|           .value=${this._currentPresetMode} |         @selected=${this._valueChanged} | ||||||
|           .disabled=${this.stateObj.state === UNAVAILABLE} |         @closed=${stopPropagation} | ||||||
|           fixedMenuPosition |       > | ||||||
|           naturalMenuWidth |         ${this._currentPresetMode | ||||||
|           @selected=${this._valueChanged} |           ? html`<ha-attribute-icon | ||||||
|           @closed=${stopPropagation} |               slot="icon" | ||||||
|         > |               .hass=${this.hass} | ||||||
|           ${this._currentPresetMode |               .stateObj=${stateObj} | ||||||
|             ? html`<ha-attribute-icon |               attribute="preset_mode" | ||||||
|                 slot="icon" |               .attributeValue=${this._currentPresetMode} | ||||||
|                 .hass=${this.hass} |             ></ha-attribute-icon>` | ||||||
|                 .stateObj=${stateObj} |           : html` | ||||||
|                 attribute="preset_mode" |               <ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon> | ||||||
|                 .attributeValue=${this._currentPresetMode} |             `} | ||||||
|               ></ha-attribute-icon>` |         ${options.map( | ||||||
|             : html` |           (option) => html` | ||||||
|                 <ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon> |             <ha-list-item .value=${option.value} graphic="icon"> | ||||||
|               `} |               ${option.icon}${option.label} | ||||||
|           ${options.map( |             </ha-list-item> | ||||||
|             (option) => html` |           ` | ||||||
|               <ha-list-item .value=${option.value} graphic="icon"> |         )} | ||||||
|                 ${option.icon}${option.label} |       </ha-control-select-menu> | ||||||
|               </ha-list-item> |  | ||||||
|             ` |  | ||||||
|           )} |  | ||||||
|         </ha-control-select-menu> |  | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -212,10 +205,6 @@ class HuiFanPresetModesCardFeature | |||||||
|         --control-select-border-radius: 10px; |         --control-select-border-radius: 10px; | ||||||
|         --control-select-button-border-radius: 10px; |         --control-select-button-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -88,35 +88,11 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { | |||||||
|       const speed = fanPercentageToSpeed(this.stateObj, percentage); |       const speed = fanPercentageToSpeed(this.stateObj, percentage); | ||||||
|  |  | ||||||
|       return html` |       return html` | ||||||
|         <div class="container"> |         <ha-control-select | ||||||
|           <ha-control-select |           .options=${options} | ||||||
|             .options=${options} |           .value=${speed} | ||||||
|             .value=${speed} |           @value-changed=${this._speedValueChanged} | ||||||
|             @value-changed=${this._speedValueChanged} |           hide-label | ||||||
|             hide-label |  | ||||||
|             .ariaLabel=${computeAttributeNameDisplay( |  | ||||||
|               this.hass.localize, |  | ||||||
|               this.stateObj, |  | ||||||
|               this.hass.entities, |  | ||||||
|               "percentage" |  | ||||||
|             )} |  | ||||||
|             .disabled=${this.stateObj!.state === UNAVAILABLE} |  | ||||||
|           > |  | ||||||
|           </ha-control-select> |  | ||||||
|         </div> |  | ||||||
|       `; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const value = Math.max(Math.round(percentage), 0); |  | ||||||
|  |  | ||||||
|     return html` |  | ||||||
|       <div class="container"> |  | ||||||
|         <ha-control-slider |  | ||||||
|           .value=${value} |  | ||||||
|           min="0" |  | ||||||
|           max="100" |  | ||||||
|           .step=${this.stateObj.attributes.percentage_step ?? 1} |  | ||||||
|           @value-changed=${this._valueChanged} |  | ||||||
|           .ariaLabel=${computeAttributeNameDisplay( |           .ariaLabel=${computeAttributeNameDisplay( | ||||||
|             this.hass.localize, |             this.hass.localize, | ||||||
|             this.stateObj, |             this.stateObj, | ||||||
| @@ -124,10 +100,30 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { | |||||||
|             "percentage" |             "percentage" | ||||||
|           )} |           )} | ||||||
|           .disabled=${this.stateObj!.state === UNAVAILABLE} |           .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|           .unit=${DOMAIN_ATTRIBUTES_UNITS.fan.percentage} |         > | ||||||
|           .locale=${this.hass.locale} |         </ha-control-select> | ||||||
|         ></ha-control-slider> |       `; | ||||||
|       </div> |     } | ||||||
|  |  | ||||||
|  |     const value = Math.max(Math.round(percentage), 0); | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <ha-control-slider | ||||||
|  |         .value=${value} | ||||||
|  |         min="0" | ||||||
|  |         max="100" | ||||||
|  |         .step=${this.stateObj.attributes.percentage_step ?? 1} | ||||||
|  |         @value-changed=${this._valueChanged} | ||||||
|  |         .ariaLabel=${computeAttributeNameDisplay( | ||||||
|  |           this.hass.localize, | ||||||
|  |           this.stateObj, | ||||||
|  |           this.hass.entities, | ||||||
|  |           "percentage" | ||||||
|  |         )} | ||||||
|  |         .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|  |         .unit=${DOMAIN_ATTRIBUTES_UNITS.fan.percentage} | ||||||
|  |         .locale=${this.hass.locale} | ||||||
|  |       ></ha-control-slider> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -170,10 +166,6 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { | |||||||
|         --control-select-border-radius: 10px; |         --control-select-border-radius: 10px; | ||||||
|         --control-select-button-border-radius: 10px; |         --control-select-button-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -143,54 +143,50 @@ class HuiHumidifierModesCardFeature | |||||||
|  |  | ||||||
|     if (this._config.style === "icons") { |     if (this._config.style === "icons") { | ||||||
|       return html` |       return html` | ||||||
|         <div class="container"> |         <ha-control-select | ||||||
|           <ha-control-select |           .options=${options} | ||||||
|             .options=${options} |           .value=${this._currentMode} | ||||||
|             .value=${this._currentMode} |           @value-changed=${this._valueChanged} | ||||||
|             @value-changed=${this._valueChanged} |           hide-label | ||||||
|             hide-label |           .ariaLabel=${this.hass!.formatEntityAttributeName(stateObj, "mode")} | ||||||
|             .ariaLabel=${this.hass!.formatEntityAttributeName(stateObj, "mode")} |           .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|             .disabled=${this.stateObj!.state === UNAVAILABLE} |         > | ||||||
|           > |         </ha-control-select> | ||||||
|           </ha-control-select> |  | ||||||
|         </div> |  | ||||||
|       `; |       `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="container"> |       <ha-control-select-menu | ||||||
|         <ha-control-select-menu |         show-arrow | ||||||
|           show-arrow |         hide-label | ||||||
|           hide-label |         .label=${this.hass!.formatEntityAttributeName(stateObj, "mode")} | ||||||
|           .label=${this.hass!.formatEntityAttributeName(stateObj, "mode")} |         .value=${this._currentMode} | ||||||
|           .value=${this._currentMode} |         .disabled=${this.stateObj.state === UNAVAILABLE} | ||||||
|           .disabled=${this.stateObj.state === UNAVAILABLE} |         fixedMenuPosition | ||||||
|           fixedMenuPosition |         naturalMenuWidth | ||||||
|           naturalMenuWidth |         @selected=${this._valueChanged} | ||||||
|           @selected=${this._valueChanged} |         @closed=${stopPropagation} | ||||||
|           @closed=${stopPropagation} |       > | ||||||
|         > |         ${this._currentMode | ||||||
|           ${this._currentMode |           ? html`<ha-attribute-icon | ||||||
|             ? html`<ha-attribute-icon |               slot="icon" | ||||||
|                 slot="icon" |               .hass=${this.hass} | ||||||
|                 .hass=${this.hass} |               .stateObj=${stateObj} | ||||||
|                 .stateObj=${stateObj} |               attribute="mode" | ||||||
|                 attribute="mode" |               .attributeValue=${this._currentMode} | ||||||
|                 .attributeValue=${this._currentMode} |             ></ha-attribute-icon>` | ||||||
|               ></ha-attribute-icon>` |           : html`<ha-svg-icon | ||||||
|             : html`<ha-svg-icon |               slot="icon" | ||||||
|                 slot="icon" |               .path=${mdiTuneVariant} | ||||||
|                 .path=${mdiTuneVariant} |             ></ha-svg-icon>`} | ||||||
|               ></ha-svg-icon>`} |         ${options.map( | ||||||
|           ${options.map( |           (option) => html` | ||||||
|             (option) => html` |             <ha-list-item .value=${option.value} graphic="icon"> | ||||||
|               <ha-list-item .value=${option.value} graphic="icon"> |               ${option.icon}${option.label} | ||||||
|                 ${option.icon}${option.label} |             </ha-list-item> | ||||||
|               </ha-list-item> |           ` | ||||||
|             ` |         )} | ||||||
|           )} |       </ha-control-select-menu> | ||||||
|         </ha-control-select-menu> |  | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -211,10 +207,6 @@ class HuiHumidifierModesCardFeature | |||||||
|         --control-select-border-radius: 10px; |         --control-select-border-radius: 10px; | ||||||
|         --control-select-button-border-radius: 10px; |         --control-select-button-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -95,20 +95,18 @@ class HuiHumidifierToggleCardFeature | |||||||
|     })); |     })); | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="container"> |       <ha-control-select | ||||||
|         <ha-control-select |         .options=${options} | ||||||
|           .options=${options} |         .value=${this._currentState} | ||||||
|           .value=${this._currentState} |         @value-changed=${this._valueChanged} | ||||||
|           @value-changed=${this._valueChanged} |         hide-label | ||||||
|           hide-label |         .ariaLabel=${this.hass.localize("ui.card.humidifier.state")} | ||||||
|           .ariaLabel=${this.hass.localize("ui.card.humidifier.state")} |         style=${styleMap({ | ||||||
|           style=${styleMap({ |           "--control-select-color": color, | ||||||
|             "--control-select-color": color, |         })} | ||||||
|           })} |         .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|           .disabled=${this.stateObj!.state === UNAVAILABLE} |       > | ||||||
|         > |       </ha-control-select> | ||||||
|         </ha-control-select> |  | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -121,10 +119,6 @@ class HuiHumidifierToggleCardFeature | |||||||
|         --control-select-border-radius: 10px; |         --control-select-border-radius: 10px; | ||||||
|         --control-select-button-border-radius: 10px; |         --control-select-button-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -173,7 +173,6 @@ class HuiLawnMowerCommandCardFeature | |||||||
|   static get styles() { |   static get styles() { | ||||||
|     return css` |     return css` | ||||||
|       ha-control-button-group { |       ha-control-button-group { | ||||||
|         margin: 0 12px 12px 12px; |  | ||||||
|         --control-button-group-spacing: 12px; |         --control-button-group-spacing: 12px; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -58,19 +58,17 @@ class HuiLightBrightnessCardFeature | |||||||
|         : undefined; |         : undefined; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="container"> |       <ha-control-slider | ||||||
|         <ha-control-slider |         .value=${position} | ||||||
|           .value=${position} |         min="1" | ||||||
|           min="1" |         max="100" | ||||||
|           max="100" |         .showHandle=${stateActive(this.stateObj)} | ||||||
|           .showHandle=${stateActive(this.stateObj)} |         .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|           .disabled=${this.stateObj!.state === UNAVAILABLE} |         @value-changed=${this._valueChanged} | ||||||
|           @value-changed=${this._valueChanged} |         .label=${this.hass.localize("ui.card.light.brightness")} | ||||||
|           .label=${this.hass.localize("ui.card.light.brightness")} |         unit="%" | ||||||
|           unit="%" |         .locale=${this.hass.locale} | ||||||
|           .locale=${this.hass.locale} |       ></ha-control-slider> | ||||||
|         ></ha-control-slider> |  | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -93,10 +91,6 @@ class HuiLightBrightnessCardFeature | |||||||
|         --control-slider-thickness: 40px; |         --control-slider-thickness: 40px; | ||||||
|         --control-slider-border-radius: 10px; |         --control-slider-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -73,23 +73,21 @@ class HuiLightColorTempCardFeature | |||||||
|     const gradient = this._generateTemperatureGradient(minKelvin!, maxKelvin); |     const gradient = this._generateTemperatureGradient(minKelvin!, maxKelvin); | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="container"> |       <ha-control-slider | ||||||
|         <ha-control-slider |         .value=${position} | ||||||
|           .value=${position} |         mode="cursor" | ||||||
|           mode="cursor" |         .showHandle=${stateActive(this.stateObj)} | ||||||
|           .showHandle=${stateActive(this.stateObj)} |         .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|           .disabled=${this.stateObj!.state === UNAVAILABLE} |         @value-changed=${this._valueChanged} | ||||||
|           @value-changed=${this._valueChanged} |         .label=${this.hass.localize("ui.card.light.color_temperature")} | ||||||
|           .label=${this.hass.localize("ui.card.light.color_temperature")} |         .min=${minKelvin} | ||||||
|           .min=${minKelvin} |         .max=${maxKelvin} | ||||||
|           .max=${maxKelvin} |         style=${styleMap({ | ||||||
|           style=${styleMap({ |           "--gradient": gradient, | ||||||
|             "--gradient": gradient, |         })} | ||||||
|           })} |         .unit=${DOMAIN_ATTRIBUTES_UNITS.light.color_temp_kelvin} | ||||||
|           .unit=${DOMAIN_ATTRIBUTES_UNITS.light.color_temp_kelvin} |         .locale=${this.hass.locale} | ||||||
|           .locale=${this.hass.locale} |       ></ha-control-slider> | ||||||
|         ></ha-control-slider> |  | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -119,10 +117,6 @@ class HuiLightColorTempCardFeature | |||||||
|         --control-slider-thickness: 40px; |         --control-slider-thickness: 40px; | ||||||
|         --control-slider-border-radius: 10px; |         --control-slider-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -90,7 +90,6 @@ class HuiLockCommandsCardFeature | |||||||
|   static get styles(): CSSResultGroup { |   static get styles(): CSSResultGroup { | ||||||
|     return css` |     return css` | ||||||
|       ha-control-button-group { |       ha-control-button-group { | ||||||
|         margin: 0 12px 12px 12px; |  | ||||||
|         --control-button-group-spacing: 12px; |         --control-button-group-spacing: 12px; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -117,7 +117,6 @@ class HuiLockOpenDoorCardFeature | |||||||
|         font-size: 14px; |         font-size: 14px; | ||||||
|       } |       } | ||||||
|       ha-control-button-group { |       ha-control-button-group { | ||||||
|         margin: 0 12px 12px 12px; |  | ||||||
|         --control-button-group-spacing: 12px; |         --control-button-group-spacing: 12px; | ||||||
|       } |       } | ||||||
|       .open-button { |       .open-button { | ||||||
| @@ -136,7 +135,6 @@ class HuiLockOpenDoorCardFeature | |||||||
|         gap: 8px; |         gap: 8px; | ||||||
|         font-weight: 500; |         font-weight: 500; | ||||||
|         color: var(--success-color); |         color: var(--success-color); | ||||||
|         margin: 0 12px 12px 12px; |  | ||||||
|         height: 40px; |         height: 40px; | ||||||
|         text-align: center; |         text-align: center; | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -82,29 +82,27 @@ class HuiNumericInputCardFeature | |||||||
|     const stateObj = this.stateObj; |     const stateObj = this.stateObj; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="container"> |       ${this._config.style === "buttons" | ||||||
|         ${this._config.style === "buttons" |         ? html`<ha-control-number-buttons | ||||||
|           ? html`<ha-control-number-buttons |             value=${stateObj.state} | ||||||
|               value=${stateObj.state} |             min=${stateObj.attributes.min} | ||||||
|               min=${stateObj.attributes.min} |             max=${stateObj.attributes.max} | ||||||
|               max=${stateObj.attributes.max} |             step=${stateObj.attributes.step} | ||||||
|               step=${stateObj.attributes.step} |             @value-changed=${this._setValue} | ||||||
|               @value-changed=${this._setValue} |             .disabled=${isUnavailableState(stateObj.state)} | ||||||
|               .disabled=${isUnavailableState(stateObj.state)} |             .unit=${stateObj.attributes.unit_of_measurement} | ||||||
|               .unit=${stateObj.attributes.unit_of_measurement} |             .locale=${this.hass.locale} | ||||||
|               .locale=${this.hass.locale} |           ></ha-control-number-buttons>` | ||||||
|             ></ha-control-number-buttons>` |         : html`<ha-control-slider | ||||||
|           : html`<ha-control-slider |             value=${stateObj.state} | ||||||
|               value=${stateObj.state} |             min=${stateObj.attributes.min} | ||||||
|               min=${stateObj.attributes.min} |             max=${stateObj.attributes.max} | ||||||
|               max=${stateObj.attributes.max} |             step=${stateObj.attributes.step} | ||||||
|               step=${stateObj.attributes.step} |             @value-changed=${this._setValue} | ||||||
|               @value-changed=${this._setValue} |             .disabled=${isUnavailableState(stateObj.state)} | ||||||
|               .disabled=${isUnavailableState(stateObj.state)} |             .unit=${stateObj.attributes.unit_of_measurement} | ||||||
|               .unit=${stateObj.attributes.unit_of_measurement} |             .locale=${this.hass.locale} | ||||||
|               .locale=${this.hass.locale} |           ></ha-control-slider>`} | ||||||
|             ></ha-control-slider>`} |  | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -120,10 +118,6 @@ class HuiNumericInputCardFeature | |||||||
|         --control-slider-thickness: 40px; |         --control-slider-thickness: 40px; | ||||||
|         --control-slider-border-radius: 10px; |         --control-slider-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -119,27 +119,25 @@ class HuiSelectOptionsCardFeature | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="container"> |       <ha-control-select-menu | ||||||
|         <ha-control-select-menu |         show-arrow | ||||||
|           show-arrow |         hide-label | ||||||
|           hide-label |         .label=${this.hass.localize("ui.card.select.option")} | ||||||
|           .label=${this.hass.localize("ui.card.select.option")} |         .value=${stateObj.state} | ||||||
|           .value=${stateObj.state} |         .disabled=${this.stateObj.state === UNAVAILABLE} | ||||||
|           .disabled=${this.stateObj.state === UNAVAILABLE} |         fixedMenuPosition | ||||||
|           fixedMenuPosition |         naturalMenuWidth | ||||||
|           naturalMenuWidth |         @selected=${this._valueChanged} | ||||||
|           @selected=${this._valueChanged} |         @closed=${stopPropagation} | ||||||
|           @closed=${stopPropagation} |       > | ||||||
|         > |         ${options.map( | ||||||
|           ${options.map( |           (option) => html` | ||||||
|             (option) => html` |             <ha-list-item .value=${option}> | ||||||
|               <ha-list-item .value=${option}> |               ${this.hass!.formatEntityState(stateObj, option)} | ||||||
|                 ${this.hass!.formatEntityState(stateObj, option)} |             </ha-list-item> | ||||||
|               </ha-list-item> |           ` | ||||||
|             ` |         )} | ||||||
|           )} |       </ha-control-select-menu> | ||||||
|         </ha-control-select-menu> |  | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -153,10 +151,6 @@ class HuiSelectOptionsCardFeature | |||||||
|         display: block; |         display: block; | ||||||
|         width: 100%; |         width: 100%; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -84,22 +84,17 @@ class HuiTargetHumidityCardFeature | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="container"> |       <ha-control-slider | ||||||
|         <ha-control-slider |         .value=${this.stateObj.attributes.humidity} | ||||||
|           .value=${this.stateObj.attributes.humidity} |         .min=${this._min} | ||||||
|           .min=${this._min} |         .max=${this._max} | ||||||
|           .max=${this._max} |         .step=${this._step} | ||||||
|           .step=${this._step} |         .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|           .disabled=${this.stateObj!.state === UNAVAILABLE} |         @value-changed=${this._valueChanged} | ||||||
|           @value-changed=${this._valueChanged} |         .label=${this.hass.formatEntityAttributeName(this.stateObj, "humidity")} | ||||||
|           .label=${this.hass.formatEntityAttributeName( |         unit="%" | ||||||
|             this.stateObj, |         .locale=${this.hass.locale} | ||||||
|             "humidity" |       ></ha-control-slider> | ||||||
|           )} |  | ||||||
|           unit="%" |  | ||||||
|           .locale=${this.hass.locale} |  | ||||||
|         ></ha-control-slider> |  | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -112,10 +107,6 @@ class HuiTargetHumidityCardFeature | |||||||
|         --control-slider-thickness: 40px; |         --control-slider-thickness: 40px; | ||||||
|         --control-slider-border-radius: 10px; |         --control-slider-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -285,7 +285,6 @@ class HuiTargetTemperatureCardFeature | |||||||
|   static get styles() { |   static get styles() { | ||||||
|     return css` |     return css` | ||||||
|       ha-control-button-group { |       ha-control-button-group { | ||||||
|         margin: 0 12px 12px 12px; |  | ||||||
|         --control-button-group-spacing: 12px; |         --control-button-group-spacing: 12px; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -151,7 +151,6 @@ class HuiUpdateActionsCardFeature | |||||||
|   static get styles() { |   static get styles() { | ||||||
|     return css` |     return css` | ||||||
|       ha-control-button-group { |       ha-control-button-group { | ||||||
|         margin: 0 12px 12px 12px; |  | ||||||
|         --control-button-group-spacing: 12px; |         --control-button-group-spacing: 12px; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -212,7 +212,6 @@ class HuiVacuumCommandCardFeature | |||||||
|   static get styles() { |   static get styles() { | ||||||
|     return css` |     return css` | ||||||
|       ha-control-button-group { |       ha-control-button-group { | ||||||
|         margin: 0 12px 12px 12px; |  | ||||||
|         --control-button-group-spacing: 12px; |         --control-button-group-spacing: 12px; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -118,20 +118,18 @@ class HuiWaterHeaterOperationModeCardFeature | |||||||
|     })); |     })); | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <div class="container"> |       <ha-control-select | ||||||
|         <ha-control-select |         .options=${options} | ||||||
|           .options=${options} |         .value=${this._currentOperationMode} | ||||||
|           .value=${this._currentOperationMode} |         @value-changed=${this._valueChanged} | ||||||
|           @value-changed=${this._valueChanged} |         hide-label | ||||||
|           hide-label |         .ariaLabel=${this.hass.localize("ui.card.water_heater.mode")} | ||||||
|           .ariaLabel=${this.hass.localize("ui.card.water_heater.mode")} |         style=${styleMap({ | ||||||
|           style=${styleMap({ |           "--control-select-color": color, | ||||||
|             "--control-select-color": color, |         })} | ||||||
|           })} |         .disabled=${this.stateObj!.state === UNAVAILABLE} | ||||||
|           .disabled=${this.stateObj!.state === UNAVAILABLE} |       > | ||||||
|         > |       </ha-control-select> | ||||||
|         </ha-control-select> |  | ||||||
|       </div> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -145,13 +143,8 @@ class HuiWaterHeaterOperationModeCardFeature | |||||||
|         --control-select-button-border-radius: 10px; |         --control-select-button-border-radius: 10px; | ||||||
|       } |       } | ||||||
|       ha-control-button-group { |       ha-control-button-group { | ||||||
|         margin: 0 12px 12px 12px; |  | ||||||
|         --control-button-group-spacing: 12px; |         --control-button-group-spacing: 12px; | ||||||
|       } |       } | ||||||
|       .container { |  | ||||||
|         padding: 0 12px 12px 12px; |  | ||||||
|         width: auto; |  | ||||||
|       } |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -126,9 +126,19 @@ export class HuiTileCard extends LitElement implements LovelaceCard { | |||||||
|       grid_columns: 2, |       grid_columns: 2, | ||||||
|       grid_rows: 1, |       grid_rows: 1, | ||||||
|     }; |     }; | ||||||
|  |     const featureLayout = this._config?.feature_layout || "vertical"; | ||||||
|  |  | ||||||
|     if (this._config?.features?.length) { |     if (this._config?.features?.length) { | ||||||
|       options.grid_rows += Math.ceil((this._config.features.length * 2) / 3); |       if (featureLayout === "compact") { | ||||||
|  |         options.grid_rows += 1; | ||||||
|  |       } else if (featureLayout === "horizontal") { | ||||||
|  |         options.grid_rows += 1; | ||||||
|  |         options.grid_columns = 4; | ||||||
|  |       } else { | ||||||
|  |         options.grid_rows += Math.ceil((this._config.features.length * 2) / 3); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (this._config?.vertical) { |     if (this._config?.vertical) { | ||||||
|       options.grid_rows++; |       options.grid_rows++; | ||||||
|     } |     } | ||||||
| @@ -428,6 +438,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard { | |||||||
|                 .stateObj=${stateObj} |                 .stateObj=${stateObj} | ||||||
|                 .color=${this._config.color} |                 .color=${this._config.color} | ||||||
|                 .features=${this._config.features} |                 .features=${this._config.features} | ||||||
|  |                 .layout=${this._config.feature_layout} | ||||||
|               ></hui-card-features> |               ></hui-card-features> | ||||||
|             ` |             ` | ||||||
|           : nothing} |           : nothing} | ||||||
|   | |||||||
| @@ -24,6 +24,8 @@ export type AlarmPanelCardConfigState = | |||||||
|   | "arm_vacation" |   | "arm_vacation" | ||||||
|   | "arm_custom_bypass"; |   | "arm_custom_bypass"; | ||||||
|  |  | ||||||
|  | export type LovelaceCardFeatureLayout = "vertical" | "horizontal" | "compact"; | ||||||
|  |  | ||||||
| export interface AlarmPanelCardConfig extends LovelaceCardConfig { | export interface AlarmPanelCardConfig extends LovelaceCardConfig { | ||||||
|   entity: string; |   entity: string; | ||||||
|   name?: string; |   name?: string; | ||||||
| @@ -506,4 +508,5 @@ export interface TileCardConfig extends LovelaceCardConfig { | |||||||
|   double_tap_action?: ActionConfig; |   double_tap_action?: ActionConfig; | ||||||
|   icon_tap_action?: ActionConfig; |   icon_tap_action?: ActionConfig; | ||||||
|   features?: LovelaceCardFeatureConfig[]; |   features?: LovelaceCardFeatureConfig[]; | ||||||
|  |   feature_layout?: LovelaceCardFeatureLayout; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,10 +3,16 @@ import { HassEntity } from "home-assistant-js-websocket"; | |||||||
| import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; | import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| import { repeat } from "lit/directives/repeat"; | import { repeat } from "lit/directives/repeat"; | ||||||
|  | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | import { fireEvent } from "../../../../common/dom/fire_event"; | ||||||
| import { stopPropagation } from "../../../../common/dom/stop_propagation"; | import { stopPropagation } from "../../../../common/dom/stop_propagation"; | ||||||
|  | import { LocalizeFunc } from "../../../../common/translations/localize"; | ||||||
| import "../../../../components/entity/ha-entity-picker"; | import "../../../../components/entity/ha-entity-picker"; | ||||||
| import "../../../../components/ha-button"; | import "../../../../components/ha-button"; | ||||||
|  | import { | ||||||
|  |   HaFormSchema, | ||||||
|  |   SchemaUnion, | ||||||
|  | } from "../../../../components/ha-form/types"; | ||||||
| import "../../../../components/ha-icon-button"; | import "../../../../components/ha-icon-button"; | ||||||
| import "../../../../components/ha-list-item"; | import "../../../../components/ha-list-item"; | ||||||
| import "../../../../components/ha-sortable"; | import "../../../../components/ha-sortable"; | ||||||
| @@ -45,7 +51,9 @@ import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update | |||||||
| import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature"; | import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature"; | ||||||
| import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature"; | import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature"; | ||||||
| import { LovelaceCardFeatureConfig } from "../../card-features/types"; | import { LovelaceCardFeatureConfig } from "../../card-features/types"; | ||||||
|  | import { LovelaceCardFeatureLayout } from "../../cards/types"; | ||||||
| import { getCardFeatureElementClass } from "../../create-element/create-card-feature-element"; | import { getCardFeatureElementClass } from "../../create-element/create-card-feature-element"; | ||||||
|  | import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter"; | ||||||
|  |  | ||||||
| export type FeatureType = LovelaceCardFeatureConfig["type"]; | export type FeatureType = LovelaceCardFeatureConfig["type"]; | ||||||
| type SupportsFeature = (stateObj: HassEntity) => boolean; | type SupportsFeature = (stateObj: HassEntity) => boolean; | ||||||
| @@ -142,6 +150,9 @@ declare global { | |||||||
|     "features-changed": { |     "features-changed": { | ||||||
|       features: LovelaceCardFeatureConfig[]; |       features: LovelaceCardFeatureConfig[]; | ||||||
|     }; |     }; | ||||||
|  |     "layout-changed": { | ||||||
|  |       layout: LovelaceCardFeatureLayout; | ||||||
|  |     }; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -154,6 +165,9 @@ export class HuiCardFeaturesEditor extends LitElement { | |||||||
|   @property({ attribute: false }) |   @property({ attribute: false }) | ||||||
|   public features?: LovelaceCardFeatureConfig[]; |   public features?: LovelaceCardFeatureConfig[]; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) | ||||||
|  |   public layout?: LovelaceCardFeatureLayout; | ||||||
|  |  | ||||||
|   @property({ attribute: false }) |   @property({ attribute: false }) | ||||||
|   public featuresTypes?: FeatureType[]; |   public featuresTypes?: FeatureType[]; | ||||||
|  |  | ||||||
| @@ -162,6 +176,37 @@ export class HuiCardFeaturesEditor extends LitElement { | |||||||
|  |  | ||||||
|   private _featuresKeys = new WeakMap<LovelaceCardFeatureConfig, string>(); |   private _featuresKeys = new WeakMap<LovelaceCardFeatureConfig, string>(); | ||||||
|  |  | ||||||
|  |   private _optionsSchema = memoizeOne( | ||||||
|  |     (_localize: LocalizeFunc) => | ||||||
|  |       [ | ||||||
|  |         { | ||||||
|  |           name: "type", | ||||||
|  |           selector: { | ||||||
|  |             select: { | ||||||
|  |               mode: "dropdown", | ||||||
|  |               options: ["vertical", "horizontal", "compact"].map((layout) => ({ | ||||||
|  |                 label: capitalizeFirstLetter(layout), | ||||||
|  |                 value: layout, | ||||||
|  |               })), | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       ] as const satisfies readonly HaFormSchema[] | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   private _computeLabelCallback = ( | ||||||
|  |     schema: SchemaUnion<ReturnType<typeof this._optionsSchema>> | ||||||
|  |   ) => { | ||||||
|  |     switch (schema.name) { | ||||||
|  |       case "type": | ||||||
|  |         return "Layout"; | ||||||
|  |       default: | ||||||
|  |         return this.hass!.localize( | ||||||
|  |           `ui.panel.lovelace.editor.card.generic.${schema.name}` | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   private _supportsFeatureType(type: string): boolean { |   private _supportsFeatureType(type: string): boolean { | ||||||
|     if (!this.stateObj) return false; |     if (!this.stateObj) return false; | ||||||
|  |  | ||||||
| @@ -235,6 +280,14 @@ export class HuiCardFeaturesEditor extends LitElement { | |||||||
|       isCustomType(type) |       isCustomType(type) | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     const schema = this._optionsSchema(this.hass.localize); | ||||||
|  |  | ||||||
|  |     const data = { ...this.layout }; | ||||||
|  |  | ||||||
|  |     if (!data.type) { | ||||||
|  |       data.type = "vertical"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-expansion-panel outlined> |       <ha-expansion-panel outlined> | ||||||
|         <h3 slot="header"> |         <h3 slot="header"> | ||||||
| @@ -251,6 +304,17 @@ export class HuiCardFeaturesEditor extends LitElement { | |||||||
|                 </ha-alert> |                 </ha-alert> | ||||||
|               ` |               ` | ||||||
|             : nothing} |             : nothing} | ||||||
|  |           ${supportedFeaturesType.length > 0 | ||||||
|  |             ? html` | ||||||
|  |                 <ha-form | ||||||
|  |                   .hass=${this.hass} | ||||||
|  |                   .data=${data} | ||||||
|  |                   .schema=${schema} | ||||||
|  |                   .computeLabel=${this._computeLabelCallback} | ||||||
|  |                   @value-changed=${this._layoutChanged} | ||||||
|  |                 ></ha-form> | ||||||
|  |               ` | ||||||
|  |             : nothing} | ||||||
|           <ha-sortable |           <ha-sortable | ||||||
|             handle-selector=".handle" |             handle-selector=".handle" | ||||||
|             @item-moved=${this._featureMoved} |             @item-moved=${this._featureMoved} | ||||||
| @@ -385,11 +449,17 @@ export class HuiCardFeaturesEditor extends LitElement { | |||||||
|  |  | ||||||
|   private _removeFeature(ev: CustomEvent): void { |   private _removeFeature(ev: CustomEvent): void { | ||||||
|     const index = (ev.currentTarget as any).index; |     const index = (ev.currentTarget as any).index; | ||||||
|     const newfeatures = this.features!.concat(); |     const newFeatures = this.features!.concat(); | ||||||
|  |  | ||||||
|     newfeatures.splice(index, 1); |     newFeatures.splice(index, 1); | ||||||
|  |  | ||||||
|     fireEvent(this, "features-changed", { features: newfeatures }); |     fireEvent(this, "features-changed", { features: newFeatures }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _layoutChanged(ev: CustomEvent): void { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     const layout = ev.detail.value; | ||||||
|  |     fireEvent(this, "layout-changed", { layout }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _editFeature(ev: CustomEvent): void { |   private _editFeature(ev: CustomEvent): void { | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ import { LitElement, css, html, nothing } from "lit"; | |||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { | import { | ||||||
|   any, |  | ||||||
|   array, |   array, | ||||||
|   assert, |   assert, | ||||||
|   assign, |   assign, | ||||||
| @@ -29,11 +28,15 @@ import { | |||||||
|   LovelaceCardFeatureContext, |   LovelaceCardFeatureContext, | ||||||
| } from "../../card-features/types"; | } from "../../card-features/types"; | ||||||
| import { getEntityDefaultTileIconAction } from "../../cards/hui-tile-card"; | import { getEntityDefaultTileIconAction } from "../../cards/hui-tile-card"; | ||||||
| import type { TileCardConfig } from "../../cards/types"; | import type { | ||||||
|  |   LovelaceCardFeatureLayout, | ||||||
|  |   TileCardConfig, | ||||||
|  | } from "../../cards/types"; | ||||||
| import type { LovelaceCardEditor } from "../../types"; | import type { LovelaceCardEditor } from "../../types"; | ||||||
| import "../hui-sub-element-editor"; | import "../hui-sub-element-editor"; | ||||||
| import { actionConfigStruct } from "../structs/action-struct"; | import { actionConfigStruct } from "../structs/action-struct"; | ||||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||||
|  | import { cardFeatureConfig } from "../structs/card-feature-struct"; | ||||||
| import { EditSubElementEvent, SubElementEditorConfig } from "../types"; | import { EditSubElementEvent, SubElementEditorConfig } from "../types"; | ||||||
| import { configElementStyle } from "./config-elements-style"; | import { configElementStyle } from "./config-elements-style"; | ||||||
| import "./hui-card-features-editor"; | import "./hui-card-features-editor"; | ||||||
| @@ -93,6 +96,7 @@ const HIDDEN_ATTRIBUTES = [ | |||||||
|  |  | ||||||
| const cardConfigStruct = assign( | const cardConfigStruct = assign( | ||||||
|   baseLovelaceCardConfig, |   baseLovelaceCardConfig, | ||||||
|  |   cardFeatureConfig, | ||||||
|   object({ |   object({ | ||||||
|     entity: optional(string()), |     entity: optional(string()), | ||||||
|     name: optional(string()), |     name: optional(string()), | ||||||
| @@ -104,7 +108,6 @@ const cardConfigStruct = assign( | |||||||
|     vertical: optional(boolean()), |     vertical: optional(boolean()), | ||||||
|     tap_action: optional(actionConfigStruct), |     tap_action: optional(actionConfigStruct), | ||||||
|     icon_tap_action: optional(actionConfigStruct), |     icon_tap_action: optional(actionConfigStruct), | ||||||
|     features: optional(array(any())), |  | ||||||
|   }) |   }) | ||||||
| ); | ); | ||||||
|  |  | ||||||
| @@ -299,6 +302,8 @@ export class HuiTileCardEditor | |||||||
|         .stateObj=${stateObj} |         .stateObj=${stateObj} | ||||||
|         .features=${this._config!.features ?? []} |         .features=${this._config!.features ?? []} | ||||||
|         @features-changed=${this._featuresChanged} |         @features-changed=${this._featuresChanged} | ||||||
|  |         .layout=${this._config!.feature_layout} | ||||||
|  |         @layout-changed=${this._layoutChanged} | ||||||
|         @edit-detail-element=${this._editDetailElement} |         @edit-detail-element=${this._editDetailElement} | ||||||
|       ></hui-card-features-editor> |       ></hui-card-features-editor> | ||||||
|     `; |     `; | ||||||
| @@ -351,6 +356,21 @@ export class HuiTileCardEditor | |||||||
|     fireEvent(this, "config-changed", { config }); |     fireEvent(this, "config-changed", { config }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _layoutChanged(ev: CustomEvent) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     if (!this._config || !this.hass) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const layout = ev.detail.layout as LovelaceCardFeatureLayout; | ||||||
|  |     const config: TileCardConfig = { | ||||||
|  |       ...this._config, | ||||||
|  |       feature_layout: layout, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     fireEvent(this, "config-changed", { config }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private subElementChanged(ev: CustomEvent): void { |   private subElementChanged(ev: CustomEvent): void { | ||||||
|     ev.stopPropagation(); |     ev.stopPropagation(); | ||||||
|     if (!this._config || !this.hass) { |     if (!this._config || !this.hass) { | ||||||
|   | |||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | import { any, array, enums, object, optional } from "superstruct"; | ||||||
|  |  | ||||||
|  | export const cardFeatureConfig = object({ | ||||||
|  |   features: optional(array(any())), | ||||||
|  |   feature_layout: optional(enums(["vertical", "horizontal", "compact"])), | ||||||
|  | }); | ||||||
		Reference in New Issue
	
	Block a user