Introduce dark mode and primary color picker (#6430)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Bram Kragten 2020-08-03 02:07:12 +02:00 committed by GitHub
parent 0d515e2303
commit 4ca13c409b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 814 additions and 265 deletions

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

@ -35,6 +35,7 @@
"@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",
@ -100,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",
@ -136,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",

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

@ -541,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;
@ -559,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 {
@ -578,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;
} }
@ -831,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

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

@ -34,6 +34,7 @@ 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 {

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

@ -717,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;

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

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

@ -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));

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

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

@ -35,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;
@ -43,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

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

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

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

@ -140,6 +140,7 @@ class DialogZoneDetail extends LitElement {
></paper-input> ></paper-input>
<ha-location-editor <ha-location-editor
class="flex" class="flex"
.hass=${this.hass}
.location=${this._locationValue} .location=${this._locationValue}
.radius=${this._radius} .radius=${this._radius}
.radiusColor=${this._passive .radiusColor=${this._passive

View File

@ -240,6 +240,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
? html` ? html`
<div class="flex"> <div class="flex">
<ha-locations-editor <ha-locations-editor
.hass=${this.hass}
.locations=${this._getZones( .locations=${this._getZones(
this._storageItems, this._storageItems,
this._stateItems this._stateItems

View File

@ -8,6 +8,7 @@ import {
Map, Map,
Marker, Marker,
Polyline, Polyline,
TileLayer,
} from "leaflet"; } from "leaflet";
import { import {
css, css,
@ -21,9 +22,9 @@ import {
} from "lit-element"; } from "lit-element";
import { classMap } from "lit-html/directives/class-map"; import { classMap } from "lit-html/directives/class-map";
import { import {
createTileLayer,
LeafletModuleType, LeafletModuleType,
setupLeafletMap, setupLeafletMap,
replaceTileLayer,
} from "../../../common/dom/setup-leaflet-map"; } from "../../../common/dom/setup-leaflet-map";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain";
@ -68,7 +69,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return { type: "map", entities: foundEntities }; return { type: "map", entities: foundEntities };
} }
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) @property({ type: Boolean, reflect: true })
public isPanel = false; public isPanel = false;
@ -91,6 +92,8 @@ class HuiMapCard extends LitElement implements LovelaceCard {
private _leafletMap?: Map; private _leafletMap?: Map;
private _tileLayer?: TileLayer;
private _resizeObserver?: ResizeObserver; private _resizeObserver?: ResizeObserver;
private _debouncedResizeListener = debounce( private _debouncedResizeListener = debounce(
@ -225,6 +228,10 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return true; return true;
} }
if (oldHass.themes.darkMode !== this.hass.themes.darkMode) {
return true;
}
// Check if any state has changed // Check if any state has changed
for (const entity of this._configEntities) { for (const entity of this._configEntities) {
if (oldHass.states[entity.entity] !== this.hass!.states[entity.entity]) { if (oldHass.states[entity.entity] !== this.hass!.states[entity.entity]) {
@ -266,6 +273,12 @@ class HuiMapCard extends LitElement implements LovelaceCard {
this._drawEntities(); this._drawEntities();
this._fitMap(); this._fitMap();
} }
if (changedProps.has("hass")) {
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (oldHass && oldHass.themes.darkMode !== this.hass.themes.darkMode) {
this._replaceTileLayer();
}
}
if ( if (
changedProps.has("_config") && changedProps.has("_config") &&
changedProps.get("_config") !== undefined changedProps.get("_config") !== undefined
@ -288,24 +301,39 @@ class HuiMapCard extends LitElement implements LovelaceCard {
} }
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._config !== undefined ? this._config.dark_mode === true : false this._config!.dark_mode ?? this.hass.themes.darkMode
); );
this._drawEntities(); this._drawEntities();
this._leafletMap.invalidateSize(); this._leafletMap.invalidateSize();
this._fitMap(); 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 { private updateMap(oldConfig: MapCardConfig): void {
const map = this._leafletMap; const map = this._leafletMap;
const config = this._config; const config = this._config;
const Leaflet = this.Leaflet; const Leaflet = this.Leaflet;
if (!map || !config || !Leaflet) { if (!map || !config || !Leaflet || !this._tileLayer) {
return; return;
} }
if (config.dark_mode !== oldConfig.dark_mode) { if (this._config!.dark_mode !== oldConfig.dark_mode) {
createTileLayer(Leaflet, config.dark_mode === true).addTo(map); this._replaceTileLayer();
} }
if ( if (
config.entities !== oldConfig.entities || config.entities !== oldConfig.entities ||
@ -493,7 +521,11 @@ class HuiMapCard extends LitElement implements LovelaceCard {
icon: Leaflet.divIcon({ icon: Leaflet.divIcon({
html: iconHTML, html: iconHTML,
iconSize: [24, 24], 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, interactive: false,
title, title,
@ -649,11 +681,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: #fafaf8; background: inherit;
}
#map.dark {
background: #090909;
} }
ha-icon-button { ha-icon-button {

View File

@ -40,15 +40,13 @@ export class HuiCardOptions extends LitElement {
return html` return html`
<slot></slot> <slot></slot>
<ha-card> <ha-card>
<div class="options"> <div class="card-actions">
<div class="primary-actions">
<mwc-button @click=${this._editCard} <mwc-button @click=${this._editCard}
>${this.hass!.localize( >${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.edit" "ui.panel.lovelace.editor.edit_card.edit"
)}</mwc-button )}</mwc-button
> >
</div> <div>
<div class="secondary-actions">
<mwc-icon-button <mwc-icon-button
title="Move card down" title="Move card down"
class="move-arrow" class="move-arrow"
@ -112,21 +110,10 @@ export class HuiCardOptions extends LitElement {
border-top-left-radius: 0; border-top-left-radius: 0;
} }
div.options { .card-actions {
border-top: 1px solid #e8e8e8;
padding: 5px 8px;
display: flex; display: flex;
margin-top: -1px; justify-content: space-between;
} align-items: center;
div.options .primary-actions {
flex: 1;
margin: auto;
}
div.options .secondary-actions {
flex: 4;
text-align: right;
} }
mwc-icon-button { mwc-icon-button {

View File

@ -22,7 +22,7 @@ class HuiDividerRow extends LitElement implements LovelaceRow {
this._config = { this._config = {
style: { style: {
height: "1px", height: "1px",
"background-color": "var(--secondary-text-color)", "background-color": "var(--divider-color)",
}, },
...config, ...config,
}; };

View File

@ -48,8 +48,7 @@ class HuiSectionRow extends LitElement implements LovelaceRow {
} }
.divider { .divider {
height: 1px; height: 1px;
background-color: var(--secondary-text-color); background-color: var(--divider-color);
opacity: 0.25;
margin-left: -16px; margin-left: -16px;
margin-right: -16px; margin-right: -16px;
margin-top: 8px; margin-top: 8px;

View File

@ -2,7 +2,10 @@ import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag"; import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */ /* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element"; import { PolymerElement } from "@polymer/polymer/polymer-element";
import { setupLeafletMap } from "../../common/dom/setup-leaflet-map"; import {
setupLeafletMap,
replaceTileLayer,
} 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";
import { navigate } from "../../common/navigate"; import { navigate } from "../../common/navigate";
@ -25,10 +28,11 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
height: calc(100vh - 64px); height: calc(100vh - 64px);
width: 100%; width: 100%;
z-index: 0; z-index: 0;
background: inherit;
} }
.light { .icon {
color: #000000; color: var(--primary-text-color);
} }
</style> </style>
@ -69,7 +73,11 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
} }
async loadMap() { 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.drawEntities(this.hass);
this._map.invalidateSize(); this._map.invalidateSize();
this.fitMap(); this.fitMap();
@ -113,6 +121,16 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
var map = this._map; var map = this._map;
if (!map) return; 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) { if (this._mapItems) {
this._mapItems.forEach(function (marker) { this._mapItems.forEach(function (marker) {
marker.remove(); marker.remove();
@ -160,7 +178,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
icon = this.Leaflet.divIcon({ icon = this.Leaflet.divIcon({
html: iconHTML, html: iconHTML,
iconSize: [24, 24], iconSize: [24, 24],
className: "light", className: "icon",
}); });
// create marker with the icon // create marker with the icon

View File

@ -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`
<style>
a {
color: var(--primary-color);
}
</style>
<ha-settings-row narrow="[[narrow]]">
<span slot="heading"
>[[localize('ui.panel.profile.themes.header')]]</span
>
<span slot="description">
<template is="dom-if" if="[[!_hasThemes]]">
[[localize('ui.panel.profile.themes.error_no_theme')]]
</template>
<a
href="https://www.home-assistant.io/integrations/frontend/#defining-themes"
target="_blank"
rel="noreferrer"
>[[localize('ui.panel.profile.themes.link_promo')]]</a
>
</span>
<ha-paper-dropdown-menu
label="[[localize('ui.panel.profile.themes.dropdown_label')]]"
dynamic-align
disabled="[[!_hasThemes]]"
>
<paper-listbox slot="dropdown-content" selected="{{selectedTheme}}">
<template is="dom-repeat" items="[[themes]]" as="theme">
<paper-item>[[theme]]</paper-item>
</template>
</paper-listbox>
</ha-paper-dropdown-menu>
</ha-settings-row>
`;
}
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);

View File

@ -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`
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading"
>${this.hass.localize("ui.panel.profile.themes.header")}</span
>
<span slot="description">
${!hasThemes
? this.hass.localize("ui.panel.profile.themes.error_no_theme")
: ""}
<a
href="https://www.home-assistant.io/integrations/frontend/#defining-themes"
target="_blank"
rel="noreferrer"
>
${this.hass.localize("ui.panel.profile.themes.link_promo")}
</a>
</span>
<ha-paper-dropdown-menu
.label=${this.hass.localize("ui.panel.profile.themes.dropdown_label")}
dynamic-align
.disabled=${!hasThemes}
>
<paper-listbox
slot="dropdown-content"
.selected=${this._selectedTheme}
@iron-select=${this._handleThemeSelection}
>
${this._themes.map(
(theme) => html`<paper-item .theme=${theme}>${theme}</paper-item>`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
</ha-settings-row>
${curTheme === "default"
? html` <div class="inputs">
<ha-formfield
.label=${this.hass!.localize(
"ui.panel.profile.themes.dark_mode.auto"
)}
>
<ha-radio
@change=${this._handleDarkMode}
name="dark_mode"
value="auto"
?checked=${this.hass.selectedTheme?.dark === undefined}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass!.localize(
"ui.panel.profile.themes.dark_mode.light"
)}
>
<ha-radio
@change=${this._handleDarkMode}
name="dark_mode"
value="light"
?checked=${this.hass.selectedTheme?.dark === false}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass!.localize(
"ui.panel.profile.themes.dark_mode.dark"
)}
>
<ha-radio
@change=${this._handleDarkMode}
name="dark_mode"
value="dark"
?checked=${this.hass.selectedTheme?.dark === true}
>
</ha-radio>
</ha-formfield>
<div class="color-pickers">
<paper-input
.value=${this.hass!.selectedTheme?.primaryColor || "#03a9f4"}
type="color"
.label=${this.hass!.localize(
"ui.panel.profile.themes.primary_color"
)}
.name=${"primaryColor"}
@change=${this._handleColorChange}
></paper-input>
<paper-input
.value=${this.hass!.selectedTheme?.accentColor || "#ff9800"}
type="color"
.label=${this.hass!.localize(
"ui.panel.profile.themes.accent_color"
)}
.name=${"accentColor"}
@change=${this._handleColorChange}
></paper-input>
${this.hass!.selectedTheme?.primaryColor ||
this.hass!.selectedTheme?.accentColor
? html` <mwc-button @click=${this._resetColors}>
${this.hass!.localize("ui.panel.profile.themes.reset")}
</mwc-button>`
: ""}
</div>
</div>`
: ""}
`;
}
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;
}
}

View File

@ -6,6 +6,7 @@ import "codemirror/mode/jinja2/jinja2";
import "codemirror/mode/yaml/yaml"; import "codemirror/mode/yaml/yaml";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
// @ts-ignore
_CodeMirror.commands.save = (cm: Editor) => { _CodeMirror.commands.save = (cm: Editor) => {
fireEvent(cm.getWrapperElement(), "editor-save"); fireEvent(cm.getWrapperElement(), "editor-save");
}; };

View File

@ -22,6 +22,7 @@ documentContainer.innerHTML = `<custom-style>
--primary-text-color: #212121; --primary-text-color: #212121;
--secondary-text-color: #727272; --secondary-text-color: #727272;
--text-primary-color: #ffffff; --text-primary-color: #ffffff;
--text-light-primary-color: #212121;
--disabled-text-color: #bdbdbd; --disabled-text-color: #bdbdbd;
/* main interface colors */ /* main interface colors */
@ -83,9 +84,6 @@ documentContainer.innerHTML = `<custom-style>
/* set our slider style */ /* set our slider style */
--ha-paper-slider-pin-font-size: 15px; --ha-paper-slider-pin-font-size: 15px;
/* markdown styles */
--markdown-code-background-color: #f6f8fa;
/* rgb */ /* rgb */
--rgb-primary-color: 3, 169, 244; --rgb-primary-color: 3, 169, 244;
--rgb-accent-color: 255, 152, 0; --rgb-accent-color: 255, 152, 0;

View File

@ -2,8 +2,7 @@
import "proxy-polyfill"; import "proxy-polyfill";
import { expose } from "comlink"; import { expose } from "comlink";
import marked from "marked"; import marked from "marked";
// @ts-ignore import { filterXSS, getDefaultWhiteList } from "xss";
import filterXSS from "xss";
interface WhiteList { interface WhiteList {
[tag: string]: string[]; [tag: string]: string[];
@ -14,7 +13,7 @@ let whiteListSvg: WhiteList | undefined;
const renderMarkdown = ( const renderMarkdown = (
content: string, content: string,
markedOptions: object, markedOptions: marked.MarkedOptions,
hassOptions: { hassOptions: {
// Do not allow SVG on untrusted content, it allows XSS. // Do not allow SVG on untrusted content, it allows XSS.
allowSvg?: boolean; allowSvg?: boolean;
@ -22,7 +21,7 @@ const renderMarkdown = (
): string => { ): string => {
if (!whiteListNormal) { if (!whiteListNormal) {
whiteListNormal = { whiteListNormal = {
...filterXSS.whiteList, ...(getDefaultWhiteList() as WhiteList),
"ha-icon": ["icon"], "ha-icon": ["icon"],
"ha-svg-icon": ["path"], "ha-svg-icon": ["path"],
}; };

View File

@ -1,5 +1,18 @@
import { css } from "lit-element"; 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 = { export const derivedStyles = {
"error-state-color": "var(--error-color)", "error-state-color": "var(--error-color)",
"state-icon-unavailable-color": "var(--disabled-text-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-secondary-color": "var(--slider-secondary-color)",
"paper-slider-container-color": "var(--slider-bar-color)", "paper-slider-container-color": "var(--slider-bar-color)",
"data-table-background-color": "var(--card-background-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-primary": "var(--primary-color)",
"mdc-theme-secondary": "var(--accent-color)", "mdc-theme-secondary": "var(--accent-color)",
"mdc-theme-background": "var(--primary-background-color)", "mdc-theme-background": "var(--primary-background-color)",
@ -48,6 +62,8 @@ export const derivedStyles = {
"material-secondary-background-color": "var(--secondary-background-color)", "material-secondary-background-color": "var(--secondary-background-color)",
"mdc-checkbox-unchecked-color": "rgba(var(--rgb-primary-text-color), 0.54)", "mdc-checkbox-unchecked-color": "rgba(var(--rgb-primary-text-color), 0.54)",
"mdc-checkbox-disabled-color": "var(--disabled-text-color)", "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)", "mdc-tab-text-label-color-default": "var(--primary-text-color)",
}; };

View File

@ -4,26 +4,34 @@ import {
} from "../common/dom/apply_themes_on_element"; } from "../common/dom/apply_themes_on_element";
import { HASSDomEvent } from "../common/dom/fire_event"; import { HASSDomEvent } from "../common/dom/fire_event";
import { subscribeThemes } from "../data/ws-themes"; import { subscribeThemes } from "../data/ws-themes";
import { Constructor } from "../types"; import { Constructor, HomeAssistant } from "../types";
import { storeState } from "../util/ha-pref-storage"; import { storeState } from "../util/ha-pref-storage";
import { HassBaseEl } from "./hass-base-mixin"; import { HassBaseEl } from "./hass-base-mixin";
declare global { declare global {
// for add event listener // for add event listener
interface HTMLElementEventMap { interface HTMLElementEventMap {
settheme: HASSDomEvent<string>; settheme: HASSDomEvent<Partial<HomeAssistant["selectedTheme"]>>;
}
interface HASSDomEvents {
settheme: Partial<HomeAssistant["selectedTheme"]>;
} }
} }
const mql = matchMedia("(prefers-color-scheme: dark)");
export default <T extends Constructor<HassBaseEl>>(superClass: T) => export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
class extends superClass { class extends superClass {
protected firstUpdated(changedProps) { protected firstUpdated(changedProps) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this.addEventListener("settheme", (ev) => { this.addEventListener("settheme", (ev) => {
this._updateHass({ selectedTheme: ev.detail }); this._updateHass({
this._applyTheme(); selectedTheme: { ...this.hass!.selectedTheme!, ...ev.detail },
});
this._applyTheme(mql.matches);
storeState(this.hass!); storeState(this.hass!);
}); });
mql.addListener((ev) => this._applyTheme(ev.matches));
} }
protected hassConnected() { protected hassConnected() {
@ -32,29 +40,68 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
subscribeThemes(this.hass!.connection, (themes) => { subscribeThemes(this.hass!.connection, (themes) => {
this._updateHass({ themes }); this._updateHass({ themes });
invalidateThemeCache(); 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<HomeAssistant["selectedTheme"]> = this.hass!
.selectedTheme;
if (themeName === "default" && options?.dark === undefined) {
options = {
...this.hass!.selectedTheme!,
dark,
};
}
applyThemesOnElement( applyThemesOnElement(
document.documentElement, document.documentElement,
this.hass!.themes, 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( const headerColor = getComputedStyle(
document.documentElement document.documentElement
).getPropertyValue("--app-header-background-color"); ).getPropertyValue("--app-header-background-color");
if (meta) { if (themeMeta) {
if (!meta.hasAttribute("default-content")) { if (!themeMeta.hasAttribute("default-content")) {
meta.setAttribute("default-content", meta.getAttribute("content")!); themeMeta.setAttribute(
"default-content",
themeMeta.getAttribute("content")!
);
} }
const themeColor = const themeColor =
headerColor.trim() || headerColor.trim() ||
(meta.getAttribute("default-content") as string); (themeMeta.getAttribute("default-content") as string);
meta.setAttribute("content", themeColor); themeMeta.setAttribute("content", themeColor);
} }
} }
}; };

View File

@ -2199,7 +2199,15 @@
"header": "Theme", "header": "Theme",
"error_no_theme": "No themes available.", "error_no_theme": "No themes available.",
"link_promo": "Learn about themes", "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": { "dashboard": {
"header": "Dashboard", "header": "Dashboard",

View File

@ -87,11 +87,21 @@ export interface Theme {
"primary-color": string; "primary-color": string;
"text-primary-color": string; "text-primary-color": string;
"accent-color": string; "accent-color": string;
[key: string]: string;
} }
export interface Themes { export interface Themes {
default_theme: string; default_theme: string;
default_dark_theme: string | null;
themes: { [key: string]: Theme }; themes: { [key: string]: Theme };
darkMode: boolean;
}
export interface ThemeSettings {
theme: string;
dark?: boolean;
primaryColor?: string;
accentColor?: string;
} }
export interface PanelInfo<T = {} | null> { export interface PanelInfo<T = {} | null> {
@ -193,7 +203,7 @@ export interface HomeAssistant {
services: HassServices; services: HassServices;
config: HassConfig; config: HassConfig;
themes: Themes; themes: Themes;
selectedTheme?: string | null; selectedTheme?: ThemeSettings | null;
panels: Panels; panels: Panels;
panelUrl: string; panelUrl: string;

View File

@ -27,6 +27,10 @@ export function getState() {
STORED_STATE.forEach((key) => { STORED_STATE.forEach((key) => {
if (key in STORAGE) { if (key in STORAGE) {
let value = JSON.parse(STORAGE[key]); 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 // dockedSidebar went from boolean to enum on 20190720
if (key === "dockedSidebar" && typeof value === "boolean") { if (key === "dockedSidebar" && typeof value === "boolean") {
value = value ? "docked" : "auto"; value = value ? "docked" : "auto";

View File

@ -14,6 +14,7 @@
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"plugins": [ "plugins": [
{ {
"name": "ts-lit-plugin", "name": "ts-lit-plugin",

View File

@ -2532,10 +2532,10 @@
dependencies: dependencies:
"@types/chrome" "*" "@types/chrome" "*"
"@types/codemirror@^0.0.78": "@types/codemirror@^0.0.97":
version "0.0.78" version "0.0.97"
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.78.tgz#75a8eabda268c8e734855fb24e8c86192e2e18ad" resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.97.tgz#6f2d8266b7f1b34aacfe8c77221fafe324c3d081"
integrity sha512-QpMQUpEL+ZNcpEhjvYM/H6jqDx9nNcJqymA2kbkNthFS2I7ekL7ofEZ7+MoQAFTBuJers91K0FGCMpL7MwC9TQ== integrity sha512-n5d7o9nWhC49DjfhsxANP7naWSeTzrjXASkUDQh7626sM4zK9XP2EVcHp1IcCf/IPV6c7ORzDUDF3Bkt231VKg==
dependencies: dependencies:
"@types/tern" "*" "@types/tern" "*"
@ -2641,6 +2641,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440"
integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== 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": "@types/memoize-one@4.1.0":
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/@types/memoize-one/-/memoize-one-4.1.0.tgz#62119f26055b3193ae43ca1882c5b29b88b71ece" 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: dependencies:
object-visit "^1.0.0" object-visit "^1.0.0"
marked@^0.6.1: marked@^1.1.1:
version "0.6.2" version "1.1.1"
resolved "https://registry.yarnpkg.com/marked/-/marked-0.6.2.tgz#c574be8b545a8b48641456ca1dbe0e37b6dccc1a" resolved "https://registry.yarnpkg.com/marked/-/marked-1.1.1.tgz#e5d61b69842210d5df57b05856e0c91572703e6a"
integrity sha512-LqxwVH3P/rqKX4EKGz7+c2G9r98WeM/SW34ybhgNGhUQNKtf1GmmSkJ6cDGJ/t6tiyae49qRkpyTw2B9HOrgUA== integrity sha512-mJzT8D2yPxoPh7h0UXkB+dBj4FykPJ2OIfxAWeIHrvoHDkFxukV/29QxoFQoPM6RLEwhIFdJpmKBlqVM3s2ZIw==
matchdep@^2.0.0: matchdep@^2.0.0:
version "2.0.0" version "2.0.0"