diff --git a/.eslintrc.json b/.eslintrc.json index b4edeb632e..49546398a7 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,7 @@ { "extends": [ - "plugin:@typescript-eslint/recommended", "airbnb-typescript/base", + "plugin:@typescript-eslint/recommended", "plugin:wc/recommended", "plugin:lit/recommended", "prettier", @@ -45,16 +45,16 @@ "func-names": 0, "prefer-arrow-callback": 0, "no-underscore-dangle": 0, - "no-var": 0, "strict": 0, "prefer-spread": 0, "no-plusplus": 0, - "no-bitwise": 0, + "no-bitwise": 2, "comma-dangle": 0, "vars-on-top": 0, "no-continue": 0, "no-param-reassign": 0, "no-multi-assign": 0, + "no-console": 2, "radix": 0, "no-alert": 0, "no-return-await": 0, diff --git a/build-scripts/removedIcons.json b/build-scripts/removedIcons.json index 887da3dab7..ed3b46bbfa 100644 --- a/build-scripts/removedIcons.json +++ b/build-scripts/removedIcons.json @@ -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", "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", "name": "nfc-off" diff --git a/cast/src/receiver/layout/hc-lovelace.ts b/cast/src/receiver/layout/hc-lovelace.ts index bb3272c033..c0d1dc1dfe 100644 --- a/cast/src/receiver/layout/hc-lovelace.ts +++ b/cast/src/receiver/layout/hc-lovelace.ts @@ -22,6 +22,8 @@ class HcLovelace extends LitElement { @property() public viewPath?: string | number; + public urlPath?: string | null; + protected render(): TemplateResult { const index = this._viewIndex; if (index === undefined) { @@ -35,6 +37,7 @@ class HcLovelace extends LitElement { const lovelace: Lovelace = { config: this.lovelaceConfig, editMode: false, + urlPath: this.urlPath!, enableFullEditMode: () => undefined, mode: "storage", language: "en", diff --git a/cast/src/receiver/layout/hc-main.ts b/cast/src/receiver/layout/hc-main.ts index 974c941d8b..03abab575b 100644 --- a/cast/src/receiver/layout/hc-main.ts +++ b/cast/src/receiver/layout/hc-main.ts @@ -87,6 +87,7 @@ export class HcMain extends HassElement { .hass=${this.hass} .lovelaceConfig=${this._lovelaceConfig} .viewPath=${this._lovelacePath} + .urlPath=${this._urlPath} @config-refresh=${this._generateLovelaceConfig} > `; diff --git a/demo/src/custom-cards/cast-demo-row.ts b/demo/src/custom-cards/cast-demo-row.ts index 9dae52a96d..04a0630418 100644 --- a/demo/src/custom-cards/cast-demo-row.ts +++ b/demo/src/custom-cards/cast-demo-row.ts @@ -52,7 +52,6 @@ class CastDemoRow extends LitElement implements LovelaceRow { }); mgr.castContext.addEventListener( // eslint-disable-next-line no-undef - // @ts-ignore cast.framework.CastContextEventType.SESSION_STATE_CHANGED, (ev) => { // On Android, opening a new session always results in SESSION_RESUMED. diff --git a/hassio/src/addon-store/hassio-addon-store.ts b/hassio/src/addon-store/hassio-addon-store.ts index b72826b3c0..c02d8c1b12 100644 --- a/hassio/src/addon-store/hassio-addon-store.ts +++ b/hassio/src/addon-store/hassio-addon-store.ts @@ -25,6 +25,7 @@ import { HomeAssistant, Route } from "../../../src/types"; import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories"; import { supervisorTabs } from "../hassio-tabs"; import "./hassio-addon-repository"; +import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => { if (a.slug === "local") { @@ -97,14 +98,18 @@ class HassioAddonStore extends LitElement { .tabs=${supervisorTabs} > Add-on store - + - + Repositories - + Reload @@ -143,6 +148,17 @@ class HassioAddonStore extends LitElement { this._loadData(); } + private _handleAction(ev: CustomEvent) { + switch (ev.detail.index) { + case 0: + this._manageRepositories(); + break; + case 1: + this.refreshData(); + break; + } + } + private apiCalled(ev) { if (ev.detail.success) { this._loadData(); diff --git a/hassio/src/hassio-main.ts b/hassio/src/hassio-main.ts index 14d6833bed..074c40baaa 100644 --- a/hassio/src/hassio-main.ts +++ b/hassio/src/hassio-main.ts @@ -94,7 +94,8 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) { applyThemesOnElement( this.parentElement, 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( diff --git a/package.json b/package.json index 9f61f7eba7..c067e53d7c 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "lint:prettier": "prettier '**/src/**/*.{js,ts,json,css,md}' --check", "format:prettier": "prettier '**/src/**/*.{js,ts,json,css,md}' --write", "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", "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", @@ -35,13 +35,14 @@ "@material/mwc-icon-button": "^0.17.2", "@material/mwc-list": "^0.17.2", "@material/mwc-menu": "^0.17.2", + "@material/mwc-radio": "^0.17.2", "@material/mwc-ripple": "^0.17.2", "@material/mwc-switch": "^0.17.2", "@material/mwc-tab": "^0.17.2", "@material/mwc-tab-bar": "^0.17.2", "@material/top-app-bar": "=8.0.0-canary.a78ceb112.0", - "@mdi/js": "5.3.45", - "@mdi/svg": "5.3.45", + "@mdi/js": "5.4.55", + "@mdi/svg": "5.4.55", "@polymer/app-layout": "^3.0.2", "@polymer/app-route": "^3.0.2", "@polymer/app-storage": "^3.0.2", @@ -74,6 +75,7 @@ "@polymer/paper-tooltip": "^3.0.1", "@polymer/polymer": "3.1.0", "@thomasloven/round-slider": "0.5.0", + "@types/chromecast-caf-sender": "^1.0.3", "@vaadin/vaadin-combo-box": "^5.0.10", "@vaadin/vaadin-date-picker": "^4.0.7", "@vue/web-component-wrapper": "^1.2.0", @@ -99,7 +101,7 @@ "lit-element": "^2.3.1", "lit-html": "^1.2.1", "lit-virtualizer": "^0.4.2", - "marked": "^0.6.1", + "marked": "^1.1.1", "mdn-polyfills": "^5.16.0", "memoize-one": "^5.0.2", "node-vibrant": "^3.1.5", @@ -107,7 +109,7 @@ "regenerator-runtime": "^0.13.2", "resize-observer-polyfill": "^1.5.1", "roboto-fontface": "^0.10.0", - "superstruct": "^0.6.1", + "superstruct": "^0.10.12", "unfetch": "^4.1.0", "vue": "^2.6.11", "vue2-daterange-picker": "^0.5.1", @@ -135,11 +137,12 @@ "@rollup/plugin-replace": "^2.3.2", "@types/chai": "^4.1.7", "@types/chromecast-caf-receiver": "^3.0.12", - "@types/codemirror": "^0.0.78", + "@types/codemirror": "^0.0.97", "@types/hls.js": "^0.12.3", "@types/js-yaml": "^3.12.1", "@types/leaflet": "^1.4.3", "@types/leaflet-draw": "^1.0.1", + "@types/marked": "^1.1.0", "@types/memoize-one": "4.1.0", "@types/mocha": "^5.2.6", "@types/resize-observer-browser": "^0.1.3", @@ -170,7 +173,7 @@ "html-minifier": "^4.0.0", "husky": "^1.3.1", "lint-staged": "^8.1.5", - "lit-analyzer": "^1.1.10", + "lit-analyzer": "^1.2.0", "lodash.template": "^4.5.0", "magic-string": "^0.25.7", "map-stream": "^0.0.7", @@ -191,7 +194,7 @@ "source-map-url": "^0.4.0", "systemjs": "^6.3.2", "terser-webpack-plugin": "^3.0.6", - "ts-lit-plugin": "^1.1.10", + "ts-lit-plugin": "^1.2.0", "ts-mocha": "^6.0.0", "typescript": "^3.8.3", "vinyl-buffer": "^1.0.1", diff --git a/setup.py b/setup.py index c068657406..4e6c716f17 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20200716.0", + version="20200803.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/cast/cast_manager.ts b/src/cast/cast_manager.ts index 6c4fcf13f4..f41811cde1 100644 --- a/src/cast/cast_manager.ts +++ b/src/cast/cast_manager.ts @@ -1,4 +1,8 @@ /* eslint-disable no-undef, no-console */ +import { + CastStateEventData, + SessionStateEventData, +} from "chromecast-caf-receiver/cast.framework"; import { Auth } from "home-assistant-js-websocket"; import { castApiAvailable } from "./cast_framework"; import { CAST_APP_ID, CAST_DEV, CAST_NS } from "./const"; @@ -40,16 +44,13 @@ export class CastManager { const context = this.castContext; context.setOptions({ receiverApplicationId: CAST_APP_ID, - // @ts-ignore autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED, }); context.addEventListener( - // @ts-ignore cast.framework.CastContextEventType.SESSION_STATE_CHANGED, (ev) => this._sessionStateChanged(ev) ); context.addEventListener( - // @ts-ignore cast.framework.CastContextEventType.CAST_STATE_CHANGED, (ev) => this._castStateChanged(ev) ); @@ -118,7 +119,7 @@ export class CastManager { } } - private _sessionStateChanged(ev) { + private _sessionStateChanged(ev: SessionStateEventData) { if (__DEV__) { console.log("Cast session state changed", ev.sessionState); } @@ -141,7 +142,7 @@ export class CastManager { } } - private _castStateChanged(ev) { + private _castStateChanged(ev: CastStateEventData) { if (__DEV__) { console.log("Cast state changed", ev.castState); } diff --git a/src/common/color/convert-color.ts b/src/common/color/convert-color.ts new file mode 100644 index 0000000000..f48e6184a0 --- /dev/null +++ b/src/common/color/convert-color.ts @@ -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); +}; diff --git a/src/common/color/lab.ts b/src/common/color/lab.ts new file mode 100644 index 0000000000..0bf1849f69 --- /dev/null +++ b/src/common/color/lab.ts @@ -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); +}; diff --git a/src/common/color/rgb.ts b/src/common/color/rgb.ts new file mode 100644 index 0000000000..3d54ee0597 --- /dev/null +++ b/src/common/color/rgb.ts @@ -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); +}; diff --git a/src/common/dom/apply_themes_on_element.ts b/src/common/dom/apply_themes_on_element.ts index 44f042beac..f994f0d99d 100644 --- a/src/common/dom/apply_themes_on_element.ts +++ b/src/common/dom/apply_themes_on_element.ts @@ -1,26 +1,20 @@ -import { derivedStyles } from "../../resources/styles"; +import { derivedStyles, darkStyles } from "../../resources/styles"; 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 { keys: { [key: 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 } = {}; /** @@ -33,17 +27,56 @@ let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {}; export const applyThemesOnElement = ( element, themes: HomeAssistant["themes"], - selectedTheme?: string + selectedTheme?: string, + themeOptions?: Partial ) => { - const newTheme = selectedTheme - ? PROCESSED_THEMES[selectedTheme] || processTheme(selectedTheme, themes) - : undefined; + let cacheKey = selectedTheme; + let themeRules: Partial = {}; - 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 return; } + const newTheme = + themeRules && cacheKey + ? PROCESSED_THEMES[cacheKey] || processTheme(cacheKey, themeRules) + : undefined; + // Add previous set keys to reset them, and new theme const styles = { ...element._themes, ...newTheme?.styles }; element._themes = newTheme?.keys; @@ -58,42 +91,45 @@ export const applyThemesOnElement = ( }; const processTheme = ( - themeName: string, - themes: HomeAssistant["themes"] + cacheKey: string, + theme: Partial ): ProcessedTheme | undefined => { - if (!themes.themes[themeName]) { + if (!theme || !Object.keys(theme).length) { return undefined; } - const theme: Theme = { + const combinedTheme: Partial = { ...derivedStyles, - ...themes.themes[themeName], + ...theme, }; const styles = {}; const keys = {}; - for (const key of Object.keys(theme)) { + for (const key of Object.keys(combinedTheme)) { const prefixedKey = `--${key}`; - const value = theme[key]; + const value = combinedTheme[key]!; styles[prefixedKey] = value; 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("#")) { - // Not a hex color + // Can't convert non hex value continue; } + const rgbKey = `rgb-${key}`; - if (theme[rgbKey] !== undefined) { + if (combinedTheme[rgbKey] !== undefined) { // Theme has it's own rgb value continue; } - const rgbValue = hexToRgb(value); - if (rgbValue !== null) { + try { + const rgbValue = hex2rgb(value).join(","); const prefixedRgbKey = `--${rgbKey}`; styles[prefixedRgbKey] = rgbValue; keys[prefixedRgbKey] = ""; + } catch (e) { + continue; } } - PROCESSED_THEMES[themeName] = { styles, keys }; + PROCESSED_THEMES[cacheKey] = { styles, keys }; return { styles, keys }; }; diff --git a/src/common/dom/setup-leaflet-map.ts b/src/common/dom/setup-leaflet-map.ts index d943fb793b..89e1abe823 100644 --- a/src/common/dom/setup-leaflet-map.ts +++ b/src/common/dom/setup-leaflet-map.ts @@ -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 export type LeafletModuleType = typeof import("leaflet"); @@ -6,9 +6,9 @@ export type LeafletDrawModuleType = typeof import("leaflet-draw"); export const setupLeafletMap = async ( mapElement: HTMLElement, - darkMode = false, + darkMode?: boolean, draw = false -): Promise<[Map, LeafletModuleType]> => { +): Promise<[Map, LeafletModuleType, TileLayer]> => { if (!mapElement.parentNode) { throw new Error("Cannot setup Leaflet map on disconnected element"); } @@ -28,15 +28,28 @@ export const setupLeafletMap = async ( style.setAttribute("rel", "stylesheet"); mapElement.parentNode.appendChild(style); 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, darkMode: boolean -) => { +): TileLayer => { return leaflet.tileLayer( `https://{s}.basemaps.cartocdn.com/${ darkMode ? "dark_all" : "light_all" diff --git a/src/common/entity/battery_icon.ts b/src/common/entity/battery_icon.ts index 26ffb3a3d8..61539c6c43 100644 --- a/src/common/entity/battery_icon.ts +++ b/src/common/entity/battery_icon.ts @@ -13,7 +13,7 @@ export const batteryIcon = ( return "hass:battery-unknown"; } - var icon = "hass:battery"; + let icon = "hass:battery"; const batteryRound = Math.round(battery / 10) * 10; if (battery_charging && battery > 10) { icon += `-charging-${batteryRound}`; diff --git a/src/common/entity/supports-feature.ts b/src/common/entity/supports-feature.ts index 24f1b598cc..45e1440981 100644 --- a/src/common/entity/supports-feature.ts +++ b/src/common/entity/supports-feature.ts @@ -4,6 +4,6 @@ export const supportsFeature = ( stateObj: HassEntity, feature: number ): boolean => { - // eslint-disable-next-line:no-bitwise + // eslint-disable-next-line no-bitwise return (stateObj.attributes.supported_features! & feature) !== 0; }; diff --git a/src/common/entity/valid_entity_id.ts b/src/common/entity/valid_entity_id.ts index c9c8d9acb9..b34d3cd797 100644 --- a/src/common/entity/valid_entity_id.ts +++ b/src/common/entity/valid_entity_id.ts @@ -2,11 +2,3 @@ const validEntityId = /^(\w+)\.(\w+)$/; export const isValidEntityId = (entityId: string) => 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 diff --git a/src/common/mwc/handle-request-selected-event.ts b/src/common/mwc/handle-request-selected-event.ts new file mode 100644 index 0000000000..3081ac5af0 --- /dev/null +++ b/src/common/mwc/handle-request-selected-event.ts @@ -0,0 +1,14 @@ +import { + RequestSelectedDetail, + ListItem, +} from "@material/mwc-list/mwc-list-item"; + +export const shouldHandleRequestSelectedEvent = ( + ev: CustomEvent +): boolean => { + if (!ev.detail.selected && ev.detail.source !== "property") { + return false; + } + (ev.target as ListItem).selected = false; + return true; +}; diff --git a/src/common/string/slugify.ts b/src/common/string/slugify.ts index a5f0c134cb..dc4015ff68 100644 --- a/src/common/string/slugify.ts +++ b/src/common/string/slugify.ts @@ -1,5 +1,5 @@ // https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1 -export const slugify = (value: string, delimiter = "-") => { +export const slugify = (value: string, delimiter = "_") => { const a = "àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;"; const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`; diff --git a/src/common/util/compute_rtl.ts b/src/common/util/compute_rtl.ts index 343ec34307..d73bf8f859 100644 --- a/src/common/util/compute_rtl.ts +++ b/src/common/util/compute_rtl.ts @@ -9,5 +9,9 @@ export function computeRTL(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"; } diff --git a/src/components/buttons/ha-call-service-button.js b/src/components/buttons/ha-call-service-button.js index 5166c615e4..391c9b8e22 100644 --- a/src/components/buttons/ha-call-service-button.js +++ b/src/components/buttons/ha-call-service-button.js @@ -54,8 +54,8 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) { callService() { this.progress = true; // eslint-disable-next-line @typescript-eslint/no-this-alias - var el = this; - var eventData = { + const el = this; + const eventData = { domain: this.domain, service: this.service, serviceData: this.serviceData, diff --git a/src/components/buttons/ha-progress-button.js b/src/components/buttons/ha-progress-button.js index 084e50c5a2..ea4f90184c 100644 --- a/src/components/buttons/ha-progress-button.js +++ b/src/components/buttons/ha-progress-button.js @@ -78,7 +78,7 @@ class HaProgressButton extends PolymerElement { } tempClass(className) { - var classList = this.$.container.classList; + const classList = this.$.container.classList; classList.add(className); setTimeout(() => { classList.remove(className); diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 05283fb7db..ba2bf0f35e 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -98,6 +98,8 @@ export class HaDataTable extends LitElement { @property({ type: String }) public noDataText?: string; + @property({ type: String }) public searchLabel?: string; + @property({ type: String }) public filter = ""; @internalProperty() private _filterable = false; @@ -202,6 +204,7 @@ export class HaDataTable extends LitElement {
` @@ -538,7 +541,7 @@ export class HaDataTable extends LitElement { border-radius: 4px; border-width: 1px; border-style: solid; - border-color: rgba(var(--rgb-primary-text-color), 0.12); + border-color: var(--divider-color); display: inline-flex; flex-direction: column; box-sizing: border-box; @@ -556,7 +559,7 @@ export class HaDataTable extends LitElement { } .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 { @@ -575,7 +578,7 @@ export class HaDataTable extends LitElement { height: 56px; display: flex; width: 100%; - border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12); + border-bottom: 1px solid var(--divider-color); overflow-x: auto; } @@ -828,7 +831,7 @@ export class HaDataTable extends LitElement { right: 12px; } .table-header { - border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12); + border-bottom: 1px solid var(--divider-color); padding: 0 16px; } search-input { diff --git a/src/components/date-range-picker.ts b/src/components/date-range-picker.ts index 48dd936f91..cc0e7d3f29 100644 --- a/src/components/date-range-picker.ts +++ b/src/components/date-range-picker.ts @@ -135,7 +135,7 @@ class DateRangePickerElement extends WrappedElement { } .daterangepicker td.in-range { 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:hover { diff --git a/src/components/device/ha-device-automation-picker.ts b/src/components/device/ha-device-automation-picker.ts index 5f05069dd1..540cfc5c9d 100644 --- a/src/components/device/ha-device-automation-picker.ts +++ b/src/components/device/ha-device-automation-picker.ts @@ -117,11 +117,7 @@ export abstract class HaDeviceAutomationPicker< > ${this.NO_AUTOMATION_TEXT} -