From 4ca13c409b9086c457435816c8d0aab7a025a684 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 3 Aug 2020 02:07:12 +0200 Subject: [PATCH] Introduce dark mode and primary color picker (#6430) Co-authored-by: Paulus Schoutsen --- hassio/src/hassio-main.ts | 3 +- package.json | 6 +- src/common/color/convert-color.ts | 113 +++++++++ src/common/color/lab.ts | 16 ++ src/common/color/rgb.ts | 24 ++ src/common/dom/apply_themes_on_element.ts | 104 +++++--- src/common/dom/setup-leaflet-map.ts | 27 +- src/components/data-table/ha-data-table.ts | 8 +- src/components/date-range-picker.ts | 2 +- src/components/ha-card.ts | 2 +- src/components/ha-dialog.ts | 1 + src/components/ha-markdown-element.ts | 1 - src/components/ha-radio.ts | 20 ++ src/components/ha-sidebar.ts | 2 +- src/components/map/ha-location-editor.ts | 34 ++- src/components/map/ha-locations-editor.ts | 30 ++- src/components/map/ha-map.ts | 30 ++- src/components/user/ha-user-badge.ts | 2 +- src/dialogs/more-info/ha-more-info-dialog.ts | 2 +- .../ha-voice-command-dialog.ts | 2 +- src/fake_data/provide_hass.ts | 6 +- src/html/index.html.template | 3 +- src/onboarding/onboarding-core-config.ts | 1 + src/panels/calendar/ha-full-calendar.ts | 14 +- src/panels/config/core/ha-config-core-form.ts | 1 + .../config/entities/dialog-entity-editor.ts | 2 +- src/panels/config/zone/dialog-zone-detail.ts | 1 + src/panels/config/zone/ha-config-zone.ts | 1 + src/panels/lovelace/cards/hui-map-card.ts | 54 +++- .../lovelace/components/hui-card-options.ts | 33 +-- .../lovelace/special-rows/hui-divider-row.ts | 2 +- .../lovelace/special-rows/hui-section-row.ts | 3 +- src/panels/map/ha-panel-map.js | 28 +- src/panels/profile/ha-pick-theme-row.js | 112 -------- src/panels/profile/ha-pick-theme-row.ts | 240 ++++++++++++++++++ src/resources/codemirror.ts | 1 + src/resources/ha-style.ts | 4 +- src/resources/markdown_worker.ts | 7 +- src/resources/styles.ts | 16 ++ src/state/themes-mixin.ts | 73 +++++- src/translations/en.json | 10 +- src/types.ts | 12 +- src/util/ha-pref-storage.ts | 4 + tsconfig.json | 1 + yarn.lock | 21 +- 45 files changed, 814 insertions(+), 265 deletions(-) create mode 100644 src/common/color/convert-color.ts create mode 100644 src/common/color/lab.ts create mode 100644 src/common/color/rgb.ts create mode 100644 src/components/ha-radio.ts delete mode 100644 src/panels/profile/ha-pick-theme-row.js create mode 100644 src/panels/profile/ha-pick-theme-row.ts 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 38653a3426..c067e53d7c 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@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", @@ -100,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", @@ -136,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", 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/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 3bd894e288..ba2bf0f35e 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -541,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; @@ -559,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 { @@ -578,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; } @@ -831,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/ha-card.ts b/src/components/ha-card.ts index 1c8fc43c97..2b23b8ec20 100644 --- a/src/components/ha-card.ts +++ b/src/components/ha-card.ts @@ -66,7 +66,7 @@ export class HaCard extends LitElement { } :host ::slotted(.card-actions) { - border-top: 1px solid #e8e8e8; + border-top: 1px solid var(--divider-color, #e8e8e8); padding: 5px 16px; } `; diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index 36790113f0..f18163ea7a 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -34,6 +34,7 @@ export class HaDialog extends MwcDialog { style, css` .mdc-dialog { + --mdc-dialog-scroll-divider-color: var(--divider-color); z-index: var(--dialog-z-index, 7); } .mdc-dialog__actions { diff --git a/src/components/ha-markdown-element.ts b/src/components/ha-markdown-element.ts index 03e3f17362..b6e78c99b9 100644 --- a/src/components/ha-markdown-element.ts +++ b/src/components/ha-markdown-element.ts @@ -23,7 +23,6 @@ class HaMarkdownElement extends UpdatingElement { { breaks: this.breaks, gfm: true, - tables: true, }, { allowSvg: this.allowSvg, diff --git a/src/components/ha-radio.ts b/src/components/ha-radio.ts new file mode 100644 index 0000000000..8796750a74 --- /dev/null +++ b/src/components/ha-radio.ts @@ -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; + +@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; + } +} diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index 72bc3cfaa3..4454ac122d 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -717,7 +717,7 @@ class HaSidebar extends LitElement { line-height: 20px; text-align: center; padding: 0px 6px; - color: var(--text-primary-color); + color: var(--text-accent-color, var(--text-primary-color)); } ha-svg-icon + .notification-badge { position: absolute; diff --git a/src/components/map/ha-location-editor.ts b/src/components/map/ha-location-editor.ts index aea7065874..be72287b3e 100644 --- a/src/components/map/ha-location-editor.ts +++ b/src/components/map/ha-location-editor.ts @@ -6,6 +6,7 @@ import { LeafletMouseEvent, Map, Marker, + TileLayer, } from "leaflet"; import { css, @@ -21,15 +22,19 @@ import { fireEvent } from "../../common/dom/fire_event"; import { LeafletModuleType, setupLeafletMap, + replaceTileLayer, } from "../../common/dom/setup-leaflet-map"; import { nextRender } from "../../common/util/render-status"; import { defaultRadiusColor } from "../../data/zone"; +import { HomeAssistant } from "../../types"; @customElement("ha-location-editor") 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; @@ -46,6 +51,8 @@ class LocationEditor extends LitElement { private _leafletMap?: Map; + private _tileLayer?: TileLayer; + private _locationMarker?: Marker | Circle; public fitMap(): void { @@ -97,6 +104,22 @@ class LocationEditor extends LitElement { if (changedProps.has("icon")) { 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 { @@ -104,9 +127,9 @@ class LocationEditor extends LitElement { } private async _initMap(): Promise { - [this._leafletMap, this.Leaflet] = await setupLeafletMap( + [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( this._mapEl, - false, + this.hass.themes.darkMode, Boolean(this.radius) ); this._leafletMap.addEventListener( @@ -255,9 +278,6 @@ class LocationEditor extends LitElement { #map { height: 100%; } - .light { - color: #000000; - } .leaflet-edit-move { border-radius: 50%; cursor: move !important; diff --git a/src/components/map/ha-locations-editor.ts b/src/components/map/ha-locations-editor.ts index 6db95339ba..06547974fe 100644 --- a/src/components/map/ha-locations-editor.ts +++ b/src/components/map/ha-locations-editor.ts @@ -6,6 +6,7 @@ import { Map, Marker, MarkerOptions, + TileLayer, } from "leaflet"; import { css, @@ -21,8 +22,10 @@ import { fireEvent } from "../../common/dom/fire_event"; import { LeafletModuleType, setupLeafletMap, + replaceTileLayer, } from "../../common/dom/setup-leaflet-map"; import { defaultRadiusColor } from "../../data/zone"; +import { HomeAssistant } from "../../types"; declare global { // for fire event @@ -47,6 +50,8 @@ export interface MarkerLocation { @customElement("ha-locations-editor") export class HaLocationsEditor extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @property() public locations?: MarkerLocation[]; public fitZoom = 16; @@ -57,6 +62,8 @@ export class HaLocationsEditor extends LitElement { // eslint-disable-next-line private _leafletMap?: Map; + private _tileLayer?: TileLayer; + private _locationMarkers?: { [key: string]: Marker | Circle }; private _circles: { [key: string]: Circle } = {}; @@ -116,6 +123,22 @@ export class HaLocationsEditor extends LitElement { if (changedProps.has("locations")) { 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 { @@ -123,9 +146,9 @@ export class HaLocationsEditor extends LitElement { } private async _initMap(): Promise { - [this._leafletMap, this.Leaflet] = await setupLeafletMap( + [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( this._mapEl, - false, + this.hass.themes.darkMode, true ); this._updateMarkers(); @@ -290,9 +313,6 @@ export class HaLocationsEditor extends LitElement { #map { height: 100%; } - .light { - color: #000000; - } .leaflet-marker-draggable { cursor: move !important; } diff --git a/src/components/map/ha-map.ts b/src/components/map/ha-map.ts index 8d25d5011a..b4bb36ea25 100644 --- a/src/components/map/ha-map.ts +++ b/src/components/map/ha-map.ts @@ -1,5 +1,5 @@ import "../ha-icon-button"; -import { Circle, Layer, Map, Marker } from "leaflet"; +import { Circle, Layer, Map, Marker, TileLayer } from "leaflet"; import { css, CSSResult, @@ -13,6 +13,7 @@ import { import { LeafletModuleType, setupLeafletMap, + replaceTileLayer, } from "../../common/dom/setup-leaflet-map"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; @@ -22,11 +23,11 @@ import { HomeAssistant } from "../../types"; @customElement("ha-map") class HaMap extends LitElement { - @property({ attribute: false }) public hass?: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; @property() public entities?: string[]; - @property() public darkMode = false; + @property() public darkMode?: boolean; @property() public zoom?: number; @@ -35,6 +36,8 @@ class HaMap extends LitElement { private _leafletMap?: Map; + private _tileLayer?: TileLayer; + // @ts-ignore private _resizeObserver?: ResizeObserver; @@ -122,6 +125,20 @@ class HaMap extends LitElement { if (changedProps.has("hass")) { this._drawEntities(); 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 { - [this._leafletMap, this.Leaflet] = await setupLeafletMap( + [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( this._mapEl, - this.darkMode + this.darkMode ?? this.hass.themes.darkMode ); this._drawEntities(); this._leafletMap.invalidateSize(); @@ -229,7 +246,8 @@ class HaMap extends LitElement { icon: Leaflet.divIcon({ html: iconHTML, iconSize: [24, 24], - className: this.darkMode ? "dark" : "light", + className: + this.darkMode ?? this.hass.themes.darkMode ? "dark" : "light", }), interactive: false, title, diff --git a/src/components/user/ha-user-badge.ts b/src/components/user/ha-user-badge.ts index cd6dd3385d..3e418ff596 100644 --- a/src/components/user/ha-user-badge.ts +++ b/src/components/user/ha-user-badge.ts @@ -57,7 +57,7 @@ class StateBadge extends LitElement { text-align: center; background-color: var(--light-primary-color); text-decoration: none; - color: var(--primary-text-color); + color: var(--text-light-primary-color, var(--primary-text-color)); overflow: hidden; } diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index 06d5a1289c..b1953f678d 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -254,7 +254,7 @@ export class MoreInfoDialog extends LitElement { ha-header-bar { --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; border-bottom: 1px solid var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); diff --git a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts index 226bebba07..aafdb13ee0 100644 --- a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts +++ b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts @@ -426,7 +426,7 @@ export class HaVoiceCommandDialog extends LitElement { text-align: right; border-bottom-right-radius: 0px; background-color: var(--light-primary-color); - color: var(--primary-text-color); + color: var(--text-light-primary-color, var(--primary-text-color)); } .message.hass { diff --git a/src/fake_data/provide_hass.ts b/src/fake_data/provide_hass.ts index c5d015b14f..f070dc7f41 100644 --- a/src/fake_data/provide_hass.ts +++ b/src/fake_data/provide_hass.ts @@ -180,7 +180,9 @@ export const provideHass = ( config: demoConfig, themes: { default_theme: "default", + default_dark_theme: null, themes: {}, + darkMode: false, }, panels: demoPanels, services: demoServices, @@ -253,7 +255,7 @@ export const provideHass = ( mockTheme(theme) { invalidateThemeCache(); hass().updateHass({ - selectedTheme: theme ? "mock" : "default", + selectedTheme: { theme: theme ? "mock" : "default" }, themes: { ...hass().themes, themes: { @@ -265,7 +267,7 @@ export const provideHass = ( applyThemesOnElement( document.documentElement, themes, - selectedTheme as string + selectedTheme!.theme ); }, diff --git a/src/html/index.html.template b/src/html/index.html.template index 4930225c3a..8f20524cd0 100644 --- a/src/html/index.html.template +++ b/src/html/index.html.template @@ -35,6 +35,7 @@ + diff --git a/src/onboarding/onboarding-core-config.ts b/src/onboarding/onboarding-core-config.ts index 4b72d0d8e6..1a9dd0c224 100644 --- a/src/onboarding/onboarding-core-config.ts +++ b/src/onboarding/onboarding-core-config.ts @@ -90,6 +90,7 @@ class OnboardingCoreConfig extends LitElement {
diff --git a/src/panels/config/entities/dialog-entity-editor.ts b/src/panels/config/entities/dialog-entity-editor.ts index 0403b0ffc5..72fda1301a 100644 --- a/src/panels/config/entities/dialog-entity-editor.ts +++ b/src/panels/config/entities/dialog-entity-editor.ts @@ -235,7 +235,7 @@ export class DialogEntityEditor extends LitElement { css` ha-header-bar { --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; } diff --git a/src/panels/config/zone/dialog-zone-detail.ts b/src/panels/config/zone/dialog-zone-detail.ts index 8b1cc43801..a7c52bda2a 100644 --- a/src/panels/config/zone/dialog-zone-detail.ts +++ b/src/panels/config/zone/dialog-zone-detail.ts @@ -140,6 +140,7 @@ class DialogZoneDetail extends LitElement { > { - [this._leafletMap, this.Leaflet] = await setupLeafletMap( + [this._leafletMap, this.Leaflet, this._tileLayer] = await setupLeafletMap( this._mapEl, - this._config !== undefined ? this._config.dark_mode === true : false + this._config!.dark_mode ?? this.hass.themes.darkMode ); this._drawEntities(); this._leafletMap.invalidateSize(); this._fitMap(); } + private _replaceTileLayer() { + const map = this._leafletMap; + const config = this._config; + const Leaflet = this.Leaflet; + if (!map || !config || !Leaflet || !this._tileLayer) { + return; + } + this._tileLayer = replaceTileLayer( + Leaflet, + map, + this._tileLayer, + this._config!.dark_mode ?? this.hass.themes.darkMode + ); + } + private updateMap(oldConfig: MapCardConfig): void { const map = this._leafletMap; const config = this._config; const Leaflet = this.Leaflet; - if (!map || !config || !Leaflet) { + if (!map || !config || !Leaflet || !this._tileLayer) { return; } - if (config.dark_mode !== oldConfig.dark_mode) { - createTileLayer(Leaflet, config.dark_mode === true).addTo(map); + if (this._config!.dark_mode !== oldConfig.dark_mode) { + this._replaceTileLayer(); } if ( config.entities !== oldConfig.entities || @@ -493,7 +521,11 @@ class HuiMapCard extends LitElement implements LovelaceCard { icon: Leaflet.divIcon({ html: iconHTML, iconSize: [24, 24], - className: this._config!.dark_mode === true ? "dark" : "light", + className: this._config!.dark_mode + ? "dark" + : this._config!.dark_mode === false + ? "light" + : "", }), interactive: false, title, @@ -649,11 +681,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { left: 0; width: 100%; height: 100%; - background: #fafaf8; - } - - #map.dark { - background: #090909; + background: inherit; } ha-icon-button { diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index 8191ea54c2..ea056dec12 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -40,15 +40,13 @@ export class HuiCardOptions extends LitElement { return html` -
-
- ${this.hass!.localize( - "ui.panel.lovelace.editor.edit_card.edit" - )} -
-
+
+ ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_card.edit" + )} +
@@ -69,7 +73,11 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) { } async loadMap() { - [this._map, this.Leaflet] = await setupLeafletMap(this.$.map); + this._darkMode = this.hass.themes.darkMode; + [this._map, this.Leaflet, this._tileLayer] = await setupLeafletMap( + this.$.map, + this._darkMode + ); this.drawEntities(this.hass); this._map.invalidateSize(); this.fitMap(); @@ -113,6 +121,16 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) { var map = this._map; if (!map) return; + if (this._darkMode !== this.hass.themes.darkMode) { + this._darkMode = this.hass.themes.darkMode; + this._tileLayer = replaceTileLayer( + this.Leaflet, + map, + this._tileLayer, + this.hass.themes.darkMode + ); + } + if (this._mapItems) { this._mapItems.forEach(function (marker) { marker.remove(); @@ -160,7 +178,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) { icon = this.Leaflet.divIcon({ html: iconHTML, iconSize: [24, 24], - className: "light", + className: "icon", }); // create marker with the icon diff --git a/src/panels/profile/ha-pick-theme-row.js b/src/panels/profile/ha-pick-theme-row.js deleted file mode 100644 index 9d4f62dec1..0000000000 --- a/src/panels/profile/ha-pick-theme-row.js +++ /dev/null @@ -1,112 +0,0 @@ -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import "../../components/ha-paper-dropdown-menu"; -import { EventsMixin } from "../../mixins/events-mixin"; -import LocalizeMixin from "../../mixins/localize-mixin"; - -/* - * @appliesMixin LocalizeMixin - * @appliesMixin EventsMixin - */ -class HaPickThemeRow extends LocalizeMixin(EventsMixin(PolymerElement)) { - static get template() { - return html` - - - [[localize('ui.panel.profile.themes.header')]] - - - [[localize('ui.panel.profile.themes.link_promo')]] - - - - - - - - `; - } - - static get properties() { - return { - hass: Object, - narrow: Boolean, - _hasThemes: { - type: Boolean, - computed: "_compHasThemes(hass)", - }, - themes: { - type: Array, - computed: "_computeThemes(hass)", - }, - selectedTheme: { - type: Number, - }, - }; - } - - static get observers() { - return ["selectionChanged(hass, selectedTheme)"]; - } - - _compHasThemes(hass) { - return ( - hass.themes && - hass.themes.themes && - Object.keys(hass.themes.themes).length - ); - } - - ready() { - super.ready(); - if ( - this.hass.selectedTheme && - this.themes.indexOf(this.hass.selectedTheme) > 0 - ) { - this.selectedTheme = this.themes.indexOf(this.hass.selectedTheme); - } else if (!this.hass.selectedTheme) { - this.selectedTheme = 0; - } - } - - _computeThemes(hass) { - if (!hass) return []; - return ["Backend-selected", "default"].concat( - Object.keys(hass.themes.themes).sort() - ); - } - - selectionChanged(hass, selection) { - if (selection > 0 && selection < this.themes.length) { - if (hass.selectedTheme !== this.themes[selection]) { - this.fire("settheme", this.themes[selection]); - } - } else if (selection === 0 && hass.selectedTheme !== "") { - this.fire("settheme", ""); - } - } -} - -customElements.define("ha-pick-theme-row", HaPickThemeRow); diff --git a/src/panels/profile/ha-pick-theme-row.ts b/src/panels/profile/ha-pick-theme-row.ts new file mode 100644 index 0000000000..84d5dd4992 --- /dev/null +++ b/src/panels/profile/ha-pick-theme-row.ts @@ -0,0 +1,240 @@ +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-listbox/paper-listbox"; +import "../../components/ha-paper-dropdown-menu"; +import { TemplateResult, html } from "lit-html"; +import { + property, + internalProperty, + LitElement, + customElement, + PropertyValues, + CSSResult, + css, +} from "lit-element"; +import { HomeAssistant } from "../../types"; +import "./ha-settings-row"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-formfield"; +import "../../components/ha-radio"; +import "@polymer/paper-input/paper-input"; +import type { HaRadio } from "../../components/ha-radio"; +import "@material/mwc-button/mwc-button"; + +@customElement("ha-pick-theme-row") +export class HaPickThemeRow extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow!: boolean; + + @internalProperty() _themes: string[] = []; + + @internalProperty() _selectedTheme = 0; + + protected render(): TemplateResult { + const hasThemes = + this.hass.themes?.themes && Object.keys(this.hass.themes.themes).length; + const curTheme = + this.hass!.selectedTheme?.theme || this.hass!.themes.default_theme; + return html` + + ${this.hass.localize("ui.panel.profile.themes.header")} + + ${!hasThemes + ? this.hass.localize("ui.panel.profile.themes.error_no_theme") + : ""} + + ${this.hass.localize("ui.panel.profile.themes.link_promo")} + + + + + ${this._themes.map( + (theme) => html`${theme}` + )} + + + + ${curTheme === "default" + ? html`
+ + + + + + + + + + + +
+ + + ${this.hass!.selectedTheme?.primaryColor || + this.hass!.selectedTheme?.accentColor + ? html` + ${this.hass!.localize("ui.panel.profile.themes.reset")} + ` + : ""} +
+
` + : ""} + `; + } + + protected updated(changedProperties: PropertyValues) { + const oldHass = changedProperties.get("hass") as undefined | HomeAssistant; + const themesChanged = + changedProperties.has("hass") && + (!oldHass || oldHass.themes?.themes !== this.hass.themes?.themes); + const selectedThemeChanged = + changedProperties.has("hass") && + (!oldHass || oldHass.selectedTheme !== this.hass.selectedTheme); + + if (themesChanged) { + this._themes = ["Backend-selected", "default"].concat( + Object.keys(this.hass.themes.themes).sort() + ); + } + + if (selectedThemeChanged) { + if ( + this.hass.selectedTheme && + this._themes.indexOf(this.hass.selectedTheme.theme) > 0 + ) { + this._selectedTheme = this._themes.indexOf( + this.hass.selectedTheme.theme + ); + } else if (!this.hass.selectedTheme) { + this._selectedTheme = 0; + } + } + } + + private _handleColorChange(ev: CustomEvent) { + const target = ev.target as any; + fireEvent(this, "settheme", { [target.name]: target.value }); + } + + private _resetColors() { + fireEvent(this, "settheme", { + primaryColor: undefined, + accentColor: undefined, + }); + } + + private _handleDarkMode(ev: CustomEvent) { + let dark: boolean | undefined; + switch ((ev.target as HaRadio).value) { + case "light": + dark = false; + break; + case "dark": + dark = true; + break; + } + fireEvent(this, "settheme", { dark }); + } + + private _handleThemeSelection(ev: CustomEvent) { + const theme = ev.detail.item.theme; + if (theme === "Backend-selected") { + if (this.hass.selectedTheme?.theme) { + fireEvent(this, "settheme", { theme: "" }); + } + return; + } + fireEvent(this, "settheme", { theme }); + } + + static get styles(): CSSResult { + return css` + a { + color: var(--primary-color); + } + .inputs { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + margin: 0 12px; + } + ha-formfield { + margin: 0 4px; + } + .color-pickers { + display: flex; + justify-content: flex-end; + align-items: center; + flex-grow: 1; + } + paper-input { + min-width: 75px; + flex-grow: 1; + margin: 0 4px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-pick-theme-row": HaPickThemeRow; + } +} diff --git a/src/resources/codemirror.ts b/src/resources/codemirror.ts index 4fc2764582..de5b446029 100644 --- a/src/resources/codemirror.ts +++ b/src/resources/codemirror.ts @@ -6,6 +6,7 @@ import "codemirror/mode/jinja2/jinja2"; import "codemirror/mode/yaml/yaml"; import { fireEvent } from "../common/dom/fire_event"; +// @ts-ignore _CodeMirror.commands.save = (cm: Editor) => { fireEvent(cm.getWrapperElement(), "editor-save"); }; diff --git a/src/resources/ha-style.ts b/src/resources/ha-style.ts index ae49b37a86..efba1b5077 100644 --- a/src/resources/ha-style.ts +++ b/src/resources/ha-style.ts @@ -22,6 +22,7 @@ documentContainer.innerHTML = ` --primary-text-color: #212121; --secondary-text-color: #727272; --text-primary-color: #ffffff; + --text-light-primary-color: #212121; --disabled-text-color: #bdbdbd; /* main interface colors */ @@ -83,9 +84,6 @@ documentContainer.innerHTML = ` /* set our slider style */ --ha-paper-slider-pin-font-size: 15px; - /* markdown styles */ - --markdown-code-background-color: #f6f8fa; - /* rgb */ --rgb-primary-color: 3, 169, 244; --rgb-accent-color: 255, 152, 0; diff --git a/src/resources/markdown_worker.ts b/src/resources/markdown_worker.ts index f248520b6a..0a72f60b51 100644 --- a/src/resources/markdown_worker.ts +++ b/src/resources/markdown_worker.ts @@ -2,8 +2,7 @@ import "proxy-polyfill"; import { expose } from "comlink"; import marked from "marked"; -// @ts-ignore -import filterXSS from "xss"; +import { filterXSS, getDefaultWhiteList } from "xss"; interface WhiteList { [tag: string]: string[]; @@ -14,7 +13,7 @@ let whiteListSvg: WhiteList | undefined; const renderMarkdown = ( content: string, - markedOptions: object, + markedOptions: marked.MarkedOptions, hassOptions: { // Do not allow SVG on untrusted content, it allows XSS. allowSvg?: boolean; @@ -22,7 +21,7 @@ const renderMarkdown = ( ): string => { if (!whiteListNormal) { whiteListNormal = { - ...filterXSS.whiteList, + ...(getDefaultWhiteList() as WhiteList), "ha-icon": ["icon"], "ha-svg-icon": ["path"], }; diff --git a/src/resources/styles.ts b/src/resources/styles.ts index 0ea48f1096..9389b40986 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -1,5 +1,18 @@ import { css } from "lit-element"; +export const darkStyles = { + "primary-background-color": "#111111", + "card-background-color": "#1c1c1c", + "secondary-background-color": "#1e1e1e", + "primary-text-color": "#e1e1e1", + "secondary-text-color": "#9b9b9b", + "app-header-text-color": "#e1e1e1", + "app-header-background-color": "#1c1c1c", + "switch-unchecked-button-color": "#999999", + "switch-unchecked-track-color": "#9b9b9b", + "divider-color": "rgba(225, 225, 225, .12)", +}; + export const derivedStyles = { "error-state-color": "var(--error-color)", "state-icon-unavailable-color": "var(--disabled-text-color)", @@ -33,6 +46,7 @@ export const derivedStyles = { "paper-slider-secondary-color": "var(--slider-secondary-color)", "paper-slider-container-color": "var(--slider-bar-color)", "data-table-background-color": "var(--card-background-color)", + "markdown-code-background-color": "var(--primary-background-color)", "mdc-theme-primary": "var(--primary-color)", "mdc-theme-secondary": "var(--accent-color)", "mdc-theme-background": "var(--primary-background-color)", @@ -48,6 +62,8 @@ export const derivedStyles = { "material-secondary-background-color": "var(--secondary-background-color)", "mdc-checkbox-unchecked-color": "rgba(var(--rgb-primary-text-color), 0.54)", "mdc-checkbox-disabled-color": "var(--disabled-text-color)", + "mdc-radio-unchecked-color": "rgba(var(--rgb-primary-text-color), 0.54)", + "mdc-radio-disabled-color": "var(--disabled-text-color)", "mdc-tab-text-label-color-default": "var(--primary-text-color)", }; diff --git a/src/state/themes-mixin.ts b/src/state/themes-mixin.ts index d93435dbda..b4470a237a 100644 --- a/src/state/themes-mixin.ts +++ b/src/state/themes-mixin.ts @@ -4,26 +4,34 @@ import { } from "../common/dom/apply_themes_on_element"; import { HASSDomEvent } from "../common/dom/fire_event"; import { subscribeThemes } from "../data/ws-themes"; -import { Constructor } from "../types"; +import { Constructor, HomeAssistant } from "../types"; import { storeState } from "../util/ha-pref-storage"; import { HassBaseEl } from "./hass-base-mixin"; declare global { // for add event listener interface HTMLElementEventMap { - settheme: HASSDomEvent; + settheme: HASSDomEvent>; + } + interface HASSDomEvents { + settheme: Partial; } } +const mql = matchMedia("(prefers-color-scheme: dark)"); + export default >(superClass: T) => class extends superClass { protected firstUpdated(changedProps) { super.firstUpdated(changedProps); this.addEventListener("settheme", (ev) => { - this._updateHass({ selectedTheme: ev.detail }); - this._applyTheme(); + this._updateHass({ + selectedTheme: { ...this.hass!.selectedTheme!, ...ev.detail }, + }); + this._applyTheme(mql.matches); storeState(this.hass!); }); + mql.addListener((ev) => this._applyTheme(ev.matches)); } protected hassConnected() { @@ -32,29 +40,68 @@ export default >(superClass: T) => subscribeThemes(this.hass!.connection, (themes) => { this._updateHass({ themes }); invalidateThemeCache(); - this._applyTheme(); + this._applyTheme(mql.matches); }); } - private _applyTheme() { + private _applyTheme(dark: boolean) { + const themeName = + this.hass!.selectedTheme?.theme || + (dark && this.hass!.themes.default_dark_theme + ? this.hass!.themes.default_dark_theme! + : this.hass!.themes.default_theme); + + let options: Partial = this.hass! + .selectedTheme; + + if (themeName === "default" && options?.dark === undefined) { + options = { + ...this.hass!.selectedTheme!, + dark, + }; + } + applyThemesOnElement( document.documentElement, this.hass!.themes, - this.hass!.selectedTheme || this.hass!.themes.default_theme + themeName, + options ); - const meta = document.querySelector("meta[name=theme-color]"); + const darkMode = + themeName === "default" + ? !!options?.dark + : !!(dark && this.hass!.themes.default_dark_theme); + + if (darkMode !== this.hass!.themes.darkMode) { + this._updateHass({ + themes: { ...this.hass!.themes, darkMode }, + }); + + const schemeMeta = document.querySelector("meta[name=color-scheme]"); + if (schemeMeta) { + schemeMeta.setAttribute( + "content", + darkMode ? "dark" : themeName === "default" ? "light" : "dark light" + ); + } + } + + const themeMeta = document.querySelector("meta[name=theme-color]"); const headerColor = getComputedStyle( document.documentElement ).getPropertyValue("--app-header-background-color"); - if (meta) { - if (!meta.hasAttribute("default-content")) { - meta.setAttribute("default-content", meta.getAttribute("content")!); + if (themeMeta) { + if (!themeMeta.hasAttribute("default-content")) { + themeMeta.setAttribute( + "default-content", + themeMeta.getAttribute("content")! + ); } const themeColor = headerColor.trim() || - (meta.getAttribute("default-content") as string); - meta.setAttribute("content", themeColor); + (themeMeta.getAttribute("default-content") as string); + themeMeta.setAttribute("content", themeColor); } } }; diff --git a/src/translations/en.json b/src/translations/en.json index 66b83f4f93..102b5dc349 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2199,7 +2199,15 @@ "header": "Theme", "error_no_theme": "No themes available.", "link_promo": "Learn about themes", - "dropdown_label": "Theme" + "dropdown_label": "Theme", + "dark_mode": { + "auto": "Auto", + "light": "Light", + "dark": "Dark" + }, + "primary_color": "Primary color", + "accent_color": "Accent color", + "reset": "Reset" }, "dashboard": { "header": "Dashboard", diff --git a/src/types.ts b/src/types.ts index 31684b0bb7..57a945a993 100644 --- a/src/types.ts +++ b/src/types.ts @@ -87,11 +87,21 @@ export interface Theme { "primary-color": string; "text-primary-color": string; "accent-color": string; + [key: string]: string; } export interface Themes { default_theme: string; + default_dark_theme: string | null; themes: { [key: string]: Theme }; + darkMode: boolean; +} + +export interface ThemeSettings { + theme: string; + dark?: boolean; + primaryColor?: string; + accentColor?: string; } export interface PanelInfo { @@ -193,7 +203,7 @@ export interface HomeAssistant { services: HassServices; config: HassConfig; themes: Themes; - selectedTheme?: string | null; + selectedTheme?: ThemeSettings | null; panels: Panels; panelUrl: string; diff --git a/src/util/ha-pref-storage.ts b/src/util/ha-pref-storage.ts index 404aabb0d9..bb06674f35 100644 --- a/src/util/ha-pref-storage.ts +++ b/src/util/ha-pref-storage.ts @@ -27,6 +27,10 @@ export function getState() { STORED_STATE.forEach((key) => { if (key in STORAGE) { let value = JSON.parse(STORAGE[key]); + // selectedTheme went from string to object on 20200718 + if (key === "selectedTheme" && typeof value === "string") { + value = { theme: value }; + } // dockedSidebar went from boolean to enum on 20190720 if (key === "dockedSidebar" && typeof value === "boolean") { value = value ? "docked" : "auto"; diff --git a/tsconfig.json b/tsconfig.json index 23d0f32d2f..47ceb96016 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "skipLibCheck": true, "resolveJsonModule": true, "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, "plugins": [ { "name": "ts-lit-plugin", diff --git a/yarn.lock b/yarn.lock index 005560d52f..c813bc0508 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2532,10 +2532,10 @@ dependencies: "@types/chrome" "*" -"@types/codemirror@^0.0.78": - version "0.0.78" - resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.78.tgz#75a8eabda268c8e734855fb24e8c86192e2e18ad" - integrity sha512-QpMQUpEL+ZNcpEhjvYM/H6jqDx9nNcJqymA2kbkNthFS2I7ekL7ofEZ7+MoQAFTBuJers91K0FGCMpL7MwC9TQ== +"@types/codemirror@^0.0.97": + version "0.0.97" + resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.97.tgz#6f2d8266b7f1b34aacfe8c77221fafe324c3d081" + integrity sha512-n5d7o9nWhC49DjfhsxANP7naWSeTzrjXASkUDQh7626sM4zK9XP2EVcHp1IcCf/IPV6c7ORzDUDF3Bkt231VKg== dependencies: "@types/tern" "*" @@ -2641,6 +2641,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== +"@types/marked@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/marked/-/marked-1.1.0.tgz#53509b5f127e0c05c19176fcf1d743a41e00ff19" + integrity sha512-j8XXj6/l9kFvCwMyVqozznqpd/nk80krrW+QiIJN60Uu9gX5Pvn4/qPJ2YngQrR3QREPwmrE1f9/EWKVTFzoEw== + "@types/memoize-one@4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@types/memoize-one/-/memoize-one-4.1.0.tgz#62119f26055b3193ae43ca1882c5b29b88b71ece" @@ -8258,10 +8263,10 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -marked@^0.6.1: - version "0.6.2" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.6.2.tgz#c574be8b545a8b48641456ca1dbe0e37b6dccc1a" - integrity sha512-LqxwVH3P/rqKX4EKGz7+c2G9r98WeM/SW34ybhgNGhUQNKtf1GmmSkJ6cDGJ/t6tiyae49qRkpyTw2B9HOrgUA== +marked@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/marked/-/marked-1.1.1.tgz#e5d61b69842210d5df57b05856e0c91572703e6a" + integrity sha512-mJzT8D2yPxoPh7h0UXkB+dBj4FykPJ2OIfxAWeIHrvoHDkFxukV/29QxoFQoPM6RLEwhIFdJpmKBlqVM3s2ZIw== matchdep@^2.0.0: version "2.0.0"