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