Merge pull request #6510 from home-assistant/dev

This commit is contained in:
Bram Kragten 2020-08-03 16:48:29 +02:00 committed by GitHub
commit bd5115f9aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
230 changed files with 6008 additions and 2669 deletions

View File

@ -1,7 +1,7 @@
{ {
"extends": [ "extends": [
"plugin:@typescript-eslint/recommended",
"airbnb-typescript/base", "airbnb-typescript/base",
"plugin:@typescript-eslint/recommended",
"plugin:wc/recommended", "plugin:wc/recommended",
"plugin:lit/recommended", "plugin:lit/recommended",
"prettier", "prettier",
@ -45,16 +45,16 @@
"func-names": 0, "func-names": 0,
"prefer-arrow-callback": 0, "prefer-arrow-callback": 0,
"no-underscore-dangle": 0, "no-underscore-dangle": 0,
"no-var": 0,
"strict": 0, "strict": 0,
"prefer-spread": 0, "prefer-spread": 0,
"no-plusplus": 0, "no-plusplus": 0,
"no-bitwise": 0, "no-bitwise": 2,
"comma-dangle": 0, "comma-dangle": 0,
"vars-on-top": 0, "vars-on-top": 0,
"no-continue": 0, "no-continue": 0,
"no-param-reassign": 0, "no-param-reassign": 0,
"no-multi-assign": 0, "no-multi-assign": 0,
"no-console": 2,
"radix": 0, "radix": 0,
"no-alert": 0, "no-alert": 0,
"no-return-await": 0, "no-return-await": 0,

View File

@ -147,6 +147,10 @@
"path": "M21.11,18.5C20.97,18.5 20.83,18.44 20.71,18.36C20.37,18.13 20.28,17.68 20.5,17.34C21.18,16.34 21.54,15.16 21.54,13.93C21.54,12.71 21.18,11.53 20.5,10.5C20.28,10.18 20.37,9.73 20.71,9.5C21.04,9.28 21.5,9.37 21.72,9.7C22.56,10.95 23,12.41 23,13.93C23,15.45 22.56,16.91 21.72,18.16C21.58,18.37 21.35,18.5 21.11,18.5M19,17.29C18.88,17.29 18.74,17.25 18.61,17.17C18.28,16.94 18.19,16.5 18.42,16.15C18.86,15.5 19.1,14.73 19.1,13.93C19.1,13.14 18.86,12.37 18.42,11.71C18.19,11.37 18.28,10.92 18.61,10.69C18.95,10.47 19.4,10.55 19.63,10.89C20.24,11.79 20.56,12.84 20.56,13.93C20.56,15 20.24,16.07 19.63,16.97C19.5,17.18 19.25,17.29 19,17.29M14.9,15.73C15.89,15.73 16.7,14.92 16.7,13.93C16.7,13.17 16.22,12.5 15.55,12.25C15.5,12.55 15.43,12.85 15.34,13.14C15.23,13.44 14.95,13.64 14.64,13.64C14.57,13.64 14.5,13.62 14.41,13.6C14.03,13.47 13.82,13.06 13.95,12.67C14.09,12.24 14.17,11.78 14.17,11.32C14.17,8.93 12.22,7 9.82,7C8.1,7 6.56,8 5.87,9.5C6.54,9.7 7.16,10.04 7.66,10.54C7.95,10.83 7.95,11.29 7.66,11.58C7.38,11.86 6.91,11.86 6.63,11.58C6.17,11.12 5.56,10.86 4.9,10.86C3.56,10.86 2.46,11.96 2.46,13.3C2.46,14.64 3.56,15.73 4.9,15.73H14.9M15.6,10.75C17.06,11.07 18.17,12.37 18.17,13.93C18.17,15.73 16.7,17.19 14.9,17.19H4.9C2.75,17.19 1,15.45 1,13.3C1,11.34 2.45,9.73 4.33,9.45C5.12,7.12 7.33,5.5 9.82,5.5C12.83,5.5 15.31,7.82 15.6,10.75Z", "path": "M21.11,18.5C20.97,18.5 20.83,18.44 20.71,18.36C20.37,18.13 20.28,17.68 20.5,17.34C21.18,16.34 21.54,15.16 21.54,13.93C21.54,12.71 21.18,11.53 20.5,10.5C20.28,10.18 20.37,9.73 20.71,9.5C21.04,9.28 21.5,9.37 21.72,9.7C22.56,10.95 23,12.41 23,13.93C23,15.45 22.56,16.91 21.72,18.16C21.58,18.37 21.35,18.5 21.11,18.5M19,17.29C18.88,17.29 18.74,17.25 18.61,17.17C18.28,16.94 18.19,16.5 18.42,16.15C18.86,15.5 19.1,14.73 19.1,13.93C19.1,13.14 18.86,12.37 18.42,11.71C18.19,11.37 18.28,10.92 18.61,10.69C18.95,10.47 19.4,10.55 19.63,10.89C20.24,11.79 20.56,12.84 20.56,13.93C20.56,15 20.24,16.07 19.63,16.97C19.5,17.18 19.25,17.29 19,17.29M14.9,15.73C15.89,15.73 16.7,14.92 16.7,13.93C16.7,13.17 16.22,12.5 15.55,12.25C15.5,12.55 15.43,12.85 15.34,13.14C15.23,13.44 14.95,13.64 14.64,13.64C14.57,13.64 14.5,13.62 14.41,13.6C14.03,13.47 13.82,13.06 13.95,12.67C14.09,12.24 14.17,11.78 14.17,11.32C14.17,8.93 12.22,7 9.82,7C8.1,7 6.56,8 5.87,9.5C6.54,9.7 7.16,10.04 7.66,10.54C7.95,10.83 7.95,11.29 7.66,11.58C7.38,11.86 6.91,11.86 6.63,11.58C6.17,11.12 5.56,10.86 4.9,10.86C3.56,10.86 2.46,11.96 2.46,13.3C2.46,14.64 3.56,15.73 4.9,15.73H14.9M15.6,10.75C17.06,11.07 18.17,12.37 18.17,13.93C18.17,15.73 16.7,17.19 14.9,17.19H4.9C2.75,17.19 1,15.45 1,13.3C1,11.34 2.45,9.73 4.33,9.45C5.12,7.12 7.33,5.5 9.82,5.5C12.83,5.5 15.31,7.82 15.6,10.75Z",
"name": "mixcloud" "name": "mixcloud"
}, },
{
"path": "M5.68,3.96L11.41,11.65C11.55,11.84 11.55,12.1 11.41,12.29L5.65,20L5.5,20.18C4.76,21 3.47,21.07 2.64,20.31C1.85,19.59 1.79,18.37 2.43,17.5L6.56,11.97L2.46,6.47C1.83,5.62 1.88,4.39 2.67,3.67L2.82,3.54C3.73,2.87 5,3.05 5.68,3.96M18.32,3.96C19,3.05 20.27,2.87 21.18,3.54L21.33,3.67C22.12,4.39 22.17,5.61 21.54,6.47L17.44,11.97L21.57,17.5C22.21,18.36 22.15,19.59 21.36,20.31C20.53,21.07 19.24,21 18.5,20.18L18.35,20L12.59,12.29C12.45,12.1 12.45,11.84 12.59,11.65L18.32,3.96Z",
"name": "mixer"
},
{ {
"path": "M3.25,4.03L19.95,20.73L18.7,22L14.86,18.13C14.77,18.12 14.68,18.09 14.59,18.05C14.26,17.89 14.14,17.62 14.11,17.38L12.18,15.45C12.14,15.53 12.09,15.6 12.05,15.66C11.62,16.26 11.19,16.26 10.86,16.04C10.54,15.83 5.5,12 5.23,11.87C4.95,11.76 4.85,12.03 5.12,13.5C5.39,15 4.95,15.39 4.57,15.45C4.2,15.5 3.06,15.18 3,12.14C2.95,9.11 3.76,8.62 4.14,8.62C4.6,8.62 7.08,10.69 8.84,12.12L2,5.28L3.25,4.03M18.38,16.56C18.75,15.4 19.12,13.8 19.1,12.03V12C19.14,8.5 17.66,5.58 17.66,5.58C17.66,5.58 17.42,4.72 18.12,4.39C18.83,4.06 19.3,4.61 19.3,4.61C21.12,8.22 21,11.64 21,12C21,12.27 21.09,14.96 19.88,18.05L18.38,16.56M15.14,13.31C15.19,12.92 15.22,12.5 15.24,12.03V12C15.14,8.5 14.13,7.21 14.13,7.21C14.13,7.21 13.89,6.34 14.59,6C15.3,5.69 15.77,6.23 15.77,6.23C17.26,8.94 17.16,11.64 17.14,12C17.15,12.2 17.2,13.38 16.82,15L15.14,13.31M10.2,8.38C10.23,7.77 10.59,7.64 10.59,7.64C10.59,7.64 11.19,7.37 11.57,7.8C11.91,8.19 12.72,9.57 12.89,11.07L10.2,8.38Z", "path": "M3.25,4.03L19.95,20.73L18.7,22L14.86,18.13C14.77,18.12 14.68,18.09 14.59,18.05C14.26,17.89 14.14,17.62 14.11,17.38L12.18,15.45C12.14,15.53 12.09,15.6 12.05,15.66C11.62,16.26 11.19,16.26 10.86,16.04C10.54,15.83 5.5,12 5.23,11.87C4.95,11.76 4.85,12.03 5.12,13.5C5.39,15 4.95,15.39 4.57,15.45C4.2,15.5 3.06,15.18 3,12.14C2.95,9.11 3.76,8.62 4.14,8.62C4.6,8.62 7.08,10.69 8.84,12.12L2,5.28L3.25,4.03M18.38,16.56C18.75,15.4 19.12,13.8 19.1,12.03V12C19.14,8.5 17.66,5.58 17.66,5.58C17.66,5.58 17.42,4.72 18.12,4.39C18.83,4.06 19.3,4.61 19.3,4.61C21.12,8.22 21,11.64 21,12C21,12.27 21.09,14.96 19.88,18.05L18.38,16.56M15.14,13.31C15.19,12.92 15.22,12.5 15.24,12.03V12C15.14,8.5 14.13,7.21 14.13,7.21C14.13,7.21 13.89,6.34 14.59,6C15.3,5.69 15.77,6.23 15.77,6.23C17.26,8.94 17.16,11.64 17.14,12C17.15,12.2 17.2,13.38 16.82,15L15.14,13.31M10.2,8.38C10.23,7.77 10.59,7.64 10.59,7.64C10.59,7.64 11.19,7.37 11.57,7.8C11.91,8.19 12.72,9.57 12.89,11.07L10.2,8.38Z",
"name": "nfc-off" "name": "nfc-off"

View File

@ -22,6 +22,8 @@ class HcLovelace extends LitElement {
@property() public viewPath?: string | number; @property() public viewPath?: string | number;
public urlPath?: string | null;
protected render(): TemplateResult { protected render(): TemplateResult {
const index = this._viewIndex; const index = this._viewIndex;
if (index === undefined) { if (index === undefined) {
@ -35,6 +37,7 @@ class HcLovelace extends LitElement {
const lovelace: Lovelace = { const lovelace: Lovelace = {
config: this.lovelaceConfig, config: this.lovelaceConfig,
editMode: false, editMode: false,
urlPath: this.urlPath!,
enableFullEditMode: () => undefined, enableFullEditMode: () => undefined,
mode: "storage", mode: "storage",
language: "en", language: "en",

View File

@ -87,6 +87,7 @@ export class HcMain extends HassElement {
.hass=${this.hass} .hass=${this.hass}
.lovelaceConfig=${this._lovelaceConfig} .lovelaceConfig=${this._lovelaceConfig}
.viewPath=${this._lovelacePath} .viewPath=${this._lovelacePath}
.urlPath=${this._urlPath}
@config-refresh=${this._generateLovelaceConfig} @config-refresh=${this._generateLovelaceConfig}
></hc-lovelace> ></hc-lovelace>
`; `;

View File

@ -52,7 +52,6 @@ class CastDemoRow extends LitElement implements LovelaceRow {
}); });
mgr.castContext.addEventListener( mgr.castContext.addEventListener(
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
// @ts-ignore
cast.framework.CastContextEventType.SESSION_STATE_CHANGED, cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
(ev) => { (ev) => {
// On Android, opening a new session always results in SESSION_RESUMED. // On Android, opening a new session always results in SESSION_RESUMED.

View File

@ -25,6 +25,7 @@ import { HomeAssistant, Route } from "../../../src/types";
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories"; import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
import { supervisorTabs } from "../hassio-tabs"; import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addon-repository"; import "./hassio-addon-repository";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => { const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
if (a.slug === "local") { if (a.slug === "local") {
@ -97,14 +98,18 @@ class HassioAddonStore extends LitElement {
.tabs=${supervisorTabs} .tabs=${supervisorTabs}
> >
<span slot="header">Add-on store</span> <span slot="header">Add-on store</span>
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon"> <ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._handleAction}
>
<mwc-icon-button slot="trigger" alt="menu"> <mwc-icon-button slot="trigger" alt="menu">
<ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon> <ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
<mwc-list-item @tap=${this._manageRepositories}> <mwc-list-item>
Repositories Repositories
</mwc-list-item> </mwc-list-item>
<mwc-list-item @tap=${this.refreshData}> <mwc-list-item>
Reload Reload
</mwc-list-item> </mwc-list-item>
</ha-button-menu> </ha-button-menu>
@ -143,6 +148,17 @@ class HassioAddonStore extends LitElement {
this._loadData(); this._loadData();
} }
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._manageRepositories();
break;
case 1:
this.refreshData();
break;
}
}
private apiCalled(ev) { private apiCalled(ev) {
if (ev.detail.success) { if (ev.detail.success) {
this._loadData(); this._loadData();

View File

@ -94,7 +94,8 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
applyThemesOnElement( applyThemesOnElement(
this.parentElement, this.parentElement,
this.hass.themes, this.hass.themes,
this.hass.selectedTheme || this.hass.themes.default_theme this.hass.selectedTheme?.theme || this.hass.themes.default_theme,
this.hass.selectedTheme
); );
this.style.setProperty( this.style.setProperty(

View File

@ -13,7 +13,7 @@
"lint:prettier": "prettier '**/src/**/*.{js,ts,json,css,md}' --check", "lint:prettier": "prettier '**/src/**/*.{js,ts,json,css,md}' --check",
"format:prettier": "prettier '**/src/**/*.{js,ts,json,css,md}' --write", "format:prettier": "prettier '**/src/**/*.{js,ts,json,css,md}' --write",
"lint:types": "tsc", "lint:types": "tsc",
"lint:lit": "lit-analyzer '**/src/**/*.ts'", "lint:lit": "lit-analyzer '**/src/**/*.ts' --format markdown --outFile result.md",
"lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:types", "lint": "npm run lint:eslint && npm run lint:prettier && npm run lint:types",
"format": "npm run format:eslint && npm run format:prettier", "format": "npm run format:eslint && npm run format:prettier",
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts", "mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
@ -35,13 +35,14 @@
"@material/mwc-icon-button": "^0.17.2", "@material/mwc-icon-button": "^0.17.2",
"@material/mwc-list": "^0.17.2", "@material/mwc-list": "^0.17.2",
"@material/mwc-menu": "^0.17.2", "@material/mwc-menu": "^0.17.2",
"@material/mwc-radio": "^0.17.2",
"@material/mwc-ripple": "^0.17.2", "@material/mwc-ripple": "^0.17.2",
"@material/mwc-switch": "^0.17.2", "@material/mwc-switch": "^0.17.2",
"@material/mwc-tab": "^0.17.2", "@material/mwc-tab": "^0.17.2",
"@material/mwc-tab-bar": "^0.17.2", "@material/mwc-tab-bar": "^0.17.2",
"@material/top-app-bar": "=8.0.0-canary.a78ceb112.0", "@material/top-app-bar": "=8.0.0-canary.a78ceb112.0",
"@mdi/js": "5.3.45", "@mdi/js": "5.4.55",
"@mdi/svg": "5.3.45", "@mdi/svg": "5.4.55",
"@polymer/app-layout": "^3.0.2", "@polymer/app-layout": "^3.0.2",
"@polymer/app-route": "^3.0.2", "@polymer/app-route": "^3.0.2",
"@polymer/app-storage": "^3.0.2", "@polymer/app-storage": "^3.0.2",
@ -74,6 +75,7 @@
"@polymer/paper-tooltip": "^3.0.1", "@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0", "@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.5.0", "@thomasloven/round-slider": "0.5.0",
"@types/chromecast-caf-sender": "^1.0.3",
"@vaadin/vaadin-combo-box": "^5.0.10", "@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7", "@vaadin/vaadin-date-picker": "^4.0.7",
"@vue/web-component-wrapper": "^1.2.0", "@vue/web-component-wrapper": "^1.2.0",
@ -99,7 +101,7 @@
"lit-element": "^2.3.1", "lit-element": "^2.3.1",
"lit-html": "^1.2.1", "lit-html": "^1.2.1",
"lit-virtualizer": "^0.4.2", "lit-virtualizer": "^0.4.2",
"marked": "^0.6.1", "marked": "^1.1.1",
"mdn-polyfills": "^5.16.0", "mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2", "memoize-one": "^5.0.2",
"node-vibrant": "^3.1.5", "node-vibrant": "^3.1.5",
@ -107,7 +109,7 @@
"regenerator-runtime": "^0.13.2", "regenerator-runtime": "^0.13.2",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"superstruct": "^0.6.1", "superstruct": "^0.10.12",
"unfetch": "^4.1.0", "unfetch": "^4.1.0",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue2-daterange-picker": "^0.5.1", "vue2-daterange-picker": "^0.5.1",
@ -135,11 +137,12 @@
"@rollup/plugin-replace": "^2.3.2", "@rollup/plugin-replace": "^2.3.2",
"@types/chai": "^4.1.7", "@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12", "@types/chromecast-caf-receiver": "^3.0.12",
"@types/codemirror": "^0.0.78", "@types/codemirror": "^0.0.97",
"@types/hls.js": "^0.12.3", "@types/hls.js": "^0.12.3",
"@types/js-yaml": "^3.12.1", "@types/js-yaml": "^3.12.1",
"@types/leaflet": "^1.4.3", "@types/leaflet": "^1.4.3",
"@types/leaflet-draw": "^1.0.1", "@types/leaflet-draw": "^1.0.1",
"@types/marked": "^1.1.0",
"@types/memoize-one": "4.1.0", "@types/memoize-one": "4.1.0",
"@types/mocha": "^5.2.6", "@types/mocha": "^5.2.6",
"@types/resize-observer-browser": "^0.1.3", "@types/resize-observer-browser": "^0.1.3",
@ -170,7 +173,7 @@
"html-minifier": "^4.0.0", "html-minifier": "^4.0.0",
"husky": "^1.3.1", "husky": "^1.3.1",
"lint-staged": "^8.1.5", "lint-staged": "^8.1.5",
"lit-analyzer": "^1.1.10", "lit-analyzer": "^1.2.0",
"lodash.template": "^4.5.0", "lodash.template": "^4.5.0",
"magic-string": "^0.25.7", "magic-string": "^0.25.7",
"map-stream": "^0.0.7", "map-stream": "^0.0.7",
@ -191,7 +194,7 @@
"source-map-url": "^0.4.0", "source-map-url": "^0.4.0",
"systemjs": "^6.3.2", "systemjs": "^6.3.2",
"terser-webpack-plugin": "^3.0.6", "terser-webpack-plugin": "^3.0.6",
"ts-lit-plugin": "^1.1.10", "ts-lit-plugin": "^1.2.0",
"ts-mocha": "^6.0.0", "ts-mocha": "^6.0.0",
"typescript": "^3.8.3", "typescript": "^3.8.3",
"vinyl-buffer": "^1.0.1", "vinyl-buffer": "^1.0.1",

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20200716.0", version="20200803.0",
description="The Home Assistant frontend", description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer", url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors", author="The Home Assistant Authors",

View File

@ -1,4 +1,8 @@
/* eslint-disable no-undef, no-console */ /* eslint-disable no-undef, no-console */
import {
CastStateEventData,
SessionStateEventData,
} from "chromecast-caf-receiver/cast.framework";
import { Auth } from "home-assistant-js-websocket"; import { Auth } from "home-assistant-js-websocket";
import { castApiAvailable } from "./cast_framework"; import { castApiAvailable } from "./cast_framework";
import { CAST_APP_ID, CAST_DEV, CAST_NS } from "./const"; import { CAST_APP_ID, CAST_DEV, CAST_NS } from "./const";
@ -40,16 +44,13 @@ export class CastManager {
const context = this.castContext; const context = this.castContext;
context.setOptions({ context.setOptions({
receiverApplicationId: CAST_APP_ID, receiverApplicationId: CAST_APP_ID,
// @ts-ignore
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED, autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
}); });
context.addEventListener( context.addEventListener(
// @ts-ignore
cast.framework.CastContextEventType.SESSION_STATE_CHANGED, cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
(ev) => this._sessionStateChanged(ev) (ev) => this._sessionStateChanged(ev)
); );
context.addEventListener( context.addEventListener(
// @ts-ignore
cast.framework.CastContextEventType.CAST_STATE_CHANGED, cast.framework.CastContextEventType.CAST_STATE_CHANGED,
(ev) => this._castStateChanged(ev) (ev) => this._castStateChanged(ev)
); );
@ -118,7 +119,7 @@ export class CastManager {
} }
} }
private _sessionStateChanged(ev) { private _sessionStateChanged(ev: SessionStateEventData) {
if (__DEV__) { if (__DEV__) {
console.log("Cast session state changed", ev.sessionState); console.log("Cast session state changed", ev.sessionState);
} }
@ -141,7 +142,7 @@ export class CastManager {
} }
} }
private _castStateChanged(ev) { private _castStateChanged(ev: CastStateEventData) {
if (__DEV__) { if (__DEV__) {
console.log("Cast state changed", ev.castState); console.log("Cast state changed", ev.castState);
} }

View File

@ -0,0 +1,113 @@
const expand_hex = (hex: string): string => {
let result = "";
for (const val of hex) {
result += val + val;
}
return result;
};
const rgb_hex = (component: number): string => {
const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16);
return hex.length === 1 ? `0${hex}` : hex;
};
// Conversion between HEX and RGB
export const hex2rgb = (hex: string): [number, number, number] => {
hex = hex.replace("#", "");
if (hex.length === 3 || hex.length === 4) {
hex = expand_hex(hex);
}
return [
parseInt(hex.substring(0, 2), 16),
parseInt(hex.substring(2, 4), 16),
parseInt(hex.substring(4, 6), 16),
];
};
export const rgb2hex = (rgb: [number, number, number]): string => {
return `#${rgb_hex(rgb[0])}${rgb_hex(rgb[1])}${rgb_hex(rgb[2])}`;
};
// Conversion between LAB, XYZ and RGB from https://github.com/gka/chroma.js
// Copyright (c) 2011-2019, Gregor Aisch
// Constants for XYZ and LAB conversion
const Xn = 0.95047;
const Yn = 1;
const Zn = 1.08883;
const t0 = 0.137931034; // 4 / 29
const t1 = 0.206896552; // 6 / 29
const t2 = 0.12841855; // 3 * t1 * t1
const t3 = 0.008856452; // t1 * t1 * t1
const rgb_xyz = (r: number) => {
r /= 255;
if (r <= 0.04045) {
return r / 12.92;
}
return ((r + 0.055) / 1.055) ** 2.4;
};
const xyz_lab = (t: number) => {
if (t > t3) {
return t ** (1 / 3);
}
return t / t2 + t0;
};
const xyz_rgb = (r: number) => {
return 255 * (r <= 0.00304 ? 12.92 * r : 1.055 * r ** (1 / 2.4) - 0.055);
};
const lab_xyz = (t: number) => {
return t > t1 ? t * t * t : t2 * (t - t0);
};
// Conversions between RGB and LAB
const rgb2xyz = (rgb: [number, number, number]): [number, number, number] => {
let [r, g, b] = rgb;
r = rgb_xyz(r);
g = rgb_xyz(g);
b = rgb_xyz(b);
const x = xyz_lab((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / Xn);
const y = xyz_lab((0.2126729 * r + 0.7151522 * g + 0.072175 * b) / Yn);
const z = xyz_lab((0.0193339 * r + 0.119192 * g + 0.9503041 * b) / Zn);
return [x, y, z];
};
export const rgb2lab = (
rgb: [number, number, number]
): [number, number, number] => {
const [x, y, z] = rgb2xyz(rgb);
const l = 116 * y - 16;
return [l < 0 ? 0 : l, 500 * (x - y), 200 * (y - z)];
};
export const lab2rgb = (
lab: [number, number, number]
): [number, number, number] => {
const [l, a, b] = lab;
let y = (l + 16) / 116;
let x = isNaN(a) ? y : y + a / 500;
let z = isNaN(b) ? y : y - b / 200;
y = Yn * lab_xyz(y);
x = Xn * lab_xyz(x);
z = Zn * lab_xyz(z);
const r = xyz_rgb(3.2404542 * x - 1.5371385 * y - 0.4985314 * z); // D65 -> sRGB
const g = xyz_rgb(-0.969266 * x + 1.8760108 * y + 0.041556 * z);
const b_ = xyz_rgb(0.0556434 * x - 0.2040259 * y + 1.0572252 * z);
return [r, g, b_];
};
export const lab2hex = (lab: [number, number, number]): string => {
const rgb = lab2rgb(lab);
return rgb2hex(rgb);
};

16
src/common/color/lab.ts Normal file
View File

@ -0,0 +1,16 @@
// From https://github.com/gka/chroma.js
// Copyright (c) 2011-2019, Gregor Aisch
export const labDarken = (
lab: [number, number, number],
amount = 1
): [number, number, number] => {
return [lab[0] - 18 * amount, lab[1], lab[2]];
};
export const labBrighten = (
lab: [number, number, number],
amount = 1
): [number, number, number] => {
return labDarken(lab, -amount);
};

24
src/common/color/rgb.ts Normal file
View File

@ -0,0 +1,24 @@
const luminosity = (rgb: [number, number, number]): number => {
// http://www.w3.org/TR/WCAG20/#relativeluminancedef
const lum: [number, number, number] = [0, 0, 0];
for (let i = 0; i < rgb.length; i++) {
const chan = rgb[i] / 255;
lum[i] = chan <= 0.03928 ? chan / 12.92 : ((chan + 0.055) / 1.055) ** 2.4;
}
return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
};
export const rgbContrast = (
color1: [number, number, number],
color2: [number, number, number]
) => {
const lum1 = luminosity(color1);
const lum2 = luminosity(color2);
if (lum1 > lum2) {
return (lum1 + 0.05) / (lum2 + 0.05);
}
return (lum2 + 0.05) / (lum1 + 0.05);
};

View File

@ -1,26 +1,20 @@
import { derivedStyles } from "../../resources/styles"; import { derivedStyles, darkStyles } from "../../resources/styles";
import { HomeAssistant, Theme } from "../../types"; import { HomeAssistant, Theme } from "../../types";
import {
hex2rgb,
rgb2hex,
rgb2lab,
lab2rgb,
lab2hex,
} from "../color/convert-color";
import { rgbContrast } from "../color/rgb";
import { labDarken, labBrighten } from "../color/lab";
interface ProcessedTheme { interface ProcessedTheme {
keys: { [key: string]: "" }; keys: { [key: string]: "" };
styles: { [key: string]: string }; styles: { [key: string]: string };
} }
const hexToRgb = (hex: string): string | null => {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
const checkHex = hex.replace(shorthandRegex, (_m, r, g, b) => {
return r + r + g + g + b + b;
});
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(checkHex);
return result
? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(
result[3],
16
)}`
: null;
};
let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {}; let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
/** /**
@ -33,17 +27,56 @@ let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {};
export const applyThemesOnElement = ( export const applyThemesOnElement = (
element, element,
themes: HomeAssistant["themes"], themes: HomeAssistant["themes"],
selectedTheme?: string selectedTheme?: string,
themeOptions?: Partial<HomeAssistant["selectedTheme"]>
) => { ) => {
const newTheme = selectedTheme let cacheKey = selectedTheme;
? PROCESSED_THEMES[selectedTheme] || processTheme(selectedTheme, themes) let themeRules: Partial<Theme> = {};
: undefined;
if (!element._themes && !newTheme) { if (selectedTheme === "default" && themeOptions) {
if (themeOptions.dark) {
cacheKey = `${cacheKey}__dark`;
themeRules = darkStyles;
}
if (themeOptions.primaryColor) {
cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`;
const rgbPrimaryColor = hex2rgb(themeOptions.primaryColor);
const labPrimaryColor = rgb2lab(rgbPrimaryColor);
themeRules["primary-color"] = themeOptions.primaryColor;
const rgbLigthPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
themeRules["light-primary-color"] = rgb2hex(rgbLigthPrimaryColor);
themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
themeRules["text-primary-color"] =
rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
themeRules["text-light-primary-color"] =
rgbContrast(rgbLigthPrimaryColor, [33, 33, 33]) < 6
? "#fff"
: "#212121";
themeRules["state-icon-color"] = themeRules["dark-primary-color"];
}
if (themeOptions.accentColor) {
cacheKey = `${cacheKey}__accent_${themeOptions.accentColor}`;
themeRules["accent-color"] = themeOptions.accentColor;
const rgbAccentColor = hex2rgb(themeOptions.accentColor);
themeRules["text-accent-color"] =
rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
}
}
if (selectedTheme && themes.themes[selectedTheme]) {
themeRules = themes.themes[selectedTheme];
}
if (!element._themes && !Object.keys(themeRules).length) {
// No styles to reset, and no styles to set // No styles to reset, and no styles to set
return; return;
} }
const newTheme =
themeRules && cacheKey
? PROCESSED_THEMES[cacheKey] || processTheme(cacheKey, themeRules)
: undefined;
// Add previous set keys to reset them, and new theme // Add previous set keys to reset them, and new theme
const styles = { ...element._themes, ...newTheme?.styles }; const styles = { ...element._themes, ...newTheme?.styles };
element._themes = newTheme?.keys; element._themes = newTheme?.keys;
@ -58,42 +91,45 @@ export const applyThemesOnElement = (
}; };
const processTheme = ( const processTheme = (
themeName: string, cacheKey: string,
themes: HomeAssistant["themes"] theme: Partial<Theme>
): ProcessedTheme | undefined => { ): ProcessedTheme | undefined => {
if (!themes.themes[themeName]) { if (!theme || !Object.keys(theme).length) {
return undefined; return undefined;
} }
const theme: Theme = { const combinedTheme: Partial<Theme> = {
...derivedStyles, ...derivedStyles,
...themes.themes[themeName], ...theme,
}; };
const styles = {}; const styles = {};
const keys = {}; const keys = {};
for (const key of Object.keys(theme)) { for (const key of Object.keys(combinedTheme)) {
const prefixedKey = `--${key}`; const prefixedKey = `--${key}`;
const value = theme[key]; const value = combinedTheme[key]!;
styles[prefixedKey] = value; styles[prefixedKey] = value;
keys[prefixedKey] = ""; keys[prefixedKey] = "";
// Try to create a rgb value for this key if it is a hex color // Try to create a rgb value for this key if it is not a var
if (!value.startsWith("#")) { if (!value.startsWith("#")) {
// Not a hex color // Can't convert non hex value
continue; continue;
} }
const rgbKey = `rgb-${key}`; const rgbKey = `rgb-${key}`;
if (theme[rgbKey] !== undefined) { if (combinedTheme[rgbKey] !== undefined) {
// Theme has it's own rgb value // Theme has it's own rgb value
continue; continue;
} }
const rgbValue = hexToRgb(value); try {
if (rgbValue !== null) { const rgbValue = hex2rgb(value).join(",");
const prefixedRgbKey = `--${rgbKey}`; const prefixedRgbKey = `--${rgbKey}`;
styles[prefixedRgbKey] = rgbValue; styles[prefixedRgbKey] = rgbValue;
keys[prefixedRgbKey] = ""; keys[prefixedRgbKey] = "";
} catch (e) {
continue;
} }
} }
PROCESSED_THEMES[themeName] = { styles, keys }; PROCESSED_THEMES[cacheKey] = { styles, keys };
return { styles, keys }; return { styles, keys };
}; };

View File

@ -1,4 +1,4 @@
import type { Map } from "leaflet"; import type { Map, TileLayer } from "leaflet";
// Sets up a Leaflet map on the provided DOM element // Sets up a Leaflet map on the provided DOM element
export type LeafletModuleType = typeof import("leaflet"); export type LeafletModuleType = typeof import("leaflet");
@ -6,9 +6,9 @@ export type LeafletDrawModuleType = typeof import("leaflet-draw");
export const setupLeafletMap = async ( export const setupLeafletMap = async (
mapElement: HTMLElement, mapElement: HTMLElement,
darkMode = false, darkMode?: boolean,
draw = false draw = false
): Promise<[Map, LeafletModuleType]> => { ): Promise<[Map, LeafletModuleType, TileLayer]> => {
if (!mapElement.parentNode) { if (!mapElement.parentNode) {
throw new Error("Cannot setup Leaflet map on disconnected element"); throw new Error("Cannot setup Leaflet map on disconnected element");
} }
@ -28,15 +28,28 @@ export const setupLeafletMap = async (
style.setAttribute("rel", "stylesheet"); style.setAttribute("rel", "stylesheet");
mapElement.parentNode.appendChild(style); mapElement.parentNode.appendChild(style);
map.setView([52.3731339, 4.8903147], 13); map.setView([52.3731339, 4.8903147], 13);
createTileLayer(Leaflet, darkMode).addTo(map);
return [map, Leaflet]; const tileLayer = createTileLayer(Leaflet, Boolean(darkMode)).addTo(map);
return [map, Leaflet, tileLayer];
}; };
export const createTileLayer = ( export const replaceTileLayer = (
leaflet: LeafletModuleType,
map: Map,
tileLayer: TileLayer,
darkMode: boolean
): TileLayer => {
map.removeLayer(tileLayer);
tileLayer = createTileLayer(leaflet, darkMode);
tileLayer.addTo(map);
return tileLayer;
};
const createTileLayer = (
leaflet: LeafletModuleType, leaflet: LeafletModuleType,
darkMode: boolean darkMode: boolean
) => { ): TileLayer => {
return leaflet.tileLayer( return leaflet.tileLayer(
`https://{s}.basemaps.cartocdn.com/${ `https://{s}.basemaps.cartocdn.com/${
darkMode ? "dark_all" : "light_all" darkMode ? "dark_all" : "light_all"

View File

@ -13,7 +13,7 @@ export const batteryIcon = (
return "hass:battery-unknown"; return "hass:battery-unknown";
} }
var icon = "hass:battery"; let icon = "hass:battery";
const batteryRound = Math.round(battery / 10) * 10; const batteryRound = Math.round(battery / 10) * 10;
if (battery_charging && battery > 10) { if (battery_charging && battery > 10) {
icon += `-charging-${batteryRound}`; icon += `-charging-${batteryRound}`;

View File

@ -4,6 +4,6 @@ export const supportsFeature = (
stateObj: HassEntity, stateObj: HassEntity,
feature: number feature: number
): boolean => { ): boolean => {
// eslint-disable-next-line:no-bitwise // eslint-disable-next-line no-bitwise
return (stateObj.attributes.supported_features! & feature) !== 0; return (stateObj.attributes.supported_features! & feature) !== 0;
}; };

View File

@ -2,11 +2,3 @@ const validEntityId = /^(\w+)\.(\w+)$/;
export const isValidEntityId = (entityId: string) => export const isValidEntityId = (entityId: string) =>
validEntityId.test(entityId); validEntityId.test(entityId);
export const createValidEntityId = (input: string) =>
input
.toLowerCase()
.replace(/\s|'|\./g, "_") // replace spaces, points and quotes with underscore
.replace(/\W/g, "") // remove not allowed chars
.replace(/_{2,}/g, "_") // replace multiple underscores with 1
.replace(/_$/, ""); // remove underscores at the end

View File

@ -0,0 +1,14 @@
import {
RequestSelectedDetail,
ListItem,
} from "@material/mwc-list/mwc-list-item";
export const shouldHandleRequestSelectedEvent = (
ev: CustomEvent<RequestSelectedDetail>
): boolean => {
if (!ev.detail.selected && ev.detail.source !== "property") {
return false;
}
(ev.target as ListItem).selected = false;
return true;
};

View File

@ -1,5 +1,5 @@
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1 // https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
export const slugify = (value: string, delimiter = "-") => { export const slugify = (value: string, delimiter = "_") => {
const a = const a =
"àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;"; "àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;";
const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`; const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`;

View File

@ -9,5 +9,9 @@ export function computeRTL(hass: HomeAssistant) {
} }
export function computeRTLDirection(hass: HomeAssistant) { export function computeRTLDirection(hass: HomeAssistant) {
return computeRTL(hass) ? "rtl" : "ltr"; return emitRTLDirection(computeRTL(hass));
}
export function emitRTLDirection(rtl: boolean) {
return rtl ? "rtl" : "ltr";
} }

View File

@ -54,8 +54,8 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
callService() { callService() {
this.progress = true; this.progress = true;
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
var el = this; const el = this;
var eventData = { const eventData = {
domain: this.domain, domain: this.domain,
service: this.service, service: this.service,
serviceData: this.serviceData, serviceData: this.serviceData,

View File

@ -78,7 +78,7 @@ class HaProgressButton extends PolymerElement {
} }
tempClass(className) { tempClass(className) {
var classList = this.$.container.classList; const classList = this.$.container.classList;
classList.add(className); classList.add(className);
setTimeout(() => { setTimeout(() => {
classList.remove(className); classList.remove(className);

View File

@ -98,6 +98,8 @@ export class HaDataTable extends LitElement {
@property({ type: String }) public noDataText?: string; @property({ type: String }) public noDataText?: string;
@property({ type: String }) public searchLabel?: string;
@property({ type: String }) public filter = ""; @property({ type: String }) public filter = "";
@internalProperty() private _filterable = false; @internalProperty() private _filterable = false;
@ -202,6 +204,7 @@ export class HaDataTable extends LitElement {
<div class="table-header"> <div class="table-header">
<search-input <search-input
@value-changed=${this._handleSearchChange} @value-changed=${this._handleSearchChange}
.label=${this.searchLabel}
></search-input> ></search-input>
</div> </div>
` `
@ -538,7 +541,7 @@ export class HaDataTable extends LitElement {
border-radius: 4px; border-radius: 4px;
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
border-color: rgba(var(--rgb-primary-text-color), 0.12); border-color: var(--divider-color);
display: inline-flex; display: inline-flex;
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
@ -556,7 +559,7 @@ export class HaDataTable extends LitElement {
} }
.mdc-data-table__row ~ .mdc-data-table__row { .mdc-data-table__row ~ .mdc-data-table__row {
border-top: 1px solid rgba(var(--rgb-primary-text-color), 0.12); border-top: 1px solid var(--divider-color);
} }
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover { .mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
@ -575,7 +578,7 @@ export class HaDataTable extends LitElement {
height: 56px; height: 56px;
display: flex; display: flex;
width: 100%; width: 100%;
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12); border-bottom: 1px solid var(--divider-color);
overflow-x: auto; overflow-x: auto;
} }
@ -828,7 +831,7 @@ export class HaDataTable extends LitElement {
right: 12px; right: 12px;
} }
.table-header { .table-header {
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12); border-bottom: 1px solid var(--divider-color);
padding: 0 16px; padding: 0 16px;
} }
search-input { search-input {

View File

@ -135,7 +135,7 @@ class DateRangePickerElement extends WrappedElement {
} }
.daterangepicker td.in-range { .daterangepicker td.in-range {
background-color: var(--light-primary-color); background-color: var(--light-primary-color);
color: var(--primary-text-color); color: var(--text-light-primary-color, var(--primary-text-color));
} }
.daterangepicker td.active, .daterangepicker td.active,
.daterangepicker td.active:hover { .daterangepicker td.active:hover {

View File

@ -117,11 +117,7 @@ export abstract class HaDeviceAutomationPicker<
> >
${this.NO_AUTOMATION_TEXT} ${this.NO_AUTOMATION_TEXT}
</paper-item> </paper-item>
<paper-item <paper-item key=${UNKNOWN_AUTOMATION_KEY} hidden>
key=${UNKNOWN_AUTOMATION_KEY}
.automation=${this.value}
hidden
>
${this.UNKNOWN_AUTOMATION_TEXT} ${this.UNKNOWN_AUTOMATION_TEXT}
</paper-item> </paper-item>
${this._automations.map( ${this._automations.map(
@ -175,18 +171,17 @@ export abstract class HaDeviceAutomationPicker<
} }
private _automationChanged(ev) { private _automationChanged(ev) {
this._setValue(ev.detail.item.automation); if (ev.detail.item.automation) {
this._setValue(ev.detail.item.automation);
}
} }
private _setValue(automation: T) { private _setValue(automation: T) {
if (this.value && deviceAutomationsEqual(automation, this.value)) { if (this.value && deviceAutomationsEqual(automation, this.value)) {
return; return;
} }
this.value = automation; fireEvent(this, "change");
setTimeout(() => { fireEvent(this, "value-changed", { value: automation });
fireEvent(this, "change");
fireEvent(this, "value-changed", { value: automation });
}, 0);
} }
static get styles(): CSSResult { static get styles(): CSSResult {

View File

@ -23,10 +23,10 @@ export const HaIronFocusablesHelper = {
* @return {!Array<!HTMLElement>} * @return {!Array<!HTMLElement>}
*/ */
getTabbableNodes: function (node) { getTabbableNodes: function (node) {
var result = []; const result = [];
// If there is at least one element with tabindex > 0, we need to sort // If there is at least one element with tabindex > 0, we need to sort
// the final array by tabindex. // the final array by tabindex.
var needsSortByTabIndex = this._collectTabbableNodes(node, result); const needsSortByTabIndex = this._collectTabbableNodes(node, result);
if (needsSortByTabIndex) { if (needsSortByTabIndex) {
return IronFocusablesHelper._sortByTabIndex(result); return IronFocusablesHelper._sortByTabIndex(result);
} }
@ -50,9 +50,9 @@ export const HaIronFocusablesHelper = {
) { ) {
return false; return false;
} }
var element = /** @type {!HTMLElement} */ (node); const element = /** @type {!HTMLElement} */ (node);
var tabIndex = IronFocusablesHelper._normalizedTabIndex(element); const tabIndex = IronFocusablesHelper._normalizedTabIndex(element);
var needsSort = tabIndex > 0; let needsSort = tabIndex > 0;
if (tabIndex >= 0) { if (tabIndex >= 0) {
result.push(element); result.push(element);
} }
@ -70,7 +70,7 @@ export const HaIronFocusablesHelper = {
// <input id="B" slot="b" tabindex="1"> // <input id="B" slot="b" tabindex="1">
// </div> // </div>
// TODO(valdrin) support ShadowDOM v1 when upgrading to Polymer v2.0. // TODO(valdrin) support ShadowDOM v1 when upgrading to Polymer v2.0.
var children; let children;
if (element.localName === "content" || element.localName === "slot") { if (element.localName === "content" || element.localName === "slot") {
children = dom(element).getDistributedNodes(); children = dom(element).getDistributedNodes();
} else { } else {
@ -80,7 +80,7 @@ export const HaIronFocusablesHelper = {
children = dom(element.shadowRoot || element.root || element).children; children = dom(element.shadowRoot || element.root || element).children;
// ///////////////////////// // /////////////////////////
} }
for (var i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
// Ensure method is always invoked to collect tabbable children. // Ensure method is always invoked to collect tabbable children.
needsSort = this._collectTabbableNodes(children[i], result) || needsSort; needsSort = this._collectTabbableNodes(children[i], result) || needsSort;
} }

View File

@ -81,7 +81,7 @@ export class HaStateLabelBadge extends LitElement {
? "" ? ""
: this.image : this.image
? this.image ? this.image
: state.attributes.entity_picture}" : state.attributes.entity_picture_local || state.attributes.entity_picture}"
.label="${this._computeLabel(domain, state, this._timerTimeRemaining)}" .label="${this._computeLabel(domain, state, this._timerTimeRemaining)}"
.description="${this.name ? this.name : computeStateName(state)}" .description="${this.name ? this.name : computeStateName(state)}"
></ha-label-badge> ></ha-label-badge>

View File

@ -73,10 +73,10 @@ export class StateBadge extends LitElement {
if (stateObj) { if (stateObj) {
// hide icon if we have entity picture // hide icon if we have entity picture
if ( if (
(stateObj.attributes.entity_picture && !this.overrideIcon) || ((stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture) && !this.overrideIcon) ||
this.overrideImage this.overrideImage
) { ) {
let imageUrl = this.overrideImage || stateObj.attributes.entity_picture; let imageUrl = this.overrideImage || stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture;
if (this.hass) { if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl); imageUrl = this.hass.hassUrl(imageUrl);
} }

View File

@ -10,7 +10,6 @@ import {
} from "lit-element"; } from "lit-element";
import "@material/mwc-button"; import "@material/mwc-button";
import "@material/mwc-menu"; import "@material/mwc-menu";
import "@material/mwc-list/mwc-list-item";
import type { Menu, Corner } from "@material/mwc-menu"; import type { Menu, Corner } from "@material/mwc-menu";
import "./ha-icon-button"; import "./ha-icon-button";
@ -19,14 +18,30 @@ import "./ha-icon-button";
export class HaButtonMenu extends LitElement { export class HaButtonMenu extends LitElement {
@property() public corner: Corner = "TOP_START"; @property() public corner: Corner = "TOP_START";
@property({ type: Boolean }) public multi = false;
@property({ type: Boolean }) public activatable = false;
@query("mwc-menu") private _menu?: Menu; @query("mwc-menu") private _menu?: Menu;
public get items() {
return this._menu?.items;
}
public get selected() {
return this._menu?.selected;
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div @click=${this._handleClick}> <div @click=${this._handleClick}>
<slot name="trigger"></slot> <slot name="trigger"></slot>
</div> </div>
<mwc-menu .corner=${this.corner}> <mwc-menu
.corner=${this.corner}
.multi=${this.multi}
.activatable=${this.activatable}
>
<slot></slot> <slot></slot>
</mwc-menu> </mwc-menu>
`; `;

View File

@ -66,7 +66,7 @@ export class HaCard extends LitElement {
} }
:host ::slotted(.card-actions) { :host ::slotted(.card-actions) {
border-top: 1px solid #e8e8e8; border-top: 1px solid var(--divider-color, #e8e8e8);
padding: 5px 16px; padding: 5px 16px;
} }
`; `;

View File

@ -188,10 +188,10 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// origin is wheel center // origin is wheel center
// returns {x: X, y: Y} object // returns {x: X, y: Y} object
convertToCanvasCoordinates(clientX, clientY) { convertToCanvasCoordinates(clientX, clientY) {
var svgPoint = this.interactionLayer.createSVGPoint(); const svgPoint = this.interactionLayer.createSVGPoint();
svgPoint.x = clientX; svgPoint.x = clientX;
svgPoint.y = clientY; svgPoint.y = clientY;
var cc = svgPoint.matrixTransform( const cc = svgPoint.matrixTransform(
this.interactionLayer.getScreenCTM().inverse() this.interactionLayer.getScreenCTM().inverse()
); );
return { x: cc.x, y: cc.y }; return { x: cc.x, y: cc.y };
@ -225,7 +225,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// Touch events // Touch events
onTouchStart(ev) { onTouchStart(ev) {
var touch = ev.changedTouches[0]; const touch = ev.changedTouches[0];
const cc = this.convertToCanvasCoordinates(touch.clientX, touch.clientY); const cc = this.convertToCanvasCoordinates(touch.clientX, touch.clientY);
// return if we're not on the wheel // return if we're not on the wheel
if (!this.isInWheel(cc.x, cc.y)) { if (!this.isInWheel(cc.x, cc.y)) {
@ -275,8 +275,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// Process user input to color // Process user input to color
processUserSelect(ev) { processUserSelect(ev) {
var canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY); const canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
var hs = this.getColor(canvasXY.x, canvasXY.y); const hs = this.getColor(canvasXY.x, canvasXY.y);
this.onColorSelect(hs); this.onColorSelect(hs);
} }
@ -319,11 +319,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// set marker position to the given color // set marker position to the given color
setMarkerOnColor(hs) { setMarkerOnColor(hs) {
var dist = hs.s * this.radius; const dist = hs.s * this.radius;
var theta = ((hs.h - 180) / 180) * Math.PI; const theta = ((hs.h - 180) / 180) * Math.PI;
var markerdX = -dist * Math.cos(theta); const markerdX = -dist * Math.cos(theta);
var markerdY = -dist * Math.sin(theta); const markerdY = -dist * Math.sin(theta);
var translateString = `translate(${markerdX},${markerdY})`; const translateString = `translate(${markerdX},${markerdY})`;
this.marker.setAttribute("transform", translateString); this.marker.setAttribute("transform", translateString);
this.tooltip.setAttribute("transform", translateString); this.tooltip.setAttribute("transform", translateString);
} }
@ -358,8 +358,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// get angle (degrees) // get angle (degrees)
getAngle(dX, dY) { getAngle(dX, dY) {
var theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive const theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive
var angle = (theta / Math.PI) * 180 + 180; // degrees, clockwise from right const angle = (theta / Math.PI) * 180 + 180; // degrees, clockwise from right
return angle; return angle;
} }
@ -378,9 +378,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
*/ */
getColor(x, y) { getColor(x, y) {
var hue = this.getAngle(x, y); // degrees, clockwise from right const hue = this.getAngle(x, y); // degrees, clockwise from right
var relativeDistance = this.getDistance(x, y); // edge of radius = 1 const relativeDistance = this.getDistance(x, y); // edge of radius = 1
var sat = Math.min(relativeDistance, 1); // Distance from center const sat = Math.min(relativeDistance, 1); // Distance from center
return { h: hue, s: sat }; return { h: hue, s: sat };
} }
@ -402,9 +402,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
if (this.saturationSegments === 1) { if (this.saturationSegments === 1) {
hs.s = 1; hs.s = 1;
} else { } else {
var segmentSize = 1 / this.saturationSegments; const segmentSize = 1 / this.saturationSegments;
var saturationStep = 1 / (this.saturationSegments - 1); const saturationStep = 1 / (this.saturationSegments - 1);
var calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep; const calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep;
hs.s = Math.min(calculatedSat, 1); hs.s = Math.min(calculatedSat, 1);
} }
} }
@ -477,9 +477,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
hueSegments = hueSegments || 360; // reset 0 segments to 360 hueSegments = hueSegments || 360; // reset 0 segments to 360
const angleStep = 360 / hueSegments; const angleStep = 360 / hueSegments;
const halfAngleStep = angleStep / 2; // center segments on color const halfAngleStep = angleStep / 2; // center segments on color
for (var angle = 0; angle <= 360; angle += angleStep) { for (let angle = 0; angle <= 360; angle += angleStep) {
var startAngle = (angle - halfAngleStep) * (Math.PI / 180); const startAngle = (angle - halfAngleStep) * (Math.PI / 180);
var endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180); const endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180);
context.beginPath(); context.beginPath();
context.moveTo(cX, cY); context.moveTo(cX, cY);
context.arc( context.arc(
@ -492,7 +492,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
); );
context.closePath(); context.closePath();
// gradient // gradient
var gradient = context.createRadialGradient( const gradient = context.createRadialGradient(
cX, cX,
cY, cY,
0, 0,
@ -507,8 +507,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
if (saturationSegments > 0) { if (saturationSegments > 0) {
const ratioStep = 1 / saturationSegments; const ratioStep = 1 / saturationSegments;
let ratio = 0; let ratio = 0;
for (var stop = 1; stop < saturationSegments; stop += 1) { for (let stop = 1; stop < saturationSegments; stop += 1) {
var prevLighness = lightness; const prevLighness = lightness;
ratio = stop * ratioStep; ratio = stop * ratioStep;
lightness = 100 - 50 * ratio; lightness = 100 - 50 * ratio;
gradient.addColorStop( gradient.addColorStop(

View File

@ -95,7 +95,7 @@ class HaCoverControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) { if (stateObj.state === UNAVAILABLE) {
return true; return true;
} }
var assumedState = stateObj.attributes.assumed_state === true; const assumedState = stateObj.attributes.assumed_state === true;
return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState; return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState;
} }
@ -103,7 +103,7 @@ class HaCoverControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) { if (stateObj.state === UNAVAILABLE) {
return true; return true;
} }
var assumedState = stateObj.attributes.assumed_state === true; const assumedState = stateObj.attributes.assumed_state === true;
return (entityObj.isFullyClosed || entityObj.isClosing) && !assumedState; return (entityObj.isFullyClosed || entityObj.isClosing) && !assumedState;
} }

View File

@ -75,7 +75,7 @@ class HaCoverTiltControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) { if (stateObj.state === UNAVAILABLE) {
return true; return true;
} }
var assumedState = stateObj.attributes.assumed_state === true; const assumedState = stateObj.attributes.assumed_state === true;
return entityObj.isFullyOpenTilt && !assumedState; return entityObj.isFullyOpenTilt && !assumedState;
} }
@ -83,7 +83,7 @@ class HaCoverTiltControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) { if (stateObj.state === UNAVAILABLE) {
return true; return true;
} }
var assumedState = stateObj.attributes.assumed_state === true; const assumedState = stateObj.attributes.assumed_state === true;
return entityObj.isFullyClosedTilt && !assumedState; return entityObj.isFullyClosedTilt && !assumedState;
} }

View File

@ -17,6 +17,8 @@ import "./ha-svg-icon";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@material/mwc-list/mwc-list"; import "@material/mwc-list/mwc-list";
import "./date-range-picker"; import "./date-range-picker";
import { computeRTLDirection } from "../common/util/compute_rtl";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
export interface DateRangePickerRanges { export interface DateRangePickerRanges {
[key: string]: [Date, Date]; [key: string]: [Date, Date];
@ -36,11 +38,14 @@ export class HaDateRangePicker extends LitElement {
@property({ type: Boolean }) private _hour24format = false; @property({ type: Boolean }) private _hour24format = false;
@property({ type: String }) private _rtlDirection = "ltr";
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
if (changedProps.has("hass")) { if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) { if (!oldHass || oldHass.language !== this.hass.language) {
this._hour24format = this._compute24hourFormat(); this._hour24format = this._compute24hourFormat();
this._rtlDirection = computeRTLDirection(this.hass);
} }
} }
} }
@ -76,16 +81,14 @@ export class HaDateRangePicker extends LitElement {
></paper-input> ></paper-input>
</div> </div>
${this.ranges ${this.ranges
? html`<div slot="ranges" class="date-range-ranges"> ? html`<div
<mwc-list @click=${this._setDateRange}> slot="ranges"
${Object.entries(this.ranges).map( class="date-range-ranges"
([name, dates]) => html`<mwc-list-item .dir=${this._rtlDirection}
.activated=${this.startDate.getTime() === >
dates[0].getTime() && <mwc-list @action=${this._setDateRange} activatable>
this.endDate.getTime() === dates[1].getTime()} ${Object.keys(this.ranges).map(
.startDate=${dates[0]} (name) => html`<mwc-list-item>
.endDate=${dates[1]}
>
${name} ${name}
</mwc-list-item>` </mwc-list-item>`
)} )}
@ -116,12 +119,10 @@ export class HaDateRangePicker extends LitElement {
); );
} }
private _setDateRange(ev: Event) { private _setDateRange(ev: CustomEvent<ActionDetail>) {
const target = ev.target as any; const dateRange = Object.values(this.ranges!)[ev.detail.index];
const startDate = target.startDate;
const endDate = target.endDate;
const dateRangePicker = this._dateRangePicker; const dateRangePicker = this._dateRangePicker;
dateRangePicker.clickRange([startDate, endDate]); dateRangePicker.clickRange(dateRange);
dateRangePicker.clickedApply(); dateRangePicker.clickedApply();
} }

View File

@ -5,7 +5,7 @@ import "./ha-icon-button";
import { css, CSSResult, customElement, html } from "lit-element"; import { css, CSSResult, customElement, html } from "lit-element";
import type { Constructor, HomeAssistant } from "../types"; import type { Constructor, HomeAssistant } from "../types";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { computeRTL } from "../common/util/compute_rtl"; import { computeRTLDirection } from "../common/util/compute_rtl";
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>; const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
@ -14,8 +14,8 @@ export const createCloseHeading = (hass: HomeAssistant, title: string) => html`
<mwc-icon-button <mwc-icon-button
aria-label=${hass.localize("ui.dialogs.generic.close")} aria-label=${hass.localize("ui.dialogs.generic.close")}
dialogAction="close" dialogAction="close"
?rtl=${computeRTL(hass)}
class="header_button" class="header_button"
dir=${computeRTLDirection(hass)}
> >
<ha-svg-icon path=${mdiClose}></ha-svg-icon> <ha-svg-icon path=${mdiClose}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
@ -34,10 +34,12 @@ export class HaDialog extends MwcDialog {
style, style,
css` css`
.mdc-dialog { .mdc-dialog {
--mdc-dialog-scroll-divider-color: var(--divider-color);
z-index: var(--dialog-z-index, 7); z-index: var(--dialog-z-index, 7);
} }
.mdc-dialog__actions { .mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end); justify-content: var(--justify-action-buttons, flex-end);
padding-bottom: max(env(safe-area-inset-bottom), 8px);
} }
.mdc-dialog__container { .mdc-dialog__container {
align-items: var(--vertial-align-dialog, center); align-items: var(--vertial-align-dialog, center);
@ -50,6 +52,12 @@ export class HaDialog extends MwcDialog {
position: var(--dialog-content-position, relative); position: var(--dialog-content-position, relative);
padding: var(--dialog-content-padding, 20px 24px); padding: var(--dialog-content-padding, 20px 24px);
} }
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
padding-bottom: max(
var(--dialog-content-padding, 20px),
env(safe-area-inset-bottom)
);
}
.mdc-dialog .mdc-dialog__surface { .mdc-dialog .mdc-dialog__surface {
position: var(--dialog-surface-position, relative); position: var(--dialog-surface-position, relative);
min-height: var(--mdc-dialog-min-height, auto); min-height: var(--mdc-dialog-min-height, auto);
@ -61,7 +69,7 @@ export class HaDialog extends MwcDialog {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
mwc-icon-button[rtl].header_button { [dir="rtl"].header_button {
right: auto; right: auto;
left: 16px; left: 16px;
} }

View File

@ -100,7 +100,7 @@ export interface HaFormTimeData {
} }
export interface HaFormElement extends LitElement { export interface HaFormElement extends LitElement {
schema: HaFormSchema; schema: HaFormSchema | HaFormSchema[];
data?: HaFormDataContainer | HaFormData; data?: HaFormDataContainer | HaFormData;
label?: string; label?: string;
suffix?: string; suffix?: string;
@ -110,7 +110,7 @@ export interface HaFormElement extends LitElement {
export class HaForm extends LitElement implements HaFormElement { export class HaForm extends LitElement implements HaFormElement {
@property() public data!: HaFormDataContainer | HaFormData; @property() public data!: HaFormDataContainer | HaFormData;
@property() public schema!: HaFormSchema; @property() public schema!: HaFormSchema | HaFormSchema[];
@property() public error; @property() public error;
@ -190,7 +190,7 @@ export class HaForm extends LitElement implements HaFormElement {
: ""; : "";
} }
private _computeError(error, schema: HaFormSchema) { private _computeError(error, schema: HaFormSchema | HaFormSchema[]) {
return this.computeError ? this.computeError(error, schema) : error; return this.computeError ? this.computeError(error, schema) : error;
} }
@ -203,7 +203,7 @@ export class HaForm extends LitElement implements HaFormElement {
private _valueChanged(ev: CustomEvent) { private _valueChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema; const schema = (ev.target as HaFormElement).schema as HaFormSchema;
const data = this.data as HaFormDataContainer; const data = this.data as HaFormDataContainer;
data[schema.name] = ev.detail.value; data[schema.name] = ev.detail.value;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {

View File

@ -9,6 +9,7 @@ import {
} from "lit-element"; } from "lit-element";
import { styleMap } from "lit-html/directives/style-map"; import { styleMap } from "lit-html/directives/style-map";
import { afterNextRender } from "../common/util/render-status"; import { afterNextRender } from "../common/util/render-status";
import { ifDefined } from "lit-html/directives/if-defined";
const getAngle = (value: number, min: number, max: number) => { const getAngle = (value: number, min: number, max: number) => {
const percentage = getValueInPercentage(normalize(value, min, max), min, max); const percentage = getValueInPercentage(normalize(value, min, max), min, max);
@ -27,6 +28,9 @@ const getValueInPercentage = (value: number, min: number, max: number) => {
return (100 * newVal) / newMax; return (100 * newVal) / newMax;
}; };
// Workaround for https://github.com/home-assistant/frontend/issues/6467
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
@customElement("ha-gauge") @customElement("ha-gauge")
export class Gauge extends LitElement { export class Gauge extends LitElement {
@property({ type: Number }) public min = 0; @property({ type: Number }) public min = 0;
@ -69,9 +73,28 @@ export class Gauge extends LitElement {
></path> ></path>
<path <path
class="value" class="value"
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
d="M 90 50.001 A 40 40 0 0 1 10 50" d="M 90 50.001 A 40 40 0 0 1 10 50"
></path> style=${ifDefined(
!isSafari
? styleMap({ transform: `rotate(${this._angle}deg)` })
: undefined
)}
transform=${ifDefined(
isSafari ? `rotate(${this._angle} 50 50)` : undefined
)}
>
${
isSafari
? svg`<animateTransform
attributeName="transform"
type="rotate"
from="0 50 50"
to="${this._angle} 50 50"
dur="1s"
/>`
: ""
}
</path>
</svg> </svg>
<svg class="text"> <svg class="text">
<text class="value-text"> <text class="value-text">
@ -106,8 +129,8 @@ export class Gauge extends LitElement {
fill: none; fill: none;
stroke-width: 15; stroke-width: 15;
stroke: var(--gauge-color); stroke: var(--gauge-color);
transition: all 1000ms ease 0s;
transform-origin: 50% 100%; transform-origin: 50% 100%;
transition: all 1s ease 0s;
} }
.gauge { .gauge {
display: block; display: block;

View File

@ -193,6 +193,7 @@ const mdiRemovedIcons = new Set([
"medium", "medium",
"meetup", "meetup",
"mixcloud", "mixcloud",
"mixer",
"nfc-off", "nfc-off",
"npm-variant", "npm-variant",
"npm-variant-outline", "npm-variant-outline",

View File

@ -23,7 +23,6 @@ class HaMarkdownElement extends UpdatingElement {
{ {
breaks: this.breaks, breaks: this.breaks,
gfm: true, gfm: true,
tables: true,
}, },
{ {
allowSvg: this.allowSvg, allowSvg: this.allowSvg,

View File

@ -0,0 +1,20 @@
import "@material/mwc-radio";
import type { Radio } from "@material/mwc-radio";
import { customElement } from "lit-element";
import type { Constructor } from "../types";
const MwcRadio = customElements.get("mwc-radio") as Constructor<Radio>;
@customElement("ha-radio")
export class HaRadio extends MwcRadio {
public firstUpdated() {
super.firstUpdated();
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-radio": HaRadio;
}
}

View File

@ -16,9 +16,9 @@ class HaServiceDescription extends PolymerElement {
} }
_getDescription(hass, domain, service) { _getDescription(hass, domain, service) {
var domainServices = hass.services[domain]; const domainServices = hass.services[domain];
if (!domainServices) return ""; if (!domainServices) return "";
var serviceObject = domainServices[service]; const serviceObject = domainServices[service];
if (!serviceObject) return ""; if (!serviceObject) return "";
return serviceObject.description; return serviceObject.description;
} }

View File

@ -126,7 +126,7 @@ class HaSidebar extends LitElement {
// property used only in css // property used only in css
// @ts-ignore // @ts-ignore
@property({ type: Boolean, reflect: true }) private _rtl = false; @property({ type: Boolean, reflect: true }) public rtl = false;
private _mouseLeaveTimeout?: number; private _mouseLeaveTimeout?: number;
@ -312,6 +312,7 @@ class HaSidebar extends LitElement {
hass.panelUrl !== oldHass.panelUrl || hass.panelUrl !== oldHass.panelUrl ||
hass.user !== oldHass.user || hass.user !== oldHass.user ||
hass.localize !== oldHass.localize || hass.localize !== oldHass.localize ||
hass.language !== oldHass.language ||
hass.states !== oldHass.states || hass.states !== oldHass.states ||
hass.defaultPanel !== oldHass.defaultPanel hass.defaultPanel !== oldHass.defaultPanel
); );
@ -339,12 +340,14 @@ class HaSidebar extends LitElement {
return; return;
} }
this._rtl = computeRTL(this.hass); const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
this.rtl = computeRTL(this.hass);
}
if (!SUPPORT_SCROLL_IF_NEEDED) { if (!SUPPORT_SCROLL_IF_NEEDED) {
return; return;
} }
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.panelUrl !== this.hass.panelUrl) { if (!oldHass || oldHass.panelUrl !== this.hass.panelUrl) {
const selectedEl = this.shadowRoot!.querySelector(".iron-selected"); const selectedEl = this.shadowRoot!.querySelector(".iron-selected");
if (selectedEl) { if (selectedEl) {
@ -496,9 +499,12 @@ class HaSidebar extends LitElement {
width: 64px; width: 64px;
} }
:host([expanded]) { :host([expanded]) {
width: 256px; width: calc(256px + env(safe-area-inset-left));
}
:host([rtl]) {
border-right: 0;
border-left: 1px solid var(--divider-color);
} }
.menu { .menu {
box-sizing: border-box; box-sizing: border-box;
height: 65px; height: 65px;
@ -512,18 +518,25 @@ class HaSidebar extends LitElement {
background-color: var(--primary-background-color); background-color: var(--primary-background-color);
font-size: 20px; font-size: 20px;
align-items: center; align-items: center;
padding-left: calc(8.5px + env(safe-area-inset-left));
}
:host([rtl]) .menu {
padding-left: 8.5px;
padding-right: calc(8.5px + env(safe-area-inset-right));
} }
:host([expanded]) .menu { :host([expanded]) .menu {
width: 256px; width: calc(256px + env(safe-area-inset-left));
}
:host([rtl][expanded]) .menu {
width: calc(256px + env(safe-area-inset-right));
} }
.menu mwc-icon-button { .menu mwc-icon-button {
color: var(--sidebar-icon-color); color: var(--sidebar-icon-color);
} }
:host([expanded]) .menu mwc-icon-button { :host([expanded]) .menu mwc-icon-button {
margin-right: 23px; margin-right: 23px;
} }
:host([expanded][_rtl]) .menu mwc-icon-button { :host([expanded][rtl]) .menu mwc-icon-button {
margin-right: 0px; margin-right: 0px;
margin-left: 23px; margin-left: 23px;
} }
@ -551,12 +564,18 @@ class HaSidebar extends LitElement {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
height: calc(100% - 196px); height: calc(100% - 196px - env(safe-area-inset-bottom));
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
scrollbar-color: var(--scrollbar-thumb-color) transparent; scrollbar-color: var(--scrollbar-thumb-color) transparent;
scrollbar-width: thin; scrollbar-width: thin;
background: none; background: none;
margin-left: env(safe-area-inset-left);
}
:host([rtl]) paper-listbox {
margin-left: initial;
margin-right: env(safe-area-inset-right);
} }
a { a {
@ -580,7 +599,7 @@ class HaSidebar extends LitElement {
:host([expanded]) paper-icon-item { :host([expanded]) paper-icon-item {
width: 240px; width: 240px;
} }
:host([_rtl]) paper-icon-item { :host([rtl]) paper-icon-item {
padding-left: auto; padding-left: auto;
padding-right: 12px; padding-right: 12px;
} }
@ -656,6 +675,11 @@ class HaSidebar extends LitElement {
} }
.notifications-container { .notifications-container {
display: flex; display: flex;
margin-left: env(safe-area-inset-left);
}
:host([rtl]) .notifications-container {
margin-left: initial;
margin-right: env(safe-area-inset-right);
} }
.notifications { .notifications {
cursor: pointer; cursor: pointer;
@ -664,18 +688,23 @@ class HaSidebar extends LitElement {
flex: 1; flex: 1;
} }
.profile { .profile {
margin-left: env(safe-area-inset-left);
}
:host([rtl]) .profile {
margin-left: initial;
margin-right: env(safe-area-inset-right);
} }
.profile paper-icon-item { .profile paper-icon-item {
padding-left: 4px; padding-left: 4px;
} }
:host([_rtl]) .profile paper-icon-item { :host([rtl]) .profile paper-icon-item {
padding-left: auto; padding-left: auto;
padding-right: 4px; padding-right: 4px;
} }
.profile .item-text { .profile .item-text {
margin-left: 8px; margin-left: 8px;
} }
:host([_rtl]) .profile .item-text { :host([rtl]) .profile .item-text {
margin-right: 8px; margin-right: 8px;
} }
@ -688,7 +717,7 @@ class HaSidebar extends LitElement {
line-height: 20px; line-height: 20px;
text-align: center; text-align: center;
padding: 0px 6px; padding: 0px 6px;
color: var(--text-primary-color); color: var(--text-accent-color, var(--text-primary-color));
} }
ha-svg-icon + .notification-badge { ha-svg-icon + .notification-badge {
position: absolute; position: absolute;
@ -735,7 +764,7 @@ class HaSidebar extends LitElement {
font-weight: 500; font-weight: 500;
} }
:host([_rtl]) .menu mwc-icon-button { :host([rtl]) .menu mwc-icon-button {
-webkit-transform: scaleX(-1); -webkit-transform: scaleX(-1);
transform: scaleX(-1); transform: scaleX(-1);
} }

View File

@ -50,6 +50,8 @@ export class HaYamlEditor extends LitElement {
try { try {
this._yaml = value && !isEmpty(value) ? safeDump(value) : ""; this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
} catch (err) { } catch (err) {
// eslint-disable-next-line no-console
console.error(err);
alert(`There was an error converting to YAML: ${err}`); alert(`There was an error converting to YAML: ${err}`);
} }
afterNextRender(() => { afterNextRender(() => {

View File

@ -6,6 +6,7 @@ import {
LeafletMouseEvent, LeafletMouseEvent,
Map, Map,
Marker, Marker,
TileLayer,
} from "leaflet"; } from "leaflet";
import { import {
css, css,
@ -21,15 +22,19 @@ import { fireEvent } from "../../common/dom/fire_event";
import { import {
LeafletModuleType, LeafletModuleType,
setupLeafletMap, setupLeafletMap,
replaceTileLayer,
} from "../../common/dom/setup-leaflet-map"; } from "../../common/dom/setup-leaflet-map";
import { nextRender } from "../../common/util/render-status"; import { nextRender } from "../../common/util/render-status";
import { defaultRadiusColor } from "../../data/zone"; import { defaultRadiusColor } from "../../data/zone";
import { HomeAssistant } from "../../types";
@customElement("ha-location-editor") @customElement("ha-location-editor")
class LocationEditor extends LitElement { class LocationEditor extends LitElement {
@property() public location?: [number, number]; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public radius?: number; @property({ type: Array }) public location?: [number, number];
@property({ type: Number }) public radius?: number;
@property() public radiusColor?: string; @property() public radiusColor?: string;
@ -46,6 +51,8 @@ class LocationEditor extends LitElement {
private _leafletMap?: Map; private _leafletMap?: Map;
private _tileLayer?: TileLayer;
private _locationMarker?: Marker | Circle; private _locationMarker?: Marker | Circle;
public fitMap(): void { public fitMap(): void {
@ -97,6 +104,22 @@ class LocationEditor extends LitElement {
if (changedProps.has("icon")) { if (changedProps.has("icon")) {
this._updateIcon(); this._updateIcon();
} }
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
return;
}
if (!this._leafletMap || !this._tileLayer) {
return;
}
this._tileLayer = replaceTileLayer(
this.Leaflet,
this._leafletMap,
this._tileLayer,
this.hass.themes.darkMode
);
}
} }
private get _mapEl(): HTMLDivElement { private get _mapEl(): HTMLDivElement {
@ -104,9 +127,9 @@ class LocationEditor extends LitElement {
} }
private async _initMap(): Promise<void> { private async _initMap(): Promise<void> {
[this._leafletMap, this.Leaflet] = await setupLeafletMap( [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl, this._mapEl,
false, this.hass.themes.darkMode,
Boolean(this.radius) Boolean(this.radius)
); );
this._leafletMap.addEventListener( this._leafletMap.addEventListener(
@ -255,9 +278,6 @@ class LocationEditor extends LitElement {
#map { #map {
height: 100%; height: 100%;
} }
.light {
color: #000000;
}
.leaflet-edit-move { .leaflet-edit-move {
border-radius: 50%; border-radius: 50%;
cursor: move !important; cursor: move !important;

View File

@ -6,6 +6,7 @@ import {
Map, Map,
Marker, Marker,
MarkerOptions, MarkerOptions,
TileLayer,
} from "leaflet"; } from "leaflet";
import { import {
css, css,
@ -21,8 +22,10 @@ import { fireEvent } from "../../common/dom/fire_event";
import { import {
LeafletModuleType, LeafletModuleType,
setupLeafletMap, setupLeafletMap,
replaceTileLayer,
} from "../../common/dom/setup-leaflet-map"; } from "../../common/dom/setup-leaflet-map";
import { defaultRadiusColor } from "../../data/zone"; import { defaultRadiusColor } from "../../data/zone";
import { HomeAssistant } from "../../types";
declare global { declare global {
// for fire event // for fire event
@ -47,6 +50,8 @@ export interface MarkerLocation {
@customElement("ha-locations-editor") @customElement("ha-locations-editor")
export class HaLocationsEditor extends LitElement { export class HaLocationsEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public locations?: MarkerLocation[]; @property() public locations?: MarkerLocation[];
public fitZoom = 16; public fitZoom = 16;
@ -57,6 +62,8 @@ export class HaLocationsEditor extends LitElement {
// eslint-disable-next-line // eslint-disable-next-line
private _leafletMap?: Map; private _leafletMap?: Map;
private _tileLayer?: TileLayer;
private _locationMarkers?: { [key: string]: Marker | Circle }; private _locationMarkers?: { [key: string]: Marker | Circle };
private _circles: { [key: string]: Circle } = {}; private _circles: { [key: string]: Circle } = {};
@ -116,6 +123,22 @@ export class HaLocationsEditor extends LitElement {
if (changedProps.has("locations")) { if (changedProps.has("locations")) {
this._updateMarkers(); this._updateMarkers();
} }
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
return;
}
if (!this._leafletMap || !this._tileLayer) {
return;
}
this._tileLayer = replaceTileLayer(
this.Leaflet,
this._leafletMap,
this._tileLayer,
this.hass.themes.darkMode
);
}
} }
private get _mapEl(): HTMLDivElement { private get _mapEl(): HTMLDivElement {
@ -123,9 +146,9 @@ export class HaLocationsEditor extends LitElement {
} }
private async _initMap(): Promise<void> { private async _initMap(): Promise<void> {
[this._leafletMap, this.Leaflet] = await setupLeafletMap( [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl, this._mapEl,
false, this.hass.themes.darkMode,
true true
); );
this._updateMarkers(); this._updateMarkers();
@ -290,9 +313,6 @@ export class HaLocationsEditor extends LitElement {
#map { #map {
height: 100%; height: 100%;
} }
.light {
color: #000000;
}
.leaflet-marker-draggable { .leaflet-marker-draggable {
cursor: move !important; cursor: move !important;
} }

View File

@ -1,5 +1,5 @@
import "../ha-icon-button"; import "../ha-icon-button";
import { Circle, Layer, Map, Marker } from "leaflet"; import { Circle, Layer, Map, Marker, TileLayer } from "leaflet";
import { import {
css, css,
CSSResult, CSSResult,
@ -13,6 +13,7 @@ import {
import { import {
LeafletModuleType, LeafletModuleType,
setupLeafletMap, setupLeafletMap,
replaceTileLayer,
} from "../../common/dom/setup-leaflet-map"; } from "../../common/dom/setup-leaflet-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
@ -22,11 +23,11 @@ import { HomeAssistant } from "../../types";
@customElement("ha-map") @customElement("ha-map")
class HaMap extends LitElement { class HaMap extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public entities?: string[]; @property() public entities?: string[];
@property() public darkMode = false; @property() public darkMode?: boolean;
@property() public zoom?: number; @property() public zoom?: number;
@ -35,6 +36,8 @@ class HaMap extends LitElement {
private _leafletMap?: Map; private _leafletMap?: Map;
private _tileLayer?: TileLayer;
// @ts-ignore // @ts-ignore
private _resizeObserver?: ResizeObserver; private _resizeObserver?: ResizeObserver;
@ -122,6 +125,20 @@ class HaMap extends LitElement {
if (changedProps.has("hass")) { if (changedProps.has("hass")) {
this._drawEntities(); this._drawEntities();
this._fitMap(); this._fitMap();
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.themes.darkMode === this.hass.themes.darkMode) {
return;
}
if (!this.Leaflet || !this._leafletMap || !this._tileLayer) {
return;
}
this._tileLayer = replaceTileLayer(
this.Leaflet,
this._leafletMap,
this._tileLayer,
this.hass.themes.darkMode
);
} }
} }
@ -130,9 +147,9 @@ class HaMap extends LitElement {
} }
private async loadMap(): Promise<void> { private async loadMap(): Promise<void> {
[this._leafletMap, this.Leaflet] = await setupLeafletMap( [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap(
this._mapEl, this._mapEl,
this.darkMode this.darkMode ?? this.hass.themes.darkMode
); );
this._drawEntities(); this._drawEntities();
this._leafletMap.invalidateSize(); this._leafletMap.invalidateSize();
@ -229,7 +246,8 @@ class HaMap extends LitElement {
icon: Leaflet.divIcon({ icon: Leaflet.divIcon({
html: iconHTML, html: iconHTML,
iconSize: [24, 24], iconSize: [24, 24],
className: this.darkMode ? "dark" : "light", className:
this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light",
}), }),
interactive: false, interactive: false,
title, title,

View File

@ -322,9 +322,9 @@ export class PaperTimeInput extends PolymerElement {
* @return {boolean} * @return {boolean}
*/ */
validate() { validate() {
var valid = true; let valid = true;
// Validate hour & min fields // Validate hour & min fields
if (!this.$.hour.validate() | !this.$.min.validate()) { if (!this.$.hour.validate() || !this.$.min.validate()) {
valid = false; valid = false;
} }
// Validate second field // Validate second field

View File

@ -57,7 +57,7 @@ class StateBadge extends LitElement {
text-align: center; text-align: center;
background-color: var(--light-primary-color); background-color: var(--light-primary-color);
text-decoration: none; text-decoration: none;
color: var(--primary-text-color); color: var(--text-light-primary-color, var(--primary-text-color));
overflow: hidden; overflow: hidden;
} }

View File

@ -46,8 +46,8 @@ export interface MqttTrigger {
export interface GeoLocationTrigger { export interface GeoLocationTrigger {
platform: "geo_location"; platform: "geo_location";
source: "string"; source: string;
zone: "string"; zone: string;
event: "enter" | "leave"; event: "enter" | "leave";
} }

View File

@ -1,5 +1,6 @@
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { HaFormSchema } from "../components/ha-form/ha-form";
export interface DeviceAutomation { export interface DeviceAutomation {
device_id: string; device_id: string;
@ -20,6 +21,10 @@ export interface DeviceTrigger extends DeviceAutomation {
platform: "device"; platform: "device";
} }
export interface DeviceCapabilities {
extra_fields: HaFormSchema[];
}
export const fetchDeviceActions = (hass: HomeAssistant, deviceId: string) => export const fetchDeviceActions = (hass: HomeAssistant, deviceId: string) =>
hass.callWS<DeviceAction[]>({ hass.callWS<DeviceAction[]>({
type: "device_automation/action/list", type: "device_automation/action/list",
@ -42,7 +47,7 @@ export const fetchDeviceActionCapabilities = (
hass: HomeAssistant, hass: HomeAssistant,
action: DeviceAction action: DeviceAction
) => ) =>
hass.callWS<DeviceAction[]>({ hass.callWS<DeviceCapabilities>({
type: "device_automation/action/capabilities", type: "device_automation/action/capabilities",
action, action,
}); });
@ -51,7 +56,7 @@ export const fetchDeviceConditionCapabilities = (
hass: HomeAssistant, hass: HomeAssistant,
condition: DeviceCondition condition: DeviceCondition
) => ) =>
hass.callWS<DeviceCondition[]>({ hass.callWS<DeviceCapabilities>({
type: "device_automation/condition/capabilities", type: "device_automation/condition/capabilities",
condition, condition,
}); });
@ -60,7 +65,7 @@ export const fetchDeviceTriggerCapabilities = (
hass: HomeAssistant, hass: HomeAssistant,
trigger: DeviceTrigger trigger: DeviceTrigger
) => ) =>
hass.callWS<DeviceTrigger[]>({ hass.callWS<DeviceCapabilities>({
type: "device_automation/trigger/capabilities", type: "device_automation/trigger/capabilities",
trigger, trigger,
}); });

View File

@ -8,6 +8,7 @@ export interface DeviceRegistryEntry {
id: string; id: string;
config_entries: string[]; config_entries: string[];
connections: Array<[string, string]>; connections: Array<[string, string]>;
identifiers: Array<[string, string]>;
manufacturer: string; manufacturer: string;
model?: string; model?: string;
name?: string; name?: string;

View File

@ -58,6 +58,31 @@ export interface WaitAction {
timeout?: number; timeout?: number;
} }
export interface RepeatAction {
repeat: CountRepeat | WhileRepeat | UntilRepeat;
}
interface BaseRepeat {
sequence: Action[];
}
export interface CountRepeat extends BaseRepeat {
count: number;
}
export interface WhileRepeat extends BaseRepeat {
while: Condition[];
}
export interface UntilRepeat extends BaseRepeat {
until: Condition[];
}
export interface ChooseAction {
choose: [{ conditions: Condition[]; sequence: Action[] }];
default?: Action[];
}
export type Action = export type Action =
| EventAction | EventAction
| DeviceAction | DeviceAction
@ -65,7 +90,9 @@ export type Action =
| Condition | Condition
| DelayAction | DelayAction
| SceneAction | SceneAction
| WaitAction; | WaitAction
| RepeatAction
| ChooseAction;
export const triggerScript = ( export const triggerScript = (
hass: HomeAssistant, hass: HomeAssistant,

View File

@ -165,7 +165,6 @@ class DialogConfigEntrySystemOptions extends LitElement {
haStyleDialog, haStyleDialog,
css` css`
ha-paper-dialog { ha-paper-dialog {
min-width: 400px;
max-width: 500px; max-width: 500px;
} }
.init-spinner { .init-spinner {

View File

@ -15,7 +15,8 @@ declare global {
} }
} }
interface HassDialog<T = HASSDomEvents[ValidHassDomEvent]> extends HTMLElement { export interface HassDialog<T = HASSDomEvents[ValidHassDomEvent]>
extends HTMLElement {
showDialog(params: T); showDialog(params: T);
closeDialog?: () => boolean | void; closeDialog?: () => boolean | void;
} }

View File

@ -123,12 +123,12 @@ class MoreInfoConfigurator extends PolymerElement {
} }
fieldChanged(ev) { fieldChanged(ev) {
var el = ev.target; const el = ev.target;
this.fieldInput[el.name] = el.value; this.fieldInput[el.name] = el.value;
} }
submitClicked() { submitClicked() {
var data = { const data = {
configure_id: this.stateObj.attributes.configure_id, configure_id: this.stateObj.attributes.configure_id,
fields: this.fieldInput, fields: this.fieldInput,
}; };

View File

@ -97,7 +97,7 @@ class MoreInfoCover extends LocalizeMixin(PolymerElement) {
} }
computeClassNames(stateObj) { computeClassNames(stateObj) {
var classes = [ const classes = [
attributeClassNames(stateObj, [ attributeClassNames(stateObj, [
"current_position", "current_position",
"current_tilt_position", "current_tilt_position",

View File

@ -140,8 +140,8 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
} }
speedChanged(ev) { speedChanged(ev) {
var oldVal = this.stateObj.attributes.speed; const oldVal = this.stateObj.attributes.speed;
var newVal = ev.detail.value; const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return; if (!newVal || oldVal === newVal) return;
@ -152,8 +152,8 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
} }
oscillationToggleChanged(ev) { oscillationToggleChanged(ev) {
var oldVal = this.stateObj.attributes.oscillating; const oldVal = this.stateObj.attributes.oscillating;
var newVal = ev.target.checked; const newVal = ev.target.checked;
if (oldVal === newVal) return; if (oldVal === newVal) return;

View File

@ -53,11 +53,11 @@ class MoreInfoGroup extends PolymerElement {
} }
computeStates(stateObj, hass) { computeStates(stateObj, hass) {
var states = []; const states = [];
var entIds = stateObj.attributes.entity_id || []; const entIds = stateObj.attributes.entity_id || [];
for (var i = 0; i < entIds.length; i++) { for (let i = 0; i < entIds.length; i++) {
var state = hass.states[entIds[i]]; const state = hass.states[entIds[i]];
if (state) { if (state) {
states.push(state); states.push(state);

View File

@ -78,14 +78,14 @@ class DatetimeInput extends PolymerElement {
if (stateObj.state === "unknown") { if (stateObj.state === "unknown") {
return ""; return "";
} }
var monthFiller; let monthFiller;
if (stateObj.attributes.month < 10) { if (stateObj.attributes.month < 10) {
monthFiller = "0"; monthFiller = "0";
} else { } else {
monthFiller = ""; monthFiller = "";
} }
var dayFiller; let dayFiller;
if (stateObj.attributes.day < 10) { if (stateObj.attributes.day < 10) {
dayFiller = "0"; dayFiller = "0";
} else { } else {
@ -119,15 +119,18 @@ class DatetimeInput extends PolymerElement {
}; };
if (this.stateObj.attributes.has_time) { if (this.stateObj.attributes.has_time) {
changed |= changed =
changed ||
parseInt(this.selectedMinute) !== this.stateObj.attributes.minute; parseInt(this.selectedMinute) !== this.stateObj.attributes.minute;
changed |= parseInt(this.selectedHour) !== this.stateObj.attributes.hour; changed =
changed ||
parseInt(this.selectedHour) !== this.stateObj.attributes.hour;
if (this.selectedMinute < 10) { if (this.selectedMinute < 10) {
minuteFiller = "0"; minuteFiller = "0";
} else { } else {
minuteFiller = ""; minuteFiller = "";
} }
var timeStr = const timeStr =
this.selectedHour + ":" + minuteFiller + this.selectedMinute; this.selectedHour + ":" + minuteFiller + this.selectedMinute;
serviceData.time = timeStr; serviceData.time = timeStr;
} }
@ -144,7 +147,7 @@ class DatetimeInput extends PolymerElement {
this.stateObj.attributes.day this.stateObj.attributes.day
); );
changed |= dateValState !== dateValInput; changed = changed || dateValState !== dateValInput;
serviceData.date = this.selectedDate; serviceData.date = this.selectedDate;
} }

View File

@ -290,8 +290,8 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
} }
effectChanged(ev) { effectChanged(ev) {
var oldVal = this.stateObj.attributes.effect; const oldVal = this.stateObj.attributes.effect;
var newVal = ev.detail.value; const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return; if (!newVal || oldVal === newVal) return;
@ -302,7 +302,7 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
} }
brightnessSliderChanged(ev) { brightnessSliderChanged(ev) {
var bri = parseInt(ev.target.value, 10); const bri = parseInt(ev.target.value, 10);
if (isNaN(bri)) return; if (isNaN(bri)) return;
@ -313,7 +313,7 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
} }
ctSliderChanged(ev) { ctSliderChanged(ev) {
var ct = parseInt(ev.target.value, 10); const ct = parseInt(ev.target.value, 10);
if (isNaN(ct)) return; if (isNaN(ct)) return;
@ -324,7 +324,7 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
} }
wvSliderChanged(ev) { wvSliderChanged(ev) {
var wv = parseInt(ev.target.value, 10); const wv = parseInt(ev.target.value, 10);
if (isNaN(wv)) return; if (isNaN(wv)) return;

View File

@ -329,8 +329,8 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
handleSourceChanged(ev) { handleSourceChanged(ev) {
if (!this.playerObj) return; if (!this.playerObj) return;
var oldVal = this.playerObj.source; const oldVal = this.playerObj.source;
var newVal = ev.detail.value; const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return; if (!newVal || oldVal === newVal) return;
@ -340,8 +340,8 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
handleSoundModeChanged(ev) { handleSoundModeChanged(ev) {
if (!this.playerObj) return; if (!this.playerObj) return;
var oldVal = this.playerObj.soundMode; const oldVal = this.playerObj.soundMode;
var newVal = ev.detail.value; const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return; if (!newVal || oldVal === newVal) return;

View File

@ -193,7 +193,7 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
4: "has-away_mode", 4: "has-away_mode",
}; };
var classes = [featureClassNames(stateObj, _featureClassNames)]; const classes = [featureClassNames(stateObj, _featureClassNames)];
classes.push("more-info-water_heater"); classes.push("more-info-water_heater");

View File

@ -254,7 +254,7 @@ export class MoreInfoDialog extends LitElement {
ha-header-bar { ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color); --mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--card-background-color); --mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0; flex-shrink: 0;
border-bottom: 1px solid border-bottom: 1px solid
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
@ -302,13 +302,15 @@ export class MoreInfoDialog extends LitElement {
} }
} }
state-history-charts {
margin-top: 16px 0;
}
ha-dialog[data-domain="camera"] { ha-dialog[data-domain="camera"] {
--dialog-content-padding: 0; --dialog-content-padding: 0;
} }
state-card-content,
state-history-charts {
display: block;
margin-bottom: 16px;
}
`, `,
]; ];
} }

View File

@ -29,9 +29,17 @@ export class HuiNotificationDrawer extends EventsMixin(
width: calc(100% - 32px); width: calc(100% - 32px);
} }
div[main-title] {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
.notifications { .notifications {
overflow-y: auto; overflow-y: auto;
padding-top: 16px; padding-top: 16px;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
height: calc(100% - 65px); height: calc(100% - 65px);
box-sizing: border-box; box-sizing: border-box;
background-color: var(--primary-background-color); background-color: var(--primary-background-color);

View File

@ -426,7 +426,7 @@ export class HaVoiceCommandDialog extends LitElement {
text-align: right; text-align: right;
border-bottom-right-radius: 0px; border-bottom-right-radius: 0px;
background-color: var(--light-primary-color); background-color: var(--light-primary-color);
color: var(--primary-text-color); color: var(--text-light-primary-color, var(--primary-text-color));
} }
.message.hass { .message.hass {

View File

@ -84,7 +84,7 @@ function initPushNotifications() {
} }
self.addEventListener("push", function (event) { self.addEventListener("push", function (event) {
var data; let data;
if (event.data) { if (event.data) {
data = event.data.json(); data = event.data.json();
if (data.dismiss) { if (data.dismiss) {
@ -113,8 +113,6 @@ function initPushNotifications() {
}); });
self.addEventListener("notificationclick", function (event) { self.addEventListener("notificationclick", function (event) {
var url;
notificationEventCallback("clicked", event); notificationEventCallback("clicked", event);
event.notification.close(); event.notification.close();
@ -127,7 +125,7 @@ function initPushNotifications() {
return; return;
} }
url = event.notification.data.url; const url = event.notification.data.url;
if (!url) return; if (!url) return;
@ -137,8 +135,8 @@ function initPushNotifications() {
type: "window", type: "window",
}) })
.then(function (windowClients) { .then(function (windowClients) {
var i; let i;
var client; let client;
for (i = 0; i < windowClients.length; i++) { for (i = 0; i < windowClients.length; i++) {
client = windowClients[i]; client = windowClients[i];
if (client.url === url && "focus" in client) { if (client.url === url && "focus" in client) {

View File

@ -180,7 +180,9 @@ export const provideHass = (
config: demoConfig, config: demoConfig,
themes: { themes: {
default_theme: "default", default_theme: "default",
default_dark_theme: null,
themes: {}, themes: {},
darkMode: false,
}, },
panels: demoPanels, panels: demoPanels,
services: demoServices, services: demoServices,
@ -253,7 +255,7 @@ export const provideHass = (
mockTheme(theme) { mockTheme(theme) {
invalidateThemeCache(); invalidateThemeCache();
hass().updateHass({ hass().updateHass({
selectedTheme: theme ? "mock" : "default", selectedTheme: { theme: theme ? "mock" : "default" },
themes: { themes: {
...hass().themes, ...hass().themes,
themes: { themes: {
@ -265,7 +267,7 @@ export const provideHass = (
applyThemesOnElement( applyThemesOnElement(
document.documentElement, document.documentElement,
themes, themes,
selectedTheme as string selectedTheme!.theme
); );
}, },

View File

@ -1,4 +1,4 @@
<meta name='viewport' content='width=device-width, user-scalable=no'> <meta name='viewport' content='width=device-width, user-scalable=no, viewport-fit=cover'>
<style> <style>
body { body {
font-family: Roboto, sans-serif; font-family: Roboto, sans-serif;
@ -7,6 +7,6 @@
font-weight: 400; font-weight: 400;
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100vh; height: 100%;
} }
</style> </style>

View File

@ -5,14 +5,16 @@
<link rel="preload" href="<%= latestAppJS %>" as="script" crossorigin="use-credentials" /> <link rel="preload" href="<%= latestAppJS %>" as="script" crossorigin="use-credentials" />
<%= renderTemplate('_header') %> <%= renderTemplate('_header') %>
<title>Home Assistant</title> <title>Home Assistant</title>
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="180x180" sizes="180x180"
href="/static/icons/favicon-apple-180x180.png" href="/static/icons/favicon-apple-180x180.png"
/> />
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
<meta name="apple-itunes-app" content="app-id=1099568401" /> <meta name="apple-itunes-app" content="app-id=1099568401" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Home Assistant">
<meta <meta
name="msapplication-square70x70logo" name="msapplication-square70x70logo"
content="/static/icons/tile-win-70x70.png" content="/static/icons/tile-win-70x70.png"
@ -33,6 +35,7 @@
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<meta name="referrer" content="same-origin" /> <meta name="referrer" content="same-origin" />
<meta name="theme-color" content="#THEMEC" /> <meta name="theme-color" content="#THEMEC" />
<meta name="color-scheme" content="dark light" />
<style> <style>
#ha-init-skeleton::before { #ha-init-skeleton::before {
display: block; display: block;
@ -41,7 +44,7 @@
background-color: #THEMEC; background-color: #THEMEC;
} }
html { html {
background-color: var(--primary-background-color, #fafafa); background-color: var(--primary-background-color);
} }
</style> </style>
</head> </head>

View File

@ -77,6 +77,8 @@ class HaAppLayout extends customElements.get("app-header-layout") {
/* Using 'transform' will cause 'position: fixed' elements to behave like /* Using 'transform' will cause 'position: fixed' elements to behave like
'position: absolute' relative to this element. */ 'position: absolute' relative to this element. */
transform: translate(0); transform: translate(0);
margin-left: env(safe-area-inset-left);
margin-right: env(safe-area-inset-right);
} }
@media print { @media print {

View File

@ -60,6 +60,11 @@ class HassSubpage extends LitElement {
background-color: var(--primary-background-color); background-color: var(--primary-background-color);
} }
:host([narrow]) {
width: 100%;
position: fixed;
}
.toolbar { .toolbar {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -26,9 +26,9 @@ import { computeRTLDirection } from "../common/util/compute_rtl";
export class HaTabsSubpageDataTable extends LitElement { export class HaTabsSubpageDataTable extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean; @property({ type: Boolean }) public isWide = false;
@property({ type: Boolean, reflect: true }) public narrow!: boolean; @property({ type: Boolean, reflect: true }) public narrow = false;
/** /**
* Object with the columns. * Object with the columns.
@ -110,6 +110,7 @@ export class HaTabsSubpageDataTable extends LitElement {
<hass-tabs-subpage <hass-tabs-subpage
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.isWide=${this.isWide}
.backPath=${this.backPath} .backPath=${this.backPath}
.backCallback=${this.backCallback} .backCallback=${this.backCallback}
.route=${this.route} .route=${this.route}
@ -168,38 +169,37 @@ export class HaTabsSubpageDataTable extends LitElement {
? html` ? html`
<div slot="header"> <div slot="header">
<slot name="header"> <slot name="header">
<slot name="header"> <div class="table-header">
<div class="table-header"> <search-input
<search-input .filter=${this.filter}
.filter=${this.filter} no-label-float
no-label-float no-underline
no-underline @value-changed=${this._handleSearchChange}
@value-changed=${this._handleSearchChange} .label=${this.hass.localize(
.label=${this.hass.localize( "ui.components.data-table.search"
"ui.components.data-table.search" )}
)} >
> </search-input>
</search-input> ${this.activeFilters
${this.activeFilters ? html`<div class="active-filters">
? html`<div class="active-filters"> ${this.hass.localize(
${this.hass.localize( "ui.panel.config.filtering.filtering_by"
"ui.panel.config.filtering.filtering_by" )}
)} ${this.activeFilters.join(", ")}
${this.activeFilters.join(", ")} <mwc-button @click=${this._clearFilter}
<mwc-button @click=${this._clearFilter} >${this.hass.localize(
>${this.hass.localize( "ui.panel.config.filtering.clear"
"ui.panel.config.filtering.clear" )}</mwc-button
)}</mwc-button >
> </div>`
</div>` : ""}
: ""} </div>
</div></slot </slot>
></slot
>
</div> </div>
` `
: html` <div slot="header"></div> `} : html` <div slot="header"></div> `}
</ha-data-table> </ha-data-table>
<div slot="fab"><slot name="fab"></slot></div>
</hass-tabs-subpage> </hass-tabs-subpage>
`; `;
} }

View File

@ -22,6 +22,7 @@ import "../components/ha-svg-icon";
import "../components/ha-icon"; import "../components/ha-icon";
import "../components/ha-tab"; import "../components/ha-tab";
import { restoreScroll } from "../common/decorators/restore-scroll"; import { restoreScroll } from "../common/decorators/restore-scroll";
import { computeRTL } from "../common/util/compute_rtl";
export interface PageNavigation { export interface PageNavigation {
path: string; path: string;
@ -53,6 +54,11 @@ class HassTabsSubpage extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false; @property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean, reflect: true, attribute: "is-wide" })
public isWide = false;
@property({ type: Boolean, reflect: true }) public rtl = false;
@internalProperty() private _activeTab?: PageNavigation; @internalProperty() private _activeTab?: PageNavigation;
// @ts-ignore // @ts-ignore
@ -107,6 +113,14 @@ class HassTabsSubpage extends LitElement {
`${this.route.prefix}${this.route.path}`.includes(tab.path) `${this.route.prefix}${this.route.path}`.includes(tab.path)
); );
} }
if (changedProperties.has("hass")) {
const oldHass = changedProperties.get("hass") as
| HomeAssistant
| undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
this.rtl = computeRTL(this.hass);
}
}
} }
protected render(): TemplateResult { protected render(): TemplateResult {
@ -152,6 +166,7 @@ class HassTabsSubpage extends LitElement {
<div class="content" @scroll=${this._saveScrollPos}> <div class="content" @scroll=${this._saveScrollPos}>
<slot></slot> <slot></slot>
</div> </div>
<div id="fab"><slot name="fab"></slot></div>
`; `;
} }
@ -184,6 +199,11 @@ class HassTabsSubpage extends LitElement {
background-color: var(--primary-background-color); background-color: var(--primary-background-color);
} }
:host([narrow]) {
width: 100%;
position: fixed;
}
ha-menu-button { ha-menu-button {
margin-right: 24px; margin-right: 24px;
} }
@ -215,9 +235,10 @@ class HassTabsSubpage extends LitElement {
background-color: var(--sidebar-background-color); background-color: var(--sidebar-background-color);
border-top: 1px solid var(--divider-color); border-top: 1px solid var(--divider-color);
justify-content: space-between; justify-content: space-between;
z-index: 1; z-index: 2;
font-size: 12px; font-size: 12px;
width: 100%; width: 100%;
padding-bottom: env(safe-area-inset-bottom);
} }
#tabbar:not(.bottom-bar) { #tabbar:not(.bottom-bar) {
@ -247,7 +268,11 @@ class HassTabsSubpage extends LitElement {
.content { .content {
position: relative; position: relative;
width: 100%; width: calc(
100% - env(safe-area-inset-left) - env(safe-area-inset-right)
);
margin-left: env(safe-area-inset-left);
margin-right: env(safe-area-inset-right);
height: calc(100% - 65px); height: calc(100% - 65px);
overflow-y: auto; overflow-y: auto;
overflow: auto; overflow: auto;
@ -256,6 +281,30 @@ class HassTabsSubpage extends LitElement {
:host([narrow]) .content { :host([narrow]) .content {
height: calc(100% - 128px); height: calc(100% - 128px);
height: calc(100% - 128px - env(safe-area-inset-bottom));
}
#fab {
position: fixed;
right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
:host([narrow]) #fab {
bottom: calc(84px + env(safe-area-inset-bottom));
}
#fab[is-wide] {
bottom: 24px;
right: 24px;
}
:host([rtl]) #fab {
right: auto;
left: calc(16px + env(safe-area-inset-left));
}
:host([rtl][is-wide]) #fab {
bottom: 24px;
left: 24px;
right: auto;
} }
`; `;
} }

View File

@ -152,16 +152,13 @@ class HomeAssistantMain extends LitElement {
--app-drawer-width: 64px; --app-drawer-width: 64px;
} }
:host([expanded]) { :host([expanded]) {
--app-drawer-width: 256px; --app-drawer-width: calc(256px + env(safe-area-inset-left));
} }
partial-panel-resolver, partial-panel-resolver,
ha-sidebar { ha-sidebar {
/* allow a light tap highlight on the actual interface elements */ /* allow a light tap highlight on the actual interface elements */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1); -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
} }
partial-panel-resolver {
height: 100%;
}
`; `;
} }
} }

View File

@ -90,6 +90,7 @@ class OnboardingCoreConfig extends LitElement {
<div class="row"> <div class="row">
<ha-location-editor <ha-location-editor
class="flex" class="flex"
.hass=${this.hass}
.location=${this._locationValue} .location=${this._locationValue}
.fitZoom=${14} .fitZoom=${14}
@change=${this._locationChanged} @change=${this._locationChanged}

View File

@ -211,6 +211,7 @@ class HAFullCalendar extends LitElement {
:host { :host {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
--fc-theme-standard-border-color: var(--divider-color);
} }
.header { .header {
@ -234,6 +235,10 @@ class HAFullCalendar extends LitElement {
flex-grow: 0; flex-grow: 0;
} }
a {
color: var(--primary-text-color);
}
.controls { .controls {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -259,6 +264,10 @@ class HAFullCalendar extends LitElement {
background-color: var(--card-background-color); background-color: var(--card-background-color);
} }
.fc-theme-standard .fc-scrollgrid {
border: 1px solid var(--divider-color);
}
.fc-scrollgrid-section-header td { .fc-scrollgrid-section-header td {
border: none; border: none;
} }
@ -293,14 +302,15 @@ class HAFullCalendar extends LitElement {
td.fc-day-today .fc-daygrid-day-number { td.fc-day-today .fc-daygrid-day-number {
height: 24px; height: 24px;
color: #fff; color: var(--text-primary-color);
background-color: #1a73e8; background-color: var(--primary-color);
border-radius: 50%; border-radius: 50%;
display: inline-block; display: inline-block;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
width: max-content; width: max-content;
min-width: 24px; min-width: 24px;
line-height: 140%;
} }
.fc-daygrid-day-events { .fc-daygrid-day-events {

View File

@ -11,7 +11,7 @@ import {
} from "lit-element"; } from "lit-element";
import { styleMap } from "lit-html/directives/style-map"; import { styleMap } from "lit-html/directives/style-map";
import "@polymer/app-layout/app-header-layout/app-header-layout"; import "../../layouts/ha-app-layout";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@material/mwc-checkbox"; import "@material/mwc-checkbox";
@ -63,7 +63,7 @@ class PanelCalendar extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<app-header-layout> <ha-app-layout>
<app-header fixed slot="header"> <app-header fixed slot="header">
<app-toolbar> <app-toolbar>
<ha-menu-button <ha-menu-button
@ -106,7 +106,7 @@ class PanelCalendar extends LitElement {
@view-changed=${this._handleViewChanged} @view-changed=${this._handleViewChanged}
></ha-full-calendar> ></ha-full-calendar>
</div> </div>
</app-header-layout> </ha-app-layout>
`; `;
} }

View File

@ -38,7 +38,6 @@ import {
showAreaRegistryDetailDialog, showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail"; } from "./show-dialog-area-registry-detail";
import { mdiPlus } from "@mdi/js"; import { mdiPlus } from "@mdi/js";
import { computeRTL } from "../../../common/util/compute_rtl";
@customElement("ha-config-areas-dashboard") @customElement("ha-config-areas-dashboard")
export class HaConfigAreasDashboard extends LitElement { export class HaConfigAreasDashboard extends LitElement {
@ -106,6 +105,7 @@ export class HaConfigAreasDashboard extends LitElement {
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.isWide=${this.isWide}
back-path="/config" back-path="/config"
.tabs=${configSections.integrations} .tabs=${configSections.integrations}
.route=${this.route} .route=${this.route}
@ -123,18 +123,16 @@ export class HaConfigAreasDashboard extends LitElement {
icon="hass:help-circle" icon="hass:help-circle"
@click=${this._showHelp} @click=${this._showHelp}
></ha-icon-button> ></ha-icon-button>
<mwc-fab
slot="fab"
title="${this.hass.localize(
"ui.panel.config.areas.picker.create_area"
)}"
@click=${this._createArea}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
<mwc-fab
?is-wide=${this.isWide}
?narrow=${this.narrow}
?rtl=${computeRTL(this.hass!)}
title="${this.hass.localize(
"ui.panel.config.areas.picker.create_area"
)}"
@click=${this._createArea}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
`; `;
} }
@ -183,28 +181,6 @@ export class HaConfigAreasDashboard extends LitElement {
--app-header-background-color: var(--sidebar-background-color); --app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color); --app-header-text-color: var(--sidebar-text-color);
} }
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[is-wide][rtl] {
bottom: 24px;
left: 24px;
right: auto;
}
`; `;
} }
} }

View File

@ -15,6 +15,7 @@ import {
LitElement, LitElement,
property, property,
internalProperty, internalProperty,
PropertyValues,
} from "lit-element"; } from "lit-element";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
@ -29,6 +30,11 @@ import "./types/ha-automation-action-event";
import "./types/ha-automation-action-scene"; import "./types/ha-automation-action-scene";
import "./types/ha-automation-action-service"; import "./types/ha-automation-action-service";
import "./types/ha-automation-action-wait_template"; import "./types/ha-automation-action-wait_template";
import "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-choose";
import { handleStructError } from "../../../lovelace/common/structs/handle-errors";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { haStyle } from "../../../../resources/styles";
const OPTIONS = [ const OPTIONS = [
"condition", "condition",
@ -38,6 +44,8 @@ const OPTIONS = [
"scene", "scene",
"service", "service",
"wait_template", "wait_template",
"repeat",
"choose",
]; ];
const getType = (action: Action) => { const getType = (action: Action) => {
@ -87,12 +95,26 @@ export default class HaAutomationActionRow extends LitElement {
@property() public totalActions!: number; @property() public totalActions!: number;
@internalProperty() private _warnings?: string[];
@internalProperty() private _uiModeAvailable = true;
@internalProperty() private _yamlMode = false; @internalProperty() private _yamlMode = false;
protected updated(changedProperties: PropertyValues) {
if (!changedProperties.has("action")) {
return;
}
this._uiModeAvailable = Boolean(getType(this.action));
if (!this._uiModeAvailable && !this._yamlMode) {
this._yamlMode = true;
}
}
protected render() { protected render() {
const type = getType(this.action); const type = getType(this.action);
const selected = type ? OPTIONS.indexOf(type) : -1; const selected = type ? OPTIONS.indexOf(type) : -1;
const yamlMode = this._yamlMode || selected === -1; const yamlMode = this._yamlMode;
return html` return html`
<ha-card> <ha-card>
@ -128,17 +150,14 @@ export default class HaAutomationActionRow extends LitElement {
</mwc-icon-button> </mwc-icon-button>
` `
: ""} : ""}
<ha-button-menu corner="BOTTOM_START"> <ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<mwc-icon-button <mwc-icon-button
slot="trigger" slot="trigger"
.title=${this.hass.localize("ui.common.menu")} .title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")} .label=${this.hass.localize("ui.common.overflow_menu")}
><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon> ><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
<mwc-list-item <mwc-list-item .disabled=${!this._uiModeAvailable}>
@tap=${this._switchYamlMode}
.disabled=${selected === -1}
>
${yamlMode ${yamlMode
? this.hass.localize( ? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui" "ui.panel.config.automation.editor.edit_ui"
@ -152,30 +171,39 @@ export default class HaAutomationActionRow extends LitElement {
"ui.panel.config.automation.editor.actions.duplicate" "ui.panel.config.automation.editor.actions.duplicate"
)} )}
</mwc-list-item> </mwc-list-item>
<mwc-list-item @tap=${this._onDelete}> <mwc-list-item>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete" "ui.panel.config.automation.editor.actions.delete"
)} )}
</mwc-list-item> </mwc-list-item>
</ha-button-menu> </ha-button-menu>
</div> </div>
${this._warnings
? html`<div class="warning">
UI editor is not supported for this config:
<br />
<ul>
${this._warnings.map((warning) => html`<li>${warning}</li>`)}
</ul>
You can still edit your config in yaml.
</div>`
: ""}
${yamlMode ${yamlMode
? html` ? html`
<div style="margin-right: 24px;"> ${selected === -1
${selected === -1 ? html`
? html` ${this.hass.localize(
${this.hass.localize( "ui.panel.config.automation.editor.actions.unsupported_action",
"ui.panel.config.automation.editor.actions.unsupported_action", "action",
"action", type
type )}
)} `
` : ""}
: ""} <h2>Edit in YAML</h2>
<ha-yaml-editor <ha-yaml-editor
.defaultValue=${this.action} .defaultValue=${this.action}
@value-changed=${this._onYamlChange} @value-changed=${this._onYamlChange}
></ha-yaml-editor> ></ha-yaml-editor>
</div>
` `
: html` : html`
<paper-dropdown-menu-light <paper-dropdown-menu-light
@ -200,7 +228,7 @@ export default class HaAutomationActionRow extends LitElement {
)} )}
</paper-listbox> </paper-listbox>
</paper-dropdown-menu-light> </paper-dropdown-menu-light>
<div> <div @ui-mode-not-available=${this._handleUiModeNotAvailable}>
${dynamicElement(`ha-automation-action-${type}`, { ${dynamicElement(`ha-automation-action-${type}`, {
hass: this.hass, hass: this.hass,
action: this.action, action: this.action,
@ -212,6 +240,13 @@ export default class HaAutomationActionRow extends LitElement {
`; `;
} }
private _handleUiModeNotAvailable(ev: CustomEvent) {
this._warnings = handleStructError(ev.detail);
if (!this._yamlMode) {
this._yamlMode = true;
}
}
private _moveUp() { private _moveUp() {
fireEvent(this, "move-action", { direction: "up" }); fireEvent(this, "move-action", { direction: "up" });
} }
@ -220,6 +255,19 @@ export default class HaAutomationActionRow extends LitElement {
fireEvent(this, "move-action", { direction: "down" }); fireEvent(this, "move-action", { direction: "down" });
} }
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._switchYamlMode();
break;
case 1:
break;
case 2:
this._onDelete();
break;
}
}
private _onDelete() { private _onDelete() {
showConfirmationDialog(this, { showConfirmationDialog(this, {
text: this.hass.localize( text: this.hass.localize(
@ -241,6 +289,11 @@ export default class HaAutomationActionRow extends LitElement {
return; return;
} }
this._uiModeAvailable = OPTIONS.includes(type);
if (!this._uiModeAvailable && !this._yamlMode) {
this._yamlMode = false;
}
if (type !== getType(this.action)) { if (type !== getType(this.action)) {
const elClass = customElements.get(`ha-automation-action-${type}`); const elClass = customElements.get(`ha-automation-action-${type}`);
@ -264,26 +317,30 @@ export default class HaAutomationActionRow extends LitElement {
this._yamlMode = !this._yamlMode; this._yamlMode = !this._yamlMode;
} }
static get styles(): CSSResult { static get styles(): CSSResult[] {
return css` return [
.card-menu { haStyle,
position: absolute; css`
top: 0; .card-menu {
right: 0; float: right;
z-index: 3; z-index: 3;
--mdc-theme-text-primary-on-background: var(--primary-text-color); --mdc-theme-text-primary-on-background: var(--primary-text-color);
} }
.rtl .card-menu { .rtl .card-menu {
right: auto; float: left;
left: 0; }
} mwc-list-item[disabled] {
ha-button-menu { --mdc-theme-text-primary-on-background: var(--disabled-text-color);
margin: 8px; }
} .warning {
mwc-list-item[disabled] { color: var(--warning-color);
--mdc-theme-text-primary-on-background: var(--disabled-text-color); margin-bottom: 8px;
} }
`; .warning ul {
margin: 4px 0;
}
`,
];
} }
} }

View File

@ -0,0 +1,176 @@
import "@polymer/paper-input/paper-input";
import {
customElement,
LitElement,
property,
CSSResult,
css,
} from "lit-element";
import { html } from "lit-html";
import { Action, ChooseAction } from "../../../../../data/script";
import { HomeAssistant } from "../../../../../types";
import { ActionElement } from "../ha-automation-action-row";
import "../../condition/ha-automation-condition-editor";
import "@polymer/paper-listbox/paper-listbox";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../ha-automation-action";
import { Condition } from "../../../../../data/automation";
import { haStyle } from "../../../../../resources/styles";
import { mdiDelete } from "@mdi/js";
@customElement("ha-automation-action-choose")
export class HaChooseAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public action!: ChooseAction;
public static get defaultConfig() {
return { choose: [{ conditions: [], sequence: [] }], default: [] };
}
protected render() {
const action = this.action;
return html`
${action.choose.map(
(option, idx) => html`<ha-card>
<mwc-icon-button
.idx=${idx}
@click=${this._removeOption}
title=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
>
<ha-svg-icon path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
<div class="card-content">
<h2>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option",
"number",
idx + 1
)}:
</h2>
<h3>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.conditions"
)}:
</h3>
<ha-automation-condition
.conditions=${option.conditions}
.hass=${this.hass}
.idx=${idx}
@value-changed=${this._conditionChanged}
></ha-automation-condition>
<h3>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.sequence"
)}:
</h3>
<ha-automation-action
.actions=${option.sequence}
.hass=${this.hass}
.idx=${idx}
@value-changed=${this._actionChanged}
></ha-automation-action>
</div>
</ha-card>`
)}
<ha-card>
<div class="card-actions add-card">
<mwc-button @click=${this._addOption}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.add_option"
)}
</mwc-button>
</div>
</ha-card>
<h2>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.default"
)}:
</h2>
<ha-automation-action
.actions=${action.default || []}
@value-changed=${this._defaultChanged}
.hass=${this.hass}
></ha-automation-action>
`;
}
private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Condition[];
const index = (ev.target as any).idx;
const choose = [...this.action.choose];
choose[index].conditions = value;
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
}
private _actionChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Action[];
const index = (ev.target as any).idx;
const choose = [...this.action.choose];
choose[index].sequence = value;
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
}
private _addOption() {
const choose = [...this.action.choose];
choose.push({ conditions: [], sequence: [] });
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
}
private _removeOption(ev: CustomEvent) {
const index = (ev.currentTarget as any).idx;
const choose = [...this.action.choose];
choose.splice(index, 1);
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
}
private _defaultChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Action[];
fireEvent(this, "value-changed", {
value: {
...this.action,
default: value,
},
});
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
ha-card {
margin-top: 16px;
}
.add-card mwc-button {
display: block;
text-align: center;
}
mwc-icon-button {
position: absolute;
right: 0;
padding: 4px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-choose": HaChooseAction;
}
}

View File

@ -14,6 +14,7 @@ import {
DeviceAction, DeviceAction,
deviceAutomationsEqual, deviceAutomationsEqual,
fetchDeviceActionCapabilities, fetchDeviceActionCapabilities,
DeviceCapabilities,
} from "../../../../../data/device_automation"; } from "../../../../../data/device_automation";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
@ -21,11 +22,11 @@ import { HomeAssistant } from "../../../../../types";
export class HaDeviceAction extends LitElement { export class HaDeviceAction extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public action!: DeviceAction; @property({ type: Object }) public action!: DeviceAction;
@internalProperty() private _deviceId?: string; @internalProperty() private _deviceId?: string;
@internalProperty() private _capabilities?; @internalProperty() private _capabilities?: DeviceCapabilities;
private _origAction?: DeviceAction; private _origAction?: DeviceAction;
@ -37,20 +38,20 @@ export class HaDeviceAction extends LitElement {
}; };
} }
private _extraFieldsData = memoizeOne((capabilities, action: DeviceAction) => private _extraFieldsData = memoizeOne(
capabilities && capabilities.extra_fields (action: DeviceAction, capabilities: DeviceCapabilities) => {
? capabilities.extra_fields.map((item) => { const extraFieldsData: { [key: string]: any } = {};
return { [item.name]: action[item.name] }; capabilities.extra_fields.forEach((item) => {
}) if (action[item.name] !== undefined) {
: undefined extraFieldsData![item.name] = action[item.name];
}
});
return extraFieldsData;
}
); );
protected render() { protected render() {
const deviceId = this._deviceId || this.action.device_id; const deviceId = this._deviceId || this.action.device_id;
const extraFieldsData = this._extraFieldsData(
this._capabilities,
this.action
);
return html` return html`
<ha-device-picker <ha-device-picker
@ -70,10 +71,10 @@ export class HaDeviceAction extends LitElement {
"ui.panel.config.automation.editor.actions.type.device_id.action" "ui.panel.config.automation.editor.actions.type.device_id.action"
)} )}
></ha-device-action-picker> ></ha-device-action-picker>
${extraFieldsData ${this._capabilities?.extra_fields
? html` ? html`
<ha-form <ha-form
.data=${Object.assign({}, ...extraFieldsData)} .data=${this._extraFieldsData(this.action, this._capabilities)}
.schema=${this._capabilities.extra_fields} .schema=${this._capabilities.extra_fields}
.computeLabel=${this._extraFieldsComputeLabelCallback( .computeLabel=${this._extraFieldsComputeLabelCallback(
this.hass.localize this.hass.localize
@ -97,6 +98,7 @@ export class HaDeviceAction extends LitElement {
protected updated(changedPros) { protected updated(changedPros) {
const prevAction = changedPros.get("action"); const prevAction = changedPros.get("action");
if (prevAction && !deviceAutomationsEqual(prevAction, this.action)) { if (prevAction && !deviceAutomationsEqual(prevAction, this.action)) {
this._deviceId = undefined;
this._getCapabilities(); this._getCapabilities();
} }
} }
@ -104,7 +106,7 @@ export class HaDeviceAction extends LitElement {
private async _getCapabilities() { private async _getCapabilities() {
this._capabilities = this.action.domain this._capabilities = this.action.domain
? await fetchDeviceActionCapabilities(this.hass, this.action) ? await fetchDeviceActionCapabilities(this.hass, this.action)
: null; : undefined;
} }
private _devicePicked(ev) { private _devicePicked(ev) {

View File

@ -0,0 +1,180 @@
import "@polymer/paper-input/paper-input";
import { customElement, LitElement, property, CSSResult } from "lit-element";
import { html } from "lit-html";
import {
RepeatAction,
Action,
CountRepeat,
WhileRepeat,
UntilRepeat,
} from "../../../../../data/script";
import { HomeAssistant } from "../../../../../types";
import { ActionElement } from "../ha-automation-action-row";
import "../../condition/ha-automation-condition-editor";
import type { PaperListboxElement } from "@polymer/paper-listbox";
import "@polymer/paper-listbox/paper-listbox";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../ha-automation-action";
import { Condition } from "../../../../lovelace/common/validate-condition";
import { haStyle } from "../../../../../resources/styles";
const OPTIONS = ["count", "while", "until"];
const getType = (action) => {
return OPTIONS.find((option) => option in action);
};
@customElement("ha-automation-action-repeat")
export class HaRepeatAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public action!: RepeatAction;
public static get defaultConfig() {
return { repeat: { count: 2, sequence: [] } };
}
protected render() {
const action = this.action.repeat;
const type = getType(action);
const selected = type ? OPTIONS.indexOf(type) : -1;
return html`
<paper-dropdown-menu-light
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type_select"
)}
no-animations
>
<paper-listbox
slot="dropdown-content"
.selected=${selected}
@iron-select=${this._typeChanged}
>
${OPTIONS.map(
(opt) => html`
<paper-item .action=${opt}>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.repeat.type.${opt}.label`
)}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
${type === "count"
? html`<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type.count.label"
)}
name="count"
.value=${(action as CountRepeat).count || "0"}
@value-changed=${this._countChanged}
></paper-input>`
: ""}
${type === "while"
? html` <h3>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.repeat.type.while.conditions`
)}:
</h3>
<ha-automation-condition
.conditions=${(action as WhileRepeat).while || []}
.hass=${this.hass}
@value-changed=${this._conditionChanged}
></ha-automation-condition>`
: ""}
${type === "until"
? html` <h3>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.repeat.type.until.conditions`
)}:
</h3>
<ha-automation-condition
.conditions=${(action as UntilRepeat).until || []}
.hass=${this.hass}
@value-changed=${this._conditionChanged}
></ha-automation-condition>`
: ""}
<h3>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.sequence"
)}:
</h3>
<ha-automation-action
.actions=${action.sequence}
@value-changed=${this._actionChanged}
.hass=${this.hass}
></ha-automation-action>
`;
}
private _typeChanged(ev: CustomEvent) {
const type = ((ev.target as PaperListboxElement)?.selectedItem as any)
?.action;
if (!type || type === getType(this.action.repeat)) {
return;
}
const value = type === "count" ? 2 : [];
fireEvent(this, "value-changed", {
value: {
repeat: { [type]: value, sequence: this.action.repeat.sequence },
},
});
}
private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Condition[];
fireEvent(this, "value-changed", {
value: {
repeat: {
...this.action.repeat,
[getType(this.action.repeat)!]: value,
},
},
});
}
private _actionChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Action[];
fireEvent(this, "value-changed", {
value: {
repeat: {
...this.action.repeat,
sequence: value,
},
},
});
}
private _countChanged(ev: CustomEvent): void {
const newVal = ev.detail.value;
if ((this.action.repeat as CountRepeat).count === newVal) {
return;
}
fireEvent(this, "value-changed", {
value: {
repeat: {
...this.action.repeat,
count: newVal,
},
},
});
}
static get styles(): CSSResult {
return haStyle;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-repeat": HaRepeatAction;
}
}

View File

@ -19,12 +19,20 @@ import { ServiceAction } from "../../../../../data/script";
import type { PolymerChangedEvent } from "../../../../../polymer-types"; import type { PolymerChangedEvent } from "../../../../../polymer-types";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
import { ActionElement, handleChangeEvent } from "../ha-automation-action-row"; import { ActionElement, handleChangeEvent } from "../ha-automation-action-row";
import { assert, optional, object, string } from "superstruct";
import { EntityId } from "../../../../lovelace/common/structs/is-entity-id";
const actionStruct = object({
service: optional(string()),
entity_id: optional(EntityId),
data: optional(object()),
});
@customElement("ha-automation-action-service") @customElement("ha-automation-action-service")
export class HaServiceAction extends LitElement implements ActionElement { export class HaServiceAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public action!: ServiceAction; @property({ attribute: false }) public action!: ServiceAction;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; @query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
@ -60,6 +68,11 @@ export class HaServiceAction extends LitElement implements ActionElement {
if (!changedProperties.has("action")) { if (!changedProperties.has("action")) {
return; return;
} }
try {
assert(this.action, actionStruct);
} catch (error) {
fireEvent(this, "ui-mode-not-available", error);
}
if (this._actionData && this._actionData !== this.action.data) { if (this._actionData && this._actionData !== this.action.data) {
if (this._yamlEditor) { if (this._yamlEditor) {
this._yamlEditor.setValue(this.action.data); this._yamlEditor.setValue(this.action.data);

View File

@ -2,7 +2,13 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-listbox/paper-listbox";
import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox"; import type { PaperListboxElement } from "@polymer/paper-listbox/paper-listbox";
import { customElement, html, LitElement, property } from "lit-element"; import {
customElement,
html,
LitElement,
property,
CSSResult,
} from "lit-element";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
@ -19,6 +25,7 @@ import "./types/ha-automation-condition-sun";
import "./types/ha-automation-condition-template"; import "./types/ha-automation-condition-template";
import "./types/ha-automation-condition-time"; import "./types/ha-automation-condition-time";
import "./types/ha-automation-condition-zone"; import "./types/ha-automation-condition-zone";
import { haStyle } from "../../../../resources/styles";
const OPTIONS = [ const OPTIONS = [
"device", "device",
@ -47,21 +54,20 @@ export default class HaAutomationConditionEditor extends LitElement {
return html` return html`
${yamlMode ${yamlMode
? html` ? html`
<div style="margin-right: 24px;"> ${selected === -1
${selected === -1 ? html`
? html` ${this.hass.localize(
${this.hass.localize( "ui.panel.config.automation.editor.conditions.unsupported_condition",
"ui.panel.config.automation.editor.conditions.unsupported_condition", "condition",
"condition", this.condition.condition
this.condition.condition )}
)} `
` : ""}
: ""} <h2>Edit in YAML</h2>
<ha-yaml-editor <ha-yaml-editor
.defaultValue=${this.condition} .defaultValue=${this.condition}
@value-changed=${this._onYamlChange} @value-changed=${this._onYamlChange}
></ha-yaml-editor> ></ha-yaml-editor>
</div>
` `
: html` : html`
<paper-dropdown-menu-light <paper-dropdown-menu-light
@ -123,6 +129,10 @@ export default class HaAutomationConditionEditor extends LitElement {
} }
fireEvent(this, "value-changed", { value: ev.detail.value }); fireEvent(this, "value-changed", { value: ev.detail.value });
} }
static get styles(): CSSResult {
return haStyle;
}
} }
declare global { declare global {

View File

@ -18,6 +18,7 @@ import { Condition } from "../../../../data/automation";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import "./ha-automation-condition-editor"; import "./ha-automation-condition-editor";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
export interface ConditionElement extends LitElement { export interface ConditionElement extends LitElement {
condition: Condition; condition: Condition;
@ -64,14 +65,14 @@ export default class HaAutomationConditionRow extends LitElement {
<ha-card> <ha-card>
<div class="card-content"> <div class="card-content">
<div class="card-menu"> <div class="card-menu">
<ha-button-menu corner="BOTTOM_START"> <ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<mwc-icon-button <mwc-icon-button
.title=${this.hass.localize("ui.common.menu")} .title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")} .label=${this.hass.localize("ui.common.overflow_menu")}
slot="trigger" slot="trigger"
><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon ><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon
></mwc-icon-button> ></mwc-icon-button>
<mwc-list-item @tap=${this._switchYamlMode}> <mwc-list-item>
${this._yamlMode ${this._yamlMode
? this.hass.localize( ? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui" "ui.panel.config.automation.editor.edit_ui"
@ -85,7 +86,7 @@ export default class HaAutomationConditionRow extends LitElement {
"ui.panel.config.automation.editor.actions.duplicate" "ui.panel.config.automation.editor.actions.duplicate"
)} )}
</mwc-list-item> </mwc-list-item>
<mwc-list-item @tap=${this._onDelete}> <mwc-list-item>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete" "ui.panel.config.automation.editor.actions.delete"
)} )}
@ -102,6 +103,19 @@ export default class HaAutomationConditionRow extends LitElement {
`; `;
} }
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._switchYamlMode();
break;
case 1:
break;
case 2:
this._onDelete();
break;
}
}
private _onDelete() { private _onDelete() {
showConfirmationDialog(this, { showConfirmationDialog(this, {
text: this.hass.localize( text: this.hass.localize(
@ -122,18 +136,12 @@ export default class HaAutomationConditionRow extends LitElement {
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
.card-menu { .card-menu {
position: absolute; float: right;
top: 0;
right: 0;
z-index: 3; z-index: 3;
--mdc-theme-text-primary-on-background: var(--primary-text-color); --mdc-theme-text-primary-on-background: var(--primary-text-color);
} }
.rtl .card-menu { .rtl .card-menu {
right: auto; float: left;
left: 0;
}
ha-button-menu {
margin: 8px;
} }
mwc-list-item[disabled] { mwc-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color); --mdc-theme-text-primary-on-background: var(--disabled-text-color);

View File

@ -13,18 +13,20 @@ import {
deviceAutomationsEqual, deviceAutomationsEqual,
DeviceCondition, DeviceCondition,
fetchDeviceConditionCapabilities, fetchDeviceConditionCapabilities,
DeviceCapabilities,
} from "../../../../../data/device_automation"; } from "../../../../../data/device_automation";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import memoizeOne from "memoize-one";
@customElement("ha-automation-condition-device") @customElement("ha-automation-condition-device")
export class HaDeviceCondition extends LitElement { export class HaDeviceCondition extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public condition!: DeviceCondition; @property({ type: Object }) public condition!: DeviceCondition;
@internalProperty() private _deviceId?: string; @internalProperty() private _deviceId?: string;
@internalProperty() private _capabilities?; @internalProperty() private _capabilities?: DeviceCapabilities;
private _origCondition?: DeviceCondition; private _origCondition?: DeviceCondition;
@ -36,16 +38,21 @@ export class HaDeviceCondition extends LitElement {
}; };
} }
private _extraFieldsData = memoizeOne(
(condition: DeviceCondition, capabilities: DeviceCapabilities) => {
const extraFieldsData: { [key: string]: any } = {};
capabilities.extra_fields.forEach((item) => {
if (condition[item.name] !== undefined) {
extraFieldsData![item.name] = condition[item.name];
}
});
return extraFieldsData;
}
);
protected render() { protected render() {
const deviceId = this._deviceId || this.condition.device_id; const deviceId = this._deviceId || this.condition.device_id;
const extraFieldsData =
this._capabilities && this._capabilities.extra_fields
? this._capabilities.extra_fields.map((item) => {
return { [item.name]: this.condition[item.name] };
})
: undefined;
return html` return html`
<ha-device-picker <ha-device-picker
.value=${deviceId} .value=${deviceId}
@ -64,10 +71,10 @@ export class HaDeviceCondition extends LitElement {
"ui.panel.config.automation.editor.conditions.type.device.condition" "ui.panel.config.automation.editor.conditions.type.device.condition"
)} )}
></ha-device-condition-picker> ></ha-device-condition-picker>
${extraFieldsData ${this._capabilities?.extra_fields
? html` ? html`
<ha-form <ha-form
.data=${Object.assign({}, ...extraFieldsData)} .data=${this._extraFieldsData(this.condition, this._capabilities)}
.schema=${this._capabilities.extra_fields} .schema=${this._capabilities.extra_fields}
.computeLabel=${this._extraFieldsComputeLabelCallback( .computeLabel=${this._extraFieldsComputeLabelCallback(
this.hass.localize this.hass.localize
@ -103,7 +110,7 @@ export class HaDeviceCondition extends LitElement {
this._capabilities = condition.domain this._capabilities = condition.domain
? await fetchDeviceConditionCapabilities(this.hass, condition) ? await fetchDeviceConditionCapabilities(this.hass, condition)
: null; : undefined;
} }
private _devicePicked(ev) { private _devicePicked(ev) {
@ -141,3 +148,9 @@ export class HaDeviceCondition extends LitElement {
) || schema.name; ) || schema.name;
} }
} }
declare global {
interface HTMLElementTagNameMap {
"ha-automation-condition-device": HaDeviceCondition;
}
}

View File

@ -13,9 +13,7 @@ import {
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "@material/mwc-fab"; import "@material/mwc-fab";
@ -46,10 +44,18 @@ import "./trigger/ha-automation-trigger";
import { HaDeviceTrigger } from "./trigger/types/ha-automation-trigger-device"; import { HaDeviceTrigger } from "./trigger/types/ha-automation-trigger-device";
import { mdiContentSave } from "@mdi/js"; import { mdiContentSave } from "@mdi/js";
import { PaperListboxElement } from "@polymer/paper-listbox"; import { PaperListboxElement } from "@polymer/paper-listbox";
import { classMap } from "lit-html/directives/class-map";
const MODES = ["single", "restart", "queued", "parallel"]; const MODES = ["single", "restart", "queued", "parallel"];
const MODES_MAX = ["queued", "parallel"]; const MODES_MAX = ["queued", "parallel"];
declare global {
// for fire event
interface HASSDomEvents {
"ui-mode-not-available": Error;
}
}
export class HaAutomationEditor extends LitElement { export class HaAutomationEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -65,7 +71,7 @@ export class HaAutomationEditor extends LitElement {
@internalProperty() private _config?: AutomationConfig; @internalProperty() private _config?: AutomationConfig;
@internalProperty() private _dirty?: boolean; @internalProperty() private _dirty = false;
@internalProperty() private _errors?: string; @internalProperty() private _errors?: string;
@ -305,16 +311,10 @@ export class HaAutomationEditor extends LitElement {
` `
: ""} : ""}
<mwc-fab <mwc-fab
?is-wide="${this.isWide}" slot="fab"
?narrow="${this.narrow}" class=${classMap({ dirty: this._dirty })}
?dirty="${this._dirty}" .title=${this.hass.localize("ui.panel.config.automation.editor.save")}
.title="${this.hass.localize(
"ui.panel.config.automation.editor.save"
)}"
@click=${this._saveAutomation} @click=${this._saveAutomation}
class="${classMap({
rtl: computeRTL(this.hass),
})}"
> >
<ha-svg-icon slot="icon" path=${mdiContentSave}></ha-svg-icon> <ha-svg-icon slot="icon" path=${mdiContentSave}></ha-svg-icon>
</mwc-fab> </mwc-fab>
@ -404,6 +404,10 @@ export class HaAutomationEditor extends LitElement {
const mode = ((ev.target as PaperListboxElement)?.selectedItem as any) const mode = ((ev.target as PaperListboxElement)?.selectedItem as any)
?.mode; ?.mode;
if (mode === this._config!.mode) {
return;
}
this._config = { ...this._config!, mode }; this._config = { ...this._config!, mode };
if (!MODES_MAX.includes(mode)) { if (!MODES_MAX.includes(mode)) {
delete this._config.max; delete this._config.max;
@ -531,35 +535,12 @@ export class HaAutomationEditor extends LitElement {
margin-right: 8px; margin-right: 8px;
} }
mwc-fab { mwc-fab {
position: fixed; position: relative;
bottom: 16px; bottom: calc(-80px - env(safe-area-inset-bottom));
right: 16px; transition: bottom 0.3s;
z-index: 1;
margin-bottom: -80px;
transition: margin-bottom 0.3s;
} }
mwc-fab.dirty {
mwc-fab[is-wide] { bottom: 0;
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
margin-bottom: -140px;
}
mwc-fab[dirty] {
margin-bottom: 0;
}
mwc-fab.rtl {
right: auto;
left: 16px;
}
mwc-fab[is-wide].rtl {
bottom: 24px;
right: auto;
left: 24px;
} }
`, `,
]; ];

View File

@ -1,13 +1,12 @@
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { import {
css,
CSSResultArray,
customElement, customElement,
html, html,
LitElement, LitElement,
property, property,
TemplateResult, TemplateResult,
CSSResult,
} from "lit-element"; } from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined"; import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@ -15,7 +14,6 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatDateTime } from "../../../common/datetime/format_date_time"; import { formatDateTime } from "../../../common/datetime/format_date_time";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { computeRTL } from "../../../common/util/compute_rtl";
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-entity-toggle"; import "../../../components/entity/ha-entity-toggle";
import "@material/mwc-fab"; import "@material/mwc-fab";
@ -169,19 +167,16 @@ class HaAutomationPicker extends LitElement {
)} )}
hasFab hasFab
> >
<mwc-fab
slot="fab"
title=${this.hass.localize(
"ui.panel.config.automation.picker.add_automation"
)}
@click=${this._createNew}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
<mwc-fab
slot="fab"
?is-wide=${this.isWide}
?narrow=${this.narrow}
title=${this.hass.localize(
"ui.panel.config.automation.picker.add_automation"
)}
?rtl=${computeRTL(this.hass)}
@click=${this._createNew}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
`; `;
} }
@ -207,37 +202,8 @@ class HaAutomationPicker extends LitElement {
}); });
} }
static get styles(): CSSResultArray { static get styles(): CSSResult {
return [ return haStyle;
haStyle,
css`
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
cursor: pointer;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[rtl][is-wide] {
bottom: 24px;
right: auto;
left: 24px;
}
`,
];
} }
} }

View File

@ -34,6 +34,8 @@ import "./types/ha-automation-trigger-time";
import "./types/ha-automation-trigger-time_pattern"; import "./types/ha-automation-trigger-time_pattern";
import "./types/ha-automation-trigger-webhook"; import "./types/ha-automation-trigger-webhook";
import "./types/ha-automation-trigger-zone"; import "./types/ha-automation-trigger-zone";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { haStyle } from "../../../../resources/styles";
const OPTIONS = [ const OPTIONS = [
"device", "device",
@ -93,17 +95,14 @@ export default class HaAutomationTriggerRow extends LitElement {
<ha-card> <ha-card>
<div class="card-content"> <div class="card-content">
<div class="card-menu"> <div class="card-menu">
<ha-button-menu corner="BOTTOM_START"> <ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<mwc-icon-button <mwc-icon-button
slot="trigger" slot="trigger"
.title=${this.hass.localize("ui.common.menu")} .title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")} .label=${this.hass.localize("ui.common.overflow_menu")}
><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon ><ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon
></mwc-icon-button> ></mwc-icon-button>
<mwc-list-item <mwc-list-item .disabled=${selected === -1}>
@tap=${this._switchYamlMode}
.disabled=${selected === -1}
>
${yamlMode ${yamlMode
? this.hass.localize( ? this.hass.localize(
"ui.panel.config.automation.editor.edit_ui" "ui.panel.config.automation.editor.edit_ui"
@ -117,7 +116,7 @@ export default class HaAutomationTriggerRow extends LitElement {
"ui.panel.config.automation.editor.actions.duplicate" "ui.panel.config.automation.editor.actions.duplicate"
)} )}
</mwc-list-item> </mwc-list-item>
<mwc-list-item @tap=${this._onDelete}> <mwc-list-item>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete" "ui.panel.config.automation.editor.actions.delete"
)} )}
@ -126,21 +125,20 @@ export default class HaAutomationTriggerRow extends LitElement {
</div> </div>
${yamlMode ${yamlMode
? html` ? html`
<div style="margin-right: 24px;"> ${selected === -1
${selected === -1 ? html`
? html` ${this.hass.localize(
${this.hass.localize( "ui.panel.config.automation.editor.triggers.unsupported_platform",
"ui.panel.config.automation.editor.triggers.unsupported_platform", "platform",
"platform", this.trigger.platform
this.trigger.platform )}
)} `
` : ""}
: ""} <h2>Edit in YAML</h2>
<ha-yaml-editor <ha-yaml-editor
.defaultValue=${this.trigger} .defaultValue=${this.trigger}
@value-changed=${this._onYamlChange} @value-changed=${this._onYamlChange}
></ha-yaml-editor> ></ha-yaml-editor>
</div>
` `
: html` : html`
<paper-dropdown-menu-light <paper-dropdown-menu-light
@ -177,6 +175,19 @@ export default class HaAutomationTriggerRow extends LitElement {
`; `;
} }
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._switchYamlMode();
break;
case 1:
break;
case 2:
this._onDelete();
break;
}
}
private _onDelete() { private _onDelete() {
showConfirmationDialog(this, { showConfirmationDialog(this, {
text: this.hass.localize( text: this.hass.localize(
@ -222,26 +233,23 @@ export default class HaAutomationTriggerRow extends LitElement {
this._yamlMode = !this._yamlMode; this._yamlMode = !this._yamlMode;
} }
static get styles(): CSSResult { static get styles(): CSSResult[] {
return css` return [
.card-menu { haStyle,
position: absolute; css`
top: 0; .card-menu {
right: 0; float: right;
z-index: 3; z-index: 3;
--mdc-theme-text-primary-on-background: var(--primary-text-color); --mdc-theme-text-primary-on-background: var(--primary-text-color);
} }
.rtl .card-menu { .rtl .card-menu {
right: auto; float: left;
left: 0; }
} mwc-list-item[disabled] {
ha-button-menu { --mdc-theme-text-primary-on-background: var(--disabled-text-color);
margin: 8px; }
} `,
mwc-list-item[disabled] { ];
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
`;
} }
} }

View File

@ -13,18 +13,20 @@ import {
deviceAutomationsEqual, deviceAutomationsEqual,
DeviceTrigger, DeviceTrigger,
fetchDeviceTriggerCapabilities, fetchDeviceTriggerCapabilities,
DeviceCapabilities,
} from "../../../../../data/device_automation"; } from "../../../../../data/device_automation";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
import memoizeOne from "memoize-one";
@customElement("ha-automation-trigger-device") @customElement("ha-automation-trigger-device")
export class HaDeviceTrigger extends LitElement { export class HaDeviceTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public trigger!: DeviceTrigger; @property({ type: Object }) public trigger!: DeviceTrigger;
@internalProperty() private _deviceId?: string; @internalProperty() private _deviceId?: string;
@internalProperty() private _capabilities?; @internalProperty() private _capabilities?: DeviceCapabilities;
private _origTrigger?: DeviceTrigger; private _origTrigger?: DeviceTrigger;
@ -36,16 +38,21 @@ export class HaDeviceTrigger extends LitElement {
}; };
} }
private _extraFieldsData = memoizeOne(
(trigger: DeviceTrigger, capabilities: DeviceCapabilities) => {
const extraFieldsData: { [key: string]: any } = {};
capabilities.extra_fields.forEach((item) => {
if (trigger[item.name] !== undefined) {
extraFieldsData![item.name] = trigger[item.name];
}
});
return extraFieldsData;
}
);
protected render() { protected render() {
const deviceId = this._deviceId || this.trigger.device_id; const deviceId = this._deviceId || this.trigger.device_id;
const extraFieldsData =
this._capabilities && this._capabilities.extra_fields
? this._capabilities.extra_fields.map((item) => {
return { [item.name]: this.trigger[item.name] };
})
: undefined;
return html` return html`
<ha-device-picker <ha-device-picker
.value=${deviceId} .value=${deviceId}
@ -64,10 +71,10 @@ export class HaDeviceTrigger extends LitElement {
"ui.panel.config.automation.editor.triggers.type.device.trigger" "ui.panel.config.automation.editor.triggers.type.device.trigger"
)} )}
></ha-device-trigger-picker> ></ha-device-trigger-picker>
${extraFieldsData ${this._capabilities?.extra_fields
? html` ? html`
<ha-form <ha-form
.data=${Object.assign({}, ...extraFieldsData)} .data=${this._extraFieldsData(this.trigger, this._capabilities)}
.schema=${this._capabilities.extra_fields} .schema=${this._capabilities.extra_fields}
.computeLabel=${this._extraFieldsComputeLabelCallback( .computeLabel=${this._extraFieldsComputeLabelCallback(
this.hass.localize this.hass.localize
@ -100,7 +107,7 @@ export class HaDeviceTrigger extends LitElement {
this._capabilities = trigger.domain this._capabilities = trigger.domain
? await fetchDeviceTriggerCapabilities(this.hass, trigger) ? await fetchDeviceTriggerCapabilities(this.hass, trigger)
: null; : undefined;
} }
private _devicePicked(ev) { private _devicePicked(ev) {
@ -120,7 +127,7 @@ export class HaDeviceTrigger extends LitElement {
fireEvent(this, "value-changed", { value: trigger }); fireEvent(this, "value-changed", { value: trigger });
} }
private _extraFieldsChanged(ev) { private _extraFieldsChanged(ev: CustomEvent) {
ev.stopPropagation(); ev.stopPropagation();
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { value: {
@ -138,3 +145,9 @@ export class HaDeviceTrigger extends LitElement {
) || schema.name; ) || schema.name;
} }
} }
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-device": HaDeviceTrigger;
}
}

View File

@ -61,6 +61,7 @@ class ConfigCoreForm extends LitElement {
<div class="row"> <div class="row">
<ha-location-editor <ha-location-editor
class="flex" class="flex"
.hass=${this.hass}
.location=${this._locationValue} .location=${this._locationValue}
@change=${this._locationChanged} @change=${this._locationChanged}
></ha-location-editor> ></ha-location-editor>

View File

@ -1,4 +1,4 @@
import "@polymer/app-layout/app-header-layout/app-header-layout"; import "../../../layouts/ha-app-layout";
import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar"; import "@polymer/app-layout/app-toolbar/app-toolbar";
import { import {
@ -127,7 +127,7 @@ class HaConfigDashboard extends LitElement {
} }
return html` return html`
<app-header-layout> <ha-app-layout>
<app-header fixed slot="header"> <app-header fixed slot="header">
<app-toolbar> <app-toolbar>
<ha-menu-button <ha-menu-button
@ -138,7 +138,7 @@ class HaConfigDashboard extends LitElement {
</app-header> </app-header>
${content} ${content}
</app-header-layout> </ha-app-layout>
`; `;
} }

View File

@ -13,7 +13,6 @@ import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { createValidEntityId } from "../../../common/entity/valid_entity_id";
import { compare } from "../../../common/string/compare"; import { compare } from "../../../common/string/compare";
import "../../../components/entity/ha-battery-icon"; import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
@ -44,6 +43,7 @@ import { configSections } from "../ha-panel-config";
import "./device-detail/ha-device-entities-card"; import "./device-detail/ha-device-entities-card";
import "./device-detail/ha-device-info-card"; import "./device-detail/ha-device-info-card";
import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation"; import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation";
import { slugify } from "../../../common/string/slugify";
export interface EntityRegistryStateEntry extends EntityRegistryEntry { export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string | null; stateName?: string | null;
@ -556,11 +556,11 @@ export class HaConfigDevicePage extends LitElement {
} }
if (renameEntityid) { if (renameEntityid) {
const oldSearch = createValidEntityId(oldDeviceName); const oldSearch = slugify(oldDeviceName);
if (entity.entity_id.includes(oldSearch)) { if (entity.entity_id.includes(oldSearch)) {
newEntityId = entity.entity_id.replace( newEntityId = entity.entity_id.replace(
oldSearch, oldSearch,
createValidEntityId(newDeviceName) slugify(newDeviceName)
); );
} }
} }
@ -571,7 +571,6 @@ export class HaConfigDevicePage extends LitElement {
return updateEntityRegistryEntry(this.hass!, entity.entity_id, { return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
name: newName || name, name: newName || name,
disabled_by: entity.disabled_by,
new_entity_id: newEntityId || entity.entity_id, new_entity_id: newEntityId || entity.entity_id,
}); });
}); });

View File

@ -235,7 +235,7 @@ export class DialogEntityEditor extends LitElement {
css` css`
ha-header-bar { ha-header-bar {
--mdc-theme-on-primary: var(--primary-text-color); --mdc-theme-on-primary: var(--primary-text-color);
--mdc-theme-primary: var(--card-background-color); --mdc-theme-primary: var(--mdc-theme-surface);
flex-shrink: 0; flex-shrink: 0;
} }

View File

@ -241,6 +241,7 @@ export class EntityRegistrySettings extends LitElement {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 8px; padding: 8px;
padding-bottom: max(env(safe-area-inset-bottom), 8px);
background-color: var(--mdc-theme-surface, #fff); background-color: var(--mdc-theme-surface, #fff);
} }
ha-switch { ha-switch {

View File

@ -57,6 +57,7 @@ import {
showEntityEditorDialog, showEntityEditorDialog,
} from "./show-dialog-entity-editor"; } from "./show-dialog-entity-editor";
import { mdiFilterVariant } from "@mdi/js"; import { mdiFilterVariant } from "@mdi/js";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
export interface StateEntity extends EntityRegistryEntry { export interface StateEntity extends EntityRegistryEntry {
readonly?: boolean; readonly?: boolean;
@ -449,7 +450,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
> >
</div>` </div>`
: ""} : ""}
<ha-button-menu corner="BOTTOM_START"> <ha-button-menu corner="BOTTOM_START" multi>
<mwc-icon-button <mwc-icon-button
slot="trigger" slot="trigger"
.label=${this.hass!.localize( .label=${this.hass!.localize(
@ -462,8 +463,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
<ha-svg-icon path=${mdiFilterVariant}></ha-svg-icon> <ha-svg-icon path=${mdiFilterVariant}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
<mwc-list-item <mwc-list-item
@click="${this._showDisabledChanged}" @request-selected="${this._showDisabledChanged}"
graphic="control" graphic="control"
.selected=${this._showDisabled}
> >
<ha-checkbox <ha-checkbox
slot="graphic" slot="graphic"
@ -474,8 +476,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
)} )}
</mwc-list-item> </mwc-list-item>
<mwc-list-item <mwc-list-item
@click="${this._showRestoredChanged}" @request-selected="${this._showRestoredChanged}"
graphic="control" graphic="control"
.selected=${this._showUnavailable}
> >
<ha-checkbox <ha-checkbox
slot="graphic" slot="graphic"
@ -486,8 +489,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
)} )}
</mwc-list-item> </mwc-list-item>
<mwc-list-item <mwc-list-item
@click="${this._showReadOnlyChanged}" @request-selected="${this._showReadOnlyChanged}"
graphic="control" graphic="control"
.selected=${this._showReadOnly}
> >
<ha-checkbox <ha-checkbox
slot="graphic" slot="graphic"
@ -579,16 +583,25 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
} }
} }
private _showDisabledChanged() { private _showDisabledChanged(ev: CustomEvent<RequestSelectedDetail>) {
this._showDisabled = !this._showDisabled; if (ev.detail.source !== "property") {
return;
}
this._showDisabled = ev.detail.selected;
} }
private _showRestoredChanged() { private _showRestoredChanged(ev: CustomEvent<RequestSelectedDetail>) {
this._showUnavailable = !this._showUnavailable; if (ev.detail.source !== "property") {
return;
}
this._showUnavailable = ev.detail.selected;
} }
private _showReadOnlyChanged() { private _showReadOnlyChanged(ev: CustomEvent<RequestSelectedDetail>) {
this._showReadOnly = !this._showReadOnly; if (ev.detail.source !== "property") {
return;
}
this._showReadOnly = ev.detail.selected;
} }
private _handleSearchChange(ev: CustomEvent) { private _handleSearchChange(ev: CustomEvent) {

View File

@ -182,9 +182,9 @@ class HaEntityConfig extends PolymerElement {
return; return;
} }
var oldEntityId = oldEntities[this.selectedEntity].entity_id; const oldEntityId = oldEntities[this.selectedEntity].entity_id;
var newIndex = entities.findIndex(function (ent) { const newIndex = entities.findIndex(function (ent) {
return ent.entity_id === oldEntityId; return ent.entity_id === oldEntityId;
}); });
@ -198,12 +198,12 @@ class HaEntityConfig extends PolymerElement {
entityChanged(index) { entityChanged(index) {
if (!this.entities || !this.formEl) return; if (!this.entities || !this.formEl) return;
var entity = this.entities[index]; const entity = this.entities[index];
if (!entity) return; if (!entity) return;
this.formState = "loading"; this.formState = "loading";
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
var el = this; const el = this;
this.formEl.loadEntity(entity).then(function () { this.formEl.loadEntity(entity).then(function () {
el.formState = "editing"; el.formState = "editing";
}); });
@ -212,7 +212,7 @@ class HaEntityConfig extends PolymerElement {
saveEntity() { saveEntity() {
this.formState = "saving"; this.formState = "saving";
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
var el = this; const el = this;
this.formEl.saveEntity().then(function () { this.formEl.saveEntity().then(function () {
el.formState = "editing"; el.formState = "editing";
}); });

View File

@ -5,8 +5,6 @@ import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-tooltip/paper-tooltip"; import "@polymer/paper-tooltip/paper-tooltip";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { import {
css,
CSSResult,
customElement, customElement,
html, html,
LitElement, LitElement,
@ -34,7 +32,6 @@ import { HELPER_DOMAINS } from "./const";
import { showHelperDetailDialog } from "./show-dialog-helper-detail"; import { showHelperDetailDialog } from "./show-dialog-helper-detail";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { mdiPlus } from "@mdi/js"; import { mdiPlus } from "@mdi/js";
import { computeRTL } from "../../../common/util/compute_rtl";
@customElement("ha-config-helpers") @customElement("ha-config-helpers")
export class HaConfigHelpers extends LitElement { export class HaConfigHelpers extends LitElement {
@ -160,18 +157,16 @@ export class HaConfigHelpers extends LitElement {
"ui.panel.config.helpers.picker.no_helpers" "ui.panel.config.helpers.picker.no_helpers"
)} )}
> >
<mwc-fab
slot="fab"
title="${this.hass.localize(
"ui.panel.config.helpers.picker.add_helper"
)}"
@click=${this._createHelpler}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
<mwc-fab
?is-wide=${this.isWide}
?narrow=${this.narrow}
?rtl=${computeRTL(this.hass!)}
title="${this.hass.localize(
"ui.panel.config.helpers.picker.add_helper"
)}"
@click=${this._createHelpler}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
`; `;
} }
@ -215,31 +210,4 @@ export class HaConfigHelpers extends LitElement {
private _createHelpler() { private _createHelpler() {
showHelperDetailDialog(this); showHelperDetailDialog(this);
} }
static get styles(): CSSResult {
return css`
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[is-wide][rtl] {
bottom: 24px;
left: 24px;
right: auto;
}
`;
}
} }

View File

@ -21,7 +21,6 @@ import { HASSDomEvent } from "../../../common/dom/fire_event";
import "../../../common/search/search-input"; import "../../../common/search/search-input";
import { caseInsensitiveCompare } from "../../../common/string/compare"; import { caseInsensitiveCompare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize"; import { LocalizeFunc } from "../../../common/translations/localize";
import { computeRTL } from "../../../common/util/compute_rtl";
import { nextRender } from "../../../common/util/render-status"; import { nextRender } from "../../../common/util/render-status";
import "../../../components/entity/ha-state-icon"; import "../../../components/entity/ha-state-icon";
import "../../../components/ha-button-menu"; import "../../../components/ha-button-menu";
@ -276,7 +275,11 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
</div> </div>
` `
: ""} : ""}
<ha-button-menu corner="BOTTOM_START" slot="toolbar-icon"> <ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@action=${this._toggleShowIgnored}
>
<mwc-icon-button <mwc-icon-button
.title=${this.hass.localize("ui.common.menu")} .title=${this.hass.localize("ui.common.menu")}
.label=${this.hass.localize("ui.common.overflow_menu")} .label=${this.hass.localize("ui.common.overflow_menu")}
@ -284,7 +287,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
> >
<ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon> <ha-svg-icon path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
<mwc-list-item @click=${this._toggleShowIgnored}> <mwc-list-item>
${this.hass.localize( ${this.hass.localize(
this._showIgnored this._showIgnored
? "ui.panel.config.integrations.ignore.hide_ignored" ? "ui.panel.config.integrations.ignore.hide_ignored"
@ -455,12 +458,10 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
: ""} : ""}
</div> </div>
<mwc-fab <mwc-fab
slot="fab"
aria-label=${this.hass.localize("ui.panel.config.integrations.new")} aria-label=${this.hass.localize("ui.panel.config.integrations.new")}
title=${this.hass.localize("ui.panel.config.integrations.new")} title=${this.hass.localize("ui.panel.config.integrations.new")}
@click=${this._createFlow} @click=${this._createFlow}
?is-wide=${this.isWide}
?narrow=${this.narrow}
?rtl=${computeRTL(this.hass!)}
> >
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab> </mwc-fab>
@ -710,28 +711,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
h2 { h2 {
margin-top: 0; margin-top: 0;
} }
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[is-wide][rtl] {
bottom: 24px;
left: 24px;
right: auto;
}
`, `,
]; ];
} }

Some files were not shown because too many files have changed in this diff Show More