Compare commits

...

54 Commits

Author SHA1 Message Date
Joakim Sørensen
3dc4b1d775 Merge to master for 20200807.1 (#6566)
* Reorder to not break jinja templates (#6564)

* Bumped version to 20200807.1
2020-08-07 16:36:49 +02:00
Bram Kragten
e605ad5e46 Merge pull request #6562 from home-assistant/dev
Merge dev to master for 20200807.0
2020-08-07 14:20:30 +02:00
Ludeeus
0d4f43472b Bumped version to 20200807.0 2020-08-07 10:47:19 +00:00
Bram Kragten
b30e467685 Fix z-index light and thermostat more info button (#6561) 2020-08-07 11:43:20 +02:00
HomeAssistant Azure
a56c0b52d5 [ci skip] Translation update 2020-08-07 00:32:18 +00:00
Bram Kragten
c17ebfd279 Show small header on ingress panels when the sidebar is hidden (#6488) 2020-08-06 23:42:10 +02:00
Joakim Sørensen
5400b1da96 Fixes display issues with longer dates (#6540)
* Fixes display issues with longer dates

* review

* Remove size
2020-08-06 19:42:27 +02:00
Joakim Sørensen
69f4a618b2 Fix color and overlap in close dialog header (#6539)
* Fix color and overlap in close dialog header

* Use haStyleDialog instead
2020-08-06 17:51:41 +02:00
Joakim Sørensen
16b8b6698c Fixes display issues with the new darkmode (#6532)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-08-06 17:51:14 +02:00
Joakim Sørensen
b29a700d40 Only show stage bage if not stable (#6546) 2020-08-06 17:50:46 +02:00
Bram Kragten
bbb1468439 Mark card editor dirty on prefilled create (#6545) 2020-08-06 15:18:08 +02:00
Joakim Sørensen
72f9d6a8d3 Open more info on top (#6543) 2020-08-06 14:28:53 +02:00
Joakim Sørensen
3ec8da1f17 Fix theme loading on earlier versions (#6521) 2020-08-06 14:04:22 +02:00
Joakim Sørensen
dbea3848df Remove !important from h2 (#6541) 2020-08-06 14:02:49 +02:00
HomeAssistant Azure
33871435e1 [ci skip] Translation update 2020-08-06 00:32:16 +00:00
Bram Kragten
f1f22b43dc Merge pull request #6524 from home-assistant/dev
20200805.0
2020-08-05 13:11:08 +02:00
Ludeeus
2fb9a56e0b Bumped version to 20200805.0 2020-08-05 10:49:41 +00:00
Joakim Sørensen
14e8f66ed7 Fix allignment in integration badge during onboarding (#6523) 2020-08-05 12:20:32 +02:00
HomeAssistant Azure
e6f5072462 [ci skip] Translation update 2020-08-05 00:32:16 +00:00
Bram Kragten
a64f50fa72 Bump mwc to 0.18 (#6517) 2020-08-04 20:52:05 +02:00
Bram Kragten
bb5f6e88d0 Close entity registry dialog when navigation away (#6511) 2020-08-04 11:13:55 +02:00
Bram Kragten
6991403203 Fix location editor in onboarding (#6512) 2020-08-03 23:26:41 +02:00
Bram Kragten
410bd22f8a Punycode client id on auth page (#6513) 2020-08-03 22:44:20 +02:00
Charles Garwood
b81d823602 Add Z-Wave device info to OZW device pages (#6508)
* Add basic device info to devices page for OZW devices

* Remove unused HassEntity

* connection -> identifier

* async fetch

* Cleanup fetch call
2020-08-03 18:53:41 +02:00
Bram Kragten
bd5115f9aa Merge pull request #6510 from home-assistant/dev 2020-08-03 16:48:29 +02:00
Bram Kragten
7bcbed80d7 Bumped version to 20200803.0 2020-08-03 16:35:40 +02:00
Bram Kragten
8fb62ebf5f Fix gauge when safari is zoomed (#6492)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-08-03 16:32:23 +02:00
Bram Kragten
209dd9923f Allow to select a different dashboard when adding entities / moving cards (#6478) 2020-08-03 16:29:26 +02:00
Charles Garwood
c75207e391 Add identifiers to DeviceRegistryEntry (#6507) 2020-08-03 15:35:52 +02:00
HomeAssistant Azure
d957f36927 [ci skip] Translation update 2020-08-03 00:33:23 +00:00
Bram Kragten
9ac459b6d9 Update lint rules (#6490) 2020-08-03 02:11:28 +02:00
Bram Kragten
e08b2817ba Move view edit dialog to mwc (#6479) 2020-08-03 02:08:05 +02:00
Bram Kragten
4ca13c409b Introduce dark mode and primary color picker (#6430)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-08-03 02:07:12 +02:00
Bram Kragten
0d515e2303 Replace createValidEntityId with slugify (#6505) 2020-08-03 02:06:08 +02:00
Bram Kragten
a2153bc6aa Add UI for new script functions (#6491) 2020-08-02 20:56:26 +02:00
HomeAssistant Azure
ca171afe6f [ci skip] Translation update 2020-08-02 00:33:15 +00:00
HomeAssistant Azure
bf4e97bd48 [ci skip] Translation update 2020-08-01 00:33:31 +00:00
dependabot[bot]
8c59a12a03 Bump elliptic from 6.4.1 to 6.5.3 (#6502)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.4.1 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.4.1...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-31 20:54:52 +02:00
Bram Kragten
89569355be Don't add undefined values to extra data in device automations (#6499) 2020-07-31 19:07:11 +02:00
MeIchthys
3a41b3bdcf standardize config menu descriptions (#6495) 2020-07-31 11:27:38 +02:00
HomeAssistant Azure
12bd7037b3 [ci skip] Translation update 2020-07-31 00:33:23 +00:00
Bram Kragten
ca4f573be0 Add support for safe area insets (#6473) 2020-07-30 18:27:27 +02:00
Andrey Kupreychik
07fceeab5a Using entity_picture_local for entity card and badge (#6489)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-07-30 16:33:08 +02:00
HomeAssistant Azure
3aa376e912 [ci skip] Translation update 2020-07-30 00:34:00 +00:00
Michael Irigoyen
92d30a8896 Update Material Design Icons to v5.4.55 (#6485) 2020-07-29 16:19:45 +02:00
Bram Kragten
83876fb9da Nullish coalescing entity card (#6484)
Fixes #6483
2020-07-29 08:40:28 -05:00
HomeAssistant Azure
29bdf7877c [ci skip] Translation update 2020-07-29 00:32:28 +00:00
Bram Kragten
29199e2782 Fix scroll to top dev tools (#6455)
Fixes https://github.com/home-assistant/frontend/issues/6448
2020-07-28 11:16:47 +02:00
Bram Kragten
68e1378615 Fix automation/scripts dirty on start edit (#6474) 2020-07-28 11:12:58 +02:00
HomeAssistant Azure
cf7efb5bfc [ci skip] Translation update 2020-07-28 00:32:36 +00:00
Bram Kragten
e8254f9aae Merge pull request #6411 from home-assistant/dev 2020-07-16 18:38:27 +02:00
Bram Kragten
2e198af8c3 Merge pull request #6399 from home-assistant/dev 2020-07-15 20:19:11 +02:00
Bram Kragten
ec36d396d9 Merge pull request #6396 from home-assistant/dev 2020-07-15 16:23:49 +02:00
Bram Kragten
78914091b1 Merge pull request #6389 from home-assistant/dev 2020-07-14 23:53:51 +02:00
184 changed files with 4711 additions and 2522 deletions

View File

@@ -1,7 +1,7 @@
{
"extends": [
"plugin:@typescript-eslint/recommended",
"airbnb-typescript/base",
"plugin:@typescript-eslint/recommended",
"plugin:wc/recommended",
"plugin:lit/recommended",
"prettier",
@@ -45,16 +45,16 @@
"func-names": 0,
"prefer-arrow-callback": 0,
"no-underscore-dangle": 0,
"no-var": 0,
"strict": 0,
"prefer-spread": 0,
"no-plusplus": 0,
"no-bitwise": 0,
"no-bitwise": 2,
"comma-dangle": 0,
"vars-on-top": 0,
"no-continue": 0,
"no-param-reassign": 0,
"no-multi-assign": 0,
"no-console": 2,
"radix": 0,
"no-alert": 0,
"no-return-await": 0,

View File

@@ -147,6 +147,10 @@
"path": "M21.11,18.5C20.97,18.5 20.83,18.44 20.71,18.36C20.37,18.13 20.28,17.68 20.5,17.34C21.18,16.34 21.54,15.16 21.54,13.93C21.54,12.71 21.18,11.53 20.5,10.5C20.28,10.18 20.37,9.73 20.71,9.5C21.04,9.28 21.5,9.37 21.72,9.7C22.56,10.95 23,12.41 23,13.93C23,15.45 22.56,16.91 21.72,18.16C21.58,18.37 21.35,18.5 21.11,18.5M19,17.29C18.88,17.29 18.74,17.25 18.61,17.17C18.28,16.94 18.19,16.5 18.42,16.15C18.86,15.5 19.1,14.73 19.1,13.93C19.1,13.14 18.86,12.37 18.42,11.71C18.19,11.37 18.28,10.92 18.61,10.69C18.95,10.47 19.4,10.55 19.63,10.89C20.24,11.79 20.56,12.84 20.56,13.93C20.56,15 20.24,16.07 19.63,16.97C19.5,17.18 19.25,17.29 19,17.29M14.9,15.73C15.89,15.73 16.7,14.92 16.7,13.93C16.7,13.17 16.22,12.5 15.55,12.25C15.5,12.55 15.43,12.85 15.34,13.14C15.23,13.44 14.95,13.64 14.64,13.64C14.57,13.64 14.5,13.62 14.41,13.6C14.03,13.47 13.82,13.06 13.95,12.67C14.09,12.24 14.17,11.78 14.17,11.32C14.17,8.93 12.22,7 9.82,7C8.1,7 6.56,8 5.87,9.5C6.54,9.7 7.16,10.04 7.66,10.54C7.95,10.83 7.95,11.29 7.66,11.58C7.38,11.86 6.91,11.86 6.63,11.58C6.17,11.12 5.56,10.86 4.9,10.86C3.56,10.86 2.46,11.96 2.46,13.3C2.46,14.64 3.56,15.73 4.9,15.73H14.9M15.6,10.75C17.06,11.07 18.17,12.37 18.17,13.93C18.17,15.73 16.7,17.19 14.9,17.19H4.9C2.75,17.19 1,15.45 1,13.3C1,11.34 2.45,9.73 4.33,9.45C5.12,7.12 7.33,5.5 9.82,5.5C12.83,5.5 15.31,7.82 15.6,10.75Z",
"name": "mixcloud"
},
{
"path": "M5.68,3.96L11.41,11.65C11.55,11.84 11.55,12.1 11.41,12.29L5.65,20L5.5,20.18C4.76,21 3.47,21.07 2.64,20.31C1.85,19.59 1.79,18.37 2.43,17.5L6.56,11.97L2.46,6.47C1.83,5.62 1.88,4.39 2.67,3.67L2.82,3.54C3.73,2.87 5,3.05 5.68,3.96M18.32,3.96C19,3.05 20.27,2.87 21.18,3.54L21.33,3.67C22.12,4.39 22.17,5.61 21.54,6.47L17.44,11.97L21.57,17.5C22.21,18.36 22.15,19.59 21.36,20.31C20.53,21.07 19.24,21 18.5,20.18L18.35,20L12.59,12.29C12.45,12.1 12.45,11.84 12.59,11.65L18.32,3.96Z",
"name": "mixer"
},
{
"path": "M3.25,4.03L19.95,20.73L18.7,22L14.86,18.13C14.77,18.12 14.68,18.09 14.59,18.05C14.26,17.89 14.14,17.62 14.11,17.38L12.18,15.45C12.14,15.53 12.09,15.6 12.05,15.66C11.62,16.26 11.19,16.26 10.86,16.04C10.54,15.83 5.5,12 5.23,11.87C4.95,11.76 4.85,12.03 5.12,13.5C5.39,15 4.95,15.39 4.57,15.45C4.2,15.5 3.06,15.18 3,12.14C2.95,9.11 3.76,8.62 4.14,8.62C4.6,8.62 7.08,10.69 8.84,12.12L2,5.28L3.25,4.03M18.38,16.56C18.75,15.4 19.12,13.8 19.1,12.03V12C19.14,8.5 17.66,5.58 17.66,5.58C17.66,5.58 17.42,4.72 18.12,4.39C18.83,4.06 19.3,4.61 19.3,4.61C21.12,8.22 21,11.64 21,12C21,12.27 21.09,14.96 19.88,18.05L18.38,16.56M15.14,13.31C15.19,12.92 15.22,12.5 15.24,12.03V12C15.14,8.5 14.13,7.21 14.13,7.21C14.13,7.21 13.89,6.34 14.59,6C15.3,5.69 15.77,6.23 15.77,6.23C17.26,8.94 17.16,11.64 17.14,12C17.15,12.2 17.2,13.38 16.82,15L15.14,13.31M10.2,8.38C10.23,7.77 10.59,7.64 10.59,7.64C10.59,7.64 11.19,7.37 11.57,7.8C11.91,8.19 12.72,9.57 12.89,11.07L10.2,8.38Z",
"name": "nfc-off"

View File

@@ -22,6 +22,8 @@ class HcLovelace extends LitElement {
@property() public viewPath?: string | number;
public urlPath?: string | null;
protected render(): TemplateResult {
const index = this._viewIndex;
if (index === undefined) {
@@ -35,6 +37,7 @@ class HcLovelace extends LitElement {
const lovelace: Lovelace = {
config: this.lovelaceConfig,
editMode: false,
urlPath: this.urlPath!,
enableFullEditMode: () => undefined,
mode: "storage",
language: "en",

View File

@@ -87,6 +87,7 @@ export class HcMain extends HassElement {
.hass=${this.hass}
.lovelaceConfig=${this._lovelaceConfig}
.viewPath=${this._lovelacePath}
.urlPath=${this._urlPath}
@config-refresh=${this._generateLovelaceConfig}
></hc-lovelace>
`;

View File

@@ -242,19 +242,23 @@ class HassioAddonInfo extends LitElement {
`
: ""}
<div class="security">
<ha-label-badge
class=${classMap({
green: this.addon.stage === "stable",
yellow: this.addon.stage === "experimental",
red: this.addon.stage === "deprecated",
})}
@click=${this._showMoreInfo}
id="stage"
label="stage"
description=""
>
<ha-svg-icon .path=${STAGE_ICON[this.addon.stage]}></ha-svg-icon>
</ha-label-badge>
${this.addon.stage !== "stable"
? html` <ha-label-badge
class=${classMap({
yellow: this.addon.stage === "experimental",
red: this.addon.stage === "deprecated",
})}
@click=${this._showMoreInfo}
id="stage"
label="stage"
description=""
>
<ha-svg-icon
.path=${STAGE_ICON[this.addon.stage]}
></ha-svg-icon>
</ha-label-badge>`
: ""}
<ha-label-badge
class=${classMap({
green: [5, 6].includes(Number(this.addon.rating)),

View File

@@ -31,6 +31,10 @@ class HassioMarkdownDialog extends LitElement {
this._opened = true;
}
public closeDialog() {
this._opened = false;
}
protected render(): TemplateResult {
if (!this._opened) {
return html``;
@@ -38,7 +42,7 @@ class HassioMarkdownDialog extends LitElement {
return html`
<ha-dialog
open
@closing=${this._closeDialog}
@closed=${this.closeDialog}
.heading=${createCloseHeading(this.hass, this.title)}
>
<ha-markdown .content=${this.content || ""}></ha-markdown>
@@ -46,10 +50,6 @@ class HassioMarkdownDialog extends LitElement {
`;
}
private _closeDialog(): void {
this._opened = false;
}
static get styles(): CSSResult[] {
return [
haStyleDialog,

View File

@@ -1,92 +1,29 @@
import { PolymerElement } from "@polymer/polymer";
import {
customElement,
property,
internalProperty,
html,
PropertyValues,
customElement,
LitElement,
property,
} from "lit-element";
import "./hassio-router";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { HomeAssistant, Route } from "../../src/types";
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import { navigate } from "../../src/common/navigate";
import { fetchHassioAddonInfo } from "../../src/data/hassio/addon";
import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
HassioHassOSInfo,
HassioHostInfo,
} from "../../src/data/hassio/host";
import {
createHassioSession,
fetchHassioHomeAssistantInfo,
fetchHassioSupervisorInfo,
fetchHassioInfo,
HassioHomeAssistantInfo,
HassioInfo,
HassioPanelInfo,
HassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
import {
AlertDialogParams,
showAlertDialog,
} from "../../src/dialogs/generic/show-dialog-box";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import {
HassRouterPage,
RouterOptions,
} from "../../src/layouts/hass-router-page";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import "../../src/resources/ha-style";
import { HomeAssistant } from "../../src/types";
// Don't codesplit it, that way the dashboard always loads fast.
import "./hassio-panel";
import { atLeastVersion } from "../../src/common/config/version";
@customElement("hassio-main")
class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public panel!: HassioPanelInfo;
@property() public narrow!: boolean;
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
defaultPage: "dashboard",
initialLoad: () => this._fetchData(),
showLoading: true,
routes: {
dashboard: {
tag: "hassio-panel",
cache: true,
},
snapshots: "dashboard",
store: "dashboard",
system: "dashboard",
addon: {
tag: "hassio-addon-dashboard",
load: () =>
import(
/* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard"
),
},
ingress: {
tag: "hassio-ingress-view",
load: () =>
import(
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
),
},
},
};
@internalProperty() private _supervisorInfo: HassioSupervisorInfo;
@internalProperty() private _hostInfo: HassioHostInfo;
@internalProperty() private _hassioInfo?: HassioInfo;
@internalProperty() private _hassOsInfo?: HassioHassOSInfo;
@internalProperty() private _hassInfo: HassioHomeAssistantInfo;
@property() public route?: Route;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
@@ -94,23 +31,13 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
applyThemesOnElement(
this.parentElement,
this.hass.themes,
this.hass.selectedTheme || this.hass.themes.default_theme
(atLeastVersion(this.hass.config.version, 0, 114)
? this.hass.selectedTheme?.theme
: ((this.hass.selectedTheme as unknown) as string)) ||
this.hass.themes.default_theme,
this.hass.selectedTheme
);
this.style.setProperty(
"--app-header-background-color",
"var(--sidebar-background-color)"
);
this.style.setProperty(
"--app-header-text-color",
"var(--sidebar-text-color)"
);
this.style.setProperty(
"--app-header-border-bottom",
"1px solid var(--divider-color)"
);
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
// Paulus - March 17, 2019
// We went to a single hass-toggle-menu event in HA 0.90. However, the
// supervisor UI can also run under older versions of Home Assistant.
@@ -143,152 +70,18 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
});
});
makeDialogManager(this, document.body);
makeDialogManager(this, this.shadowRoot!);
}
protected updatePageEl(el) {
// the tabs page does its own routing so needs full route.
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
if ("setProperties" in el) {
// As long as we have Polymer pages
(el as PolymerElement).setProperties({
hass: this.hass,
narrow: this.narrow,
supervisorInfo: this._supervisorInfo,
hassioInfo: this._hassioInfo,
hostInfo: this._hostInfo,
hassInfo: this._hassInfo,
hassOsInfo: this._hassOsInfo,
route,
});
} else {
el.hass = this.hass;
el.narrow = this.narrow;
el.supervisorInfo = this._supervisorInfo;
el.hassioInfo = this._hassioInfo;
el.hostInfo = this._hostInfo;
el.hassInfo = this._hassInfo;
el.hassOsInfo = this._hassOsInfo;
el.route = route;
}
}
private async _fetchData() {
if (this.panel.config && this.panel.config.ingress) {
await this._redirectIngress(this.panel.config.ingress);
return;
}
const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = await Promise.all([
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
]);
this._supervisorInfo = supervisorInfo;
this._hassioInfo = hassioInfo;
this._hostInfo = hostInfo;
this._hassInfo = hassInfo;
if (this._hostInfo.features && this._hostInfo.features.includes("hassos")) {
this._hassOsInfo = await fetchHassioHassOsInfo(this.hass);
}
}
private async _redirectIngress(addonSlug: string) {
// When we trigger a navigation, we sleep to make sure we don't
// show the hassio dashboard before navigating away.
const awaitAlert = async (
alertParams: AlertDialogParams,
action: () => void
) => {
await new Promise((resolve) => {
alertParams.confirm = resolve;
showAlertDialog(this, alertParams);
});
action();
await new Promise((resolve) => setTimeout(resolve, 1000));
};
const createSessionPromise = createHassioSession(this.hass).then(
() => true,
() => false
);
let addon;
try {
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
} catch (err) {
await awaitAlert(
{
text: "Unable to fetch add-on info to start Ingress",
title: "Supervisor",
},
() => history.back()
);
return;
}
if (!addon.ingress_url) {
await awaitAlert(
{
text: "Add-on does not support Ingress",
title: addon.name,
},
() => history.back()
);
return;
}
if (addon.state !== "started") {
await awaitAlert(
{
text: "Add-on is not running. Please start it first",
title: addon.name,
},
() => navigate(this, `/hassio/addon/${addon.slug}/info`, true)
);
return;
}
if (!(await createSessionPromise)) {
await awaitAlert(
{
text: "Unable to create an Ingress session",
title: addon.name,
},
() => history.back()
);
return;
}
location.assign(addon.ingress_url);
// await a promise that doesn't resolve, so we show the loading screen
// while we load the next page.
await new Promise(() => undefined);
}
private _apiCalled(ev) {
if (!ev.detail.success) {
return;
}
let tries = 1;
const tryUpdate = () => {
this._fetchData().catch(() => {
tries += 1;
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
});
};
tryUpdate();
protected render() {
return html`
<hassio-router
.hass=${this.hass}
.route=${this.route}
.panel=${this.panel}
.narrow=${this.narrow}
></hassio-router>
`;
}
}

View File

@@ -4,6 +4,8 @@ import {
LitElement,
property,
TemplateResult,
css,
CSSResult,
} from "lit-element";
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host";
import {
@@ -33,6 +35,9 @@ class HassioPanel extends LitElement {
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult {
if (!this.supervisorInfo) {
return html``;
}
return html`
<hassio-panel-router
.route=${this.route}
@@ -46,6 +51,16 @@ class HassioPanel extends LitElement {
></hassio-panel-router>
`;
}
static get styles(): CSSResult {
return css`
:host {
--app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color);
--app-header-border-bottom: 1px solid var(--divider-color);
}
`;
}
}
declare global {

150
hassio/src/hassio-router.ts Normal file
View File

@@ -0,0 +1,150 @@
import {
customElement,
property,
internalProperty,
PropertyValues,
} from "lit-element";
import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
HassioHassOSInfo,
HassioHostInfo,
} from "../../src/data/hassio/host";
import {
fetchHassioHomeAssistantInfo,
fetchHassioSupervisorInfo,
fetchHassioInfo,
HassioHomeAssistantInfo,
HassioInfo,
HassioPanelInfo,
HassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
import {
HassRouterPage,
RouterOptions,
} from "../../src/layouts/hass-router-page";
import "../../src/resources/ha-style";
import { HomeAssistant } from "../../src/types";
// Don't codesplit it, that way the dashboard always loads fast.
import "./hassio-panel";
@customElement("hassio-router")
class HassioRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public panel!: HassioPanelInfo;
@property() public narrow!: boolean;
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
defaultPage: "dashboard",
initialLoad: () => this._fetchData(),
showLoading: true,
routes: {
dashboard: {
tag: "hassio-panel",
cache: true,
},
snapshots: "dashboard",
store: "dashboard",
system: "dashboard",
addon: {
tag: "hassio-addon-dashboard",
load: () =>
import(
/* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard"
),
},
ingress: {
tag: "hassio-ingress-view",
load: () =>
import(
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
),
},
},
};
@internalProperty() private _supervisorInfo: HassioSupervisorInfo;
@internalProperty() private _hostInfo: HassioHostInfo;
@internalProperty() private _hassioInfo?: HassioInfo;
@internalProperty() private _hassOsInfo?: HassioHassOSInfo;
@internalProperty() private _hassInfo: HassioHomeAssistantInfo;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
protected updatePageEl(el) {
// the tabs page does its own routing so needs full route.
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
el.hass = this.hass;
el.narrow = this.narrow;
el.supervisorInfo = this._supervisorInfo;
el.hassioInfo = this._hassioInfo;
el.hostInfo = this._hostInfo;
el.hassInfo = this._hassInfo;
el.hassOsInfo = this._hassOsInfo;
el.route = route;
if (el.localName === "hassio-ingress-view") {
el.ingressPanel = this.panel.config && this.panel.config.ingress;
}
}
private async _fetchData() {
if (this.panel.config && this.panel.config.ingress) {
this._redirectIngress(this.panel.config.ingress);
return;
}
const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = await Promise.all([
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
]);
this._supervisorInfo = supervisorInfo;
this._hassioInfo = hassioInfo;
this._hostInfo = hostInfo;
this._hassInfo = hassInfo;
if (this._hostInfo.features && this._hostInfo.features.includes("hassos")) {
this._hassOsInfo = await fetchHassioHassOsInfo(this.hass);
}
}
private _redirectIngress(addonSlug: string) {
this.route = { prefix: "/hassio", path: `/ingress/${addonSlug}` };
}
private _apiCalled(ev) {
if (!ev.detail.success) {
return;
}
let tries = 1;
const tryUpdate = () => {
this._fetchData().catch(() => {
tries += 1;
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
});
};
tryUpdate();
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-router": HassioRouter;
}
}

View File

@@ -17,6 +17,10 @@ import { createHassioSession } from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import { HomeAssistant, Route } from "../../../src/types";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { navigate } from "../../../src/common/navigate";
import { mdiMenu } from "@mdi/js";
import { fireEvent } from "../../../src/common/dom/fire_event";
@customElement("hassio-ingress-view")
class HassioIngressView extends LitElement {
@@ -24,22 +28,45 @@ class HassioIngressView extends LitElement {
@property() public route!: Route;
@property() public ingressPanel = false;
@internalProperty() private _addon?: HassioAddonDetails;
@property({ type: Boolean })
public narrow = false;
protected render(): TemplateResult {
if (!this._addon) {
return html` <hass-loading-screen></hass-loading-screen> `;
}
return html`
<hass-subpage .header=${this._addon.name} hassio>
<iframe src=${this._addon.ingress_url}></iframe>
</hass-subpage>
`;
const iframe = html`<iframe src=${this._addon.ingress_url!}></iframe>`;
if (!this.ingressPanel) {
return html`<hass-subpage
.header=${this._addon.name}
.narrow=${this.narrow}
>
${iframe}
</hass-subpage>`;
}
return html`${this.narrow || this.hass.dockedSidebar === "always_hidden"
? html`<div class="header">
<mwc-icon-button
aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
@click=${this._toggleMenu}
>
<ha-svg-icon path=${mdiMenu}></ha-svg-icon>
</mwc-icon-button>
<div class="main-title">${this._addon.name}</div>
</div>
${iframe}`
: iframe}`;
}
protected updated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
super.updated(changedProps);
if (!changedProps.has("route")) {
return;
@@ -56,27 +83,56 @@ class HassioIngressView extends LitElement {
}
private async _fetchData(addonSlug: string) {
const createSessionPromise = createHassioSession(this.hass).then(
() => true,
() => false
);
let addon;
try {
const [addon] = await Promise.all([
fetchHassioAddonInfo(this.hass, addonSlug).catch(() => {
throw new Error("Failed to fetch add-on info");
}),
createHassioSession(this.hass).catch(() => {
throw new Error("Failed to create an ingress session");
}),
]);
if (!addon.ingress) {
throw new Error("This add-on does not support ingress");
}
this._addon = addon;
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
} catch (err) {
// eslint-disable-next-line
console.error(err);
alert(err.message || "Unknown error starting ingress.");
await showAlertDialog(this, {
text: "Unable to fetch add-on info to start Ingress",
title: "Supervisor",
});
history.back();
return;
}
if (!addon.ingress_url) {
await showAlertDialog(this, {
text: "Add-on does not support Ingress",
title: addon.name,
});
history.back();
return;
}
if (addon.state !== "started") {
await showAlertDialog(this, {
text: "Add-on is not running. Please start it first",
title: addon.name,
});
navigate(this, `/hassio/addon/${addon.slug}/info`, true);
return;
}
if (!(await createSessionPromise)) {
await showAlertDialog(this, {
text: "Unable to create an Ingress session",
title: addon.name,
});
history.back();
return;
}
this._addon = addon;
}
private _toggleMenu(): void {
fireEvent(this, "hass-toggle-menu");
}
static get styles(): CSSResult {
@@ -87,6 +143,41 @@ class HassioIngressView extends LitElement {
height: 100%;
border: 0;
}
.header + iframe {
height: calc(100% - 40px);
}
.header {
display: flex;
align-items: center;
font-size: 16px;
height: 40px;
padding: 0 16px;
pointer-events: none;
background-color: var(--app-header-background-color);
font-weight: 400;
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
box-sizing: border-box;
--mdc-icon-size: 20px;
}
.main-title {
margin: 0 0 0 24px;
line-height: 20px;
flex-grow: 1;
}
mwc-icon-button {
pointer-events: auto;
}
hass-subpage {
--app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color);
--app-header-border-bottom: 1px solid var(--divider-color);
}
`;
}
}

View File

@@ -25,23 +25,24 @@
"@formatjs/intl-pluralrules": "^1.5.8",
"@fullcalendar/core": "^5.0.0-beta.2",
"@fullcalendar/daygrid": "^5.0.0-beta.2",
"@material/chips": "=8.0.0-canary.a78ceb112.0",
"@material/chips": "=8.0.0-canary.096a7a066.0",
"@material/circular-progress": "=8.0.0-canary.a78ceb112.0",
"@material/mwc-button": "^0.17.2",
"@material/mwc-checkbox": "^0.17.2",
"@material/mwc-dialog": "^0.17.2",
"@material/mwc-fab": "^0.17.2",
"@material/mwc-formfield": "^0.17.2",
"@material/mwc-icon-button": "^0.17.2",
"@material/mwc-list": "^0.17.2",
"@material/mwc-menu": "^0.17.2",
"@material/mwc-ripple": "^0.17.2",
"@material/mwc-switch": "^0.17.2",
"@material/mwc-tab": "^0.17.2",
"@material/mwc-tab-bar": "^0.17.2",
"@material/top-app-bar": "=8.0.0-canary.a78ceb112.0",
"@mdi/js": "5.3.45",
"@mdi/svg": "5.3.45",
"@material/mwc-button": "^0.18.0",
"@material/mwc-checkbox": "^0.18.0",
"@material/mwc-dialog": "^0.18.0",
"@material/mwc-fab": "^0.18.0",
"@material/mwc-formfield": "^0.18.0",
"@material/mwc-icon-button": "^0.18.0",
"@material/mwc-list": "^0.18.0",
"@material/mwc-menu": "^0.18.0",
"@material/mwc-radio": "^0.18.0",
"@material/mwc-ripple": "^0.18.0",
"@material/mwc-switch": "^0.18.0",
"@material/mwc-tab": "^0.18.0",
"@material/mwc-tab-bar": "^0.18.0",
"@material/top-app-bar": "=8.0.0-canary.096a7a066.0",
"@mdi/js": "5.4.55",
"@mdi/svg": "5.4.55",
"@polymer/app-layout": "^3.0.2",
"@polymer/app-route": "^3.0.2",
"@polymer/app-storage": "^3.0.2",
@@ -100,11 +101,12 @@
"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",
"proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1",
"regenerator-runtime": "^0.13.2",
"resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0",
@@ -136,11 +138,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",
@@ -210,7 +213,11 @@
"@webcomponents/webcomponentsjs": "^2.2.10",
"@polymer/polymer": "3.1.0",
"lit-html": "1.2.1",
"lit-element": "2.3.1"
"lit-element": "2.3.1",
"@material/animation": "8.0.0-canary.096a7a066.0",
"@material/base": "8.0.0-canary.096a7a066.0",
"@material/feature-targeting": "8.0.0-canary.096a7a066.0",
"@material/theme": "8.0.0-canary.096a7a066.0"
},
"main": "src/home-assistant.js",
"husky": {

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20200716.0",
version="20200807.1",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",

View File

@@ -16,6 +16,7 @@ import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { registerServiceWorker } from "../util/register-service-worker";
import "./ha-auth-flow";
import { extractSearchParamsObject } from "../common/url/search-params";
import punycode from "punycode";
import(/* webpackChunkName: "pick-auth-provider" */ "./ha-pick-auth-provider");
@@ -75,7 +76,7 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
${this.localize(
"ui.panel.page-authorize.authorizing_client",
"clientId",
this.clientId
this.clientId ? punycode.toASCII(this.clientId) : this.clientId
)}
</p>
${loggingInWith}

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

@@ -13,7 +13,7 @@ export const batteryIcon = (
return "hass:battery-unknown";
}
var icon = "hass:battery";
let icon = "hass:battery";
const batteryRound = Math.round(battery / 10) * 10;
if (battery_charging && battery > 10) {
icon += `-charging-${batteryRound}`;

View File

@@ -4,6 +4,6 @@ export const supportsFeature = (
stateObj: HassEntity,
feature: number
): boolean => {
// eslint-disable-next-line:no-bitwise
// eslint-disable-next-line no-bitwise
return (stateObj.attributes.supported_features! & feature) !== 0;
};

View File

@@ -2,11 +2,3 @@ const validEntityId = /^(\w+)\.(\w+)$/;
export const isValidEntityId = (entityId: string) =>
validEntityId.test(entityId);
export const createValidEntityId = (input: string) =>
input
.toLowerCase()
.replace(/\s|'|\./g, "_") // replace spaces, points and quotes with underscore
.replace(/\W/g, "") // remove not allowed chars
.replace(/_{2,}/g, "_") // replace multiple underscores with 1
.replace(/_$/, ""); // remove underscores at the end

View File

@@ -1,5 +1,5 @@
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
export const slugify = (value: string, delimiter = "-") => {
export const slugify = (value: string, delimiter = "_") => {
const a =
"àáäâãåăæąçćčđďèéěėëêęğǵḧìíïîįłḿǹńňñòóöôœøṕŕřßşśšșťțùúüûǘůűūųẃẍÿýźžż·/_,:;";
const b = `aaaaaaaaacccddeeeeeeegghiiiiilmnnnnooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}${delimiter}`;

View File

@@ -54,8 +54,8 @@ class HaCallServiceButton extends EventsMixin(PolymerElement) {
callService() {
this.progress = true;
// eslint-disable-next-line @typescript-eslint/no-this-alias
var el = this;
var eventData = {
const el = this;
const eventData = {
domain: this.domain,
service: this.service,
serviceData: this.serviceData,

View File

@@ -78,7 +78,7 @@ class HaProgressButton extends PolymerElement {
}
tempClass(className) {
var classList = this.$.container.classList;
const classList = this.$.container.classList;
classList.add(className);
setTimeout(() => {
classList.remove(className);

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

@@ -23,10 +23,10 @@ export const HaIronFocusablesHelper = {
* @return {!Array<!HTMLElement>}
*/
getTabbableNodes: function (node) {
var result = [];
const result = [];
// If there is at least one element with tabindex > 0, we need to sort
// the final array by tabindex.
var needsSortByTabIndex = this._collectTabbableNodes(node, result);
const needsSortByTabIndex = this._collectTabbableNodes(node, result);
if (needsSortByTabIndex) {
return IronFocusablesHelper._sortByTabIndex(result);
}
@@ -50,9 +50,9 @@ export const HaIronFocusablesHelper = {
) {
return false;
}
var element = /** @type {!HTMLElement} */ (node);
var tabIndex = IronFocusablesHelper._normalizedTabIndex(element);
var needsSort = tabIndex > 0;
const element = /** @type {!HTMLElement} */ (node);
const tabIndex = IronFocusablesHelper._normalizedTabIndex(element);
let needsSort = tabIndex > 0;
if (tabIndex >= 0) {
result.push(element);
}
@@ -70,7 +70,7 @@ export const HaIronFocusablesHelper = {
// <input id="B" slot="b" tabindex="1">
// </div>
// TODO(valdrin) support ShadowDOM v1 when upgrading to Polymer v2.0.
var children;
let children;
if (element.localName === "content" || element.localName === "slot") {
children = dom(element).getDistributedNodes();
} else {
@@ -80,7 +80,7 @@ export const HaIronFocusablesHelper = {
children = dom(element.shadowRoot || element.root || element).children;
// /////////////////////////
}
for (var i = 0; i < children.length; i++) {
for (let i = 0; i < children.length; i++) {
// Ensure method is always invoked to collect tabbable children.
needsSort = this._collectTabbableNodes(children[i], result) || needsSort;
}

View File

@@ -81,7 +81,7 @@ export class HaStateLabelBadge extends LitElement {
? ""
: this.image
? this.image
: state.attributes.entity_picture}"
: state.attributes.entity_picture_local || state.attributes.entity_picture}"
.label="${this._computeLabel(domain, state, this._timerTimeRemaining)}"
.description="${this.name ? this.name : computeStateName(state)}"
></ha-label-badge>

View File

@@ -73,10 +73,10 @@ export class StateBadge extends LitElement {
if (stateObj) {
// hide icon if we have entity picture
if (
(stateObj.attributes.entity_picture && !this.overrideIcon) ||
((stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture) && !this.overrideIcon) ||
this.overrideImage
) {
let imageUrl = this.overrideImage || stateObj.attributes.entity_picture;
let imageUrl = this.overrideImage || stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture;
if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl);
}

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

@@ -188,10 +188,10 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// origin is wheel center
// returns {x: X, y: Y} object
convertToCanvasCoordinates(clientX, clientY) {
var svgPoint = this.interactionLayer.createSVGPoint();
const svgPoint = this.interactionLayer.createSVGPoint();
svgPoint.x = clientX;
svgPoint.y = clientY;
var cc = svgPoint.matrixTransform(
const cc = svgPoint.matrixTransform(
this.interactionLayer.getScreenCTM().inverse()
);
return { x: cc.x, y: cc.y };
@@ -225,7 +225,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// Touch events
onTouchStart(ev) {
var touch = ev.changedTouches[0];
const touch = ev.changedTouches[0];
const cc = this.convertToCanvasCoordinates(touch.clientX, touch.clientY);
// return if we're not on the wheel
if (!this.isInWheel(cc.x, cc.y)) {
@@ -275,8 +275,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// Process user input to color
processUserSelect(ev) {
var canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
var hs = this.getColor(canvasXY.x, canvasXY.y);
const canvasXY = this.convertToCanvasCoordinates(ev.clientX, ev.clientY);
const hs = this.getColor(canvasXY.x, canvasXY.y);
this.onColorSelect(hs);
}
@@ -319,11 +319,11 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// set marker position to the given color
setMarkerOnColor(hs) {
var dist = hs.s * this.radius;
var theta = ((hs.h - 180) / 180) * Math.PI;
var markerdX = -dist * Math.cos(theta);
var markerdY = -dist * Math.sin(theta);
var translateString = `translate(${markerdX},${markerdY})`;
const dist = hs.s * this.radius;
const theta = ((hs.h - 180) / 180) * Math.PI;
const markerdX = -dist * Math.cos(theta);
const markerdY = -dist * Math.sin(theta);
const translateString = `translate(${markerdX},${markerdY})`;
this.marker.setAttribute("transform", translateString);
this.tooltip.setAttribute("transform", translateString);
}
@@ -358,8 +358,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
// get angle (degrees)
getAngle(dX, dY) {
var theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive
var angle = (theta / Math.PI) * 180 + 180; // degrees, clockwise from right
const theta = Math.atan2(-dY, -dX); // radians from the left edge, clockwise = positive
const angle = (theta / Math.PI) * 180 + 180; // degrees, clockwise from right
return angle;
}
@@ -378,9 +378,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
*/
getColor(x, y) {
var hue = this.getAngle(x, y); // degrees, clockwise from right
var relativeDistance = this.getDistance(x, y); // edge of radius = 1
var sat = Math.min(relativeDistance, 1); // Distance from center
const hue = this.getAngle(x, y); // degrees, clockwise from right
const relativeDistance = this.getDistance(x, y); // edge of radius = 1
const sat = Math.min(relativeDistance, 1); // Distance from center
return { h: hue, s: sat };
}
@@ -402,9 +402,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
if (this.saturationSegments === 1) {
hs.s = 1;
} else {
var segmentSize = 1 / this.saturationSegments;
var saturationStep = 1 / (this.saturationSegments - 1);
var calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep;
const segmentSize = 1 / this.saturationSegments;
const saturationStep = 1 / (this.saturationSegments - 1);
const calculatedSat = Math.floor(hs.s / segmentSize) * saturationStep;
hs.s = Math.min(calculatedSat, 1);
}
}
@@ -477,9 +477,9 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
hueSegments = hueSegments || 360; // reset 0 segments to 360
const angleStep = 360 / hueSegments;
const halfAngleStep = angleStep / 2; // center segments on color
for (var angle = 0; angle <= 360; angle += angleStep) {
var startAngle = (angle - halfAngleStep) * (Math.PI / 180);
var endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180);
for (let angle = 0; angle <= 360; angle += angleStep) {
const startAngle = (angle - halfAngleStep) * (Math.PI / 180);
const endAngle = (angle + halfAngleStep + 1) * (Math.PI / 180);
context.beginPath();
context.moveTo(cX, cY);
context.arc(
@@ -492,7 +492,7 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
);
context.closePath();
// gradient
var gradient = context.createRadialGradient(
const gradient = context.createRadialGradient(
cX,
cY,
0,
@@ -507,8 +507,8 @@ class HaColorPicker extends EventsMixin(PolymerElement) {
if (saturationSegments > 0) {
const ratioStep = 1 / saturationSegments;
let ratio = 0;
for (var stop = 1; stop < saturationSegments; stop += 1) {
var prevLighness = lightness;
for (let stop = 1; stop < saturationSegments; stop += 1) {
const prevLighness = lightness;
ratio = stop * ratioStep;
lightness = 100 - 50 * ratio;
gradient.addColorStop(

View File

@@ -95,7 +95,7 @@ class HaCoverControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) {
return true;
}
var assumedState = stateObj.attributes.assumed_state === true;
const assumedState = stateObj.attributes.assumed_state === true;
return (entityObj.isFullyOpen || entityObj.isOpening) && !assumedState;
}
@@ -103,7 +103,7 @@ class HaCoverControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) {
return true;
}
var assumedState = stateObj.attributes.assumed_state === true;
const assumedState = stateObj.attributes.assumed_state === true;
return (entityObj.isFullyClosed || entityObj.isClosing) && !assumedState;
}

View File

@@ -75,7 +75,7 @@ class HaCoverTiltControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) {
return true;
}
var assumedState = stateObj.attributes.assumed_state === true;
const assumedState = stateObj.attributes.assumed_state === true;
return entityObj.isFullyOpenTilt && !assumedState;
}
@@ -83,7 +83,7 @@ class HaCoverTiltControls extends PolymerElement {
if (stateObj.state === UNAVAILABLE) {
return true;
}
var assumedState = stateObj.attributes.assumed_state === true;
const assumedState = stateObj.attributes.assumed_state === true;
return entityObj.isFullyClosedTilt && !assumedState;
}

View File

@@ -163,13 +163,6 @@ export class HaDateRangePicker extends LitElement {
border-right: 1px solid var(--divider-color);
}
@media only screen and (max-width: 800px) {
.date-range-ranges {
border-right: none;
border-bottom: 1px solid var(--divider-color);
}
}
.date-range-footer {
display: flex;
justify-content: flex-end;
@@ -179,12 +172,30 @@ export class HaDateRangePicker extends LitElement {
paper-input {
display: inline-block;
max-width: 200px;
max-width: 250px;
min-width: 200px;
}
paper-input:last-child {
margin-left: 8px;
}
@media only screen and (max-width: 800px) {
.date-range-ranges {
border-right: none;
border-bottom: 1px solid var(--divider-color);
}
}
@media only screen and (max-width: 500px) {
paper-input {
min-width: inherit;
}
ha-svg-icon {
display: none;
}
}
`;
}
}

View File

@@ -34,10 +34,12 @@ 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 {
justify-content: var(--justify-action-buttons, flex-end);
padding-bottom: max(env(safe-area-inset-bottom), 8px);
}
.mdc-dialog__container {
align-items: var(--vertial-align-dialog, center);
@@ -50,6 +52,12 @@ export class HaDialog extends MwcDialog {
position: var(--dialog-content-position, relative);
padding: var(--dialog-content-padding, 20px 24px);
}
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
padding-bottom: max(
var(--dialog-content-padding, 20px),
env(safe-area-inset-bottom)
);
}
.mdc-dialog .mdc-dialog__surface {
position: var(--dialog-surface-position, relative);
min-height: var(--mdc-dialog-min-height, auto);

View File

@@ -100,7 +100,7 @@ export interface HaFormTimeData {
}
export interface HaFormElement extends LitElement {
schema: HaFormSchema;
schema: HaFormSchema | HaFormSchema[];
data?: HaFormDataContainer | HaFormData;
label?: string;
suffix?: string;
@@ -110,7 +110,7 @@ export interface HaFormElement extends LitElement {
export class HaForm extends LitElement implements HaFormElement {
@property() public data!: HaFormDataContainer | HaFormData;
@property() public schema!: HaFormSchema;
@property() public schema!: HaFormSchema | HaFormSchema[];
@property() public error;
@@ -190,7 +190,7 @@ export class HaForm extends LitElement implements HaFormElement {
: "";
}
private _computeError(error, schema: HaFormSchema) {
private _computeError(error, schema: HaFormSchema | HaFormSchema[]) {
return this.computeError ? this.computeError(error, schema) : error;
}
@@ -203,7 +203,7 @@ export class HaForm extends LitElement implements HaFormElement {
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema;
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
const data = this.data as HaFormDataContainer;
data[schema.name] = ev.detail.value;
fireEvent(this, "value-changed", {

View File

@@ -9,6 +9,7 @@ import {
} from "lit-element";
import { styleMap } from "lit-html/directives/style-map";
import { afterNextRender } from "../common/util/render-status";
import { ifDefined } from "lit-html/directives/if-defined";
const getAngle = (value: number, min: number, max: number) => {
const percentage = getValueInPercentage(normalize(value, min, max), min, max);
@@ -27,6 +28,9 @@ const getValueInPercentage = (value: number, min: number, max: number) => {
return (100 * newVal) / newMax;
};
// Workaround for https://github.com/home-assistant/frontend/issues/6467
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
@customElement("ha-gauge")
export class Gauge extends LitElement {
@property({ type: Number }) public min = 0;
@@ -69,9 +73,28 @@ export class Gauge extends LitElement {
></path>
<path
class="value"
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
d="M 90 50.001 A 40 40 0 0 1 10 50"
></path>
style=${ifDefined(
!isSafari
? styleMap({ transform: `rotate(${this._angle}deg)` })
: undefined
)}
transform=${ifDefined(
isSafari ? `rotate(${this._angle} 50 50)` : undefined
)}
>
${
isSafari
? svg`<animateTransform
attributeName="transform"
type="rotate"
from="0 50 50"
to="${this._angle} 50 50"
dur="1s"
/>`
: ""
}
</path>
</svg>
<svg class="text">
<text class="value-text">
@@ -106,8 +129,8 @@ export class Gauge extends LitElement {
fill: none;
stroke-width: 15;
stroke: var(--gauge-color);
transition: all 1000ms ease 0s;
transform-origin: 50% 100%;
transition: all 1s ease 0s;
}
.gauge {
display: block;

View File

@@ -193,6 +193,7 @@ const mdiRemovedIcons = new Set([
"medium",
"meetup",
"mixcloud",
"mixer",
"nfc-off",
"npm-variant",
"npm-variant-outline",

View File

@@ -23,7 +23,6 @@ class HaMarkdownElement extends UpdatingElement {
{
breaks: this.breaks,
gfm: true,
tables: true,
},
{
allowSvg: this.allowSvg,

View File

@@ -57,6 +57,10 @@ class HaMarkdown extends LitElement {
background-color: var(--markdown-code-background-color, none);
border-radius: 3px;
}
ha-markdown-element svg {
background-color: var(--markdown-svg-background-color, none);
color: var(--markdown-svg-color, none);
}
ha-markdown-element code {
font-size: 85%;
padding: 0.2em 0.4em;
@@ -70,8 +74,8 @@ class HaMarkdown extends LitElement {
line-height: 1.45;
}
ha-markdown-element h2 {
font-size: 1.5em !important;
font-weight: bold !important;
font-size: 1.5em;
font-weight: bold;
}
`;
}

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

@@ -97,6 +97,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
</h3>
<a
href=${`/config/integrations#config_entry=${relatedConfigEntryId}`}
@click=${this._navigateAwayClose}
>
${this.hass.localize(`component.${entry.domain}.title`)}:
${entry.title}
@@ -116,7 +117,10 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
<h3>
${this.hass.localize("ui.components.related-items.device")}:
</h3>
<a href="/config/devices/device/${relatedDeviceId}">
<a
href="/config/devices/device/${relatedDeviceId}"
@click=${this._navigateAwayClose}
>
${device.name_by_user || device.name}
</a>
`;
@@ -134,7 +138,10 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
<h3>
${this.hass.localize("ui.components.related-items.area")}:
</h3>
<a href="/config/areas/area/${relatedAreaId}">
<a
href="/config/areas/area/${relatedAreaId}"
@click=${this._navigateAwayClose}
>
${area.name}
</a>
`;
@@ -278,6 +285,12 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
`;
}
private async _navigateAwayClose() {
// allow new page to open before closing dialog
await new Promise((resolve) => setTimeout(resolve, 0));
fireEvent(this, "close-dialog");
}
private async _findRelated() {
this._related = await findRelated(this.hass, this.itemType, this.itemId);
await this.updateComplete;

View File

@@ -16,9 +16,9 @@ class HaServiceDescription extends PolymerElement {
}
_getDescription(hass, domain, service) {
var domainServices = hass.services[domain];
const domainServices = hass.services[domain];
if (!domainServices) return "";
var serviceObject = domainServices[service];
const serviceObject = domainServices[service];
if (!serviceObject) return "";
return serviceObject.description;
}

View File

@@ -126,7 +126,7 @@ class HaSidebar extends LitElement {
// property used only in css
// @ts-ignore
@property({ type: Boolean, reflect: true }) private _rtl = false;
@property({ type: Boolean, reflect: true }) public rtl = false;
private _mouseLeaveTimeout?: number;
@@ -312,6 +312,7 @@ class HaSidebar extends LitElement {
hass.panelUrl !== oldHass.panelUrl ||
hass.user !== oldHass.user ||
hass.localize !== oldHass.localize ||
hass.language !== oldHass.language ||
hass.states !== oldHass.states ||
hass.defaultPanel !== oldHass.defaultPanel
);
@@ -339,12 +340,14 @@ class HaSidebar extends LitElement {
return;
}
this._rtl = computeRTL(this.hass);
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
this.rtl = computeRTL(this.hass);
}
if (!SUPPORT_SCROLL_IF_NEEDED) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.panelUrl !== this.hass.panelUrl) {
const selectedEl = this.shadowRoot!.querySelector(".iron-selected");
if (selectedEl) {
@@ -496,9 +499,12 @@ class HaSidebar extends LitElement {
width: 64px;
}
:host([expanded]) {
width: 256px;
width: calc(256px + env(safe-area-inset-left));
}
:host([rtl]) {
border-right: 0;
border-left: 1px solid var(--divider-color);
}
.menu {
box-sizing: border-box;
height: 65px;
@@ -512,18 +518,25 @@ class HaSidebar extends LitElement {
background-color: var(--primary-background-color);
font-size: 20px;
align-items: center;
padding-left: calc(8.5px + env(safe-area-inset-left));
}
:host([rtl]) .menu {
padding-left: 8.5px;
padding-right: calc(8.5px + env(safe-area-inset-right));
}
:host([expanded]) .menu {
width: 256px;
width: calc(256px + env(safe-area-inset-left));
}
:host([rtl][expanded]) .menu {
width: calc(256px + env(safe-area-inset-right));
}
.menu mwc-icon-button {
color: var(--sidebar-icon-color);
}
:host([expanded]) .menu mwc-icon-button {
margin-right: 23px;
}
:host([expanded][_rtl]) .menu mwc-icon-button {
:host([expanded][rtl]) .menu mwc-icon-button {
margin-right: 0px;
margin-left: 23px;
}
@@ -551,12 +564,18 @@ class HaSidebar extends LitElement {
display: flex;
flex-direction: column;
box-sizing: border-box;
height: calc(100% - 196px);
height: calc(100% - 196px - env(safe-area-inset-bottom));
overflow-y: auto;
overflow-x: hidden;
scrollbar-color: var(--scrollbar-thumb-color) transparent;
scrollbar-width: thin;
background: none;
margin-left: env(safe-area-inset-left);
}
:host([rtl]) paper-listbox {
margin-left: initial;
margin-right: env(safe-area-inset-right);
}
a {
@@ -580,7 +599,7 @@ class HaSidebar extends LitElement {
:host([expanded]) paper-icon-item {
width: 240px;
}
:host([_rtl]) paper-icon-item {
:host([rtl]) paper-icon-item {
padding-left: auto;
padding-right: 12px;
}
@@ -656,6 +675,11 @@ class HaSidebar extends LitElement {
}
.notifications-container {
display: flex;
margin-left: env(safe-area-inset-left);
}
:host([rtl]) .notifications-container {
margin-left: initial;
margin-right: env(safe-area-inset-right);
}
.notifications {
cursor: pointer;
@@ -664,18 +688,23 @@ class HaSidebar extends LitElement {
flex: 1;
}
.profile {
margin-left: env(safe-area-inset-left);
}
:host([rtl]) .profile {
margin-left: initial;
margin-right: env(safe-area-inset-right);
}
.profile paper-icon-item {
padding-left: 4px;
}
:host([_rtl]) .profile paper-icon-item {
:host([rtl]) .profile paper-icon-item {
padding-left: auto;
padding-right: 4px;
}
.profile .item-text {
margin-left: 8px;
}
:host([_rtl]) .profile .item-text {
:host([rtl]) .profile .item-text {
margin-right: 8px;
}
@@ -688,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;
@@ -735,7 +764,7 @@ class HaSidebar extends LitElement {
font-weight: 500;
}
:host([_rtl]) .menu mwc-icon-button {
:host([rtl]) .menu mwc-icon-button {
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
}

View File

@@ -50,6 +50,8 @@ export class HaYamlEditor extends LitElement {
try {
this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
alert(`There was an error converting to YAML: ${err}`);
}
afterNextRender(() => {

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

@@ -322,9 +322,9 @@ export class PaperTimeInput extends PolymerElement {
* @return {boolean}
*/
validate() {
var valid = true;
let valid = true;
// Validate hour & min fields
if (!this.$.hour.validate() | !this.$.min.validate()) {
if (!this.$.hour.validate() || !this.$.min.validate()) {
valid = false;
}
// Validate second field

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

@@ -1,5 +1,6 @@
import { computeStateName } from "../common/entity/compute_state_name";
import { HomeAssistant } from "../types";
import { HaFormSchema } from "../components/ha-form/ha-form";
export interface DeviceAutomation {
device_id: string;
@@ -20,6 +21,10 @@ export interface DeviceTrigger extends DeviceAutomation {
platform: "device";
}
export interface DeviceCapabilities {
extra_fields: HaFormSchema[];
}
export const fetchDeviceActions = (hass: HomeAssistant, deviceId: string) =>
hass.callWS<DeviceAction[]>({
type: "device_automation/action/list",
@@ -42,7 +47,7 @@ export const fetchDeviceActionCapabilities = (
hass: HomeAssistant,
action: DeviceAction
) =>
hass.callWS<DeviceAction[]>({
hass.callWS<DeviceCapabilities>({
type: "device_automation/action/capabilities",
action,
});
@@ -51,7 +56,7 @@ export const fetchDeviceConditionCapabilities = (
hass: HomeAssistant,
condition: DeviceCondition
) =>
hass.callWS<DeviceCondition[]>({
hass.callWS<DeviceCapabilities>({
type: "device_automation/condition/capabilities",
condition,
});
@@ -60,7 +65,7 @@ export const fetchDeviceTriggerCapabilities = (
hass: HomeAssistant,
trigger: DeviceTrigger
) =>
hass.callWS<DeviceTrigger[]>({
hass.callWS<DeviceCapabilities>({
type: "device_automation/trigger/capabilities",
trigger,
});

View File

@@ -8,6 +8,7 @@ export interface DeviceRegistryEntry {
id: string;
config_entries: string[];
connections: Array<[string, string]>;
identifiers: Array<[string, string]>;
manufacturer: string;
model?: string;
name?: string;

21
src/data/ozw.ts Normal file
View File

@@ -0,0 +1,21 @@
import { HomeAssistant } from "../types";
export interface OZWDevice {
node_id: number;
node_query_stage: string;
is_awake: boolean;
is_failed: boolean;
is_zwave_plus: boolean;
ozw_instance: number;
}
export const fetchOZWNodeStatus = (
hass: HomeAssistant,
ozw_instance: string,
node_id: string
): Promise<OZWDevice> =>
hass.callWS({
type: "ozw/node_status",
ozw_instance: ozw_instance,
node_id: node_id,
});

View File

@@ -58,6 +58,31 @@ export interface WaitAction {
timeout?: number;
}
export interface RepeatAction {
repeat: CountRepeat | WhileRepeat | UntilRepeat;
}
interface BaseRepeat {
sequence: Action[];
}
export interface CountRepeat extends BaseRepeat {
count: number;
}
export interface WhileRepeat extends BaseRepeat {
while: Condition[];
}
export interface UntilRepeat extends BaseRepeat {
until: Condition[];
}
export interface ChooseAction {
choose: [{ conditions: Condition[]; sequence: Action[] }];
default?: Action[];
}
export type Action =
| EventAction
| DeviceAction
@@ -65,7 +90,9 @@ export type Action =
| Condition
| DelayAction
| SceneAction
| WaitAction;
| WaitAction
| RepeatAction
| ChooseAction;
export const triggerScript = (
hass: HomeAssistant,

View File

@@ -165,7 +165,6 @@ class DialogConfigEntrySystemOptions extends LitElement {
haStyleDialog,
css`
ha-paper-dialog {
min-width: 400px;
max-width: 500px;
}
.init-spinner {

View File

@@ -123,12 +123,12 @@ class MoreInfoConfigurator extends PolymerElement {
}
fieldChanged(ev) {
var el = ev.target;
const el = ev.target;
this.fieldInput[el.name] = el.value;
}
submitClicked() {
var data = {
const data = {
configure_id: this.stateObj.attributes.configure_id,
fields: this.fieldInput,
};

View File

@@ -97,7 +97,7 @@ class MoreInfoCover extends LocalizeMixin(PolymerElement) {
}
computeClassNames(stateObj) {
var classes = [
const classes = [
attributeClassNames(stateObj, [
"current_position",
"current_tilt_position",

View File

@@ -140,8 +140,8 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
speedChanged(ev) {
var oldVal = this.stateObj.attributes.speed;
var newVal = ev.detail.value;
const oldVal = this.stateObj.attributes.speed;
const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return;
@@ -152,8 +152,8 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
oscillationToggleChanged(ev) {
var oldVal = this.stateObj.attributes.oscillating;
var newVal = ev.target.checked;
const oldVal = this.stateObj.attributes.oscillating;
const newVal = ev.target.checked;
if (oldVal === newVal) return;

View File

@@ -53,11 +53,11 @@ class MoreInfoGroup extends PolymerElement {
}
computeStates(stateObj, hass) {
var states = [];
var entIds = stateObj.attributes.entity_id || [];
const states = [];
const entIds = stateObj.attributes.entity_id || [];
for (var i = 0; i < entIds.length; i++) {
var state = hass.states[entIds[i]];
for (let i = 0; i < entIds.length; i++) {
const state = hass.states[entIds[i]];
if (state) {
states.push(state);

View File

@@ -78,14 +78,14 @@ class DatetimeInput extends PolymerElement {
if (stateObj.state === "unknown") {
return "";
}
var monthFiller;
let monthFiller;
if (stateObj.attributes.month < 10) {
monthFiller = "0";
} else {
monthFiller = "";
}
var dayFiller;
let dayFiller;
if (stateObj.attributes.day < 10) {
dayFiller = "0";
} else {
@@ -119,15 +119,18 @@ class DatetimeInput extends PolymerElement {
};
if (this.stateObj.attributes.has_time) {
changed |=
changed =
changed ||
parseInt(this.selectedMinute) !== this.stateObj.attributes.minute;
changed |= parseInt(this.selectedHour) !== this.stateObj.attributes.hour;
changed =
changed ||
parseInt(this.selectedHour) !== this.stateObj.attributes.hour;
if (this.selectedMinute < 10) {
minuteFiller = "0";
} else {
minuteFiller = "";
}
var timeStr =
const timeStr =
this.selectedHour + ":" + minuteFiller + this.selectedMinute;
serviceData.time = timeStr;
}
@@ -144,7 +147,7 @@ class DatetimeInput extends PolymerElement {
this.stateObj.attributes.day
);
changed |= dateValState !== dateValInput;
changed = changed || dateValState !== dateValInput;
serviceData.date = this.selectedDate;
}

View File

@@ -290,8 +290,8 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
effectChanged(ev) {
var oldVal = this.stateObj.attributes.effect;
var newVal = ev.detail.value;
const oldVal = this.stateObj.attributes.effect;
const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return;
@@ -302,7 +302,7 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
brightnessSliderChanged(ev) {
var bri = parseInt(ev.target.value, 10);
const bri = parseInt(ev.target.value, 10);
if (isNaN(bri)) return;
@@ -313,7 +313,7 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
ctSliderChanged(ev) {
var ct = parseInt(ev.target.value, 10);
const ct = parseInt(ev.target.value, 10);
if (isNaN(ct)) return;
@@ -324,7 +324,7 @@ class MoreInfoLight extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
wvSliderChanged(ev) {
var wv = parseInt(ev.target.value, 10);
const wv = parseInt(ev.target.value, 10);
if (isNaN(wv)) return;

View File

@@ -329,8 +329,8 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
handleSourceChanged(ev) {
if (!this.playerObj) return;
var oldVal = this.playerObj.source;
var newVal = ev.detail.value;
const oldVal = this.playerObj.source;
const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return;
@@ -340,8 +340,8 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
handleSoundModeChanged(ev) {
if (!this.playerObj) return;
var oldVal = this.playerObj.soundMode;
var newVal = ev.detail.value;
const oldVal = this.playerObj.soundMode;
const newVal = ev.detail.value;
if (!newVal || oldVal === newVal) return;

View File

@@ -193,7 +193,7 @@ class MoreInfoWaterHeater extends LocalizeMixin(EventsMixin(PolymerElement)) {
4: "has-away_mode",
};
var classes = [featureClassNames(stateObj, _featureClassNames)];
const classes = [featureClassNames(stateObj, _featureClassNames)];
classes.push("more-info-water_heater");

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

@@ -29,9 +29,17 @@ export class HuiNotificationDrawer extends EventsMixin(
width: calc(100% - 32px);
}
div[main-title] {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
.notifications {
overflow-y: auto;
padding-top: 16px;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
height: calc(100% - 65px);
box-sizing: border-box;
background-color: var(--primary-background-color);

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

@@ -84,7 +84,7 @@ function initPushNotifications() {
}
self.addEventListener("push", function (event) {
var data;
let data;
if (event.data) {
data = event.data.json();
if (data.dismiss) {
@@ -113,8 +113,6 @@ function initPushNotifications() {
});
self.addEventListener("notificationclick", function (event) {
var url;
notificationEventCallback("clicked", event);
event.notification.close();
@@ -127,7 +125,7 @@ function initPushNotifications() {
return;
}
url = event.notification.data.url;
const url = event.notification.data.url;
if (!url) return;
@@ -137,8 +135,8 @@ function initPushNotifications() {
type: "window",
})
.then(function (windowClients) {
var i;
var client;
let i;
let client;
for (i = 0; i < windowClients.length; i++) {
client = windowClients[i];
if (client.url === url && "focus" in client) {

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

@@ -1,4 +1,4 @@
<meta name='viewport' content='width=device-width, user-scalable=no'>
<meta name='viewport' content='width=device-width, user-scalable=no, viewport-fit=cover'>
<style>
body {
font-family: Roboto, sans-serif;
@@ -7,6 +7,6 @@
font-weight: 400;
margin: 0;
padding: 0;
height: 100vh;
height: 100%;
}
</style>

View File

@@ -5,14 +5,16 @@
<link rel="preload" href="<%= latestAppJS %>" as="script" crossorigin="use-credentials" />
<%= renderTemplate('_header') %>
<title>Home Assistant</title>
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/static/icons/favicon-apple-180x180.png"
/>
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#03a9f4" />
<meta name="apple-itunes-app" content="app-id=1099568401" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Home Assistant">
<meta
name="msapplication-square70x70logo"
content="/static/icons/tile-win-70x70.png"
@@ -33,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;
@@ -41,7 +44,15 @@
background-color: #THEMEC;
}
html {
background-color: var(--primary-background-color, #fafafa);
background-color: var(--primary-background-color);
}
@media (prefers-color-scheme: dark) {
html {
background-color: #111111;
}
#ha-init-skeleton::before {
background-color: #1c1c1c;
}
}
</style>
</head>

View File

@@ -77,6 +77,8 @@ class HaAppLayout extends customElements.get("app-header-layout") {
/* Using 'transform' will cause 'position: fixed' elements to behave like
'position: absolute' relative to this element. */
transform: translate(0);
margin-left: env(safe-area-inset-left);
margin-right: env(safe-area-inset-right);
}
@media print {

View File

@@ -60,6 +60,7 @@ class HaInitPage extends LitElement {
}
p {
max-width: 350px;
color: var(--text-primary-color);
}
`;
}

View File

@@ -60,6 +60,11 @@ class HassSubpage extends LitElement {
background-color: var(--primary-background-color);
}
:host([narrow]) {
width: 100%;
position: fixed;
}
.toolbar {
display: flex;
align-items: center;

View File

@@ -26,9 +26,9 @@ import { computeRTLDirection } from "../common/util/compute_rtl";
export class HaTabsSubpageDataTable extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public isWide!: boolean;
@property({ type: Boolean }) public isWide = false;
@property({ type: Boolean, reflect: true }) public narrow!: boolean;
@property({ type: Boolean, reflect: true }) public narrow = false;
/**
* Object with the columns.
@@ -110,6 +110,7 @@ export class HaTabsSubpageDataTable extends LitElement {
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.backPath=${this.backPath}
.backCallback=${this.backCallback}
.route=${this.route}
@@ -168,38 +169,37 @@ export class HaTabsSubpageDataTable extends LitElement {
? html`
<div slot="header">
<slot name="header">
<slot name="header">
<div class="table-header">
<search-input
.filter=${this.filter}
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
.label=${this.hass.localize(
"ui.components.data-table.search"
)}
>
</search-input>
${this.activeFilters
? html`<div class="active-filters">
${this.hass.localize(
"ui.panel.config.filtering.filtering_by"
)}
${this.activeFilters.join(", ")}
<mwc-button @click=${this._clearFilter}
>${this.hass.localize(
"ui.panel.config.filtering.clear"
)}</mwc-button
>
</div>`
: ""}
</div></slot
></slot
>
<div class="table-header">
<search-input
.filter=${this.filter}
no-label-float
no-underline
@value-changed=${this._handleSearchChange}
.label=${this.hass.localize(
"ui.components.data-table.search"
)}
>
</search-input>
${this.activeFilters
? html`<div class="active-filters">
${this.hass.localize(
"ui.panel.config.filtering.filtering_by"
)}
${this.activeFilters.join(", ")}
<mwc-button @click=${this._clearFilter}
>${this.hass.localize(
"ui.panel.config.filtering.clear"
)}</mwc-button
>
</div>`
: ""}
</div>
</slot>
</div>
`
: html` <div slot="header"></div> `}
</ha-data-table>
<div slot="fab"><slot name="fab"></slot></div>
</hass-tabs-subpage>
`;
}

View File

@@ -22,6 +22,7 @@ import "../components/ha-svg-icon";
import "../components/ha-icon";
import "../components/ha-tab";
import { restoreScroll } from "../common/decorators/restore-scroll";
import { computeRTL } from "../common/util/compute_rtl";
export interface PageNavigation {
path: string;
@@ -53,6 +54,11 @@ class HassTabsSubpage extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean, reflect: true, attribute: "is-wide" })
public isWide = false;
@property({ type: Boolean, reflect: true }) public rtl = false;
@internalProperty() private _activeTab?: PageNavigation;
// @ts-ignore
@@ -107,6 +113,14 @@ class HassTabsSubpage extends LitElement {
`${this.route.prefix}${this.route.path}`.includes(tab.path)
);
}
if (changedProperties.has("hass")) {
const oldHass = changedProperties.get("hass") as
| HomeAssistant
| undefined;
if (!oldHass || oldHass.language !== this.hass.language) {
this.rtl = computeRTL(this.hass);
}
}
}
protected render(): TemplateResult {
@@ -152,6 +166,7 @@ class HassTabsSubpage extends LitElement {
<div class="content" @scroll=${this._saveScrollPos}>
<slot></slot>
</div>
<div id="fab"><slot name="fab"></slot></div>
`;
}
@@ -184,6 +199,11 @@ class HassTabsSubpage extends LitElement {
background-color: var(--primary-background-color);
}
:host([narrow]) {
width: 100%;
position: fixed;
}
ha-menu-button {
margin-right: 24px;
}
@@ -215,9 +235,10 @@ class HassTabsSubpage extends LitElement {
background-color: var(--sidebar-background-color);
border-top: 1px solid var(--divider-color);
justify-content: space-between;
z-index: 1;
z-index: 2;
font-size: 12px;
width: 100%;
padding-bottom: env(safe-area-inset-bottom);
}
#tabbar:not(.bottom-bar) {
@@ -247,7 +268,11 @@ class HassTabsSubpage extends LitElement {
.content {
position: relative;
width: 100%;
width: calc(
100% - env(safe-area-inset-left) - env(safe-area-inset-right)
);
margin-left: env(safe-area-inset-left);
margin-right: env(safe-area-inset-right);
height: calc(100% - 65px);
overflow-y: auto;
overflow: auto;
@@ -256,6 +281,30 @@ class HassTabsSubpage extends LitElement {
:host([narrow]) .content {
height: calc(100% - 128px);
height: calc(100% - 128px - env(safe-area-inset-bottom));
}
#fab {
position: fixed;
right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
:host([narrow]) #fab {
bottom: calc(84px + env(safe-area-inset-bottom));
}
#fab[is-wide] {
bottom: 24px;
right: 24px;
}
:host([rtl]) #fab {
right: auto;
left: calc(16px + env(safe-area-inset-left));
}
:host([rtl][is-wide]) #fab {
bottom: 24px;
left: 24px;
right: auto;
}
`;
}

View File

@@ -152,16 +152,13 @@ class HomeAssistantMain extends LitElement {
--app-drawer-width: 64px;
}
:host([expanded]) {
--app-drawer-width: 256px;
--app-drawer-width: calc(256px + env(safe-area-inset-left));
}
partial-panel-resolver,
ha-sidebar {
/* allow a light tap highlight on the actual interface elements */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
}
partial-panel-resolver {
height: 100%;
}
`;
}
}

View File

@@ -62,9 +62,6 @@ class IntegrationBadge extends LitElement {
justify-content: center;
}
:host([clickable]) .icon {
}
.badge {
position: absolute;
color: white;
@@ -72,9 +69,8 @@ class IntegrationBadge extends LitElement {
right: -10px;
background-color: var(--label-badge-green);
border-radius: 50%;
width: 18px;
display: block;
height: 18px;
--mdc-icon-size: 18px;
border: 2px solid white;
}

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

@@ -11,7 +11,7 @@ import {
} from "lit-element";
import { styleMap } from "lit-html/directives/style-map";
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "../../layouts/ha-app-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@material/mwc-checkbox";
@@ -63,7 +63,7 @@ class PanelCalendar extends LitElement {
protected render(): TemplateResult {
return html`
<app-header-layout>
<ha-app-layout>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button
@@ -106,7 +106,7 @@ class PanelCalendar extends LitElement {
@view-changed=${this._handleViewChanged}
></ha-full-calendar>
</div>
</app-header-layout>
</ha-app-layout>
`;
}

View File

@@ -38,7 +38,6 @@ import {
showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail";
import { mdiPlus } from "@mdi/js";
import { computeRTL } from "../../../common/util/compute_rtl";
@customElement("ha-config-areas-dashboard")
export class HaConfigAreasDashboard extends LitElement {
@@ -106,6 +105,7 @@ export class HaConfigAreasDashboard extends LitElement {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
back-path="/config"
.tabs=${configSections.integrations}
.route=${this.route}
@@ -123,18 +123,16 @@ export class HaConfigAreasDashboard extends LitElement {
icon="hass:help-circle"
@click=${this._showHelp}
></ha-icon-button>
<mwc-fab
slot="fab"
title="${this.hass.localize(
"ui.panel.config.areas.picker.create_area"
)}"
@click=${this._createArea}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</hass-tabs-subpage-data-table>
<mwc-fab
?is-wide=${this.isWide}
?narrow=${this.narrow}
?rtl=${computeRTL(this.hass!)}
title="${this.hass.localize(
"ui.panel.config.areas.picker.create_area"
)}"
@click=${this._createArea}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
`;
}
@@ -183,28 +181,6 @@ export class HaConfigAreasDashboard extends LitElement {
--app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color);
}
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[is-wide][rtl] {
bottom: 24px;
left: 24px;
right: auto;
}
`;
}
}

View File

@@ -15,6 +15,7 @@ import {
LitElement,
property,
internalProperty,
PropertyValues,
} from "lit-element";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
@@ -29,6 +30,8 @@ import "./types/ha-automation-action-event";
import "./types/ha-automation-action-scene";
import "./types/ha-automation-action-service";
import "./types/ha-automation-action-wait_template";
import "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-choose";
import { handleStructError } from "../../../lovelace/common/structs/handle-errors";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { haStyle } from "../../../../resources/styles";
@@ -41,6 +44,8 @@ const OPTIONS = [
"scene",
"service",
"wait_template",
"repeat",
"choose",
];
const getType = (action: Action) => {
@@ -96,6 +101,16 @@ export default class HaAutomationActionRow extends LitElement {
@internalProperty() private _yamlMode = false;
protected updated(changedProperties: PropertyValues) {
if (!changedProperties.has("action")) {
return;
}
this._uiModeAvailable = Boolean(getType(this.action));
if (!this._uiModeAvailable && !this._yamlMode) {
this._yamlMode = true;
}
}
protected render() {
const type = getType(this.action);
const selected = type ? OPTIONS.indexOf(type) : -1;

View File

@@ -0,0 +1,176 @@
import "@polymer/paper-input/paper-input";
import {
customElement,
LitElement,
property,
CSSResult,
css,
} from "lit-element";
import { html } from "lit-html";
import { Action, ChooseAction } from "../../../../../data/script";
import { HomeAssistant } from "../../../../../types";
import { ActionElement } from "../ha-automation-action-row";
import "../../condition/ha-automation-condition-editor";
import "@polymer/paper-listbox/paper-listbox";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../ha-automation-action";
import { Condition } from "../../../../../data/automation";
import { haStyle } from "../../../../../resources/styles";
import { mdiDelete } from "@mdi/js";
@customElement("ha-automation-action-choose")
export class HaChooseAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public action!: ChooseAction;
public static get defaultConfig() {
return { choose: [{ conditions: [], sequence: [] }], default: [] };
}
protected render() {
const action = this.action;
return html`
${action.choose.map(
(option, idx) => html`<ha-card>
<mwc-icon-button
.idx=${idx}
@click=${this._removeOption}
title=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
>
<ha-svg-icon path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
<div class="card-content">
<h2>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option",
"number",
idx + 1
)}:
</h2>
<h3>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.conditions"
)}:
</h3>
<ha-automation-condition
.conditions=${option.conditions}
.hass=${this.hass}
.idx=${idx}
@value-changed=${this._conditionChanged}
></ha-automation-condition>
<h3>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.sequence"
)}:
</h3>
<ha-automation-action
.actions=${option.sequence}
.hass=${this.hass}
.idx=${idx}
@value-changed=${this._actionChanged}
></ha-automation-action>
</div>
</ha-card>`
)}
<ha-card>
<div class="card-actions add-card">
<mwc-button @click=${this._addOption}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.add_option"
)}
</mwc-button>
</div>
</ha-card>
<h2>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.default"
)}:
</h2>
<ha-automation-action
.actions=${action.default || []}
@value-changed=${this._defaultChanged}
.hass=${this.hass}
></ha-automation-action>
`;
}
private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Condition[];
const index = (ev.target as any).idx;
const choose = [...this.action.choose];
choose[index].conditions = value;
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
}
private _actionChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Action[];
const index = (ev.target as any).idx;
const choose = [...this.action.choose];
choose[index].sequence = value;
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
}
private _addOption() {
const choose = [...this.action.choose];
choose.push({ conditions: [], sequence: [] });
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
}
private _removeOption(ev: CustomEvent) {
const index = (ev.currentTarget as any).idx;
const choose = [...this.action.choose];
choose.splice(index, 1);
fireEvent(this, "value-changed", {
value: { ...this.action, choose },
});
}
private _defaultChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Action[];
fireEvent(this, "value-changed", {
value: {
...this.action,
default: value,
},
});
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
ha-card {
margin-top: 16px;
}
.add-card mwc-button {
display: block;
text-align: center;
}
mwc-icon-button {
position: absolute;
right: 0;
padding: 4px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-choose": HaChooseAction;
}
}

View File

@@ -14,6 +14,7 @@ import {
DeviceAction,
deviceAutomationsEqual,
fetchDeviceActionCapabilities,
DeviceCapabilities,
} from "../../../../../data/device_automation";
import { HomeAssistant } from "../../../../../types";
@@ -21,11 +22,11 @@ import { HomeAssistant } from "../../../../../types";
export class HaDeviceAction extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public action!: DeviceAction;
@property({ type: Object }) public action!: DeviceAction;
@internalProperty() private _deviceId?: string;
@internalProperty() private _capabilities?;
@internalProperty() private _capabilities?: DeviceCapabilities;
private _origAction?: DeviceAction;
@@ -37,20 +38,20 @@ export class HaDeviceAction extends LitElement {
};
}
private _extraFieldsData = memoizeOne((capabilities, action: DeviceAction) =>
capabilities && capabilities.extra_fields
? capabilities.extra_fields.map((item) => {
return { [item.name]: action[item.name] };
})
: undefined
private _extraFieldsData = memoizeOne(
(action: DeviceAction, capabilities: DeviceCapabilities) => {
const extraFieldsData: { [key: string]: any } = {};
capabilities.extra_fields.forEach((item) => {
if (action[item.name] !== undefined) {
extraFieldsData![item.name] = action[item.name];
}
});
return extraFieldsData;
}
);
protected render() {
const deviceId = this._deviceId || this.action.device_id;
const extraFieldsData = this._extraFieldsData(
this._capabilities,
this.action
);
return html`
<ha-device-picker
@@ -70,10 +71,10 @@ export class HaDeviceAction extends LitElement {
"ui.panel.config.automation.editor.actions.type.device_id.action"
)}
></ha-device-action-picker>
${extraFieldsData
${this._capabilities?.extra_fields
? html`
<ha-form
.data=${Object.assign({}, ...extraFieldsData)}
.data=${this._extraFieldsData(this.action, this._capabilities)}
.schema=${this._capabilities.extra_fields}
.computeLabel=${this._extraFieldsComputeLabelCallback(
this.hass.localize
@@ -105,7 +106,7 @@ export class HaDeviceAction extends LitElement {
private async _getCapabilities() {
this._capabilities = this.action.domain
? await fetchDeviceActionCapabilities(this.hass, this.action)
: null;
: undefined;
}
private _devicePicked(ev) {

View File

@@ -0,0 +1,180 @@
import "@polymer/paper-input/paper-input";
import { customElement, LitElement, property, CSSResult } from "lit-element";
import { html } from "lit-html";
import {
RepeatAction,
Action,
CountRepeat,
WhileRepeat,
UntilRepeat,
} from "../../../../../data/script";
import { HomeAssistant } from "../../../../../types";
import { ActionElement } from "../ha-automation-action-row";
import "../../condition/ha-automation-condition-editor";
import type { PaperListboxElement } from "@polymer/paper-listbox";
import "@polymer/paper-listbox/paper-listbox";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../ha-automation-action";
import { Condition } from "../../../../lovelace/common/validate-condition";
import { haStyle } from "../../../../../resources/styles";
const OPTIONS = ["count", "while", "until"];
const getType = (action) => {
return OPTIONS.find((option) => option in action);
};
@customElement("ha-automation-action-repeat")
export class HaRepeatAction extends LitElement implements ActionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public action!: RepeatAction;
public static get defaultConfig() {
return { repeat: { count: 2, sequence: [] } };
}
protected render() {
const action = this.action.repeat;
const type = getType(action);
const selected = type ? OPTIONS.indexOf(type) : -1;
return html`
<paper-dropdown-menu-light
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type_select"
)}
no-animations
>
<paper-listbox
slot="dropdown-content"
.selected=${selected}
@iron-select=${this._typeChanged}
>
${OPTIONS.map(
(opt) => html`
<paper-item .action=${opt}>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.repeat.type.${opt}.label`
)}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
${type === "count"
? html`<paper-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type.count.label"
)}
name="count"
.value=${(action as CountRepeat).count || "0"}
@value-changed=${this._countChanged}
></paper-input>`
: ""}
${type === "while"
? html` <h3>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.repeat.type.while.conditions`
)}:
</h3>
<ha-automation-condition
.conditions=${(action as WhileRepeat).while || []}
.hass=${this.hass}
@value-changed=${this._conditionChanged}
></ha-automation-condition>`
: ""}
${type === "until"
? html` <h3>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.repeat.type.until.conditions`
)}:
</h3>
<ha-automation-condition
.conditions=${(action as UntilRepeat).until || []}
.hass=${this.hass}
@value-changed=${this._conditionChanged}
></ha-automation-condition>`
: ""}
<h3>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.sequence"
)}:
</h3>
<ha-automation-action
.actions=${action.sequence}
@value-changed=${this._actionChanged}
.hass=${this.hass}
></ha-automation-action>
`;
}
private _typeChanged(ev: CustomEvent) {
const type = ((ev.target as PaperListboxElement)?.selectedItem as any)
?.action;
if (!type || type === getType(this.action.repeat)) {
return;
}
const value = type === "count" ? 2 : [];
fireEvent(this, "value-changed", {
value: {
repeat: { [type]: value, sequence: this.action.repeat.sequence },
},
});
}
private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Condition[];
fireEvent(this, "value-changed", {
value: {
repeat: {
...this.action.repeat,
[getType(this.action.repeat)!]: value,
},
},
});
}
private _actionChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = ev.detail.value as Action[];
fireEvent(this, "value-changed", {
value: {
repeat: {
...this.action.repeat,
sequence: value,
},
},
});
}
private _countChanged(ev: CustomEvent): void {
const newVal = ev.detail.value;
if ((this.action.repeat as CountRepeat).count === newVal) {
return;
}
fireEvent(this, "value-changed", {
value: {
repeat: {
...this.action.repeat,
count: newVal,
},
},
});
}
static get styles(): CSSResult {
return haStyle;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-repeat": HaRepeatAction;
}
}

View File

@@ -13,18 +13,20 @@ import {
deviceAutomationsEqual,
DeviceCondition,
fetchDeviceConditionCapabilities,
DeviceCapabilities,
} from "../../../../../data/device_automation";
import { HomeAssistant } from "../../../../../types";
import memoizeOne from "memoize-one";
@customElement("ha-automation-condition-device")
export class HaDeviceCondition extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public condition!: DeviceCondition;
@property({ type: Object }) public condition!: DeviceCondition;
@internalProperty() private _deviceId?: string;
@internalProperty() private _capabilities?;
@internalProperty() private _capabilities?: DeviceCapabilities;
private _origCondition?: DeviceCondition;
@@ -36,16 +38,21 @@ export class HaDeviceCondition extends LitElement {
};
}
private _extraFieldsData = memoizeOne(
(condition: DeviceCondition, capabilities: DeviceCapabilities) => {
const extraFieldsData: { [key: string]: any } = {};
capabilities.extra_fields.forEach((item) => {
if (condition[item.name] !== undefined) {
extraFieldsData![item.name] = condition[item.name];
}
});
return extraFieldsData;
}
);
protected render() {
const deviceId = this._deviceId || this.condition.device_id;
const extraFieldsData =
this._capabilities && this._capabilities.extra_fields
? this._capabilities.extra_fields.map((item) => {
return { [item.name]: this.condition[item.name] };
})
: undefined;
return html`
<ha-device-picker
.value=${deviceId}
@@ -64,10 +71,10 @@ export class HaDeviceCondition extends LitElement {
"ui.panel.config.automation.editor.conditions.type.device.condition"
)}
></ha-device-condition-picker>
${extraFieldsData
${this._capabilities?.extra_fields
? html`
<ha-form
.data=${Object.assign({}, ...extraFieldsData)}
.data=${this._extraFieldsData(this.condition, this._capabilities)}
.schema=${this._capabilities.extra_fields}
.computeLabel=${this._extraFieldsComputeLabelCallback(
this.hass.localize
@@ -103,7 +110,7 @@ export class HaDeviceCondition extends LitElement {
this._capabilities = condition.domain
? await fetchDeviceConditionCapabilities(this.hass, condition)
: null;
: undefined;
}
private _devicePicked(ev) {
@@ -141,3 +148,9 @@ export class HaDeviceCondition extends LitElement {
) || schema.name;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-condition-device": HaDeviceCondition;
}
}

View File

@@ -13,9 +13,7 @@ import {
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-card";
import "../../../components/ha-svg-icon";
import "@material/mwc-fab";
@@ -46,6 +44,7 @@ import "./trigger/ha-automation-trigger";
import { HaDeviceTrigger } from "./trigger/types/ha-automation-trigger-device";
import { mdiContentSave } from "@mdi/js";
import { PaperListboxElement } from "@polymer/paper-listbox";
import { classMap } from "lit-html/directives/class-map";
const MODES = ["single", "restart", "queued", "parallel"];
const MODES_MAX = ["queued", "parallel"];
@@ -72,7 +71,7 @@ export class HaAutomationEditor extends LitElement {
@internalProperty() private _config?: AutomationConfig;
@internalProperty() private _dirty?: boolean;
@internalProperty() private _dirty = false;
@internalProperty() private _errors?: string;
@@ -312,16 +311,10 @@ export class HaAutomationEditor extends LitElement {
`
: ""}
<mwc-fab
?is-wide="${this.isWide}"
?narrow="${this.narrow}"
?dirty="${this._dirty}"
.title="${this.hass.localize(
"ui.panel.config.automation.editor.save"
)}"
slot="fab"
class=${classMap({ dirty: this._dirty })}
.title=${this.hass.localize("ui.panel.config.automation.editor.save")}
@click=${this._saveAutomation}
class="${classMap({
rtl: computeRTL(this.hass),
})}"
>
<ha-svg-icon slot="icon" path=${mdiContentSave}></ha-svg-icon>
</mwc-fab>
@@ -411,6 +404,10 @@ export class HaAutomationEditor extends LitElement {
const mode = ((ev.target as PaperListboxElement)?.selectedItem as any)
?.mode;
if (mode === this._config!.mode) {
return;
}
this._config = { ...this._config!, mode };
if (!MODES_MAX.includes(mode)) {
delete this._config.max;
@@ -538,35 +535,12 @@ export class HaAutomationEditor extends LitElement {
margin-right: 8px;
}
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 3;
margin-bottom: -80px;
transition: margin-bottom 0.3s;
position: relative;
bottom: calc(-80px - env(safe-area-inset-bottom));
transition: bottom 0.3s;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
margin-bottom: -140px;
}
mwc-fab[dirty] {
margin-bottom: 0;
}
mwc-fab.rtl {
right: auto;
left: 16px;
}
mwc-fab[is-wide].rtl {
bottom: 24px;
right: auto;
left: 24px;
mwc-fab.dirty {
bottom: 0;
}
`,
];

View File

@@ -1,13 +1,12 @@
import "../../../components/ha-icon-button";
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
CSSResultArray,
customElement,
html,
LitElement,
property,
TemplateResult,
CSSResult,
} from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one";
@@ -15,7 +14,6 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatDateTime } from "../../../common/datetime/format_date_time";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { computeRTL } from "../../../common/util/compute_rtl";
import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-entity-toggle";
import "@material/mwc-fab";
@@ -169,19 +167,16 @@ class HaAutomationPicker extends LitElement {
)}
hasFab
>
<mwc-fab
slot="fab"
title=${this.hass.localize(
"ui.panel.config.automation.picker.add_automation"
)}
@click=${this._createNew}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</hass-tabs-subpage-data-table>
<mwc-fab
slot="fab"
?is-wide=${this.isWide}
?narrow=${this.narrow}
title=${this.hass.localize(
"ui.panel.config.automation.picker.add_automation"
)}
?rtl=${computeRTL(this.hass)}
@click=${this._createNew}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
`;
}
@@ -207,37 +202,8 @@ class HaAutomationPicker extends LitElement {
});
}
static get styles(): CSSResultArray {
return [
haStyle,
css`
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
cursor: pointer;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[rtl][is-wide] {
bottom: 24px;
right: auto;
left: 24px;
}
`,
];
static get styles(): CSSResult {
return haStyle;
}
}

View File

@@ -13,18 +13,20 @@ import {
deviceAutomationsEqual,
DeviceTrigger,
fetchDeviceTriggerCapabilities,
DeviceCapabilities,
} from "../../../../../data/device_automation";
import { HomeAssistant } from "../../../../../types";
import memoizeOne from "memoize-one";
@customElement("ha-automation-trigger-device")
export class HaDeviceTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public trigger!: DeviceTrigger;
@property({ type: Object }) public trigger!: DeviceTrigger;
@internalProperty() private _deviceId?: string;
@internalProperty() private _capabilities?;
@internalProperty() private _capabilities?: DeviceCapabilities;
private _origTrigger?: DeviceTrigger;
@@ -36,16 +38,21 @@ export class HaDeviceTrigger extends LitElement {
};
}
private _extraFieldsData = memoizeOne(
(trigger: DeviceTrigger, capabilities: DeviceCapabilities) => {
const extraFieldsData: { [key: string]: any } = {};
capabilities.extra_fields.forEach((item) => {
if (trigger[item.name] !== undefined) {
extraFieldsData![item.name] = trigger[item.name];
}
});
return extraFieldsData;
}
);
protected render() {
const deviceId = this._deviceId || this.trigger.device_id;
const extraFieldsData =
this._capabilities && this._capabilities.extra_fields
? this._capabilities.extra_fields.map((item) => {
return { [item.name]: this.trigger[item.name] };
})
: undefined;
return html`
<ha-device-picker
.value=${deviceId}
@@ -64,10 +71,10 @@ export class HaDeviceTrigger extends LitElement {
"ui.panel.config.automation.editor.triggers.type.device.trigger"
)}
></ha-device-trigger-picker>
${extraFieldsData
${this._capabilities?.extra_fields
? html`
<ha-form
.data=${Object.assign({}, ...extraFieldsData)}
.data=${this._extraFieldsData(this.trigger, this._capabilities)}
.schema=${this._capabilities.extra_fields}
.computeLabel=${this._extraFieldsComputeLabelCallback(
this.hass.localize
@@ -100,7 +107,7 @@ export class HaDeviceTrigger extends LitElement {
this._capabilities = trigger.domain
? await fetchDeviceTriggerCapabilities(this.hass, trigger)
: null;
: undefined;
}
private _devicePicked(ev) {
@@ -120,7 +127,7 @@ export class HaDeviceTrigger extends LitElement {
fireEvent(this, "value-changed", { value: trigger });
}
private _extraFieldsChanged(ev) {
private _extraFieldsChanged(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
@@ -138,3 +145,9 @@ export class HaDeviceTrigger extends LitElement {
) || schema.name;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-device": HaDeviceTrigger;
}
}

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

@@ -1,4 +1,4 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "../../../layouts/ha-app-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import {
@@ -127,7 +127,7 @@ class HaConfigDashboard extends LitElement {
}
return html`
<app-header-layout>
<ha-app-layout>
<app-header fixed slot="header">
<app-toolbar>
<ha-menu-button
@@ -138,7 +138,7 @@ class HaConfigDashboard extends LitElement {
</app-header>
${content}
</app-header-layout>
</ha-app-layout>
`;
}

View File

@@ -0,0 +1,89 @@
import {
CSSResult,
customElement,
html,
LitElement,
property,
internalProperty,
TemplateResult,
css,
PropertyValues,
} from "lit-element";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { haStyle } from "../../../../../../resources/styles";
import { HomeAssistant } from "../../../../../../types";
import { OZWDevice, fetchOZWNodeStatus } from "../../../../../../data/ozw";
@customElement("ha-device-info-ozw")
export class HaDeviceInfoOzw extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public device!: DeviceRegistryEntry;
@internalProperty() private _ozwDevice?: OZWDevice;
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
this._fetchNodeDetails(this.device);
}
}
protected async _fetchNodeDetails(device) {
const ozwIdentifier = device.identifiers.find(
(identifier) => identifier[0] === "ozw"
);
if (!ozwIdentifier) {
return;
}
const identifiers = ozwIdentifier[1].split(".");
this._ozwDevice = await fetchOZWNodeStatus(
this.hass,
identifiers[0],
identifiers[1]
);
}
protected render(): TemplateResult {
if (!this._ozwDevice) {
return html``;
}
return html`
<h4>
${this.hass.localize("ui.panel.config.ozw.device_info.zwave_info")}
</h4>
<div>
${this.hass.localize("ui.panel.config.ozw.common.node_id")}:
${this._ozwDevice.node_id}
</div>
<div>
${this.hass.localize("ui.panel.config.ozw.device_info.stage")}:
${this._ozwDevice.node_query_stage}
</div>
<div>
${this.hass.localize("ui.panel.config.ozw.common.ozw_instance")}:
${this._ozwDevice.ozw_instance}
</div>
<div>
${this.hass.localize("ui.panel.config.ozw.device_info.node_failed")}:
${this._ozwDevice.is_failed
? this.hass.localize("ui.common.yes")
: this.hass.localize("ui.common.no")}
</div>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
h4 {
margin-bottom: 4px;
}
div {
word-break: break-all;
margin-top: 2px;
}
`,
];
}
}

View File

@@ -13,7 +13,6 @@ import { ifDefined } from "lit-html/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { createValidEntityId } from "../../../common/entity/valid_entity_id";
import { compare } from "../../../common/string/compare";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-icon-next";
@@ -44,6 +43,7 @@ import { configSections } from "../ha-panel-config";
import "./device-detail/ha-device-entities-card";
import "./device-detail/ha-device-info-card";
import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation";
import { slugify } from "../../../common/string/slugify";
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string | null;
@@ -501,6 +501,15 @@ export class HaConfigDevicePage extends LitElement {
</div>
`);
}
if (integrations.includes("ozw")) {
import("./device-detail/integration-elements/ozw/ha-device-info-ozw");
templates.push(html`
<ha-device-info-ozw
.hass=${this.hass}
.device=${device}
></ha-device-info-ozw>
`);
}
if (integrations.includes("zha")) {
import("./device-detail/integration-elements/zha/ha-device-actions-zha");
import("./device-detail/integration-elements/zha/ha-device-info-zha");
@@ -556,11 +565,11 @@ export class HaConfigDevicePage extends LitElement {
}
if (renameEntityid) {
const oldSearch = createValidEntityId(oldDeviceName);
const oldSearch = slugify(oldDeviceName);
if (entity.entity_id.includes(oldSearch)) {
newEntityId = entity.entity_id.replace(
oldSearch,
createValidEntityId(newDeviceName)
slugify(newDeviceName)
);
}
}
@@ -571,7 +580,6 @@ export class HaConfigDevicePage extends LitElement {
return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
name: newName || name,
disabled_by: entity.disabled_by,
new_entity_id: newEntityId || entity.entity_id,
});
});

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;
}
@@ -247,6 +247,7 @@ export class DialogEntityEditor extends LitElement {
ha-dialog {
--dialog-content-position: static;
--dialog-content-padding: 0;
--dialog-z-index: 6;
}
@media all and (min-width: 451px) and (min-height: 501px) {

View File

@@ -73,7 +73,7 @@ export class EntityRegistrySettings extends LitElement {
return html`
${!stateObj
? html`
<div>
<div class="container">
${this.hass!.localize(
"ui.dialogs.entity_registry.editor.unavailable"
)}
@@ -81,7 +81,7 @@ export class EntityRegistrySettings extends LitElement {
`
: ""}
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
<div class="form">
<div class="form container">
<paper-input
.value=${this._name}
@value-changed=${this._nameChanged}
@@ -227,8 +227,10 @@ export class EntityRegistrySettings extends LitElement {
:host {
display: block;
}
.form {
.container {
padding: 20px 24px;
}
.form {
margin-bottom: 53px;
}
.buttons {
@@ -241,6 +243,7 @@ export class EntityRegistrySettings extends LitElement {
display: flex;
justify-content: space-between;
padding: 8px;
padding-bottom: max(env(safe-area-inset-bottom), 8px);
background-color: var(--mdc-theme-surface, #fff);
}
ha-switch {

View File

@@ -182,9 +182,9 @@ class HaEntityConfig extends PolymerElement {
return;
}
var oldEntityId = oldEntities[this.selectedEntity].entity_id;
const oldEntityId = oldEntities[this.selectedEntity].entity_id;
var newIndex = entities.findIndex(function (ent) {
const newIndex = entities.findIndex(function (ent) {
return ent.entity_id === oldEntityId;
});
@@ -198,12 +198,12 @@ class HaEntityConfig extends PolymerElement {
entityChanged(index) {
if (!this.entities || !this.formEl) return;
var entity = this.entities[index];
const entity = this.entities[index];
if (!entity) return;
this.formState = "loading";
// eslint-disable-next-line @typescript-eslint/no-this-alias
var el = this;
const el = this;
this.formEl.loadEntity(entity).then(function () {
el.formState = "editing";
});
@@ -212,7 +212,7 @@ class HaEntityConfig extends PolymerElement {
saveEntity() {
this.formState = "saving";
// eslint-disable-next-line @typescript-eslint/no-this-alias
var el = this;
const el = this;
this.formEl.saveEntity().then(function () {
el.formState = "editing";
});

View File

@@ -5,8 +5,6 @@ import "@polymer/paper-listbox/paper-listbox";
import "@polymer/paper-tooltip/paper-tooltip";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
CSSResult,
customElement,
html,
LitElement,
@@ -34,7 +32,6 @@ import { HELPER_DOMAINS } from "./const";
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
import "../../../components/ha-svg-icon";
import { mdiPlus } from "@mdi/js";
import { computeRTL } from "../../../common/util/compute_rtl";
@customElement("ha-config-helpers")
export class HaConfigHelpers extends LitElement {
@@ -160,18 +157,16 @@ export class HaConfigHelpers extends LitElement {
"ui.panel.config.helpers.picker.no_helpers"
)}
>
<mwc-fab
slot="fab"
title="${this.hass.localize(
"ui.panel.config.helpers.picker.add_helper"
)}"
@click=${this._createHelpler}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
</hass-tabs-subpage-data-table>
<mwc-fab
?is-wide=${this.isWide}
?narrow=${this.narrow}
?rtl=${computeRTL(this.hass!)}
title="${this.hass.localize(
"ui.panel.config.helpers.picker.add_helper"
)}"
@click=${this._createHelpler}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
`;
}
@@ -215,31 +210,4 @@ export class HaConfigHelpers extends LitElement {
private _createHelpler() {
showHelperDetailDialog(this);
}
static get styles(): CSSResult {
return css`
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[is-wide][rtl] {
bottom: 24px;
left: 24px;
right: auto;
}
`;
}
}

View File

@@ -21,7 +21,6 @@ import { HASSDomEvent } from "../../../common/dom/fire_event";
import "../../../common/search/search-input";
import { caseInsensitiveCompare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize";
import { computeRTL } from "../../../common/util/compute_rtl";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/entity/ha-state-icon";
import "../../../components/ha-button-menu";
@@ -459,12 +458,10 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
: ""}
</div>
<mwc-fab
slot="fab"
aria-label=${this.hass.localize("ui.panel.config.integrations.new")}
title=${this.hass.localize("ui.panel.config.integrations.new")}
@click=${this._createFlow}
?is-wide=${this.isWide}
?narrow=${this.narrow}
?rtl=${computeRTL(this.hass!)}
>
<ha-svg-icon slot="icon" path=${mdiPlus}></ha-svg-icon>
</mwc-fab>
@@ -714,28 +711,6 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
h2 {
margin-top: 0;
}
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[is-wide][rtl] {
bottom: 24px;
left: 24px;
right: auto;
}
`,
];
}

View File

@@ -106,9 +106,9 @@ export class HaIntegrationCard extends LitElement {
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
<h1>
<h2>
${domainToName(this.hass.localize, this.domain)}
</h1>
</h2>
</div>
<paper-listbox>
${this.items.map(
@@ -156,12 +156,12 @@ export class HaIntegrationCard extends LitElement {
@load=${this._onImageLoad}
/>
</div>
<h1>
${item.localized_domain_name}
</h1>
<h2>
${item.localized_domain_name === item.title ? "" : item.title}
${item.localized_domain_name}
</h2>
<h3>
${item.localized_domain_name === item.title ? "" : item.title}
</h3>
${devices.length || entities.length
? html`
<div>
@@ -453,6 +453,9 @@ export class HaIntegrationCard extends LitElement {
cursor: pointer;
min-height: 35px;
}
mwc-list-item ha-svg-icon {
color: var(--secondary-text-color);
}
.back-btn {
position: absolute;
background: rgba(var(--rgb-card-background-color), 0.6);

View File

@@ -19,6 +19,7 @@ import { mdiNetwork, mdiFolderMultipleOutline, mdiPlus } from "@mdi/js";
import "../../../../../layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import { computeRTL } from "../../../../../common/util/compute_rtl";
import "@material/mwc-button/mwc-button";
export const zhaTabs: PageNavigation[] = [
{
@@ -81,10 +82,8 @@ class ZHAConfigDashboard extends LitElement {
</div>`
: ""}
</ha-card>
<a href="/config/zha/add">
<a href="/config/zha/add" slot="fab">
<mwc-fab
?is-wide=${this.isWide}
?narrow=${this.narrow}
title=${this.hass.localize("ui.panel.config.zha.add_device")}
?rtl=${computeRTL(this.hass)}
>
@@ -104,30 +103,6 @@ class ZHAConfigDashboard extends LitElement {
margin-top: 16px;
max-width: 500px;
}
mwc-fab {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 1;
}
mwc-fab[is-wide] {
bottom: 24px;
right: 24px;
}
mwc-fab[narrow] {
bottom: 84px;
}
mwc-fab[rtl] {
right: auto;
left: 16px;
}
mwc-fab[rtl][is-wide] {
bottom: 24px;
right: auto;
left: 24px;
}
`,
];
}

Some files were not shown because too many files have changed in this diff Show More