mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 15:26:36 +00:00
Introduce dark mode and primary color picker (#6430)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
0d515e2303
commit
4ca13c409b
@ -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(
|
||||
|
@ -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",
|
||||
|
113
src/common/color/convert-color.ts
Normal file
113
src/common/color/convert-color.ts
Normal 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
16
src/common/color/lab.ts
Normal 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
24
src/common/color/rgb.ts
Normal 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);
|
||||
};
|
@ -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 };
|
||||
};
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
`;
|
||||
|
@ -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 {
|
||||
|
@ -23,7 +23,6 @@ class HaMarkdownElement extends UpdatingElement {
|
||||
{
|
||||
breaks: this.breaks,
|
||||
gfm: true,
|
||||
tables: true,
|
||||
},
|
||||
{
|
||||
allowSvg: this.allowSvg,
|
||||
|
20
src/components/ha-radio.ts
Normal file
20
src/components/ha-radio.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
240
src/panels/profile/ha-pick-theme-row.ts
Normal file
240
src/panels/profile/ha-pick-theme-row.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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");
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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"],
|
||||
};
|
||||
|
@ -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)",
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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",
|
||||
|
12
src/types.ts
12
src/types.ts
@ -87,11 +87,21 @@ export interface Theme {
|
||||
"primary-color": string;
|
||||
"text-primary-color": string;
|
||||
"accent-color": string;
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface Themes {
|
||||
default_theme: string;
|
||||
default_dark_theme: string | null;
|
||||
themes: { [key: string]: Theme };
|
||||
darkMode: boolean;
|
||||
}
|
||||
|
||||
export interface ThemeSettings {
|
||||
theme: string;
|
||||
dark?: boolean;
|
||||
primaryColor?: string;
|
||||
accentColor?: string;
|
||||
}
|
||||
|
||||
export interface PanelInfo<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;
|
||||
|
||||
|
@ -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";
|
||||
|
@ -14,6 +14,7 @@
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "ts-lit-plugin",
|
||||
|
21
yarn.lock
21
yarn.lock
@ -2532,10 +2532,10 @@
|
||||
dependencies:
|
||||
"@types/chrome" "*"
|
||||
|
||||
"@types/codemirror@^0.0.78":
|
||||
version "0.0.78"
|
||||
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.78.tgz#75a8eabda268c8e734855fb24e8c86192e2e18ad"
|
||||
integrity sha512-QpMQUpEL+ZNcpEhjvYM/H6jqDx9nNcJqymA2kbkNthFS2I7ekL7ofEZ7+MoQAFTBuJers91K0FGCMpL7MwC9TQ==
|
||||
"@types/codemirror@^0.0.97":
|
||||
version "0.0.97"
|
||||
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.97.tgz#6f2d8266b7f1b34aacfe8c77221fafe324c3d081"
|
||||
integrity sha512-n5d7o9nWhC49DjfhsxANP7naWSeTzrjXASkUDQh7626sM4zK9XP2EVcHp1IcCf/IPV6c7ORzDUDF3Bkt231VKg==
|
||||
dependencies:
|
||||
"@types/tern" "*"
|
||||
|
||||
@ -2641,6 +2641,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440"
|
||||
integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==
|
||||
|
||||
"@types/marked@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-1.1.0.tgz#53509b5f127e0c05c19176fcf1d743a41e00ff19"
|
||||
integrity sha512-j8XXj6/l9kFvCwMyVqozznqpd/nk80krrW+QiIJN60Uu9gX5Pvn4/qPJ2YngQrR3QREPwmrE1f9/EWKVTFzoEw==
|
||||
|
||||
"@types/memoize-one@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/memoize-one/-/memoize-one-4.1.0.tgz#62119f26055b3193ae43ca1882c5b29b88b71ece"
|
||||
@ -8258,10 +8263,10 @@ map-visit@^1.0.0:
|
||||
dependencies:
|
||||
object-visit "^1.0.0"
|
||||
|
||||
marked@^0.6.1:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-0.6.2.tgz#c574be8b545a8b48641456ca1dbe0e37b6dccc1a"
|
||||
integrity sha512-LqxwVH3P/rqKX4EKGz7+c2G9r98WeM/SW34ybhgNGhUQNKtf1GmmSkJ6cDGJ/t6tiyae49qRkpyTw2B9HOrgUA==
|
||||
marked@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-1.1.1.tgz#e5d61b69842210d5df57b05856e0c91572703e6a"
|
||||
integrity sha512-mJzT8D2yPxoPh7h0UXkB+dBj4FykPJ2OIfxAWeIHrvoHDkFxukV/29QxoFQoPM6RLEwhIFdJpmKBlqVM3s2ZIw==
|
||||
|
||||
matchdep@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user