Compare commits

..

1 Commits

Author SHA1 Message Date
Bram Kragten 081a13b3d7 Add back path to protocol config pages 2026-02-03 14:28:12 +01:00
183 changed files with 3680 additions and 4071 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/devcontainers/python:3.14
FROM mcr.microsoft.com/devcontainers/python:1-3.14
ENV \
DEBIAN_FRONTEND=noninteractive \
+1
View File
@@ -251,6 +251,7 @@ For browser support, API details, and current specifications, refer to these aut
**Available Dialog Types:**
- `ha-wa-dialog` - Preferred for new dialogs (Web Awesome based)
- `ha-md-dialog` - Material Design 3 dialog component
- `ha-dialog` - Legacy component (still widely used)
**Opening Dialogs (Fire Event Pattern - Recommended):**
+13 -9
View File
@@ -1,12 +1,14 @@
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
let sidebarChangeCallback;
let changeFunction;
export const mockFrontend = (hass: MockHomeAssistant) => {
hass.mockWS("frontend/get_user_data", () => ({ value: null }));
hass.mockWS("frontend/get_user_data", () => ({
value: null,
}));
hass.mockWS("frontend/set_user_data", ({ key, value }) => {
if (key === "sidebar") {
sidebarChangeCallback?.({
changeFunction?.({
value: {
panelOrder: value.panelOrder || [],
hiddenPanels: value.hiddenPanels || [],
@@ -14,11 +16,14 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
});
}
});
hass.mockWS("frontend/subscribe_user_data", (msg, _hass, onChange) => {
if (msg.key === "sidebar") {
sidebarChangeCallback = onChange;
}
onChange?.({ value: null });
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
changeFunction = onChange;
onChange?.({
value: {
panelOrder: [],
hiddenPanels: [],
},
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
});
@@ -43,5 +48,4 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
return () => {};
});
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
hass.mockWS("frontend/get_themes", (_msg, currentHass) => currentHass.themes);
};
-1
View File
@@ -29,7 +29,6 @@ export const mockLovelace = (
hass.mockWS("lovelace/config/save", () => Promise.resolve());
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
hass.mockWS("lovelace/dashboards/list", () => Promise.resolve([]));
};
customElements.whenDefined("hui-root").then(() => {
+1 -3
View File
@@ -10,9 +10,7 @@ As a community, we are proud of our logo. Follow these guidelines to ensure it a
![Logo](/images/brand/logo.png)
<ha-alert alert-type="info">
This logo is trademarked and the property of the Open Home Foundation. This means it is not available for commercial use without express written permission from the foundation. We regard commercial use as anything designed to market or promote a product, software or service that is for sale. Please contact <a href="mailto:partner@openhomefoundation.org">partner@openhomefoundation.org</a> for further information
</ha-alert>
Please note that this logo is not released under the CC license. All rights reserved.
# Design
-1
View File
@@ -1 +0,0 @@
import "../../../../src/components/ha-alert";
+1 -1
View File
@@ -18,7 +18,7 @@ The Home Assistant interface is based on Material Design. It's a design system c
We want to make it as easy for designers to contribute as it is for developers. Theres a lot a designer can contribute to:
- Meet us at <a href="https://www.home-assistant.io/join-chat-design" rel="noopener noreferrer" target="_blank">Discord #designers channel</a>. If you can't see the channel, make sure you set the correct role in Channels & Roles.
- Meet us at <a href="https://www.home-assistant.io/join-chat" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
+1
View File
@@ -100,6 +100,7 @@ class HaLandingPage extends LandingPageBaseElement {
button-style
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<ha-button
appearance="plain"
+20 -19
View File
@@ -37,15 +37,15 @@
"@codemirror/view": "6.39.12",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.2.1",
"@formatjs/intl-displaynames": "7.2.1",
"@formatjs/intl-durationformat": "0.10.1",
"@formatjs/intl-getcanonicallocales": "3.2.1",
"@formatjs/intl-listformat": "8.2.1",
"@formatjs/intl-locale": "5.2.1",
"@formatjs/intl-numberformat": "9.2.2",
"@formatjs/intl-pluralrules": "6.2.2",
"@formatjs/intl-relativetimeformat": "12.2.2",
"@formatjs/intl-datetimeformat": "7.2.0",
"@formatjs/intl-displaynames": "7.2.0",
"@formatjs/intl-durationformat": "0.10.0",
"@formatjs/intl-getcanonicallocales": "3.2.0",
"@formatjs/intl-listformat": "8.2.0",
"@formatjs/intl-locale": "5.2.0",
"@formatjs/intl-numberformat": "9.2.1",
"@formatjs/intl-pluralrules": "6.2.1",
"@formatjs/intl-relativetimeformat": "12.2.1",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
"@fullcalendar/interaction": "6.1.20",
@@ -71,6 +71,7 @@
"@material/mwc-icon-button": "0.27.0",
"@material/mwc-linear-progress": "0.27.0",
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"@material/mwc-menu": "0.27.0",
"@material/mwc-radio": "0.27.0",
"@material/mwc-select": "0.27.0",
"@material/mwc-snackbar": "0.27.0",
@@ -111,7 +112,7 @@
"hls.js": "1.6.15",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "11.1.2",
"intl-messageformat": "11.1.1",
"js-yaml": "4.1.1",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -132,7 +133,7 @@
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
"ua-parser-js": "2.0.9",
"ua-parser-js": "2.0.8",
"vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
@@ -145,17 +146,17 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.29.0",
"@babel/core": "7.28.6",
"@babel/helper-define-polyfill-provider": "0.6.6",
"@babel/plugin-transform-runtime": "7.29.0",
"@babel/preset-env": "7.29.0",
"@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.6",
"@bundle-stats/plugin-webpack-filter": "4.21.9",
"@lokalise/node-api": "15.6.1",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.2",
"@rspack/core": "1.7.5",
"@rsdoctor/rspack-plugin": "1.5.1",
"@rspack/core": "1.7.4",
"@rspack/dev-server": "1.2.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.25",
@@ -191,14 +192,14 @@
"eslint-plugin-wc": "3.0.2",
"fancy-log": "2.0.0",
"fs-extra": "11.3.3",
"glob": "13.0.1",
"glob": "13.0.0",
"gulp": "5.0.1",
"gulp-brotli": "3.0.0",
"gulp-json-transform": "0.5.0",
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "28.0.0",
"jsdom": "27.4.0",
"jszip": "3.10.1",
"lint-staged": "16.2.7",
"lit-analyzer": "2.0.3",
@@ -228,7 +229,7 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.2",
"@fullcalendar/daygrid": "6.1.20",
"globals": "17.3.0",
"globals": "17.2.0",
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"glob@^10.2.2": "^10.5.0"
-6
View File
@@ -16,12 +16,6 @@ if [[ -n "$DEVCONTAINER" ]]; then
nvm install --reinstall-packages-from="$nodeCurrent" --default
nvm uninstall "$nodeCurrent"
fi
# install yarn if not already available
if ! command -v yarn &> /dev/null; then
npm install -g corepack
yes | yarn
fi
fi
if ! command -v yarn &> /dev/null; then
+1
View File
@@ -194,6 +194,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
button-style
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<ha-button
appearance="plain"
+12 -69
View File
@@ -19,7 +19,6 @@ import { styleMap } from "lit/directives/style-map";
import { ensureArray } from "../../common/array/ensure-array";
import { getAllGraphColors } from "../../common/color/colors";
import { fireEvent } from "../../common/dom/fire_event";
import type { HASSDomEvent } from "../../common/dom/fire_event";
import { listenMediaQuery } from "../../common/dom/media_query";
import { themesContext } from "../../data/context";
import type { Themes } from "../../data/ws-themes";
@@ -28,7 +27,6 @@ import type { HomeAssistant } from "../../types";
import { isMac } from "../../util/is_mac";
import "../chips/ha-assist-chip";
import "../ha-icon-button";
import { afterNextRender } from "../../common/util/render-status";
import { filterXSS } from "../../common/util/xss";
import { formatTimeLabel } from "./axis-label";
import { downSampleLineData } from "./down-sample";
@@ -94,18 +92,10 @@ export class HaChartBase extends LitElement {
private _resizeAnimationDuration?: number;
private _suspendResize = false;
private _layoutTransitionActive = false;
// @ts-ignore
private _resizeController = new ResizeController(this, {
callback: () => {
if (this.chart) {
if (this._suspendResize) {
this._shouldResizeChart = true;
return;
}
if (!this.chart.getZr().animation.isFinished()) {
this._shouldResizeChart = true;
} else {
@@ -123,11 +113,8 @@ export class HaChartBase extends LitElement {
private _originalZrFlush?: () => void;
private _pendingSetup = false;
public disconnectedCallback() {
super.disconnectedCallback();
this._pendingSetup = false;
while (this._listeners.length) {
this._listeners.pop()!();
}
@@ -139,13 +126,7 @@ export class HaChartBase extends LitElement {
public connectedCallback() {
super.connectedCallback();
if (this.hasUpdated) {
this._pendingSetup = true;
afterNextRender(() => {
if (this.isConnected && this._pendingSetup) {
this._pendingSetup = false;
this._setupChart();
}
});
this._setupChart();
}
this._listeners.push(
@@ -200,26 +181,6 @@ export class HaChartBase extends LitElement {
() => window.removeEventListener("keyup", handleKeyUp)
);
}
const handleLayoutTransition: EventListener = (ev) => {
const event = ev as HASSDomEvent<HASSDomEvents["hass-layout-transition"]>;
this._layoutTransitionActive = Boolean(event.detail?.active);
this.toggleAttribute(
"layout-transition-active",
this._layoutTransitionActive
);
this._suspendResize = this._layoutTransitionActive;
if (!this._suspendResize) {
this._resizeChartIfNeeded();
}
};
window.addEventListener("hass-layout-transition", handleLayoutTransition);
this._listeners.push(() =>
window.removeEventListener(
"hass-layout-transition",
handleLayoutTransition
)
);
}
protected firstUpdated() {
@@ -1027,29 +988,19 @@ export class HaChartBase extends LitElement {
}
private _handleChartRenderFinished = () => {
this._resizeChartIfNeeded();
if (this._shouldResizeChart) {
this.chart?.resize({
animation:
this._reducedMotion ||
typeof this._resizeAnimationDuration !== "number"
? undefined
: { duration: this._resizeAnimationDuration },
});
this._shouldResizeChart = false;
this._resizeAnimationDuration = undefined;
}
};
private _resizeChartIfNeeded() {
if (!this.chart || !this._shouldResizeChart) {
return;
}
if (this._suspendResize) {
return;
}
if (!this.chart.getZr().animation.isFinished()) {
return;
}
this.chart.resize({
animation:
this._reducedMotion || typeof this._resizeAnimationDuration !== "number"
? undefined
: { duration: this._resizeAnimationDuration },
});
this._shouldResizeChart = false;
this._resizeAnimationDuration = undefined;
}
private _compareCustomLegendOptions(
oldOptions: ECOption | undefined,
newOptions: ECOption | undefined
@@ -1071,18 +1022,11 @@ export class HaChartBase extends LitElement {
display: block;
position: relative;
letter-spacing: normal;
overflow: visible;
}
:host([layout-transition-active]),
:host([layout-transition-active]) .container,
:host([layout-transition-active]) .chart-container {
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
position: relative;
overflow: visible;
}
.container.has-height {
max-height: var(--chart-max-height, 350px);
@@ -1090,7 +1034,6 @@ export class HaChartBase extends LitElement {
.chart-container {
width: 100%;
max-height: var(--chart-max-height, 350px);
overflow: visible;
}
.has-height .chart-container {
flex: 1;
@@ -11,7 +11,7 @@ import {
sortDeviceAutomations,
} from "../../data/device/device_automation";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import type { HomeAssistant } from "../../types";
import "../ha-generic-picker";
import "../ha-md-select";
import "../ha-md-select-option";
@@ -192,7 +192,7 @@ export abstract class HaDeviceAutomationPicker<
this._renderEmpty = false;
}
private _automationChanged(ev: ValueChangedEvent<string>) {
private _automationChanged(ev: CustomEvent<{ value: string }>) {
ev.stopPropagation();
const value = ev.detail.value;
if (!value || NO_AUTOMATION_KEY === value) {
+1
View File
@@ -48,6 +48,7 @@ export class HaAnsiToHtml extends LitElement {
static styles = css`
pre {
overflow-x: auto;
margin: 0;
}
pre.wrap {
@@ -9,7 +9,7 @@ import { computeFloorName } from "../common/entity/compute_floor_name";
import { getAreaContext } from "../common/entity/context/get_area_context";
import type { FloorRegistryEntry } from "../data/floor_registry";
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HomeAssistant } from "../types";
import "./ha-expansion-panel";
import "./ha-floor-icon";
import "./ha-items-display-editor";
@@ -200,7 +200,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
fireEvent(this, "value-changed", { value: newValue });
}
private async _areaDisplayChanged(ev: ValueChangedEvent<DisplayValue>) {
private async _areaDisplayChanged(ev: CustomEvent<{ value: DisplayValue }>) {
ev.stopPropagation();
const value = ev.detail.value;
const currentFloorId = (ev.currentTarget as any).floorId;
+35 -33
View File
@@ -2,12 +2,14 @@ import type { PropertyValueMap } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { formatLanguageCode } from "../common/language/format_language";
import type { AssistPipeline } from "../data/assist_pipeline";
import { listAssistPipelines } from "../data/assist_pipeline";
import type { HomeAssistant } from "../types";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
const PREFERRED = "preferred";
const LAST_USED = "last_used";
@@ -39,31 +41,6 @@ export class HaAssistPipelinePicker extends LitElement {
return nothing;
}
const value = this.value ?? this._default;
const options: HaSelectOption[] = [
{
value: PREFERRED,
label: this.hass.localize("ui.components.pipeline-picker.preferred", {
preferred: this._pipelines.find(
(pipeline) => pipeline.id === this._preferredPipeline
)?.name,
}),
},
];
if (this.includeLastUsed) {
options.unshift({
value: LAST_USED,
label: this.hass.localize("ui.components.pipeline-picker.last_used"),
});
}
options.push(
...this._pipelines.map((pipeline) => ({
value: pipeline.id,
label: `${pipeline.name} (${formatLanguageCode(pipeline.language, this.hass.locale)})`,
}))
);
return html`
<ha-select
.label=${this.label ||
@@ -72,8 +49,33 @@ export class HaAssistPipelinePicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
.options=${options}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${this.includeLastUsed
? html`
<ha-list-item .value=${LAST_USED}>
${this.hass!.localize(
"ui.components.pipeline-picker.last_used"
)}
</ha-list-item>
`
: null}
<ha-list-item .value=${PREFERRED}>
${this.hass!.localize("ui.components.pipeline-picker.preferred", {
preferred: this._pipelines.find(
(pipeline) => pipeline.id === this._preferredPipeline
)?.name,
})}
</ha-list-item>
${this._pipelines.map(
(pipeline) =>
html`<ha-list-item .value=${pipeline.id}>
${pipeline.name}
(${formatLanguageCode(pipeline.language, this.hass.locale)})
</ha-list-item>`
)}
</ha-select>
`;
}
@@ -94,17 +96,17 @@ export class HaAssistPipelinePicker extends LitElement {
}
`;
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
value === "" ||
value === this.value ||
(this.value === undefined && value === this._default)
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === this._default)
) {
return;
}
this.value = value === this._default ? undefined : value;
this.value = target.value === this._default ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
}
}
+14 -7
View File
@@ -4,9 +4,10 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import type { HaSelectSelectEvent } from "./ha-select";
import { stopPropagation } from "../common/dom/stop_propagation";
import "./ha-icon-button";
import "./ha-input-helper-text";
import "./ha-list-item";
import "./ha-select";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@@ -259,10 +260,14 @@ export class HaBaseTimeInput extends LitElement {
.required=${this.required}
.value=${this.amPm}
.disabled=${this.disabled}
.name=${"amPm"}
name="amPm"
naturalMenuWidth
fixedMenuPosition
@selected=${this._valueChanged}
.options=${["AM", "PM"]}
@closed=${stopPropagation}
>
<ha-list-item value="AM">AM</ha-list-item>
<ha-list-item value="PM">PM</ha-list-item>
</ha-select>`}
</div>
${this.helper
@@ -277,12 +282,10 @@ export class HaBaseTimeInput extends LitElement {
fireEvent(this, "value-changed");
}
private _valueChanged(ev: InputEvent | HaSelectSelectEvent): void {
private _valueChanged(ev: InputEvent) {
const textField = ev.currentTarget as HaTextField;
this[textField.name] =
textField.name === "amPm"
? (ev as HaSelectSelectEvent).detail.value
: Number(textField.value);
textField.name === "amPm" ? textField.value : Number(textField.value);
const value: TimeChangedEvent = {
hours: this.hours,
minutes: this.minutes,
@@ -363,6 +366,10 @@ export class HaBaseTimeInput extends LitElement {
ha-textfield:last-child {
--text-field-border-top-right-radius: var(--mdc-shape-medium);
}
ha-select {
--mdc-shape-small: 0;
width: 85px;
}
:host([clearable]) .mdc-select__anchor {
padding-inline-end: var(--select-selected-text-padding-end, 12px);
}
+14 -9
View File
@@ -2,11 +2,12 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { stringCompare } from "../common/string/compare";
import type { Blueprint, BlueprintDomain, Blueprints } from "../data/blueprint";
import { fetchBlueprints } from "../data/blueprint";
import type { HomeAssistant } from "../types";
import type { HaSelectSelectEvent } from "./ha-select";
import "./ha-list-item";
import "./ha-select";
@customElement("ha-blueprint-picker")
@@ -54,16 +55,20 @@ class HaBluePrintPicker extends LitElement {
<ha-select
.label=${this.label ||
this.hass.localize("ui.components.blueprint-picker.select_blueprint")}
fixedMenuPosition
naturalMenuWidth
.value=${this.value}
.disabled=${this.disabled}
@selected=${this._blueprintChanged}
.options=${this._processedBlueprints(this.blueprints).map(
(blueprint) => ({
value: blueprint.path,
label: blueprint.name,
})
)}
@closed=${stopPropagation}
>
${this._processedBlueprints(this.blueprints).map(
(blueprint) => html`
<ha-list-item .value=${blueprint.path}>
${blueprint.name}
</ha-list-item>
`
)}
</ha-select>
`;
}
@@ -77,8 +82,8 @@ class HaBluePrintPicker extends LitElement {
}
}
private _blueprintChanged(ev: HaSelectSelectEvent) {
const newValue = ev.detail.value;
private _blueprintChanged(ev) {
const newValue = ev.target.value;
if (newValue !== this.value) {
this.value = newValue;
+31 -27
View File
@@ -3,6 +3,7 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { debounce } from "../common/util/debounce";
import type { ConfigEntry, SubEntry } from "../data/config_entries";
import { getConfigEntry, getSubEntries } from "../data/config_entries";
@@ -13,8 +14,9 @@ import { fetchIntegrationManifest } from "../data/integration";
import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow";
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import type { HaSelect } from "./ha-select";
const NONE = "__NONE_OPTION__";
@@ -71,35 +73,37 @@ export class HaConversationAgentPicker extends LitElement {
value = NONE;
}
const options: HaSelectOption[] = this._agents.map((agent) => ({
value: agent.id,
label: agent.name,
disabled:
agent.supported_languages !== "*" &&
agent.supported_languages.length === 0,
}));
if (!this.required) {
options.unshift({
value: NONE,
label: this.hass.localize(
"ui.components.conversation-agent-picker.none"
),
});
}
return html`
<ha-select
.label=${this.label ||
this.hass!.localize(
"ui.components.conversation-agent-picker.conversation_agent"
"ui.components.coversation-agent-picker.conversation_agent"
)}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
.options=${options}
></ha-select
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize(
"ui.components.coversation-agent-picker.none"
)}
</ha-list-item>`
: nothing}
${this._agents.map(
(agent) =>
html`<ha-list-item
.value=${agent.id}
.disabled=${agent.supported_languages !== "*" &&
agent.supported_languages.length === 0}
>
${agent.name}
</ha-list-item>`
)}</ha-select
>${(this._subConfigEntry &&
this._configEntry?.supported_subentry_types[
this._subConfigEntry.subentry_type
@@ -234,17 +238,17 @@ export class HaConversationAgentPicker extends LitElement {
}
`;
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
) {
return;
}
this.value = value === NONE ? undefined : value;
this.value = target.value === NONE ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "supported-languages-changed", {
value: this._agents!.find((agent) => agent.id === this.value)
-13
View File
@@ -65,10 +65,6 @@ export class HaDateRangePicker extends LitElement {
@property({ attribute: "time-picker", type: Boolean })
public timePicker = false;
public open(): void {
this._openPicker();
}
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public minimal = false;
@@ -310,15 +306,6 @@ export class HaDateRangePicker extends LitElement {
return dateRangePicker.vueComponent.$children[0];
}
private _openPicker() {
if (!this._dateRangePicker.open) {
const datePicker = this.shadowRoot!.querySelector(
"date-range-picker div.date-range-inputs"
) as any;
datePicker?.click();
}
}
private _handleInputClick() {
// close the date picker, so it will open again on the click event
if (this._dateRangePicker.open) {
+3 -12
View File
@@ -76,18 +76,6 @@ export class HaDialog extends DialogBase {
var(--divider-color)
);
z-index: var(--dialog-z-index, 8);
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
--mdc-typography-headline6-font-size: 1.574rem;
}
.mdc-dialog::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
-webkit-backdrop-filter: var(
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter, none)
@@ -96,6 +84,9 @@ export class HaDialog extends DialogBase {
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter, none)
);
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
--mdc-typography-headline6-font-size: 1.574rem;
}
.mdc-dialog .mdc-dialog__scrim {
background-color: var(--mdc-dialog-scrim-color, none);
-87
View File
@@ -4,18 +4,6 @@ import type { PropertyValues } from "lit";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { HASSDomEvent } from "../common/dom/fire_event";
declare global {
interface HASSDomEvents {
"hass-layout-transition": { active: boolean; reason?: string };
}
interface HTMLElementEventMap {
"hass-layout-transition": HASSDomEvent<
HASSDomEvents["hass-layout-transition"]
>;
}
}
const blockingElements = (document as any).$blockingElements;
@@ -27,30 +15,6 @@ export class HaDrawer extends DrawerBase {
private _rtlStyle?: HTMLElement;
private _sidebarTransitionActive = false;
private _handleDrawerTransitionStart = (ev: TransitionEvent) => {
if (ev.propertyName !== "width" || this._sidebarTransitionActive) {
return;
}
this._sidebarTransitionActive = true;
fireEvent(window, "hass-layout-transition", {
active: true,
reason: "sidebar",
});
};
private _handleDrawerTransitionEnd = (ev: TransitionEvent) => {
if (ev.propertyName !== "width" || !this._sidebarTransitionActive) {
return;
}
this._sidebarTransitionActive = false;
fireEvent(window, "hass-layout-transition", {
active: false,
reason: "sidebar",
});
};
protected createAdapter() {
return {
...super.createAdapter(),
@@ -99,38 +63,6 @@ export class HaDrawer extends DrawerBase {
}
}
protected firstUpdated() {
super.firstUpdated();
this.mdcRoot?.addEventListener(
"transitionstart",
this._handleDrawerTransitionStart
);
this.mdcRoot?.addEventListener(
"transitionend",
this._handleDrawerTransitionEnd
);
this.mdcRoot?.addEventListener(
"transitioncancel",
this._handleDrawerTransitionEnd
);
}
public disconnectedCallback() {
super.disconnectedCallback();
this.mdcRoot?.removeEventListener(
"transitionstart",
this._handleDrawerTransitionStart
);
this.mdcRoot?.removeEventListener(
"transitionend",
this._handleDrawerTransitionEnd
);
this.mdcRoot?.removeEventListener(
"transitioncancel",
this._handleDrawerTransitionEnd
);
}
private async _setupSwipe() {
const hammer = await import("../resources/hammer");
this._mc = new hammer.Manager(document, {
@@ -158,16 +90,6 @@ export class HaDrawer extends DrawerBase {
border-color: var(--divider-color, rgba(0, 0, 0, 0.12));
inset-inline-start: 0 !important;
inset-inline-end: initial !important;
transition-property: transform, width;
transition-duration:
var(--mdc-drawer-transition-duration, 0.2s),
var(--ha-animation-duration-normal);
transition-timing-function:
var(
--mdc-drawer-transition-timing-function,
cubic-bezier(0.4, 0, 0.2, 1)
),
ease;
}
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
z-index: 200;
@@ -181,15 +103,6 @@ export class HaDrawer extends DrawerBase {
direction: var(--direction);
width: 100%;
box-sizing: border-box;
transition:
padding-left var(--ha-animation-duration-normal) ease,
padding-inline-start var(--ha-animation-duration-normal) ease;
}
@media (prefers-reduced-motion: reduce) {
.mdc-drawer,
.mdc-drawer-app-content {
transition: none;
}
}
`,
];
-1
View File
@@ -37,7 +37,6 @@ export class HaDropdownItem extends DropdownItem {
#check {
visibility: visible;
flex-shrink: 0;
}
#icon ::slotted(*) {
+1 -7
View File
@@ -3,13 +3,7 @@ import { css, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import type { HaDropdownItem } from "./ha-dropdown-item";
/**
* Event type for the ha-dropdown component when an item is selected.
* @param T - The type of the value of the selected item.
*/
export type HaDropdownSelectEvent<T = string> = CustomEvent<{
item: Omit<HaDropdownItem, "value"> & { value: T };
}>;
export type HaDropdownSelectEvent = CustomEvent<{ item: HaDropdownItem }>;
/**
* Home Assistant dropdown component
+3
View File
@@ -107,6 +107,9 @@ export class HaLanguagePicker extends LitElement {
@property({ attribute: "no-sort", type: Boolean }) public noSort = false;
@property({ attribute: "inline-arrow", type: Boolean })
public inlineArrow = false;
@state() _defaultLanguages: string[] = [];
@query("ha-generic-picker", true) public genericPicker!: HaGenericPicker;
+263
View File
@@ -0,0 +1,263 @@
import { Dialog } from "@material/web/dialog/internal/dialog";
import { styles } from "@material/web/dialog/internal/dialog-styles";
import {
type DialogAnimation,
DIALOG_DEFAULT_CLOSE_ANIMATION,
DIALOG_DEFAULT_OPEN_ANIMATION,
} from "@material/web/dialog/internal/animations";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
// workaround to be able to overlay a dialog with another dialog
Dialog.addInitializer(async (instance) => {
await instance.updateComplete;
const dialogInstance = instance as HaMdDialog;
// @ts-expect-error dialog is private
dialogInstance.dialog.prepend(dialogInstance.scrim);
// @ts-expect-error scrim is private
dialogInstance.scrim.style.inset = 0;
// @ts-expect-error scrim is private
dialogInstance.scrim.style.zIndex = 0;
const { getOpenAnimation, getCloseAnimation } = dialogInstance;
dialogInstance.getOpenAnimation = () => {
const animations = getOpenAnimation.call(this);
animations.container = [
...(animations.container ?? []),
...(animations.dialog ?? []),
];
animations.dialog = [];
return animations;
};
dialogInstance.getCloseAnimation = () => {
const animations = getCloseAnimation.call(this);
animations.container = [
...(animations.container ?? []),
...(animations.dialog ?? []),
];
animations.dialog = [];
return animations;
};
});
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>;
/**
* Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs
*
*/
@customElement("ha-md-dialog")
export class HaMdDialog extends Dialog {
/**
* When true the dialog will not close when the user presses the esc key or press out of the dialog.
*/
@property({ attribute: "disable-cancel-action", type: Boolean })
public disableCancelAction = false;
private _polyfillDialogRegistered = false;
constructor() {
super();
this.addEventListener("cancel", this._handleCancel);
if (typeof HTMLDialogElement !== "function") {
this.addEventListener("open", this._handleOpen);
if (!DIALOG_POLYFILL) {
DIALOG_POLYFILL = import("dialog-polyfill");
}
}
// if browser doesn't support animate API disable open/close animations
if (this.animate === undefined) {
this.quick = true;
}
// if browser doesn't support animate API disable open/close animations
if (this.animate === undefined) {
this.quick = true;
}
}
// prevent open in older browsers and wait for polyfill to load
private async _handleOpen(openEvent: Event) {
openEvent.preventDefault();
if (this._polyfillDialogRegistered) {
return;
}
this._polyfillDialogRegistered = true;
this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css");
const dialog = this.shadowRoot?.querySelector(
"dialog"
) as HTMLDialogElement;
const dialogPolyfill = await DIALOG_POLYFILL;
dialogPolyfill.default.registerDialog(dialog);
this.removeEventListener("open", this._handleOpen);
this.show();
}
private async _loadPolyfillStylesheet(href) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = href;
return new Promise<void>((resolve, reject) => {
link.onload = () => resolve();
link.onerror = () =>
reject(new Error(`Stylesheet failed to load: ${href}`));
this.shadowRoot?.appendChild(link);
});
}
private _handleCancel(closeEvent: Event) {
if (this.disableCancelAction) {
closeEvent.preventDefault();
const dialogElement = this.shadowRoot?.querySelector("dialog .container");
if (this.animate !== undefined) {
dialogElement?.animate(
[
{
transform: "rotate(-1deg)",
"animation-timing-function": "ease-in",
},
{
transform: "rotate(1.5deg)",
"animation-timing-function": "ease-out",
},
{
transform: "rotate(0deg)",
"animation-timing-function": "ease-in",
},
],
{
duration: 200,
iterations: 2,
}
);
}
}
}
static override styles = [
styles,
css`
:host {
--md-dialog-container-color: var(--card-background-color);
--md-dialog-headline-color: var(--primary-text-color);
--md-dialog-supporting-text-color: var(--primary-text-color);
--md-sys-color-scrim: #000000;
--md-dialog-headline-weight: var(--ha-font-weight-normal);
--md-dialog-headline-size: var(--ha-font-size-xl);
--md-dialog-supporting-text-size: var(--ha-font-size-m);
--md-dialog-supporting-text-line-height: var(--ha-line-height-normal);
--md-divider-color: var(--divider-color);
}
:host([type="alert"]) {
min-width: 320px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
:host(:not([type="alert"])) {
min-width: var(--mdc-dialog-min-width, 100vw);
min-height: 100%;
max-height: 100%;
--md-dialog-container-shape: 0;
}
.container {
margin-top: var(--safe-area-inset-top, 0);
margin-bottom: var(--safe-area-inset-bottom, 0);
margin-left: var(--safe-area-inset-left, 0);
margin-right: var(--safe-area-inset-right, 0);
}
}
::slotted(ha-dialog-header[slot="headline"]) {
display: contents;
}
slot[name="actions"]::slotted(*) {
padding: var(--ha-space-4);
}
.scroller {
overflow: var(--dialog-content-overflow, auto);
}
slot[name="content"]::slotted(*) {
padding: var(--dialog-content-padding, var(--ha-space-6));
}
.scrim {
z-index: 10; /* overlay navigation */
}
`,
];
}
// by default the dialog open/close animation will be from/to the top
// but if we have a special mobile dialog which is at the bottom of the screen, a from bottom animation can be used:
const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = {
...DIALOG_DEFAULT_OPEN_ANIMATION,
dialog: [
[
// Dialog slide up
[{ transform: "translateY(50px)" }, { transform: "translateY(0)" }],
{ duration: 500, easing: "cubic-bezier(.3,0,0,1)" },
],
],
container: [
[
// Container fade in
[{ opacity: 0 }, { opacity: 1 }],
{ duration: 50, easing: "linear", pseudoElement: "::before" },
],
],
};
const CLOSE_TO_BOTTOM_ANIMATION: DialogAnimation = {
...DIALOG_DEFAULT_CLOSE_ANIMATION,
dialog: [
[
// Dialog slide down
[{ transform: "translateY(0)" }, { transform: "translateY(50px)" }],
{ duration: 150, easing: "cubic-bezier(.3,0,0,1)" },
],
],
container: [
[
// Container fade out
[{ opacity: "1" }, { opacity: "0" }],
{ delay: 100, duration: 50, easing: "linear", pseudoElement: "::before" },
],
],
};
export const getMobileOpenFromBottomAnimation = () => {
const matches = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
return matches ? OPEN_FROM_BOTTOM_ANIMATION : DIALOG_DEFAULT_OPEN_ANIMATION;
};
export const getMobileCloseToBottomAnimation = () => {
const matches = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
return matches ? CLOSE_TO_BOTTOM_ANIMATION : DIALOG_DEFAULT_CLOSE_ANIMATION;
};
declare global {
interface HTMLElementTagNameMap {
"ha-md-dialog": HaMdDialog;
}
}
+45
View File
@@ -0,0 +1,45 @@
import { MenuBase } from "@material/mwc-menu/mwc-menu-base";
import { styles } from "@material/mwc-menu/mwc-menu.css";
import { html } from "lit";
import { customElement } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "./ha-list";
@customElement("ha-menu")
export class HaMenu extends MenuBase {
protected get listElement() {
if (!this.listElement_) {
this.listElement_ = this.renderRoot.querySelector("ha-list");
return this.listElement_;
}
return this.listElement_;
}
protected renderList() {
const itemRoles = this.innerRole === "menu" ? "menuitem" : "option";
const classes = this.renderListClasses();
return html`<ha-list
rootTabbable
.innerAriaLabel=${this.innerAriaLabel}
.innerRole=${this.innerRole}
.multi=${this.multi}
class=${classMap(classes)}
.itemRoles=${itemRoles}
.wrapFocus=${this.wrapFocus}
.activatable=${this.activatable}
@action=${this.onAction}
>
<slot></slot>
</ha-list>`;
}
static styles = styles;
}
declare global {
interface HTMLElementTagNameMap {
"ha-menu": HaMenu;
}
}
+55 -42
View File
@@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { SupervisorMounts } from "../data/supervisor/mounts";
import {
@@ -14,9 +15,9 @@ import {
} from "../data/supervisor/mounts";
import type { HomeAssistant } from "../types";
import "./ha-alert";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
const _BACKUP_DATA_DISK_ = "/backup";
@@ -51,54 +52,60 @@ class HaMountPicker extends LitElement {
if (!this._mounts) {
return nothing;
}
const options: HaSelectOption[] = this._filterMounts(
this._mounts,
this.usage
).map((mount) => ({
value: mount.name,
label: mount.name,
secondary: `${mount.server}${mount.port ? `:${mount.port}` : ""}${
mount.type === SupervisorMountType.NFS ? mount.path : `:${mount.share}`
}`,
iconPath:
mount.usage === SupervisorMountUsage.MEDIA
? mdiPlayBox
: mount.usage === SupervisorMountUsage.SHARE
? mdiFolder
: mdiBackupRestore,
}));
if (this.usage === SupervisorMountUsage.BACKUP) {
const dataDiskOption = {
value: _BACKUP_DATA_DISK_,
iconPath: mdiHarddisk,
label:
this.hass.localize("ui.components.mount-picker.use_datadisk") ||
"Use data disk for backup",
};
if (
!this._mounts.default_backup_mount ||
this._mounts.default_backup_mount === _BACKUP_DATA_DISK_
) {
options.unshift(dataDiskOption);
} else {
options.push(dataDiskOption);
}
}
const dataDiskOption = html`<ha-list-item
graphic="icon"
.value=${_BACKUP_DATA_DISK_}
>
<span>
${this.hass.localize("ui.components.mount-picker.use_datadisk") ||
"Use data disk for backup"}
</span>
<ha-svg-icon slot="graphic" .path=${mdiHarddisk}></ha-svg-icon>
</ha-list-item>`;
return html`
<ha-select
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.mount-picker.mount")
: this.label}
.value=${this.value}
.value=${this._value}
.required=${this.required}
.disabled=${this.disabled}
.helper=${this.helper}
@selected=${this._mountChanged}
.options=${options}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${this.usage === SupervisorMountUsage.BACKUP &&
(!this._mounts.default_backup_mount ||
this._mounts.default_backup_mount === _BACKUP_DATA_DISK_)
? dataDiskOption
: nothing}
${this._filterMounts(this._mounts, this.usage).map(
(mount) =>
html`<ha-list-item twoline graphic="icon" .value=${mount.name}>
<span>${mount.name}</span>
<span slot="secondary"
>${mount.server}${mount.port
? `:${mount.port}`
: nothing}${mount.type === SupervisorMountType.NFS
? mount.path
: `:${mount.share}`}</span
>
<ha-svg-icon
slot="graphic"
.path=${mount.usage === SupervisorMountUsage.MEDIA
? mdiPlayBox
: mount.usage === SupervisorMountUsage.SHARE
? mdiFolder
: mdiBackupRestore}
></ha-svg-icon>
</ha-list-item>`
)}
${this.usage === SupervisorMountUsage.BACKUP &&
this._mounts.default_backup_mount
? dataDiskOption
: nothing}
</ha-select>
`;
}
@@ -146,10 +153,16 @@ class HaMountPicker extends LitElement {
}
}
private _mountChanged(ev: HaSelectSelectEvent) {
const newValue = ev.detail.value;
private get _value() {
return this.value || "";
}
if (newValue !== this.value) {
private _mountChanged(ev: Event) {
ev.stopPropagation();
const target = ev.target as HaSelect;
const newValue = target.value;
if (newValue !== this._value) {
this._setValue(newValue);
}
}
+6 -1
View File
@@ -262,7 +262,12 @@ export class HaNavigationPicker extends LitElement {
const viewConfigs = await Promise.all(
lovelacePanels.map((panel) =>
fetchConfig(this.hass!.connection, panel.url_path, true)
fetchConfig(
this.hass!.connection,
// path should be null to fetch default lovelace panel
panel.url_path === "lovelace" ? null : panel.url_path,
true
)
.then((config) => [panel.id, config] as [string, typeof config])
.catch((_) => [panel.id, undefined] as [string, undefined])
)
-1
View File
@@ -55,7 +55,6 @@ export interface PickerComboBoxItem {
sorting_label?: string;
icon_path?: string;
icon?: string;
isRelated?: boolean;
}
export interface PickerComboBoxIndexSelectedDetail {
+158 -192
View File
@@ -1,221 +1,187 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-picker-field";
import type { HaPickerField } from "./ha-picker-field";
import "./ha-svg-icon";
export interface HaSelectOption {
value: string;
label?: string;
secondary?: string;
iconPath?: string;
disabled?: boolean;
}
/**
* Event type for the ha-select component when an item is selected.
* @param T - The type of the value of the selected item.
* @param Clearable - Whether the select is clearable (allows undefined values).
*/
export type HaSelectSelectEvent<
T = string,
Clearable extends boolean = false,
> = CustomEvent<{
value: Clearable extends true ? T | undefined : T;
}>;
import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { styles } from "@material/mwc-select/mwc-select.css";
import { mdiClose } from "@mdi/js";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status";
import "./ha-icon-button";
import "./ha-menu";
@customElement("ha-select")
export class HaSelect extends LitElement {
@property({ type: Boolean }) public clearable = false;
export class HaSelect extends SelectBase {
// @ts-ignore
@property({ type: Boolean }) public icon = false;
@property({ attribute: false }) public options?: HaSelectOption[] | string[];
@property({ type: Boolean, reflect: true }) public clearable = false;
@property() public label?: string;
@property({ attribute: "inline-arrow", type: Boolean })
public inlineArrow = false;
@property() public helper?: string;
@property() public value?: string;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean }) public disabled = false;
@state() private _opened = false;
@query("ha-picker-field") private _triggerField!: HaPickerField;
private _getValueLabel = memoizeOne(
(
options: HaSelectOption[] | string[] | undefined,
value: string | undefined
) => {
if (!options || !value) {
return value;
}
for (const option of options) {
if (
(typeof option === "string" && option === value) ||
(typeof option !== "string" && option.value === value)
) {
return typeof option === "string"
? option
: option.label || option.value;
}
}
return value;
}
);
@property() public options;
protected override render() {
if (this.disabled) {
return this._renderField();
}
return html`
<ha-dropdown
placement="bottom"
@wa-select=${this._handleSelect}
@wa-show=${this._handleShow}
@wa-hide=${this._handleHide}
>
${this._renderField()}
${this.options
? this.options.map(
(option) => html`
<ha-dropdown-item
.value=${typeof option === "string" ? option : option.value}
.disabled=${typeof option === "string"
? false
: (option.disabled ?? false)}
class=${this.value ===
(typeof option === "string" ? option : option.value)
? "selected"
: ""}
>
${option.iconPath
? html`<ha-svg-icon
slot="icon"
.path=${option.iconPath}
></ha-svg-icon>`
: nothing}
<div class="content">
${typeof option === "string"
? option
: option.label || option.value}
${option.secondary
? html`<div class="secondary">${option.secondary}</div>`
: nothing}
</div>
</ha-dropdown-item>
`
)
: html`<slot></slot>`}
</ha-dropdown>
${super.render()}
${this.clearable && !this.required && !this.disabled && this.value
? html`<ha-icon-button
label="clear"
@click=${this._clearValue}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
`;
}
private _renderField() {
const valueLabel = this._getValueLabel(this.options, this.value);
return html`
<ha-picker-field
slot="trigger"
type="button"
class=${this._opened ? "opened" : ""}
compact
aria-label=${ifDefined(this.label)}
@clear=${this._clearValue}
.label=${this.label}
.helper=${this.helper}
.value=${valueLabel}
.required=${this.required}
.disabled=${this.disabled}
.hideClearIcon=${!this.clearable ||
this.required ||
this.disabled ||
!this.value}
>
</ha-picker-field>
`;
protected override renderMenu() {
const classes = this.getMenuClasses();
return html`<ha-menu
innerRole="listbox"
wrapFocus
class=${classMap(classes)}
activatable
.fullwidth=${this.fixedMenuPosition ? false : !this.naturalMenuWidth}
.open=${this.menuOpen}
.anchor=${this.anchorElement}
.fixed=${this.fixedMenuPosition}
@selected=${this.onSelected}
@opened=${this.onOpened}
@closed=${this.onClosed}
@items-updated=${this.onItemsUpdated}
@keydown=${this.handleTypeahead}
>
${this.renderMenuContent()}
</ha-menu>`;
}
private _handleSelect(ev: CustomEvent<{ item: { value: string } }>) {
ev.stopPropagation();
const value = ev.detail.item.value;
if (value === this.value) {
return;
protected override renderLeadingIcon() {
if (!this.icon) {
return nothing;
}
fireEvent(this, "selected", { value });
return html`<span class="mdc-select__icon"
><slot name="icon"></slot
></span>`;
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("translations-updated", this._translationsUpdated);
}
protected async firstUpdated() {
super.firstUpdated();
if (this.inlineArrow) {
this.shadowRoot
?.querySelector(".mdc-select__selected-text-container")
?.classList.add("inline-arrow");
}
}
protected updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has("inlineArrow")) {
const textContainerElement = this.shadowRoot?.querySelector(
".mdc-select__selected-text-container"
);
if (this.inlineArrow) {
textContainerElement?.classList.add("inline-arrow");
} else {
textContainerElement?.classList.remove("inline-arrow");
}
}
if (changedProperties.get("options")) {
this.layoutOptions();
this.selectByValue(this.value);
}
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener(
"translations-updated",
this._translationsUpdated
);
}
private _clearValue(): void {
if (this.disabled || !this.value) {
return;
}
fireEvent(this, "selected", { value: undefined });
this.valueSetDirectly = true;
this.select(-1);
this.mdcFoundation.handleChange();
}
private _handleShow() {
this.style.setProperty(
"--select-menu-width",
`${this._triggerField.offsetWidth}px`
);
this._opened = true;
}
private _translationsUpdated = debounce(async () => {
await nextRender();
this.layoutOptions();
}, 500);
private _handleHide() {
this._opened = false;
}
static styles = css`
:host {
position: relative;
}
ha-picker-field.opened {
--mdc-text-field-idle-line-color: var(--primary-color);
}
ha-dropdown-item.selected:hover {
background-color: var(--ha-color-fill-primary-quiet-hover);
}
ha-dropdown-item .content {
display: flex;
gap: var(--ha-space-1);
flex-direction: column;
}
ha-dropdown-item .secondary {
font-size: var(--ha-font-size-s);
color: var(--ha-color-text-secondary);
}
ha-dropdown::part(menu) {
min-width: var(--select-menu-width);
}
:host ::slotted(ha-dropdown-item.selected),
ha-dropdown-item.selected {
font-weight: var(--ha-font-weight-medium);
color: var(--primary-color);
background-color: var(--ha-color-fill-primary-quiet-resting);
--icon-primary-color: var(--primary-color);
}
`;
static override styles = [
styles,
css`
:host([clearable]) {
position: relative;
}
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
color: var(--secondary-text-color);
}
.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: var(--ha-space-4);
inset-inline-end: initial;
direction: var(--direction);
}
.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label {
inset-inline-start: 48px;
inset-inline-end: initial;
direction: var(--direction);
}
.mdc-select .mdc-select__anchor {
padding-inline-start: var(--ha-space-4);
padding-inline-end: 0px;
direction: var(--direction);
}
.mdc-select__anchor .mdc-floating-label--float-above {
transform-origin: var(--float-start);
}
.mdc-select__selected-text-container {
padding-inline-end: var(--select-selected-text-padding-end, 0px);
}
:host([clearable]) .mdc-select__selected-text-container {
padding-inline-end: var(
--select-selected-text-padding-end,
var(--ha-space-4)
);
}
ha-icon-button {
position: absolute;
top: 10px;
right: 28px;
--mdc-icon-button-size: 36px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
inset-inline-start: initial;
inset-inline-end: 28px;
direction: var(--direction);
}
.inline-arrow {
flex-grow: 0;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-select": HaSelect;
}
interface HASSDomEvents {
selected: { value: string | undefined };
}
}
@@ -5,16 +5,17 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { SelectOption, SelectSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-checkbox";
import "../ha-dropdown-item";
import "../ha-formfield";
import "../ha-generic-picker";
import "../ha-input-helper-text";
import "../ha-list-item";
import "../ha-radio";
import "../ha-select";
import "../ha-select-box";
@@ -230,15 +231,24 @@ export class HaSelectSelector extends LitElement {
return html`
<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.label ?? ""}
.value=${(this.value as string) ?? ""}
.value=${this.value ?? ""}
.helper=${this.helper ?? ""}
.disabled=${this.disabled}
.required=${this.required}
clearable
@closed=${stopPropagation}
@selected=${this._valueChanged}
.options=${options}
>
${options.map(
(item: SelectOption) => html`
<ha-list-item .value=${item.value} .disabled=${!!item.disabled}
>${item.label}</ha-list-item
>
`
)}
</ha-select>
`;
}
@@ -285,7 +295,7 @@ export class HaSelectSelector extends LitElement {
private _valueChanged(ev) {
ev.stopPropagation();
if (ev.detail?.value === undefined && this.value !== undefined) {
if (ev.detail?.index === -1 && this.value !== undefined) {
fireEvent(this, "value-changed", {
value: undefined,
});
@@ -375,7 +385,7 @@ export class HaSelectSelector extends LitElement {
ha-formfield {
display: block;
}
ha-dropdown-item[disabled] {
ha-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
ha-chip-set {
-1
View File
@@ -108,7 +108,6 @@ export class HaSettingsRow extends LitElement {
white-space: normal;
}
.prefix-wrap {
flex: 1;
display: var(--settings-row-prefix-display);
}
:host([narrow]) .prefix-wrap {
+16 -61
View File
@@ -492,22 +492,19 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
@mouseleave=${this._itemMouseLeave}
>
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
${this._updatesCount > 0 || this._issuesCount > 0
? html`
<span class="badge" slot="start">
${this._updatesCount + this._issuesCount}
</span>
`
${!this.alwaysExpand &&
(this._updatesCount > 0 || this._issuesCount > 0)
? html`<span class="badge" slot="start"
>${this._updatesCount + this._issuesCount}</span
>`
: nothing}
<span class="item-text" slot="headline"
>${this.hass.localize("panel.config")}</span
>
${this._updatesCount > 0 || this._issuesCount > 0
? html`
<span class="badge" slot="end"
>${this._updatesCount + this._issuesCount}</span
>
`
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
? html`<span class="badge" slot="end"
>${this._updatesCount + this._issuesCount}</span
>`
: nothing}
</ha-md-list-item>
`;
@@ -527,15 +524,13 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
type="button"
>
<ha-svg-icon slot="start" .path=${mdiBell}></ha-svg-icon>
${notificationCount > 0
? html`
<span class="badge" slot="start"> ${notificationCount} </span>
`
${!this.alwaysExpand && notificationCount > 0
? html`<span class="badge" slot="start">${notificationCount}</span>`
: nothing}
<span class="item-text" slot="headline"
>${this.hass.localize("ui.notification_drawer.title")}</span
>
${notificationCount > 0
${this.alwaysExpand && notificationCount > 0
? html`<span class="badge" slot="end">${notificationCount}</span>`
: nothing}
</ha-md-list-item>
@@ -744,8 +739,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
);
font-size: var(--ha-font-size-xl);
align-items: center;
overflow: hidden;
width: calc(56px + var(--safe-area-inset-left, 0px));
padding-left: calc(
var(--ha-space-1) + var(--safe-area-inset-left, 0px)
);
@@ -754,7 +747,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
);
padding-inline-end: initial;
padding-top: var(--safe-area-inset-top, 0px);
transition: width var(--ha-animation-duration-normal) ease;
}
:host([expanded]) .menu {
width: calc(256px + var(--safe-area-inset-left, 0px));
@@ -769,22 +761,15 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
margin-left: 3px;
margin-inline-start: 3px;
margin-inline-end: initial;
flex: 1;
min-width: 0;
max-width: 0;
opacity: 0;
transition:
max-width var(--ha-animation-duration-normal) ease,
opacity var(--ha-animation-duration-normal) ease;
width: 100%;
display: none;
}
:host([narrow]) .title {
margin: 0;
padding: 0 var(--ha-space-4);
}
:host([expanded]) .title {
max-width: 100%;
opacity: 1;
transition-delay: 0ms, 80ms;
display: initial;
}
.panels-list {
@@ -842,7 +827,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--ha-space-3);
--md-list-item-leading-icon-size: var(--ha-space-6);
transition: width var(--ha-animation-duration-normal) ease;
}
:host([expanded]) ha-md-list-item {
width: 248px;
@@ -883,22 +867,11 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
}
ha-md-list-item .item-text {
display: block;
max-width: 0;
opacity: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: none;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
transition:
max-width var(--ha-animation-duration-normal) ease,
opacity var(--ha-animation-duration-normal) ease;
}
:host([expanded]) ha-md-list-item .item-text {
max-width: 100%;
opacity: 1;
transition-delay: 0ms, 80ms;
display: block;
overflow: hidden;
text-overflow: ellipsis;
@@ -916,9 +889,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
background-color: var(--accent-color);
padding: 2px 6px;
color: var(--text-accent-color, var(--text-primary-color));
transition:
opacity var(--ha-animation-duration-normal) ease,
transform var(--ha-animation-duration-normal) ease;
}
ha-svg-icon + .badge {
@@ -930,12 +900,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
line-height: var(--ha-line-height-expanded);
padding: 0 var(--ha-space-1);
}
:host([expanded]) .badge[slot="start"],
:host(:not([expanded])) .badge[slot="end"] {
opacity: 0;
transform: scale(0.8);
pointer-events: none;
}
ha-md-list-item.user {
--md-list-item-leading-icon-size: var(--ha-space-10);
@@ -974,15 +938,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
-webkit-transform: scaleX(var(--scale-direction));
transform: scaleX(var(--scale-direction));
}
@media (prefers-reduced-motion: reduce) {
.menu,
ha-md-list-item,
ha-md-list-item .item-text,
.title {
transition: none;
}
}
`,
];
}
+36 -33
View File
@@ -2,14 +2,16 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeStateName } from "../common/entity/compute_state_name";
import { debounce } from "../common/util/debounce";
import type { STTEngine } from "../data/stt";
import { listSTTEngines } from "../data/stt";
import type { HomeAssistant } from "../types";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import { computeDomain } from "../common/entity/compute_domain";
const NONE = "__NONE_OPTION__";
@@ -59,30 +61,6 @@ export class HaSTTPicker extends LitElement {
value = NONE;
}
const options: HaSelectOption[] = this._engines
.filter((engine) => !engine.deprecated || engine.engine_id !== value)
.map((engine) => {
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return {
value: engine.engine_id,
label,
disabled: engine.supported_languages?.length === 0,
};
});
if (this.required || value === NONE) {
options.unshift({
value: NONE,
label: this.hass.localize("ui.components.stt-picker.none") || "None",
});
}
return html`
<ha-select
.label=${this.label ||
@@ -91,8 +69,33 @@ export class HaSTTPicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
.options=${options}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize("ui.components.stt-picker.none")}
</ha-list-item>`
: nothing}
${this._engines.map((engine) => {
if (engine.deprecated && engine.engine_id !== value) {
return nothing;
}
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass!.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return html`<ha-list-item
.value=${engine.engine_id}
.disabled=${engine.supported_languages?.length === 0}
>
${label}
</ha-list-item>`;
})}
</ha-select>
`;
}
@@ -141,17 +144,17 @@ export class HaSTTPicker extends LitElement {
}
`;
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
) {
return;
}
this.value = value === NONE ? undefined : value;
this.value = target.value === NONE ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "supported-languages-changed", {
value: this._engines!.find((engine) => engine.engine_id === this.value)
+2 -2
View File
@@ -53,7 +53,7 @@ import {
multiTermSortedSearch,
type FuseWeightedKey,
} from "../resources/fuseMultiTerm";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HomeAssistant } from "../types";
import { brandsUrl } from "../util/brands-url";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-generic-picker";
@@ -403,7 +403,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
`;
}
private _targetPicked(ev: ValueChangedEvent<string>) {
private _targetPicked(ev: CustomEvent<{ value: string }>) {
ev.stopPropagation();
const value = ev.detail.value;
if (value.startsWith(CREATE_ID)) {
+31 -27
View File
@@ -1,10 +1,11 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, nothing, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import type { HomeAssistant } from "../types";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-select";
import "./ha-list-item";
const DEFAULT_THEME = "default";
@@ -24,26 +25,6 @@ export class HaThemePicker extends LitElement {
@property({ type: Boolean }) public required = false;
protected render(): TemplateResult {
const options: HaSelectOption[] = Object.keys(
this.hass?.themes.themes || {}
).map((theme) => ({
value: theme,
}));
if (this.includeDefault) {
options.unshift({
value: DEFAULT_THEME,
label: "Home Assistant",
});
}
if (!this.required) {
options.unshift({
value: "remove",
label: this.hass!.localize("ui.components.theme-picker.no_theme"),
});
}
return html`
<ha-select
.label=${this.label ||
@@ -52,8 +33,31 @@ export class HaThemePicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
.options=${options}
></ha-select>
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`
<ha-list-item value="remove">
${this.hass!.localize("ui.components.theme-picker.no_theme")}
</ha-list-item>
`
: nothing}
${this.includeDefault
? html`
<ha-list-item .value=${DEFAULT_THEME}>
Home Assistant
</ha-list-item>
`
: nothing}
${Object.keys(this.hass!.themes.themes)
.sort()
.map(
(theme) =>
html`<ha-list-item .value=${theme}>${theme}</ha-list-item>`
)}
</ha-select>
`;
}
@@ -63,11 +67,11 @@ export class HaThemePicker extends LitElement {
}
`;
private _changed(ev: HaSelectSelectEvent): void {
if (!this.hass || ev.detail.value === "") {
private _changed(ev): void {
if (!this.hass || ev.target.value === "") {
return;
}
this.value = ev.detail.value === "remove" ? undefined : ev.detail.value;
this.value = ev.target.value === "remove" ? undefined : ev.target.value;
fireEvent(this, "value-changed", { value: this.value });
}
}
-9
View File
@@ -36,19 +36,10 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
transition:
width var(--ha-animation-duration-normal) ease,
padding-left var(--ha-animation-duration-normal) ease,
padding-right var(--ha-animation-duration-normal) ease;
}
:host([narrow]) .mdc-top-app-bar {
padding-left: var(--safe-area-inset-left);
}
@media (prefers-reduced-motion: reduce) {
.mdc-top-app-bar {
transition: none;
}
}
.mdc-top-app-bar__title {
font-size: var(--ha-font-size-xl);
padding-inline-start: var(--ha-space-6);
+36 -33
View File
@@ -2,14 +2,16 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeStateName } from "../common/entity/compute_state_name";
import { debounce } from "../common/util/debounce";
import type { TTSEngine } from "../data/tts";
import { listTTSEngines } from "../data/tts";
import type { HomeAssistant } from "../types";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import { computeDomain } from "../common/entity/compute_domain";
const NONE = "__NONE_OPTION__";
@@ -59,30 +61,6 @@ export class HaTTSPicker extends LitElement {
value = NONE;
}
const options: HaSelectOption[] = this._engines
.filter((engine) => !engine.deprecated || engine.engine_id === value)
.map((engine) => {
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return {
value: engine.engine_id,
label,
disabled: engine.supported_languages?.length === 0,
};
});
if (!this.required || value === NONE) {
options.unshift({
value: NONE,
label: this.hass.localize("ui.components.tts-picker.none"),
});
}
return html`
<ha-select
.label=${this.label ||
@@ -91,8 +69,33 @@ export class HaTTSPicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
.options=${options}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize("ui.components.tts-picker.none")}
</ha-list-item>`
: nothing}
${this._engines.map((engine) => {
if (engine.deprecated && engine.engine_id !== value) {
return nothing;
}
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass!.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return html`<ha-list-item
.value=${engine.engine_id}
.disabled=${engine.supported_languages?.length === 0}
>
${label}
</ha-list-item>`;
})}
</ha-select>
`;
}
@@ -141,17 +144,17 @@ export class HaTTSPicker extends LitElement {
}
`;
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
) {
return;
}
this.value = value === NONE ? undefined : value;
this.value = target.value === NONE ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "supported-languages-changed", {
value: this._engines!.find((engine) => engine.engine_id === this.value)
+37 -24
View File
@@ -1,13 +1,15 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { debounce } from "../common/util/debounce";
import type { TTSVoice } from "../data/tts";
import { listTTSVoices } from "../data/tts";
import type { HomeAssistant } from "../types";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
const NONE = "__NONE_OPTION__";
@@ -29,25 +31,14 @@ export class HaTTSVoicePicker extends LitElement {
@state() _voices?: TTSVoice[] | null;
@query("ha-select") private _select?: HaSelect;
protected render() {
if (!this._voices) {
return nothing;
}
const value =
this.value ?? (this.required ? this._voices[0]?.voice_id : NONE);
const options: HaSelectOption[] = (this._voices || []).map((voice) => ({
value: voice.voice_id,
label: voice.name,
}));
if (!this.required || !this.value) {
options.unshift({
value: NONE,
label: this.hass!.localize("ui.components.tts-voice-picker.none"),
});
}
return html`
<ha-select
.label=${this.label ||
@@ -56,8 +47,21 @@ export class HaTTSVoicePicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
.options=${options}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize("ui.components.tts-voice-picker.none")}
</ha-list-item>`
: nothing}
${this._voices.map(
(voice) =>
html`<ha-list-item .value=${voice.voice_id}>
${voice.name}
</ha-list-item>`
)}
</ha-select>
`;
}
@@ -98,25 +102,34 @@ export class HaTTSVoicePicker extends LitElement {
}
}
protected updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
if (
changedProperties.has("_voices") &&
this._select?.value !== this.value
) {
this._select?.layoutOptions();
fireEvent(this, "value-changed", { value: this._select?.value });
}
}
static styles = css`
ha-select {
width: 100%;
text-align: start;
display: block;
}
`;
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
private _changed(ev): void {
const target = ev.target as HaSelect;
if (
!this.hass ||
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
) {
return;
}
this.value = value === NONE ? undefined : value;
this.value = target.value === NONE ? undefined : target.value;
fireEvent(this, "value-changed", { value: this.value });
}
}
@@ -288,19 +288,10 @@ export class TopAppBarBaseBase extends BaseElement {
);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
transition:
width var(--ha-animation-duration-normal) ease,
padding-left var(--ha-animation-duration-normal) ease,
padding-right var(--ha-animation-duration-normal) ease;
}
:host([narrow]) .mdc-top-app-bar {
padding-left: var(--safe-area-inset-left);
}
@media (prefers-reduced-motion: reduce) {
.mdc-top-app-bar {
transition: none;
}
}
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
box-shadow: none;
}
@@ -1,9 +1,11 @@
import { mdiClose } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { supportsFeature } from "../../common/entity/supports-feature";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import { extractApiErrorMessage } from "../../data/hassio/common";
@@ -17,9 +19,8 @@ import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-alert";
import "../ha-button";
import "../ha-dialog-footer";
import "../ha-dialog";
import "../ha-dialog-header";
import "../ha-wa-dialog";
import "./ha-media-player-toggle";
import type { JoinMediaPlayersDialogParams } from "./show-join-media-players-dialog";
@@ -37,11 +38,8 @@ class DialogJoinMediaPlayers extends LitElement {
@state() private _error?: string;
@state() private _open = false;
public showDialog(params: JoinMediaPlayersDialogParams): void {
this._entityId = params.entityId;
this._open = true;
const stateObj = this.hass.states[params.entityId] as
| MediaPlayerEntity
@@ -56,11 +54,6 @@ class DialogJoinMediaPlayers extends LitElement {
}
public closeDialog() {
this._open = false;
}
private _dialogClosed(): void {
this._open = false;
this._entityId = undefined;
this._selectedEntities = [];
this._groupMembers = [];
@@ -75,18 +68,23 @@ class DialogJoinMediaPlayers extends LitElement {
}
const entityId = this._entityId;
const stateObj = this.hass.states[entityId] as HassEntity | undefined;
const name = (stateObj && computeStateName(stateObj)) || entityId;
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
flexcontent
@closed=${this._dialogClosed}
<ha-dialog
open
scrimClickAction
escapeKeyAction
flexContent
.heading=${name}
@closed=${this.closeDialog}
>
<ha-dialog-header show-border slot="header">
<ha-dialog-header show-border slot="heading">
<ha-icon-button
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
data-dialog="close"
dialogAction="close"
slot="navigationIcon"
></ha-icon-button>
<span slot="title"
@@ -120,23 +118,21 @@ class DialogJoinMediaPlayers extends LitElement {
></ha-media-player-toggle>`
)}
</div>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
.disabled=${!!this._submitting}
slot="primaryAction"
@click=${this._submit}
>
${this.hass.localize("ui.common.apply")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
.disabled=${!!this._submitting}
slot="primaryAction"
@click=${this._submit}
>
${this.hass.localize("ui.common.apply")}
</ha-button>
</ha-dialog>
`;
}
@@ -221,7 +217,6 @@ class DialogJoinMediaPlayers extends LitElement {
.content {
display: flex;
flex-direction: column;
padding-top: var(--ha-space-6);
row-gap: var(--ha-space-4);
}
@@ -24,8 +24,6 @@ import "../ha-icon-button";
import "./hat-logbook-note";
import type { NodeInfo } from "./hat-script-graph";
import { traceTabStyles } from "./trace-tab-styles";
import type { Trigger } from "../../data/automation";
import { migrateAutomationTrigger } from "../../data/automation";
const TRACE_PATH_TABS = [
"step_config",
@@ -168,9 +166,7 @@ export class HaTracePathDetails extends LitElement {
: selectedType === "trigger"
? html`<h2>
${describeTrigger(
migrateAutomationTrigger({
...currentDetail,
}) as Trigger,
currentDetail,
this.hass,
this._entityReg
)}
-12
View File
@@ -1,5 +1,4 @@
import type { HomeAssistant } from "../../types";
import type { LovelaceRawConfig } from "./config/types";
export type LovelaceDashboard =
| LovelaceYamlDashboard
@@ -35,11 +34,6 @@ export interface LovelaceDashboardCreateParams extends LovelaceDashboardMutableP
mode: "storage";
}
export interface LovelaceAIDashboardResult {
conversation_id: string;
config: LovelaceRawConfig;
}
export const fetchDashboards = (
hass: HomeAssistant
): Promise<LovelaceDashboard[]> =>
@@ -72,9 +66,3 @@ export const deleteDashboard = (hass: HomeAssistant, id: string) =>
type: "lovelace/dashboards/delete",
dashboard_id: id,
});
export const generateDashboardWithAI = (hass: HomeAssistant, prompt: string) =>
hass.callWS<LovelaceAIDashboardResult>({
type: "lovelace/config/generate",
prompt,
});
@@ -13,6 +13,7 @@ import "../../../components/ha-cover-controls";
import "../../../components/ha-cover-tilt-controls";
import "../../../components/ha-date-input";
import "../../../components/ha-humidifier-state";
import "../../../components/ha-list-item";
import "../../../components/ha-select";
import "../../../components/ha-slider";
import "../../../components/ha-time-input";
@@ -295,11 +296,17 @@ class EntityPreviewRow extends LitElement {
.label=${computeStateName(stateObj)}
.value=${stateObj.state}
.disabled=${isUnavailableState(stateObj.state)}
.options=${stateObj.attributes.options?.map((option) => ({
value: option,
label: this.hass!.formatEntityState(stateObj, option),
})) || []}
naturalMenuWidth
>
${stateObj.attributes.options
? stateObj.attributes.options.map(
(option) => html`
<ha-list-item .value=${option}>
${this.hass!.formatEntityState(stateObj, option)}
</ha-list-item>
`
)
: ""}
</ha-select>
`;
}
@@ -1,12 +1,17 @@
import { mdiClose } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button-toggle";
import "../../../../components/ha-wa-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import {
getMobileCloseToBottomAnimation,
getMobileOpenFromBottomAnimation,
} from "../../../../components/ha-md-dialog";
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type { LightColor, LightEntity } from "../../../../data/light";
import {
@@ -36,7 +41,7 @@ class DialogLightColorFavorite extends LitElement {
@state() private _modes: LightPickerMode[] = [];
@state() private _open = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(
dialogParams: LightColorFavoriteDialogParams
@@ -45,11 +50,10 @@ class DialogLightColorFavorite extends LitElement {
this._dialogParams = dialogParams;
this._color = dialogParams.initialColor ?? this._computeCurrentColor();
this._updateModes();
this._open = true;
}
public closeDialog(): void {
this._open = false;
this._dialog?.close();
}
private _updateModes() {
@@ -116,8 +120,16 @@ class DialogLightColorFavorite extends LitElement {
);
}
private async _cancel() {
this._dialogParams?.cancel?.();
}
private _cancelDialog() {
this._cancel();
this.closeDialog();
}
private _dialogClosed(): void {
this._open = false;
this._dialogParams = undefined;
this._entry = undefined;
this._color = undefined;
@@ -126,7 +138,7 @@ class DialogLightColorFavorite extends LitElement {
private async _save() {
if (!this._color) {
this.closeDialog();
this._cancel();
return;
}
this._dialogParams?.submit?.(this._color);
@@ -147,76 +159,83 @@ class DialogLightColorFavorite extends LitElement {
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
.headerTitle=${this._dialogParams?.title}
<ha-md-dialog
open
@cancel=${this._cancel}
@closed=${this._dialogClosed}
aria-labelledby="dialog-light-color-favorite-title"
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
.getCloseAnimation=${getMobileCloseToBottomAnimation}
>
<div class="header">
${this._modes.length > 1
? html`
<div class="modes">
${this._modes.map(
(value) => html`
<ha-icon-button-toggle
border-only
.selected=${value === this._mode}
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
.mode=${value}
@click=${this._modeChanged}
>
<span
class="wheel ${classMap({ [value]: true })}"
></span>
</ha-icon-button-toggle>
`
)}
</div>
`
: nothing}
</div>
<div class="content">
${this._mode === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-temp-picker>
`
: nothing}
${this._mode === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-rgb-picker>
`
: nothing}
</div>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title" id="dialog-light-color-favorite-title"
>${this._dialogParams?.title}</span
>
</ha-dialog-header>
<div slot="content">
<div class="header">
${this._modes.length > 1
? html`
<div class="modes">
${this._modes.map(
(value) => html`
<ha-icon-button-toggle
border-only
.selected=${value === this._mode}
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
.mode=${value}
@click=${this._modeChanged}
>
<span
class="wheel ${classMap({ [value]: true })}"
></span>
</ha-icon-button-toggle>
`
)}
</div>
`
: nothing}
</div>
<div class="content">
${this._mode === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-temp-picker>
`
: nothing}
${this._mode === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-rgb-picker>
`
: nothing}
</div>
</div>
<div slot="actions">
<ha-button appearance="plain" @click=${this._cancelDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${!this._color}
<ha-button @click=${this._save} .disabled=${!this._color}
>${this.hass.localize("ui.common.save")}</ha-button
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
</div>
</ha-md-dialog>
`;
}
@@ -224,18 +243,23 @@ class DialogLightColorFavorite extends LitElement {
return [
haStyleDialog,
css`
ha-wa-dialog {
--ha-dialog-width-md: 420px; /* prevent width jumps when switching modes */
--ha-dialog-max-height: min(
ha-md-dialog {
min-width: 420px; /* prevent width jumps when switching modes */
max-height: min(
600px,
100% - 48px
); /* prevent scrolling on desktop */
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-wa-dialog {
--ha-dialog-width-md: 100vw;
--ha-dialog-max-height: calc(100% - 100px);
ha-md-dialog {
min-width: 100%;
min-height: auto;
max-height: calc(100% - 100px);
margin-bottom: 0;
--md-dialog-container-shape-start-start: 28px;
--md-dialog-container-shape-start-end: 28px;
}
}
@@ -7,6 +7,7 @@ export interface LightColorFavoriteDialogParams {
title: string;
initialColor?: LightColor;
submit?: (color?: LightColor) => void;
cancel?: () => void;
}
export const loadLightColorFavoriteDialog = () =>
@@ -17,6 +18,7 @@ export const showLightColorFavoriteDialog = (
dialogParams: LightColorFavoriteDialogParams
) =>
new Promise<LightColor | null>((resolve) => {
const origCancel = dialogParams.cancel;
const origSubmit = dialogParams.submit;
fireEvent(element, "show-dialog", {
@@ -24,6 +26,12 @@ export const showLightColorFavoriteDialog = (
dialogImport: loadLightColorFavoriteDialog,
dialogParams: {
...dialogParams,
cancel: () => {
resolve(null);
if (origCancel) {
origCancel();
}
},
submit: (color: LightColor) => {
resolve(color);
if (origSubmit) {
@@ -1,19 +1,23 @@
import { mdiPlay, mdiStop } from "@mdi/js";
import { mdiClose, mdiPlay, mdiStop } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import "../../../../components/ha-button";
import "../../../../components/ha-control-button";
import type { HaSelectSelectEvent } from "../../../../components/ha-select";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import {
getMobileCloseToBottomAnimation,
getMobileOpenFromBottomAnimation,
} from "../../../../components/ha-md-dialog";
import "../../../../components/ha-select";
import "../../../../components/ha-textfield";
import "../../../../components/ha-wa-dialog";
import { SirenEntityFeature } from "../../../../data/siren";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@@ -24,25 +28,23 @@ class MoreInfoSirenAdvancedControls extends LitElement {
@state() _stateObj?: HassEntity;
@state() private _open = false;
@state() _tone?: string;
@state() _volume?: number;
@state() _duration?: number;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public showDialog({ stateObj }: { stateObj: HassEntity }) {
this._stateObj = stateObj;
this._open = true;
}
public closeDialog(): void {
this._open = false;
this._dialog?.close();
}
private _dialogClosed(): void {
this._open = false;
this._stateObj = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -63,33 +65,50 @@ class MoreInfoSirenAdvancedControls extends LitElement {
SirenEntityFeature.DURATION
);
return html`
<ha-wa-dialog
.open=${this._open}
.hass=${this.hass}
header-title=${this.hass.localize(
"ui.components.siren.advanced_controls"
)}
<ha-md-dialog
open
@closed=${this._dialogClosed}
aria-labelledby="dialog-light-color-favorite-title"
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
.getCloseAnimation=${getMobileCloseToBottomAnimation}
>
<div>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title" id="dialog-light-color-favorite-title"
>${this.hass.localize(
"ui.components.siren.advanced_controls"
)}</span
>
</ha-dialog-header>
<div slot="content">
<div class="options">
${supportsTones
? html`
<ha-select
.label=${this.hass.localize("ui.components.siren.tone")}
@selected=${this._handleToneChange}
@closed=${stopPropagation}
@change=${this._handleToneChange}
.value=${this._tone}
.options=${Object.entries(
this._stateObj!.attributes.available_tones
).map(([toneId, toneName]) => ({
value: Array.isArray(
this._stateObj!.attributes.available_tones
)
? toneName
: toneId,
label: toneName,
}))}
>
${Object.entries(
this._stateObj.attributes.available_tones
).map(
([toneId, toneName]) => html`
<ha-list-item
.value=${Array.isArray(
this._stateObj!.attributes.available_tones
)
? toneName
: toneId}
>${toneName}</ha-list-item
>
`
)}
</ha-select>
`
: nothing}
@@ -134,21 +153,17 @@ class MoreInfoSirenAdvancedControls extends LitElement {
</ha-control-button>
</div>
</div>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
>
<div slot="actions">
<ha-button @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
</div>
</ha-md-dialog>
`;
}
private _handleToneChange(ev: HaSelectSelectEvent) {
this._tone = ev.detail.value;
private _handleToneChange(ev) {
this._tone = ev.target.value;
}
private _handleVolumeChange(ev) {
@@ -5,7 +5,7 @@ import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { setDateValue } from "../../../data/date";
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-date")
class MoreInfoDate extends LitElement {
@@ -31,7 +31,7 @@ class MoreInfoDate extends LitElement {
`;
}
private _dateChanged(ev: ValueChangedEvent<string>): void {
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
if (ev.detail.value) {
setDateValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
}
@@ -6,7 +6,7 @@ import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { setDateTimeValue } from "../../../data/datetime";
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-datetime")
class MoreInfoDatetime extends LitElement {
@@ -45,7 +45,7 @@ class MoreInfoDatetime extends LitElement {
ev.stopPropagation();
}
private _timeChanged(ev: ValueChangedEvent<string>): void {
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
if (ev.detail.value) {
const dateObj = new Date(this.stateObj!.state);
const newTime = ev.detail.value.split(":").map(Number);
@@ -55,7 +55,7 @@ class MoreInfoDatetime extends LitElement {
}
}
private _dateChanged(ev: ValueChangedEvent<string>): void {
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
if (ev.detail.value) {
const dateObj = new Date(this.stateObj!.state);
const newDate = ev.detail.value.split("-").map(Number);
@@ -8,7 +8,7 @@ import {
setInputDateTimeValue,
stateToIsoDateString,
} from "../../../data/input_datetime";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-input_datetime")
class MoreInfoInputDatetime extends LitElement {
@@ -55,7 +55,7 @@ class MoreInfoInputDatetime extends LitElement {
ev.stopPropagation();
}
private _timeChanged(ev: ValueChangedEvent<string>): void {
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
setInputDateTimeValue(
this.hass!,
this.stateObj!.entity_id,
@@ -66,7 +66,7 @@ class MoreInfoInputDatetime extends LitElement {
);
}
private _dateChanged(ev: ValueChangedEvent<string>): void {
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
setInputDateTimeValue(
this.hass!,
this.stateObj!.entity_id,
@@ -202,11 +202,12 @@ class MoreInfoMediaPlayer extends LitElement {
return nothing;
}
return html`<ha-dropdown @wa-select=${this._handleSourceChange}>
return html`<ha-dropdown>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize(`ui.card.media_player.source`)}
.path=${mdiLoginVariant}
@wa-select=${this._handleSourceChange}
>
</ha-icon-button>
${this.stateObj.attributes.source_list!.map(
@@ -1,11 +1,12 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { HaSelectSelectEvent } from "../../../components/ha-select";
import "../../../components/ha-select";
import type { RemoteEntity } from "../../../data/remote";
import { REMOTE_SUPPORT_ACTIVITY } from "../../../data/remote";
import type { HomeAssistant } from "../../../types";
import "../../../components/ha-select";
import "../../../components/ha-list-item";
@customElement("more-info-remote")
class MoreInfoRemote extends LitElement {
@@ -29,24 +30,30 @@ class MoreInfoRemote extends LitElement {
)}
.value=${stateObj.attributes.current_activity || ""}
@selected=${this._handleActivityChanged}
.options=${stateObj.attributes.activity_list?.map((activity) => ({
value: activity,
label: this.hass!.formatEntityAttributeValue(
stateObj,
"activity",
activity
),
}))}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
>
${stateObj.attributes.activity_list?.map(
(activity) => html`
<ha-list-item .value=${activity}>
${this.hass.formatEntityAttributeValue(
stateObj,
"activity",
activity
)}
</ha-list-item>
`
)}
</ha-select>
`
: nothing}
`;
}
private _handleActivityChanged(ev: HaSelectSelectEvent) {
private _handleActivityChanged(ev) {
const oldVal = this.stateObj!.attributes.current_activity;
const newVal = ev.detail.value;
const newVal = ev.target.value;
if (!newVal || oldVal === newVal) {
return;
@@ -5,7 +5,7 @@ import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
import { setTimeValue } from "../../../data/time";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-time")
class MoreInfoTime extends LitElement {
@@ -35,7 +35,7 @@ class MoreInfoTime extends LitElement {
ev.stopPropagation();
}
private _timeChanged(ev: ValueChangedEvent<string>): void {
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
if (ev.detail.value) {
setTimeValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
}
@@ -11,12 +11,13 @@ import {
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/entity/ha-battery-icon";
import type { HaSelectSelectEvent } from "../../../components/ha-select";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-select";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../../data/entity/entity_registry";
@@ -171,17 +172,21 @@ class MoreInfoVacuum extends LitElement {
.disabled=${stateObj.state === UNAVAILABLE}
.value=${stateObj.attributes.fan_speed}
@selected=${this._handleFanSpeedChanged}
.options=${stateObj.attributes.fan_speed_list!.map(
(mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
stateObj,
"fan_speed",
mode
),
})
)}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
>
${stateObj.attributes.fan_speed_list!.map(
(mode) => html`
<ha-list-item .value=${mode}>
${this.hass.formatEntityAttributeValue(
stateObj,
"fan_speed",
mode
)}
</ha-list-item>
`
)}
</ha-select>
<div
style="justify-content: center; align-self: center; padding-top: 1.3em"
@@ -286,9 +291,9 @@ class MoreInfoVacuum extends LitElement {
});
}
private _handleFanSpeedChanged(ev: HaSelectSelectEvent) {
private _handleFanSpeedChanged(ev) {
const oldVal = this.stateObj!.attributes.fan_speed;
const newVal = ev.detail.value;
const newVal = ev.target.value;
if (!newVal || oldVal === newVal) {
return;
+20 -84
View File
@@ -47,7 +47,6 @@ import {
type ActionCommandComboBoxItem,
type NavigationComboBoxItem,
} from "../../data/quick_bar";
import type { RelatedResult } from "../../data/search";
import {
multiTermSortedSearch,
type FuseWeightedKey,
@@ -76,8 +75,6 @@ export class QuickBar extends LitElement {
@state() private _opened = false;
@state() private _relatedResult?: RelatedResult;
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
private get _showEntityId() {
@@ -103,9 +100,6 @@ export class QuickBar extends LitElement {
this._initialize();
this._selectedSection = params.mode;
this._showHint = params.showHint ?? false;
this._relatedResult = params.contextItem ? params.related : undefined;
this._open = true;
}
@@ -423,7 +417,6 @@ export class QuickBar extends LitElement {
this._selectedSection = section as QuickBarSection | undefined;
return this._getItemsMemoized(
this._configEntryLookup,
this._relatedResult,
searchString,
this._selectedSection
);
@@ -432,12 +425,10 @@ export class QuickBar extends LitElement {
private _getItemsMemoized = memoizeOne(
(
configEntryLookup: Record<string, ConfigEntry>,
relatedResult: RelatedResult | undefined,
filter?: string,
section?: QuickBarSection
) => {
const items: (string | PickerComboBoxItem)[] = [];
const relatedIdSets = this._getRelatedIdSets(relatedResult);
if (!section || section === "navigate") {
let navigateItems = this._generateNavigationCommandsMemoized(
@@ -486,29 +477,17 @@ export class QuickBar extends LitElement {
}
if (!section || section === "entity") {
let entityItems = this._getEntitiesMemoized(this.hass);
// Mark related items
if (relatedIdSets.entities.size > 0) {
entityItems = entityItems.map((item) => ({
...item,
isRelated: relatedIdSets.entities.has(
(item as EntityComboBoxItem).stateObj?.entity_id || ""
),
}));
}
let entityItems = this._getEntitiesMemoized(this.hass).sort(
this._sortBySortingLabel
);
if (filter) {
entityItems = this._sortRelatedFirst(
this._filterGroup(
"entity",
entityItems,
filter,
entityComboBoxKeys
) as EntityComboBoxItem[]
);
} else {
entityItems = this._sortRelatedByLabel(entityItems);
entityItems = this._filterGroup(
"entity",
entityItems,
filter,
entityComboBoxKeys
) as EntityComboBoxItem[];
}
if (!section && entityItems.length) {
@@ -525,25 +504,15 @@ export class QuickBar extends LitElement {
let deviceItems = this._getDevicesMemoized(
this.hass,
configEntryLookup
);
// Mark related items
if (relatedIdSets.devices.size > 0) {
deviceItems = deviceItems.map((item) => {
const deviceId = item.id.split(SEPARATOR)[1];
return {
...item,
isRelated: relatedIdSets.devices.has(deviceId || ""),
};
});
}
).sort(this._sortBySortingLabel);
if (filter) {
deviceItems = this._sortRelatedFirst(
this._filterGroup("device", deviceItems, filter, deviceComboBoxKeys)
deviceItems = this._filterGroup(
"device",
deviceItems,
filter,
deviceComboBoxKeys
);
} else {
deviceItems = this._sortRelatedByLabel(deviceItems);
}
if (!section && deviceItems.length) {
@@ -559,23 +528,13 @@ export class QuickBar extends LitElement {
if (this.hass.user?.is_admin && (!section || section === "area")) {
let areaItems = this._getAreasMemoized(this.hass);
// Mark related items
if (relatedIdSets.areas.size > 0) {
areaItems = areaItems.map((item) => {
const areaId = item.id.split(SEPARATOR)[1];
return {
...item,
isRelated: relatedIdSets.areas.has(areaId || ""),
};
});
}
if (filter) {
areaItems = this._sortRelatedFirst(
this._filterGroup("area", areaItems, filter, areaComboBoxKeys)
areaItems = this._filterGroup(
"area",
areaItems,
filter,
areaComboBoxKeys
);
} else {
areaItems = this._sortRelatedByLabel(areaItems);
}
if (!section && areaItems.length) {
@@ -592,12 +551,6 @@ export class QuickBar extends LitElement {
}
);
private _getRelatedIdSets = memoizeOne((related?: RelatedResult) => ({
entities: new Set(related?.entity || []),
devices: new Set(related?.device || []),
areas: new Set(related?.area || []),
}));
private _getEntitiesMemoized = memoizeOne((hass: HomeAssistant) =>
getEntities(
hass,
@@ -701,23 +654,6 @@ export class QuickBar extends LitElement {
this.hass.locale.language
);
private _sortRelatedByLabel = (items: PickerComboBoxItem[]) =>
[...items].sort((a, b) => {
if (a.isRelated && !b.isRelated) return -1;
if (!a.isRelated && b.isRelated) return 1;
return this._sortBySortingLabel(a, b);
});
private _sortRelatedFirst = (items: PickerComboBoxItem[]) =>
[...items].sort((a, b) => {
const aRelated = Boolean(a.isRelated);
const bRelated = Boolean(b.isRelated);
if (aRelated === bRelated) {
return 0;
}
return aRelated ? -1 : 1;
});
// #endregion data
// #region interaction
@@ -1,5 +1,4 @@
import { fireEvent } from "../../common/dom/fire_event";
import type { ItemType, RelatedResult } from "../../data/search";
import { closeDialog } from "../make-dialog-manager";
export type QuickBarSection =
@@ -9,17 +8,10 @@ export type QuickBarSection =
| "navigate"
| "command";
export interface QuickBarContextItem {
itemType: ItemType;
itemId: string;
}
export interface QuickBarParams {
entityFilter?: string;
mode?: QuickBarSection;
showHint?: boolean;
contextItem?: QuickBarContextItem;
related?: RelatedResult;
}
export const loadQuickBar = () => import("./ha-quick-bar");
+2 -2
View File
@@ -30,7 +30,7 @@ import {
getPanelIconPath,
getPanelTitle,
} from "../../data/panel";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import type { HomeAssistant } from "../../types";
import { showConfirmationDialog } from "../generic/show-dialog-box";
@customElement("dialog-edit-sidebar")
@@ -206,7 +206,7 @@ class DialogEditSidebar extends LitElement {
`;
}
private _changed(ev: ValueChangedEvent<DisplayValue>): void {
private _changed(ev: CustomEvent<{ value: DisplayValue }>): void {
const { order = [], hidden = [] } = ev.detail.value;
this._order = [...order];
this._hidden = [...hidden];
@@ -5,8 +5,8 @@ import "../../components/ha-button";
import "../../components/ha-spinner";
import { testAssistSatelliteConnection } from "../../data/assist_satellite";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { AssistantSetupStyles } from "./styles";
import { documentationUrl } from "../../util/documentation-url";
@customElement("ha-voice-assistant-setup-step-check")
export class HaVoiceAssistantSetupStepCheck extends LitElement {
@@ -58,7 +58,7 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement {
"/voice_control/troubleshooting/#i-dont-get-a-voice-response"
)}
>
${this.hass.localize(
>${this.hass.localize(
"ui.panel.config.voice_assistants.satellite_wizard.check.help"
)}</ha-button
>
@@ -4,11 +4,11 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import type { HaSelectSelectEvent } from "../../components/ha-select";
import {
computeDeviceName,
computeDeviceNameDisplay,
} from "../../common/entity/compute_device_name";
import "../../components/ha-list-item";
import "../../components/ha-select";
import "../../components/ha-tts-voice-picker";
import type { AssistPipeline } from "../../data/assist_pipeline";
@@ -115,15 +115,19 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
.label=${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.pipeline.detail.form.wake_word_id"
)}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this.assistConfiguration.active_wake_words[0]}
@selected=${this._wakeWordPicked}
.options=${this.assistConfiguration.available_wake_words.map(
(wakeword) => ({
value: wakeword.id,
label: wakeword.wake_word,
})
>
${this.assistConfiguration.available_wake_words.map(
(wakeword) =>
html`<ha-list-item .value=${wakeword.id}>
${wakeword.wake_word}
</ha-list-item>`
)}
></ha-select>
</ha-select>
<ha-button
appearance="plain"
size="small"
@@ -147,17 +151,16 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
)}
@closed=${stopPropagation}
.value=${pipelineEntity?.state}
fixedMenuPosition
naturalMenuWidth
@selected=${this._pipelinePicked}
.options=${pipelineEntity?.attributes.options.map(
(pipeline) => ({
value: pipeline,
label: this.hass.formatEntityState(
pipelineEntity,
pipeline
),
})
)}
>
${pipelineEntity?.attributes.options.map(
(pipeline) =>
html`<ha-list-item .value=${pipeline}>
${this.hass.formatEntityState(pipelineEntity, pipeline)}
</ha-list-item>`
)}
</ha-select>
<ha-button
appearance="plain"
@@ -232,19 +235,16 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
this._deviceName = ev.target.value;
}
private async _wakeWordPicked(ev: HaSelectSelectEvent) {
const option = ev.detail.value;
if (this.assistConfiguration) {
this.assistConfiguration.active_wake_words = [option];
}
private async _wakeWordPicked(ev) {
const option = ev.target.value;
await setWakeWords(this.hass, this.assistEntityId!, [option]);
}
private _pipelinePicked(ev: HaSelectSelectEvent) {
private _pipelinePicked(ev) {
const stateObj = this.hass!.states[
this.assistConfiguration!.pipeline_entity_id
] as InputSelectEntity;
const option = ev.detail.value;
const option = ev.target.value;
if (
option === stateObj.state ||
!stateObj.attributes.options.includes(option)
@@ -384,11 +384,6 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
.row ha-button {
width: 82px;
}
ha-select {
display: block;
text-align: start;
}
`,
];
}
+2 -2
View File
@@ -3,8 +3,8 @@ import type { Panels } from "../types";
export const demoPanels: Panels = {
lovelace: {
component_name: "lovelace",
icon: "mdi:view-dashboard",
title: "demo",
icon: null,
title: null,
config: { mode: "storage" },
url_path: "lovelace",
},
+2 -6
View File
@@ -256,10 +256,6 @@ export const provideHass = (
darkMode: false,
theme: "default",
},
selectedTheme: {
theme: "default",
dark: false,
},
panels: demoPanels,
services: demoServices,
user: {
@@ -352,7 +348,7 @@ export const provideHass = (
mockTheme(theme) {
invalidateThemeCache();
hass().updateHass({
selectedTheme: { theme: theme ? "mock" : "default", dark: false },
selectedTheme: { theme: theme ? "mock" : "default" },
themes: {
...hass().themes,
themes: {
@@ -365,7 +361,7 @@ export const provideHass = (
document.documentElement,
themes,
selectedTheme!.theme,
{ dark: false },
undefined,
true
);
},
+2 -2
View File
@@ -29,11 +29,11 @@
}
}
::view-transition-group(launch-screen) {
animation-duration: var(--ha-animation-duration-slow, 350ms);
animation-duration: var(--ha-animation-base-duration, 350ms);
animation-timing-function: ease-out;
}
::view-transition-old(launch-screen) {
animation: fade-out var(--ha-animation-duration-slow, 350ms) ease-out;
animation: fade-out var(--ha-animation-base-duration, 350ms) ease-out;
}
html {
background-color: var(--primary-background-color, #fafafa);
+1
View File
@@ -144,6 +144,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
.label=${""}
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<a
href="https://www.home-assistant.io/getting-started/onboarding/"
@@ -1,17 +1,19 @@
import type { SelectedDetail } from "@material/mwc-list";
import { TZDate } from "@date-fns/tz";
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { ByWeekday, Options, WeekdayStr } from "rrule";
import { customElement, property, query, state } from "lit/decorators";
import type { Options, WeekdayStr, ByWeekday } from "rrule";
import { RRule, Weekday } from "rrule";
import { formatDate, formatTime } from "../../common/datetime/calc_date";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import { stopPropagation } from "../../common/dom/stop_propagation";
import type { LocalizeKeys } from "../../common/translations/localize";
import "../../components/chips/ha-chip-set";
import "../../components/chips/ha-filter-chip";
import "../../components/ha-date-input";
import "../../components/ha-list-item";
import "../../components/ha-select";
import type { HaSelectSelectEvent } from "../../components/ha-select";
import type { HaSelect } from "../../components/ha-select";
import "../../components/ha-textfield";
import type { HomeAssistant } from "../../types";
import type {
@@ -31,6 +33,7 @@ import {
ruleByWeekDay,
untilValue,
} from "./recurrence";
import { formatDate, formatTime } from "../../common/datetime/calc_date";
@customElement("ha-recurrence-rule-editor")
export class RecurrenceRuleEditor extends LitElement {
@@ -68,6 +71,8 @@ export class RecurrenceRuleEditor extends LitElement {
@state() private _untilDay?: Date;
@query("#monthly") private _monthlyRepeatSelect!: HaSelect;
private _allWeekdays?: WeekdayStr[];
private _monthlyRepeatItems: MonthlyRepeatItem[] = [];
@@ -86,6 +91,14 @@ export class RecurrenceRuleEditor extends LitElement {
? getMonthlyRepeatItems(this.hass, this._interval, this.dtstart)
: [];
this._computeWeekday();
const selectElement = this._monthlyRepeatSelect;
if (selectElement) {
const oldSelected = selectElement.index;
selectElement.select(-1);
this.updateComplete.then(() => {
selectElement.select(changedProps.has("dtstart") ? 0 : oldSelected);
});
}
}
if (
@@ -171,16 +184,35 @@ export class RecurrenceRuleEditor extends LitElement {
id="freq"
label=${this.hass.localize("ui.components.calendar.event.repeat.label")}
@selected=${this._onRepeatSelected}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._freq}
.options=${["none", "yearly", "monthly", "weekly", "daily"].map(
(freq) => ({
value: freq,
label: this.hass.localize(
`ui.components.calendar.event.repeat.freq.${freq}` as LocalizeKeys
),
})
)}
></ha-select>
>
<ha-list-item value="none">
${this.hass.localize("ui.components.calendar.event.repeat.freq.none")}
</ha-list-item>
<ha-list-item value="yearly">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.yearly"
)}
</ha-list-item>
<ha-list-item value="monthly">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.monthly"
)}
</ha-list-item>
<ha-list-item value="weekly">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.weekly"
)}
</ha-list-item>
<ha-list-item value="daily">
${this.hass.localize(
"ui.components.calendar.event.repeat.freq.daily"
)}
</ha-list-item>
</ha-select>
`;
}
@@ -195,8 +227,18 @@ export class RecurrenceRuleEditor extends LitElement {
)}
@selected=${this._onMonthlyDetailSelected}
.value=${this._monthlyRepeat || this._monthlyRepeatItems[0]?.value}
.options=${this._monthlyRepeatItems}
></ha-select>`
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${this._monthlyRepeatItems!.map(
(item) => html`
<ha-list-item .value=${item.value} .item=${item}>
${item.label}
</ha-list-item>
`
)}
</ha-select>`
: nothing}
`;
}
@@ -257,13 +299,19 @@ export class RecurrenceRuleEditor extends LitElement {
)}
.value=${this._end}
@selected=${this._onEndSelected}
.options=${["never", "after", "on"].map((end) => ({
value: end,
label: this.hass.localize(
`ui.components.calendar.event.repeat.end.${end as RepeatEnd}`
),
}))}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
<ha-list-item value="never">
${this.hass.localize("ui.components.calendar.event.repeat.end.never")}
</ha-list-item>
<ha-list-item value="after">
${this.hass.localize("ui.components.calendar.event.repeat.end.after")}
</ha-list-item>
<ha-list-item value="on">
${this.hass.localize("ui.components.calendar.event.repeat.end.on")}
</ha-list-item>
</ha-select>
${this._end === "after"
? html`
@@ -312,8 +360,8 @@ export class RecurrenceRuleEditor extends LitElement {
this._interval = (e.target! as any).value;
}
private _onRepeatSelected(e: HaSelectSelectEvent<RepeatFrequency>) {
this._freq = e.detail.value;
private _onRepeatSelected(e: CustomEvent<SelectedDetail<number>>) {
this._freq = (e.target as HaSelect).value as RepeatFrequency;
if (this._freq === "yearly") {
this._interval = 1;
@@ -322,14 +370,12 @@ export class RecurrenceRuleEditor extends LitElement {
this._weekday.clear();
this._computeWeekday();
}
e.stopPropagation();
}
private _onMonthlyDetailSelected(
e: HaSelectSelectEvent<MonthlyRepeatItem["value"]>
) {
const selectedItem = this._monthlyRepeatItems.find(
(item) => item.value === e.detail.value
);
private _onMonthlyDetailSelected(e: CustomEvent<SelectedDetail<number>>) {
e.stopPropagation();
const selectedItem = this._monthlyRepeatItems[e.detail.index];
if (!selectedItem) {
return;
}
@@ -349,8 +395,8 @@ export class RecurrenceRuleEditor extends LitElement {
this.requestUpdate("_weekday");
}
private _onEndSelected(e: HaSelectSelectEvent<RepeatEnd>) {
const end = e.detail.value;
private _onEndSelected(e: CustomEvent<SelectedDetail<number>>) {
const end = (e.target as HaSelect).value as RepeatEnd;
if (end === this._end) {
return;
}
+1
View File
@@ -186,6 +186,7 @@ class PanelClimate extends LitElement {
);
padding-top: var(--safe-area-inset-top);
z-index: 4;
transition: box-shadow 200ms linear;
display: flex;
flex-direction: row;
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
@@ -1,11 +1,12 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-card";
import "../../../../../components/ha-list-item";
import "../../../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
import type {
HassioAddonDetails,
HassioAddonSetOptionParams,
@@ -15,8 +16,8 @@ import type { HassioHardwareAudioDevice } from "../../../../../data/hassio/hardw
import { fetchHassioHardwareAudio } from "../../../../../data/hassio/hardware";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
import { suggestSupervisorAppRestart } from "../dialogs/suggestSupervisorAppRestart";
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
@customElement("supervisor-app-audio")
class SupervisorAppAudio extends LitElement {
@@ -54,13 +55,19 @@ class SupervisorAppAudio extends LitElement {
"ui.panel.config.apps.configuration.audio.input"
)}
@selected=${this._setInputDevice}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._selectedInput!}
.disabled=${this.disabled}
.options=${this._inputDevices.map((item) => ({
value: item.device || "",
label: item.name,
}))}
>
${this._inputDevices.map(
(item) => html`
<ha-list-item .value=${item.device || ""}>
${item.name}
</ha-list-item>
`
)}
</ha-select>`}
${this._outputDevices &&
html`<ha-select
@@ -68,13 +75,19 @@ class SupervisorAppAudio extends LitElement {
"ui.panel.config.apps.configuration.audio.output"
)}
@selected=${this._setOutputDevice}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.value=${this._selectedOutput!}
.disabled=${this.disabled}
.options=${this._outputDevices.map((item) => ({
value: item.device || "",
label: item.name,
}))}
>
${this._outputDevices.map(
(item) => html`
<ha-list-item .value=${item.device || ""}
>${item.name}</ha-list-item
>
`
)}
</ha-select>`}
</div>
<div class="card-actions">
@@ -103,7 +116,6 @@ class SupervisorAppAudio extends LitElement {
}
ha-select {
width: 100%;
display: block;
}
ha-select:last-child {
margin-top: var(--ha-space-2);
@@ -119,14 +131,14 @@ class SupervisorAppAudio extends LitElement {
}
}
private _setInputDevice(ev: HaSelectSelectEvent): void {
const device = ev.detail.value;
this._selectedInput = device ?? null;
private _setInputDevice(ev): void {
const device = ev.target.value;
this._selectedInput = device;
}
private _setOutputDevice(ev: HaSelectSelectEvent): void {
const device = ev.detail.value;
this._selectedOutput = device ?? null;
private _setOutputDevice(ev): void {
const device = ev.target.value;
this._selectedOutput = device;
}
private async _addonChanged(): Promise<void> {
@@ -2,12 +2,13 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
import { stringCompare } from "../../../../../common/string/compare";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import { CONDITION_ICONS } from "../../../../../components/ha-condition-icon";
import "../../../../../components/ha-dropdown-item";
import "../../../../../components/ha-list-item";
import "../../../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
import type { HaSelect } from "../../../../../components/ha-select";
import {
DYNAMIC_PREFIX,
getValueFromDynamic,
@@ -84,47 +85,37 @@ export class HaConditionAction
this.action.condition
);
const value =
this.action.condition in this._conditionDescriptions
? `${DYNAMIC_PREFIX}${this.action.condition}`
: this.action.condition;
let valueLabel = value;
const items = html`${this._processedTypes(
this._conditionDescriptions,
this.hass.localize
).map(([opt, label, condition]) => {
const selected = value === opt;
if (selected) {
valueLabel = label;
}
return html`
<ha-dropdown-item .value=${opt} class=${selected ? "selected" : ""}>
<ha-condition-icon
.hass=${this.hass}
slot="icon"
.condition=${condition}
></ha-condition-icon>
${label}
</ha-dropdown-item>
`;
})}`;
return html`
${this.inSidebar || (!this.inSidebar && !this.indent)
? html`
<ha-select
fixedMenuPosition
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type_select"
)}
.disabled=${this.disabled}
.value=${valueLabel}
.value=${this.action.condition in this._conditionDescriptions
? `${DYNAMIC_PREFIX}${this.action.condition}`
: this.action.condition}
naturalMenuWidth
@selected=${this._typeChanged}
@closed=${stopPropagation}
>
${items}
${this._processedTypes(
this._conditionDescriptions,
this.hass.localize
).map(
([opt, label, condition]) => html`
<ha-list-item .value=${opt} graphic="icon">
${label}
<ha-condition-icon
.hass=${this.hass}
slot="graphic"
.condition=${condition}
></ha-condition-icon>
</ha-list-item>
`
)}
</ha-select>
`
: nothing}
@@ -201,8 +192,8 @@ export class HaConditionAction
});
}
private _typeChanged(ev: HaSelectSelectEvent) {
const type = ev.detail.value;
private _typeChanged(ev: CustomEvent) {
const type = (ev.target as HaSelect).value;
if (!type) {
return;
@@ -251,7 +242,6 @@ export class HaConditionAction
static styles = css`
ha-select {
margin-bottom: 24px;
display: block;
}
`;
}
@@ -8,7 +8,7 @@ import "../../../../../components/ha-duration-input";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-textfield";
import type { WaitForTriggerAction } from "../../../../../data/script";
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
import type { HomeAssistant } from "../../../../../types";
import "../../trigger/ha-automation-trigger";
import type { ActionElement } from "../ha-automation-action-row";
import { handleChangeEvent } from "../ha-automation-action-row";
@@ -78,7 +78,7 @@ export class HaWaitForTriggerAction
`;
}
private _timeoutChanged(ev: ValueChangedEvent<TimeChangedEvent>): void {
private _timeoutChanged(ev: CustomEvent<{ value: TimeChangedEvent }>): void {
ev.stopPropagation();
const value = ev.detail.value;
fireEvent(this, "value-changed", {
@@ -114,7 +114,7 @@ import {
} from "../../../data/trigger";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type { HomeAssistant } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import "./add-automation-element/ha-automation-add-from-target";
@@ -1752,7 +1752,7 @@ class DialogAddAutomationElement
this.closeDialog();
}
private _selected(ev: ValueChangedEvent<string>) {
private _selected(ev: CustomEvent<{ value: string }>) {
let target: HassServiceTarget | undefined;
if (
this._tab === "targets" &&
@@ -1766,7 +1766,7 @@ class DialogAddAutomationElement
}
private _handleTargetSelected = (
ev: ValueChangedEvent<SingleHassServiceTarget>
ev: CustomEvent<{ value: SingleHassServiceTarget }>
) => {
this._targetItems = undefined;
this._loadItemsError = false;
@@ -77,12 +77,7 @@ import "../../../layouts/hass-subpage";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
import { haStyle } from "../../../resources/styles";
import type {
Entries,
HomeAssistant,
Route,
ValueChangedEvent,
} from "../../../types";
import type { Entries, HomeAssistant, Route } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
@@ -758,7 +753,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
}
private _valueChanged(ev: ValueChangedEvent<AutomationConfig>) {
private _valueChanged(ev: CustomEvent<{ value: AutomationConfig }>) {
ev.stopPropagation();
if (this._config) {
@@ -19,6 +19,7 @@ import {
mdiToggleSwitchOffOutline,
mdiTransitConnection,
} from "@mdi/js";
import { differenceInDays } from "date-fns";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
@@ -27,11 +28,14 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { formatShortDateTimeWithConditionalYear } from "../../../common/datetime/format_date_time";
import { relativeTime } from "../../../common/datetime/relative_time";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify";
import type { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
@@ -111,13 +115,6 @@ import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route, ServiceCallResponse } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity";
import {
getEntityIdHiddenTableColumn,
getAreaTableColumn,
getCategoryTableColumn,
getLabelsTableColumn,
getTriggeredAtTableColumn,
} from "../common/data-table-columns";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
@@ -137,7 +134,7 @@ type AutomationItem = AutomationEntity & {
last_triggered: string | undefined;
formatted_state: string;
category: string | undefined;
label_entries: LabelRegistryEntry[];
labels: LabelRegistryEntry[];
assistants: string[];
assistants_sortable_key: string | undefined;
};
@@ -272,7 +269,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
category: category
? categoryReg?.find((cat) => cat.category_id === category)?.name
: undefined,
label_entries: (labels || []).map(
labels: (labels || []).map(
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
),
assistants,
@@ -287,6 +284,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
(
narrow: boolean,
localize: LocalizeFunc,
locale: HomeAssistant["locale"],
entitiesToCheck?: any[]
): DataTableColumnContainer<AutomationItem> => {
const columns: DataTableColumnContainer<AutomationItem> = {
@@ -308,7 +306,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
})}
></ha-state-icon>`,
},
entity_id: getEntityIdHiddenTableColumn(),
entity_id: {
title: "",
hidden: true,
filterable: true,
},
name: {
title: localize("ui.panel.config.automation.picker.headers.name"),
main: true,
@@ -317,17 +319,59 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
direction: "asc",
flex: 2,
extraTemplate: (automation) =>
automation.label_entries.length
automation.labels.length
? html`<ha-data-table-labels
@label-clicked=${narrow ? undefined : this._labelClicked}
.labels=${automation.label_entries}
.labels=${automation.labels}
></ha-data-table-labels>`
: nothing,
},
area: getAreaTableColumn(localize),
category: getCategoryTableColumn(localize),
labels: getLabelsTableColumn(),
last_triggered: getTriggeredAtTableColumn(localize, this.hass),
area: {
title: localize("ui.panel.config.automation.picker.headers.area"),
groupable: true,
filterable: true,
sortable: true,
},
category: {
title: localize("ui.panel.config.automation.picker.headers.category"),
defaultHidden: true,
groupable: true,
filterable: true,
sortable: true,
},
labels: {
title: "",
hidden: true,
filterable: true,
template: (automation) =>
automation.labels.map((lbl) => lbl.name).join(" "),
},
last_triggered: {
sortable: true,
title: localize("ui.card.automation.last_triggered"),
template: (automation) => {
if (!automation.last_triggered) {
return this.hass.localize("ui.components.relative_time.never");
}
const date = new Date(automation.last_triggered);
const now = new Date();
const dayDifference = differenceInDays(now, date);
const formattedTime = formatShortDateTimeWithConditionalYear(
date,
this.hass.locale,
this.hass.config
);
const elementId = "last-triggered-" + slugify(automation.entity_id);
return html`
${dayDifference > 3
? formattedTime
: html`
<ha-tooltip for=${elementId}>${formattedTime}</ha-tooltip>
<span id=${elementId}>${relativeTime(date, locale)}</span>
`}
`;
},
},
formatted_state: {
minWidth: "82px",
maxWidth: "82px",
@@ -441,7 +485,12 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
Array.isArray(val) ? val.length : val
)
).length}
.columns=${this._columns(this.narrow, this.hass.localize, automations)}
.columns=${this._columns(
this.narrow,
this.hass.localize,
this.hass.locale,
automations
)}
.initialGroupColumn=${this._activeGrouping ?? "category"}
.initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting}
@@ -54,7 +54,7 @@ import {
import { configEntriesContext } from "../../../data/context";
import { getActionType, type Action } from "../../../data/script";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import "./action/ha-automation-action";
@@ -383,7 +383,7 @@ export class HaManualAutomationEditor extends SubscribeMixin(LitElement) {
this._sidebarElement?.focus();
}
private _sidebarConfigChanged(ev: ValueChangedEvent<SidebarConfig>) {
private _sidebarConfigChanged(ev: CustomEvent<{ value: SidebarConfig }>) {
ev.stopPropagation();
if (!this._sidebarConfig) {
return;
@@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../../common/string/compare";
import "../../../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
import "../../../../../components/ha-list-item";
import type { TagTrigger } from "../../../../../data/automation";
import type { Tag } from "../../../../../data/tag";
import { fetchTags } from "../../../../../data/tag";
@@ -42,11 +42,16 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
.disabled=${this.disabled || this._tags.length === 0}
.value=${this.trigger.tag_id}
@selected=${this._tagChanged}
.options=${this._tags.map((tag) => ({
value: tag.id,
label: tag.name || tag.id,
}))}
fixedMenuPosition
naturalMenuWidth
>
${this._tags.map(
(tag) => html`
<ha-list-item .value=${tag.id}>
${tag.name || tag.id}
</ha-list-item>
`
)}
</ha-select>
`;
}
@@ -61,18 +66,18 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
);
}
private _tagChanged(ev: HaSelectSelectEvent) {
private _tagChanged(ev) {
if (
!ev.detail.value ||
!ev.target.value ||
!this._tags ||
this.trigger.tag_id === ev.detail.value
this.trigger.tag_id === ev.target.value
) {
return;
}
fireEvent(this, "value-changed", {
value: {
...this.trigger,
tag_id: ev.detail.value,
tag_id: ev.target.value,
},
});
}
@@ -28,7 +28,7 @@ import {
sortWeekdays,
} from "../../../../../data/backup";
import type { SupervisorUpdateConfig } from "../../../../../data/supervisor/update";
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
import type { HomeAssistant } from "../../../../../types";
import { documentationUrl } from "../../../../../util/documentation-url";
import "./ha-backup-config-retention";
@@ -405,7 +405,7 @@ class HaBackupConfigSchedule extends LitElement {
});
}
private _retentionChanged(ev: ValueChangedEvent<Retention>) {
private _retentionChanged(ev: CustomEvent<{ value: Retention }>) {
ev.stopPropagation();
const retention = ev.detail.value;
@@ -1,16 +1,17 @@
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-password-field";
@@ -85,12 +86,14 @@ const RECOMMENDED_CONFIG: BackupConfig = {
class DialogBackupOnboarding extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@state() private _opened = false;
@state() private _step?: Step;
@state() private _params?: BackupOnboardingDialogParams;
@query("ha-md-dialog") private _dialog!: HaMdDialog;
@state() private _config?: BackupConfig;
public showDialog(params: BackupOnboardingDialogParams): void {
@@ -112,23 +115,21 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
};
}
this._open = true;
this._opened = true;
}
public closeDialog() {
this._open = false;
return true;
}
private _dialogClosed() {
if (this._params?.cancel) {
this._params.cancel();
if (this._params!.cancel) {
this._params!.cancel();
}
if (this._opened) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._opened = false;
this._step = undefined;
this._config = undefined;
this._params = undefined;
this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
private get _firstStep(): Step {
@@ -167,7 +168,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
try {
await this._save(true);
this._params?.submit!(true);
this.closeDialog();
this._dialog.close();
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
@@ -201,7 +202,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
}
protected render() {
if (!this._params || !this._step) {
if (!this._opened || !this._params || !this._step) {
return nothing;
}
@@ -209,37 +210,33 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
const isFirstStep = this._step === this._firstStep;
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this._stepTitle}
width="medium"
prevent-scrim-close
@closed=${this._dialogClosed}
>
${isFirstStep
? html`
<ha-icon-button
slot="headerNavigationIcon"
data-dialog="close"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
`
: html`
<ha-icon-button-prev
slot="headerNavigationIcon"
@click=${this._previousStep}
></ha-icon-button-prev>
`}
<div>${this._renderStepContent()}</div>
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
<ha-dialog-header slot="headline">
${isFirstStep
? html`
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
`
: html`
<ha-icon-button-prev
slot="navigationIcon"
@click=${this._previousStep}
></ha-icon-button-prev>
`}
<span slot="title">${this._stepTitle}</span>
</ha-dialog-header>
<div slot="content">${this._renderStepContent()}</div>
${!FULL_DIALOG_STEPS.has(this._step)
? html`
<ha-dialog-footer slot="footer">
<div slot="actions">
${isLastStep
? html`
<ha-button
slot="primaryAction"
@click=${this._done}
.disabled=${!this._isStepValid()}
>
@@ -250,17 +247,16 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
`
: html`
<ha-button
slot="primaryAction"
@click=${this._nextStep}
.disabled=${!this._isStepValid()}
>
${this.hass.localize("ui.common.next")}
</ha-button>
`}
</ha-dialog-footer>
</div>
`
: nothing}
</ha-wa-dialog>
</ha-md-dialog>
`;
}
@@ -544,9 +540,11 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
haStyle,
haStyleDialog,
css`
ha-wa-dialog {
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
--ha-dialog-max-height: min(605px, 100% - 48px);
ha-md-dialog {
width: 90vw;
max-width: 560px;
--dialog-content-padding: 8px 24px;
max-height: min(605px, 100% - 48px);
}
ha-md-list {
background: none;
@@ -559,6 +557,14 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
margin-left: -24px;
margin-right: -24px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-md-dialog {
max-width: none;
}
div[slot="content"] {
margin-top: 0;
}
}
p {
margin-top: 0;
}
@@ -1,17 +1,20 @@
import { mdiClose } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-spinner";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-password-field";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import "../../../../components/ha-alert";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-wa-dialog";
import type { RestoreBackupParams } from "../../../../data/backup";
import {
fetchBackupConfig,
@@ -51,8 +54,6 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
@state() private _formData?: FormData;
@state() private _open = false;
@state() private _backupEncryptionKey?: string;
@state() private _userPassword?: string;
@@ -67,6 +68,8 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
@state() private _unsub?: Promise<UnsubscribeFunc>;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(params: RestoreBackupDialogParams) {
this._params = params;
@@ -91,12 +94,10 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
} else {
this._step = STEPS[0];
}
this._open = true;
}
public closeDialog() {
this._open = false;
this._dialog?.close();
return true;
}
@@ -111,7 +112,6 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
this._stage = undefined;
this._step = undefined;
this._unsubscribe();
this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -136,14 +136,17 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
);
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${dialogTitle}
width="medium"
@closed=${this._dialogClosed}
>
<div class="content">
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
<span slot="title" .title=${dialogTitle}>${dialogTitle}</span>
</ha-dialog-header>
<div slot="content" class="content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: this._step === "confirm"
@@ -152,18 +155,18 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
? this._renderEncryption()
: this._renderProgress()}
</div>
${this._error
? html`
<ha-dialog-footer slot="footer">
<ha-button slot="primaryAction" @click=${this.closeDialog}>
<div slot="actions">
${this._error
? html`
<ha-button @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</ha-button>
</ha-dialog-footer>
`
: this._step === "confirm" || this._step === "encryption"
? this._renderConfirmActions()
: nothing}
</ha-wa-dialog>
`
: this._step === "confirm" || this._step === "encryption"
? this._renderConfirmActions()
: nothing}
</div>
</ha-md-dialog>
`;
}
@@ -213,7 +216,6 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
${this._renderEncryptionIntro()}
<ha-password-field
autofocus
@input=${this._passwordChanged}
.label=${this.hass.localize(
"ui.panel.config.backup.dialogs.restore.encryption.input_label"
@@ -225,24 +227,14 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
private _renderConfirmActions() {
return html`
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._restoreBackup}
variant="danger"
>
${this.hass.localize(
"ui.panel.config.backup.dialogs.restore.actions.restore"
)}
</ha-button>
</ha-dialog-footer>
<ha-button appearance="plain" @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._restoreBackup} variant="danger">
${this.hass.localize(
"ui.panel.config.backup.dialogs.restore.actions.restore"
)}
</ha-button>
`;
}
@@ -373,6 +365,10 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
haStyle,
haStyleDialog,
css`
ha-md-dialog {
max-width: 500px;
width: 100%;
}
.content p {
margin: 0 0 16px;
}
@@ -1,13 +1,15 @@
import { mdiContentCopy, mdiDownload } from "@mdi/js";
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-password-field";
@@ -29,40 +31,40 @@ type Step = (typeof STEPS)[number];
class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@state() private _opened = false;
@state() private _step?: Step;
@state() private _params?: SetBackupEncryptionKeyDialogParams;
@query("ha-md-dialog") private _dialog!: HaMdDialog;
@state() private _newEncryptionKey?: string;
public showDialog(params: SetBackupEncryptionKeyDialogParams): void {
this._params = params;
this._step = STEPS[0];
this._open = true;
this._opened = true;
this._newEncryptionKey = generateEncryptionKey();
}
public closeDialog() {
this._open = false;
return true;
}
private _dialogClosed() {
if (this._params?.cancel) {
this._params.cancel();
if (this._params!.cancel) {
this._params!.cancel();
}
if (this._opened) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._opened = false;
this._step = undefined;
this._params = undefined;
this._newEncryptionKey = undefined;
this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
private _done() {
this._params?.submit!(true);
this.closeDialog();
this._dialog.close();
}
private _nextStep() {
@@ -74,7 +76,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
}
protected render() {
if (!this._params || !this._step) {
if (!this._opened || !this._params) {
return nothing;
}
@@ -86,20 +88,21 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
: "";
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${dialogTitle}
width="medium"
prevent-scrim-close
@closed=${this._dialogClosed}
>
${this._renderStepContent()}
<ha-dialog-footer slot="footer">
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
<span slot="title">${dialogTitle}</span>
</ha-dialog-header>
<div slot="content">${this._renderStepContent()}</div>
<div slot="actions">
${this._step === "key"
? html`
<ha-button
slot="primaryAction"
@click=${this._submit}
.disabled=${!this._newEncryptionKey}
>
@@ -109,14 +112,14 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
</ha-button>
`
: html`
<ha-button slot="primaryAction" @click=${this._done}>
<ha-button @click=${this._done}>
${this.hass.localize(
"ui.panel.config.backup.dialogs.set_encryption_key.actions.done"
)}
</ha-button>
`}
</ha-dialog-footer>
</ha-wa-dialog>
</div>
</ha-md-dialog>
`;
}
@@ -210,8 +213,10 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
haStyle,
haStyleDialog,
css`
ha-wa-dialog {
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
ha-md-dialog {
width: 90vw;
max-width: 560px;
--dialog-content-padding: 8px 24px;
}
ha-md-list {
background: none;
@@ -242,6 +247,14 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
flex: none;
margin: -16px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-md-dialog {
max-width: none;
}
div[slot="content"] {
margin-top: 0;
}
}
p {
margin-top: 0;
}
@@ -25,7 +25,7 @@ import {
updateBackupConfig,
} from "../../../data/backup";
import "../../../layouts/hass-subpage";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type { HomeAssistant } from "../../../types";
import { showConfirmationDialog } from "../../lovelace/custom-card-helpers";
import "./components/config/ha-backup-config-retention";
import "./components/ha-backup-data-picker";
@@ -284,7 +284,7 @@ class HaConfigBackupDetails extends LitElement {
}
}
private _retentionChanged(ev: ValueChangedEvent<Retention>) {
private _retentionChanged(ev: CustomEvent<{ value: Retention }>) {
const retention = ev.detail.value;
this._updateAgentConfig({
retention,
@@ -1,14 +1,13 @@
import { mdiContentCopy } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { mdiContentCopy } from "@mdi/js";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-button";
import "../../../../components/ha-card";
import "../../../../components/ha-button";
import "../../../../components/ha-language-picker";
import "../../../../components/ha-list-item";
import "../../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-switch";
import type { CloudStatusLoggedIn } from "../../../../data/cloud";
@@ -20,8 +19,9 @@ import {
} from "../../../../data/cloud/tts";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import { showTryTtsDialog } from "./show-dialog-cloud-tts-try";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import { showToast } from "../../../../util/toast";
export const getCloudTtsSupportedVoices = (
language: string,
@@ -96,11 +96,13 @@ export class CloudTTSPref extends LitElement {
.disabled=${this.savingPreferences}
.value=${defaultVoice[1]}
@selected=${this._handleVoiceChange}
.options=${voices.map((voice) => ({
value: voice.voiceId,
label: voice.voiceName,
}))}
>
${voices.map(
(voice) =>
html`<ha-list-item .value=${voice.voiceId}>
${voice.voiceName}
</ha-list-item>`
)}
</ha-select>
</div>
</div>
@@ -132,6 +134,16 @@ export class CloudTTSPref extends LitElement {
`;
}
protected updated(changedProps) {
if (
changedProps.has("cloudStatus") &&
this.cloudStatus?.prefs.tts_default_voice?.[0] !==
changedProps.get("cloudStatus")?.prefs.tts_default_voice?.[0]
) {
this.renderRoot.querySelector("ha-select")?.layoutOptions();
}
}
protected willUpdate(changedProps) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
@@ -183,13 +195,13 @@ export class CloudTTSPref extends LitElement {
}
}
private async _handleVoiceChange(ev: HaSelectSelectEvent) {
const voice = ev.detail.value;
if (!voice || voice === this.cloudStatus!.prefs.tts_default_voice[1]) {
private async _handleVoiceChange(ev) {
if (ev.target.value === this.cloudStatus!.prefs.tts_default_voice[1]) {
return;
}
this.savingPreferences = true;
const language = this.cloudStatus!.prefs.tts_default_voice[0];
const voice = ev.target.value;
try {
await updateCloudPref(this.hass, {
@@ -1,11 +1,13 @@
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-markdown-element";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-select";
import "../../../../components/ha-spinner";
import "../../../../components/ha-textarea";
@@ -21,6 +23,8 @@ export class DialogSupportPackage extends LitElement {
@state() private _supportPackage?: string;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public showDialog() {
this._open = true;
this._loadSupportPackage();
@@ -33,50 +37,53 @@ export class DialogSupportPackage extends LitElement {
}
public closeDialog() {
this._open = false;
this._dialog?.close();
return true;
}
protected render() {
if (!this._open) {
return nothing;
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
width="full"
header-title="Download support package"
@closed=${this._dialogClosed}
>
${this._supportPackage
? html`<ha-markdown-element
.content=${this._supportPackage}
breaks
></ha-markdown-element>`
: html`
<div class="progress-container">
<ha-spinner></ha-spinner>
Generating preview...
</div>
`}
<div slot="footer" class="footer">
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
<span slot="title">Download support package</span>
</ha-dialog-header>
<div slot="content">
${this._supportPackage
? html`<ha-markdown-element
.content=${this._supportPackage}
breaks
></ha-markdown-element>`
: html`
<div class="progress-container">
<ha-spinner></ha-spinner>
Generating preview...
</div>
`}
</div>
<div class="footer" slot="actions">
<ha-alert>
This file may contain personal data about your home. Avoid sharing
them with unverified or untrusted parties.
</ha-alert>
<hr />
<ha-dialog-footer>
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
<div class="actions">
<ha-button appearance="plain" @click=${this.closeDialog}
>Close</ha-button
>
Close
</ha-button>
<ha-button slot="primaryAction" @click=${this._download}>
Download
</ha-button>
</ha-dialog-footer>
<ha-button @click=${this._download}>Download</ha-button>
</div>
</div>
</ha-wa-dialog>
</ha-md-dialog>
`;
}
@@ -93,6 +100,11 @@ export class DialogSupportPackage extends LitElement {
}
static styles = css`
ha-md-dialog {
min-width: 90vw;
min-height: 90vh;
}
.progress-container {
display: flex;
flex-direction: column;
@@ -102,23 +114,30 @@ export class DialogSupportPackage extends LitElement {
width: 100%;
}
.footer {
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
gap: var(--ha-space-3);
width: 100%;
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-md-dialog {
min-width: 100vw;
min-height: 100vh;
}
.progress-container {
height: calc(100vh - 260px);
}
}
ha-dialog-footer {
display: block;
width: 100%;
.footer {
flex-direction: column;
}
.actions {
display: flex;
gap: var(--ha-space-2);
justify-content: flex-end;
}
hr {
border: none;
border-top: 1px solid var(--divider-color);
width: 100%;
margin: 0;
width: calc(100% + 48px);
margin-right: -24px;
margin-left: -24px;
}
table,
th,
@@ -4,16 +4,14 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import "../../../../components/ha-button";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-list-item";
import "../../../../components/ha-select";
import type {
HaSelectOption,
HaSelectSelectEvent,
} from "../../../../components/ha-select";
import "../../../../components/ha-button";
import "../../../../components/ha-textarea";
import type { HaTextArea } from "../../../../components/ha-textarea";
import { showAutomationEditor } from "../../../../data/automation";
@@ -62,25 +60,6 @@ export class DialogTryTts extends LitElement {
return nothing;
}
const target = this._target || "browser";
const targetOptions: HaSelectOption[] = Object.values(this.hass.states)
.filter(
(entity) =>
computeStateDomain(entity) === "media_player" &&
supportsFeature(entity, MediaPlayerEntityFeature.PLAY_MEDIA)
)
.map((entity) => ({
value: entity.entity_id,
label: computeStateName(entity),
}));
targetOptions.unshift({
value: "browser",
label: this.hass.localize(
"ui.panel.config.cloud.account.tts.dialog.target_browser"
),
});
return html`
<ha-dialog
open
@@ -114,8 +93,28 @@ export class DialogTryTts extends LitElement {
id="target"
.value=${target}
@selected=${this._handleTargetChanged}
.options=${targetOptions}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
>
<ha-list-item value="browser">
${this.hass.localize(
"ui.panel.config.cloud.account.tts.dialog.target_browser"
)}
</ha-list-item>
${Object.values(this.hass.states)
.filter(
(entity) =>
computeStateDomain(entity) === "media_player" &&
supportsFeature(entity, MediaPlayerEntityFeature.PLAY_MEDIA)
)
.map(
(entity) => html`
<ha-list-item .value=${entity.entity_id}>
${computeStateName(entity)}
</ha-list-item>
`
)}
</ha-select>
</div>
<ha-button
@@ -141,8 +140,8 @@ export class DialogTryTts extends LitElement {
`;
}
private _handleTargetChanged(ev: HaSelectSelectEvent) {
this._target = ev.detail.value;
private _handleTargetChanged(ev) {
this._target = ev.target.value;
this.requestUpdate("_target");
}
@@ -228,10 +227,11 @@ export class DialogTryTts extends LitElement {
}
ha-textarea,
ha-select {
display: block;
margin-top: 8px;
width: 100%;
}
ha-select {
margin-top: 8px;
}
`,
];
}
@@ -1,204 +0,0 @@
import { html, nothing } from "lit";
import { differenceInDays } from "date-fns";
import { mdiPencilOff } from "@mdi/js";
import type { HomeAssistant } from "../../../types";
import type { LocalizeFunc } from "../../../common/translations/localize";
import type { DataTableColumnData } from "../../../components/data-table/ha-data-table";
import { slugify } from "../../../common/string/slugify";
import { relativeTime } from "../../../common/datetime/relative_time";
import { formatShortDateTimeWithConditionalYear } from "../../../common/datetime/format_date_time";
import { isUnavailableState } from "../../../data/entity/entity";
import "../../../components/ha-tooltip";
import "../../../components/ha-svg-icon";
export function getEntityIdHiddenTableColumn<T>(): DataTableColumnData<T> {
return {
title: "",
hidden: true,
filterable: true,
};
}
export function getEntityIdTableColumn<T>(
localize: LocalizeFunc,
defaultHidden?: boolean
): DataTableColumnData<T> {
return {
title: localize("ui.panel.config.generic.headers.entity_id"),
defaultHidden: defaultHidden,
filterable: true,
sortable: true,
};
}
export function getDomainTableColumn<T>(
localize: LocalizeFunc
): DataTableColumnData<T> {
return {
title: localize("ui.panel.config.generic.headers.domain"),
hidden: true,
groupable: true,
filterable: true,
sortable: false,
};
}
export function getAreaTableColumn<T>(
localize: LocalizeFunc
): DataTableColumnData<T> {
return {
title: localize("ui.panel.config.generic.headers.area"),
groupable: true,
filterable: true,
sortable: true,
minWidth: "120px",
};
}
export function getFloorTableColumn<T>(
localize: LocalizeFunc
): DataTableColumnData<T> {
return {
title: localize("ui.panel.config.generic.headers.floor"),
defaultHidden: true,
groupable: true,
filterable: true,
sortable: true,
minWidth: "120px",
};
}
export function getCategoryTableColumn<T>(
localize: LocalizeFunc
): DataTableColumnData<T> {
return {
title: localize("ui.panel.config.generic.headers.category"),
defaultHidden: true,
groupable: true,
filterable: true,
sortable: true,
};
}
export function getEditableTableColumn<T>(
localize: LocalizeFunc,
tooltip: string
): DataTableColumnData<T> {
return {
title: localize("ui.panel.config.generic.headers.editable"),
type: "icon",
showNarrow: true,
sortable: true,
minWidth: "88px",
maxWidth: "88px",
template: (entry: any) => html`
${!entry.editable
? html`
<ha-svg-icon
.id="icon-edit-${slugify(entry.entity_id)}"
.path=${mdiPencilOff}
style="color: var(--secondary-text-color)"
></ha-svg-icon>
<ha-tooltip
.for="icon-edit-${slugify(entry.entity_id)}"
placement="left"
>
${tooltip}
</ha-tooltip>
`
: nothing}
`,
};
}
export function getLabelsTableColumn<T>(): DataTableColumnData<T> {
return {
title: "",
hidden: true,
filterable: true,
template: (entry: any) =>
entry.label_entries.map((lbl) => lbl.name).join(" "),
};
}
export function getTriggeredAtTableColumn<T>(
localize: LocalizeFunc,
hass: HomeAssistant
): DataTableColumnData<T> {
return {
title: localize("ui.card.automation.last_triggered"),
sortable: true,
template: (entry: any) =>
renderRelativeTimeColumn(
entry.last_triggered,
"last-triggered",
entry.entity_id,
localize,
hass
),
};
}
export const renderRelativeTimeColumn = (
valueRelativeTime: string,
valueName: string,
entity_id: string,
localize: LocalizeFunc,
hass: HomeAssistant
) => {
if (!valueRelativeTime || isUnavailableState(valueRelativeTime)) {
return localize("ui.components.relative_time.never");
}
const date = new Date(valueRelativeTime);
const now = new Date();
const dayDifference = differenceInDays(now, date);
const formattedTime = formatShortDateTimeWithConditionalYear(
date,
hass.locale,
hass.config
);
const elementId = valueName + "-" + slugify(entity_id);
return html`
${dayDifference > 3
? formattedTime
: html`
<ha-tooltip for=${elementId}>${formattedTime}</ha-tooltip>
<span id=${elementId}>${relativeTime(date, hass.locale)}</span>
`}
`;
};
export function getCreatedAtTableColumn<T>(
localize: LocalizeFunc,
hass: HomeAssistant
): DataTableColumnData<T> {
return {
title: localize("ui.panel.config.generic.headers.created_at"),
defaultHidden: true,
sortable: true,
minWidth: "128px",
template: (entry: any) => renderDateTimeColumn(entry.created_at, hass),
};
}
export function getModifiedAtTableColumn<T>(
localize: LocalizeFunc,
hass: HomeAssistant
): DataTableColumnData<T> {
return {
title: localize("ui.panel.config.generic.headers.modified_at"),
defaultHidden: true,
sortable: true,
minWidth: "128px",
template: (entry: any) => renderDateTimeColumn(entry.modified_at, hass),
};
}
const renderDateTimeColumn = (valueDateTime: number, hass: HomeAssistant) =>
html`${valueDateTime
? formatShortDateTimeWithConditionalYear(
new Date(valueDateTime * 1000),
hass.locale,
hass.config
)
: nothing}`;
+2 -2
View File
@@ -14,7 +14,7 @@ import {
saveAITaskPreferences,
type AITaskPreferences,
} from "../../../data/ai_task";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { documentationUrl } from "../../../util/documentation-url";
import { computeDomain } from "../../../common/entity/compute_domain";
@@ -137,7 +137,7 @@ export class AITaskPref extends LitElement {
`;
}
private _handlePrefChange(ev: ValueChangedEvent<string | undefined>) {
private _handlePrefChange(ev: CustomEvent<{ value: string | undefined }>) {
const input = ev.target as HaEntityPicker;
const key = input.dataset.name as keyof AITaskPreferences;
const value = ev.detail.value || null;
@@ -71,7 +71,6 @@ class HaConfigSectionAnalytics extends LitElement {
display: block;
max-width: 600px;
margin: 0 auto;
margin-bottom: 24px;
}
`;
}
@@ -5,8 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-wa-dialog";
import { createCloseHeading } from "../../../../components/ha-dialog";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@@ -22,21 +21,14 @@ export class DialogJoinBeta
@state() private _dialogParams?: JoinBetaDialogParams;
@state() private _open = false;
public showDialog(dialogParams: JoinBetaDialogParams): void {
this._dialogParams = dialogParams;
this._open = true;
}
public closeDialog() {
this._open = false;
return true;
}
private _dialogClosed() {
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
protected render() {
@@ -45,11 +37,13 @@ export class DialogJoinBeta
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize("ui.dialogs.join_beta_channel.title")}
@closed=${this._dialogClosed}
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.join_beta_channel.title")
)}
>
<ha-alert alert-type="warning">
${this.hass.localize("ui.dialogs.join_beta_channel.backup")}
@@ -73,19 +67,17 @@ export class DialogJoinBeta
)}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this._cancel}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._join}>
${this.hass.localize("ui.dialogs.join_beta_channel.join")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
<ha-button
appearance="plain"
slot="primaryAction"
@click=${this._cancel}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._join}>
${this.hass.localize("ui.dialogs.join_beta_channel.join")}
</ha-button>
</ha-dialog>
`;
}
@@ -171,22 +171,6 @@ class DialogNewDashboard extends LitElement implements HassDialog {
@click=${this._selected}
.config=${defaultConfig}
></dashboard-card>
<dashboard-card
.name=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.create_ai`
)}
.description=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.create_ai_description`
)}
.img=${this.hass.themes.darkMode
? "/static/images/dashboard-options/dark/icon-dashboard-new.svg"
: "/static/images/dashboard-options/light/icon-dashboard-new.svg"}
.alt=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.create_ai`
)}
@click=${this._selected}
.generateWithAI=${true}
></dashboard-card>
</div>
<div class="cards-container">
<div class="cards-container-header">
@@ -257,12 +241,6 @@ class DialogNewDashboard extends LitElement implements HassDialog {
const target = ev.currentTarget as any;
let config: any = null;
if (target.generateWithAI) {
this._params?.generateWithAI?.();
this.closeDialog();
return;
}
if (target.config) {
config = target.config;
} else if (target.strategy) {
@@ -3,7 +3,6 @@ import type { LovelaceConfig } from "../../../data/lovelace/config/types";
export interface NewDashboardDialogParams {
selectConfig: (config: LovelaceConfig | undefined) => any;
generateWithAI?: () => void;
}
export const loadNewDashboardDialog = () => import("./dialog-new-dashboard");
@@ -20,7 +20,6 @@ import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { ASSIST_ENTITIES, SENSOR_ENTITIES } from "../../../common/const";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeEntityEntryName } from "../../../common/entity/compute_entity_name";
@@ -304,11 +303,6 @@ export class HaConfigDevicePage extends LitElement {
super.updated(changedProps);
if (changedProps.has("deviceId")) {
this._findRelated();
// Broadcast device context for quick bar
fireEvent(this, "hass-quick-bar-context", {
itemType: "device",
itemId: this.deviceId,
});
}
}
@@ -16,6 +16,7 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
@@ -95,13 +96,6 @@ import { configSections } from "../ha-panel-config";
import "../integrations/ha-integration-overflow-menu";
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import {
getAreaTableColumn,
getFloorTableColumn,
getLabelsTableColumn,
getCreatedAtTableColumn,
getModifiedAtTableColumn,
} from "../common/data-table-columns";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
interface DeviceRowData extends DeviceRegistryEntry {
@@ -109,7 +103,7 @@ interface DeviceRowData extends DeviceRegistryEntry {
area?: string;
integration?: string;
battery_entity?: [string | undefined, string | undefined];
label_entries: LabelRegistryEntry[];
label_entries: EntityRegistryEntry[];
}
@customElement("ha-config-devices-dashboard")
@@ -456,7 +450,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
);
let floorName;
let floorName = "—";
if (
device.area_id &&
areas[device.area_id]?.floor_id &&
@@ -562,8 +556,22 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
: nothing}
`,
},
area: getAreaTableColumn(localize),
floor: getFloorTableColumn(localize),
area: {
title: localize("ui.panel.config.devices.data_table.area"),
sortable: true,
filterable: true,
groupable: true,
minWidth: "120px",
template: (device) => device.area || "—",
},
floor: {
title: localize("ui.panel.config.devices.data_table.floor"),
sortable: true,
filterable: true,
groupable: true,
minWidth: "120px",
defaultHidden: true,
},
integration: {
title: localize("ui.panel.config.devices.data_table.integration"),
sortable: true,
@@ -621,8 +629,34 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
: "—";
},
},
created_at: getCreatedAtTableColumn(localize, this.hass),
modified_at: getModifiedAtTableColumn(localize, this.hass),
created_at: {
title: localize("ui.panel.config.generic.headers.created_at"),
defaultHidden: true,
sortable: true,
minWidth: "128px",
template: (entry) =>
entry.created_at
? formatShortDateTime(
new Date(entry.created_at * 1000),
this.hass.locale,
this.hass.config
)
: "—",
},
modified_at: {
title: localize("ui.panel.config.generic.headers.modified_at"),
defaultHidden: true,
sortable: true,
minWidth: "128px",
template: (entry) =>
entry.modified_at
? formatShortDateTime(
new Date(entry.modified_at * 1000),
this.hass.locale,
this.hass.config
)
: "—",
},
disabled_by: {
title: localize("ui.panel.config.devices.picker.state"),
type: "icon",
@@ -651,7 +685,13 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
`
: "—",
},
labels: getLabelsTableColumn(),
labels: {
title: "",
hidden: true,
filterable: true,
template: (device) =>
device.label_entries.map((lbl) => lbl.name).join(" "),
},
} as DataTableColumnContainer<DeviceItem>;
});
@@ -20,7 +20,7 @@ import {
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type { HomeAssistant } from "../../../../types";
import type { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy";
const energyUnitClasses = ["energy"];
@@ -361,11 +361,11 @@ export class DialogEnergyBatterySettings
}
}
private _statisticToChanged(ev: ValueChangedEvent<string>) {
private _statisticToChanged(ev: CustomEvent<{ value: string }>) {
this._source = { ...this._source!, stat_energy_to: ev.detail.value };
}
private _statisticFromChanged(ev: ValueChangedEvent<string>) {
private _statisticFromChanged(ev: CustomEvent<{ value: string }>) {
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
}
@@ -376,26 +376,26 @@ export class DialogEnergyBatterySettings
this._powerConfig = {};
}
private _standardPowerChanged(ev: ValueChangedEvent<string>) {
private _standardPowerChanged(ev: CustomEvent<{ value: string }>) {
this._powerConfig = {
stat_rate: ev.detail.value,
};
}
private _invertedPowerChanged(ev: ValueChangedEvent<string>) {
private _invertedPowerChanged(ev: CustomEvent<{ value: string }>) {
this._powerConfig = {
stat_rate_inverted: ev.detail.value,
};
}
private _dischargePowerChanged(ev: ValueChangedEvent<string>) {
private _dischargePowerChanged(ev: CustomEvent<{ value: string }>) {
this._powerConfig = {
...this._powerConfig,
stat_rate_from: ev.detail.value,
};
}
private _chargePowerChanged(ev: ValueChangedEvent<string>) {
private _chargePowerChanged(ev: CustomEvent<{ value: string }>) {
this._powerConfig = {
...this._powerConfig,
stat_rate_to: ev.detail.value,
@@ -3,20 +3,21 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-radio";
import "../../../../components/ha-button";
import "../../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../../components/ha-select";
import "../../../../components/ha-list-item";
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
import { energyStatisticHelpUrl } from "../../../../data/energy";
import { getStatisticLabel } from "../../../../data/recorder";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type { HomeAssistant } from "../../../../types";
import type { EnergySettingsDeviceWaterDialogParams } from "./show-dialogs-energy";
const volumeUnitClasses = ["volume"];
@@ -94,27 +95,6 @@ export class DialogEnergyDeviceSettingsWater
const pickableUnit = this._volume_units?.join(", ") || "";
const includedInDeviceOptions = !this._possibleParents.length
? [
{
value: "-",
disabled: true,
label: this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.no_upstream_devices"
),
},
]
: this._possibleParents.map((stat) => ({
value: stat.stat_consumption,
label:
stat.name ||
getStatisticLabel(
this.hass,
stat.stat_consumption,
this._params?.statsMetadata?.[stat.stat_consumption]
),
}));
return html`
<ha-dialog
open
@@ -176,9 +156,31 @@ export class DialogEnergyDeviceSettingsWater
)}
.disabled=${!this._device}
@selected=${this._parentSelected}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
clearable
.options=${includedInDeviceOptions}
>
${!this._possibleParents.length
? html`
<ha-list-item disabled value="-"
>${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.no_upstream_devices"
)}</ha-list-item
>
`
: this._possibleParents.map(
(stat) => html`
<ha-list-item .value=${stat.stat_consumption}
>${stat.name ||
getStatisticLabel(
this.hass,
stat.stat_consumption,
this._params?.statsMetadata?.[stat.stat_consumption]
)}</ha-list-item
>
`
)}
</ha-select>
<ha-button
@@ -199,7 +201,7 @@ export class DialogEnergyDeviceSettingsWater
`;
}
private _statisticChanged(ev: ValueChangedEvent<string>) {
private _statisticChanged(ev: CustomEvent<{ value: string }>) {
if (!ev.detail.value) {
this._device = undefined;
return;
@@ -219,10 +221,10 @@ export class DialogEnergyDeviceSettingsWater
this._device = newDevice;
}
private _parentSelected(ev: HaSelectSelectEvent<string, true>) {
private _parentSelected(ev) {
const newDevice = {
...this._device!,
included_in_stat: ev.detail.value,
included_in_stat: ev.target!.value,
} as DeviceConsumptionEnergyPreference;
if (!newDevice.included_in_stat) {
delete newDevice.included_in_stat;
@@ -247,7 +249,6 @@ export class DialogEnergyDeviceSettingsWater
width: 100%;
}
ha-select {
display: block;
margin-top: 16px;
width: 100%;
}
@@ -3,23 +3,21 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-radio";
import "../../../../components/ha-button";
import "../../../../components/ha-select";
import type {
HaSelectOption,
HaSelectSelectEvent,
} from "../../../../components/ha-select";
import "../../../../components/ha-list-item";
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
import { energyStatisticHelpUrl } from "../../../../data/energy";
import { getStatisticLabel } from "../../../../data/recorder";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type { HomeAssistant } from "../../../../types";
import type { EnergySettingsDeviceDialogParams } from "./show-dialogs-energy";
const energyUnitClasses = ["energy"];
@@ -106,28 +104,6 @@ export class DialogEnergyDeviceSettings
return nothing;
}
const includedInDeviceOptions: HaSelectOption[] = this._possibleParents
.length
? this._possibleParents.map((stat) => ({
value: stat.stat_consumption,
label:
stat.name ||
getStatisticLabel(
this.hass,
stat.stat_consumption,
this._params?.statsMetadata?.[stat.stat_consumption]
),
}))
: [
{
value: "-",
disabled: true,
label: this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.no_upstream_devices"
),
},
];
return html`
<ha-dialog
open
@@ -202,9 +178,31 @@ export class DialogEnergyDeviceSettings
)}
.disabled=${!this._device}
@selected=${this._parentSelected}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
clearable
.options=${includedInDeviceOptions}
>
${!this._possibleParents.length
? html`
<ha-list-item disabled value="-"
>${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.no_upstream_devices"
)}</ha-list-item
>
`
: this._possibleParents.map(
(stat) => html`
<ha-list-item .value=${stat.stat_consumption}
>${stat.name ||
getStatisticLabel(
this.hass,
stat.stat_consumption,
this._params?.statsMetadata?.[stat.stat_consumption]
)}</ha-list-item
>
`
)}
</ha-select>
<ha-button
@@ -225,7 +223,7 @@ export class DialogEnergyDeviceSettings
`;
}
private _statisticChanged(ev: ValueChangedEvent<string>) {
private _statisticChanged(ev: CustomEvent<{ value: string }>) {
if (!ev.detail.value) {
this._device = undefined;
return;
@@ -234,7 +232,7 @@ export class DialogEnergyDeviceSettings
this._computePossibleParents();
}
private _powerStatisticChanged(ev: ValueChangedEvent<string>) {
private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) {
if (!this._device) {
return;
}
@@ -259,10 +257,10 @@ export class DialogEnergyDeviceSettings
this._device = newDevice;
}
private _parentSelected(ev: HaSelectSelectEvent<string, true>) {
private _parentSelected(ev) {
const newDevice = {
...this._device!,
included_in_stat: ev.detail.value,
included_in_stat: ev.target!.value,
} as DeviceConsumptionEnergyPreference;
if (!newDevice.included_in_stat) {
delete newDevice.included_in_stat;
@@ -291,7 +289,6 @@ export class DialogEnergyDeviceSettings
width: 100%;
}
ha-select {
display: block;
margin-top: var(--ha-space-4);
width: 100%;
}
@@ -25,7 +25,7 @@ import {
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type { HomeAssistant } from "../../../../types";
import type { EnergySettingsGasDialogParams } from "./show-dialogs-energy";
const gasDeviceClasses = ["gas", "energy"];
@@ -331,7 +331,7 @@ export class DialogEnergyGasSettings
};
}
private async _statisticChanged(ev: ValueChangedEvent<string>) {
private async _statisticChanged(ev: CustomEvent<{ value: string }>) {
if (ev.detail.value) {
const metadata = await getStatisticMetadata(this.hass, [ev.detail.value]);
this._pickedDisplayUnit = getDisplayUnit(
@@ -24,7 +24,7 @@ import { isExternalStatistic } from "../../../../data/recorder";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type { HomeAssistant } from "../../../../types";
import type { EnergySettingsGridFlowDialogParams } from "./show-dialogs-energy";
const energyUnitClasses = ["energy"];
@@ -325,7 +325,7 @@ export class DialogEnergyGridFlowSettings
};
}
private async _statisticChanged(ev: ValueChangedEvent<string>) {
private async _statisticChanged(ev: CustomEvent<{ value: string }>) {
if (
ev.detail.value &&
isExternalStatistic(ev.detail.value) &&
@@ -17,7 +17,7 @@ import { energyStatisticHelpUrl } from "../../../../data/energy";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type { HomeAssistant } from "../../../../types";
import type { EnergySettingsGridPowerDialogParams } from "./show-dialogs-energy";
const powerUnitClasses = ["power"];
@@ -287,26 +287,26 @@ export class DialogEnergyGridPowerSettings
this._powerConfig = {};
}
private _standardStatisticChanged(ev: ValueChangedEvent<string>) {
private _standardStatisticChanged(ev: CustomEvent<{ value: string }>) {
this._powerConfig = {
stat_rate: ev.detail.value,
};
}
private _invertedStatisticChanged(ev: ValueChangedEvent<string>) {
private _invertedStatisticChanged(ev: CustomEvent<{ value: string }>) {
this._powerConfig = {
stat_rate_inverted: ev.detail.value,
};
}
private _fromStatisticChanged(ev: ValueChangedEvent<string>) {
private _fromStatisticChanged(ev: CustomEvent<{ value: string }>) {
this._powerConfig = {
...this._powerConfig,
stat_rate_from: ev.detail.value,
};
}
private _toStatisticChanged(ev: ValueChangedEvent<string>) {
private _toStatisticChanged(ev: CustomEvent<{ value: string }>) {
this._powerConfig = {
...this._powerConfig,
stat_rate_to: ev.detail.value,
@@ -23,7 +23,7 @@ import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import { showConfigFlowDialog } from "../../../../dialogs/config-flow/show-dialog-config-flow";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type { HomeAssistant } from "../../../../types";
import { brandsUrl } from "../../../../util/brands-url";
import type { EnergySettingsSolarDialogParams } from "./show-dialogs-energy";
@@ -285,11 +285,11 @@ export class DialogEnergySolarSettings
});
}
private _statisticChanged(ev: ValueChangedEvent<string>) {
private _statisticChanged(ev: CustomEvent<{ value: string }>) {
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
}
private _powerStatisticChanged(ev: ValueChangedEvent<string>) {
private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) {
this._source = { ...this._source!, stat_rate: ev.detail.value };
}
@@ -21,7 +21,7 @@ import { isExternalStatistic } from "../../../../data/recorder";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type { HomeAssistant } from "../../../../types";
import type { EnergySettingsWaterDialogParams } from "./show-dialogs-energy";
@customElement("dialog-energy-water-settings")
@@ -277,7 +277,7 @@ export class DialogEnergyWaterSettings
};
}
private async _statisticChanged(ev: ValueChangedEvent<string>) {
private async _statisticChanged(ev: CustomEvent<{ value: string }>) {
if (
ev.detail.value &&
isExternalStatistic(ev.detail.value) &&
@@ -1,4 +1,3 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { mdiContentCopy, mdiRestore } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
@@ -8,6 +7,7 @@ import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import { supportsFeature } from "../../../common/entity/supports-feature";
@@ -22,7 +22,6 @@ import { copyToClipboard } from "../../../common/util/copy-clipboard";
import "../../../components/ha-alert";
import "../../../components/ha-area-picker";
import "../../../components/ha-color-picker";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button-next";
import "../../../components/ha-icon-picker";
@@ -35,7 +34,6 @@ import "../../../components/ha-state-icon";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-textfield";
import type { HaSelectSelectEvent } from "../../../components/ha-select";
import {
CAMERA_ORIENTATIONS,
CAMERA_SUPPORT_STREAM,
@@ -426,46 +424,34 @@ export class EntityRegistrySettingsEditor extends LitElement {
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_class"
)}
naturalMenuWidth
fixedMenuPosition
@selected=${this._switchAsDomainChanged}
value=${this._switchAsLabel(
this._switchAsDomain,
this._deviceClass
)}
@closed=${stopPropagation}
>
<ha-dropdown-item
<ha-list-item
value="switch"
class=${this._switchAsDomain === "switch" &&
(!this._deviceClass || this._deviceClass === "switch")
? "selected"
: ""}
.selected=${!this._deviceClass || this._deviceClass === "switch"}
>
${domainToName(this.hass.localize, "switch")}
</ha-dropdown-item>
<ha-dropdown-item
</ha-list-item>
<ha-list-item
value="outlet"
class=${this._switchAsDomain === "switch" &&
this._deviceClass === "outlet"
? "selected"
: ""}
.selected=${this._deviceClass === "outlet"}
>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_classes.switch.outlet"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
</ha-list-item>
<li divider role="separator"></li>
${this._switchAsDomainsSorted(
SWITCH_AS_DOMAINS,
this.hass.localize
).map(
(entry) => html`
<ha-dropdown-item
.value=${entry.domain}
class=${this._switchAsDomain === entry.domain
? "selected"
: ""}
>
<ha-list-item .value=${entry.domain}>
${entry.label}
</ha-dropdown-item>
</ha-list-item>
`
)}
</ha-select>`
@@ -474,22 +460,19 @@ export class EntityRegistrySettingsEditor extends LitElement {
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.switch_as_x"
)}
.value=${this._switchAsLabel(this._switchAsDomain)}
.value=${this._switchAsDomain}
naturalMenuWidth
fixedMenuPosition
@selected=${this._switchAsDomainChanged}
@closed=${stopPropagation}
>
<ha-dropdown-item
value="switch"
class=${this._switchAsDomain === "switch" ? "selected" : ""}
>
<ha-list-item value="switch">
${domainToName(this.hass.localize, "switch")}
</ha-dropdown-item>
<ha-dropdown-item
.value=${domain}
class=${this._switchAsDomain === domain ? "selected" : ""}
>
</ha-list-item>
<ha-list-item .value=${domain}>
${domainToName(this.hass.localize, domain)}
</ha-dropdown-item>
<wa-divider></wa-divider>
</ha-list-item>
<li divider role="separator"></li>
${this._switchAsDomainsSorted(
SWITCH_AS_DOMAINS,
this.hass.localize
@@ -497,14 +480,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
domain === entry.domain
? nothing
: html`
<ha-dropdown-item
.value=${entry.domain}
class=${this._switchAsDomain === entry.domain
? "selected"
: ""}
>
<ha-list-item .value=${entry.domain}>
${entry.label}
</ha-dropdown-item>
</ha-list-item>
`
)}
</ha-select>
@@ -535,13 +513,12 @@ export class EntityRegistrySettingsEditor extends LitElement {
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_class"
)}
.value=${this._deviceClass
? this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${this._deviceClass}`
)
: undefined}
.value=${this._deviceClass}
naturalMenuWidth
fixedMenuPosition
clearable
@selected=${this._deviceClassChanged}
@closed=${stopPropagation}
>
${this._deviceClassesSorted(
domain,
@@ -549,39 +526,29 @@ export class EntityRegistrySettingsEditor extends LitElement {
this.hass.localize
).map(
(entry) => html`
<ha-dropdown-item
.value=${entry.deviceClass}
class=${entry.deviceClass === this._deviceClass
? "selected"
: ""}
>
<ha-list-item .value=${entry.deviceClass}>
${entry.label}
</ha-dropdown-item>
</ha-list-item>
`
)}
${this._deviceClassOptions[0].length &&
this._deviceClassOptions[1].length
? html`<wa-divider></wa-divider>`
: nothing}
? html`<li divider role="separator"></li>`
: ""}
${this._deviceClassesSorted(
domain,
this._deviceClassOptions[1],
this.hass.localize
).map(
(entry) => html`
<ha-dropdown-item
.value=${entry.deviceClass}
class=${entry.deviceClass === this._deviceClass
? "selected"
: ""}
>
<ha-list-item .value=${entry.deviceClass}>
${entry.label}
</ha-dropdown-item>
</ha-list-item>
`
)}
</ha-select>
`
: nothing}
: ""}
${domain === "number" &&
this._deviceClass &&
stateObj?.attributes.unit_of_measurement &&
@@ -593,14 +560,20 @@ export class EntityRegistrySettingsEditor extends LitElement {
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.unit_of_measurement"
)}
.value=${this._unit_of_measurement ||
stateObj.attributes.unit_of_measurement}
.value=${stateObj.attributes.unit_of_measurement}
naturalMenuWidth
fixedMenuPosition
@selected=${this._unitChanged}
.options=${this._numberDeviceClassConvertibleUnits}
@closed=${stopPropagation}
>
${this._numberDeviceClassConvertibleUnits.map(
(unit: string) => html`
<ha-list-item .value=${unit}>${unit}</ha-list-item>
`
)}
</ha-select>
`
: nothing}
: ""}
${domain === "lock"
? html`
<ha-textfield
@@ -617,7 +590,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
@input=${this._defaultcodeChanged}
></ha-textfield>
`
: nothing}
: ""}
${domain === "alarm_control_panel"
? html`
<ha-textfield
@@ -630,7 +603,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
@input=${this._defaultcodeChanged}
></ha-textfield>
`
: nothing}
: ""}
${domain === "calendar"
? html`
<ha-color-picker
@@ -643,7 +616,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
@value-changed=${this._calendarColorChanged}
></ha-color-picker>
`
: nothing}
: ""}
${domain === "sensor" &&
this._deviceClass &&
stateObj?.attributes.unit_of_measurement &&
@@ -655,14 +628,20 @@ export class EntityRegistrySettingsEditor extends LitElement {
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.unit_of_measurement"
)}
.value=${this._unit_of_measurement ||
stateObj.attributes.unit_of_measurement}
.value=${stateObj.attributes.unit_of_measurement}
naturalMenuWidth
fixedMenuPosition
@selected=${this._unitChanged}
.options=${this._sensorDeviceClassConvertibleUnits}
@closed=${stopPropagation}
>
${this._sensorDeviceClassConvertibleUnits.map(
(unit: string) => html`
<ha-list-item .value=${unit}>${unit}</ha-list-item>
`
)}
</ha-select>
`
: nothing}
: ""}
${domain === "sensor" &&
// Allow customizing the precision for a sensor with numerical device class,
// a unit of measurement or state class
@@ -678,78 +657,116 @@ export class EntityRegistrySettingsEditor extends LitElement {
.value=${this._precision == null
? "default"
: this._precision.toString()}
naturalMenuWidth
fixedMenuPosition
@selected=${this._precisionChanged}
.options=${[
{
value: "default",
label: this.hass.localize(
"ui.dialogs.entity_registry.editor.precision_default",
{
value: this._precisionLabel(
defaultPrecision,
stateObj?.state
),
}
),
},
...PRECISIONS.map((precision) => ({
value: precision.toString(),
label: this._precisionLabel(precision, stateObj?.state),
})),
]}
@closed=${stopPropagation}
>
<ha-list-item value="default"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.precision_default",
{
value: this._precisionLabel(
defaultPrecision,
stateObj?.state
),
}
)}</ha-list-item
>
${PRECISIONS.map(
(precision) => html`
<ha-list-item .value=${precision.toString()}>
${this._precisionLabel(precision, stateObj?.state)}
</ha-list-item>
`
)}
</ha-select>
`
: nothing}
: ""}
${domain === "weather"
? html`
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.precipitation_unit"
)}
.value=${this._precipitation_unit || undefined}
.value=${this._precipitation_unit}
naturalMenuWidth
fixedMenuPosition
@selected=${this._precipitationUnitChanged}
.options=${this._weatherConvertibleUnits?.precipitation_unit}
@closed=${stopPropagation}
>
${this._weatherConvertibleUnits?.precipitation_unit.map(
(unit: string) => html`
<ha-list-item .value=${unit}>${unit}</ha-list-item>
`
)}
</ha-select>
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.pressure_unit"
)}
.value=${this._pressure_unit || undefined}
.value=${this._pressure_unit}
naturalMenuWidth
fixedMenuPosition
@selected=${this._pressureUnitChanged}
.options=${this._weatherConvertibleUnits?.pressure_unit}
@closed=${stopPropagation}
>
${this._weatherConvertibleUnits?.pressure_unit.map(
(unit: string) => html`
<ha-list-item .value=${unit}>${unit}</ha-list-item>
`
)}
</ha-select>
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.temperature_unit"
)}
.value=${this._temperature_unit || undefined}
.value=${this._temperature_unit}
naturalMenuWidth
fixedMenuPosition
@selected=${this._temperatureUnitChanged}
.options=${this._weatherConvertibleUnits?.temperature_unit}
@closed=${stopPropagation}
>
${this._weatherConvertibleUnits?.temperature_unit.map(
(unit: string) => html`
<ha-list-item .value=${unit}>${unit}</ha-list-item>
`
)}
</ha-select>
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.visibility_unit"
)}
.value=${this._visibility_unit || undefined}
.value=${this._visibility_unit}
naturalMenuWidth
fixedMenuPosition
@selected=${this._visibilityUnitChanged}
.options=${this._weatherConvertibleUnits?.visibility_unit}
@closed=${stopPropagation}
>
${this._weatherConvertibleUnits?.visibility_unit.map(
(unit: string) => html`
<ha-list-item .value=${unit}>${unit}</ha-list-item>
`
)}
</ha-select>
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.wind_speed_unit"
)}
.value=${this._wind_speed_unit || undefined}
.value=${this._wind_speed_unit}
naturalMenuWidth
fixedMenuPosition
@selected=${this._windSpeedUnitChanged}
.options=${this._weatherConvertibleUnits?.wind_speed_unit}
@closed=${stopPropagation}
>
${this._weatherConvertibleUnits?.wind_speed_unit.map(
(unit: string) => html`
<ha-list-item .value=${unit}>${unit}</ha-list-item>
`
)}
</ha-select>
`
: nothing}
: ""}
<ha-textfield
class="entityId"
.value=${computeObjectId(this._entityId)}
@@ -784,7 +801,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
.disabled=${this.disabled}
@value-changed=${this._areaPicked}
></ha-area-picker>`
: nothing}
: ""}
<ha-labels-picker
.hass=${this.hass}
.value=${this._labels}
@@ -826,21 +843,26 @@ export class EntityRegistrySettingsEditor extends LitElement {
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.stream.stream_orientation"
)}
naturalMenuWidth
fixedMenuPosition
.disabled=${this.disabled}
@selected=${this._handleCameraOrientationChanged}
.options=${CAMERA_ORIENTATIONS.map((num) => ({
value: num.toString(),
label: this.hass.localize(
("ui.dialogs.entity_registry.editor.stream.stream_orientation_" +
num.toString()) as LocalizeKeys
),
}))}
.value=${this._cameraPrefs.orientation.toString()}
@closed=${stopPropagation}
>
${CAMERA_ORIENTATIONS.map((num) => {
const localizeStr =
"ui.dialogs.entity_registry.editor.stream.stream_orientation_" +
num.toString();
return html`
<ha-list-item value=${num}>
${this.hass.localize(localizeStr as LocalizeKeys)}
</ha-list-item>
`;
})}
</ha-select>
</ha-settings-row>
`
: nothing}
: ""}
${this.helperConfigEntry &&
this.helperConfigEntry.supports_options &&
this.helperConfigEntry.domain !== "switch_as_x"
@@ -877,7 +899,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
: nothing}
: ""}
<ha-list-item
class="menu-item"
@@ -916,7 +938,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
}
)}</ha-alert
>`
: nothing}
: ""}
<ha-settings-row>
<span slot="heading"
@@ -951,7 +973,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
}
)}</ha-alert
>`
: nothing}
: ""}
<ha-settings-row>
<span slot="heading"
@@ -983,7 +1005,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
this.hass.devices[this.entry.device_id].area_id!
]?.name
})`
: nothing}</span
: ""}</span
>
<span slot="description"
>${this.hass.localize(
@@ -1014,8 +1036,8 @@ export class EntityRegistrySettingsEditor extends LitElement {
.disabled=${this.disabled}
@value-changed=${this._areaPicked}
></ha-area-picker>`
: nothing} `
: nothing}
: ""} `
: ""}
`;
}
@@ -1321,14 +1343,14 @@ export class EntityRegistrySettingsEditor extends LitElement {
this._entityId = `${computeDomain(this._origEntityId)}.${ev.target.value}`;
}
private _deviceClassChanged(ev: HaSelectSelectEvent<string, true>): void {
private _deviceClassChanged(ev): void {
fireEvent(this, "change");
this._deviceClass = ev.detail.value;
this._deviceClass = ev.target.value;
}
private _unitChanged(ev: HaSelectSelectEvent): void {
private _unitChanged(ev): void {
fireEvent(this, "change");
this._unit_of_measurement = ev.detail.value;
this._unit_of_measurement = ev.target.value;
}
private _defaultcodeChanged(ev): void {
@@ -1341,53 +1363,53 @@ export class EntityRegistrySettingsEditor extends LitElement {
this._calendarColor = ev.detail.value || null;
}
private _precipitationUnitChanged(ev: HaSelectSelectEvent): void {
private _precipitationUnitChanged(ev): void {
fireEvent(this, "change");
this._precipitation_unit = ev.detail.value;
this._precipitation_unit = ev.target.value;
}
private _precisionChanged(ev: HaSelectSelectEvent): void {
private _precisionChanged(ev): void {
fireEvent(this, "change");
this._precision =
ev.detail.value === "default" ? null : Number(ev.detail.value);
ev.target.value === "default" ? null : Number(ev.target.value);
}
private _pressureUnitChanged(ev: HaSelectSelectEvent): void {
private _pressureUnitChanged(ev): void {
fireEvent(this, "change");
this._pressure_unit = ev.detail.value;
this._pressure_unit = ev.target.value;
}
private _temperatureUnitChanged(ev: HaSelectSelectEvent): void {
private _temperatureUnitChanged(ev): void {
fireEvent(this, "change");
this._temperature_unit = ev.detail.value;
this._temperature_unit = ev.target.value;
}
private _visibilityUnitChanged(ev: HaSelectSelectEvent): void {
private _visibilityUnitChanged(ev): void {
fireEvent(this, "change");
this._visibility_unit = ev.detail.value;
this._visibility_unit = ev.target.value;
}
private _windSpeedUnitChanged(ev: HaSelectSelectEvent): void {
private _windSpeedUnitChanged(ev): void {
fireEvent(this, "change");
this._wind_speed_unit = ev.detail.value;
this._wind_speed_unit = ev.target.value;
}
private _switchAsDomainChanged(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
if (value === "") {
private _switchAsDomainChanged(ev): void {
if (ev.target.value === "") {
return;
}
// If value is "outlet" that means the user kept the "switch" domain, but actually changed
// the device_class of the switch to "outlet".
this._switchAsDomain = value === "outlet" ? "switch" : value;
const switchAs = ev.target.value === "outlet" ? "switch" : ev.target.value;
this._switchAsDomain = switchAs;
if (
(computeDomain(this.entry.entity_id) === "switch" &&
value === "outlet") ||
value === "switch"
ev.target.value === "outlet") ||
ev.target.value === "switch"
) {
this._deviceClass = value;
this._deviceClass = ev.target.value;
}
}
@@ -1444,13 +1466,13 @@ export class EntityRegistrySettingsEditor extends LitElement {
}
}
private async _handleCameraOrientationChanged(ev: HaSelectSelectEvent) {
private async _handleCameraOrientationChanged(ev) {
try {
this._cameraPrefs = await updateCameraPrefs(
this.hass,
this.entry.entity_id,
{
orientation: Number(ev.detail.value),
orientation: ev.currentTarget.value,
}
);
} catch (err: any) {
@@ -1525,35 +1547,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
)
);
private _switchAsLabel = memoizeOne(
(switchAsDomain: string, deviceClass?: string) => {
if (switchAsDomain !== "switch") {
const switchAsDomains = this._switchAsDomainsSorted(
SWITCH_AS_DOMAINS,
this.hass.localize
);
for (const entry of switchAsDomains) {
if (entry.domain === switchAsDomain) {
return entry.label;
}
}
}
if (!deviceClass || deviceClass === "switch") {
return domainToName(this.hass.localize, "switch");
}
if (deviceClass === "outlet") {
return this.hass.localize(
"ui.dialogs.entity_registry.editor.device_classes.switch.outlet"
);
}
return switchAsDomain;
}
);
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -1599,6 +1592,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
margin: var(--ha-space-2) 0;
width: 100%;
}
li[divider] {
border-bottom-color: var(--divider-color);
}
.menu-item {
border-radius: var(--ha-border-radius-sm);
margin-top: 3px;
@@ -24,6 +24,7 @@ import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoize from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { formatShortDateTimeWithConditionalYear } from "../../../common/datetime/format_date_time";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeAreaName } from "../../../common/entity/compute_area_name";
@@ -119,14 +120,6 @@ import { isHelperDomain } from "../helpers/const";
import "../integrations/ha-integration-overflow-menu";
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import {
getEntityIdTableColumn,
getDomainTableColumn,
getAreaTableColumn,
getLabelsTableColumn,
getCreatedAtTableColumn,
getModifiedAtTableColumn,
} from "../common/data-table-columns";
import {
getAssistantsSortableKey,
getAssistantsTableColumn,
@@ -383,15 +376,32 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
groupable: true,
hidden: true,
},
area: getAreaTableColumn(localize),
entity_id: getEntityIdTableColumn(localize, true),
area: {
title: localize("ui.panel.config.entities.picker.headers.area"),
sortable: true,
filterable: true,
groupable: true,
template: (entry) => entry.area || "—",
},
entity_id: {
title: localize("ui.panel.config.entities.picker.headers.entity_id"),
sortable: true,
filterable: true,
defaultHidden: true,
},
localized_platform: {
title: localize("ui.panel.config.entities.picker.headers.integration"),
sortable: true,
groupable: true,
filterable: true,
},
domain: getDomainTableColumn(localize),
domain: {
title: localize("ui.panel.config.entities.picker.headers.domain"),
sortable: false,
hidden: true,
filterable: true,
groupable: true,
},
disabled_by: {
title: localize("ui.panel.config.entities.picker.headers.disabled_by"),
hidden: true,
@@ -465,8 +475,34 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
`
: "—",
},
created_at: getCreatedAtTableColumn(localize, this.hass),
modified_at: getModifiedAtTableColumn(localize, this.hass),
created_at: {
title: localize("ui.panel.config.generic.headers.created_at"),
defaultHidden: true,
sortable: true,
minWidth: "128px",
template: (entry) =>
entry.created_at
? formatShortDateTimeWithConditionalYear(
new Date(entry.created_at * 1000),
this.hass.locale,
this.hass.config
)
: "—",
},
modified_at: {
title: localize("ui.panel.config.generic.headers.modified_at"),
defaultHidden: true,
sortable: true,
minWidth: "128px",
template: (entry) =>
entry.modified_at
? formatShortDateTimeWithConditionalYear(
new Date(entry.modified_at * 1000),
this.hass.locale,
this.hass.config
)
: "—",
},
available: {
title: localize("ui.panel.config.entities.picker.headers.availability"),
sortable: true,
@@ -485,7 +521,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
groupable: true,
hidden: true,
},
labels: getLabelsTableColumn(),
labels: {
title: "",
hidden: true,
filterable: true,
template: (entry) =>
entry.label_entries.map((lbl) => lbl.name).join(" "),
},
assistants: getAssistantsTableColumn(
localize,
this.hass,

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