Compare commits

..

30 Commits

Author SHA1 Message Date
Bram Kragten
c0a5c6fa61 fix typing 2023-08-30 12:54:03 +02:00
Bram Kragten
fe91dbb139 Start updating styling of onboarding (#17698)
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-08-30 12:45:03 +02:00
Bram Kragten
a8e17da9f3 Add language picker to onboarding (#17668) 2023-08-30 12:45:03 +02:00
Bram Kragten
e8f1a86005 Remove name and core config steps from onboarding (#17670) 2023-08-30 12:45:03 +02:00
Bram Kragten
45b04a6188 Simplify onboarding integrations page (#17684) 2023-08-30 12:45:03 +02:00
karwosts
034ce56da5 Updates to alarm panel card configuration (#17598)
* Updates to alarm panel card configuration

* changes from feedback
2023-08-30 11:42:51 +02:00
karwosts
ae9fcebfd5 Add sortable options to input_select settings menu (#17706)
* Sortable options in input_select settings menu

* fix lint

* no ripple, default cursor

* Update src/panels/config/helpers/forms/ha-input_select-form.ts

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

* sortableStyles

* Use ha-list-item and mwc-list

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2023-08-30 07:59:06 +00:00
renovate[bot]
6197b55da8 Update dependency luxon to v3.4.2 (#17710)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-30 09:54:39 +02:00
Bram Kragten
4e5d57b5f3 Ask user to logout all devices when changing password (#17523)
* Ask user to logout all devices when changing password

* missing import

* use core command
2023-08-29 17:09:17 +02:00
Paul Bottein
7040c6d469 Add target temperature tile feature for climate and water heater (#17697) 2023-08-29 16:36:07 +02:00
Paul Bottein
6f99a39b55 Adapt more info button layout depending of number of items and screen (#17691) 2023-08-29 15:13:08 +02:00
Paul Bottein
7483833dcd Use active color for position cover tile feature even if it's closed (#17685) 2023-08-29 15:11:30 +02:00
Sam Reed
38fb48b231 Add trailing full stop to visit_addon_page message (#17719) 2023-08-29 14:38:15 +02:00
karwosts
166acee1c6 Fix language picker in profile displaying wrong language (#17725)
* fix language picker when changing language sort order #16642

* alternative fix
2023-08-29 09:45:54 +02:00
renovate[bot]
916a6df39b Update vaadinWebComponents monorepo to v24.1.6 (#17724)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-28 18:03:02 +02:00
renovate[bot]
70f37158fb Update dependency @material/web to v1.0.0-pre.16 (#17703)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-28 18:01:28 +02:00
renovate[bot]
5011bba20e Update dependency typescript to v5.2.2 (#17720)
* Update dependency typescript to v5.2.2

* Remove expect error for URL.canParse

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Steve Repsher <steverep@users.noreply.github.com>
2023-08-27 17:43:49 +00:00
renovate[bot]
8897bc703d Update babel monorepo to v7.22.11 (#17717)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-27 13:15:40 -04:00
renovate[bot]
ea6e7d441a Update dependency chai to v4.3.8 (#17718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-27 13:10:52 -04:00
renovate[bot]
f91396c986 Update Yarn to v3.6.3 (#17715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-27 13:09:28 -04:00
renovate[bot]
4598b530af Update dependency @lit-labs/virtualizer to v2.0.6 (#17702)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-26 13:14:25 -04:00
renovate[bot]
dfabb4bc36 Update dependency @rollup/plugin-node-resolve to v15.2.1 (#17704) 2023-08-25 21:18:32 -04:00
Bram Kragten
66e0100c95 Fix combobox picking first item with label on blur (#17701) 2023-08-24 16:22:26 -04:00
renovate[bot]
a08a989ef5 Update dependency lint-staged to v14.0.1 (#17694)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-24 15:18:37 -04:00
renovate[bot]
000c28abf9 Update dependency magic-string to v0.30.3 (#17695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-24 15:16:40 -04:00
renovate[bot]
6b67397c83 Update typescript-eslint monorepo to v6.4.1 (#17700)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-24 15:15:50 -04:00
karwosts
f68823a09e Add for_each to repeat action UI. Convert repeat to ha-form (#17688)
* Add for_each to repeat action UI. Convert repeat to ha-form

* reordermode as a selector option

* css styling
2023-08-24 18:13:10 +02:00
Bram Kragten
fc1782e676 Use 1 element for all group previews (#17693) 2023-08-24 10:14:30 +00:00
Simon Lamon
b4975344a1 Don't show a battery if the entity domain is Number (#17631) 2023-08-24 11:56:03 +02:00
Joakim Sørensen
2dc08d782f Hide STT and TTS entities from generated dashboard (#17689) 2023-08-24 08:59:41 +02:00
85 changed files with 9876 additions and 2179 deletions

File diff suppressed because one or more lines are too long

View File

@@ -8,4 +8,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.6.2.cjs
yarnPath: .yarn/releases/yarn-3.6.3.cjs

View File

@@ -0,0 +1,3 @@
---
title: Control Number Buttons
---

View File

@@ -0,0 +1,100 @@
import { LitElement, TemplateResult, css, html } from "lit";
import { customElement, state } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-control-number-buttons";
import { repeat } from "lit/directives/repeat";
import { ifDefined } from "lit/directives/if-defined";
const buttons: {
id: string;
label: string;
min?: number;
max?: number;
step?: number;
class?: string;
}[] = [
{
id: "basic",
label: "Basic",
},
{
id: "min_max_step",
label: "With min/max and step",
min: 5,
max: 25,
step: 0.5,
},
{
id: "custom",
label: "Custom",
class: "custom",
},
];
@customElement("demo-components-ha-control-number-buttons")
export class DemoHarControlNumberButtons extends LitElement {
@state() value = 5;
private _valueChanged(ev) {
this.value = ev.detail.value;
}
protected render(): TemplateResult {
return html`
${repeat(buttons, (button) => {
const { id, label, ...config } = button;
return html`
<ha-card>
<div class="card-content">
<label id=${id}>${label}</label>
<pre>Config: ${JSON.stringify(config)}</pre>
<ha-control-number-buttons
.value=${this.value}
.min=${config.min}
.max=${config.max}
.step=${config.step}
class=${ifDefined(config.class)}
@value-changed=${this._valueChanged}
.label=${label}
>
</ha-control-number-buttons>
</div>
</ha-card>
`;
})}
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
pre {
margin-top: 0;
margin-bottom: 8px;
}
p {
margin: 0;
}
label {
font-weight: 600;
}
.custom {
color: #2196f3;
--control-number-buttons-color: #2196f3;
--control-number-buttons-background-color: #2196f3;
--control-number-buttons-background-opacity: 0.1;
--control-number-buttons-thickness: 100px;
--control-number-buttons-border-radius: 24px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-control-number-buttons": DemoHarControlNumberButtons;
}
}

View File

@@ -173,6 +173,7 @@ class HassioBackupDialog
private async _restoreClicked() {
const backupDetails = this._backupContent.backupDetails();
this._restoringBackup = true;
this._dialogParams?.onRestoring?.();
if (this._backupContent.backupType === "full") {
await this._fullRestoreClicked(backupDetails);
} else {
@@ -219,7 +220,7 @@ class HassioBackupDialog
this._error = error.body.message;
}
} else {
fireEvent(this, "restoring");
this._dialogParams?.onRestoring?.();
await fetch(`/api/hassio/backups/${this._backup!.slug}/restore/partial`, {
method: "POST",
body: JSON.stringify(backupDetails),
@@ -268,7 +269,7 @@ class HassioBackupDialog
}
);
} else {
fireEvent(this, "restoring");
this._dialogParams?.onRestoring?.();
fetch(`/api/hassio/backups/${this._backup!.slug}/restore/full`, {
method: "POST",
body: JSON.stringify(backupDetails),

View File

@@ -5,6 +5,7 @@ import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioBackupDialogParams {
slug: string;
onDelete?: () => void;
onRestoring?: () => void;
onboarding?: boolean;
supervisor?: Supervisor;
localize?: LocalizeFunc;

View File

@@ -25,7 +25,7 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.22.10",
"@babel/runtime": "7.22.11",
"@braintree/sanitize-url": "6.0.4",
"@codemirror/autocomplete": "6.9.0",
"@codemirror/commands": "6.2.4",
@@ -52,7 +52,7 @@
"@lezer/highlight": "1.1.6",
"@lit-labs/context": "0.4.0",
"@lit-labs/motion": "1.0.4",
"@lit-labs/virtualizer": "2.0.5",
"@lit-labs/virtualizer": "2.0.6",
"@lrnwebcomponents/simple-tooltip": "7.0.16",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
@@ -79,7 +79,7 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "=1.0.0-pre.15",
"@material/web": "=1.0.0-pre.16",
"@mdi/js": "7.2.96",
"@mdi/svg": "7.2.96",
"@polymer/app-layout": "3.1.0",
@@ -94,8 +94,8 @@
"@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.1.5",
"@vaadin/vaadin-themable-mixin": "24.1.5",
"@vaadin/combo-box": "24.1.6",
"@vaadin/vaadin-themable-mixin": "24.1.6",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -121,7 +121,7 @@
"leaflet": "1.9.4",
"leaflet-draw": "1.0.4",
"lit": "2.8.0",
"luxon": "3.4.0",
"luxon": "3.4.2",
"marked": "7.0.4",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
@@ -154,11 +154,11 @@
"xss": "1.0.14"
},
"devDependencies": {
"@babel/core": "7.22.10",
"@babel/core": "7.22.11",
"@babel/plugin-proposal-decorators": "7.22.10",
"@babel/plugin-transform-runtime": "7.22.10",
"@babel/preset-env": "7.22.10",
"@babel/preset-typescript": "7.22.5",
"@babel/preset-typescript": "7.22.11",
"@koa/cors": "4.0.0",
"@octokit/auth-oauth-device": "6.0.0",
"@octokit/plugin-retry": "6.0.0",
@@ -167,7 +167,7 @@
"@rollup/plugin-babel": "6.0.3",
"@rollup/plugin-commonjs": "25.0.4",
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.2.0",
"@rollup/plugin-node-resolve": "15.2.1",
"@rollup/plugin-replace": "5.0.2",
"@types/babel__plugin-transform-runtime": "7.9.2",
"@types/chromecast-caf-receiver": "6.0.9",
@@ -186,13 +186,13 @@
"@types/tar": "6.1.5",
"@types/ua-parser-js": "0.7.36",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "6.4.0",
"@typescript-eslint/parser": "6.4.0",
"@typescript-eslint/eslint-plugin": "6.4.1",
"@typescript-eslint/parser": "6.4.1",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
"babel-plugin-template-html-minifier": "4.1.0",
"chai": "4.3.7",
"chai": "4.3.8",
"del": "7.0.0",
"eslint": "8.47.0",
"eslint-config-airbnb-base": "15.0.0",
@@ -219,10 +219,10 @@
"husky": "8.0.3",
"instant-mocha": "1.5.2",
"jszip": "3.10.1",
"lint-staged": "14.0.0",
"lint-staged": "14.0.1",
"lit-analyzer": "2.0.0-pre.3",
"lodash.template": "4.5.0",
"magic-string": "0.30.2",
"magic-string": "0.30.3",
"map-stream": "0.0.7",
"mocha": "10.2.0",
"object-hash": "3.0.0",
@@ -240,7 +240,7 @@
"tar": "6.1.15",
"terser-webpack-plugin": "5.3.9",
"ts-lit-plugin": "2.0.0-pre.1",
"typescript": "5.1.6",
"typescript": "5.2.2",
"vinyl-buffer": "1.0.1",
"vinyl-source-stream": "2.0.0",
"webpack": "5.88.2",
@@ -257,5 +257,5 @@
"sortablejs@1.15.0": "patch:sortablejs@npm%3A1.15.0#./.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
},
"packageManager": "yarn@3.6.2"
"packageManager": "yarn@3.6.3"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -35,6 +35,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@property() public oauth2State?: string;
@property() public translationFragment = "page-authorize";
@state() private _authProvider?: AuthProvider;
@state() private _authProviders?: AuthProvider[];
@@ -45,7 +47,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
constructor() {
super();
this.translationFragment = "page-authorize";
const query = extractSearchParamsObject() as AuthUrlSearchParams;
if (query.client_id) {
this.clientId = query.client_id;
@@ -102,7 +103,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
: nothing}
<ha-auth-flow
.resources=${this.resources}
.clientId=${this.clientId}
.redirectUri=${this.redirectUri}
.oauth2State=${this.oauth2State}

View File

@@ -4,7 +4,7 @@ export const clamp = (value: number, min: number, max: number) =>
// Variant that only applies the clamping to a border if the border is defined
export const conditionalClamp = (value: number, min?: number, max?: number) => {
let result: number;
result = min ? Math.max(value, min) : value;
result = max ? Math.min(result, max) : result;
result = min != null ? Math.max(value, min) : value;
result = max != null ? Math.min(result, max) : result;
return result;
};

View File

@@ -324,6 +324,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
.renderer=${rowRenderer}
.disabled=${this.disabled}
.required=${this.required}
item-id-path="id"
item-value-path="id"
item-label-path="name"
@opened-changed=${this._openedChanged}

View File

@@ -312,6 +312,10 @@ export class HaComboBox extends LitElement {
private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
ev.stopPropagation();
if (!this.allowCustomValue) {
// @ts-ignore
this._comboBox._closeOnBlurIsPrevented = true;
}
const newValue = ev.detail.value;
if (newValue !== this.value) {

View File

@@ -0,0 +1,258 @@
import { mdiMinus, mdiPlus } from "@mdi/js";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { customElement, property, query } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { conditionalClamp } from "../common/number/clamp";
import { formatNumber } from "../common/number/format_number";
import { FrontendLocaleData } from "../data/translation";
import { fireEvent } from "../common/dom/fire_event";
const A11Y_KEY_CODES = new Set([
"ArrowRight",
"ArrowUp",
"ArrowLeft",
"ArrowDown",
"PageUp",
"PageDown",
"Home",
"End",
]);
@customElement("ha-control-number-buttons")
export class HaControlNumberButton extends LitElement {
@property({ attribute: false }) public locale?: FrontendLocaleData;
@property({ type: Boolean, reflect: true }) disabled = false;
@property() public label?: string;
@property({ type: Number }) public step?: number;
@property({ type: Number }) public value?: number;
@property({ type: Number }) public min?: number;
@property({ type: Number }) public max?: number;
@property({ attribute: "false" })
public formatOptions: Intl.NumberFormatOptions = {};
@query("#input") _input!: HTMLDivElement;
private boundedValue(value: number) {
const clamped = conditionalClamp(value, this.min, this.max);
return Math.round(clamped / this._step) * this._step;
}
private get _step() {
return this.step ?? 1;
}
private get _value() {
return this.value ?? 0;
}
private get _tenPercentStep() {
if (this.max == null || this.min == null) return this._step;
const range = this.max - this.min / 10;
if (range <= this._step) return this._step;
return Math.max(range / 10);
}
private _handlePlusButton() {
this._increment();
fireEvent(this, "value-changed", { value: this.value });
this._input.focus();
}
private _handleMinusButton() {
this._decrement();
fireEvent(this, "value-changed", { value: this.value });
this._input.focus();
}
private _increment() {
this.value = this.boundedValue(this._value + this._step);
}
private _decrement() {
this.value = this.boundedValue(this._value - this._step);
}
_handleKeyDown(e: KeyboardEvent) {
if (!A11Y_KEY_CODES.has(e.code)) return;
e.preventDefault();
switch (e.code) {
case "ArrowRight":
case "ArrowUp":
this._increment();
break;
case "ArrowLeft":
case "ArrowDown":
this._decrement();
break;
case "PageUp":
this.value = this.boundedValue(this._value + this._tenPercentStep);
break;
case "PageDown":
this.value = this.boundedValue(this._value - this._tenPercentStep);
break;
case "Home":
if (this.min != null) {
this.value = this.min;
}
break;
case "End":
if (this.max != null) {
this.value = this.max;
}
break;
}
fireEvent(this, "value-changed", { value: this.value });
}
protected render(): TemplateResult {
const displayedValue =
this.value != null
? formatNumber(this.value, this.locale, this.formatOptions)
: "-";
return html`
<div class="container">
<div
id="input"
class="value"
role="number-button"
tabindex="0"
aria-valuenow=${this.value}
aria-valuemin=${this.min}
aria-valuemax=${this.max}
aria-label=${ifDefined(this.label)}
.disabled=${this.disabled}
@keydown=${this._handleKeyDown}
>
${displayedValue}
</div>
<button
class="button minus"
type="button"
tabindex="-1"
aria-label="decrement"
@click=${this._handleMinusButton}
.disabled=${this.disabled ||
(this.min != null && this._value <= this.min)}
>
<ha-svg-icon aria-hidden .path=${mdiMinus}></ha-svg-icon>
</button>
<button
class="button plus"
type="button"
tabindex="-1"
aria-label="increment"
@click=${this._handlePlusButton}
.disabled=${this.disabled ||
(this.max != null && this._value >= this.max)}
>
<ha-svg-icon aria-hidden .path=${mdiPlus}></ha-svg-icon>
</button>
</div>
`;
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
--control-number-buttons-focus-color: var(--primary-color);
--control-number-buttons-background-color: var(--disabled-color);
--control-number-buttons-background-opacity: 0.2;
--control-number-buttons-border-radius: 10px;
--mdc-icon-size: 16px;
height: 40px;
width: 200px;
color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent;
font-style: normal;
font-weight: 500;
transition: color 180ms ease-in-out;
}
:host([disabled]) {
color: var(--disabled-color);
}
.container {
position: relative;
width: 100%;
height: 100%;
}
.value {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
width: 100%;
height: 100%;
padding: 0 44px;
border-radius: var(--control-number-buttons-border-radius);
padding: 0;
margin: 0;
box-sizing: border-box;
line-height: 0;
overflow: hidden;
/* For safari border-radius overflow */
z-index: 0;
font-size: inherit;
color: inherit;
user-select: none;
-webkit-tap-highlight-color: transparent;
outline: none;
}
.value::before {
content: "";
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: var(--control-number-buttons-background-color);
transition:
background-color 180ms ease-in-out,
opacity 180ms ease-in-out;
opacity: var(--control-number-buttons-background-opacity);
}
.value:focus-visible {
box-shadow: 0 0 0 2px var(--control-number-buttons-focus-color);
}
.button {
color: inherit;
position: absolute;
top: 0;
bottom: 0;
padding: 0;
width: 35px;
height: 40px;
border: none;
background: none;
cursor: pointer;
outline: none;
}
.button[disabled] {
opacity: 0.4;
pointer-events: none;
}
.button.minus {
left: 0;
}
.button.plus {
right: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-control-number-buttons": HaControlNumberButton;
}
}

View File

@@ -171,15 +171,12 @@ export class HaControlSelectMenu extends SelectBase {
--control-select-menu-background-color: var(--disabled-color);
--control-select-menu-background-opacity: 0.2;
--control-select-menu-border-radius: 14px;
--control-select-menu-min-width: 120px;
--control-select-menu-max-width: 200px;
--control-select-menu-width: 100%;
--mdc-icon-size: 20px;
width: auto;
color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent;
}
.select-anchor {
color: var(--control-select-menu-text-color);
height: 48px;
padding: 6px 10px;
overflow: hidden;
@@ -198,11 +195,8 @@ export class HaControlSelectMenu extends SelectBase {
z-index: 0;
font-size: inherit;
transition: color 180ms ease-in-out;
color: var(--control-text-icon-color);
gap: 10px;
min-width: var(--control-select-menu-min-width);
max-width: var(--control-select-menu-max-width);
width: var(--control-select-menu-width);
width: 100%;
user-select: none;
font-size: 14px;
font-style: normal;

View File

@@ -10,12 +10,12 @@ import "./ha-icon-button";
const SUPPRESS_DEFAULT_PRESS_SELECTOR = ["button", "ha-list-item"];
export const createCloseHeading = (
hass: HomeAssistant,
hass: HomeAssistant | undefined,
title: string | TemplateResult
) => html`
<div class="header_title">${title}</div>
<ha-icon-button
.label=${hass.localize("ui.dialogs.generic.close")}
.label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"}
.path=${mdiClose}
dialogAction="close"
class="header_button"

View File

@@ -7,6 +7,7 @@ import { formatLanguageCode } from "../common/language/format_language";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { FrontendLocaleData } from "../data/translation";
import "../resources/intl-polyfill";
import { translationMetadata } from "../resources/translations-metadata";
import { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
@@ -20,7 +21,7 @@ export class HaLanguagePicker extends LitElement {
@property() public languages?: string[];
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean, reflect: true }) public disabled = false;
@@ -41,7 +42,18 @@ export class HaLanguagePicker extends LitElement {
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("languages") || changedProperties.has("value")) {
const localeChanged =
changedProperties.has("hass") &&
this.hass &&
changedProperties.get("hass") &&
changedProperties.get("hass").locale.language !==
this.hass.locale.language;
if (
changedProperties.has("languages") ||
changedProperties.has("value") ||
localeChanged
) {
this._select.layoutOptions();
if (this._select.value !== this.value) {
fireEvent(this, "value-changed", { value: this._select.value });
@@ -51,24 +63,27 @@ export class HaLanguagePicker extends LitElement {
}
const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages,
this.hass.locale,
this.nativeName
this.nativeName,
this.hass?.locale
);
const selectedItem = languageOptions.find(
const selectedItemIndex = languageOptions.findIndex(
(option) => option.value === this.value
);
if (!selectedItem) {
if (selectedItemIndex === -1) {
this.value = undefined;
}
if (localeChanged) {
this._select.select(selectedItemIndex);
}
}
}
private _getLanguagesOptions = memoizeOne(
(languages: string[], locale: FrontendLocaleData, nativeName: boolean) => {
(languages: string[], nativeName: boolean, locale?: FrontendLocaleData) => {
let options: { label: string; value: string }[] = [];
if (nativeName) {
const translations = this.hass.translationMetadata.translations;
const translations = translationMetadata.translations;
options = languages.map((lang) => {
let label = translations[lang]?.nativeName;
if (!label) {
@@ -87,14 +102,14 @@ export class HaLanguagePicker extends LitElement {
label,
};
});
} else {
} else if (locale) {
options = languages.map((lang) => ({
value: lang,
label: formatLanguageCode(lang, locale),
}));
}
if (!this.noSort) {
if (!this.noSort && locale) {
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, locale.language)
);
@@ -104,20 +119,14 @@ export class HaLanguagePicker extends LitElement {
);
private _computeDefaultLanguageOptions() {
if (!this.hass.translationMetadata?.translations) {
return;
}
this._defaultLanguages = Object.keys(
this.hass.translationMetadata.translations
);
this._defaultLanguages = Object.keys(translationMetadata.translations);
}
protected render() {
const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages,
this.hass.locale,
this.nativeName
this.nativeName,
this.hass?.locale
);
const value =
@@ -125,9 +134,10 @@ export class HaLanguagePicker extends LitElement {
return html`
<ha-select
.label=${this.label ||
this.hass.localize("ui.components.language-picker.language")}
.value=${value}
.label=${this.label ??
(this.hass?.localize("ui.components.language-picker.language") ||
"Language")}
.value=${value || ""}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@@ -137,9 +147,9 @@ export class HaLanguagePicker extends LitElement {
>
${languageOptions.length === 0
? html`<ha-list-item value=""
>${this.hass.localize(
>${this.hass?.localize(
"ui.components.language-picker.no_languages"
)}</ha-list-item
) || "No languages"}</ha-list-item
>`
: languageOptions.map(
(option) => html`
@@ -162,7 +172,7 @@ export class HaLanguagePicker extends LitElement {
private _changed(ev): void {
const target = ev.target as HaSelect;
if (!this.hass || target.value === "" || target.value === this.value) {
if (target.value === "" || target.value === this.value) {
return;
}
this.value = target.value;

View File

@@ -47,6 +47,9 @@ export class HaSelect extends SelectBase {
.mdc-select__anchor {
width: var(--ha-select-min-width, 200px);
}
.mdc-select--filled .mdc-select__anchor {
height: var(--ha-select-height, 56px);
}
.mdc-select--filled .mdc-floating-label {
inset-inline-start: 12px;
inset-inline-end: initial;

View File

@@ -1,4 +1,4 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { Action } from "../../data/script";
import { ActionSelector } from "../../data/selector";
@@ -19,10 +19,13 @@ export class HaActionSelector extends LitElement {
protected render() {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-action
.disabled=${this.disabled}
.actions=${this.value || []}
.hass=${this.hass}
.nested=${this.selector.action?.nested}
.reOrderMode=${this.selector.action?.reorder_mode}
></ha-automation-action>
`;
}
@@ -37,6 +40,11 @@ export class HaActionSelector extends LitElement {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
label {
display: block;
margin-bottom: 4px;
font-weight: 500;
}
`;
}
}

View File

@@ -1,4 +1,4 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { Condition } from "../../data/automation";
import { ConditionSelector } from "../../data/selector";
@@ -19,10 +19,13 @@ export class HaConditionSelector extends LitElement {
protected render() {
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<ha-automation-condition
.disabled=${this.disabled}
.conditions=${this.value || []}
.hass=${this.hass}
.nested=${this.selector.condition?.nested}
.reOrderMode=${this.selector.condition?.reorder_mode}
></ha-automation-condition>
`;
}
@@ -37,6 +40,11 @@ export class HaConditionSelector extends LitElement {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
label {
display: block;
margin-bottom: 4px;
font-weight: 500;
}
`;
}
}

View File

@@ -1,15 +0,0 @@
import { HomeAssistant } from "../types";
export const createLanguageListEl = (hass: HomeAssistant) => {
const list = document.createElement("datalist");
list.id = "languages";
for (const [language, metadata] of Object.entries(
hass.translationMetadata.translations
)) {
const option = document.createElement("option");
option.value = language;
option.innerText = metadata.nativeName;
list.appendChild(option);
}
return list;
};

View File

@@ -61,7 +61,18 @@ export const createAuthForUser = async (
password,
});
export const adminChangePassword = async (
export const changePassword = (
hass: HomeAssistant,
current_password: string,
new_password: string
) =>
hass.callWS({
type: "config/auth_provider/homeassistant/change_password",
current_password,
new_password,
});
export const adminChangePassword = (
hass: HomeAssistant,
userId: string,
password: string
@@ -71,3 +82,8 @@ export const adminChangePassword = async (
user_id: userId,
password,
});
export const deleteAllRefreshTokens = (hass: HomeAssistant) =>
hass.callWS({
type: "auth/delete_all_refresh_tokens",
});

View File

@@ -67,7 +67,7 @@ export const DOMAIN_ATTRIBUTES_UNITS: Record<string, Record<string, string>> = {
sun: {
elevation: "°",
},
vaccum: {
vacuum: {
battery_level: "%",
},
};

View File

@@ -6,6 +6,7 @@ import { caseInsensitiveStringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { HomeAssistant } from "../types";
import { LightColor } from "./light";
import { computeDomain } from "../common/entity/compute_domain";
type entityCategory = "config" | "diagnostic";
@@ -129,15 +130,29 @@ export interface EntityRegistryEntryUpdateParams {
aliases?: string[];
}
const batteryPriorities = ["sensor", "binary_sensor"];
export const findBatteryEntity = <T extends { entity_id: string }>(
hass: HomeAssistant,
entities: T[]
): T | undefined =>
entities.find(
(entity) =>
hass.states[entity.entity_id] &&
hass.states[entity.entity_id].attributes.device_class === "battery"
);
): T | undefined => {
const batteryEntities = entities
.filter(
(entity) =>
hass.states[entity.entity_id] &&
hass.states[entity.entity_id].attributes.device_class === "battery" &&
batteryPriorities.includes(computeDomain(entity.entity_id))
)
.sort(
(a, b) =>
batteryPriorities.indexOf(computeDomain(a.entity_id)) -
batteryPriorities.indexOf(computeDomain(b.entity_id))
);
if (batteryEntities.length > 0) {
return batteryEntities[0];
}
return undefined;
};
export const findBatteryChargingEntity = <T extends { entity_id: string }>(
hass: HomeAssistant,

View File

@@ -17,6 +17,11 @@ export interface GroupEntity extends HassEntityBase {
attributes: GroupEntityAttributes;
}
export interface GroupPreview {
state: string;
attributes: Record<string, any>;
}
export const computeGroupDomain = (
stateObj: GroupEntity
): string | undefined => {
@@ -27,35 +32,15 @@ export const computeGroupDomain = (
return uniqueDomains.length === 1 ? uniqueDomains[0] : undefined;
};
export const subscribePreviewGroupSensor = (
export const subscribePreviewGroup = (
hass: HomeAssistant,
flow_id: string,
flow_type: "config_flow" | "options_flow",
user_input: Record<string, any>,
callback: (preview: {
state: string;
attributes: Record<string, any>;
}) => void
callback: (preview: GroupPreview) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(callback, {
type: "group/sensor/start_preview",
flow_id,
flow_type,
user_input,
});
export const subscribePreviewGroupBinarySensor = (
hass: HomeAssistant,
flow_id: string,
flow_type: "config_flow" | "options_flow",
user_input: Record<string, any>,
callback: (preview: {
state: string;
attributes: Record<string, any>;
}) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(callback, {
type: "group/binary_sensor/start_preview",
type: "group/start_preview",
flow_id,
flow_type,
user_input,

View File

@@ -54,8 +54,10 @@ export type Selector =
| UiColorSelector;
export interface ActionSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
action: {} | null;
action: {
reorder_mode?: boolean;
nested?: boolean;
} | null;
}
export interface AddonSelector {
@@ -98,8 +100,10 @@ export interface ColorTempSelector {
}
export interface ConditionSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
condition: {} | null;
condition: {
reorder_mode?: boolean;
nested?: boolean;
} | null;
}
export interface ConversationAgentSelector {

View File

@@ -2,19 +2,20 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { FlowType } from "../../../data/data_entry_flow";
import { subscribePreviewGroupBinarySensor } from "../../../data/group";
import { GroupPreview, subscribePreviewGroup } from "../../../data/group";
import { HomeAssistant } from "../../../types";
import "./entity-preview-row";
import { debounce } from "../../../common/util/debounce";
@customElement("flow-preview-group_binary_sensor")
class FlowPreviewGroupBinarySensor extends LitElement {
@customElement("flow-preview-group")
class FlowPreviewGroup extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public flowType!: FlowType;
public handler!: string;
public stepId!: string;
@property() public stepId!: string;
@property() public flowId!: string;
@@ -34,7 +35,7 @@ class FlowPreviewGroupBinarySensor extends LitElement {
willUpdate(changedProps) {
if (changedProps.has("stepData")) {
this._subscribePreview();
this._debouncedSubscribePreview();
}
}
@@ -45,13 +46,10 @@ class FlowPreviewGroupBinarySensor extends LitElement {
></entity-preview-row>`;
}
private _setPreview = (preview: {
state: string;
attributes: Record<string, any>;
}) => {
private _setPreview = (preview: GroupPreview) => {
const now = new Date().toISOString();
this._preview = {
entity_id: "binary_sensor.flow_preview",
entity_id: `${this.stepId}.flow_preview`,
last_changed: now,
last_updated: now,
context: { id: "", parent_id: null, user_id: null },
@@ -59,6 +57,10 @@ class FlowPreviewGroupBinarySensor extends LitElement {
};
};
private _debouncedSubscribePreview = debounce(() => {
this._subscribePreview();
}, 250);
private async _subscribePreview() {
if (this._unsub) {
(await this._unsub)();
@@ -68,7 +70,7 @@ class FlowPreviewGroupBinarySensor extends LitElement {
return;
}
try {
this._unsub = subscribePreviewGroupBinarySensor(
this._unsub = subscribePreviewGroup(
this.hass,
this.flowId,
this.flowType,
@@ -83,6 +85,6 @@ class FlowPreviewGroupBinarySensor extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"flow-preview-group_binary_sensor": FlowPreviewGroupBinarySensor;
"flow-preview-group": FlowPreviewGroup;
}
}

View File

@@ -1,92 +0,0 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { FlowType } from "../../../data/data_entry_flow";
import { subscribePreviewGroupSensor } from "../../../data/group";
import { HomeAssistant } from "../../../types";
import "./entity-preview-row";
@customElement("flow-preview-group_sensor")
class FlowPreviewGroupSensor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public flowType!: FlowType;
public handler!: string;
public stepId!: string;
@property() public flowId!: string;
@property() public stepData!: Record<string, any>;
@state() private _preview?: HassEntity;
private _unsub?: Promise<UnsubscribeFunc>;
disconnectedCallback(): void {
super.disconnectedCallback();
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
}
willUpdate(changedProps) {
if (changedProps.has("stepData")) {
this._subscribePreview();
}
}
protected render() {
return html`<entity-preview-row
.hass=${this.hass}
.stateObj=${this._preview}
></entity-preview-row>`;
}
private _setPreview = (preview: {
state: string;
attributes: Record<string, any>;
}) => {
const now = new Date().toISOString();
this._preview = {
entity_id: "sensor.flow_preview",
last_changed: now,
last_updated: now,
context: { id: "", parent_id: null, user_id: null },
...preview,
};
};
private async _subscribePreview() {
if (this._unsub) {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType === "repair_flow") {
return;
}
if (!this.stepData.type) {
this._preview = undefined;
return;
}
try {
this._unsub = subscribePreviewGroupSensor(
this.hass,
this.flowId,
this.flowType,
this.stepData,
this._setPreview
);
} catch (err) {
this._preview = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"flow-preview-group_sensor": FlowPreviewGroupSensor;
}
}

View File

@@ -10,6 +10,7 @@ import {
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { UNIT_F } from "../../../../common/const";
import { computeAttributeValueDisplay } from "../../../../common/entity/compute_attribute_display";
import { stateActive } from "../../../../common/entity/state_active";
import { stateColorCss } from "../../../../common/entity/state_color";
@@ -67,7 +68,7 @@ export class HaMoreInfoClimateTemperature extends LitElement {
private get _step() {
return (
this.stateObj.attributes.target_temp_step ||
(this.hass.config.unit_system.temperature.indexOf("F") === -1 ? 0.5 : 1)
(this.hass.config.unit_system.temperature === UNIT_F ? 1 : 0.5)
);
}

View File

@@ -35,7 +35,9 @@ export class HaMoreInfoCoverPosition extends LitElement {
}
protected render(): TemplateResult {
const color = stateColorCss(this.stateObj);
const forcedState = this.stateObj.state === "closed" ? "open" : undefined;
const color = stateColorCss(this.stateObj, forcedState);
return html`
<ha-control-slider
@@ -68,8 +70,6 @@ export class HaMoreInfoCoverPosition extends LitElement {
height: 45vh;
max-height: 320px;
min-height: 200px;
/* Force inactive state to be colored for the slider */
--state-cover-inactive-color: var(--state-cover-active-color);
--control-slider-thickness: 100px;
--control-slider-border-radius: 24px;
--control-slider-color: var(--primary-color);

View File

@@ -72,8 +72,9 @@ export class HaMoreInfoCoverTiltPosition extends LitElement {
}
protected render(): TemplateResult {
const color = stateColorCss(this.stateObj);
const isUnavailable = this.stateObj.state === UNAVAILABLE;
const forcedState = this.stateObj.state === "closed" ? "open" : undefined;
const color = stateColorCss(this.stateObj, forcedState);
return html`
<ha-control-slider
@@ -93,7 +94,7 @@ export class HaMoreInfoCoverTiltPosition extends LitElement {
"--control-slider-color": color,
"--control-slider-background": color,
})}
.disabled=${isUnavailable}
.disabled=${this.stateObj.state === UNAVAILABLE}
>
<div slot="background" class="gradient"></div>
</ha-control-slider>
@@ -106,8 +107,6 @@ export class HaMoreInfoCoverTiltPosition extends LitElement {
height: 45vh;
max-height: 320px;
min-height: 200px;
/* Force inactive state to be colored for the slider */
--state-cover-inactive-color: var(--state-cover-active-color);
--control-slider-thickness: 100px;
--control-slider-border-radius: 24px;
--control-slider-color: var(--primary-color);

View File

@@ -0,0 +1,79 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@customElement("ha-more-info-control-select-container")
export class HaMoreInfoControlSelectContainer extends LitElement {
protected render(): TemplateResult {
const classname = `items-${this.childElementCount}`;
return html`
<div class="controls">
<div
class="controls-scroll ${classMap({
[classname]: true,
multiline: this.childElementCount >= 4,
})}"
>
<slot></slot>
</div>
</div>
`;
}
static get styles(): CSSResultGroup {
return css`
.controls {
display: flex;
flex-direction: row;
justify-content: center;
}
.controls-scroll {
display: flex;
flex-direction: row;
justify-content: flex-start;
gap: 12px;
margin: auto;
overflow: auto;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
margin: 0 -24px;
padding: 0 24px;
}
.controls-scroll::-webkit-scrollbar {
display: none;
}
::slotted(*) {
min-width: 120px;
max-width: 160px;
flex: none;
}
@media all and (hover: hover),
all and (min-width: 600px) and (min-height: 501px) {
.controls-scroll {
justify-content: center;
flex-wrap: wrap;
width: 100%;
max-width: 450px;
}
.controls-scroll.items-4 {
max-width: 300px;
}
.controls-scroll.items-3 ::slotted(*) {
max-width: 140px;
}
.multiline ::slotted(*) {
width: 140px;
}
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-control-select-container": HaMoreInfoControlSelectContainer;
}
}

View File

@@ -22,35 +22,6 @@ export const moreInfoControlStyle = css`
margin-bottom: 24px;
}
.secondary-controls {
display: flex;
flex-direction: row;
justify-content: center;
}
.secondary-controls-scroll {
display: flex;
flex-direction: row;
justify-content: flex-start;
gap: 12px;
margin: auto;
overflow: auto;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
margin: 0 -24px;
padding: 0 24px;
}
.secondary-controls-scroll::-webkit-scrollbar {
display: none;
}
/* Don't use scroll on device without touch support */
@media (hover: hover) {
.secondary-controls-scroll {
justify-content: center;
flex-wrap: wrap;
}
}
.buttons {
display: flex;
align-items: center;

View File

@@ -9,6 +9,7 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { UNIT_F } from "../../../../common/const";
import { stateActive } from "../../../../common/entity/state_active";
import { stateColorCss } from "../../../../common/entity/state_color";
import { supportsFeature } from "../../../../common/entity/supports-feature";
@@ -44,7 +45,7 @@ export class HaMoreInfoWaterHeaterTemperature extends LitElement {
private get _step() {
return (
this.stateObj.attributes.target_temp_step ||
(this.hass.config.unit_system.temperature.indexOf("F") === -1 ? 0.5 : 1)
(this.hass.config.unit_system.temperature === UNIT_F ? 1 : 0.5)
);
}

View File

@@ -38,6 +38,7 @@ import { haOscillating } from "../../../data/icons/haOscillating";
import { HomeAssistant } from "../../../types";
import "../components/climate/ha-more-info-climate-humidity";
import "../components/climate/ha-more-info-climate-temperature";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
type MainControl = "temperature" | "humidity";
@@ -163,140 +164,135 @@ class MoreInfoClimate extends LitElement {
`
: nothing}
</div>
<div class="secondary-controls">
<div class="secondary-controls-scroll">
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"hvac_mode"
)}
.value=${stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOperationModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiThermostat}></ha-svg-icon>
${stateObj.attributes.hvac_modes
.concat()
.sort(compareClimateHvacModes)
.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeHvacModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityState(stateObj, mode)}
</ha-list-item>
`
)}
</ha-control-select-menu>
${supportPresetMode && stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"preset_mode"
)}
.value=${stateObj.attributes.preset_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handlePresetmodeChanged}
@closed=${stopPropagation}
>
<ha-more-info-control-select-container>
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"hvac_mode"
)}
.value=${stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOperationModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiThermostat}></ha-svg-icon>
${stateObj.attributes.hvac_modes
.concat()
.sort(compareClimateHvacModes)
.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="icon"
.path=${mdiTuneVariant}
slot="graphic"
.path=${computeHvacModeIcon(mode)}
></ha-svg-icon>
${stateObj.attributes.preset_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computePresetModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"preset_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
${this.hass.formatEntityState(stateObj, mode)}
</ha-list-item>
`
: nothing}
${supportFanMode && stateObj.attributes.fan_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"fan_mode"
)}
.value=${stateObj.attributes.fan_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleFanModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
${stateObj.attributes.fan_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeFanModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"fan_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
${supportSwingMode && stateObj.attributes.swing_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"swing_mode"
)}
.value=${stateObj.attributes.swing_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleSwingmodeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${haOscillating}></ha-svg-icon>
${stateObj.attributes.swing_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeSwingModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"swing_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
</div>
</div>
)}
</ha-control-select-menu>
${supportPresetMode && stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"preset_mode"
)}
.value=${stateObj.attributes.preset_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handlePresetmodeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
${stateObj.attributes.preset_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computePresetModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"preset_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
${supportFanMode && stateObj.attributes.fan_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"fan_mode"
)}
.value=${stateObj.attributes.fan_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleFanModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
${stateObj.attributes.fan_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeFanModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"fan_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
${supportSwingMode && stateObj.attributes.swing_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"swing_mode"
)}
.value=${stateObj.attributes.swing_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleSwingmodeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${haOscillating}></ha-svg-icon>
${stateObj.attributes.swing_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeSwingModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"swing_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
</ha-more-info-control-select-container>
`;
}

View File

@@ -27,6 +27,7 @@ import { haOscillating } from "../../../data/icons/haOscillating";
import { haOscillatingOff } from "../../../data/icons/haOscillatingOff";
import type { HomeAssistant } from "../../../types";
import "../components/fan/ha-more-info-fan-speed";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
import "../components/ha-more-info-state-header";
import "../components/ha-more-info-toggle";
@@ -191,117 +192,112 @@ class MoreInfoFan extends LitElement {
`
: nothing}
</div>
<div class="secondary-controls">
<div class="secondary-controls-scroll">
${supportsPresetMode && this.stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"preset_mode"
)}
.value=${this.stateObj.attributes.preset_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handlePresetMode}
@closed=${stopPropagation}
>
<ha-more-info-control-select-container>
${supportsPresetMode && this.stateObj.attributes.preset_modes
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"preset_mode"
)}
.value=${this.stateObj.attributes.preset_mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handlePresetMode}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
${this.stateObj.attributes.preset_modes?.map(
(mode) => html`
<ha-list-item .value=${mode}>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
${supportsDirection
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"direction"
)}
.value=${this.stateObj.attributes.direction}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleDirection}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiRotateLeft}></ha-svg-icon>
<ha-list-item value="forward" graphic="icon">
<ha-svg-icon
slot="icon"
.path=${mdiTuneVariant}
slot="graphic"
.path=${mdiRotateRight}
></ha-svg-icon>
${this.stateObj.attributes.preset_modes?.map(
(mode) => html`
<ha-list-item .value=${mode}>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
"preset_mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
${supportsDirection
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
${this.hass.formatEntityAttributeValue(
this.stateObj,
"direction"
"direction",
"forward"
)}
.value=${this.stateObj.attributes.direction}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleDirection}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiRotateLeft}></ha-svg-icon>
<ha-list-item value="forward" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiRotateRight}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"direction",
"forward"
)}
</ha-list-item>
<ha-list-item value="reverse" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiRotateLeft}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"direction",
"reverse"
)}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}
${supportsOscillate
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"oscillating"
)}
.value=${this.stateObj.attributes.oscillating ? "on" : "off"}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOscillating}
@closed=${stopPropagation}
>
</ha-list-item>
<ha-list-item value="reverse" graphic="icon">
<ha-svg-icon
slot="icon"
slot="graphic"
.path=${mdiRotateLeft}
></ha-svg-icon>
${this.hass.formatEntityAttributeValue(
this.stateObj,
"direction",
"reverse"
)}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}
${supportsOscillate
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"oscillating"
)}
.value=${this.stateObj.attributes.oscillating ? "on" : "off"}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOscillating}
@closed=${stopPropagation}
>
<ha-svg-icon
slot="icon"
.path=${haOscillatingOff}
></ha-svg-icon>
<ha-list-item value="on" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${haOscillating}
></ha-svg-icon>
${this.hass.localize("state.default.on")}
</ha-list-item>
<ha-list-item value="off" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${haOscillatingOff}
></ha-svg-icon>
<ha-list-item value="on" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${haOscillating}
></ha-svg-icon>
${this.hass.localize("state.default.on")}
</ha-list-item>
<ha-list-item value="off" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${haOscillatingOff}
></ha-svg-icon>
${this.hass.localize("state.default.off")}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}
</div>
</div>
${this.hass.localize("state.default.off")}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}
</ha-more-info-control-select-container>
`;
}

View File

@@ -25,6 +25,7 @@ import {
computeHumidiferModeIcon,
} from "../../../data/humidifier";
import { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
import "../components/humidifier/ha-more-info-humidifier-humidity";
@@ -91,79 +92,74 @@ class MoreInfoHumidifier extends LitElement {
></ha-more-info-humidifier-humidity>
</div>
<div class="secondary-controls">
<div class="secondary-controls-scroll">
<ha-control-select-menu
.label=${this.hass.localize("ui.card.humidifier.state")}
.value=${this.stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleStateChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiPower}></ha-svg-icon>
<ha-list-item value="off">
${computeStateDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"off"
)}
</ha-list-item>
<ha-list-item value="on">
${computeStateDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"on"
)}
</ha-list-item>
</ha-control-select-menu>
<ha-more-info-control-select-container>
<ha-control-select-menu
.label=${this.hass.localize("ui.card.humidifier.state")}
.value=${this.stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleStateChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiPower}></ha-svg-icon>
<ha-list-item value="off">
${computeStateDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"off"
)}
</ha-list-item>
<ha-list-item value="on">
${computeStateDisplay(
this.hass.localize,
this.stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"on"
)}
</ha-list-item>
</ha-control-select-menu>
${supportModes
? html`
<ha-control-select-menu
.label=${hass.localize("ui.card.humidifier.mode")}
.value=${stateObj.attributes.mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon
slot="icon"
.path=${mdiTuneVariant}
></ha-svg-icon>
${stateObj.attributes.available_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeHumidiferModeIcon(mode)}
></ha-svg-icon>
${computeAttributeValueDisplay(
hass.localize,
stateObj!,
hass.locale,
hass.config,
hass.entities,
"mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
</div>
</div>
${supportModes
? html`
<ha-control-select-menu
.label=${hass.localize("ui.card.humidifier.mode")}
.value=${stateObj.attributes.mode}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
${stateObj.attributes.available_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeHumidiferModeIcon(mode)}
></ha-svg-icon>
${computeAttributeValueDisplay(
hass.localize,
stateObj!,
hass.locale,
hass.config,
hass.entities,
"mode",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
</ha-more-info-control-select-container>
`;
}

View File

@@ -36,6 +36,7 @@ import {
lightSupportsFavoriteColors,
} from "../../../data/light";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
import "../components/ha-more-info-state-header";
import "../components/ha-more-info-toggle";
@@ -286,39 +287,37 @@ class MoreInfoLight extends LitElement {
: nothing}
</div>
<div>
<div class="secondary-controls">
<div class="secondary-controls-scroll">
${supportsEffects && this.stateObj.attributes.effect_list
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"effect"
)}
.value=${this.stateObj.attributes.effect}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleEffect}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiCreation}></ha-svg-icon>
${this.stateObj.attributes.effect_list?.map(
(mode) => html`
<ha-list-item .value=${mode}>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
"effect",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
</div>
</div>
<ha-more-info-control-select-container>
${supportsEffects && this.stateObj.attributes.effect_list
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"effect"
)}
.value=${this.stateObj.attributes.effect}
.disabled=${this.stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleEffect}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiCreation}></ha-svg-icon>
${this.stateObj.attributes.effect_list?.map(
(mode) => html`
<ha-list-item .value=${mode}>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
"effect",
mode
)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
</ha-more-info-control-select-container>
<ha-attributes
.hass=${this.hass}
.stateObj=${this.stateObj}

View File

@@ -262,12 +262,13 @@ class MoreInfoVacuum extends LitElement {
const battery = batteryEntity
? this.hass.states[batteryEntity.entity_id]
: undefined;
const batteryIsBinary =
battery && computeStateDomain(battery) === "binary_sensor";
const batteryDomain = battery ? computeStateDomain(battery) : undefined;
// Use device battery entity
if (battery && (batteryIsBinary || !isNaN(battery.state as any))) {
if (
battery &&
(batteryDomain === "binary_sensor" || !isNaN(battery.state as any))
) {
const batteryChargingEntity = findBatteryChargingEntity(
this.hass,
entities
@@ -279,7 +280,9 @@ class MoreInfoVacuum extends LitElement {
return html`
<div>
<span>
${batteryIsBinary ? "" : this.hass.formatEntityState(battery)}
${batteryDomain === "sensor"
? this.hass.formatEntityState(battery)
: nothing}
<ha-battery-icon
.hass=${this.hass}
.batteryStateObj=${battery}

View File

@@ -13,6 +13,7 @@ import {
computeOperationModeIcon,
} from "../../../data/water_heater";
import { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
import "../components/water_heater/ha-more-info-water_heater-temperature";
@@ -68,77 +69,69 @@ class MoreInfoWaterHeater extends LitElement {
.stateObj=${this.stateObj}
></ha-more-info-water_heater-temperature>
</div>
<div class="secondary-controls">
<div class="secondary-controls-scroll">
${supportOperationMode && stateObj.attributes.operation_list
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"operation"
<ha-more-info-control-select-container>
${supportOperationMode && stateObj.attributes.operation_list
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"operation"
)}
.value=${stateObj.state}
.disabled=${stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOperationModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiWaterBoiler}></ha-svg-icon>
${stateObj.attributes.operation_list
.concat()
.sort(compareWaterHeaterOperationMode)
.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeOperationModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityState(stateObj, mode)}
</ha-list-item>
`
)}
.value=${stateObj.state}
.disabled=${stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleOperationModeChanged}
@closed=${stopPropagation}
>
</ha-control-select-menu>
`
: nothing}
${supportAwayMode
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"away_mode"
)}
.value=${stateObj.attributes.away_mode}
.disabled=${stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleAwayModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiAccount}></ha-svg-icon>
<ha-list-item value="on" graphic="icon">
<ha-svg-icon
slot="icon"
.path=${mdiWaterBoiler}
slot="graphic"
.path=${mdiAccountArrowRight}
></ha-svg-icon>
${stateObj.attributes.operation_list
.concat()
.sort(compareWaterHeaterOperationMode)
.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${computeOperationModeIcon(mode)}
></ha-svg-icon>
${this.hass.formatEntityState(stateObj, mode)}
</ha-list-item>
`
)}
</ha-control-select-menu>
`
: nothing}
${supportAwayMode
? html`
<ha-control-select-menu
.label=${this.hass.formatEntityAttributeName(
stateObj,
"away_mode"
)}
.value=${stateObj.attributes.away_mode}
.disabled=${stateObj.state === UNAVAILABLE}
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleAwayModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiAccount}></ha-svg-icon>
<ha-list-item value="on" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiAccountArrowRight}
></ha-svg-icon>
${this.hass.localize("state.default.on")}
</ha-list-item>
<ha-list-item value="off" graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiAccount}
></ha-svg-icon>
${this.hass.localize("state.default.off")}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}
</div>
</div>
${this.hass.localize("state.default.on")}
</ha-list-item>
<ha-list-item value="off" graphic="icon">
<ha-svg-icon slot="graphic" .path=${mdiAccount}></ha-svg-icon>
${this.hass.localize("state.default.off")}
</ha-list-item>
</ha-control-select-menu>
`
: nothing}
</ha-more-info-control-select-container>
`;
}

View File

@@ -6,57 +6,40 @@
<%= renderTemplate("_style_base.html.template") %>
<style>
html {
background-color: var(--primary-background-color, #fafafa);
color: var(--primary-text-color, #212121);
background-color: #0277bd !important;
}
@media (prefers-color-scheme: dark) {
html {
background-color: var(--primary-background-color, #111111);
color: var(--primary-text-color, #e1e1e1);
}
}
body {
height: auto;
padding: 64px 0;
padding: 32px 0;
}
.content {
box-sizing: border-box;
padding: 20px 16px;
border-radius: var(--ha-card-border-radius, 12px);
max-width: 432px;
max-width: 560px;
margin: 0 auto;
box-shadow: var(
--ha-card-box-shadow,
rgba(0, 0, 0, 0.25) 0px 54px 55px,
rgba(0, 0, 0, 0.12) 0px -12px 30px,
rgba(0, 0, 0, 0.12) 0px 4px 6px,
rgba(0, 0, 0, 0.17) 0px 12px 13px,
rgba(0, 0, 0, 0.09) 0px -3px 5px
);
background: var(
--ha-card-background,
var(--card-background-color, white)
);
color: var(--primary-text-color, #212121);
box-sizing: border-box;
}
.header {
text-align: center;
font-size: 1.96em;
display: flex;
align-items: center;
justify-content: center;
font-weight: 300;
margin-bottom: 32px;
}
.header img {
margin-right: 16px;
height: 56px;
width: 56px;
}
@media (prefers-color-scheme: dark) {
html {
color: #e1e1e1;
}
}
@media (max-width: 450px) {
@media (max-width: 592px) {
.content {
min-height: 100%;
margin: 0;
margin: 0 16px;
}
}
</style>
@@ -64,8 +47,7 @@
<body id="particles">
<div class="content">
<div class="header">
<img src="/static/icons/favicon-192x192.png" height="52" width="52" alt="" />
Home Assistant
<img src="/static/icons/favicon-192x192.png" alt="Home Assistant" />
</div>
<ha-onboarding></ha-onboarding>
</div>

View File

@@ -1,5 +1,5 @@
import { LitElement, PropertyValues } from "lit";
import { property } from "lit/decorators";
import { property, state } from "lit/decorators";
import { computeLocalize, LocalizeFunc } from "../common/translations/localize";
import { Constructor, Resources } from "../types";
import { getLocalLanguage, getTranslation } from "../util/common-translation";
@@ -15,13 +15,13 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
// Initialized to empty will prevent undefined errors if called before connected to DOM.
@property() public localize: LocalizeFunc = empty;
@property() public resources?: Resources;
// Use browser language setup before login.
@property() public language?: string = getLocalLanguage();
@property() public translationFragment?: string;
@state() private _resources?: Resources;
public connectedCallback(): void {
super.connectedCallback();
this._initializeLocalizeLite();
@@ -35,22 +35,27 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
);
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
protected willUpdate(changedProperties: PropertyValues) {
super.willUpdate(changedProperties);
if (changedProperties.get("language")) {
this._resources = undefined;
this._initializeLocalizeLite();
}
if (changedProperties.get("translationFragment")) {
this._initializeLocalizeLite();
}
if (
this.language &&
this.resources &&
this._resources &&
(changedProperties.has("language") ||
changedProperties.has("resources"))
changedProperties.has("_resources"))
) {
computeLocalize(
this.constructor.prototype,
this.language,
this.resources
this._resources
).then((localize) => {
this.localize = localize;
});
@@ -58,7 +63,7 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
}
protected async _initializeLocalizeLite() {
if (this.resources) {
if (this._resources) {
return;
}
@@ -68,7 +73,7 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
if (__DEV__) {
setTimeout(
() =>
!this.resources &&
!this._resources &&
// eslint-disable-next-line
console.error(
"Forgot to pass in resources or set translationFragment for",
@@ -84,7 +89,7 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
this.translationFragment!,
this.language!
);
this.resources = {
this._resources = {
[this.language!]: data,
};
}

View File

@@ -1,85 +0,0 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import "../components/ha-svg-icon";
@customElement("action-badge")
class ActionBadge extends LitElement {
@property() public icon!: string;
@property() public title!: string;
@property() public badgeIcon?: string;
@property({ type: Boolean, reflect: true }) public clickable = false;
protected render(): TemplateResult {
return html`
<div class="icon">
<ha-svg-icon .path=${this.icon}></ha-svg-icon>
${this.badgeIcon
? html`<ha-svg-icon
class="badge"
.path=${this.badgeIcon}
></ha-svg-icon>`
: ""}
</div>
<div class="title">${this.title}</div>
`;
}
static get styles(): CSSResultGroup {
return css`
:host {
display: inline-flex;
flex-direction: column;
text-align: center;
color: var(--primary-text-color);
}
:host([clickable]) {
color: var(--primary-text-color);
}
.icon {
position: relative;
box-sizing: border-box;
margin: 0 auto 8px;
height: 40px;
width: 40px;
border-radius: 50%;
border: 1px solid var(--secondary-text-color);
display: flex;
align-items: center;
justify-content: center;
}
:host([clickable]) .icon {
border-color: var(--primary-color);
border-width: 2px;
}
.badge {
position: absolute;
color: var(--primary-color);
bottom: -5px;
right: -5px;
background-color: white;
border-radius: 50%;
width: 18px;
display: block;
height: 18px;
}
.title {
min-height: 2.3em;
word-break: break-word;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"action-badge": ActionBadge;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,106 @@
import "@material/mwc-list/mwc-list";
import { mdiOpenInNew } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { LocalizeFunc } from "../../common/translations/localize";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-list-item";
@customElement("community-dialog")
class DialogCommunity extends LitElement {
@property({ attribute: false }) public localize?: LocalizeFunc;
public async showDialog(params): Promise<void> {
this.localize = params.localize;
}
public async closeDialog(): Promise<void> {
this.localize = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this.localize) {
return nothing;
}
return html`<ha-dialog
open
hideActions
@closed=${this.closeDialog}
.heading=${createCloseHeading(
undefined,
this.localize("ui.panel.page-onboarding.welcome.community")
)}
>
<mwc-list>
<a
target="_blank"
rel="noreferrer noopener"
href="https://community.home-assistant.io/"
>
<ha-list-item hasMeta graphic="icon">
<img src="/static/icons/favicon-192x192.png" slot="graphic" />
${this.localize("ui.panel.page-onboarding.welcome.forums")}
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item>
</a>
<a
target="_blank"
rel="noreferrer noopener"
href="https://www.home-assistant.io/newsletter/"
>
<ha-list-item hasMeta graphic="icon">
<img src="/static/icons/favicon-192x192.png" slot="graphic" />
${this.localize(
"ui.panel.page-onboarding.welcome.open_home_newsletter"
)}
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item>
</a>
<a
target="_blank"
rel="noreferrer noopener"
href="https://www.home-assistant.io/join-chat"
>
<ha-list-item hasMeta graphic="icon">
<img src="/static/images/logo_discord.png" slot="graphic" />
${this.localize("ui.panel.page-onboarding.welcome.discord")}
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item>
</a>
<a
target="_blank"
rel="noreferrer noopener"
href="https://twitter.com/home_assistant"
>
<ha-list-item hasMeta graphic="icon">
<img src="/static/images/logo_twitter.png" slot="graphic" />
${this.localize("ui.panel.page-onboarding.welcome.twitter")}
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item>
</a>
</mwc-list>
</ha-dialog>`;
}
static styles = css`
ha-dialog {
--mdc-dialog-min-width: min(400px, 90vw);
--dialog-content-padding: 0;
}
ha-list-item {
height: 56px;
--mdc-list-item-meta-size: 20px;
}
a {
text-decoration: none;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"community-dialog": DialogCommunity;
}
}

View File

@@ -0,0 +1,15 @@
import { fireEvent } from "../../common/dom/fire_event";
import { LocalizeFunc } from "../../common/translations/localize";
export const loadAppDialog = () => import("./app-dialog");
export const showAppDialog = (
element: HTMLElement,
params: { localize: LocalizeFunc }
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "app-dialog",
dialogImport: loadAppDialog,
dialogParams: params,
});
};

View File

@@ -0,0 +1,15 @@
import { fireEvent } from "../../common/dom/fire_event";
import { LocalizeFunc } from "../../common/translations/localize";
export const loadCommunityDialog = () => import("./community-dialog");
export const showCommunityDialog = (
element: HTMLElement,
params: { localize: LocalizeFunc }
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "community-dialog",
dialogImport: loadCommunityDialog,
dialogParams: params,
});
};

View File

@@ -1,3 +1,4 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import {
Auth,
createConnection,
@@ -5,35 +6,45 @@ import {
getAuth,
subscribeConfig,
} from "home-assistant-js-websocket";
import { html, PropertyValues, nothing } from "lit";
import { PropertyValues, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import {
enableWrite,
loadTokens,
saveTokens,
} from "../common/auth/token_storage";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { HASSDomEvent } from "../common/dom/fire_event";
import { extractSearchParamsObject } from "../common/url/search-params";
import { subscribeOne } from "../common/util/subscribe-one";
import "../components/ha-card";
import "../components/ha-language-picker";
import { AuthUrlSearchParams, hassUrl } from "../data/auth";
import {
fetchInstallationType,
fetchOnboardingOverview,
OnboardingResponses,
OnboardingStep,
fetchInstallationType,
fetchOnboardingOverview,
onboardIntegrationStep,
} from "../data/onboarding";
import { subscribeUser } from "../data/ws-user";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { HassElement } from "../state/hass-element";
import { HomeAssistant } from "../types";
import { storeState } from "../util/ha-pref-storage";
import { registerServiceWorker } from "../util/register-service-worker";
import "./onboarding-analytics";
import "./onboarding-create-user";
import "./onboarding-loading";
import {
enableWrite,
loadTokens,
saveTokens,
} from "../common/auth/token_storage";
import "./onboarding-welcome";
import "./onboarding-welcome-links";
import { makeDialogManager } from "../dialogs/make-dialog-manager";
type OnboardingEvent =
| {
type: "init";
result: { restore: boolean };
}
| {
type: "user";
result: OnboardingResponses["user"];
@@ -49,13 +60,21 @@ type OnboardingEvent =
type: "analytics";
};
interface OnboardingProgressEvent {
increase?: number;
decrease?: number;
progress?: number;
}
declare global {
interface HASSDomEvents {
"onboarding-step": OnboardingEvent;
"onboarding-progress": OnboardingProgressEvent;
}
interface GlobalEventHandlersEventMap {
"onboarding-step": HASSDomEvent<OnboardingEvent>;
"onboarding-progress": HASSDomEvent<OnboardingProgressEvent>;
}
}
@@ -65,8 +84,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
@property() public translationFragment = "page-onboarding";
@state() private _progress = 0;
@state() private _loading = false;
@state() private _init = false;
@state() private _restoring = false;
@state() private _supervisor?: boolean;
@@ -74,29 +97,58 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
@state() private _steps?: OnboardingStep[];
protected render() {
return html`<mwc-linear-progress
.progress=${this._progress}
></mwc-linear-progress>
<ha-card>
<div class="card-content">${this._renderStep()}</div>
</ha-card>
${this._init
? html`<onboarding-welcome-links
.localize=${this.localize}
></onboarding-welcome-links>`
: nothing}
<div class="footer">
<ha-language-picker
.value=${this.language}
.label=${""}
nativeName
@value-changed=${this._languageChanged}
></ha-language-picker>
<a
href="https://www.home-assistant.io/getting-started/onboarding/"
target="_blank"
rel="noreferrer noopener"
>${this.localize("ui.panel.page-onboarding.help")}</a
>
</div>`;
}
private _renderStep() {
if (this._init) {
return html`<onboarding-welcome
.localize=${this.localize}
.language=${this.language}
.supervisor=${this._supervisor}
></onboarding-welcome>`;
}
if (this._restoring) {
return html`<onboarding-restore-backup .localize=${this.localize}>
</onboarding-restore-backup>`;
}
const step = this._curStep()!;
if (this._loading || !step) {
return html`<onboarding-loading></onboarding-loading> `;
}
if (step.step === "user") {
return html`
${!this._restoring
? html`<onboarding-create-user
.localize=${this.localize}
.language=${this.language}
>
</onboarding-create-user>`
: ""}
${this._supervisor
? html`<onboarding-restore-backup
.localize=${this.localize}
.restoring=${this._restoring}
@restoring=${this._restoringBackup}
>
</onboarding-restore-backup>`
: ""}
`;
return html`<onboarding-create-user
.localize=${this.localize}
.language=${this.language}
>
</onboarding-create-user>`;
}
if (step.step === "core_config") {
return html`
@@ -114,7 +166,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
></onboarding-analytics>
`;
}
if (step.step === "integration") {
return html`
<onboarding-integrations
@@ -133,9 +184,13 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
import("./onboarding-core-config");
registerServiceWorker(this, false);
this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev));
this.addEventListener("onboarding-progress", (ev) =>
this._handleProgress(ev)
);
if (window.innerWidth > 450) {
import("./particles");
}
makeDialogManager(this, this.shadowRoot!);
}
protected updated(changedProps: PropertyValues) {
@@ -170,10 +225,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
return this._steps ? this._steps.find((stp) => !stp.done) : undefined;
}
private _restoringBackup() {
this._restoring = true;
}
private async _fetchInstallationType(): Promise<void> {
try {
const response = await fetchInstallationType();
@@ -222,8 +273,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
});
history.replaceState(null, "", location.pathname);
await this._connectHass(auth);
const currentStep = steps.findIndex((stp) => !stp.done);
const singelStepProgress = 1 / steps.length;
this._progress = currentStep * singelStepProgress + singelStepProgress;
} else {
// User creating screen needs to know the installation type.
this._init = true;
// Init screen needs to know the installation type.
this._fetchInstallationType();
}
@@ -233,15 +288,35 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
}
}
private _handleProgress(ev: HASSDomEvent<OnboardingProgressEvent>) {
const stepSize = 1 / this._steps!.length;
if (ev.detail.increase) {
this._progress += ev.detail.increase * stepSize;
}
if (ev.detail.decrease) {
this._progress -= ev.detail.decrease * stepSize;
}
if (ev.detail.progress) {
this._progress = ev.detail.progress;
}
}
private async _handleStepDone(ev: HASSDomEvent<OnboardingEvent>) {
const stepResult = ev.detail;
this._steps = this._steps!.map((step) =>
step.step === stepResult.type ? { ...step, done: true } : step
);
if (stepResult.type === "user") {
if (stepResult.type === "init") {
this._init = false;
this._restoring = stepResult.result.restore;
if (!this._restoring) {
this._progress = 0.25;
}
} else if (stepResult.type === "user") {
const result = stepResult.result as OnboardingResponses["user"];
this._loading = true;
this._progress = 0.5;
enableWrite();
try {
const auth = await getAuth({
@@ -258,6 +333,10 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
this._loading = false;
}
} else if (stepResult.type === "core_config") {
this._progress = 0.75;
// We do nothing
} else if (stepResult.type === "analytics") {
this._progress = 1;
// We do nothing
} else if (stepResult.type === "integration") {
this._loading = true;
@@ -331,6 +410,14 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
subscribeOne(conn, subscribeUser),
]);
this.initializeHass(auth, conn);
if (this.language && this.language !== this.hass!.language) {
this._updateHass({
locale: { ...this.hass!.locale, language: this.language },
language: this.language,
selectedLanguage: this.language,
});
storeState(this.hass!);
}
// Load config strings for integrations
(this as any)._loadFragmentTranslations(this.hass!.language, "config");
// Make sure hass is initialized + the config/user callbacks have called.
@@ -338,6 +425,60 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
setTimeout(resolve, 0);
});
}
private _languageChanged(ev: CustomEvent) {
const language = ev.detail.value;
this.language = language;
if (this.hass) {
this._updateHass({
locale: { ...this.hass!.locale, language },
language,
selectedLanguage: language,
});
storeState(this.hass!);
} else {
try {
localStorage.setItem("selectedLanguage", JSON.stringify(language));
} catch (err: any) {
// Ignore
}
}
}
static styles = css`
mwc-linear-progress {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 10;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
}
ha-language-picker {
display: block;
width: 200px;
margin-top: 8px;
border-radius: 4px;
overflow: hidden;
--ha-select-height: 40px;
--mdc-select-fill-color: none;
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
--mdc-select-ink-color: var(--primary-text-color, #212121);
--mdc-select-idle-line-color: transparent;
--mdc-select-hover-line-color: transparent;
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
--mdc-shape-small: 0;
}
a {
text-decoration: none;
color: var(--primary-text-color);
margin-right: 16px;
}
`;
}
declare global {

View File

@@ -9,8 +9,6 @@ class IntegrationBadge extends LitElement {
@property() public title!: string;
@property() public badgeIcon?: string;
@property({ type: Boolean }) public darkOptimizedIcon?: boolean;
@property({ type: Boolean, reflect: true }) public clickable = false;
@@ -27,12 +25,6 @@ class IntegrationBadge extends LitElement {
})}
referrerpolicy="no-referrer"
/>
${this.badgeIcon
? html`<ha-svg-icon
class="badge"
.path=${this.badgeIcon}
></ha-svg-icon>`
: ""}
</div>
<div class="title">${this.title}</div>
`;
@@ -47,10 +39,6 @@ class IntegrationBadge extends LitElement {
color: var(--primary-text-color);
}
:host([clickable]) {
color: var(--primary-text-color);
}
img {
max-width: 100%;
max-height: 100%;
@@ -66,18 +54,6 @@ class IntegrationBadge extends LitElement {
justify-content: center;
}
.badge {
position: absolute;
color: white;
bottom: -7px;
right: -10px;
background-color: var(--label-badge-green);
border-radius: 50%;
display: block;
--mdc-icon-size: 18px;
border: 2px solid white;
}
.title {
min-height: 2.3em;
word-break: break-word;

View File

@@ -1,4 +1,5 @@
import "@material/mwc-button/mwc-button";
import { mdiOpenInNew } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
@@ -8,6 +9,7 @@ import { Analytics, setAnalyticsPreferences } from "../data/analytics";
import { onboardAnalyticsStep } from "../data/onboarding";
import type { HomeAssistant } from "../types";
import { documentationUrl } from "../util/documentation-url";
import { onBoardingStyles } from "./styles";
@customElement("onboarding-analytics")
class OnboardingAnalytics extends LitElement {
@@ -23,7 +25,18 @@ class OnboardingAnalytics extends LitElement {
protected render(): TemplateResult {
return html`
<h1>${this.localize("ui.panel.page-onboarding.analytics.header")}</h1>
<p>${this.localize("ui.panel.page-onboarding.analytics.intro")}</p>
<p>
<a
.href=${documentationUrl(this.hass, "/integrations/analytics/")}
target="_blank"
rel="noreferrer"
>
${this.localize("ui.panel.page-onboarding.analytics.learn_more")}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>
</p>
<ha-analytics
translation_key_panel="page-onboarding"
@analytics-preferences-changed=${this._preferencesChanged}
@@ -33,16 +46,13 @@ class OnboardingAnalytics extends LitElement {
</ha-analytics>
${this._error ? html`<div class="error">${this._error}</div>` : ""}
<div class="footer">
<mwc-button @click=${this._save} .disabled=${!this._analyticsDetails}>
<mwc-button
unelevated
@click=${this._save}
.disabled=${!this._analyticsDetails}
>
${this.localize("ui.panel.page-onboarding.analytics.finish")}
</mwc-button>
<a
.href=${documentationUrl(this.hass, "/integrations/analytics/")}
target="_blank"
rel="noreferrer"
>
${this.localize("ui.panel.page-onboarding.analytics.learn_more")}
</a>
</div>
`;
}
@@ -81,27 +91,19 @@ class OnboardingAnalytics extends LitElement {
}
static get styles(): CSSResultGroup {
return css`
p {
font-size: 14px;
line-height: 20px;
}
.error {
color: var(--error-color);
}
.footer {
margin-top: 16px;
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: row-reverse;
}
a {
color: var(--primary-color);
}
`;
// footer is direction reverse to tab to "NEXT" first
return [
onBoardingStyles,
css`
.error {
color: var(--error-color);
}
a {
color: var(--primary-color);
text-decoration: none;
--mdc-icon-size: 14px;
}
`,
];
}
}

View File

@@ -13,22 +13,12 @@ import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert";
import "../components/ha-country-picker";
import "../components/ha-currency-picker";
import "../components/ha-formfield";
import "../components/ha-language-picker";
import "../components/ha-radio";
import type { HaRadio } from "../components/ha-radio";
import "../components/ha-textfield";
import type { HaTextField } from "../components/ha-textfield";
import "../components/ha-timezone-picker";
import "../components/map/ha-locations-editor";
import { ConfigUpdateValues, saveCoreConfig } from "../data/core";
import { countryCurrency } from "../data/currency";
import { onboardCoreConfigStep } from "../data/onboarding";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import { getLocalLanguage } from "../util/common-translation";
import "./onboarding-location";
import "./onboarding-name";
@customElement("onboarding-core-config")
class OnboardingCoreConfig extends LitElement {
@@ -38,32 +28,26 @@ class OnboardingCoreConfig extends LitElement {
@state() private _working = false;
@state() private _name?: ConfigUpdateValues["location_name"];
@state() private _location?: [number, number];
@state() private _elevation?: string;
private _elevation = "0";
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
private _unitSystem: ConfigUpdateValues["unit_system"] = "metric";
@state() private _currency?: ConfigUpdateValues["currency"];
private _currency: ConfigUpdateValues["currency"] = "EUR";
@state() private _timeZone?: ConfigUpdateValues["time_zone"];
private _timeZone: ConfigUpdateValues["time_zone"] =
Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
@state() private _language: ConfigUpdateValues["language"];
private _language: ConfigUpdateValues["language"] = getLocalLanguage();
@state() private _country?: ConfigUpdateValues["country"];
@state() private _error?: string;
@state() private _skipCore = false;
protected render(): TemplateResult {
if (!this._name) {
return html`<onboarding-name
.hass=${this.hass}
.onboardingLocalize=${this.onboardingLocalize}
@value-changed=${this._nameChanged}
></onboarding-name>`;
}
if (!this._location) {
return html`<onboarding-location
.hass=${this.hass}
@@ -71,156 +55,34 @@ class OnboardingCoreConfig extends LitElement {
@value-changed=${this._locationChanged}
></onboarding-location>`;
}
if (this._skipCore) {
return html`<div class="row center">
<ha-circular-progress active></ha-circular-progress>
</div>`;
}
return html`
${
this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing
}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<p>
${this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.intro_core_config"
)}
${this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.country_intro"
)}
</p>
<div class="row">
<ha-country-picker
class="flex"
.language=${this.hass.locale.language}
.label=${
this.hass.localize(
"ui.panel.config.core.section.core.core_config.country"
) || "Country"
}
name="country"
required
.disabled=${this._working}
.value=${this._countryValue}
@value-changed=${this._handleValueChanged}
>
</ha-country-picker>
<ha-language-picker
class="flex"
.hass=${this.hass}
nativeName
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.language"
)}
name="language"
required
.value=${this._languageValue}
.disabled=${this._working}
@value-changed=${this._handleValueChanged}
>
</ha-language-picker>
</div>
<div class="row">
<ha-timezone-picker
class="flex"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.time_zone"
)}
name="timeZone"
.disabled=${this._working}
.value=${this._timeZoneValue}
@value-changed=${this._handleValueChanged}
>
</ha-timezone-picker>
<ha-textfield
class="flex"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation"
)}
name="elevation"
type="number"
.disabled=${this._working}
.value=${this._elevationValue}
.suffix=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_meters"
)}
@change=${this._handleChange}
>
</ha-textfield>
</div>
<div class="row">
<div class="flex">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system"
)}
</div>
<div class="radio-group">
<ha-formfield
.label=${html`${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_metric"
)}
<div class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.metric_example"
)}
</div>`}
>
<ha-radio
name="unit_system"
value="metric"
.checked=${this._unitSystemValue === "metric"}
@change=${this._unitSystemChanged}
.disabled=${this._working}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_us_customary"
)}
<div class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.us_customary_example"
)}
</div>`}
>
<ha-radio
name="unit_system"
value="us_customary"
.checked=${this._unitSystemValue === "us_customary"}
@change=${this._unitSystemChanged}
.disabled=${this._working}
></ha-radio>
</ha-formfield>
</div>
</div>
<div class="row">
<div class="flex">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.currency"
)}<br />
<a
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
target="_blank"
rel="noopener noreferrer"
>${this.hass.localize(
"ui.panel.config.core.section.core.core_config.find_currency_value"
)}</a
>
</div>
<ha-currency-picker
class="flex"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.currency"
)}
name="currency"
.disabled=${this._working}
.value=${this._currencyValue}
@value-changed=${this._handleValueChanged}
>
</ha-currency-picker
>
</div>
</div>
<ha-country-picker
class="flex"
.language=${this.hass.locale.language}
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.country"
) || "Country"}
required
.disabled=${this._working}
.value=${this._countryValue}
@value-changed=${this._handleCountryChanged}
>
</ha-country-picker>
<div class="footer">
<mwc-button @click=${this._save} .disabled=${this._working}>
@@ -232,20 +94,6 @@ class OnboardingCoreConfig extends LitElement {
`;
}
protected willUpdate(changedProps: PropertyValues): void {
if (!changedProps.has("_country") || !this._country) {
return;
}
if (!this._currency) {
this._currency = countryCurrency[this._country];
}
if (!this._unitSystem) {
this._unitSystem = ["US", "MM", "LR"].includes(this._country)
? "us_customary"
: "metric";
}
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.addEventListener("keyup", (ev) => {
@@ -255,84 +103,69 @@ class OnboardingCoreConfig extends LitElement {
});
}
private get _elevationValue() {
return this._elevation !== undefined ? this._elevation : 0;
}
private get _timeZoneValue() {
return this._timeZone || "";
}
private get _languageValue() {
return this._language || "";
}
private get _countryValue() {
return this._country || "";
}
private get _unitSystemValue() {
return this._unitSystem !== undefined ? this._unitSystem : "metric";
}
private get _currencyValue() {
return this._currency !== undefined ? this._currency : "";
}
private _handleValueChanged(ev: ValueChangedEvent<string>) {
const target = ev.currentTarget as HTMLElement;
this[`_${target.getAttribute("name")}`] = ev.detail.value;
}
private _handleChange(ev: Event) {
const target = ev.currentTarget as HaTextField;
this[`_${target.name}`] = target.value;
}
private _nameChanged(ev: CustomEvent) {
this._name = ev.detail.value;
private _handleCountryChanged(ev: ValueChangedEvent<string>) {
this._country = ev.detail.value;
}
private async _locationChanged(ev) {
this._location = ev.detail.value.location;
this._country = ev.detail.value.country;
this._elevation = ev.detail.value.elevation;
this._currency = ev.detail.value.currency;
this._language = ev.detail.value.language || getLocalLanguage();
this._timeZone =
ev.detail.value.timezone ||
Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
this._unitSystem = ev.detail.value.unit_system;
if (ev.detail.value.country) {
this._country = ev.detail.value.country;
}
if (ev.detail.value.elevation) {
this._elevation = ev.detail.value.elevation;
}
if (ev.detail.value.currency) {
this._currency = ev.detail.value.currency;
}
if (ev.detail.value.language) {
this._language = ev.detail.value.language;
}
if (ev.detail.value.timezone) {
this._timeZone = ev.detail.value.timezone;
}
if (ev.detail.value.unit_system) {
this._unitSystem = ev.detail.value.unit_system;
}
if (this._country) {
this._skipCore = true;
this._save(ev);
return;
}
fireEvent(this, "onboarding-progress", { increase: 0.5 });
await this.updateComplete;
setTimeout(
() => this.renderRoot.querySelector("ha-textfield")!.focus(),
() => this.renderRoot.querySelector("ha-country-picker")!.focus(),
100
);
}
private _unitSystemChanged(ev: CustomEvent) {
this._unitSystem = (ev.target as HaRadio).value as
| "metric"
| "us_customary";
}
private async _save(ev) {
if (!this._location) {
if (!this._location || !this._country) {
return;
}
ev.preventDefault();
this._working = true;
try {
await saveCoreConfig(this.hass, {
location_name: this._name,
location_name: this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.location_name_default"
),
latitude: this._location[0],
longitude: this._location[1],
elevation: Number(this._elevationValue),
unit_system: this._unitSystemValue,
time_zone: this._timeZoneValue || "UTC",
currency: this._currencyValue || "EUR",
country: this._countryValue,
language: this._languageValue,
elevation: Number(this._elevation),
unit_system:
this._unitSystem || ["US", "MM", "LR"].includes(this._country)
? "us_customary"
: "metric",
time_zone: this._timeZone || "UTC",
currency: this._currency || countryCurrency[this._country] || "EUR",
country: this._country,
language: this._language,
});
const result = await onboardCoreConfigStep(this.hass);
fireEvent(this, "onboarding-step", {
@@ -340,6 +173,7 @@ class OnboardingCoreConfig extends LitElement {
result,
});
} catch (err: any) {
this._skipCore = false;
this._working = false;
this._error = err.message;
}
@@ -380,6 +214,10 @@ class OnboardingCoreConfig extends LitElement {
margin-top: 16px;
}
.center {
justify-content: center;
}
.row > * {
margin: 0 8px;
}

View File

@@ -1,7 +1,6 @@
import "@material/mwc-button";
import { genClientId } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
@@ -16,6 +15,8 @@ import type { HaForm } from "../components/ha-form/ha-form";
import { HaFormDataContainer, HaFormSchema } from "../components/ha-form/types";
import { onboardUserStep } from "../data/onboarding";
import { ValueChangedEvent } from "../types";
import { onBoardingStyles } from "./styles";
import { debounce } from "../common/util/debounce";
const CREATE_USER_SCHEMA: HaFormSchema[] = [
{
@@ -58,7 +59,7 @@ class OnboardingCreateUser extends LitElement {
protected render(): TemplateResult {
return html`
<p>${this.localize("ui.panel.page-onboarding.intro")}</p>
<h1>${this.localize("ui.panel.page-onboarding.user.header")}</h1>
<p>${this.localize("ui.panel.page-onboarding.user.intro")}</p>
${this._errorMsg
@@ -67,25 +68,27 @@ class OnboardingCreateUser extends LitElement {
<ha-form
.computeLabel=${this._computeLabel(this.localize)}
.computeHelper=${this._computeHelper(this.localize)}
.data=${this._newUser}
.disabled=${this._loading}
.error=${this._formError}
.schema=${CREATE_USER_SCHEMA}
@value-changed=${this._handleValueChanged}
></ha-form>
<mwc-button
raised
@click=${this._submitForm}
.disabled=${this._loading ||
!this._newUser.name ||
!this._newUser.username ||
!this._newUser.password ||
!this._newUser.password_confirm ||
this._newUser.password !== this._newUser.password_confirm}
>
${this.localize("ui.panel.page-onboarding.user.create_account")}
</mwc-button>
<div class="footer">
<mwc-button
unelevated
@click=${this._submitForm}
.disabled=${this._loading ||
!this._newUser.name ||
!this._newUser.username ||
!this._newUser.password ||
!this._newUser.password_confirm ||
this._newUser.password !== this._newUser.password_confirm}
>
${this.localize("ui.panel.page-onboarding.user.create_account")}
</mwc-button>
</div>
`;
}
@@ -111,20 +114,48 @@ class OnboardingCreateUser extends LitElement {
localize(`ui.panel.page-onboarding.user.data.${schema.name}`);
}
private _computeHelper(localize) {
return (schema: HaFormSchema) =>
localize(`ui.panel.page-onboarding.user.helper.${schema.name}`);
}
private _handleValueChanged(
ev: ValueChangedEvent<HaFormDataContainer>
): void {
const nameChanged = ev.detail.value.name !== this._newUser.name;
const passwordChanged =
ev.detail.value.password !== this._newUser.password ||
ev.detail.value.password_confirm !== this._newUser.password_confirm;
this._newUser = ev.detail.value;
if (nameChanged) {
this._maybePopulateUsername();
}
if (passwordChanged) {
if (this._formError.password_confirm) {
this._checkPasswordMatch();
} else {
this._debouncedCheckPasswordMatch();
}
}
}
private _debouncedCheckPasswordMatch = debounce(
() => this._checkPasswordMatch(),
500
);
private _checkPasswordMatch(): void {
const old = this._formError.password_confirm;
this._formError.password_confirm =
this._newUser.password_confirm &&
this._newUser.password !== this._newUser.password_confirm
? this.localize(
"ui.panel.page-onboarding.user.error.password_not_match"
)
: "";
if (old !== this._formError.password_confirm) {
this.requestUpdate("_formError");
}
}
private _maybePopulateUsername(): void {
@@ -167,14 +198,7 @@ class OnboardingCreateUser extends LitElement {
}
static get styles(): CSSResultGroup {
return css`
mwc-button {
margin: 32px 0 0;
text-align: center;
display: block;
text-align: right;
}
`;
return onBoardingStyles;
}
}

View File

@@ -1,14 +1,12 @@
import "@material/mwc-button/mwc-button";
import { mdiCheck, mdiDotsHorizontal } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
PropertyValues,
TemplateResult,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
@@ -16,23 +14,14 @@ import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import { LocalizeFunc } from "../common/translations/localize";
import { ConfigEntry, subscribeConfigEntries } from "../data/config_entries";
import {
getConfigFlowInProgressCollection,
localizeConfigFlowTitle,
subscribeConfigFlowInProgress,
} from "../data/config_flow";
import { subscribeConfigFlowInProgress } from "../data/config_flow";
import { DataEntryFlowProgress } from "../data/data_entry_flow";
import { domainToName } from "../data/integration";
import { scanUSBDevices } from "../data/usb";
import {
loadConfigFlowDialog,
showConfigFlowDialog,
} from "../dialogs/config-flow/show-dialog-config-flow";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { showAddIntegrationDialog } from "../panels/config/integrations/show-add-integration-dialog";
import { HomeAssistant } from "../types";
import "./action-badge";
import "./integration-badge";
import { onBoardingStyles } from "./styles";
const HIDDEN_DOMAINS = new Set([
"hassio",
@@ -63,7 +52,7 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
integrations.add(flow.handler);
}
}
this.hass.loadBackendTranslation("config", Array.from(integrations));
this.hass.loadBackendTranslation("title", Array.from(integrations));
}),
subscribeConfigEntries(
this.hass,
@@ -109,65 +98,68 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
return nothing;
}
// Render discovered and existing entries together sorted by localized title.
const entries: Array<[string, TemplateResult]> = this._entries.map(
(entry) => {
const title =
entry.title ||
domainToName(this.hass.localize, entry.domain) ||
entry.domain;
return [
title,
html`
<integration-badge
.domain=${entry.domain}
.title=${title}
.badgeIcon=${mdiCheck}
.darkOptimizedIcon=${this.hass.themes?.darkMode}
></integration-badge>
`,
];
}
const entries: Array<[string, string]> = this._entries.map((entry) => [
entry.domain,
domainToName(this.hass.localize, entry.domain),
]);
const discovered: Array<[string, string]> = this._discovered.map((flow) => [
flow.handler,
domainToName(this.hass.localize, flow.handler),
]);
let domains = [...entries, ...discovered].sort((a, b) =>
stringCompare(a[0], b[0], this.hass.locale.language)
);
const discovered: Array<[string, TemplateResult]> = this._discovered.map(
(flow) => {
const title = localizeConfigFlowTitle(this.hass.localize, flow);
return [
title,
html`
<button .flowId=${flow.flow_id} @click=${this._continueFlow}>
<integration-badge
clickable
.domain=${flow.handler}
.title=${title}
.darkOptimizedIcon=${this.hass.themes?.darkMode}
></integration-badge>
</button>
`,
];
const foundDevices = domains.length;
if (domains.length > 12) {
const uniqueDomains: Set<string> = new Set();
domains.forEach(([domain]) => {
uniqueDomains.add(domain);
});
if (uniqueDomains.size < domains.length) {
domains = domains.filter(([domain]) => {
if (uniqueDomains.has(domain)) {
uniqueDomains.delete(domain);
return true;
}
return false;
});
}
);
const content = [...entries, ...discovered]
.sort((a, b) => stringCompare(a[0], b[0], this.hass.locale.language))
.map((item) => item[1]);
if (domains.length > 12) {
domains = domains.slice(0, 11);
}
}
return html`
<h1>
${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.header"
)}
</h1>
<p>
${this.onboardingLocalize("ui.panel.page-onboarding.integration.intro")}
</p>
<div class="badges">
${content}
<button @click=${this._createFlow}>
<action-badge
clickable
title=${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.more_integrations"
)}
.icon=${mdiDotsHorizontal}
></action-badge>
</button>
${domains.map(
([domain, title]) =>
html`<integration-badge
.domain=${domain}
.title=${title}
.darkOptimizedIcon=${this.hass.themes?.darkMode}
></integration-badge>`
)}
${foundDevices > domains.length
? html`<div class="more">
${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.more_integrations",
{ count: foundDevices - domains.length }
)}
</div>`
: nothing}
</div>
<div class="footer">
<mwc-button @click=${this._finish}>
<mwc-button unelevated @click=${this._finish}>
${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.finish"
)}
@@ -178,22 +170,8 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.hass.loadBackendTranslation("title", undefined, true);
this.hass.loadBackendTranslation("title");
this._scanUSBDevices();
loadConfigFlowDialog();
}
private _createFlow() {
showAddIntegrationDialog(this);
}
private _continueFlow(ev) {
showConfigFlowDialog(this, {
continueFlowId: ev.currentTarget.flowId,
dialogClosedCallback: () => {
getConfigFlowInProgressCollection(this.hass!.connection).refresh();
},
});
}
private async _scanUSBDevices() {
@@ -210,34 +188,23 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
}
static get styles(): CSSResultGroup {
return css`
p {
font-size: 14px;
line-height: 20px;
}
.badges {
margin-top: 24px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
align-items: flex-start;
}
.badges > * {
width: 96px;
margin-bottom: 24px;
}
button {
cursor: pointer;
padding: 0;
border: 0;
background: 0;
font: inherit;
}
.footer {
text-align: right;
}
`;
return [
onBoardingStyles,
css`
.badges {
margin-top: 24px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(106px, 1fr));
row-gap: 24px;
}
.more {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
`,
];
}
}

View File

@@ -4,7 +4,7 @@ import { customElement } from "lit/decorators";
@customElement("onboarding-loading")
class OnboardingLoading extends LitElement {
protected render(): TemplateResult {
return html` <div class="loader"></div> `;
return html`<div class="loader"></div>`;
}
static get styles(): CSSResultGroup {

View File

@@ -1,5 +1,10 @@
import "@material/mwc-button/mwc-button";
import { mdiCrosshairsGps, mdiMapMarker, mdiMapSearchOutline } from "@mdi/js";
import {
mdiCrosshairsGps,
mdiMagnify,
mdiMapMarker,
mdiMapSearchOutline,
} from "@mdi/js";
import {
css,
CSSResultGroup,
@@ -30,6 +35,7 @@ import {
reverseGeocode,
searchPlaces,
} from "../data/openstreetmap";
import { onBoardingStyles } from "./styles";
const AMSTERDAM: [number, number] = [52.3731339, 4.8903147];
const mql = matchMedia("(prefers-color-scheme: dark)");
@@ -43,7 +49,7 @@ class OnboardingLocation extends LitElement {
@state() private _working = false;
@state() private _location?: [number, number];
@state() private _location: [number, number] = AMSTERDAM;
@state() private _places?: OpenStreetMapPlace[] | null;
@@ -87,6 +93,11 @@ class OnboardingLocation extends LitElement {
);
return html`
<h1>
${this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.location_header"
)}
</h1>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
@@ -97,78 +108,85 @@ class OnboardingLocation extends LitElement {
)}
</p>
<ha-textfield
label=${this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.address_label"
)}
.disabled=${this._working}
iconTrailing
@keyup=${this._addressSearch}
>
${this._working
<div class="location-search">
<ha-textfield
label=${this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.address_label"
)}
.disabled=${this._working}
icon
iconTrailing
@keyup=${this._addressSearch}
>
<ha-svg-icon slot="leadingIcon" .path=${mdiMagnify}></ha-svg-icon>
${this._working
? html`
<ha-circular-progress
slot="trailingIcon"
active
size="small"
></ha-circular-progress>
`
: html`
<ha-icon-button
@click=${this._handleButtonClick}
slot="trailingIcon"
.disabled=${this._working}
.label=${this.onboardingLocalize(
this._search
? "ui.common.search"
: "ui.panel.page-onboarding.core-config.button_detect"
)}
.path=${this._search ? mdiMapSearchOutline : mdiCrosshairsGps}
></ha-icon-button>
`}
</ha-textfield>
${this._places !== undefined
? html`
<ha-circular-progress
slot="trailingIcon"
active
size="small"
></ha-circular-progress>
<mwc-list activatable>
${this._places?.length
? this._places.map((place) => {
const primary = [
place.name || place.address[place.category],
place.address.house_number,
place.address.road || place.address.waterway,
place.address.village || place.address.town,
place.address.suburb || place.address.subdivision,
place.address.city || place.address.municipality,
]
.filter(Boolean)
.join(", ");
const secondary = [
place.address.county ||
place.address.state_district ||
place.address.region,
place.address.state,
place.address.country,
]
.filter(Boolean)
.join(", ");
return html`<ha-list-item
@click=${this._itemClicked}
.placeId=${place.place_id}
.selected=${this._highlightedMarker === place.place_id}
.activated=${this._highlightedMarker === place.place_id}
.twoline=${primary && secondary}
>
${primary || secondary}
<span slot="secondary"
>${primary ? secondary : ""}</span
>
</ha-list-item>`;
})
: html`<ha-list-item noninteractive
>${this._places === null
? ""
: "No results"}</ha-list-item
>`}
</mwc-list>
`
: html`
<ha-icon-button
@click=${this._handleButtonClick}
slot="trailingIcon"
.disabled=${this._working}
.label=${this.onboardingLocalize(
this._search
? "ui.common.search"
: "ui.panel.page-onboarding.core-config.button_detect"
)}
.path=${this._search ? mdiMapSearchOutline : mdiCrosshairsGps}
></ha-icon-button>
`}
</ha-textfield>
${this._places !== undefined
? html`
<mwc-list activatable>
${this._places?.length
? this._places.map((place) => {
const primary = [
place.name || place.address[place.category],
place.address.house_number,
place.address.road || place.address.waterway,
place.address.village || place.address.town,
place.address.suburb || place.address.subdivision,
place.address.city || place.address.municipality,
]
.filter(Boolean)
.join(", ");
const secondary = [
place.address.county ||
place.address.state_district ||
place.address.region,
place.address.state,
place.address.country,
]
.filter(Boolean)
.join(", ");
return html`<ha-list-item
@click=${this._itemClicked}
.placeId=${place.place_id}
.selected=${this._highlightedMarker === place.place_id}
.activated=${this._highlightedMarker === place.place_id}
.twoline=${primary && secondary}
>
${primary || secondary}
<span slot="secondary">${primary ? secondary : ""}</span>
</ha-list-item>`;
})
: html`<ha-list-item noninteractive
>${this._places === null ? "" : "No results"}</ha-list-item
>`}
</mwc-list>
`
: nothing}
<p class="attribution">${addressAttribution}</p>
: nothing}
</div>
<ha-locations-editor
class="flex"
.hass=${this.hass}
@@ -184,11 +202,10 @@ class OnboardingLocation extends LitElement {
@marker-clicked=${this._markerClicked}
></ha-locations-editor>
<p class="attribution">${addressAttribution}</p>
<div class="footer">
<mwc-button
@click=${this._save}
.disabled=${!this._location || this._working}
>
<mwc-button @click=${this._save} unelevated .disabled=${this._working}>
${this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.finish"
)}
@@ -301,7 +318,6 @@ class OnboardingLocation extends LitElement {
private async _searchAddress(address: string) {
this._working = true;
this._location = undefined;
this._highlightedMarker = undefined;
this._error = undefined;
this._places = null;
@@ -464,74 +480,76 @@ class OnboardingLocation extends LitElement {
}
static get styles(): CSSResultGroup {
return css`
p {
font-size: 14px;
line-height: 20px;
}
ha-textfield {
display: block;
}
ha-textfield > ha-icon-button {
position: absolute;
top: 10px;
right: 10px;
--mdc-icon-button-size: 36px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
inset-inline-start: initial;
inset-inline-end: 10px;
direction: var(--direction);
}
ha-textfield > ha-circular-progress {
position: relative;
left: 12px;
}
ha-locations-editor {
display: block;
height: 300px;
margin-top: 8px;
border-radius: var(--mdc-shape-small, 4px);
overflow: hidden;
}
mwc-list {
width: 100%;
border: 1px solid var(--divider-color);
box-sizing: border-box;
border-top-width: 0;
border-bottom-left-radius: var(--mdc-shape-small, 4px);
border-bottom-right-radius: var(--mdc-shape-small, 4px);
--mdc-list-vertical-padding: 0;
}
ha-list-item {
height: 72px;
}
.footer {
margin-top: 16px;
text-align: right;
}
.attribution {
/* textfield helper style */
margin: 0;
padding: 4px 16px 12px 16px;
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
font-family: var(
--mdc-typography-caption-font-family,
var(--mdc-typography-font-family, Roboto, sans-serif)
);
font-size: var(--mdc-typography-caption-font-size, 0.75rem);
font-weight: var(--mdc-typography-caption-font-weight, 400);
letter-spacing: var(
--mdc-typography-caption-letter-spacing,
0.0333333333em
);
text-decoration: var(--mdc-typography-caption-text-decoration, inherit);
text-transform: var(--mdc-typography-caption-text-transform, inherit);
}
.attribution a {
color: inherit;
}
`;
return [
onBoardingStyles,
css`
.location-search {
margin-top: 32px;
margin-bottom: 32px;
}
ha-textfield {
display: block;
}
ha-textfield > ha-icon-button {
position: absolute;
top: 10px;
right: 10px;
--mdc-icon-button-size: 36px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
inset-inline-start: initial;
inset-inline-end: 10px;
direction: var(--direction);
}
ha-textfield > ha-circular-progress {
position: relative;
left: 12px;
}
ha-locations-editor {
display: block;
height: 300px;
margin-top: 8px;
border-radius: var(--mdc-shape-large, 16px);
overflow: hidden;
}
mwc-list {
width: 100%;
border: 1px solid var(--divider-color);
box-sizing: border-box;
border-top-width: 0;
border-bottom-left-radius: var(--mdc-shape-small, 4px);
border-bottom-right-radius: var(--mdc-shape-small, 4px);
--mdc-list-vertical-padding: 0;
}
ha-list-item {
height: 72px;
}
.attribution {
/* textfield helper style */
margin: 0;
padding: 4px 16px 12px 16px;
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
font-family: var(
--mdc-typography-caption-font-family,
var(--mdc-typography-font-family, Roboto, sans-serif)
);
font-size: var(--mdc-typography-caption-font-size, 0.75rem);
font-weight: var(--mdc-typography-caption-font-weight, 400);
letter-spacing: var(
--mdc-typography-caption-letter-spacing,
0.0333333333em
);
text-decoration: var(
--mdc-typography-caption-text-decoration,
inherit
);
text-transform: var(--mdc-typography-caption-text-transform, inherit);
}
.attribution a {
color: inherit;
}
`,
];
}
}

View File

@@ -1,111 +0,0 @@
import "@material/mwc-button/mwc-button";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert";
import "../components/ha-formfield";
import "../components/ha-radio";
import "../components/ha-textfield";
import "../components/map/ha-locations-editor";
import { ConfigUpdateValues } from "../data/core";
import type { HomeAssistant } from "../types";
@customElement("onboarding-name")
class OnboardingName extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public onboardingLocalize!: LocalizeFunc;
private _name?: ConfigUpdateValues["location_name"];
protected render(): TemplateResult {
return html`
<p>
${this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.intro",
{ name: this.hass.user!.name }
)}
</p>
<ha-textfield
.label=${this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.location_name"
)}
.value=${this._nameValue}
@change=${this._nameChanged}
></ha-textfield>
<p>
${this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.intro_core"
)}
</p>
<div class="footer">
<mwc-button @click=${this._save} .disabled=${!this._nameValue}>
${this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.finish"
)}
</mwc-button>
</div>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
setTimeout(
() => this.renderRoot.querySelector("ha-textfield")!.focus(),
100
);
this.addEventListener("keyup", (ev) => {
if (ev.key === "Enter") {
this._save(ev);
}
});
}
private get _nameValue() {
return this._name !== undefined
? this._name
: this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.location_name_default"
);
}
private _nameChanged(ev) {
this._name = ev.target.value;
}
private async _save(ev) {
ev.preventDefault();
fireEvent(this, "value-changed", {
value: this._nameValue,
});
}
static get styles(): CSSResultGroup {
return css`
ha-textfield {
display: block;
}
p {
font-size: 14px;
line-height: 20px;
}
.footer {
margin-top: 16px;
text-align: right;
}
a {
color: var(--primary-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-name": OnboardingName;
}
}

View File

@@ -1,44 +1,34 @@
import "@material/mwc-button/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload";
import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-card";
import "../components/ha-ansi-to-html";
import "../components/ha-card";
import { fetchInstallationType } from "../data/onboarding";
import { makeDialogManager } from "../dialogs/make-dialog-manager";
import { ProvideHassLitMixin } from "../mixins/provide-hass-lit-mixin";
import { haStyle } from "../resources/styles";
import "./onboarding-loading";
declare global {
interface HASSDomEvents {
restoring: undefined;
}
}
import { onBoardingStyles } from "./styles";
@customElement("onboarding-restore-backup")
class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) {
class OnboardingRestoreBackup extends LitElement {
@property() public localize!: LocalizeFunc;
@property() public language!: string;
@property({ type: Boolean }) public restoring = false;
@state() public _restoring = false;
protected render(): TemplateResult {
return this.restoring
? html`<ha-card
.header=${this.localize(
"ui.panel.page-onboarding.restore.in_progress"
)}
>
<onboarding-loading></onboarding-loading>
</ha-card>`
return this._restoring
? html`<h1>
${this.localize("ui.panel.page-onboarding.restore.in_progress")}
</h1>
<onboarding-loading></onboarding-loading>`
: html`
<button class="link" @click=${this._uploadBackup}>
${this.localize("ui.panel.page-onboarding.restore.description")}
</button>
<h1>${this.localize("ui.panel.page-onboarding.restore.header")}</h1>
<ha-button unelevated @click=${this._uploadBackup}>
${this.localize("ui.panel.page-onboarding.restore.upload_backup")}
</ha-button>
`;
}
@@ -51,12 +41,11 @@ class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
makeDialogManager(this, this.shadowRoot!);
setInterval(() => this._checkRestoreStatus(), 1000);
}
private async _checkRestoreStatus(): Promise<void> {
if (this.restoring) {
if (this._restoring) {
try {
await fetchInstallationType();
} catch (err: any) {
@@ -72,32 +61,20 @@ class OnboardingRestoreBackup extends ProvideHassLitMixin(LitElement) {
slug,
onboarding: true,
localize: this.localize,
onRestoring: () => {
this._restoring = true;
},
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
onBoardingStyles,
css`
.logentry {
text-align: center;
}
ha-card {
padding: 4px;
margin-top: 8px;
}
ha-ansi-to-html {
display: block;
line-height: 22px;
padding: 0 8px;
white-space: pre-wrap;
}
@media all and (min-width: 600px) {
ha-card {
width: 600px;
margin-left: -100px;
}
:host {
display: flex;
flex-direction: column;
align-items: center;
}
`,
];

View File

@@ -0,0 +1,103 @@
import "@material/mwc-ripple";
import type { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import {
customElement,
eventOptions,
property,
queryAsync,
state,
} from "lit/decorators";
import "../components/ha-card";
@customElement("onboarding-welcome-link")
class OnboardingWelcomeLink extends LitElement {
@property() public label!: string;
@property() public iconPath!: string;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
protected render(): TemplateResult {
return html`
<ha-card
@focus=${this.handleRippleFocus}
@blur=${this.handleRippleBlur}
@mousedown=${this.handleRippleActivate}
@mouseup=${this.handleRippleDeactivate}
@mouseenter=${this.handleRippleMouseEnter}
@mouseleave=${this.handleRippleMouseLeave}
@touchstart=${this.handleRippleActivate}
@touchend=${this.handleRippleDeactivate}
@touchcancel=${this.handleRippleDeactivate}
>
<ha-svg-icon .path=${this.iconPath}></ha-svg-icon>
${this.label}
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
</ha-card>
`;
}
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
private handleRippleMouseEnter() {
this._rippleHandlers.startHover();
}
private handleRippleMouseLeave() {
this._rippleHandlers.endHover();
}
@eventOptions({ passive: true })
private handleRippleActivate(evt?: Event) {
this._rippleHandlers.startPress(evt);
}
private handleRippleDeactivate() {
this._rippleHandlers.endPress();
}
private handleRippleFocus() {
this._rippleHandlers.startFocus();
}
private handleRippleBlur() {
this._rippleHandlers.endFocus();
}
static get styles(): CSSResultGroup {
return css`
:host {
cursor: pointer;
}
ha-card {
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
font-weight: 500;
padding: 32px 16px;
}
ha-svg-icon {
color: var(--text-primary-color);
background: var(--welcome-link-color, var(--primary-color));
border-radius: 50%;
padding: 8px;
margin-bottom: 16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-welcome-link": OnboardingWelcomeLink;
}
}

View File

@@ -0,0 +1,84 @@
import { mdiAccountGroup, mdiFileDocument, mdiTabletCellphone } from "@mdi/js";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-card";
import type { HomeAssistant } from "../types";
import { showAppDialog } from "./dialogs/show-app-dialog";
import { showCommunityDialog } from "./dialogs/show-community-dialog";
import "./onboarding-welcome-link";
@customElement("onboarding-welcome-links")
class OnboardingWelcomeLinks extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public localize!: LocalizeFunc;
protected render(): TemplateResult {
return html`<a
target="_blank"
rel="noreferrer noopener"
href="https://www.home-assistant.io/blog/2016/01/19/perfect-home-automation/"
>
<onboarding-welcome-link
.iconPath=${mdiFileDocument}
.label=${this.localize("ui.panel.page-onboarding.welcome.vision")}
>
</onboarding-welcome-link>
</a>
<onboarding-welcome-link
class="community"
@click=${this._openCommunity}
.iconPath=${mdiAccountGroup}
.label=${this.localize("ui.panel.page-onboarding.welcome.community")}
>
</onboarding-welcome-link>
<onboarding-welcome-link
class="app"
@click=${this._openApp}
.iconPath=${mdiTabletCellphone}
.label=${this.localize("ui.panel.page-onboarding.welcome.download_app")}
>
</onboarding-welcome-link>`;
}
private _openCommunity(): void {
showCommunityDialog(this, { localize: this.localize });
}
private _openApp(): void {
showAppDialog(this, { localize: this.localize });
}
static get styles(): CSSResultGroup {
return css`
:host {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
margin-top: 16px;
column-gap: 16px;
row-gap: 16px;
}
@media (max-width: 550px) {
:host {
grid-template-columns: 1fr;
}
}
.community {
--welcome-link-color: #008142;
}
.app {
--welcome-link-color: #6e41ab;
}
a {
text-decoration: none;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-welcome-links": OnboardingWelcomeLinks;
}
}

View File

@@ -0,0 +1,79 @@
import {
CSSResultGroup,
LitElement,
TemplateResult,
css,
html,
nothing,
} from "lit";
import { customElement, property } from "lit/decorators";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import { onBoardingStyles } from "./styles";
import { fireEvent } from "../common/dom/fire_event";
import "../components/ha-button";
@customElement("onboarding-welcome")
class OnboardingWelcome extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public localize!: LocalizeFunc;
@property({ type: Boolean }) public supervisor?: boolean;
protected render(): TemplateResult {
return html`
<h1>${this.localize("ui.panel.page-onboarding.welcome.header")}</h1>
<p>${this.localize("ui.panel.page-onboarding.intro")}</p>
<ha-button unelevated @click=${this._start} class="start">
${this.localize("ui.panel.page-onboarding.welcome.start")}
</ha-button>
${this.supervisor
? html`<ha-button @click=${this._restoreBackup}>
${this.localize("ui.panel.page-onboarding.welcome.restore_backup")}
</ha-button>`
: nothing}
`;
}
private _start(): void {
fireEvent(this, "onboarding-step", {
type: "init",
result: { restore: false },
});
}
private _restoreBackup(): void {
fireEvent(this, "onboarding-step", {
type: "init",
result: { restore: true },
});
}
static get styles(): CSSResultGroup {
return [
onBoardingStyles,
css`
:host {
display: flex;
flex-direction: column;
align-items: center;
}
.start {
--button-height: 48px;
--mdc-typography-button-font-size: 1rem;
--mdc-button-horizontal-padding: 24px;
margin: 16px 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"onboarding-welcome": OnboardingWelcome;
}
}

View File

@@ -1,5 +1,6 @@
import { tsParticles } from "tsparticles-engine";
import { loadLinksPreset } from "tsparticles-preset-links";
import { DEFAULT_PRIMARY_COLOR } from "../resources/ha-style";
loadLinksPreset(tsParticles).then(() => {
tsParticles.load("particles", {
@@ -22,16 +23,16 @@ loadLinksPreset(tsParticles).then(() => {
},
particles: {
color: {
value: "#fff",
animation: {
enable: true,
speed: 50,
sync: false,
},
value: DEFAULT_PRIMARY_COLOR,
},
animation: {
enable: true,
speed: 50,
sync: false,
},
links: {
color: {
value: "random",
value: DEFAULT_PRIMARY_COLOR,
},
distance: 100,
enable: true,

19
src/onboarding/styles.ts Normal file
View File

@@ -0,0 +1,19 @@
import { css } from "lit";
export const onBoardingStyles = css`
h1 {
text-align: center;
font-weight: 400;
font-size: 28px;
line-height: 36px;
}
p {
font-size: 1rem;
line-height: 1.5rem;
text-align: center;
}
.footer {
margin-top: 16px;
text-align: right;
}
`;

View File

@@ -1,21 +1,19 @@
import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-textfield";
import {
Action,
CountRepeat,
RepeatAction,
UntilRepeat,
WhileRepeat,
} from "../../../../../data/script";
import { RepeatAction } from "../../../../../data/script";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import type { Condition } from "../../../../lovelace/common/validate-condition";
import "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row";
const OPTIONS = ["count", "while", "until"] as const;
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
const OPTIONS = ["count", "while", "until", "for_each"] as const;
const getType = (action) => OPTIONS.find((option) => option in action);
@@ -33,144 +31,115 @@ export class HaRepeatAction extends LitElement implements ActionElement {
return { repeat: { count: 2, sequence: [] } };
}
private _schema = memoizeOne(
(localize: LocalizeFunc, type: string, reOrderMode: boolean) =>
[
{
name: "type",
selector: {
select: {
mode: "dropdown",
options: OPTIONS.map((opt) => ({
value: opt,
label: localize(
`ui.panel.config.automation.editor.actions.type.repeat.type.${opt}.label`
),
})),
},
},
},
...(type === "count"
? ([
{
name: "count",
required: true,
selector: { number: { mode: "box", min: 1 } },
},
] as const)
: []),
...(type === "until" || type === "while"
? ([
{
name: type,
selector: {
condition: { nested: true, reorder_mode: reOrderMode },
},
},
] as const)
: []),
...(type === "for_each"
? ([
{
name: "for_each",
required: true,
selector: { object: {} },
},
] as const)
: []),
{
name: "sequence",
selector: { action: { nested: true, reorder_mode: reOrderMode } },
},
] as const
);
protected render() {
const action = this.action.repeat;
const type = getType(action);
return html`
<ha-select
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type_select"
)}
.value=${type}
.disabled=${this.disabled}
@selected=${this._typeChanged}
>
${OPTIONS.map(
(opt) => html`
<mwc-list-item .value=${opt}>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.repeat.type.${opt}.label`
)}
</mwc-list-item>
`
)}
</ha-select>
<div>
${type === "count"
? html`
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type.count.label"
)}
name="count"
.value=${(action as CountRepeat).count || "0"}
.disabled=${this.disabled}
@change=${this._countChanged}
></ha-textfield>
`
: type === "while"
? html` <h3>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.type.repeat.type.while.conditions`
)}:
</h3>
<ha-automation-condition
nested
.conditions=${(action as WhileRepeat).while || []}
.hass=${this.hass}
.disabled=${this.disabled}
@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
nested
.conditions=${(action as UntilRepeat).until || []}
.hass=${this.hass}
.disabled=${this.disabled}
@value-changed=${this._conditionChanged}
></ha-automation-condition>`
: ""}
</div>
<h3>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.sequence"
)}:
</h3>
<ha-automation-action
nested
.actions=${action.sequence}
.reOrderMode=${this.reOrderMode}
.disabled=${this.disabled}
@value-changed=${this._actionChanged}
.hass=${this.hass}
></ha-automation-action>
`;
const schema = this._schema(
this.hass.localize,
type ?? "count",
this.reOrderMode
);
const data = { ...action, type };
return html` <ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
.computeLabel=${this._computeLabelCallback}
></ha-form>`;
}
private _typeChanged(ev) {
const type = ev.target.value;
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const newVal = ev.detail.value;
if (!type || type === getType(this.action.repeat)) {
return;
const newType = newVal.type;
delete newVal.type;
const oldType = getType(this.action.repeat);
if (newType !== oldType) {
if (newType === "count") {
newVal.count = 2;
delete newVal.while;
delete newVal.until;
delete newVal.for_each;
}
if (newType === "while") {
newVal.while = newVal.until ?? [];
delete newVal.count;
delete newVal.until;
delete newVal.for_each;
}
if (newType === "until") {
newVal.until = newVal.while ?? [];
delete newVal.count;
delete newVal.while;
delete newVal.for_each;
}
if (newType === "for_each") {
newVal.for_each = {};
delete newVal.count;
delete newVal.while;
delete newVal.until;
}
}
const value = type === "count" ? 2 : [];
fireEvent(this, "value-changed", {
value: {
...this.action,
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: {
...this.action,
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: {
...this.action,
repeat: {
...this.action.repeat,
sequence: value,
},
},
});
}
private _countChanged(ev: CustomEvent): void {
const newVal = (ev.target as any).value;
if ((this.action.repeat as CountRepeat).count === newVal) {
return;
}
fireEvent(this, "value-changed", {
value: {
...this.action,
repeat: {
...this.action.repeat,
count: newVal,
},
repeat: { ...newVal },
},
});
}
@@ -185,6 +154,46 @@ export class HaRepeatAction extends LitElement implements ActionElement {
`,
];
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
switch (schema.name) {
case "type":
return this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type_select"
);
case "count":
return this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type.count.label"
);
case "while":
return (
this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type.while.conditions"
) + ":"
);
case "until":
return (
this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type.until.conditions"
) + ":"
);
case "for_each":
return (
this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type.for_each.items"
) + ":"
);
case "sequence":
return (
this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.sequence"
) + ":"
);
}
return "";
};
}
declare global {

View File

@@ -28,7 +28,6 @@ import { computeStateDomain } from "../../../common/entity/compute_state_domain"
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stringCompare } from "../../../common/string/compare";
import { slugify } from "../../../common/string/slugify";
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
import { groupBy } from "../../../common/util/group-by";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-alert";
@@ -329,11 +328,11 @@ export class HaConfigDevicePage extends LitElement {
const entitiesByCategory = this._entitiesByCategory(entities);
const batteryEntity = this._batteryEntity(entities);
const batteryChargingEntity = this._batteryChargingEntity(entities);
const batteryState = batteryEntity
const battery = batteryEntity
? this.hass.states[batteryEntity.entity_id]
: undefined;
const batteryIsBinary =
batteryState && computeStateDomain(batteryState) === "binary_sensor";
const batteryDomain = battery ? computeStateDomain(battery) : undefined;
const batteryChargingState = batteryChargingEntity
? this.hass.states[batteryChargingEntity.entity_id]
: undefined;
@@ -712,17 +711,17 @@ export class HaConfigDevicePage extends LitElement {
}
<div class="header-right">
${
batteryState
battery &&
(batteryDomain === "binary_sensor" ||
!isNaN(battery.state as any))
? html`
<div class="battery">
${batteryIsBinary
? ""
: batteryState.state +
blankBeforePercent(this.hass.locale) +
"%"}
${batteryDomain === "sensor"
? this.hass.formatEntityState(battery)
: nothing}
<ha-battery-icon
.hass=${this.hass!}
.batteryStateObj=${batteryState}
.hass=${this.hass}
.batteryStateObj=${battery}
.batteryChargingStateObj=${batteryChargingState}
></ha-battery-icon>
</div>

View File

@@ -1,7 +1,14 @@
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { mdiCancel, mdiFilterVariant, mdiPlus } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import {
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { HASSDomEvent } from "../../../common/dom/fire_event";
@@ -11,7 +18,6 @@ import {
PROTOCOL_INTEGRATIONS,
} from "../../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../../common/navigate";
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
import { LocalizeFunc } from "../../../common/translations/localize";
import { computeRTL } from "../../../common/util/compute_rtl";
import {
@@ -374,22 +380,22 @@ export class HaConfigDeviceDashboard extends LitElement {
batteryEntityPair && batteryEntityPair[0]
? this.hass.states[batteryEntityPair[0]]
: undefined;
const batteryDomain = battery
? computeStateDomain(battery)
: undefined;
const batteryCharging =
batteryEntityPair && batteryEntityPair[1]
? this.hass.states[batteryEntityPair[1]]
: undefined;
const batteryIsBinary =
battery && computeStateDomain(battery) === "binary_sensor";
return battery && (batteryIsBinary || !isNaN(battery.state as any))
return battery &&
(batteryDomain === "binary_sensor" || !isNaN(battery.state as any))
? html`
${batteryIsBinary
? ""
: Number(battery.state).toFixed() +
blankBeforePercent(this.hass.locale) +
"%"}
${batteryDomain === "sensor"
? this.hass.formatEntityState(battery)
: nothing}
<ha-battery-icon
.hass=${this.hass!}
.hass=${this.hass}
.batteryStateObj=${battery}
.batteryChargingStateObj=${batteryCharging}
></ha-battery-icon>

View File

@@ -1,10 +1,14 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list-item";
import { mdiDelete } from "@mdi/js";
import "@material/mwc-list/mwc-list";
import { mdiDelete, mdiDrag } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import type { SortableEvent } from "sortablejs";
import { sortableStyles } from "../../../../resources/ha-sortable-style";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item";
import "../../../../components/ha-icon-picker";
import "../../../../components/ha-textfield";
import type { HaTextField } from "../../../../components/ha-textfield";
@@ -12,6 +16,10 @@ import type { InputSelect } from "../../../../data/input_select";
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import {
loadSortable,
SortableInstance,
} from "../../../../resources/sortable.ondemand";
@customElement("ha-input_select-form")
class HaInputSelectForm extends LitElement {
@@ -27,8 +35,59 @@ class HaInputSelectForm extends LitElement {
@state() private _options: string[] = [];
private _sortable?: SortableInstance;
@query("#option_input", true) private _optionInput?: HaTextField;
public connectedCallback() {
super.connectedCallback();
this._createSortable();
}
public disconnectedCallback() {
super.disconnectedCallback();
this._destroySortable();
}
private async _createSortable() {
const Sortable = await loadSortable();
this._sortable = new Sortable(this.shadowRoot!.querySelector(".options")!, {
animation: 150,
fallbackClass: "sortable-fallback",
handle: ".handle",
onChoose: (evt: SortableEvent) => {
(evt.item as any).placeholder =
document.createComment("sort-placeholder");
evt.item.after((evt.item as any).placeholder);
},
onEnd: (evt: SortableEvent) => {
// put back in original location
if ((evt.item as any).placeholder) {
(evt.item as any).placeholder.replaceWith(evt.item);
delete (evt.item as any).placeholder;
}
this._dragged(evt);
},
});
}
private _dragged(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) return;
const options = this._options.concat();
const option = options.splice(ev.oldIndex!, 1)[0];
options.splice(ev.newIndex!, 0, option);
fireEvent(this, "value-changed", {
value: { ...this._item, options },
});
}
private _destroySortable() {
this._sortable?.destroy();
this._sortable = undefined;
}
set item(item: InputSelect) {
this._item = item;
if (item) {
@@ -86,30 +145,39 @@ class HaInputSelectForm extends LitElement {
"ui.dialogs.helper_settings.input_select.options"
)}:
</div>
${this._options.length
? this._options.map(
(option, index) => html`
<mwc-list-item class="option" hasMeta>
${option}
<ha-icon-button
slot="meta"
.index=${index}
.label=${this.hass.localize(
"ui.dialogs.helper_settings.input_select.remove_option"
)}
@click=${this._removeOption}
.path=${mdiDelete}
></ha-icon-button>
</mwc-list-item>
`
)
: html`
<mwc-list-item noninteractive>
${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.no_options"
)}
</mwc-list-item>
`}
<mwc-list class="options">
${this._options.length
? repeat(
this._options,
(option) => option,
(option, index) => html`
<ha-list-item class="option" hasMeta>
<div class="optioncontent">
<div class="handle">
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
${option}
</div>
<ha-icon-button
slot="meta"
.index=${index}
.label=${this.hass.localize(
"ui.dialogs.helper_settings.input_select.remove_option"
)}
@click=${this._removeOption}
.path=${mdiDelete}
></ha-icon-button>
</ha-list-item>
`
)
: html`
<ha-list-item noninteractive>
${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.no_options"
)}
</ha-list-item>
`}
</mwc-list>
<div class="layout horizontal center">
<ha-textfield
class="flex-auto"
@@ -119,10 +187,10 @@ class HaInputSelectForm extends LitElement {
)}
@keydown=${this._handleKeyAdd}
></ha-textfield>
<mwc-button @click=${this._addOption}
<ha-button @click=${this._addOption}
>${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.add"
)}</mwc-button
)}</ha-button
>
</div>
</div>
@@ -190,6 +258,7 @@ class HaInputSelectForm extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
sortableStyles,
css`
.form {
color: var(--primary-text-color);
@@ -199,6 +268,10 @@ class HaInputSelectForm extends LitElement {
border-radius: 4px;
margin-top: 4px;
--mdc-icon-button-size: 24px;
--mdc-ripple-color: transparent;
--mdc-list-side-padding: 16px;
cursor: default;
background-color: var(--card-background-color);
}
mwc-button {
margin-left: 8px;
@@ -214,6 +287,18 @@ class HaInputSelectForm extends LitElement {
margin-top: 8px;
margin-bottom: 8px;
}
.handle {
cursor: move;
padding-right: 12px;
}
.handle ha-svg-icon {
pointer-events: none;
height: 24px;
}
.optioncontent {
display: flex;
align-items: center;
}
`,
];
}

View File

@@ -9,6 +9,7 @@ import {
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { HassEntity } from "home-assistant-js-websocket";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { alarmPanelIcon } from "../../../common/entity/alarm_panel_icon";
@@ -20,16 +21,48 @@ import type { HaTextField } from "../../../components/ha-textfield";
import {
callAlarmAction,
FORMAT_NUMBER,
ALARM_MODES,
AlarmMode,
} from "../../../data/alarm_control_panel";
import { UNAVAILABLE } from "../../../data/entity";
import type { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entities";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard } from "../types";
import { AlarmPanelCardConfig } from "./types";
import { AlarmPanelCardConfig, AlarmPanelCardConfigState } from "./types";
import { supportsFeature } from "../../../common/entity/supports-feature";
const BUTTONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "clear"];
export const DEFAULT_STATES = [
"arm_home",
"arm_away",
] as AlarmPanelCardConfigState[];
export const ALARM_MODE_STATE_MAP: Record<
AlarmPanelCardConfigState,
AlarmMode
> = {
arm_home: "armed_home",
arm_away: "armed_away",
arm_night: "armed_night",
arm_vacation: "armed_vacation",
arm_custom_bypass: "armed_custom_bypass",
};
export const filterSupportedAlarmStates = (
stateObj: HassEntity | undefined,
states: AlarmPanelCardConfigState[]
): AlarmPanelCardConfigState[] =>
states.filter(
(s) =>
stateObj &&
supportsFeature(
stateObj,
ALARM_MODES[ALARM_MODE_STATE_MAP[s]].feature || 0
)
);
@customElement("hui-alarm-panel-card")
class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
public static async getConfigElement() {
@@ -52,10 +85,13 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
includeDomains
);
const entity = foundEntities[0] || "";
const stateObj = hass.states[entity];
return {
type: "alarm-panel",
states: ["arm_home", "arm_away"],
entity: foundEntities[0] || "",
states: filterSupportedAlarmStates(stateObj, DEFAULT_STATES),
entity,
};
}
@@ -86,11 +122,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
throw new Error("Invalid configuration");
}
const defaults = {
states: ["arm_away", "arm_home"] as const,
};
this._config = { ...defaults, ...config };
this._config = { ...config };
}
protected updated(changedProps: PropertyValues): void {
@@ -138,6 +170,9 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
return nothing;
}
const stateObj = this.hass.states[this._config.entity];
const states =
this._config.states ||
filterSupportedAlarmStates(stateObj, DEFAULT_STATES);
if (!stateObj) {
return html`
@@ -170,7 +205,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
</h1>
<div id="armActions" class="actions">
${(stateObj.state === "disarmed"
? this._config.states!
? states
: (["disarm"] as const)
).map(
(stateAction) => html`

View File

@@ -424,6 +424,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
if (this.hass) {
element.hass = this.hass;
(element as LovelaceTileFeature).stateObj = stateObj;
(element as LovelaceTileFeature).color = this._config!.color;
}
return html`${element}`;

View File

@@ -14,10 +14,17 @@ import { HaDurationData } from "../../../components/ha-duration-input";
import { LovelaceTileFeatureConfig } from "../tile-features/types";
import { ForecastType } from "../../../data/weather";
export type AlarmPanelCardConfigState =
| "arm_away"
| "arm_home"
| "arm_night"
| "arm_vacation"
| "arm_custom_bypass";
export interface AlarmPanelCardConfig extends LovelaceCardConfig {
entity: string;
name?: string;
states?: readonly (keyof TranslationDict["ui"]["card"]["alarm_control_panel"])[];
states?: AlarmPanelCardConfigState[];
theme?: string;
}

View File

@@ -37,6 +37,8 @@ const HIDE_DOMAIN = new Set([
"sun",
"zone",
"event",
"tts",
"stt",
]);
const HIDE_PLATFORM = new Set(["mobile_app"]);

View File

@@ -1,14 +1,15 @@
import "../tile-features/hui-alarm-modes-tile-feature";
import "../tile-features/hui-climate-hvac-modes-tile-feature";
import "../tile-features/hui-target-temperature-tile-feature";
import "../tile-features/hui-cover-open-close-tile-feature";
import "../tile-features/hui-cover-position-tile-feature";
import "../tile-features/hui-cover-tilt-position-tile-feature";
import "../tile-features/hui-cover-tilt-tile-feature";
import "../tile-features/hui-fan-speed-tile-feature";
import "../tile-features/hui-lawn-mower-commands-tile-feature";
import "../tile-features/hui-light-brightness-tile-feature";
import "../tile-features/hui-light-color-temp-tile-feature";
import "../tile-features/hui-vacuum-commands-tile-feature";
import "../tile-features/hui-lawn-mower-commands-tile-feature";
import "../tile-features/hui-water-heater-operation-modes-tile-feature";
import { LovelaceTileFeatureConfig } from "../tile-features/types";
import {
@@ -17,17 +18,18 @@ import {
} from "./create-element-base";
const TYPES: Set<LovelaceTileFeatureConfig["type"]> = new Set([
"cover-open-close",
"cover-position",
"cover-tilt",
"cover-tilt-position",
"light-brightness",
"light-color-temp",
"vacuum-commands",
"lawn-mower-commands",
"fan-speed",
"alarm-modes",
"climate-hvac-modes",
"cover-open-close",
"cover-position",
"cover-tilt-position",
"cover-tilt",
"fan-speed",
"lawn-mower-commands",
"light-brightness",
"light-color-temp",
"target-temperature",
"vacuum-commands",
"water-heater-operation-modes",
]);

View File

@@ -2,14 +2,25 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { array, assert, assign, object, optional, string } from "superstruct";
import { HassEntity } from "home-assistant-js-websocket";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { AlarmPanelCardConfig } from "../../cards/types";
import type {
AlarmPanelCardConfig,
AlarmPanelCardConfigState,
} from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import {
DEFAULT_STATES,
ALARM_MODE_STATE_MAP,
filterSupportedAlarmStates,
} from "../../cards/hui-alarm-panel-card";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import { ALARM_MODES } from "../../../../data/alarm_control_panel";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -21,13 +32,7 @@ const cardConfigStruct = assign(
})
);
const states = [
"arm_home",
"arm_away",
"arm_night",
"arm_vacation",
"arm_custom_bypass",
] as const;
const states = Object.keys(ALARM_MODE_STATE_MAP) as AlarmPanelCardConfigState[];
@customElement("hui-alarm-panel-card-editor")
export class HuiAlarmPanelCardEditor
@@ -44,7 +49,11 @@ export class HuiAlarmPanelCardEditor
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
(
localize: LocalizeFunc,
stateObj: HassEntity | undefined,
config_states: AlarmPanelCardConfigState[]
) =>
[
{
name: "entity",
@@ -60,12 +69,24 @@ export class HuiAlarmPanelCardEditor
],
},
{
type: "multi_select",
name: "states",
options: states.map((s) => [
s,
localize(`ui.card.alarm_control_panel.${s}`),
]) as [string, string][],
selector: {
select: {
multiple: true,
mode: "list",
options: states.map((s) => ({
value: s,
label: localize(`ui.card.alarm_control_panel.${s}`),
disabled:
!config_states.includes(s) &&
(!stateObj ||
!supportsFeature(
stateObj,
ALARM_MODES[ALARM_MODE_STATE_MAP[s]].feature || 0
)),
})),
},
},
},
] as const
);
@@ -75,11 +96,18 @@ export class HuiAlarmPanelCardEditor
return nothing;
}
const stateObj = this.hass.states[this._config.entity];
const defaultFilteredStates = filterSupportedAlarmStates(
stateObj,
DEFAULT_STATES
);
const config = { states: defaultFilteredStates, ...this._config };
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${this._schema(this.hass.localize)}
.data=${config}
.schema=${this._schema(this.hass.localize, stateObj, config.states)}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
@@ -87,7 +115,26 @@ export class HuiAlarmPanelCardEditor
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
const newConfig = ev.detail.value;
// Sort states in a consistent order
if (newConfig.states) {
const sortStates = states.filter((s) => newConfig.states.includes(s));
newConfig.states = sortStates;
}
// When changing entities, clear any states that the new entity does not support
if (newConfig.states && newConfig.entity !== this._config?.entity) {
const newStateObj = this.hass?.states[newConfig.entity];
if (newStateObj) {
newConfig.states = filterSupportedAlarmStates(
newStateObj,
newConfig.states
);
}
}
fireEvent(this, "config-changed", { config: newConfig });
}
private _computeLabelCallback = (

View File

@@ -37,6 +37,7 @@ import { supportsLightColorTempTileFeature } from "../../tile-features/hui-light
import { supportsVacuumCommandTileFeature } from "../../tile-features/hui-vacuum-commands-tile-feature";
import { supportsWaterHeaterOperationModesTileFeature } from "../../tile-features/hui-water-heater-operation-modes-tile-feature";
import { LovelaceTileFeatureConfig } from "../../tile-features/types";
import { supportsTargetTemperatureTileFeature } from "../../tile-features/hui-target-temperature-tile-feature";
type FeatureType = LovelaceTileFeatureConfig["type"];
type SupportsFeature = (stateObj: HassEntity) => boolean;
@@ -44,6 +45,7 @@ type SupportsFeature = (stateObj: HassEntity) => boolean;
const FEATURE_TYPES: FeatureType[] = [
"alarm-modes",
"climate-hvac-modes",
"target-temperature",
"cover-open-close",
"cover-position",
"cover-tilt-position",
@@ -76,6 +78,7 @@ const SUPPORTS_FEATURE_TYPES: Record<FeatureType, SupportsFeature | undefined> =
"lawn-mower-commands": supportsLawnMowerCommandTileFeature,
"light-brightness": supportsLightBrightnessTileFeature,
"light-color-temp": supportsLightColorTempTileFeature,
"target-temperature": supportsTargetTemperatureTileFeature,
"vacuum-commands": supportsVacuumCommandTileFeature,
"water-heater-operation-modes":
supportsWaterHeaterOperationModesTileFeature,
@@ -151,8 +154,10 @@ export class HuiTileCardFeaturesEditor extends LitElement {
const customFeatureEntry = CUSTOM_FEATURE_ENTRIES[customType];
return customFeatureEntry?.name || type;
}
return this.hass!.localize(
`ui.panel.lovelace.editor.card.tile.features.types.${type}.label`
return (
this.hass!.localize(
`ui.panel.lovelace.editor.card.tile.features.types.${type}.label`
) || type
);
}

View File

@@ -324,8 +324,6 @@ export class LovelacePanel extends LitElement {
urlPath: this.urlPath,
editMode: this.lovelace ? this.lovelace.editMode : false,
locale: this.hass!.locale,
configHistory: this.lovelace ? this.lovelace.configHistory : [config],
configHistoryIndex: this.lovelace ? this.lovelace.configHistoryIndex : 0,
enableFullEditMode: () => {
if (!editorLoaded) {
editorLoaded = true;
@@ -345,11 +343,7 @@ export class LovelacePanel extends LitElement {
}
if (!editMode || this.lovelace!.mode !== "generated") {
this._updateLovelace({
editMode,
configHistory: [this.lovelace!.config],
configHistoryIndex: 0,
});
this._updateLovelace({ editMode });
return;
}
@@ -364,8 +358,6 @@ export class LovelacePanel extends LitElement {
config: previousConfig,
rawConfig: previousRawConfig,
mode: previousMode,
configHistory: previousConfigHistory,
configHistoryIndex: previousConfigHistoryIndex,
} = this.lovelace!;
newConfig = this._checkLovelaceConfig(newConfig);
let conf: LovelaceConfig;
@@ -380,22 +372,11 @@ export class LovelacePanel extends LitElement {
conf = newConfig;
}
try {
const newConfigHistory = [...previousConfigHistory];
let newConfigHistoryIndex = previousConfigHistoryIndex;
if (previousConfigHistoryIndex !== 0) {
newConfigHistory.splice(0, previousConfigHistoryIndex);
newConfigHistoryIndex = 0;
}
newConfigHistory.unshift(newConfig);
// Optimistic update
this._updateLovelace({
config: conf,
rawConfig: newConfig,
mode: "storage",
configHistory: newConfigHistory,
configHistoryIndex: newConfigHistoryIndex,
});
this._ignoreNextUpdateEvent = true;
await saveConfig(this.hass!, urlPath, newConfig);
@@ -407,7 +388,6 @@ export class LovelacePanel extends LitElement {
config: previousConfig,
rawConfig: previousRawConfig,
mode: previousMode,
configHistory: previousConfigHistory,
});
throw err;
}
@@ -447,57 +427,6 @@ export class LovelacePanel extends LitElement {
throw err;
}
},
restoreConfigFromHistory: async (
configHistoryIndex: number
): Promise<void> => {
const {
configHistory,
configHistoryIndex: previousConfigHistoryIndex,
config: previousConfig,
rawConfig: previousRawConfig,
} = this.lovelace!;
if (
previousConfigHistoryIndex === configHistoryIndex ||
configHistoryIndex < 0 ||
configHistoryIndex >= configHistory.length
) {
return;
}
let conf: LovelaceConfig;
const newConfig = configHistory[configHistoryIndex];
if (newConfig.strategy) {
conf = await generateLovelaceDashboardStrategy({
config: newConfig,
hass: this.hass!,
narrow: this.narrow,
});
} else {
conf = newConfig;
}
try {
// Optimistic update
this._updateLovelace({
config: conf,
rawConfig: newConfig,
configHistoryIndex: configHistoryIndex,
});
this._ignoreNextUpdateEvent = true;
await saveConfig(this.hass!, urlPath, newConfig);
} catch (err: any) {
// eslint-disable-next-line
console.error(err);
// Rollback the optimistic update
this._updateLovelace({
config: previousConfig,
rawConfig: previousRawConfig,
configHistoryIndex: previousConfigHistoryIndex,
});
throw err;
}
},
};
}

View File

@@ -12,10 +12,8 @@ import {
mdiMagnify,
mdiPencil,
mdiPlus,
mdiRedo,
mdiRefresh,
mdiShape,
mdiUndo,
mdiViewDashboard,
} from "@mdi/js";
import "@polymer/paper-tabs/paper-tab";
@@ -108,31 +106,12 @@ class HUIRoot extends LitElement {
);
}
private _undo() {
if (this.lovelace) {
this.lovelace.restoreConfigFromHistory(
this.lovelace.configHistoryIndex + 1
);
}
}
private _redo() {
if (this.lovelace) {
this.lovelace.restoreConfigFromHistory(
this.lovelace.configHistoryIndex - 1
);
}
}
protected render(): TemplateResult {
const views = this.lovelace?.config.views ?? [];
const curViewConfig =
typeof this._curView === "number" ? views[this._curView] : undefined;
const historyTotal = this.lovelace?.configHistory.length ?? 0 - 1;
const historyIndex = this.lovelace?.configHistoryIndex ?? 0;
return html`
<div
class=${classMap({
@@ -165,18 +144,6 @@ class HUIRoot extends LitElement {
)}
@click=${this._editModeDisable}
></mwc-button>
<ha-icon-button
label="Undo"
.path=${mdiUndo}
.disabled=${historyIndex + 1 >= historyTotal}
@click=${this._undo}
></ha-icon-button>
<ha-icon-button
label="Redo"
.path=${mdiRedo}
.disabled=${historyIndex === 0}
@click=${this._redo}
></ha-icon-button>
<a
href=${documentationUrl(this.hass, "/dashboards/")}
rel="noreferrer"

View File

@@ -1,15 +1,18 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { CoverEntityFeature } from "../../../data/cover";
import { UNAVAILABLE } from "../../../data/entity";
import { HomeAssistant } from "../../../types";
import { LovelaceTileFeature } from "../types";
import { CoverPositionTileFeatureConfig } from "./types";
import { stateActive } from "../../../common/entity/state_active";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
import { UNAVAILABLE } from "../../../data/entity";
export const supportsCoverPositionTileFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
@@ -28,6 +31,8 @@ class HuiCoverPositionTileFeature
@property({ attribute: false }) public stateObj?: HassEntity;
@property({ attribute: false }) public color?: string;
@state() private _config?: CoverPositionTileFeatureConfig;
static getStubConfig(): CoverPositionTileFeatureConfig {
@@ -59,24 +64,36 @@ class HuiCoverPositionTileFeature
const value = Math.max(Math.round(percentage), 0);
return html` <div class="container">
<ha-control-slider
.value=${value}
min="0"
max="100"
step="1"
inverted
show-handle
@value-changed=${this._valueChanged}
.ariaLabel=${computeAttributeNameDisplay(
this.hass.localize,
this.stateObj,
this.hass.entities,
"current_position"
)}
.disabled=${this.stateObj!.state === UNAVAILABLE}
></ha-control-slider>
</div>`;
const forcedState = this.stateObj.state === "closed" ? "open" : undefined;
const color = this.color
? computeCssColor(this.color)
: stateColorCss(this.stateObj, forcedState);
const style = {
"--color": color,
};
return html`
<div class="container" style=${styleMap(style)}>
<ha-control-slider
.value=${value}
min="0"
max="100"
step="1"
inverted
show-handle
@value-changed=${this._valueChanged}
.ariaLabel=${computeAttributeNameDisplay(
this.hass.localize,
this.stateObj,
this.hass.entities,
"current_position"
)}
.disabled=${this.stateObj!.state === UNAVAILABLE}
></ha-control-slider>
</div>
`;
}
private _valueChanged(ev: CustomEvent) {
@@ -92,9 +109,8 @@ class HuiCoverPositionTileFeature
static get styles() {
return css`
ha-control-slider {
/* Force inactive state to be colored for the slider */
--control-slider-color: var(--tile-color);
--control-slider-background: var(--tile-color);
--control-slider-color: var(--color);
--control-slider-background: var(--color);
--control-slider-background-opacity: 0.2;
--control-slider-thickness: 40px;
--control-slider-border-radius: 10px;

View File

@@ -1,8 +1,11 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { CoverEntity, CoverEntityFeature } from "../../../data/cover";
import { UNAVAILABLE } from "../../../data/entity";
@@ -30,6 +33,8 @@ class HuiCoverTiltPositionTileFeature
@property({ attribute: false }) public stateObj?: CoverEntity;
@property({ attribute: false }) public color?: string;
@state() private _config?: CoverTiltPositionTileFeatureConfig;
static getStubConfig(): CoverTiltPositionTileFeatureConfig {
@@ -59,8 +64,18 @@ class HuiCoverTiltPositionTileFeature
const value = Math.max(Math.round(percentage), 0);
const forcedState = this.stateObj.state === "closed" ? "open" : undefined;
const color = this.color
? computeCssColor(this.color)
: stateColorCss(this.stateObj, forcedState);
const style = {
"--color": color,
};
return html`
<div class="container">
<div class="container" style=${styleMap(style)}>
<ha-control-slider
.value=${value}
min="0"
@@ -96,8 +111,8 @@ class HuiCoverTiltPositionTileFeature
return css`
ha-control-slider {
/* Force inactive state to be colored for the slider */
--control-slider-color: var(--tile-color);
--control-slider-background: var(--tile-color);
--control-slider-color: var(--color);
--control-slider-background: var(--color);
--control-slider-background-opacity: 0.2;
--control-slider-thickness: 40px;
--control-slider-border-radius: 10px;

View File

@@ -0,0 +1,254 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { UNIT_F } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { stateColorCss } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-control-button-group";
import "../../../components/ha-control-number-buttons";
import { ClimateEntity, ClimateEntityFeature } from "../../../data/climate";
import { UNAVAILABLE } from "../../../data/entity";
import {
WaterHeaterEntity,
WaterHeaterEntityFeature,
} from "../../../data/water_heater";
import { HomeAssistant } from "../../../types";
import { LovelaceTileFeature } from "../types";
import { TargetTemperatureTileFeatureConfig } from "./types";
type Target = "value" | "low" | "high";
export const supportsTargetTemperatureTileFeature = (stateObj: HassEntity) => {
const domain = computeDomain(stateObj.entity_id);
return (
(domain === "climate" &&
(supportsFeature(stateObj, ClimateEntityFeature.TARGET_TEMPERATURE) ||
supportsFeature(
stateObj,
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
))) ||
(domain === "water_heater" &&
supportsFeature(stateObj, WaterHeaterEntityFeature.TARGET_TEMPERATURE))
);
};
@customElement("hui-target-temperature-tile-feature")
class HuiTargetTemperatureTileFeature
extends LitElement
implements LovelaceTileFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?:
| ClimateEntity
| WaterHeaterEntity;
@state() private _config?: TargetTemperatureTileFeatureConfig;
@state() private _targetTemperature: Partial<Record<Target, number>> = {};
static getStubConfig(): TargetTemperatureTileFeatureConfig {
return {
type: "target-temperature",
};
}
public setConfig(config: TargetTemperatureTileFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
protected willUpdate(changedProp: PropertyValues): void {
super.willUpdate(changedProp);
if (changedProp.has("stateObj")) {
this._targetTemperature = {
value: this.stateObj!.attributes.temperature,
low:
"target_temp_low" in this.stateObj!.attributes
? this.stateObj!.attributes.target_temp_low
: undefined,
high:
"target_temp_high" in this.stateObj!.attributes
? this.stateObj!.attributes.target_temp_high
: undefined,
};
}
}
private get _step() {
return (
this.stateObj!.attributes.target_temp_step ||
(this.hass!.config.unit_system.temperature === UNIT_F ? 1 : 0.5)
);
}
private get _min() {
return this.stateObj!.attributes.min_temp;
}
private get _max() {
return this.stateObj!.attributes.max_temp;
}
private async _valueChanged(ev: CustomEvent) {
const value = (ev.detail as any).value;
if (isNaN(value)) return;
const target = (ev.currentTarget as any).target ?? "value";
this._targetTemperature = {
...this._targetTemperature,
[target]: value,
};
this._debouncedCallService(target);
}
private _debouncedCallService = debounce(
(target: Target) => this._callService(target),
1000
);
private _callService(type: string) {
const domain = computeStateDomain(this.stateObj!);
if (type === "high" || type === "low") {
this.hass!.callService(domain, "set_temperature", {
entity_id: this.stateObj!.entity_id,
target_temp_low: this._targetTemperature.low,
target_temp_high: this._targetTemperature.high,
});
return;
}
this.hass!.callService(domain, "set_temperature", {
entity_id: this.stateObj!.entity_id,
temperature: this._targetTemperature.value,
});
}
protected render() {
if (
!this._config ||
!this.hass ||
!this.stateObj ||
!supportsTargetTemperatureTileFeature(this.stateObj)
) {
return nothing;
}
const stateColor = stateColorCss(this.stateObj);
const digits = this._step.toString().split(".")?.[1]?.length ?? 0;
const options = {
maximumFractionDigits: digits,
minimumFractionDigits: digits,
};
const domain = computeStateDomain(this.stateObj!);
if (
(domain === "climate" &&
supportsFeature(
this.stateObj,
ClimateEntityFeature.TARGET_TEMPERATURE
)) ||
(domain === "water_heater" &&
supportsFeature(
this.stateObj,
WaterHeaterEntityFeature.TARGET_TEMPERATURE
))
) {
return html`
<ha-control-button-group>
<ha-control-number-buttons
.formatOptions=${options}
.target="value"
.value=${this.stateObj.attributes.temperature}
.min=${this._min}
.max=${this._max}
.step=${this._step}
@value-changed=${this._valueChanged}
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"temperature"
)}
style=${styleMap({
"--control-number-buttons-focus-color": stateColor,
})}
.disabled=${this.stateObj!.state === UNAVAILABLE}
>
</ha-control-number-buttons>
</ha-control-number-buttons>
`;
}
if (
domain === "climate" &&
supportsFeature(
this.stateObj,
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
)
) {
return html`
<ha-control-button-group>
<ha-control-number-buttons
.formatOptions=${options}
.target=${"low"}
.value=${(this.stateObj as ClimateEntity).attributes.target_temp_low}
.min=${this._min}
.max=${Math.min(this._max, this._targetTemperature.high ?? this._max)}
.step=${this._step}
@value-changed=${this._valueChanged}
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"temperature"
)}
style=${styleMap({
"--control-number-buttons-focus-color": stateColor,
})}
.disabled=${this.stateObj!.state === UNAVAILABLE}
>
</ha-control-number-buttons>
<ha-control-number-buttons
.formatOptions=${options}
.target=${"high"}
.value=${(this.stateObj as ClimateEntity).attributes.target_temp_high}
.min=${Math.max(this._min, this._targetTemperature.low ?? this._min)}
.max=${this._max}
.step=${this._step}
@value-changed=${this._valueChanged}
.label=${this.hass.formatEntityAttributeName(
this.stateObj,
"temperature"
)}
style=${styleMap({
"--control-number-buttons-focus-color": stateColor,
})}
.disabled=${this.stateObj!.state === UNAVAILABLE}
>
</ha-control-number-buttons>
</ha-control-number-buttons>
`;
}
return nothing;
}
static get styles() {
return css`
ha-control-button-group {
margin: 0 12px 12px 12px;
--control-button-group-spacing: 12px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-target-temperature-tile-feature": HuiTargetTemperatureTileFeature;
}
}

View File

@@ -40,6 +40,10 @@ export interface ClimateHvacModesTileFeatureConfig {
hvac_modes?: HvacMode[];
}
export interface TargetTemperatureTileFeatureConfig {
type: "target-temperature";
}
export interface WaterHeaterOperationModesTileFeatureConfig {
type: "water-heater-operation-modes";
operation_modes?: OperationMode[];
@@ -81,6 +85,7 @@ export type LovelaceTileFeatureConfig =
| LightBrightnessTileFeatureConfig
| LightColorTempTileFeatureConfig
| VacuumCommandsTileFeatureConfig
| TargetTemperatureTileFeatureConfig
| WaterHeaterOperationModesTileFeatureConfig;
export type LovelaceTileFeatureContext = {

View File

@@ -32,9 +32,6 @@ export interface Lovelace {
setEditMode: (editMode: boolean) => void;
saveConfig: (newConfig: LovelaceConfig) => Promise<void>;
deleteConfig: () => Promise<void>;
configHistory: LovelaceConfig[];
configHistoryIndex: number;
restoreConfigFromHistory: (index: number) => Promise<void>;
}
export interface LovelaceBadge extends HTMLElement {
@@ -119,6 +116,7 @@ export interface LovelaceTileFeature extends HTMLElement {
hass?: HomeAssistant;
stateObj?: HassEntity;
setConfig(config: LovelaceTileFeatureConfig);
color?: string;
}
export interface LovelaceTileFeatureConstructor

View File

@@ -14,6 +14,12 @@ import "../../components/ha-textfield";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../../components/ha-alert";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../dialogs/generic/show-dialog-box";
import { RefreshToken } from "../../data/refresh_token";
import { changePassword, deleteAllRefreshTokens } from "../../data/auth";
@customElement("ha-change-password-card")
class HaChangePasswordCard extends LitElement {
@@ -31,6 +37,8 @@ class HaChangePasswordCard extends LitElement {
@state() private _passwordConfirm = "";
@property({ attribute: false }) public refreshTokens?: RefreshToken[];
protected render(): TemplateResult {
return html`
<ha-card
@@ -148,11 +156,7 @@ class HaChangePasswordCard extends LitElement {
this._errorMsg = undefined;
try {
await this.hass.callWS({
type: "config/auth_provider/homeassistant/change_password",
current_password: this._currentPassword,
new_password: this._password,
});
await changePassword(this.hass, this._currentPassword, this._password);
} catch (err: any) {
this._errorMsg = err.message;
return;
@@ -163,6 +167,33 @@ class HaChangePasswordCard extends LitElement {
this._statusMsg = this.hass.localize(
"ui.panel.profile.change_password.success"
);
if (
this.refreshTokens &&
(await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.profile.change_password.logout_all_sessions"
),
text: this.hass.localize(
"ui.panel.profile.change_password.logout_all_sessions_text"
),
dismissText: this.hass.localize("ui.common.no"),
confirmText: this.hass.localize("ui.common.yes"),
destructive: true,
}))
) {
try {
await deleteAllRefreshTokens(this.hass);
} catch (err: any) {
await showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.profile.change_password.delete_failed"
),
text: err.message,
});
}
}
this._currentPassword = "";
this._password = "";
this._passwordConfirm = "";

View File

@@ -188,6 +188,8 @@ class HaPanelProfile extends LitElement {
)
? html`
<ha-change-password-card
.refreshTokens=${this._refreshTokens}
@hass-refresh-tokens=${this._refreshRefreshTokens}
.hass=${this.hass}
></ha-change-password-card>
`

View File

@@ -38,7 +38,6 @@ export const createLogMessage = async (
.slice(0, MAX_STACK_FRAMES)
.map((frame) => {
frame.fileName ??= "";
// @ts-expect-error canParse not in DOM library yet
if (URL.canParse(frame.fileName)) {
frame.fileName = new URL(frame.fileName).pathname;
}

View File

@@ -2735,6 +2735,10 @@
"until": {
"label": "Until",
"conditions": "Until conditions"
},
"for_each": {
"label": "For each",
"items": "For each item in list"
}
},
"sequence": "Actions",
@@ -5027,6 +5031,9 @@
"label": "Climate HVAC modes",
"hvac_modes": "HVAC modes"
},
"target-temperature": {
"label": "Target temperature"
},
"water-heater-operation-modes": {
"label": "Water heater operation modes",
"operation_modes": "Operation modes"
@@ -5305,7 +5312,10 @@
"submit": "Submit",
"error_new_mismatch": "Entered new password values do not match",
"error_new_is_old": "New password must be different than current password",
"success": "Password changed successfully"
"success": "Password changed successfully",
"logout_all_sessions": "Do you want to log out from all devices?",
"logout_all_sessions_text": "This will require you to log in again on all devices with your new password, including this one.",
"delete_failed": "Not all devices could be logged out"
},
"mfa": {
"header": "Multi-factor authentication modules",
@@ -5677,7 +5687,21 @@
"intro": "Are you ready to awaken your home, reclaim your privacy and join a worldwide community of tinkerers?",
"next": "Next",
"finish": "Finish",
"help": "Help",
"welcome": {
"header": "Welcome!",
"start": "Create my smart home",
"restore_backup": "Restore from backup",
"vision": "Read our vision",
"community": "Join our community",
"download_app": "Download our app",
"forums": "Home Assistant forums",
"open_home_newsletter": "Building the Open Home newsletter",
"discord": "Discord chat",
"twitter": "Twitter"
},
"user": {
"header": "Create user",
"intro": "Let's get started by creating a user account.",
"required_field": "Required",
"data": {
@@ -5686,20 +5710,22 @@
"password": "Password",
"password_confirm": "Confirm password"
},
"helper": {
"password": "Choose a strong and unique password. Make sure to save it, so you don't forget it."
},
"create_account": "Create account",
"error": {
"password_not_match": "Passwords don't match"
}
},
"core-config": {
"intro": "Hello {name}, welcome to Home Assistant. How would you like to name your home?",
"intro_core": "We will set up the basics together. You can always change this later in the settings.",
"intro_location": "Let's set up the location of your home so that you can display information such as the local weather and use sun-based or presence-based automations. This data is never shared outside of your network.",
"location_header": "Home location",
"intro_location": "Let's set up the location of your home so that you can display information such as the local weather and use sun-based or presence-based automations.",
"location_address": "Powered by {openstreetmap} ({osm_privacy_policy}).",
"osm_privacy_policy": "Privacy policy",
"title_location_detect": "Do you want us to detect your location?",
"intro_location_detect": "We can detect your location by making a one-time request to an external service.",
"intro_core_config": "We filled out some details about your location. Please check if they are correct and continue.",
"country_intro": "We would like to know the country your home is in, so we can use the correct units for you.",
"location_name": "Name of your Home Assistant installation",
"location_name_default": "Home",
"address_label": "Search address",
@@ -5707,11 +5733,13 @@
"finish": "Next"
},
"integration": {
"intro": "Devices and services are represented in Home Assistant as integrations. You can set them up now, or do it later from the settings.",
"more_integrations": "More",
"header": "We found compatible devices!",
"intro": "These are found on your local network. Some are already added, others may need extra configuration.",
"more_integrations": "+{count} more",
"finish": "Finish"
},
"analytics": {
"header": "Help us help you",
"finish": "Next",
"preferences": {
"base": {
@@ -5736,8 +5764,9 @@
"intro": "[%key:ui::panel::config::analytics::intro%]"
},
"restore": {
"description": "Alternatively you can restore from a previous backup.",
"header": "Restore a backup",
"in_progress": "Restore in progress",
"upload_backup": "[%key:supervisor::backup::upload_backup%]",
"show_log": "Show full log",
"hide_log": "Hide full log",
"full_backup": "[%key:supervisor::backup::full_backup%]",
@@ -5749,8 +5778,7 @@
"addons": "[%key:supervisor::backup::addons%]",
"password_protection": "[%key:supervisor::backup::password_protection%]",
"password": "[%key:supervisor::backup::password%]",
"confirm_password": "[%key:supervisor::backup::confirm_password%]",
"upload_backup": "[%key:supervisor::backup::upload_backup%]"
"confirm_password": "[%key:supervisor::backup::confirm_password%]"
}
},
"custom": {
@@ -5843,7 +5871,7 @@
"new_update_available": "{name} {version} is available",
"not_available_arch": "This add-on is not compatible with the processor of your device or the operating system you have installed on your device.",
"not_available_version": "You are running Home Assistant {core_version_installed}, to update to this version of the add-on you need at least version {core_version_needed} of Home Assistant",
"visit_addon_page": "Visit the {name} page for more details",
"visit_addon_page": "Visit the {name} page for more details.",
"restart": "restart",
"start": "start",
"stop": "stop",

472
yarn.lock
View File

@@ -62,26 +62,26 @@ __metadata:
languageName: node
linkType: hard
"@babel/core@npm:7.22.10, @babel/core@npm:^7.11.1, @babel/core@npm:^7.12.3":
version: 7.22.10
resolution: "@babel/core@npm:7.22.10"
"@babel/core@npm:7.22.11, @babel/core@npm:^7.11.1, @babel/core@npm:^7.12.3":
version: 7.22.11
resolution: "@babel/core@npm:7.22.11"
dependencies:
"@ampproject/remapping": ^2.2.0
"@babel/code-frame": ^7.22.10
"@babel/generator": ^7.22.10
"@babel/helper-compilation-targets": ^7.22.10
"@babel/helper-module-transforms": ^7.22.9
"@babel/helpers": ^7.22.10
"@babel/parser": ^7.22.10
"@babel/helpers": ^7.22.11
"@babel/parser": ^7.22.11
"@babel/template": ^7.22.5
"@babel/traverse": ^7.22.10
"@babel/types": ^7.22.10
"@babel/traverse": ^7.22.11
"@babel/types": ^7.22.11
convert-source-map: ^1.7.0
debug: ^4.1.0
gensync: ^1.0.0-beta.2
json5: ^2.2.2
json5: ^2.2.3
semver: ^6.3.1
checksum: cc4efa09209fe1f733cf512e9e4bb50870b191ab2dee8014e34cd6e731f204e48476cc53b4bbd0825d4d342304d577ae43ff5fd8ab3896080673c343321acb32
checksum: f258b2539ea2e5bfe55a708c2f3e1093a1b4744f12becc35abeb896037b66210de9a8ad6296a706046d5dc3a24e564362b73a9b814e5bfe500c8baab60c22d2e
languageName: node
linkType: hard
@@ -128,9 +128,9 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-create-class-features-plugin@npm:^7.22.10, @babel/helper-create-class-features-plugin@npm:^7.22.5":
version: 7.22.10
resolution: "@babel/helper-create-class-features-plugin@npm:7.22.10"
"@babel/helper-create-class-features-plugin@npm:^7.22.10, @babel/helper-create-class-features-plugin@npm:^7.22.11, @babel/helper-create-class-features-plugin@npm:^7.22.5":
version: 7.22.11
resolution: "@babel/helper-create-class-features-plugin@npm:7.22.11"
dependencies:
"@babel/helper-annotate-as-pure": ^7.22.5
"@babel/helper-environment-visitor": ^7.22.5
@@ -143,7 +143,7 @@ __metadata:
semver: ^6.3.1
peerDependencies:
"@babel/core": ^7.0.0
checksum: 9683edbf73889abce183b06eac29524448aaab1dba7bdccdd6c26cf03e5ade3903b581b4d681da88fbff824fa117b840cc945bebf7db3c1f8c745f3c5a8a2595
checksum: b7aeb22e29aba5327616328576363522b3b186918faeda605e300822af4a5f29416eb34b5bd825d07ab496550e271d02d7634f0022a62b5b8cbf0eb6389bc3fa
languageName: node
linkType: hard
@@ -335,14 +335,14 @@ __metadata:
languageName: node
linkType: hard
"@babel/helpers@npm:^7.22.10":
version: 7.22.10
resolution: "@babel/helpers@npm:7.22.10"
"@babel/helpers@npm:^7.22.11":
version: 7.22.11
resolution: "@babel/helpers@npm:7.22.11"
dependencies:
"@babel/template": ^7.22.5
"@babel/traverse": ^7.22.10
"@babel/types": ^7.22.10
checksum: 3b1219e362df390b6c5d94b75a53fc1c2eb42927ced0b8022d6a29b833a839696206b9bdad45b4805d05591df49fc16b6fb7db758c9c2ecfe99e3e94cb13020f
"@babel/traverse": ^7.22.11
"@babel/types": ^7.22.11
checksum: 93186544228b5e371486466ec3b86a77cce91beeff24a5670ca8ec46d50328f7700dab82d532351286e9d68624dc51d6d71589b051dd9535e44be077a43ec013
languageName: node
linkType: hard
@@ -357,12 +357,12 @@ __metadata:
languageName: node
linkType: hard
"@babel/parser@npm:^7.18.4, @babel/parser@npm:^7.22.10, @babel/parser@npm:^7.22.5":
version: 7.22.10
resolution: "@babel/parser@npm:7.22.10"
"@babel/parser@npm:^7.18.4, @babel/parser@npm:^7.22.11, @babel/parser@npm:^7.22.5":
version: 7.22.11
resolution: "@babel/parser@npm:7.22.11"
bin:
parser: ./bin/babel-parser.js
checksum: af51567b7d3cdf523bc608eae057397486c7fa6c2e5753027c01fe5c36f0767b2d01ce3049b222841326cc5b8c7fda1d810ac1a01af0a97bb04679e2ef9f7049
checksum: 332079ed09794d3685343e9fc39c6a12dcb6ea589119f2135952cdef2424296786bb609a33f6dfa9be271797bbf8339f1865118418ea50b32a0c701734c96664
languageName: node
linkType: hard
@@ -914,16 +914,16 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-modules-commonjs@npm:^7.22.5":
version: 7.22.5
resolution: "@babel/plugin-transform-modules-commonjs@npm:7.22.5"
"@babel/plugin-transform-modules-commonjs@npm:^7.22.11, @babel/plugin-transform-modules-commonjs@npm:^7.22.5":
version: 7.22.11
resolution: "@babel/plugin-transform-modules-commonjs@npm:7.22.11"
dependencies:
"@babel/helper-module-transforms": ^7.22.5
"@babel/helper-module-transforms": ^7.22.9
"@babel/helper-plugin-utils": ^7.22.5
"@babel/helper-simple-access": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 2067aca8f6454d54ffcce69b02c457cfa61428e11372f6a1d99ff4fcfbb55c396ed2ca6ca886bf06c852e38c1a205b8095921b2364fd0243f3e66bc1dda61caa
checksum: c15ad7f1234a930cab214224bb85f6b3a3f301fa1d4d15bef193e5c11c614ce369551e5cbb708fde8d3f7e1cb84b05e9798a3647a11b56c3d67580e362a712d4
languageName: node
linkType: hard
@@ -1195,17 +1195,17 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-typescript@npm:^7.22.5":
version: 7.22.10
resolution: "@babel/plugin-transform-typescript@npm:7.22.10"
"@babel/plugin-transform-typescript@npm:^7.22.11":
version: 7.22.11
resolution: "@babel/plugin-transform-typescript@npm:7.22.11"
dependencies:
"@babel/helper-annotate-as-pure": ^7.22.5
"@babel/helper-create-class-features-plugin": ^7.22.10
"@babel/helper-create-class-features-plugin": ^7.22.11
"@babel/helper-plugin-utils": ^7.22.5
"@babel/plugin-syntax-typescript": ^7.22.5
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: e15ca8cd9f9715db5ca42a4b7883bc85356424497a711ea01ccb39793e647c54d9bd8ab37d9953ed9bed5d06b705fca681dce1f41d121bc47638f50a5b5ce9ee
checksum: a0dc3c2427b55602944705c9a91b4c074524badd5ea87edb603ddeabe7fae531bcbe68475106d7a00079b67bb422dbf2e9f50e15c25ac24d7e9fe77f37ebcfb4
languageName: node
linkType: hard
@@ -1359,18 +1359,18 @@ __metadata:
languageName: node
linkType: hard
"@babel/preset-typescript@npm:7.22.5":
version: 7.22.5
resolution: "@babel/preset-typescript@npm:7.22.5"
"@babel/preset-typescript@npm:7.22.11":
version: 7.22.11
resolution: "@babel/preset-typescript@npm:7.22.11"
dependencies:
"@babel/helper-plugin-utils": ^7.22.5
"@babel/helper-validator-option": ^7.22.5
"@babel/plugin-syntax-jsx": ^7.22.5
"@babel/plugin-transform-modules-commonjs": ^7.22.5
"@babel/plugin-transform-typescript": ^7.22.5
"@babel/plugin-transform-modules-commonjs": ^7.22.11
"@babel/plugin-transform-typescript": ^7.22.11
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 7be1670cb4404797d3a473bd72d66eb2b3e0f2f8a672a5e40bdb0812cc66085ec84bcd7b896709764cabf042fdc6b7f2d4755ac7cce10515eb596ff61dab5154
checksum: 8ae7162c31db896f5eeecd6f67ab2e58555fdc06fe84e95fe4a3f60b64cd6f782d2d7dfbde0c0eac04b55dac18222752d91dd8786245cccedd7e42f080e07233
languageName: node
linkType: hard
@@ -1381,12 +1381,12 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:7.22.10, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.4":
version: 7.22.10
resolution: "@babel/runtime@npm:7.22.10"
"@babel/runtime@npm:7.22.11, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.4":
version: 7.22.11
resolution: "@babel/runtime@npm:7.22.11"
dependencies:
regenerator-runtime: ^0.14.0
checksum: 524d41517e68953dbc73a4f3616b8475e5813f64e28ba89ff5fca2c044d535c2ea1a3f310df1e5bb06162e1f0b401b5c4af73fe6e2519ca2450d9d8c44cf268d
checksum: a5cd6683a8fcdb8065cb1677f221e22f6c67ec8f15ad1d273b180b93ab3bd86c66da2c48f500d4e72d8d2cfa85ff4872a3f350e5aa3855630036af5da765c001
languageName: node
linkType: hard
@@ -1401,9 +1401,9 @@ __metadata:
languageName: node
linkType: hard
"@babel/traverse@npm:^7.22.10":
version: 7.22.10
resolution: "@babel/traverse@npm:7.22.10"
"@babel/traverse@npm:^7.22.11":
version: 7.22.11
resolution: "@babel/traverse@npm:7.22.11"
dependencies:
"@babel/code-frame": ^7.22.10
"@babel/generator": ^7.22.10
@@ -1411,22 +1411,22 @@ __metadata:
"@babel/helper-function-name": ^7.22.5
"@babel/helper-hoist-variables": ^7.22.5
"@babel/helper-split-export-declaration": ^7.22.6
"@babel/parser": ^7.22.10
"@babel/types": ^7.22.10
"@babel/parser": ^7.22.11
"@babel/types": ^7.22.11
debug: ^4.1.0
globals: ^11.1.0
checksum: 9f7b358563bfb0f57ac4ed639f50e5c29a36b821a1ce1eea0c7db084f5b925e3275846d0de63bde01ca407c85d9804e0efbe370d92cd2baaafde3bd13b0f4cdb
checksum: 4ad62d548ca8b95dbf45bae16cc167428f174f3c837d55a5878b1f17bdbc8b384d6df741dc7c461b62c04d881cf25644d3ab885909ba46e3ac43224e2b15b504
languageName: node
linkType: hard
"@babel/types@npm:^7.22.10, @babel/types@npm:^7.22.5, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
version: 7.22.10
resolution: "@babel/types@npm:7.22.10"
"@babel/types@npm:^7.22.10, @babel/types@npm:^7.22.11, @babel/types@npm:^7.22.5, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3":
version: 7.22.11
resolution: "@babel/types@npm:7.22.11"
dependencies:
"@babel/helper-string-parser": ^7.22.5
"@babel/helper-validator-identifier": ^7.22.5
to-fast-properties: ^2.0.0
checksum: 095c4f4b7503fa816e4094113f0ec2351ef96ff32012010b771693066ff628c7c664b21c6bd3fb93aeb46fe7c61f6b3a3c9e4ed0034d6a2481201c417371c8af
checksum: 431a6446896adb62c876d0fe75263835735d3c974aae05356a87eb55f087c20a777028cf08eadcace7993e058bbafe3b21ce2119363222c6cef9eedd7a204810
languageName: node
linkType: hard
@@ -2097,13 +2097,13 @@ __metadata:
languageName: node
linkType: hard
"@lit-labs/virtualizer@npm:2.0.5":
version: 2.0.5
resolution: "@lit-labs/virtualizer@npm:2.0.5"
"@lit-labs/virtualizer@npm:2.0.6":
version: 2.0.6
resolution: "@lit-labs/virtualizer@npm:2.0.6"
dependencies:
lit: ^2.8.0
tslib: ^2.0.3
checksum: f2f2805551fe97128d131ce3fb12a275ab0935c3be4d4da816d088ee4abf0737b63e3ad0310c24bfb0afd89a85120033e55dd3ec4887e33b538d45e9644a02d9
checksum: 8bf5d45f2aafe53cdee3a320a03bd3f95e80a1ff0aab9058624f9cec80d842b08765f04a68555146ad2aa236e974451a7d4539a8533259a7d11b5afc3459a90a
languageName: node
linkType: hard
@@ -3150,13 +3150,13 @@ __metadata:
languageName: node
linkType: hard
"@material/web@npm:=1.0.0-pre.15":
version: 1.0.0-pre.15
resolution: "@material/web@npm:1.0.0-pre.15"
"@material/web@npm:=1.0.0-pre.16":
version: 1.0.0-pre.16
resolution: "@material/web@npm:1.0.0-pre.16"
dependencies:
lit: ^2.7.4
tslib: ^2.4.0
checksum: 83eaa3ba9988a2c20df72219aeef86afba9f8b388011fd66fc021d36ebfa54dc4b5ce8676a94fb7d7f908dab2a8566e734a07e13f6319a00a6c5a27543f85643
checksum: 387e7ea35aa280b3122bd998345402ec5d5c8e416b376bc1ea52584142ed79b469d2bf74e8a2b10f1dc1782fb7c50318e4aa5aff465e6798974cfbd1abf361ff
languageName: node
linkType: hard
@@ -3866,9 +3866,9 @@ __metadata:
languageName: node
linkType: hard
"@rollup/plugin-node-resolve@npm:15.2.0":
version: 15.2.0
resolution: "@rollup/plugin-node-resolve@npm:15.2.0"
"@rollup/plugin-node-resolve@npm:15.2.1":
version: 15.2.1
resolution: "@rollup/plugin-node-resolve@npm:15.2.1"
dependencies:
"@rollup/pluginutils": ^5.0.1
"@types/resolve": 1.20.2
@@ -3881,7 +3881,7 @@ __metadata:
peerDependenciesMeta:
rollup:
optional: true
checksum: 92abfd7f1f5df08d71e4b8dd433d31cf1d62f68891ae3dd5e03a8a7f8d42d9c4433c14a25ead04204fea8235c663a3bbb66ba5528103a6dab66bd8c124f85a0d
checksum: e8f706db6ab826e80d1c9a85d2d1e736f2f78a34ea5d49dd0004d6603249a504696967674b2f021cd144536b88d24ffa058383f08b61b4b665be3c7bfacc006a
languageName: node
linkType: hard
@@ -4634,15 +4634,15 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:6.4.0":
version: 6.4.0
resolution: "@typescript-eslint/eslint-plugin@npm:6.4.0"
"@typescript-eslint/eslint-plugin@npm:6.4.1":
version: 6.4.1
resolution: "@typescript-eslint/eslint-plugin@npm:6.4.1"
dependencies:
"@eslint-community/regexpp": ^4.5.1
"@typescript-eslint/scope-manager": 6.4.0
"@typescript-eslint/type-utils": 6.4.0
"@typescript-eslint/utils": 6.4.0
"@typescript-eslint/visitor-keys": 6.4.0
"@typescript-eslint/scope-manager": 6.4.1
"@typescript-eslint/type-utils": 6.4.1
"@typescript-eslint/utils": 6.4.1
"@typescript-eslint/visitor-keys": 6.4.1
debug: ^4.3.4
graphemer: ^1.4.0
ignore: ^5.2.4
@@ -4655,44 +4655,44 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: d59e88228a4088f3dcaa614103eefa7a0c57315ed79ee1c48afd9817ad013522aa9a9f987e90e1fd7dccc0bbb03ed23e4df6f5ea5cceef8856db33c78ea13d53
checksum: aa5f2f516a4ea07d1a9878d347dcb915808862f41efd3c4acd4955e616d265e051c4c93d597d30e54bee10bab9b965e2ef9cea1b497bf16f23a475d7911a8078
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:6.4.0":
version: 6.4.0
resolution: "@typescript-eslint/parser@npm:6.4.0"
"@typescript-eslint/parser@npm:6.4.1":
version: 6.4.1
resolution: "@typescript-eslint/parser@npm:6.4.1"
dependencies:
"@typescript-eslint/scope-manager": 6.4.0
"@typescript-eslint/types": 6.4.0
"@typescript-eslint/typescript-estree": 6.4.0
"@typescript-eslint/visitor-keys": 6.4.0
"@typescript-eslint/scope-manager": 6.4.1
"@typescript-eslint/types": 6.4.1
"@typescript-eslint/typescript-estree": 6.4.1
"@typescript-eslint/visitor-keys": 6.4.1
debug: ^4.3.4
peerDependencies:
eslint: ^7.0.0 || ^8.0.0
peerDependenciesMeta:
typescript:
optional: true
checksum: 36c8dbeacfc03af9c5a4a0f065861ac6f3747fc64be582a32b0b084de5b5247cef086a0c0052291b97145e0ea8f82acbec452dd927b7b7a1917d56381d59a17c
checksum: cb61c757963f2a7964c2f846087eadda044720da769d96600f9f0069fe796d612caef5d9bb0c785aa4fa95028b2d231e7c83847ce44f02b1fa41f2102d6f444c
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:6.4.0":
version: 6.4.0
resolution: "@typescript-eslint/scope-manager@npm:6.4.0"
"@typescript-eslint/scope-manager@npm:6.4.1":
version: 6.4.1
resolution: "@typescript-eslint/scope-manager@npm:6.4.1"
dependencies:
"@typescript-eslint/types": 6.4.0
"@typescript-eslint/visitor-keys": 6.4.0
checksum: 19406eac3a1899f77eb7c3aa52577e2146075e1318c6eb34d220678afa167832b89c90860714f33b99e107544b48f6970594ca4bcf48c5ede8f2a14a0795ba33
"@typescript-eslint/types": 6.4.1
"@typescript-eslint/visitor-keys": 6.4.1
checksum: 8f7f90aa378a19838301b31cfa58a4b0641d2b84891705c8c006c67aacb5c0d07112b714e1f0e7a159c5736779c934ec26dadef42a0711fccb635596aba391fc
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:6.4.0":
version: 6.4.0
resolution: "@typescript-eslint/type-utils@npm:6.4.0"
"@typescript-eslint/type-utils@npm:6.4.1":
version: 6.4.1
resolution: "@typescript-eslint/type-utils@npm:6.4.1"
dependencies:
"@typescript-eslint/typescript-estree": 6.4.0
"@typescript-eslint/utils": 6.4.0
"@typescript-eslint/typescript-estree": 6.4.1
"@typescript-eslint/utils": 6.4.1
debug: ^4.3.4
ts-api-utils: ^1.0.1
peerDependencies:
@@ -4700,23 +4700,23 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: 7930d2ffdc844a5b706d48ae3e4584882f7f0c06d581a3b06bc280a351c55974b16dbb73f1842f7389f04b80c2cfaf867edd2f261b699804d8a4fea9c20b3869
checksum: 33bcdd48bd4e07258ed1919b598d50354dd67d8f01702cd2fd46aa9250b7b7cba9caab640df01f4dc0e45dabeddbb3ca47bee88f81fe2087350ed6f70a4cbe5d
languageName: node
linkType: hard
"@typescript-eslint/types@npm:6.4.0":
version: 6.4.0
resolution: "@typescript-eslint/types@npm:6.4.0"
checksum: 85b293ad1559dbf8103b2c4cfd0db11c3d9c970d502e2c13d4b1d35e420567042d7077a716d2b4e5113286314d5260f378f242a6dd22ad4b94b4aa69c5f79223
"@typescript-eslint/types@npm:6.4.1":
version: 6.4.1
resolution: "@typescript-eslint/types@npm:6.4.1"
checksum: 16ba46140dbe426407bbb940e87fb347e7eb53b64f74e8f6a819cd662aa25ccd0c25b1e588867ce3cd36a8b4eccea7bd81f4d429595e6e86d9a24c655b1c8617
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:6.4.0":
version: 6.4.0
resolution: "@typescript-eslint/typescript-estree@npm:6.4.0"
"@typescript-eslint/typescript-estree@npm:6.4.1":
version: 6.4.1
resolution: "@typescript-eslint/typescript-estree@npm:6.4.1"
dependencies:
"@typescript-eslint/types": 6.4.0
"@typescript-eslint/visitor-keys": 6.4.0
"@typescript-eslint/types": 6.4.1
"@typescript-eslint/visitor-keys": 6.4.1
debug: ^4.3.4
globby: ^11.1.0
is-glob: ^4.0.3
@@ -4725,157 +4725,157 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: a8db3896550515d0adf140ee115527b409916c4a14ac1f45b5623d130a27ae2d08a1ac906ceda440b01167c88846e2b91ca2025f3d718bff389948f66990c1e7
checksum: 34c289e50a6337321154efe6c20c762e94fea308f9032971e356a266f63e99b908b1a00dd8cf51eba50a6f69db01d665faf2cf13454b355767fd167eebe60f1c
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:6.4.0":
version: 6.4.0
resolution: "@typescript-eslint/utils@npm:6.4.0"
"@typescript-eslint/utils@npm:6.4.1":
version: 6.4.1
resolution: "@typescript-eslint/utils@npm:6.4.1"
dependencies:
"@eslint-community/eslint-utils": ^4.4.0
"@types/json-schema": ^7.0.12
"@types/semver": ^7.5.0
"@typescript-eslint/scope-manager": 6.4.0
"@typescript-eslint/types": 6.4.0
"@typescript-eslint/typescript-estree": 6.4.0
"@typescript-eslint/scope-manager": 6.4.1
"@typescript-eslint/types": 6.4.1
"@typescript-eslint/typescript-estree": 6.4.1
semver: ^7.5.4
peerDependencies:
eslint: ^7.0.0 || ^8.0.0
checksum: abc55382c601c7ed298076548d2df78f15b07ed6830086db6ce1b82d461f0a190ee103a804690ac9205cdca9f373a864e1dd3e20012e9d103f3137963e0aa5ea
checksum: 54e642a345790f912393a6f2821495e2359eff0f874a94cbe6fb3ef4411702983ed54fe88ca3ea9d28f2e93800a74dee22b7888838154bc1afd57c7e119e17ec
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:6.4.0":
version: 6.4.0
resolution: "@typescript-eslint/visitor-keys@npm:6.4.0"
"@typescript-eslint/visitor-keys@npm:6.4.1":
version: 6.4.1
resolution: "@typescript-eslint/visitor-keys@npm:6.4.1"
dependencies:
"@typescript-eslint/types": 6.4.0
"@typescript-eslint/types": 6.4.1
eslint-visitor-keys: ^3.4.1
checksum: 42eb614b9c0a49b6929e093757d772fd27fe5dda9c75f4c7820d1710012c8257eea9bd4f1c4173e2265a8a9ad86cefc1a21869893e7304f3b29b94fa1f987554
checksum: bd9cd56fc793e1d880c24193f939c4992b2653f330baece41cd461d1fb48edb2c53696987cba0e29074bbb452dd181fd009db92dd19060fdcc417ad76768f18a
languageName: node
linkType: hard
"@vaadin/a11y-base@npm:~24.1.5":
version: 24.1.5
resolution: "@vaadin/a11y-base@npm:24.1.5"
"@vaadin/a11y-base@npm:~24.1.6":
version: 24.1.6
resolution: "@vaadin/a11y-base@npm:24.1.6"
dependencies:
"@open-wc/dedupe-mixin": ^1.3.0
"@polymer/polymer": ^3.0.0
"@vaadin/component-base": ~24.1.5
"@vaadin/component-base": ~24.1.6
lit: ^2.0.0
checksum: b005dabc5bf0173c0b51149a0444364ce054d990017c75d3955169f9a95ba8fd19de8578b65003bca0ebcd8179d3b52834fe332a29a89c6de396792ac7217d3f
checksum: 5f739cba4c950d8a4f0ce9bfdab296241c922389fdf9929e371c11e76f0b33d961d0dacc4bbeae8c04b6fbc68a194db631bb49cf415670fa63fb49007f90d01c
languageName: node
linkType: hard
"@vaadin/combo-box@npm:24.1.5":
version: 24.1.5
resolution: "@vaadin/combo-box@npm:24.1.5"
"@vaadin/combo-box@npm:24.1.6":
version: 24.1.6
resolution: "@vaadin/combo-box@npm:24.1.6"
dependencies:
"@open-wc/dedupe-mixin": ^1.3.0
"@polymer/polymer": ^3.0.0
"@vaadin/a11y-base": ~24.1.5
"@vaadin/component-base": ~24.1.5
"@vaadin/field-base": ~24.1.5
"@vaadin/input-container": ~24.1.5
"@vaadin/item": ~24.1.5
"@vaadin/lit-renderer": ~24.1.5
"@vaadin/overlay": ~24.1.5
"@vaadin/vaadin-lumo-styles": ~24.1.5
"@vaadin/vaadin-material-styles": ~24.1.5
"@vaadin/vaadin-themable-mixin": ~24.1.5
checksum: d632a8d8226f8c2ea1146c10d947182379fabd533d53e3602e1f58b6b89fc4bcc3b862e704efcbd29fffb647839b9783eb87c248d9961e56e080a526f07d4cbd
"@vaadin/a11y-base": ~24.1.6
"@vaadin/component-base": ~24.1.6
"@vaadin/field-base": ~24.1.6
"@vaadin/input-container": ~24.1.6
"@vaadin/item": ~24.1.6
"@vaadin/lit-renderer": ~24.1.6
"@vaadin/overlay": ~24.1.6
"@vaadin/vaadin-lumo-styles": ~24.1.6
"@vaadin/vaadin-material-styles": ~24.1.6
"@vaadin/vaadin-themable-mixin": ~24.1.6
checksum: e5c1579af87612acbc66b5d64ab85d674c7be179feec3659e879400021277c2edee8b117d0ae5742e6026bbb5d2631b59a55c4273be8d828a0397b4c682725c7
languageName: node
linkType: hard
"@vaadin/component-base@npm:~24.1.5":
version: 24.1.5
resolution: "@vaadin/component-base@npm:24.1.5"
"@vaadin/component-base@npm:~24.1.6":
version: 24.1.6
resolution: "@vaadin/component-base@npm:24.1.6"
dependencies:
"@open-wc/dedupe-mixin": ^1.3.0
"@polymer/polymer": ^3.0.0
"@vaadin/vaadin-development-mode-detector": ^2.0.0
"@vaadin/vaadin-usage-statistics": ^2.1.0
lit: ^2.0.0
checksum: 0150b72ab01bc9c5408bd144196451964f781f1791f45b061d2fd74ec04590a7f0a0f23988b16de80447aebef78eae75f8a2a558414df01fd0cbc9b6fbc964ab
checksum: 034da5eb6367ca2e6b1f129fee453341390f967a05a8fec15c66316a0f5d6de78c534e6e41bb4a5acbb93075b4405b0018f7f64c6d8d09add0b949174164ec2b
languageName: node
linkType: hard
"@vaadin/field-base@npm:~24.1.5":
version: 24.1.5
resolution: "@vaadin/field-base@npm:24.1.5"
"@vaadin/field-base@npm:~24.1.6":
version: 24.1.6
resolution: "@vaadin/field-base@npm:24.1.6"
dependencies:
"@open-wc/dedupe-mixin": ^1.3.0
"@polymer/polymer": ^3.0.0
"@vaadin/a11y-base": ~24.1.5
"@vaadin/component-base": ~24.1.5
"@vaadin/a11y-base": ~24.1.6
"@vaadin/component-base": ~24.1.6
lit: ^2.0.0
checksum: 21391eb15ab64e2351b15ec5bf6e529625921414eecc9d8e6f7b57d60f5c32ce84a1a5a44b8f957053fb398e6cf68cbb7031a10e7d1ad331927711a11b9f9da5
checksum: 547324a7c84781781979eec988740acac8c2eefc4be3fef5953a668ff43b7ebc8bb7373fb3f32b3874ddc1f32965c4c98381a8ff38f1822e39ca5173c8163115
languageName: node
linkType: hard
"@vaadin/icon@npm:~24.1.5":
version: 24.1.5
resolution: "@vaadin/icon@npm:24.1.5"
"@vaadin/icon@npm:~24.1.6":
version: 24.1.6
resolution: "@vaadin/icon@npm:24.1.6"
dependencies:
"@polymer/polymer": ^3.0.0
"@vaadin/component-base": ~24.1.5
"@vaadin/vaadin-lumo-styles": ~24.1.5
"@vaadin/vaadin-themable-mixin": ~24.1.5
"@vaadin/component-base": ~24.1.6
"@vaadin/vaadin-lumo-styles": ~24.1.6
"@vaadin/vaadin-themable-mixin": ~24.1.6
lit: ^2.0.0
checksum: 54f6312a5c00b3c86dd17bd17ea4da430197ede7ab6465073315c10d62c70c1d9f5d570463876f3e222c62a5ca7ff037c16b310fc310ec0b6c99ba16a91a7b83
checksum: dfb5c8d586f058c0bc09e17562e469325aef6fffaa3fe27024c6782c787c0a9ab774cdf121ce4a7da4c08418ffc6d4a5a0f101f60064a29d0f2c73973b34c101
languageName: node
linkType: hard
"@vaadin/input-container@npm:~24.1.5":
version: 24.1.5
resolution: "@vaadin/input-container@npm:24.1.5"
"@vaadin/input-container@npm:~24.1.6":
version: 24.1.6
resolution: "@vaadin/input-container@npm:24.1.6"
dependencies:
"@polymer/polymer": ^3.0.0
"@vaadin/component-base": ~24.1.5
"@vaadin/vaadin-lumo-styles": ~24.1.5
"@vaadin/vaadin-material-styles": ~24.1.5
"@vaadin/vaadin-themable-mixin": ~24.1.5
checksum: ad903518d15f05b832ea45edb86f4814891f879ff9d7c485bdcf49377774dd6d73b5223de1ec9b69712aa1f71e3280aa6db664060d897041c0cea402f81ddfe7
"@vaadin/component-base": ~24.1.6
"@vaadin/vaadin-lumo-styles": ~24.1.6
"@vaadin/vaadin-material-styles": ~24.1.6
"@vaadin/vaadin-themable-mixin": ~24.1.6
checksum: 0f36911a99f76afec7bbbbe36972edb7bebdaea270dbe1457cbe4c29d0ed250616f2dd41bc97f28aa9486151980ffb133293d6cb02547ff496d4986343ea47fb
languageName: node
linkType: hard
"@vaadin/item@npm:~24.1.5":
version: 24.1.5
resolution: "@vaadin/item@npm:24.1.5"
"@vaadin/item@npm:~24.1.6":
version: 24.1.6
resolution: "@vaadin/item@npm:24.1.6"
dependencies:
"@open-wc/dedupe-mixin": ^1.3.0
"@polymer/polymer": ^3.0.0
"@vaadin/a11y-base": ~24.1.5
"@vaadin/component-base": ~24.1.5
"@vaadin/vaadin-lumo-styles": ~24.1.5
"@vaadin/vaadin-material-styles": ~24.1.5
"@vaadin/vaadin-themable-mixin": ~24.1.5
checksum: 73ec92a3487ec206877c8395c48aa0540a9037d78ada8ba161fa33a9d6596c9a7ed8cecca5654d63847f325862157e6aec1a50dd432d5c88a3f69a1b5990e6ab
"@vaadin/a11y-base": ~24.1.6
"@vaadin/component-base": ~24.1.6
"@vaadin/vaadin-lumo-styles": ~24.1.6
"@vaadin/vaadin-material-styles": ~24.1.6
"@vaadin/vaadin-themable-mixin": ~24.1.6
checksum: 815b7aa44370c0f1b0166390134868995987c8349a53725b014475eaf2cf0db034c95948424c882b2dc8c5405994b9ffdd897022781243727039bc542ad1bf5e
languageName: node
linkType: hard
"@vaadin/lit-renderer@npm:~24.1.5":
version: 24.1.5
resolution: "@vaadin/lit-renderer@npm:24.1.5"
"@vaadin/lit-renderer@npm:~24.1.6":
version: 24.1.6
resolution: "@vaadin/lit-renderer@npm:24.1.6"
dependencies:
lit: ^2.0.0
checksum: 963567770025297b3fecddc7f568835d2ca3304b3f80e524635360319e1bb82138cf8ecc1af5b5f4e6fe3751b67144f864481e4ac211e7d64f1c0b98a44da6cf
checksum: 4aee7c9b204db58146fbb1a08d5b9eb6636fdc52c44dc3ca94b37e3f5caa95255a0dcf4ceaf62a6a32a5426a144954e64733d9b4f14fde2d6fe8bbdec9270118
languageName: node
linkType: hard
"@vaadin/overlay@npm:~24.1.5":
version: 24.1.5
resolution: "@vaadin/overlay@npm:24.1.5"
"@vaadin/overlay@npm:~24.1.6":
version: 24.1.6
resolution: "@vaadin/overlay@npm:24.1.6"
dependencies:
"@open-wc/dedupe-mixin": ^1.3.0
"@polymer/polymer": ^3.0.0
"@vaadin/a11y-base": ~24.1.5
"@vaadin/component-base": ~24.1.5
"@vaadin/vaadin-lumo-styles": ~24.1.5
"@vaadin/vaadin-material-styles": ~24.1.5
"@vaadin/vaadin-themable-mixin": ~24.1.5
checksum: df8bb6e5a20870d70a06a50ff6f6042f032758b21ce73abb133f29b3575724a3152353e56c8a5cc0511723ad5527cf75e7581cab41c9ffdd4b0ec5ee147925f3
"@vaadin/a11y-base": ~24.1.6
"@vaadin/component-base": ~24.1.6
"@vaadin/vaadin-lumo-styles": ~24.1.6
"@vaadin/vaadin-material-styles": ~24.1.6
"@vaadin/vaadin-themable-mixin": ~24.1.6
checksum: 34829445869911439222749814783863e603cc4162486016e7bc73b83cc414dab479bc51e6f13d1afab4c1536782d9257cab5af6c3f7f5115d70e11ca0ea04f9
languageName: node
linkType: hard
@@ -4886,34 +4886,34 @@ __metadata:
languageName: node
linkType: hard
"@vaadin/vaadin-lumo-styles@npm:~24.1.5":
version: 24.1.5
resolution: "@vaadin/vaadin-lumo-styles@npm:24.1.5"
"@vaadin/vaadin-lumo-styles@npm:~24.1.6":
version: 24.1.6
resolution: "@vaadin/vaadin-lumo-styles@npm:24.1.6"
dependencies:
"@polymer/polymer": ^3.0.0
"@vaadin/icon": ~24.1.5
"@vaadin/vaadin-themable-mixin": ~24.1.5
checksum: ecec8b2c0039f7bef23b19717f5a590f083a6c9c62f09ac355858fbcac56b9f164efe92f93254a27ee5661e1cead249bd6af618b5a31ad597119063e9c1ecfd9
"@vaadin/icon": ~24.1.6
"@vaadin/vaadin-themable-mixin": ~24.1.6
checksum: 5cf898c166c9ad369e384b493b0b0b0e0ae98388056fb7b6e5539a05a0be87cd48499f184cbe48be8222092abec8386fe36549d4588bfc2b2150fda8acd09c65
languageName: node
linkType: hard
"@vaadin/vaadin-material-styles@npm:~24.1.5":
version: 24.1.5
resolution: "@vaadin/vaadin-material-styles@npm:24.1.5"
"@vaadin/vaadin-material-styles@npm:~24.1.6":
version: 24.1.6
resolution: "@vaadin/vaadin-material-styles@npm:24.1.6"
dependencies:
"@polymer/polymer": ^3.0.0
"@vaadin/vaadin-themable-mixin": ~24.1.5
checksum: fe4cc4a3c981a5c041b57a2f57bbb6ebd9c4ba0a393258faf195d94eb9f74f28085ba699451c00c37c29dbacad13843b8606fed98f1d1f0c20b8f39b7514d6a2
"@vaadin/vaadin-themable-mixin": ~24.1.6
checksum: 27a3bd5651338419e0b3ecc63fc7186c2ae8db0e6029ebb5b0ab935f1abc52ff0d8a7e7134916019b61bb6b156e9e2e051ea175f7b7c568c8164e959a50a75f0
languageName: node
linkType: hard
"@vaadin/vaadin-themable-mixin@npm:24.1.5, @vaadin/vaadin-themable-mixin@npm:~24.1.5":
version: 24.1.5
resolution: "@vaadin/vaadin-themable-mixin@npm:24.1.5"
"@vaadin/vaadin-themable-mixin@npm:24.1.6, @vaadin/vaadin-themable-mixin@npm:~24.1.6":
version: 24.1.6
resolution: "@vaadin/vaadin-themable-mixin@npm:24.1.6"
dependencies:
"@open-wc/dedupe-mixin": ^1.3.0
lit: ^2.0.0
checksum: d543cb06f2b6f767c0db4d7ff9541f7906fb84570d6d9993e512a5d99797430f748c9aa71f135a8f11f30e1946ec80bc1325badf6972d6feb945410794563f04
checksum: b80493fb7445ad6ba5c5bb272b544541625abbb62f06bc4d7c15c1f8d68addba6c4ac2b0c86b433e192b40bb0fd18c439687c0a6cc735dd7dbbdee93408976d1
languageName: node
linkType: hard
@@ -6518,9 +6518,9 @@ __metadata:
languageName: node
linkType: hard
"chai@npm:4.3.7":
version: 4.3.7
resolution: "chai@npm:4.3.7"
"chai@npm:4.3.8":
version: 4.3.8
resolution: "chai@npm:4.3.8"
dependencies:
assertion-error: ^1.1.0
check-error: ^1.0.2
@@ -6529,7 +6529,7 @@ __metadata:
loupe: ^2.3.1
pathval: ^1.1.1
type-detect: ^4.0.5
checksum: 0bba7d267848015246a66995f044ce3f0ebc35e530da3cbdf171db744e14cbe301ab913a8d07caf7952b430257ccbb1a4a983c570a7c5748dc537897e5131f7c
checksum: 29e0984ed13308319cadc35437c8ef0a3e271544d226c991bf7e3b6d771bf89707321669e11d05e362bc0ad0bd26585079b989d1032f3c106e3bb95d7f079cce
languageName: node
linkType: hard
@@ -9603,12 +9603,12 @@ __metadata:
version: 0.0.0-use.local
resolution: "home-assistant-frontend@workspace:."
dependencies:
"@babel/core": 7.22.10
"@babel/core": 7.22.11
"@babel/plugin-proposal-decorators": 7.22.10
"@babel/plugin-transform-runtime": 7.22.10
"@babel/preset-env": 7.22.10
"@babel/preset-typescript": 7.22.5
"@babel/runtime": 7.22.10
"@babel/preset-typescript": 7.22.11
"@babel/runtime": 7.22.11
"@braintree/sanitize-url": 6.0.4
"@codemirror/autocomplete": 6.9.0
"@codemirror/commands": 6.2.4
@@ -9636,7 +9636,7 @@ __metadata:
"@lezer/highlight": 1.1.6
"@lit-labs/context": 0.4.0
"@lit-labs/motion": 1.0.4
"@lit-labs/virtualizer": 2.0.5
"@lit-labs/virtualizer": 2.0.6
"@lrnwebcomponents/simple-tooltip": 7.0.16
"@material/chips": =14.0.0-canary.53b3cad2f.0
"@material/data-table": =14.0.0-canary.53b3cad2f.0
@@ -9663,7 +9663,7 @@ __metadata:
"@material/mwc-top-app-bar": 0.27.0
"@material/mwc-top-app-bar-fixed": 0.27.0
"@material/top-app-bar": =14.0.0-canary.53b3cad2f.0
"@material/web": =1.0.0-pre.15
"@material/web": =1.0.0-pre.16
"@mdi/js": 7.2.96
"@mdi/svg": 7.2.96
"@octokit/auth-oauth-device": 6.0.0
@@ -9684,7 +9684,7 @@ __metadata:
"@rollup/plugin-babel": 6.0.3
"@rollup/plugin-commonjs": 25.0.4
"@rollup/plugin-json": 6.0.0
"@rollup/plugin-node-resolve": 15.2.0
"@rollup/plugin-node-resolve": 15.2.1
"@rollup/plugin-replace": 5.0.2
"@thomasloven/round-slider": 0.6.0
"@types/babel__plugin-transform-runtime": 7.9.2
@@ -9704,10 +9704,10 @@ __metadata:
"@types/tar": 6.1.5
"@types/ua-parser-js": 0.7.36
"@types/webspeechapi": 0.0.29
"@typescript-eslint/eslint-plugin": 6.4.0
"@typescript-eslint/parser": 6.4.0
"@vaadin/combo-box": 24.1.5
"@vaadin/vaadin-themable-mixin": 24.1.5
"@typescript-eslint/eslint-plugin": 6.4.1
"@typescript-eslint/parser": 6.4.1
"@vaadin/combo-box": 24.1.6
"@vaadin/vaadin-themable-mixin": 24.1.6
"@vibrant/color": 3.2.1-alpha.1
"@vibrant/core": 3.2.1-alpha.1
"@vibrant/quantizer-mmcq": 3.2.1-alpha.1
@@ -9719,7 +9719,7 @@ __metadata:
app-datepicker: 5.1.1
babel-loader: 9.1.3
babel-plugin-template-html-minifier: 4.1.0
chai: 4.3.7
chai: 4.3.8
chart.js: 3.3.2
comlink: 4.4.1
core-js: 3.32.1
@@ -9763,12 +9763,12 @@ __metadata:
jszip: 3.10.1
leaflet: 1.9.4
leaflet-draw: 1.0.4
lint-staged: 14.0.0
lint-staged: 14.0.1
lit: 2.8.0
lit-analyzer: 2.0.0-pre.3
lodash.template: 4.5.0
luxon: 3.4.0
magic-string: 0.30.2
luxon: 3.4.2
magic-string: 0.30.3
map-stream: 0.0.7
marked: 7.0.4
memoize-one: 6.0.0
@@ -9802,7 +9802,7 @@ __metadata:
ts-lit-plugin: 2.0.0-pre.1
tsparticles-engine: 2.12.0
tsparticles-preset-links: 2.12.0
typescript: 5.1.6
typescript: 5.2.2
ua-parser-js: 1.0.35
unfetch: 5.0.0
vinyl-buffer: 1.0.1
@@ -11023,7 +11023,7 @@ __metadata:
languageName: node
linkType: hard
"json5@npm:^2.2.0, json5@npm:^2.2.1, json5@npm:^2.2.2":
"json5@npm:^2.2.0, json5@npm:^2.2.1, json5@npm:^2.2.3":
version: 2.2.3
resolution: "json5@npm:2.2.3"
bin:
@@ -11337,9 +11337,9 @@ __metadata:
languageName: node
linkType: hard
"lint-staged@npm:14.0.0":
version: 14.0.0
resolution: "lint-staged@npm:14.0.0"
"lint-staged@npm:14.0.1":
version: 14.0.1
resolution: "lint-staged@npm:14.0.1"
dependencies:
chalk: 5.3.0
commander: 11.0.0
@@ -11353,7 +11353,7 @@ __metadata:
yaml: 2.3.1
bin:
lint-staged: bin/lint-staged.js
checksum: 7269cd21c15a7b5734a28775f879a91f6b59291bec9a897a1f2faae1cd8316dd50e19fd13207fb6798c705eb5371a25caee0e2dfd5932fbcd38ea5f8155f918c
checksum: 8c5d740cb3c90fab2d970fa6bbffe5ddf35ec66ed374a52caf3a3cf83d8f4d5fd01a949994822bce5ea18c0b8dc8fa4ce087ef886a8c11db674139a063cdfe4f
languageName: node
linkType: hard
@@ -11662,19 +11662,19 @@ __metadata:
languageName: node
linkType: hard
"luxon@npm:3.4.0":
version: 3.4.0
resolution: "luxon@npm:3.4.0"
checksum: ca9b6d0e0a8d156574b7e84014ac8e4b60d116d6d7a0fb65ef48eeb8e4415dad6350b76c5ee34f795cadf8b3f9a5fe7a6885733f561e554c50dffb0b704a840b
"luxon@npm:3.4.2":
version: 3.4.2
resolution: "luxon@npm:3.4.2"
checksum: efefdfaea90f4c8e502db8eb255385e6dac6733b6a9e1372eaf1a866cc1118363e235a3f6df505b837abc8bbcdfd8f0718a8de387b80cef9ee624d8791ca0844
languageName: node
linkType: hard
"magic-string@npm:0.30.2":
version: 0.30.2
resolution: "magic-string@npm:0.30.2"
"magic-string@npm:0.30.3":
version: 0.30.3
resolution: "magic-string@npm:0.30.3"
dependencies:
"@jridgewell/sourcemap-codec": ^1.4.15
checksum: c0bbb9b27b2772e6bfaa5d0f6452d47c462d588ae7c43fbaac062b07836d3ec0140fcdd42a57aa53ed990abafcdd0fc17907813921b5df04eccf43e67674bc57
checksum: a5a9ddf9bd3bf49a2de1048bf358464f1bda7b3cc1311550f4a0ba8f81a4070e25445d53a5ee28850161336f1bff3cf28aa3320c6b4aeff45ce3e689f300b2f3
languageName: node
linkType: hard
@@ -15576,13 +15576,13 @@ __metadata:
languageName: node
linkType: hard
"typescript@npm:5.1.6":
version: 5.1.6
resolution: "typescript@npm:5.1.6"
"typescript@npm:5.2.2":
version: 5.2.2
resolution: "typescript@npm:5.2.2"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: b2f2c35096035fe1f5facd1e38922ccb8558996331405eb00a5111cc948b2e733163cc22fab5db46992aba7dd520fff637f2c1df4996ff0e134e77d3249a7350
checksum: 7912821dac4d962d315c36800fe387cdc0a6298dba7ec171b350b4a6e988b51d7b8f051317786db1094bd7431d526b648aba7da8236607febb26cf5b871d2d3c
languageName: node
linkType: hard
@@ -15596,13 +15596,13 @@ __metadata:
languageName: node
linkType: hard
"typescript@patch:typescript@5.1.6#~builtin<compat/typescript>":
version: 5.1.6
resolution: "typescript@patch:typescript@npm%3A5.1.6#~builtin<compat/typescript>::version=5.1.6&hash=5da071"
"typescript@patch:typescript@5.2.2#~builtin<compat/typescript>":
version: 5.2.2
resolution: "typescript@patch:typescript@npm%3A5.2.2#~builtin<compat/typescript>::version=5.2.2&hash=f3b441"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: f53bfe97f7c8b2b6d23cf572750d4e7d1e0c5fff1c36d859d0ec84556a827b8785077bc27676bf7e71fae538e517c3ecc0f37e7f593be913d884805d931bc8be
checksum: 0f4da2f15e6f1245e49db15801dbee52f2bbfb267e1c39225afdab5afee1a72839cd86000e65ee9d7e4dfaff12239d28beaf5ee431357fcced15fb08583d72ca
languageName: node
linkType: hard