Use new scaling features of round-slider (#4172)

* Refresh light card.

* Refresh thermostat card

* Fix paddings

* Fix #4175

* Use action handler

* Address review comments

* Lint

* Padding on percentage

* Remove typo
This commit is contained in:
Thomas Lovén 2019-11-19 00:32:23 +01:00 committed by Bram Kragten
parent 258cfddc3f
commit 9f520d7628
4 changed files with 364 additions and 403 deletions

View File

@ -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",

View File

@ -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()}
<ha-card>
${stateObj.state === "unavailable"
? html`
@ -93,36 +95,38 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
@click="${this._handleMoreInfo}"
></paper-icon-button>
<div id="light">
${supportsFeature(stateObj, SUPPORT_BRIGHTNESS)
? html`
<round-slider
min="1"
.value=${brightness}
@value-changing=${this._dragEvent}
@value-changed=${this._setBrightness}
></round-slider>
`
: ""}
<ha-icon
class="light-icon"
data-state="${stateObj.state}"
.icon="${this._config.icon || stateIcon(stateObj)}"
style="${styleMap({
filter: this._computeBrightness(stateObj),
color: this._computeColor(stateObj),
})}"
@click="${this._handleClick}"
></ha-icon>
<div id="controls">
<div id="slider">
${supportsFeature(stateObj, SUPPORT_BRIGHTNESS)
? html`
<round-slider
min="1"
.value=${brightness}
@value-changing=${this._dragEvent}
@value-changed=${this._setBrightness}
></round-slider>
`
: ""}
<ha-icon
class="slider-center"
data-state="${stateObj.state}"
.icon="${this._config.icon || stateIcon(stateObj)}"
style="${styleMap({
filter: this._computeBrightness(stateObj),
color: this._computeColor(stateObj),
})}"
@action="${this._handleClick}"
.actionHandler=${actionHandler()}
tabindex="0"
></ha-icon>
</div>
</div>
<div id="tooltip">
<div id="info">
<div class="brightness" @ha-click="${this._handleClick}">
${brightness} %
</div>
<div class="name">
${this._config.name || computeStateName(stateObj)}
%
</div>
${this._config.name || computeStateName(stateObj)}
</div>
</ha-card>
`;
@ -159,110 +163,10 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
}
}
private renderStyle(): TemplateResult {
return html`
<style>
:host {
display: block;
}
ha-card {
position: relative;
overflow: hidden;
--name-font-size: 1.2rem;
--brightness-font-size: 1.2rem;
--rail-border-color: transparent;
}
#tooltip {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100%;
text-align: center;
}
#light {
margin: auto;
padding-top: 0;
padding-bottom: 32px;
display: flex;
justify-content: center;
align-items: center;
height: 160px;
width: 160px;
}
#light round-slider {
margin: 0 auto;
display: inline-block;
--round-slider-path-color: var(--disabled-text-color);
--round-slider-bar-color: var(--primary-color);
z-index: 20;
}
.light-icon {
position: absolute;
margin: 0 auto;
width: 76px;
height: 76px;
color: var(--paper-item-icon-color, #44739e);
cursor: pointer;
z-index: 20;
}
.light-icon[data-state="on"] {
color: var(--paper-item-icon-active-color, #fdd835);
}
.light-icon[data-state="unavailable"] {
color: var(--state-icon-unavailable-color);
}
.name {
position: absolute;
font-size: var(--name-font-size);
bottom: 16px;
box-sizing: border-box;
text-align: center;
width: 100%;
padding: 0 16px;
}
.brightness {
font-size: var(--brightness-font-size);
position: absolute;
margin: 0 auto;
top: 135px;
left: 50%;
transform: translate(-50%);
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;
}
.show_brightness {
opacity: 1;
}
.more-info {
position: absolute;
cursor: pointer;
top: 0;
right: 0;
z-index: 25;
color: var(--secondary-text-color);
}
</style>
`;
}
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 {

View File

@ -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`
<round-slider disabled="true"></round-slider>
`
: html`
<round-slider
.value=${targetTemp}
.low=${stateObj.attributes.target_temp_low}
.high=${stateObj.attributes.target_temp_high}
.min=${stateObj.attributes.min_temp}
.max=${stateObj.attributes.max_temp}
.step=${this._stepSize}
@value-changing=${this._dragEvent}
@value-changed=${this._setTemperature}
></round-slider>
`;
const currentTemperature = stateObj.attributes.current_temperature
? svg`
<svg viewBox="0 0 40 20">
<text
x=${23 - (stateObj.attributes.current_temperature < 0 ? 2 : 0)}
y="75%"
text-anchor="middle"
style="font-size: 15px;"
>
${stateObj.attributes.current_temperature}
<tspan dx="-4" dy="-7" style="font-size: 5px;">
${this.hass.config.unit_system.temperature}
</tspan>
</text>
</svg>
`
: "";
const setValues = svg`
<svg id="set-values">
<g>
<text text-anchor="middle" style="font-size: 20px;" class="set-value">
${
!this._setTemp
? ""
: Array.isArray(this._setTemp)
? svg`
${this._setTemp[0].toFixed(1)} -
${this._setTemp[1].toFixed(1)}
`
: svg`
${this._setTemp.toFixed(1)}
`
}
</text>
<text
dy="22"
text-anchor="middle"
style="font-size: 16px"
id="set-mode"
>
${
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}
`
: ""
}
</text>
</g>
</svg>
`;
return html`
<ha-card
class=${classMap({
[mode]: true,
large: this._large!,
medium: this._medium!,
small: this._small!,
longName: name.length > 10,
})}
>
<div id="root">
<paper-icon-button
icon="hass:dots-vertical"
class="more-info"
@click=${this._handleMoreInfo}
></paper-icon-button>
<div id="thermostat">
${stateObj.state === "unavailable"
? html`
<round-slider
.radius=${this._radius}
disabled="true"
></round-slider>
`
: stateObj.attributes.target_temp_low &&
stateObj.attributes.target_temp_high
? html`
<round-slider
.radius=${this._radius}
.low=${stateObj.attributes.target_temp_low}
.high=${stateObj.attributes.target_temp_high}
.min=${stateObj.attributes.min_temp}
.max=${stateObj.attributes.max_temp}
.step=${this._stepSize}
@value-changing=${this._dragEvent}
@value-changed=${this._setTemperature}
></round-slider>
`
: html`
<round-slider
.radius=${this._radius}
.value=${stateObj.attributes.temperature !== null &&
Number.isFinite(Number(stateObj.attributes.temperature))
? stateObj.attributes.temperature
: stateObj.attributes.min_temp}
.step=${this._stepSize}
.min=${stateObj.attributes.min_temp}
.max=${stateObj.attributes.max_temp}
@value-changing=${this._dragEvent}
@value-changed=${this._setTemperature}
></round-slider>
`}
</div>
<div id="tooltip">
<div class="title">${name}</div>
<div class="current-temperature">
<span class="current-temperature-text">
${stateObj.attributes.current_temperature}
${stateObj.attributes.current_temperature
? html`
<span class="uom"
>${this.hass.config.unit_system.temperature}</span
>
`
: ""}
</span>
</div>
<div class="climate-info">
<div id="set-temperature">
${!this._setTemp
? ""
: Array.isArray(this._setTemp)
? html`
${this._setTemp[0].toFixed(1)} -
${this._setTemp[1].toFixed(1)}
`
: html`
${this._setTemp.toFixed(1)}
`}
</div>
<div class="current-mode">
${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}
`
: ""}
</div>
<div class="modes">
${(stateObj.attributes.hvac_modes || [])
.concat()
.sort(compareClimateHvacModes)
.map((modeItem) => this._renderIcon(modeItem, mode))}
<paper-icon-button
icon="hass:dots-vertical"
class="more-info"
@click=${this._handleMoreInfo}
></paper-icon-button>
<div id="controls">
<div id="slider">
${slider}
<div id="slider-center">
<div id="temperature">
${currentTemperature} ${setValues}
</div>
</div>
</div>
</div>
<div id="info">
<div id="modes">
${(stateObj.attributes.hvac_modes || [])
.concat()
.sort(compareClimateHvacModes)
.map((modeItem) => this._renderIcon(modeItem, mode))}
</div>
${name}
</div>
</ha-card>
`;
}
@ -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<void> {
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"
></ha-icon>
`;
}
@ -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);
}
`;
}

View File

@ -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"