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

View File

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

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 {
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<HomeAssistant["selectedTheme"]>
) => {
const newTheme = selectedTheme
? PROCESSED_THEMES[selectedTheme] || processTheme(selectedTheme, themes)
: undefined;
let cacheKey = selectedTheme;
let themeRules: Partial<Theme> = {};
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<Theme>
): ProcessedTheme | undefined => {
if (!themes.themes[themeName]) {
if (!theme || !Object.keys(theme).length) {
return undefined;
}
const theme: Theme = {
const combinedTheme: Partial<Theme> = {
...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 };
};

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

View File

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

View File

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

View File

@ -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;
}
`;

View File

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

View File

@ -23,7 +23,6 @@ class HaMarkdownElement extends UpdatingElement {
{
breaks: this.breaks,
gfm: true,
tables: true,
},
{
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;
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;

View File

@ -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<void> {
[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;

View File

@ -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<void> {
[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;
}

View File

@ -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<void> {
[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,

View File

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

View File

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

View File

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

View File

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

View File

@ -35,6 +35,7 @@
<meta name="mobile-web-app-capable" content="yes" />
<meta name="referrer" content="same-origin" />
<meta name="theme-color" content="#THEMEC" />
<meta name="color-scheme" content="dark light" />
<style>
#ha-init-skeleton::before {
display: block;
@ -43,7 +44,7 @@
background-color: #THEMEC;
}
html {
background-color: var(--primary-background-color, #fafafa);
background-color: var(--primary-background-color);
}
</style>
</head>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import {
Map,
Marker,
Polyline,
TileLayer,
} from "leaflet";
import {
css,
@ -21,9 +22,9 @@ import {
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import {
createTileLayer,
LeafletModuleType,
setupLeafletMap,
replaceTileLayer,
} from "../../../common/dom/setup-leaflet-map";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
@ -68,7 +69,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return { type: "map", entities: foundEntities };
}
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true })
public isPanel = false;
@ -91,6 +92,8 @@ class HuiMapCard extends LitElement implements LovelaceCard {
private _leafletMap?: Map;
private _tileLayer?: TileLayer;
private _resizeObserver?: ResizeObserver;
private _debouncedResizeListener = debounce(
@ -225,6 +228,10 @@ class HuiMapCard extends LitElement implements LovelaceCard {
return true;
}
if (oldHass.themes.darkMode !== this.hass.themes.darkMode) {
return true;
}
// Check if any state has changed
for (const entity of this._configEntities) {
if (oldHass.states[entity.entity] !== this.hass!.states[entity.entity]) {
@ -266,6 +273,12 @@ class HuiMapCard extends LitElement implements LovelaceCard {
this._drawEntities();
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 (
changedProps.has("_config") &&
changedProps.get("_config") !== undefined
@ -288,24 +301,39 @@ class HuiMapCard extends LitElement implements LovelaceCard {
}
private async loadMap(): Promise<void> {
[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 {

View File

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

View File

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

View File

@ -48,8 +48,7 @@ class HuiSectionRow extends LitElement implements LovelaceRow {
}
.divider {
height: 1px;
background-color: var(--secondary-text-color);
opacity: 0.25;
background-color: var(--divider-color);
margin-left: -16px;
margin-right: -16px;
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";
/* eslint-plugin-disable lit */
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 { computeStateName } from "../../common/entity/compute_state_name";
import { navigate } from "../../common/navigate";
@ -25,10 +28,11 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
height: calc(100vh - 64px);
width: 100%;
z-index: 0;
background: inherit;
}
.light {
color: #000000;
.icon {
color: var(--primary-text-color);
}
</style>
@ -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

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 { fireEvent } from "../common/dom/fire_event";
// @ts-ignore
_CodeMirror.commands.save = (cm: Editor) => {
fireEvent(cm.getWrapperElement(), "editor-save");
};

View File

@ -22,6 +22,7 @@ documentContainer.innerHTML = `<custom-style>
--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 = `<custom-style>
/* 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;

View File

@ -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"],
};

View File

@ -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)",
};

View File

@ -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<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) =>
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 <T extends Constructor<HassBaseEl>>(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<HomeAssistant["selectedTheme"]> = 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);
}
}
};

View File

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

View File

@ -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<T = {} | null> {
@ -193,7 +203,7 @@ export interface HomeAssistant {
services: HassServices;
config: HassConfig;
themes: Themes;
selectedTheme?: string | null;
selectedTheme?: ThemeSettings | null;
panels: Panels;
panelUrl: string;

View File

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

View File

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

View File

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