diff --git a/package.json b/package.json index 1ef24fb8f0..b7bf844e38 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@polymer/paper-toast": "^3.0.1", "@polymer/paper-tooltip": "^3.0.1", "@polymer/polymer": "3.1.0", - "@thomasloven/round-slider": "^0.2.2", + "@thomasloven/round-slider": "0.3.5", "@vaadin/vaadin-combo-box": "^4.2.8", "@vaadin/vaadin-date-picker": "^3.3.3", "@webcomponents/shadycss": "^1.9.0", diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts index 5aa4606197..541d04908c 100644 --- a/src/panels/lovelace/cards/hui-light-card.ts +++ b/src/panels/lovelace/cards/hui-light-card.ts @@ -5,6 +5,8 @@ import { TemplateResult, property, customElement, + css, + CSSResult, } from "lit-element"; import "@polymer/paper-icon-button/paper-icon-button"; import "@thomasloven/round-slider"; @@ -27,6 +29,7 @@ import { toggleEntity } from "../common/entity/toggle-entity"; import { LightCardConfig } from "./types"; import { supportsFeature } from "../../../common/entity/supports-feature"; import { SUPPORT_BRIGHTNESS } from "../../../data/light"; +import { actionHandler } from "../common/directives/action-handler-directive"; @customElement("hui-light-card") export class HuiLightCard extends LitElement implements LovelaceCard { @@ -78,7 +81,6 @@ export class HuiLightCard extends LitElement implements LovelaceCard { } return html` - ${this.renderStyle()} ${stateObj.state === "unavailable" ? html` @@ -93,36 +95,38 @@ export class HuiLightCard extends LitElement implements LovelaceCard { @click="${this._handleMoreInfo}" > -
- ${supportsFeature(stateObj, SUPPORT_BRIGHTNESS) - ? html` - - ` - : ""} - +
+
+ ${supportsFeature(stateObj, SUPPORT_BRIGHTNESS) + ? html` + + ` + : ""} + +
-
+
- ${brightness} % -
-
- ${this._config.name || computeStateName(stateObj)} + %
+ ${this._config.name || computeStateName(stateObj)}
`; @@ -159,110 +163,10 @@ export class HuiLightCard extends LitElement implements LovelaceCard { } } - private renderStyle(): TemplateResult { - return html` - - `; - } - private _dragEvent(e: any): void { - this.shadowRoot!.querySelector(".brightness")!.innerHTML = - e.detail.value + "%"; + this.shadowRoot!.querySelector(".brightness")!.innerHTML = `${ + e.detail.value + } %`; this._showBrightness(); this._hideBrightness(); } @@ -317,6 +221,103 @@ export class HuiLightCard extends LitElement implements LovelaceCard { entityId: this._config!.entity, }); } + + static get styles(): CSSResult { + return css` + :host { + display: block; + } + + ha-card { + position: relative; + overflow: hidden; + --name-font-size: 1.2rem; + --brightness-font-size: 1.2rem; + } + + .more-info { + position: absolute; + cursor: pointer; + top: 0; + right: 0; + border-radius: 100%; + color: var(--secondary-text-color); + z-index: 25; + } + + #controls { + display: flex; + justify-content: center; + padding: 16px; + position: relative; + } + + #slider { + height: 100%; + width: 100%; + position: relative; + max-width: 200px; + min-width: 100px; + } + + round-slider { + --round-slider-path-color: var(--disabled-text-color); + --round-slider-bar-color: var(--primary-color); + padding-bottom: 10%; + } + + .slider-center { + position: absolute; + width: 70%; + height: 70%; + max-height: calc(100% - 40px); + max-width: calc(100% - 40px); + box-sizing: border-box; + border-radius: 100%; + top: 50%; + left: 50%; + color: var(--paper-item-icon-color, #44739e); + cursor: pointer; + transform: translate(-50%, -50%); + } + .slider-center:focus { + outline: none; + background: var(--divider-color); + } + + .slider-center[data-state="on"] { + color: var(--paper-item-icon-active-color, #fdd835); + } + + .slider-center[data-state="unavailable"] { + color: var(--state-icon-unavailable-color); + } + + #info { + display: flex-vertical; + justify-content: center; + text-align: center; + margin-top: -56px; + padding: 16px; + font-size: var(--name-font-size); + } + + .brightness { + font-size: var(--brightness-font-size); + opacity: 0; + transition: opacity 0.5s ease-in-out; + -moz-transition: opacity 0.5s ease-in-out; + -webkit-transition: opacity 0.5s ease-in-out; + cursor: pointer; + pointer-events: none; + padding-left: 0.5em; + } + + .show_brightness { + opacity: 1; + } + `; + } } declare global { diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts index 631fd5b1b7..4f1a7cdfbf 100644 --- a/src/panels/lovelace/cards/hui-thermostat-card.ts +++ b/src/panels/lovelace/cards/hui-thermostat-card.ts @@ -7,6 +7,7 @@ import { property, css, CSSResult, + svg, } from "lit-element"; import { classMap } from "lit-html/directives/class-map"; import "@polymer/paper-icon-button/paper-icon-button"; @@ -32,6 +33,7 @@ import { CLIMATE_PRESET_NONE, } from "../../../data/climate"; import { HassEntity } from "home-assistant-js-websocket"; +import { actionHandler } from "../common/directives/action-handler-directive"; const modeIcons: { [mode in HvacMode]: string } = { auto: "hass:calendar-repeat", @@ -56,15 +58,8 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { @property() public hass?: HomeAssistant; @property() private _config?: ThermostatCardConfig; - @property() private _loaded?: boolean; @property() private _setTemp?: number | number[]; - private _updated?: boolean; - private _large?: boolean; - private _medium?: boolean; - private _small?: boolean; - private _radius?: number; - public getCardSize(): number { return 4; } @@ -79,9 +74,11 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { public connectedCallback(): void { super.connectedCallback(); - if (this._updated && !this._loaded) { - this._initialLoad(); - } + this.rescale_svg(); + } + + protected firstUpdated(): void { + this.rescale_svg(); } protected render(): TemplateResult | void { @@ -106,120 +103,129 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { const name = this._config!.name || computeStateName(this.hass!.states[this._config!.entity]); + const targetTemp = + stateObj.attributes.temperature !== null && + Number.isFinite(Number(stateObj.attributes.temperature)) + ? stateObj.attributes.temperature + : stateObj.attributes.min_temp; - if (!this._radius || this._radius === 0) { - this._radius = 100; - } + const slider = + stateObj.state === "unvailable" + ? html` + + ` + : html` + + `; + + const currentTemperature = stateObj.attributes.current_temperature + ? svg` + + + ${stateObj.attributes.current_temperature} + + ${this.hass.config.unit_system.temperature} + + + + ` + : ""; + + const setValues = svg` + + + + ${ + !this._setTemp + ? "" + : Array.isArray(this._setTemp) + ? svg` + ${this._setTemp[0].toFixed(1)} - + ${this._setTemp[1].toFixed(1)} + ` + : svg` + ${this._setTemp.toFixed(1)} + ` + } + + + ${ + stateObj.attributes.hvac_action + ? this.hass!.localize( + `state_attributes.climate.hvac_action.${ + stateObj.attributes.hvac_action + }` + ) + : this.hass!.localize(`state.climate.${stateObj.state}`) + } + ${ + stateObj.attributes.preset_mode && + stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE + ? html` + - + ${this.hass!.localize( + `state_attributes.climate.preset_mode.${ + stateObj.attributes.preset_mode + }` + ) || stateObj.attributes.preset_mode} + ` + : "" + } + + + + `; return html` 10, })} > -
- -
- ${stateObj.state === "unavailable" - ? html` - - ` - : stateObj.attributes.target_temp_low && - stateObj.attributes.target_temp_high - ? html` - - ` - : html` - - `} -
-
-
${name}
-
- - ${stateObj.attributes.current_temperature} - ${stateObj.attributes.current_temperature - ? html` - ${this.hass.config.unit_system.temperature} - ` - : ""} - -
-
-
- ${!this._setTemp - ? "" - : Array.isArray(this._setTemp) - ? html` - ${this._setTemp[0].toFixed(1)} - - ${this._setTemp[1].toFixed(1)} - ` - : html` - ${this._setTemp.toFixed(1)} - `} -
-
- ${stateObj.attributes.hvac_action - ? this.hass!.localize( - `state_attributes.climate.hvac_action.${ - stateObj.attributes.hvac_action - }` - ) - : this.hass!.localize(`state.climate.${stateObj.state}`)} - ${stateObj.attributes.preset_mode && - stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE - ? html` - - - ${this.hass!.localize( - `state_attributes.climate.preset_mode.${ - stateObj.attributes.preset_mode - }` - ) || stateObj.attributes.preset_mode} - ` - : ""} -
-
- ${(stateObj.attributes.hvac_modes || []) - .concat() - .sort(compareClimateHvacModes) - .map((modeItem) => this._renderIcon(modeItem, mode))} + + +
+
+ ${slider} +
+
+ ${currentTemperature} ${setValues}
+
+
+ ${(stateObj.attributes.hvac_modes || []) + .concat() + .sort(compareClimateHvacModes) + .map((modeItem) => this._renderIcon(modeItem, mode))} +
+ ${name} +
`; } @@ -249,30 +255,31 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { } this._setTemp = this._getSetTemp(this.hass!.states[this._config!.entity]); + this.rescale_svg(); } - protected firstUpdated(): void { - this._updated = true; - if (this.isConnected && !this._loaded) { - this._initialLoad(); + private rescale_svg() { + // Set the viewbox of the SVG containing the set temperature to perfectly + // fit the text + // That way it will auto-scale correctly + // This is not done to the SVG containing the current temperature, because + // it should not be centered on the text, but only on the value + if (this.shadowRoot && this.shadowRoot.querySelector("ha-card")) { + (this.shadowRoot.querySelector( + "ha-card" + ) as LitElement).updateComplete.then(() => { + const svgRoot = this.shadowRoot!.querySelector("#set-values"); + const box = svgRoot!.querySelector("g")!.getBBox(); + svgRoot!.setAttribute( + "viewBox", + `${box!.x} ${box!.y} ${box!.width} ${box!.height}` + ); + svgRoot!.setAttribute("width", `${box!.width}`); + svgRoot!.setAttribute("height", `${box!.height}`); + }); } } - private async _initialLoad(): Promise { - this._large = this._medium = this._small = false; - this._radius = this.clientWidth / 3.9; - - if (this.clientWidth > 450) { - this._large = true; - } else if (this.clientWidth < 350) { - this._small = true; - } else { - this._medium = true; - } - - this._loaded = true; - } - private get _stepSize(): number { const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity; @@ -344,7 +351,9 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { class="${classMap({ "selected-icon": currentMode === mode })}" .mode="${mode}" .icon="${modeIcons[mode]}" - @click="${this._handleModeClick}" + @action="${this._handleModeClick}" + .actionHandler=${actionHandler()} + tabindex="0" > `; } @@ -367,8 +376,12 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { :host { display: block; } + ha-card { + position: relative; overflow: hidden; + --name-font-size: 1.2rem; + --brightness-font-size: 1.2rem; --rail-border-color: transparent; --auto-color: green; --eco-color: springgreen; @@ -381,10 +394,6 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { --idle-color: #8a8a8a; --unknown-color: #bac; } - #root { - position: relative; - overflow: hidden; - } .auto, .heat_cool { --mode-color: var(--auto-color); @@ -416,144 +425,95 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { .unknown-mode { --mode-color: var(--unknown-color); } - .no-title { - --title-position-top: 33% !important; - } - .large { - --thermostat-padding-top: 32px; - --thermostat-margin-bottom: 32px; - --title-font-size: 28px; - --title-position-top: 25%; - --climate-info-position-top: 80%; - --set-temperature-font-size: 25px; - --current-temperature-font-size: 71px; - --current-temperature-position-top: 10%; - --current-temperature-text-padding-left: 15px; - --uom-font-size: 20px; - --uom-margin-left: -18px; - --current-mode-font-size: 18px; - --current-mod-margin-top: 6px; - --current-mod-margin-bottom: 12px; - --set-temperature-margin-bottom: -5px; - } - .medium { - --thermostat-padding-top: 20px; - --thermostat-margin-bottom: 20px; - --title-font-size: 23px; - --title-position-top: 27%; - --climate-info-position-top: 84%; - --set-temperature-font-size: 20px; - --current-temperature-font-size: 65px; - --current-temperature-position-top: 10%; - --current-temperature-text-padding-left: 15px; - --uom-font-size: 18px; - --uom-margin-left: -16px; - --current-mode-font-size: 16px; - --current-mod-margin-top: 4px; - --current-mod-margin-bottom: 4px; - --set-temperature-margin-bottom: -5px; - } - .small { - --thermostat-padding-top: 15px; - --thermostat-margin-bottom: 15px; - --title-font-size: 18px; - --title-position-top: 28%; - --climate-info-position-top: 78%; - --set-temperature-font-size: 16px; - --current-temperature-font-size: 55px; - --current-temperature-position-top: 5%; - --current-temperature-text-padding-left: 16px; - --uom-font-size: 16px; - --uom-margin-left: -14px; - --current-mode-font-size: 14px; - --current-mod-margin-top: 2px; - --current-mod-margin-bottom: 4px; - --set-temperature-margin-bottom: 0px; - } - .longName { - --title-font-size: 18px; - } - #thermostat { - margin: 0 auto var(--thermostat-margin-bottom); - padding-top: var(--thermostat-padding-top); - padding-bottom: 32px; - display: flex; - justify-content: center; - align-items: center; - } - #thermostat round-slider { - margin: 0 auto; - display: inline-block; - --round-slider-path-color: var(--disabled-text-color); - --round-slider-bar-color: var(--mode-color); - z-index: 20; - } - #tooltip { - position: absolute; - top: 0; - left: 0; - right: 0; - height: 100%; - text-align: center; - z-index: 15; - color: var(--primary-text-color); - } - #set-temperature { - font-size: var(--set-temperature-font-size); - margin-bottom: var(--set-temperature-margin-bottom); - min-height: 1.2em; - } - .title { - font-size: var(--title-font-size); - position: absolute; - top: var(--title-position-top); - left: 50%; - transform: translate(-50%, -50%); - } - .climate-info { - position: absolute; - top: var(--climate-info-position-top); - left: 50%; - transform: translate(-50%, -50%); - width: 100%; - } - .current-mode { - font-size: var(--current-mode-font-size); - color: var(--secondary-text-color); - margin-top: var(--current-mod-margin-top); - margin-bottom: var(--current-mod-margin-bottom); - } - .modes ha-icon { - color: var(--disabled-text-color); - cursor: pointer; - display: inline-block; - margin: 0 10px; - } - .modes ha-icon.selected-icon { - color: var(--mode-color); - } - .current-temperature { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: var(--current-temperature-font-size); - } - .current-temperature-text { - padding-left: var(--current-temperature-text-padding-left); - } - .uom { - font-size: var(--uom-font-size); - vertical-align: top; - margin-left: var(--uom-margin-left); - } + .more-info { position: absolute; cursor: pointer; top: 0; right: 0; - z-index: 25; + border-radius: 100%; color: var(--secondary-text-color); + z-index: 25; + } + + #controls { + display: flex; + justify-content: center; + padding: 16px; + position: relative; + } + + #slider { + height: 100%; + width: 100%; + position: relative; + max-width: 300px; + min-width: 100px; + } + + round-slider { + --round-slider-path-color: var(--disabled-text-color); + --round-slider-bar-color: var(--mode-color); + padding-bottom: 10%; + } + + #slider-center { + position: absolute; + width: calc(100% - 40px); + height: calc(100% - 40px); + box-sizing: border-box; + border-radius: 100%; + left: 20px; + top: 20px; + text-align: center; + overflow-wrap: break-word; + pointer-events: none; + } + + #temperature { + position: absolute; + transform: translate(-50%, -50%); + width: 100%; + height: 50%; + top: 45%; + left: 50%; + } + + #set-values { + max-width: 80%; + transform: translate(0, -50%); + } + + #set-mode { + fill: var(--secondary-text-color); + } + + #info { + display: flex-vertical; + justify-content: center; + text-align: center; + padding: 16px; + margin-top: -60px; + font-size: var(--name-font-size); + } + + #modes { + } + + #modes ha-icon { + color: var(--disabled-text-color); + cursor: pointer; + display: inline-block; + margin: 0 10px; + border-radius: 100%; + } + #modes ha-icon:focus { + outline: none; + background: var(--divider-color); + } + + #modes ha-icon.selected-icon { + color: var(--mode-color); } `; } diff --git a/yarn.lock b/yarn.lock index bd5bb940c6..295d68b55b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1651,10 +1651,10 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== -"@thomasloven/round-slider@^0.2.2": - version "0.2.2" - resolved "https://registry.yarnpkg.com/@thomasloven/round-slider/-/round-slider-0.2.2.tgz#498e2d0b545cefd457c1249e3f90dec9b91dd91b" - integrity sha512-nh4Um3srnTnWaOWkq6sMaXsSgn07MfV/u5rjFZAoSETJrLCBkwWM5IToN3Tqy9SSQk6Zonk1/wpcY5tdACq2lg== +"@thomasloven/round-slider@0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@thomasloven/round-slider/-/round-slider-0.3.5.tgz#95bba92a6aa90953c7999ddf3eaab904aeb041e2" + integrity sha512-BxtZ3AtIt3b00dVjwCYK1N7T6wmuXSqp3yrlh41YPLaCPyCrUY3Za2rR8tL1tuRQCInXpaTG328otgMe30sNSQ== dependencies: lit-element "^2.2.1"