mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-03 21:52:12 +00:00
Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa1678582d | |||
| a1bf30e501 | |||
| 37a45d1729 | |||
| 6962a915a3 | |||
| de3e2bcafa | |||
| e732280b70 | |||
| 4fb3453f73 | |||
| 0f9cb9c13e | |||
| 7aa235c6af | |||
| e8ddae8189 | |||
| c26e59f19c | |||
| 83aa06cb18 | |||
| 9d3d0dac48 | |||
| 8da1154924 | |||
| eb588075b8 | |||
| bdeaf10d74 | |||
| bec0d19fc9 | |||
| 325a7974c2 | |||
| fab1fde6e3 | |||
| 0e9564e676 | |||
| 244eb75049 | |||
| 644bb016d6 | |||
| 02dbcf0946 | |||
| f7df4d8a90 | |||
| e00ced23ee | |||
| f5cc2104ef | |||
| 161dd26b4d | |||
| 29cee99f10 | |||
| 47341e93fc | |||
| cbae7d6e2f | |||
| ebff35d17f | |||
| aec4a06156 | |||
| 917f2b4434 | |||
| 79ec6b972e | |||
| 9e35befa99 | |||
| 75160d67d3 | |||
| b145d09041 | |||
| f3f7a1e46a | |||
| 091315d9a9 | |||
| 75b830cdf9 | |||
| e4b8352832 | |||
| 4e193187f9 | |||
| 5394b3b8cf | |||
| 2ab867986a | |||
| a1c3a6c662 | |||
| 11296adbd4 | |||
| 4e04f4284e | |||
| a0cc0d9cca | |||
| c925053bb8 | |||
| 22a7aa8f8e | |||
| 3a5f719a3e | |||
| 7b7182c147 | |||
| 0eb7229819 | |||
| fcc6f1b5e9 | |||
| 19dc2a5865 | |||
| ae63530123 | |||
| 97e1f47af9 | |||
| 346d916944 | |||
| ab326b3277 | |||
| 55e251c04f | |||
| b902f3e6f5 | |||
| 9daac6c49f | |||
| 65ea0c9121 | |||
| 505cc698f6 | |||
| b0f9b31dae | |||
| 2b2966a214 | |||
| 558b251e32 | |||
| df28eaa99e | |||
| 40342c9cfd | |||
| c6573b9c1b | |||
| 54c46ad362 | |||
| 3376036392 | |||
| ff5ecc047a | |||
| a8393cddd4 | |||
| edfc33039b | |||
| 14f8d982a9 | |||
| 847a040fa7 | |||
| 8611359481 | |||
| f473ebf18c | |||
| 9010898742 | |||
| 1e30394bf3 | |||
| bca2cb0c1e | |||
| 29317eb842 | |||
| a8327ef59a | |||
| 01c8832024 | |||
| a8c633e627 | |||
| b659671814 | |||
| 3060cdf355 | |||
| 215241df56 | |||
| f6852894b0 | |||
| 86d7205a3a | |||
| 9c16ceda71 | |||
| 288789a604 | |||
| 09139d5bec | |||
| 2d90be9af3 | |||
| 1c6464663e | |||
| 189f0b9472 | |||
| 771f5eaff4 | |||
| a4cb3b5b01 | |||
| eb1ae99a1f | |||
| 655643eb3f | |||
| 02eb1e6832 | |||
| 62f7a2eea1 | |||
| a7f9b93018 | |||
| 3e7011e2c8 | |||
| 97f89bd983 | |||
| 9aedaeabbf | |||
| 7e839dc895 | |||
| 1f6c916d11 | |||
| 71bd12bb90 | |||
| e741c14482 | |||
| a496448ed9 |
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.13
|
||||
FROM mcr.microsoft.com/devcontainers/python:3.14
|
||||
|
||||
ENV \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
|
||||
@@ -251,7 +251,6 @@ 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):**
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
|
||||
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
|
||||
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
|
||||
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- cron: "0 1 * * *"
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.14"
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- published
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.13"
|
||||
PYTHON_VERSION: "3.14"
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
# Set default workflow permissions
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.12.0
|
||||
with:
|
||||
abi: cp313
|
||||
abi: cp314
|
||||
tag: musllinux_1_2
|
||||
arch: amd64
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
|
||||
@@ -9,11 +9,14 @@ import { selectedDemoConfig } from "./configs/demo-configs";
|
||||
import { mockAreaRegistry } from "./stubs/area_registry";
|
||||
import { mockAuth } from "./stubs/auth";
|
||||
import { mockConfigEntries } from "./stubs/config_entries";
|
||||
import { mockDeviceRegistry } from "./stubs/device_registry";
|
||||
import { mockEnergy } from "./stubs/energy";
|
||||
import { energyEntities } from "./stubs/entities";
|
||||
import { mockEntityRegistry } from "./stubs/entity_registry";
|
||||
import { mockEvents } from "./stubs/events";
|
||||
import { mockFloorRegistry } from "./stubs/floor_registry";
|
||||
import { mockFrontend } from "./stubs/frontend";
|
||||
import { mockLabelRegistry } from "./stubs/label_registry";
|
||||
import { mockIcons } from "./stubs/icons";
|
||||
import { mockHistory } from "./stubs/history";
|
||||
import { mockLovelace } from "./stubs/lovelace";
|
||||
@@ -60,6 +63,9 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockPersistentNotification(hass);
|
||||
mockConfigEntries(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockDeviceRegistry(hass);
|
||||
mockFloorRegistry(hass);
|
||||
mockLabelRegistry(hass);
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
|
||||
+28
-11
@@ -1,14 +1,12 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
let changeFunction;
|
||||
let sidebarChangeCallback;
|
||||
|
||||
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") {
|
||||
changeFunction?.({
|
||||
sidebarChangeCallback?.({
|
||||
value: {
|
||||
panelOrder: value.panelOrder || [],
|
||||
hiddenPanels: value.hiddenPanels || [],
|
||||
@@ -16,15 +14,34 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
|
||||
changeFunction = onChange;
|
||||
hass.mockWS("frontend/subscribe_user_data", (msg, _hass, onChange) => {
|
||||
if (msg.key === "sidebar") {
|
||||
sidebarChangeCallback = onChange;
|
||||
}
|
||||
onChange?.({ value: null });
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
hass.mockWS(
|
||||
"frontend/subscribe_system_data",
|
||||
(_msg, currentHass, onChange) => {
|
||||
onChange?.({
|
||||
value: currentHass.systemData,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
}
|
||||
);
|
||||
hass.mockWS("labs/subscribe", (_msg, _currentHass, onChange) => {
|
||||
onChange?.({
|
||||
value: {
|
||||
panelOrder: [],
|
||||
hiddenPanels: [],
|
||||
},
|
||||
preview_feature: _msg.preview_feature,
|
||||
domain: _msg.domain,
|
||||
enabled: false,
|
||||
is_built_in: true,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
|
||||
hass.mockWS("frontend/get_themes", (_msg, currentHass) => currentHass.themes);
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ 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(() => {
|
||||
|
||||
@@ -7,8 +7,18 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
|
||||
})
|
||||
);
|
||||
hass.mockWS("render_template", (msg, _hass, onChange) => {
|
||||
let result = msg.template;
|
||||
// Simple variable substitution for demo purposes
|
||||
if (msg.variables) {
|
||||
for (const [key, value] of Object.entries(msg.variables)) {
|
||||
result = result.replace(
|
||||
new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, "g"),
|
||||
String(value)
|
||||
);
|
||||
}
|
||||
}
|
||||
onChange!({
|
||||
result: msg.template,
|
||||
result,
|
||||
listeners: { all: false, domains: [], entities: [], time: false },
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
|
||||
@@ -10,7 +10,9 @@ As a community, we are proud of our logo. Follow these guidelines to ensure it a
|
||||
|
||||

|
||||
|
||||
Please note that this logo is not released under the CC license. All rights reserved.
|
||||
<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>
|
||||
|
||||
# Design
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
import "../../../../src/components/ha-alert";
|
||||
@@ -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. There’s a lot a designer can contribute to:
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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!
|
||||
|
||||
|
||||
@@ -100,7 +100,6 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
button-style
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
|
||||
+25
-26
@@ -27,25 +27,25 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.6",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.1",
|
||||
"@codemirror/language": "6.12.1",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.5.4",
|
||||
"@codemirror/view": "6.39.11",
|
||||
"@codemirror/view": "6.39.12",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@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",
|
||||
"@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",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
@@ -71,7 +71,6 @@
|
||||
"@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",
|
||||
@@ -89,7 +88,7 @@
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
@@ -112,7 +111,7 @@
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.1.1",
|
||||
"intl-messageformat": "11.1.2",
|
||||
"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",
|
||||
@@ -122,7 +121,7 @@
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"node-vibrant": "4.0.4",
|
||||
"object-hash": "3.0.0",
|
||||
"punycode": "2.3.1",
|
||||
"qr-scanner": "1.4.2",
|
||||
@@ -133,7 +132,7 @@
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"ua-parser-js": "2.0.8",
|
||||
"ua-parser-js": "2.0.9",
|
||||
"vue": "2.7.16",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
@@ -146,17 +145,17 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.6",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.6",
|
||||
"@babel/plugin-transform-runtime": "7.28.5",
|
||||
"@babel/preset-env": "7.28.6",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.9",
|
||||
"@lokalise/node-api": "15.6.0",
|
||||
"@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.0",
|
||||
"@rspack/core": "1.7.3",
|
||||
"@rsdoctor/rspack-plugin": "1.5.2",
|
||||
"@rspack/core": "1.7.5",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@@ -192,14 +191,14 @@
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"glob": "13.0.0",
|
||||
"glob": "13.0.1",
|
||||
"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": "27.4.0",
|
||||
"jsdom": "28.0.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@@ -211,7 +210,7 @@
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.1",
|
||||
"tar": "7.5.6",
|
||||
"tar": "7.5.7",
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
@@ -229,7 +228,7 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"globals": "17.1.0",
|
||||
"globals": "17.3.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"
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ readme = "README.md"
|
||||
authors = [
|
||||
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
|
||||
]
|
||||
requires-python = ">=3.13.0"
|
||||
requires-python = ">=3.14.0"
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/home-assistant/frontend"
|
||||
|
||||
@@ -16,6 +16,12 @@ 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
|
||||
|
||||
@@ -194,7 +194,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
button-style
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
let isViewTransitionDisabled = false;
|
||||
try {
|
||||
isViewTransitionDisabled =
|
||||
window.localStorage.getItem("disableViewTransition") === "true";
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
export const setViewTransitionDisabled = (disabled: boolean): void => {
|
||||
isViewTransitionDisabled = disabled;
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes a synchronous callback within a View Transition if supported, otherwise runs it directly.
|
||||
*
|
||||
@@ -14,7 +26,7 @@
|
||||
export const withViewTransition = (
|
||||
callback: (viewTransitionAvailable: boolean) => void
|
||||
): Promise<void> => {
|
||||
if (!document.startViewTransition) {
|
||||
if (!document.startViewTransition || isViewTransitionDisabled) {
|
||||
callback(false);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ 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";
|
||||
@@ -27,6 +28,7 @@ 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";
|
||||
@@ -92,10 +94,18 @@ 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 {
|
||||
@@ -113,8 +123,11 @@ 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()!();
|
||||
}
|
||||
@@ -126,7 +139,13 @@ export class HaChartBase extends LitElement {
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._setupChart();
|
||||
this._pendingSetup = true;
|
||||
afterNextRender(() => {
|
||||
if (this.isConnected && this._pendingSetup) {
|
||||
this._pendingSetup = false;
|
||||
this._setupChart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this._listeners.push(
|
||||
@@ -181,6 +200,26 @@ 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() {
|
||||
@@ -988,19 +1027,29 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
private _handleChartRenderFinished = () => {
|
||||
if (this._shouldResizeChart) {
|
||||
this.chart?.resize({
|
||||
animation:
|
||||
this._reducedMotion ||
|
||||
typeof this._resizeAnimationDuration !== "number"
|
||||
? undefined
|
||||
: { duration: this._resizeAnimationDuration },
|
||||
});
|
||||
this._shouldResizeChart = false;
|
||||
this._resizeAnimationDuration = undefined;
|
||||
}
|
||||
this._resizeChartIfNeeded();
|
||||
};
|
||||
|
||||
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
|
||||
@@ -1022,11 +1071,18 @@ 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);
|
||||
@@ -1034,6 +1090,7 @@ export class HaChartBase extends LitElement {
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
max-height: var(--chart-max-height, 350px);
|
||||
overflow: visible;
|
||||
}
|
||||
.has-height .chart-container {
|
||||
flex: 1;
|
||||
|
||||
@@ -58,7 +58,7 @@ export class HaSankeyChart extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public vertical = false;
|
||||
|
||||
@property({ type: String, attribute: false }) public valueFormatter?: (
|
||||
@property({ attribute: false }) public valueFormatter?: (
|
||||
value: number
|
||||
) => string;
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export class HaSunburstChart extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public data?: SunburstNode;
|
||||
|
||||
@property({ type: String, attribute: false }) public valueFormatter?: (
|
||||
@property({ attribute: false }) public valueFormatter?: (
|
||||
value: number
|
||||
) => string;
|
||||
|
||||
|
||||
@@ -50,16 +50,16 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
|
||||
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
|
||||
@property({ attribute: false }) public paddingYAxis = 0;
|
||||
|
||||
@property({ attribute: false, type: Number }) public chartIndex?;
|
||||
@property({ attribute: false }) public chartIndex?;
|
||||
|
||||
@property({ attribute: "logarithmic-scale", type: Boolean })
|
||||
public logarithmicScale = false;
|
||||
|
||||
@property({ attribute: false, type: Number }) public minYAxis?: number;
|
||||
@property({ attribute: false }) public minYAxis?: number;
|
||||
|
||||
@property({ attribute: false, type: Number }) public maxYAxis?: number;
|
||||
@property({ attribute: false }) public maxYAxis?: number;
|
||||
|
||||
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
|
||||
|
||||
@@ -716,6 +716,18 @@ export class StateHistoryChartLine extends LitElement {
|
||||
// Add an entry for final values
|
||||
pushData(endTime, prevValues);
|
||||
|
||||
// For sensors, append current state if viewing recent data
|
||||
const now = new Date();
|
||||
// allow 1s of leeway for "now"
|
||||
const isUpToNow = now.getTime() - endTime.getTime() <= 1000;
|
||||
if (domain === "sensor" && isUpToNow && data.length === 1) {
|
||||
const stateObj = this.hass.states[states.entity_id];
|
||||
const currentValue = stateObj ? safeParseFloat(stateObj.state) : null;
|
||||
if (currentValue !== null) {
|
||||
data[0].data!.push([now, currentValue]);
|
||||
}
|
||||
}
|
||||
|
||||
// Concat two arrays
|
||||
Array.prototype.push.apply(datasets, data);
|
||||
});
|
||||
|
||||
@@ -47,9 +47,9 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
|
||||
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
|
||||
@property({ attribute: false }) public paddingYAxis = 0;
|
||||
|
||||
@property({ attribute: false, type: Number }) public chartIndex?;
|
||||
@property({ attribute: false }) public chartIndex?;
|
||||
|
||||
@property({ attribute: "hide-reset-button", type: Boolean })
|
||||
public hideResetButton?: boolean;
|
||||
|
||||
@@ -60,7 +60,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
|
||||
|
||||
@property({ attribute: false, type: Number }) public hoursToShow?: number;
|
||||
@property({ attribute: false }) public hoursToShow?: number;
|
||||
|
||||
@property({ attribute: "show-names", type: Boolean }) public showNames = true;
|
||||
|
||||
@@ -73,9 +73,9 @@ export class StateHistoryCharts extends LitElement {
|
||||
@property({ attribute: "logarithmic-scale", type: Boolean })
|
||||
public logarithmicScale = false;
|
||||
|
||||
@property({ attribute: false, type: Number }) public minYAxis?: number;
|
||||
@property({ attribute: false }) public minYAxis?: number;
|
||||
|
||||
@property({ attribute: false, type: Number }) public maxYAxis?: number;
|
||||
@property({ attribute: false }) public maxYAxis?: number;
|
||||
|
||||
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
|
||||
|
||||
|
||||
@@ -62,14 +62,14 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
@property({ attribute: false })
|
||||
public statTypes: StatisticType[] = ["sum", "min", "mean", "max"];
|
||||
|
||||
@property({ attribute: false }) public chartType: "line" | "bar" = "line";
|
||||
|
||||
@property({ attribute: false, type: Number }) public minYAxis?: number;
|
||||
@property({ attribute: false }) public minYAxis?: number;
|
||||
|
||||
@property({ attribute: false, type: Number }) public maxYAxis?: number;
|
||||
@property({ attribute: false }) public maxYAxis?: number;
|
||||
|
||||
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
|
||||
|
||||
@@ -605,6 +605,57 @@ export class StatisticsChart extends LitElement {
|
||||
}
|
||||
});
|
||||
|
||||
// Append current state if viewing recent data
|
||||
const now = new Date();
|
||||
// allow 10m of leeway for "now", because stats are 5 minute aggregated
|
||||
const isUpToNow = now.getTime() - endTime.getTime() <= 600000;
|
||||
if (isUpToNow) {
|
||||
// Skip external statistics (they have ":" in the ID)
|
||||
if (!statistic_id.includes(":")) {
|
||||
const stateObj = this.hass.states[statistic_id];
|
||||
if (stateObj) {
|
||||
const currentValue = parseFloat(stateObj.state);
|
||||
if (
|
||||
isFinite(currentValue) &&
|
||||
!this._hiddenStats.has(statistic_id)
|
||||
) {
|
||||
// First, close out the last stat segment at prevEndTime
|
||||
const lastEndTime = prevEndTime;
|
||||
const lastValues = prevValues;
|
||||
if (lastEndTime && lastValues) {
|
||||
statDataSets.forEach((d, i) => {
|
||||
d.data!.push(
|
||||
this._transformDataValue([lastEndTime, ...lastValues[i]!])
|
||||
);
|
||||
});
|
||||
}
|
||||
// Then push the current state at now
|
||||
statTypes.forEach((type, i) => {
|
||||
const val: (number | null)[] = [];
|
||||
if (type === "sum" || type === "change") {
|
||||
// Skip cumulative types - need special calculation
|
||||
val.push(null);
|
||||
} else if (
|
||||
type === bandTop &&
|
||||
this.chartType === "line" &&
|
||||
drawBands &&
|
||||
!this._hiddenStats.has(`${statistic_id}-${bandBottom}`)
|
||||
) {
|
||||
// For band chart, current value is both min and max, so diff is 0
|
||||
val.push(0);
|
||||
val.push(currentValue);
|
||||
} else {
|
||||
val.push(currentValue);
|
||||
}
|
||||
statDataSets[i].data!.push(
|
||||
this._transformDataValue([now, ...val])
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Concat two arrays
|
||||
Array.prototype.push.apply(totalDataSets, statDataSets);
|
||||
Array.prototype.push.apply(legendData, statLegendData);
|
||||
|
||||
@@ -130,9 +130,9 @@ export class HaDataTable extends LitElement {
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: String }) public id = "id";
|
||||
|
||||
@property({ attribute: false, type: String }) public noDataText?: string;
|
||||
@property({ attribute: false }) public noDataText?: string;
|
||||
|
||||
@property({ attribute: false, type: String }) public searchLabel?: string;
|
||||
@property({ attribute: false }) public searchLabel?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-label-float" })
|
||||
public noLabelFloat? = false;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
sortDeviceAutomations,
|
||||
} from "../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } 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: CustomEvent<{ value: string }>) {
|
||||
private _automationChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (!value || NO_AUTOMATION_KEY === value) {
|
||||
|
||||
@@ -48,7 +48,7 @@ export class HaDevicePicker extends LitElement {
|
||||
@property({ type: String, attribute: "search-label" })
|
||||
public searchLabel?: string;
|
||||
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
@property({ attribute: false }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only devices with entities from specific domains.
|
||||
|
||||
@@ -76,7 +76,7 @@ class HaEntitiesPicker extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
@property({ attribute: false }) public createDomains?: string[];
|
||||
|
||||
@property({ type: Boolean })
|
||||
public reorder = false;
|
||||
|
||||
@@ -58,7 +58,7 @@ export class HaEntityPicker extends LitElement {
|
||||
@property({ type: String, attribute: "search-label" })
|
||||
public searchLabel?: string;
|
||||
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
@property({ attribute: false }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show entities from specific domains.
|
||||
|
||||
@@ -78,7 +78,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||
public allowCustomEntity;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
@property({ attribute: false })
|
||||
public statisticIds?: StatisticsMetaData[];
|
||||
|
||||
@property({ attribute: false }) public helpMissingEntityUrl =
|
||||
|
||||
@@ -11,7 +11,7 @@ class HaStatisticsPicker extends LitElement {
|
||||
|
||||
@property({ type: Array }) public value?: string[];
|
||||
|
||||
@property({ attribute: false, type: Array }) public statisticIds?: string[];
|
||||
@property({ attribute: false }) public statisticIds?: string[];
|
||||
|
||||
@property({ attribute: "statistic-types" })
|
||||
public statisticTypes?: "mean" | "sum";
|
||||
|
||||
@@ -48,7 +48,6 @@ 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 } from "../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } 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: CustomEvent<{ value: DisplayValue }>) {
|
||||
private async _areaDisplayChanged(ev: ValueChangedEvent<DisplayValue>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
const currentFloorId = (ev.currentTarget as any).floorId;
|
||||
|
||||
@@ -36,7 +36,7 @@ export class HaAssistChat extends LitElement {
|
||||
@property({ type: Boolean, attribute: "disable-speech" })
|
||||
public disableSpeech = false;
|
||||
|
||||
@property({ type: Boolean, attribute: false })
|
||||
@property({ attribute: false })
|
||||
public startListening?: boolean;
|
||||
|
||||
@query("#message-input") private _messageInput!: HaTextField;
|
||||
|
||||
@@ -2,14 +2,12 @@ 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 "./ha-list-item";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const PREFERRED = "preferred";
|
||||
const LAST_USED = "last_used";
|
||||
@@ -41,6 +39,31 @@ 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 ||
|
||||
@@ -49,33 +72,8 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.options=${options}
|
||||
>
|
||||
${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>
|
||||
`;
|
||||
}
|
||||
@@ -96,17 +94,17 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === this._default)
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === this._default)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value === this._default ? undefined : target.value;
|
||||
this.value = value === this._default ? undefined : value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,9 @@ 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 { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
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";
|
||||
@@ -260,14 +259,10 @@ export class HaBaseTimeInput extends LitElement {
|
||||
.required=${this.required}
|
||||
.value=${this.amPm}
|
||||
.disabled=${this.disabled}
|
||||
name="amPm"
|
||||
naturalMenuWidth
|
||||
fixedMenuPosition
|
||||
.name=${"amPm"}
|
||||
@selected=${this._valueChanged}
|
||||
@closed=${stopPropagation}
|
||||
.options=${["AM", "PM"]}
|
||||
>
|
||||
<ha-list-item value="AM">AM</ha-list-item>
|
||||
<ha-list-item value="PM">PM</ha-list-item>
|
||||
</ha-select>`}
|
||||
</div>
|
||||
${this.helper
|
||||
@@ -282,10 +277,12 @@ export class HaBaseTimeInput extends LitElement {
|
||||
fireEvent(this, "value-changed");
|
||||
}
|
||||
|
||||
private _valueChanged(ev: InputEvent) {
|
||||
private _valueChanged(ev: InputEvent | HaSelectSelectEvent): void {
|
||||
const textField = ev.currentTarget as HaTextField;
|
||||
this[textField.name] =
|
||||
textField.name === "amPm" ? textField.value : Number(textField.value);
|
||||
textField.name === "amPm"
|
||||
? (ev as HaSelectSelectEvent).detail.value
|
||||
: Number(textField.value);
|
||||
const value: TimeChangedEvent = {
|
||||
hours: this.hours,
|
||||
minutes: this.minutes,
|
||||
@@ -366,10 +363,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@ 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 "./ha-list-item";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
|
||||
@customElement("ha-blueprint-picker")
|
||||
@@ -55,20 +54,16 @@ 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}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${this._processedBlueprints(this.blueprints).map(
|
||||
(blueprint) => html`
|
||||
<ha-list-item .value=${blueprint.path}>
|
||||
${blueprint.name}
|
||||
</ha-list-item>
|
||||
`
|
||||
.options=${this._processedBlueprints(this.blueprints).map(
|
||||
(blueprint) => ({
|
||||
value: blueprint.path,
|
||||
label: blueprint.name,
|
||||
})
|
||||
)}
|
||||
>
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@@ -82,8 +77,8 @@ class HaBluePrintPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _blueprintChanged(ev) {
|
||||
const newValue = ev.target.value;
|
||||
private _blueprintChanged(ev: HaSelectSelectEvent) {
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (newValue !== this.value) {
|
||||
this.value = newValue;
|
||||
|
||||
@@ -51,6 +51,7 @@ export class HaCard extends LitElement {
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
}
|
||||
|
||||
/* clean-css ignore:start */
|
||||
:host
|
||||
::slotted(
|
||||
.card-content:not(:nth-child(1 of .card-content, .card-header))
|
||||
@@ -59,6 +60,7 @@ export class HaCard extends LitElement {
|
||||
padding-top: 0;
|
||||
margin-top: calc(var(--ha-space-2) * -1);
|
||||
}
|
||||
/* clean-css ignore:end */
|
||||
|
||||
:host ::slotted(.card-content) {
|
||||
padding: var(--ha-space-4);
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
||||
import { mdiMenuDown } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, nothing } from "lit";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-attribute-icon";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-icon";
|
||||
import type { HaIcon } from "./ha-icon";
|
||||
import "./ha-ripple";
|
||||
import "./ha-svg-icon";
|
||||
import type { HaSvgIcon } from "./ha-svg-icon";
|
||||
import "./ha-menu";
|
||||
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
value: string;
|
||||
iconPath?: string;
|
||||
icon?: string;
|
||||
attributeIcon?: {
|
||||
stateObj: HassEntity;
|
||||
attribute: string;
|
||||
attributeValue?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@customElement("ha-control-select-menu")
|
||||
export class HaControlSelectMenu extends SelectBase {
|
||||
@query(".select") protected mdcRoot!: HTMLElement;
|
||||
|
||||
@query(".select-anchor") protected anchorElement!: HTMLDivElement | null;
|
||||
export class HaControlSelectMenu extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-arrow" })
|
||||
public showArrow = false;
|
||||
@@ -26,95 +33,83 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
@property({ type: Boolean, attribute: "hide-label" })
|
||||
public hideLabel = false;
|
||||
|
||||
@property() public options;
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.get("options")) {
|
||||
this.layoutOptions();
|
||||
this.selectByValue(this.value);
|
||||
}
|
||||
}
|
||||
@property({ type: Boolean })
|
||||
public required = false;
|
||||
|
||||
@property()
|
||||
public label?: string;
|
||||
|
||||
@property()
|
||||
public value?: string;
|
||||
|
||||
@property({ attribute: false }) public options: SelectOption[] = [];
|
||||
|
||||
@query("button") private _triggerButton!: HTMLButtonElement;
|
||||
|
||||
public override render() {
|
||||
const classes = {
|
||||
"select-disabled": this.disabled,
|
||||
"select-required": this.required,
|
||||
"select-invalid": !this.isUiValid,
|
||||
"select-no-value": !this.selectedText,
|
||||
};
|
||||
|
||||
const labelledby = this.label && !this.hideLabel ? "label" : undefined;
|
||||
const labelAttribute =
|
||||
this.label && this.hideLabel ? this.label : undefined;
|
||||
if (this.disabled) {
|
||||
return this._renderTrigger();
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="select ${classMap(classes)}">
|
||||
<input
|
||||
class="formElement"
|
||||
.name=${this.name}
|
||||
.value=${this.value}
|
||||
hidden
|
||||
?disabled=${this.disabled}
|
||||
?required=${this.required}
|
||||
/>
|
||||
<!-- @ts-ignore -->
|
||||
<div
|
||||
class="select-anchor"
|
||||
aria-autocomplete="none"
|
||||
role="combobox"
|
||||
aria-expanded=${this.menuOpen}
|
||||
aria-invalid=${!this.isUiValid}
|
||||
aria-haspopup="listbox"
|
||||
aria-labelledby=${ifDefined(labelledby)}
|
||||
aria-label=${ifDefined(labelAttribute)}
|
||||
aria-required=${this.required}
|
||||
aria-controls="listbox"
|
||||
@focus=${this.onFocus}
|
||||
@blur=${this.onBlur}
|
||||
@click=${this.onClick}
|
||||
@keydown=${this.onKeydown}
|
||||
>
|
||||
${this._renderIcon()}
|
||||
<div class="content">
|
||||
${this.hideLabel
|
||||
? nothing
|
||||
: html`<p id="label" class="label">${this.label}</p>`}
|
||||
${this.selectedText
|
||||
? html`<p class="value">${this.selectedText}</p>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._renderArrow()}
|
||||
<ha-ripple .disabled=${this.disabled}></ha-ripple>
|
||||
</div>
|
||||
${this.renderMenu()}
|
||||
</div>
|
||||
<ha-dropdown placement="bottom" @wa-show=${this._showDropdown}>
|
||||
${this._renderTrigger()} ${this.options.map(this._renderOption)}
|
||||
</ha-dropdown>
|
||||
`;
|
||||
}
|
||||
|
||||
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}
|
||||
private _renderTrigger() {
|
||||
const selectedText = this.getValueObject(this.options, this.value)?.label;
|
||||
|
||||
const classes = {
|
||||
"select-disabled": this.disabled,
|
||||
"select-required": this.required,
|
||||
"select-no-value": !selectedText,
|
||||
};
|
||||
|
||||
return html`<button
|
||||
?disabled=${this.disabled}
|
||||
slot="trigger"
|
||||
class="select-anchor ${classMap(classes)}"
|
||||
>
|
||||
${this.renderMenuContent()}
|
||||
</ha-menu>`;
|
||||
${this._renderIcon()}
|
||||
<div class="content">
|
||||
${this.hideLabel
|
||||
? nothing
|
||||
: html`<p id="label" class="label">${this.label}</p>`}
|
||||
${selectedText ? html`<p class="value">${selectedText}</p>` : nothing}
|
||||
</div>
|
||||
${this._renderArrow()}
|
||||
</button>`;
|
||||
}
|
||||
|
||||
private _renderOption = (option: SelectOption) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${option.value}
|
||||
class=${this.value === option.value ? "selected" : ""}
|
||||
>${option.iconPath
|
||||
? html`<ha-svg-icon slot="icon" .path=${option.iconPath}></ha-svg-icon>`
|
||||
: option.icon
|
||||
? html`<ha-icon slot="icon" .icon=${option.icon}></ha-icon>`
|
||||
: option.attributeIcon
|
||||
? html`<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${option.attributeIcon.stateObj}
|
||||
.attribute=${option.attributeIcon.attribute}
|
||||
.attributeValue=${option.attributeIcon.attributeValue}
|
||||
></ha-attribute-icon>`
|
||||
: nothing}
|
||||
${option.label}</ha-dropdown-item
|
||||
>`;
|
||||
|
||||
private _renderArrow() {
|
||||
if (!this.showArrow) return nothing;
|
||||
if (!this.showArrow) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="icon">
|
||||
@@ -124,47 +119,42 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
}
|
||||
|
||||
private _renderIcon() {
|
||||
const index = this.mdcFoundation?.getSelectedIndex();
|
||||
const items = this.menuElement?.items ?? [];
|
||||
const item = index != null ? items[index] : undefined;
|
||||
const { iconPath, icon, attributeIcon } =
|
||||
this.getValueObject(this.options, this.value) ?? {};
|
||||
const defaultIcon = this.querySelector("[slot='icon']");
|
||||
const icon = (item?.querySelector("[slot='graphic']") ?? null) as
|
||||
| HaSvgIcon
|
||||
| HaIcon
|
||||
| null;
|
||||
|
||||
if (!defaultIcon && !icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="icon">
|
||||
${icon && icon.localName === "ha-svg-icon" && "path" in icon
|
||||
? html`<ha-svg-icon .path=${icon.path}></ha-svg-icon>`
|
||||
: icon && icon.localName === "ha-icon" && "icon" in icon
|
||||
? html`<ha-icon .path=${icon.icon}></ha-icon>`
|
||||
: html`<slot name="icon"></slot>`}
|
||||
${iconPath
|
||||
? html`<ha-svg-icon slot="icon" .path=${iconPath}></ha-svg-icon>`
|
||||
: icon
|
||||
? html`<ha-icon slot="icon" .icon=${icon}></ha-icon>`
|
||||
: attributeIcon
|
||||
? html`<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${attributeIcon.stateObj}
|
||||
.attribute=${attributeIcon.attribute}
|
||||
.attributeValue=${attributeIcon.attributeValue}
|
||||
></ha-attribute-icon>`
|
||||
: defaultIcon
|
||||
? html`<slot name="icon"></slot>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("translations-updated", this._translationsUpdated);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener(
|
||||
"translations-updated",
|
||||
this._translationsUpdated
|
||||
private _showDropdown() {
|
||||
this.style.setProperty(
|
||||
"--control-select-menu-width",
|
||||
`${this._triggerButton.offsetWidth}px`
|
||||
);
|
||||
}
|
||||
|
||||
private _translationsUpdated = debounce(async () => {
|
||||
await nextRender();
|
||||
this.layoutOptions();
|
||||
}, 500);
|
||||
private getValueObject = memoizeOne(
|
||||
(options: SelectOption[], value?: string) =>
|
||||
options.find((option) => option.value === value)
|
||||
);
|
||||
|
||||
static override styles = [
|
||||
css`
|
||||
@@ -186,6 +176,8 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.select-anchor {
|
||||
border: none;
|
||||
text-align: left;
|
||||
height: var(--control-select-menu-height);
|
||||
padding: var(--control-select-menu-padding);
|
||||
overflow: hidden;
|
||||
@@ -211,6 +203,12 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
letter-spacing: 0.25px;
|
||||
}
|
||||
.select-anchor:hover {
|
||||
--control-select-menu-background-color: var(
|
||||
--ha-color-on-neutral-quiet
|
||||
);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -230,16 +228,19 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.85em;
|
||||
font-size: var(--ha-font-size-s);
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.select-no-value .label {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
letter-spacing: inherit;
|
||||
}
|
||||
|
||||
.content .value {
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
.select-anchor:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--control-select-menu-focus-color);
|
||||
}
|
||||
@@ -258,10 +259,23 @@ export class HaControlSelectMenu extends SelectBase {
|
||||
opacity: var(--control-select-menu-background-opacity);
|
||||
}
|
||||
|
||||
.select-disabled .select-anchor {
|
||||
.select-disabled.select-anchor {
|
||||
cursor: not-allowed;
|
||||
color: var(--disabled-color);
|
||||
}
|
||||
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);
|
||||
}
|
||||
ha-dropdown-item.selected:hover {
|
||||
background-color: var(--ha-color-fill-primary-quiet-hover);
|
||||
}
|
||||
|
||||
ha-dropdown::part(menu) {
|
||||
min-width: var(--control-select-menu-width);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ export class HaControlSwitch extends LitElement {
|
||||
@property({ type: Boolean }) public checked = false;
|
||||
|
||||
// SVG icon path (if you need a non SVG icon instead, use the provided on icon slot to pass an <ha-icon slot="icon-on"> in)
|
||||
@property({ attribute: false, type: String }) pathOn?: string;
|
||||
@property({ attribute: false }) pathOn?: string;
|
||||
|
||||
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
|
||||
@property({ attribute: false, type: String }) pathOff?: string;
|
||||
@property({ attribute: false }) pathOff?: string;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
@@ -3,7 +3,6 @@ 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";
|
||||
@@ -14,9 +13,8 @@ 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 { HaSelect } from "./ha-select";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@@ -73,37 +71,35 @@ 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.coversation-agent-picker.conversation_agent"
|
||||
"ui.components.conversation-agent-picker.conversation_agent"
|
||||
)}
|
||||
.value=${value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@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
|
||||
.options=${options}
|
||||
></ha-select
|
||||
>${(this._subConfigEntry &&
|
||||
this._configEntry?.supported_subentry_types[
|
||||
this._subConfigEntry.subentry_type
|
||||
@@ -238,17 +234,17 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
this.value = value === NONE ? undefined : value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._agents!.find((agent) => agent.id === this.value)
|
||||
|
||||
@@ -65,6 +65,10 @@ 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;
|
||||
@@ -306,6 +310,15 @@ 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) {
|
||||
|
||||
@@ -76,6 +76,18 @@ 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)
|
||||
@@ -84,9 +96,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);
|
||||
}
|
||||
.mdc-dialog__actions {
|
||||
justify-content: var(--justify-action-buttons, flex-end);
|
||||
|
||||
@@ -4,6 +4,18 @@ 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;
|
||||
|
||||
@@ -15,6 +27,30 @@ 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(),
|
||||
@@ -63,6 +99,38 @@ 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, {
|
||||
@@ -90,6 +158,16 @@ 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;
|
||||
@@ -103,6 +181,15 @@ 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,9 +1,9 @@
|
||||
import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-item/dropdown-item";
|
||||
import "@home-assistant/webawesome/dist/components/icon/icon";
|
||||
import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
|
||||
import { css, type CSSResultGroup, html } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "./ha-svg-icon";
|
||||
import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
|
||||
|
||||
/**
|
||||
* Home Assistant dropdown item component
|
||||
@@ -37,6 +37,7 @@ export class HaDropdownItem extends DropdownItem {
|
||||
|
||||
#check {
|
||||
visibility: visible;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#icon ::slotted(*) {
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdown";
|
||||
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 };
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Home Assistant dropdown component
|
||||
|
||||
@@ -31,6 +31,7 @@ import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-list";
|
||||
import "./ha-list-item";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
|
||||
@customElement("ha-filter-categories")
|
||||
export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
@@ -174,7 +175,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<{ item: { value: string } }>) {
|
||||
private _handleAction(ev: HaDropdownSelectEvent) {
|
||||
const categoryId = (ev.currentTarget as any).categoryId;
|
||||
const action = ev.detail.item.value;
|
||||
switch (action) {
|
||||
|
||||
@@ -11,8 +11,7 @@ import "../ha-formfield";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-picker-field";
|
||||
|
||||
import type { HaDropdown } from "../ha-dropdown";
|
||||
import type { HaDropdownItem } from "../ha-dropdown-item";
|
||||
import type { HaDropdown, HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
import type {
|
||||
HaFormElement,
|
||||
HaFormMultiSelectData,
|
||||
@@ -108,7 +107,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected _toggleItem(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
protected _toggleItem(ev: HaDropdownSelectEvent) {
|
||||
ev.preventDefault(); // keep the dropdown open
|
||||
const value = ev.detail.item.value;
|
||||
const action = (ev.detail.item as any).action;
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
HaFormOptionalActionsSchema,
|
||||
HaFormSchema,
|
||||
} from "./types";
|
||||
import type { HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
|
||||
const NO_ACTIONS = [];
|
||||
|
||||
@@ -142,7 +143,7 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAddAction(ev: CustomEvent<{ item: { value: string } }>) {
|
||||
private _handleAddAction(ev: HaDropdownSelectEvent) {
|
||||
const action = ev.detail.item.value;
|
||||
this._displayActions = [...(this._displayActions ?? []), action];
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class HaGauge extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public formatOptions?: Intl.NumberFormatOptions;
|
||||
|
||||
@property({ attribute: false, type: String }) public valueText?: string;
|
||||
@property({ attribute: false }) public valueText?: string;
|
||||
|
||||
@property({ attribute: false }) public locale!: FrontendLocaleData;
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[] | undefined;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
@property({ attribute: false })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
|
||||
@property({ attribute: false })
|
||||
|
||||
@@ -113,13 +113,13 @@ class HaHsColorPicker extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public disabled = false;
|
||||
|
||||
@property({ type: Number, attribute: false })
|
||||
@property({ attribute: false })
|
||||
public renderSize?: number;
|
||||
|
||||
@property({ type: Array })
|
||||
public value?: [number, number];
|
||||
|
||||
@property({ attribute: false, type: Number })
|
||||
@property({ attribute: false })
|
||||
public colorBrightness?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
@@ -131,10 +131,10 @@ class HaHsColorPicker extends LitElement {
|
||||
@property({ type: Number })
|
||||
public ww?: number;
|
||||
|
||||
@property({ attribute: false, type: Number })
|
||||
@property({ attribute: false })
|
||||
public minKelvin?: number;
|
||||
|
||||
@property({ attribute: false, type: Number })
|
||||
@property({ attribute: false })
|
||||
public maxKelvin?: number;
|
||||
|
||||
@query("#canvas") private _canvas!: HTMLCanvasElement;
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface HaIconButtonToolbarItem {
|
||||
|
||||
@customElement("ha-icon-button-toolbar")
|
||||
export class HaIconButtonToolbar extends LitElement {
|
||||
@property({ type: Array, attribute: false })
|
||||
@property({ attribute: false })
|
||||
public items: (HaIconButtonToolbarItem | string)[] = [];
|
||||
|
||||
@queryAll("ha-icon-button") private _buttons?: HaIconButton[];
|
||||
|
||||
@@ -107,9 +107,6 @@ 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;
|
||||
|
||||
@@ -134,16 +134,51 @@ export class HaMarkdown extends LitElement {
|
||||
}
|
||||
table[role="presentation"] {
|
||||
--markdown-table-border-collapse: separate;
|
||||
--markdown-table-border-width: attr(border, 0);
|
||||
--markdown-table-border-width: 0;
|
||||
--markdown-table-padding-inline: 0;
|
||||
--markdown-table-padding-block: 0;
|
||||
th {
|
||||
vertical-align: attr(valign, middle);
|
||||
}
|
||||
th,
|
||||
td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
table[role="presentation"] td[valign="top"],
|
||||
table[role="presentation"] th[valign="top"] {
|
||||
vertical-align: top;
|
||||
}
|
||||
table[role="presentation"] td[valign="middle"],
|
||||
table[role="presentation"] th[valign="middle"] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
table[role="presentation"] td[valign="bottom"],
|
||||
table[role="presentation"] th[valign="bottom"] {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
table[role="presentation"] td[valign="baseline"],
|
||||
table[role="presentation"] th[valign="baseline"] {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
@supports (border-width: attr(border px, 0)) {
|
||||
table[role="presentation"] {
|
||||
--markdown-table-border-width: attr(border px, 0);
|
||||
}
|
||||
table[role="presentation"] th,
|
||||
table[role="presentation"] td {
|
||||
vertical-align: attr(valign, middle);
|
||||
}
|
||||
}
|
||||
table[role="presentation"][border="0"] {
|
||||
--markdown-table-border-width: 0;
|
||||
}
|
||||
table[role="presentation"][border="1"] {
|
||||
--markdown-table-border-width: 1px;
|
||||
}
|
||||
table[role="presentation"][border="2"] {
|
||||
--markdown-table-border-width: 2px;
|
||||
}
|
||||
table[role="presentation"][border="3"] {
|
||||
--markdown-table-border-width: 3px;
|
||||
}
|
||||
table {
|
||||
border-collapse: var(--markdown-table-border-collapse, collapse);
|
||||
}
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ 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 {
|
||||
@@ -15,9 +14,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";
|
||||
|
||||
@@ -52,60 +51,54 @@ class HaMountPicker extends LitElement {
|
||||
if (!this._mounts) {
|
||||
return nothing;
|
||||
}
|
||||
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>`;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.options=${options}
|
||||
>
|
||||
${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>
|
||||
`;
|
||||
}
|
||||
@@ -153,16 +146,10 @@ class HaMountPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
private _mountChanged(ev: HaSelectSelectEvent) {
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
private _mountChanged(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HaSelect;
|
||||
const newValue = target.value;
|
||||
|
||||
if (newValue !== this._value) {
|
||||
if (newValue !== this.value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { fetchConfig } from "../data/lovelace/config/types";
|
||||
import { getPanelIcon, getPanelTitle } from "../data/panel";
|
||||
import { findRelated, type RelatedResult } from "../data/search";
|
||||
import { PANEL_DASHBOARDS } from "../panels/config/lovelace/dashboards/ha-config-lovelace-dashboards";
|
||||
import { computeAreaPath } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import { multiTermSortedSearch } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { ActionRelatedContext } from "../panels/lovelace/components/hui-action-editor";
|
||||
@@ -25,8 +26,9 @@ import {
|
||||
type NavigationGroup = "related" | "dashboards" | "views" | "other_routes";
|
||||
|
||||
const RELATED_SORT_PREFIX = {
|
||||
area: "0_area",
|
||||
device: "1_device",
|
||||
area_view: "0_area_view",
|
||||
area: "1_area_settings",
|
||||
device: "2_device",
|
||||
} as const;
|
||||
|
||||
interface NavigationItem extends PickerComboBoxItem {
|
||||
@@ -260,12 +262,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
|
||||
const viewConfigs = await Promise.all(
|
||||
lovelacePanels.map((panel) =>
|
||||
fetchConfig(
|
||||
this.hass!.connection,
|
||||
// path should be null to fetch default lovelace panel
|
||||
panel.url_path === "lovelace" ? null : panel.url_path,
|
||||
true
|
||||
)
|
||||
fetchConfig(this.hass!.connection, panel.url_path, true)
|
||||
.then((config) => [panel.id, config] as [string, typeof config])
|
||||
.catch((_) => [panel.id, undefined] as [string, undefined])
|
||||
)
|
||||
@@ -437,10 +434,31 @@ export class HaNavigationPicker extends LitElement {
|
||||
for (const areaId of relatedAreaIds) {
|
||||
const area = this.hass.areas[areaId];
|
||||
const primary = area?.name ?? areaId;
|
||||
|
||||
// Area dashboard view
|
||||
const viewPath = `/home/${computeAreaPath(areaId)}`;
|
||||
relatedItems.push({
|
||||
id: viewPath,
|
||||
primary,
|
||||
secondary: viewPath,
|
||||
icon: area?.icon ?? undefined,
|
||||
icon_path: area?.icon ? undefined : mdiTextureBox,
|
||||
sorting_label: createSortingLabel(
|
||||
RELATED_SORT_PREFIX.area_view,
|
||||
primary,
|
||||
viewPath
|
||||
),
|
||||
group: "related",
|
||||
});
|
||||
|
||||
// Area settings
|
||||
const path = `/config/areas/area/${areaId}`;
|
||||
relatedItems.push({
|
||||
id: path,
|
||||
primary,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.navigation-picker.area_settings",
|
||||
{ area: primary }
|
||||
),
|
||||
secondary: path,
|
||||
icon: area?.icon ?? undefined,
|
||||
icon_path: area?.icon ? undefined : mdiTextureBox,
|
||||
|
||||
@@ -89,7 +89,7 @@ export class HaPasswordField extends LitElement {
|
||||
@property({ type: Boolean }) readOnly = false;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ attribute: false, type: String }) autocapitalize = "";
|
||||
@property({ attribute: false }) autocapitalize = "";
|
||||
|
||||
@state() private _unmaskedPassword = false;
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isTouch } from "../util/is_touch";
|
||||
import "./chips/ha-chip-set";
|
||||
import "./chips/ha-filter-chip";
|
||||
import "./ha-combo-box-item";
|
||||
@@ -54,6 +55,7 @@ export interface PickerComboBoxItem {
|
||||
sorting_label?: string;
|
||||
icon_path?: string;
|
||||
icon?: string;
|
||||
isRelated?: boolean;
|
||||
}
|
||||
|
||||
export interface PickerComboBoxIndexSelectedDetail {
|
||||
@@ -118,7 +120,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
section?: string
|
||||
) => PickerComboBoxItem[] | undefined;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
@property({ attribute: false })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
|
||||
@property({ attribute: false })
|
||||
@@ -284,6 +286,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
section === "separator"
|
||||
? html`<div class="separator"></div>`
|
||||
: html`<ha-filter-chip
|
||||
@mousedown=${isTouch ? undefined : this._preventBlur}
|
||||
@click=${this._toggleSection}
|
||||
.section-id=${section.id}
|
||||
.selected=${this._selectedSection === section.id}
|
||||
@@ -506,6 +509,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this._valuePinned = true;
|
||||
};
|
||||
|
||||
private _preventBlur(ev: Event) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private _toggleSection(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._resetSelectedItem();
|
||||
|
||||
@@ -22,7 +22,7 @@ export class HaQrCode extends LitElement {
|
||||
@property({ type: Number })
|
||||
public margin = 4;
|
||||
|
||||
@property({ attribute: false, type: Number })
|
||||
@property({ attribute: false })
|
||||
public maskPattern?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
|
||||
@property({ attribute: "center-image" }) public centerImage?: string;
|
||||
|
||||
@@ -14,8 +14,8 @@ import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import "./ha-button";
|
||||
import "./ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "./ha-dropdown-item";
|
||||
import "./ha-spinner";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
@@ -259,7 +259,7 @@ class HaQrScanner extends LitElement {
|
||||
this._qrCodeScanned(this._manualInput!.value);
|
||||
}
|
||||
|
||||
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
|
||||
const cameraId = ev.detail?.item?.value;
|
||||
if (cameraId) {
|
||||
this._selectedCamera = cameraId;
|
||||
|
||||
+192
-158
@@ -1,187 +1,221 @@
|
||||
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";
|
||||
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;
|
||||
}>;
|
||||
|
||||
@customElement("ha-select")
|
||||
export class HaSelect extends SelectBase {
|
||||
// @ts-ignore
|
||||
@property({ type: Boolean }) public icon = false;
|
||||
export class HaSelect extends LitElement {
|
||||
@property({ type: Boolean }) public clearable = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable = false;
|
||||
@property({ attribute: false }) public options?: HaSelectOption[] | string[];
|
||||
|
||||
@property({ attribute: "inline-arrow", type: Boolean })
|
||||
public inlineArrow = false;
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public options;
|
||||
@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;
|
||||
}
|
||||
);
|
||||
|
||||
protected override render() {
|
||||
if (this.disabled) {
|
||||
return this._renderField();
|
||||
}
|
||||
|
||||
return html`
|
||||
${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}
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
||||
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 _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 renderLeadingIcon() {
|
||||
if (!this.icon) {
|
||||
return nothing;
|
||||
private _handleSelect(ev: CustomEvent<{ item: { value: string } }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.item.value;
|
||||
if (value === this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
fireEvent(this, "selected", { value });
|
||||
}
|
||||
|
||||
private _clearValue(): void {
|
||||
if (this.disabled || !this.value) {
|
||||
return;
|
||||
}
|
||||
this.valueSetDirectly = true;
|
||||
this.select(-1);
|
||||
this.mdcFoundation.handleChange();
|
||||
|
||||
fireEvent(this, "selected", { value: undefined });
|
||||
}
|
||||
|
||||
private _translationsUpdated = debounce(async () => {
|
||||
await nextRender();
|
||||
this.layoutOptions();
|
||||
}, 500);
|
||||
private _handleShow() {
|
||||
this.style.setProperty(
|
||||
"--select-menu-width",
|
||||
`${this._triggerField.offsetWidth}px`
|
||||
);
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
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);
|
||||
}
|
||||
`;
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-select": HaSelect;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
selected: { value: string | undefined };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,16 @@ 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";
|
||||
@@ -231,24 +230,15 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.label ?? ""}
|
||||
.value=${this.value ?? ""}
|
||||
.value=${(this.value as string) ?? ""}
|
||||
.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>
|
||||
`;
|
||||
}
|
||||
@@ -295,7 +285,7 @@ export class HaSelectSelector extends LitElement {
|
||||
private _valueChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (ev.detail?.index === -1 && this.value !== undefined) {
|
||||
if (ev.detail?.value === undefined && this.value !== undefined) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: undefined,
|
||||
});
|
||||
@@ -385,7 +375,7 @@ export class HaSelectSelector extends LitElement {
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
ha-list-item[disabled] {
|
||||
ha-dropdown-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
ha-chip-set {
|
||||
|
||||
@@ -224,6 +224,7 @@ ${typeof this._templateResult.result === "object"
|
||||
margin-bottom: 0;
|
||||
direction: ltr;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ export class HaSettingsRow extends LitElement {
|
||||
white-space: normal;
|
||||
}
|
||||
.prefix-wrap {
|
||||
flex: 1;
|
||||
display: var(--settings-row-prefix-display);
|
||||
}
|
||||
:host([narrow]) .prefix-wrap {
|
||||
|
||||
@@ -492,19 +492,22 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
|
||||
${!this.alwaysExpand &&
|
||||
(this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`<span class="badge" slot="start"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>`
|
||||
${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.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`<span class="badge" slot="end"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>`
|
||||
${this._updatesCount > 0 || this._issuesCount > 0
|
||||
? html`
|
||||
<span class="badge" slot="end"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
@@ -524,13 +527,15 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
type="button"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiBell}></ha-svg-icon>
|
||||
${!this.alwaysExpand && notificationCount > 0
|
||||
? html`<span class="badge" slot="start">${notificationCount}</span>`
|
||||
${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
|
||||
>
|
||||
${this.alwaysExpand && notificationCount > 0
|
||||
${notificationCount > 0
|
||||
? html`<span class="badge" slot="end">${notificationCount}</span>`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
@@ -739,6 +744,8 @@ 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)
|
||||
);
|
||||
@@ -747,6 +754,7 @@ 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));
|
||||
@@ -761,15 +769,22 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
margin-left: 3px;
|
||||
margin-inline-start: 3px;
|
||||
margin-inline-end: initial;
|
||||
width: 100%;
|
||||
display: none;
|
||||
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;
|
||||
}
|
||||
:host([narrow]) .title {
|
||||
margin: 0;
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
:host([expanded]) .title {
|
||||
display: initial;
|
||||
max-width: 100%;
|
||||
opacity: 1;
|
||||
transition-delay: 0ms, 80ms;
|
||||
}
|
||||
|
||||
.panels-list {
|
||||
@@ -827,6 +842,7 @@ 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;
|
||||
@@ -867,11 +883,22 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
}
|
||||
|
||||
ha-md-list-item .item-text {
|
||||
display: none;
|
||||
display: block;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
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;
|
||||
@@ -889,6 +916,9 @@ 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 {
|
||||
@@ -900,6 +930,12 @@ 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);
|
||||
@@ -938,6 +974,15 @@ 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;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,16 +2,14 @@ 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 { computeDomain } from "../common/entity/compute_domain";
|
||||
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 "./ha-list-item";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@@ -61,6 +59,30 @@ 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 ||
|
||||
@@ -69,33 +91,8 @@ export class HaSTTPicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.options=${options}
|
||||
>
|
||||
${!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>
|
||||
`;
|
||||
}
|
||||
@@ -144,17 +141,17 @@ export class HaSTTPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
this.value = value === NONE ? undefined : value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._engines!.find((engine) => engine.engine_id === this.value)
|
||||
|
||||
@@ -53,7 +53,7 @@ import {
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
} from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-generic-picker";
|
||||
@@ -76,7 +76,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public compact = false;
|
||||
|
||||
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||
@property({ attribute: false }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only targets with entities from specific domains.
|
||||
@@ -403,7 +403,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _targetPicked(ev: CustomEvent<{ value: string }>) {
|
||||
private _targetPicked(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (value.startsWith(CREATE_ID)) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, nothing, LitElement } from "lit";
|
||||
import { css, html, 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";
|
||||
|
||||
@@ -25,6 +24,26 @@ 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 ||
|
||||
@@ -33,31 +52,8 @@ export class HaThemePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@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>
|
||||
.options=${options}
|
||||
></ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -67,11 +63,11 @@ export class HaThemePicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev): void {
|
||||
if (!this.hass || ev.target.value === "") {
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
if (!this.hass || ev.detail.value === "") {
|
||||
return;
|
||||
}
|
||||
this.value = ev.target.value === "remove" ? undefined : ev.target.value;
|
||||
this.value = ev.detail.value === "remove" ? undefined : ev.detail.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +36,19 @@ 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);
|
||||
|
||||
@@ -2,16 +2,14 @@ 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 { computeDomain } from "../common/entity/compute_domain";
|
||||
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 "./ha-list-item";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@@ -61,6 +59,30 @@ 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 ||
|
||||
@@ -69,33 +91,8 @@ export class HaTTSPicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.options=${options}
|
||||
>
|
||||
${!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>
|
||||
`;
|
||||
}
|
||||
@@ -144,17 +141,17 @@ export class HaTTSPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
this.value = value === NONE ? undefined : value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._engines!.find((engine) => engine.engine_id === this.value)
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
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 { TTSVoice } from "../data/tts";
|
||||
import { listTTSVoices } from "../data/tts";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@@ -31,14 +29,25 @@ 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 ||
|
||||
@@ -47,21 +56,8 @@ export class HaTTSVoicePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.options=${options}
|
||||
>
|
||||
${!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>
|
||||
`;
|
||||
}
|
||||
@@ -102,34 +98,25 @@ 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): void {
|
||||
const target = ev.target as HaSelect;
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
!this.hass ||
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
this.value = value === NONE ? undefined : value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export class TopAppBarBaseBase extends BaseElement {
|
||||
|
||||
@query(".pane .ha-scrollbar") private _paneElement?: HTMLElement;
|
||||
|
||||
@property({ attribute: false, type: Object })
|
||||
@property({ attribute: false })
|
||||
get scrollTarget() {
|
||||
return this._scrollTarget || window;
|
||||
}
|
||||
@@ -288,10 +288,19 @@ 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,11 +1,9 @@
|
||||
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";
|
||||
@@ -19,8 +17,9 @@ import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-alert";
|
||||
import "../ha-button";
|
||||
import "../ha-dialog";
|
||||
import "../ha-dialog-footer";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-wa-dialog";
|
||||
import "./ha-media-player-toggle";
|
||||
import type { JoinMediaPlayersDialogParams } from "./show-join-media-players-dialog";
|
||||
|
||||
@@ -38,8 +37,11 @@ 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
|
||||
@@ -54,6 +56,11 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._entityId = undefined;
|
||||
this._selectedEntities = [];
|
||||
this._groupMembers = [];
|
||||
@@ -68,23 +75,18 @@ 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-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
flexContent
|
||||
.heading=${name}
|
||||
@closed=${this.closeDialog}
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
flexcontent
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-dialog-header show-border slot="heading">
|
||||
<ha-dialog-header show-border slot="header">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
data-dialog="close"
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button>
|
||||
<span slot="title"
|
||||
@@ -118,21 +120,23 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
></ha-media-player-toggle>`
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -217,6 +221,7 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: var(--ha-space-6);
|
||||
row-gap: var(--ha-space-4);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
import "../ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../ha-dropdown-item";
|
||||
import "../ha-icon-button-arrow-prev";
|
||||
import "../ha-wa-dialog";
|
||||
import "./ha-media-manage-button";
|
||||
@@ -177,7 +177,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
this.classList.add("opened");
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
private async _handleMenuAction(ev: HaDropdownSelectEvent) {
|
||||
const action = ev.detail?.item?.value;
|
||||
switch (action) {
|
||||
case "auto":
|
||||
|
||||
@@ -15,7 +15,7 @@ export class HaTileContainer extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
public vertical = false;
|
||||
|
||||
@property({ type: Boolean, attribute: false })
|
||||
@property({ attribute: false })
|
||||
public interactive = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
|
||||
@@ -24,6 +24,8 @@ 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",
|
||||
@@ -166,7 +168,9 @@ export class HaTracePathDetails extends LitElement {
|
||||
: selectedType === "trigger"
|
||||
? html`<h2>
|
||||
${describeTrigger(
|
||||
currentDetail,
|
||||
migrateAutomationTrigger({
|
||||
...currentDetail,
|
||||
}) as Trigger,
|
||||
this.hass,
|
||||
this._entityReg
|
||||
)}
|
||||
|
||||
@@ -53,6 +53,7 @@ export type ClimateEntity = HassEntityBase & {
|
||||
current_humidity?: number;
|
||||
target_humidity_low?: number;
|
||||
target_humidity_high?: number;
|
||||
target_humidity_step?: number;
|
||||
min_humidity?: number;
|
||||
max_humidity?: number;
|
||||
fan_mode?: string;
|
||||
|
||||
@@ -35,7 +35,7 @@ export const deserializeFilters = (value: DataTableFilters) => {
|
||||
|
||||
// returns true when this filter has *selected* options and the filter's name
|
||||
// equals the given filterName
|
||||
export const isUsedFilter = (
|
||||
export const isFilterUsed = (
|
||||
key: string,
|
||||
filter: DataTableFilter,
|
||||
filterName: string
|
||||
@@ -49,7 +49,7 @@ export const isUsedFilter = (
|
||||
// which has resulted in a list of items that match these selected opions
|
||||
// (this list can be empty),
|
||||
// and the filter's name equals (one of) the given filterName(s)
|
||||
export const isUsedRelatedItemsFilter = (
|
||||
export const isRelatedItemsFilterUsed = (
|
||||
key: string,
|
||||
filter: DataTableFilter,
|
||||
filterName: string | string[]
|
||||
|
||||
@@ -511,6 +511,15 @@ const getEnergyData = async (
|
||||
"mean",
|
||||
])
|
||||
: {};
|
||||
// If power stats 5 minute data is selected, then also fetch hourly data which
|
||||
// will be used to back-fill any missing data points in the 5 minute data when
|
||||
// the requested range is beyond the limit of short term statistics.
|
||||
const _powerStatsHour: Statistics | Promise<Statistics> =
|
||||
powerStatIds.length && finePeriod === "5minute"
|
||||
? fetchStatistics(hass!, start, end, powerStatIds, "hour", powerUnits, [
|
||||
"mean",
|
||||
])
|
||||
: {};
|
||||
|
||||
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
|
||||
? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [
|
||||
@@ -619,6 +628,7 @@ const getEnergyData = async (
|
||||
const [
|
||||
energyStats,
|
||||
powerStats,
|
||||
powerStatsHour,
|
||||
waterStats,
|
||||
energyStatsCompare,
|
||||
waterStatsCompare,
|
||||
@@ -627,12 +637,44 @@ const getEnergyData = async (
|
||||
] = await Promise.all([
|
||||
_energyStats,
|
||||
_powerStats,
|
||||
_powerStatsHour,
|
||||
_waterStats,
|
||||
_energyStatsCompare,
|
||||
_waterStatsCompare,
|
||||
_fossilEnergyConsumption,
|
||||
_fossilEnergyConsumptionCompare,
|
||||
]);
|
||||
|
||||
// Back-fill any missing power statistics from hourly data if present
|
||||
if (Object.keys(powerStatsHour).length) {
|
||||
powerStatIds.forEach((powerId) => {
|
||||
if (powerId in powerStatsHour) {
|
||||
// If we have extra hourly power statistics for an ID, we may need to
|
||||
// insert data into statistics
|
||||
if (powerId in powerStats && powerStats[powerId].length) {
|
||||
// We have 5-minute data. Only insert hourly values for time periods
|
||||
// before the first 5-minute value.
|
||||
const powerStatFirst = powerStats[powerId][0];
|
||||
const powerStatHour = powerStatsHour[powerId];
|
||||
let powerStatHourLast = 0;
|
||||
for (const powerStat of powerStatHour) {
|
||||
if (powerStat.end > powerStatFirst.start) {
|
||||
break;
|
||||
}
|
||||
powerStatHourLast++;
|
||||
}
|
||||
powerStats[powerId] = [
|
||||
...powerStatHour.slice(0, powerStatHourLast),
|
||||
...powerStats[powerId],
|
||||
];
|
||||
} else {
|
||||
// There was no 5-minute data, so simply insert full hourly data
|
||||
powerStats[powerId] = powerStatsHour[powerId];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const stats = { ...energyStats, ...waterStats, ...powerStats };
|
||||
if (compare) {
|
||||
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { LovelaceRawConfig } from "./config/types";
|
||||
|
||||
export type LovelaceDashboard =
|
||||
| LovelaceYamlDashboard
|
||||
@@ -34,6 +35,11 @@ export interface LovelaceDashboardCreateParams extends LovelaceDashboardMutableP
|
||||
mode: "storage";
|
||||
}
|
||||
|
||||
export interface LovelaceAIDashboardResult {
|
||||
conversation_id: string;
|
||||
config: LovelaceRawConfig;
|
||||
}
|
||||
|
||||
export const fetchDashboards = (
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceDashboard[]> =>
|
||||
@@ -66,3 +72,9 @@ 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,7 +13,6 @@ 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";
|
||||
@@ -296,17 +295,11 @@ class EntityPreviewRow extends LitElement {
|
||||
.label=${computeStateName(stateObj)}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
naturalMenuWidth
|
||||
.options=${stateObj.attributes.options?.map((option) => ({
|
||||
value: option,
|
||||
label: this.hass!.formatEntityState(stateObj, option),
|
||||
})) || []}
|
||||
>
|
||||
${stateObj.attributes.options
|
||||
? stateObj.attributes.options.map(
|
||||
(option) => html`
|
||||
<ha-list-item .value=${option}>
|
||||
${this.hass!.formatEntityState(stateObj, option)}
|
||||
</ha-list-item>
|
||||
`
|
||||
)
|
||||
: ""}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -91,9 +91,11 @@ export const showConfigFlowDialog = (
|
||||
|
||||
renderShowFormStepFieldLabel(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.sections.${field.name}.name`,
|
||||
step.description_placeholders
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.sections.${field.name}.name`,
|
||||
step.description_placeholders
|
||||
) || field.name
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -95,9 +95,11 @@ export const showOptionsFlowDialog = (
|
||||
|
||||
renderShowFormStepFieldLabel(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.name`,
|
||||
step.description_placeholders
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.name`,
|
||||
step.description_placeholders
|
||||
) || field.name
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,9 +86,11 @@ export const showSubConfigFlowDialog = (
|
||||
|
||||
renderShowFormStepFieldLabel(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.name`,
|
||||
step.description_placeholders
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.sections.${field.name}.name`,
|
||||
step.description_placeholders
|
||||
) || field.name
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, 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-header";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button-toggle";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import {
|
||||
getMobileCloseToBottomAnimation,
|
||||
getMobileOpenFromBottomAnimation,
|
||||
} from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { LightColor, LightEntity } from "../../../../data/light";
|
||||
import {
|
||||
@@ -41,7 +36,7 @@ class DialogLightColorFavorite extends LitElement {
|
||||
|
||||
@state() private _modes: LightPickerMode[] = [];
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: LightColorFavoriteDialogParams
|
||||
@@ -50,10 +45,11 @@ class DialogLightColorFavorite extends LitElement {
|
||||
this._dialogParams = dialogParams;
|
||||
this._color = dialogParams.initialColor ?? this._computeCurrentColor();
|
||||
this._updateModes();
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._dialog?.close();
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _updateModes() {
|
||||
@@ -120,16 +116,8 @@ 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;
|
||||
@@ -138,7 +126,7 @@ class DialogLightColorFavorite extends LitElement {
|
||||
|
||||
private async _save() {
|
||||
if (!this._color) {
|
||||
this._cancel();
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
this._dialogParams?.submit?.(this._color);
|
||||
@@ -159,83 +147,76 @@ class DialogLightColorFavorite extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-md-dialog
|
||||
open
|
||||
@cancel=${this._cancel}
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
.headerTitle=${this._dialogParams?.title}
|
||||
@closed=${this._dialogClosed}
|
||||
aria-labelledby="dialog-light-color-favorite-title"
|
||||
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
|
||||
.getCloseAnimation=${getMobileCloseToBottomAnimation}
|
||||
>
|
||||
<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 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 slot="actions">
|
||||
<ha-button appearance="plain" @click=${this._cancelDialog}>
|
||||
<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"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button @click=${this._save} .disabled=${!this._color}
|
||||
>${this.hass.localize("ui.common.save")}</ha-button
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${!this._color}
|
||||
>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -243,23 +224,18 @@ class DialogLightColorFavorite extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-md-dialog {
|
||||
min-width: 420px; /* prevent width jumps when switching modes */
|
||||
max-height: min(
|
||||
ha-wa-dialog {
|
||||
--ha-dialog-width-md: 420px; /* prevent width jumps when switching modes */
|
||||
--ha-dialog-max-height: min(
|
||||
600px,
|
||||
100% - 48px
|
||||
); /* prevent scrolling on desktop */
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
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;
|
||||
ha-wa-dialog {
|
||||
--ha-dialog-width-md: 100vw;
|
||||
--ha-dialog-max-height: calc(100% - 100px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ export interface LightColorFavoriteDialogParams {
|
||||
title: string;
|
||||
initialColor?: LightColor;
|
||||
submit?: (color?: LightColor) => void;
|
||||
cancel?: () => void;
|
||||
}
|
||||
|
||||
export const loadLightColorFavoriteDialog = () =>
|
||||
@@ -18,7 +17,6 @@ export const showLightColorFavoriteDialog = (
|
||||
dialogParams: LightColorFavoriteDialogParams
|
||||
) =>
|
||||
new Promise<LightColor | null>((resolve) => {
|
||||
const origCancel = dialogParams.cancel;
|
||||
const origSubmit = dialogParams.submit;
|
||||
|
||||
fireEvent(element, "show-dialog", {
|
||||
@@ -26,12 +24,6 @@ export const showLightColorFavoriteDialog = (
|
||||
dialogImport: loadLightColorFavoriteDialog,
|
||||
dialogParams: {
|
||||
...dialogParams,
|
||||
cancel: () => {
|
||||
resolve(null);
|
||||
if (origCancel) {
|
||||
origCancel();
|
||||
}
|
||||
},
|
||||
submit: (color: LightColor) => {
|
||||
resolve(color);
|
||||
if (origSubmit) {
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { mdiClose, mdiPlay, mdiStop } from "@mdi/js";
|
||||
import { 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, query, state } from "lit/decorators";
|
||||
import { customElement, property, 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 "../../../../components/ha-dialog-header";
|
||||
import type { HaSelectSelectEvent } from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
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";
|
||||
@@ -28,23 +24,25 @@ 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._dialog?.close();
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._stateObj = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -65,50 +63,33 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
SirenEntityFeature.DURATION
|
||||
);
|
||||
return html`
|
||||
<ha-md-dialog
|
||||
open
|
||||
<ha-wa-dialog
|
||||
.open=${this._open}
|
||||
.hass=${this.hass}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.components.siren.advanced_controls"
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
aria-labelledby="dialog-light-color-favorite-title"
|
||||
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
|
||||
.getCloseAnimation=${getMobileCloseToBottomAnimation}
|
||||
>
|
||||
<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>
|
||||
<div class="options">
|
||||
${supportsTones
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this.hass.localize("ui.components.siren.tone")}
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleToneChange}
|
||||
@selected=${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}
|
||||
@@ -153,17 +134,21 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
</ha-control-button>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<ha-button @click=${this.closeDialog}>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleToneChange(ev) {
|
||||
this._tone = ev.target.value;
|
||||
private _handleToneChange(ev: HaSelectSelectEvent) {
|
||||
this._tone = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleVolumeChange(ev) {
|
||||
|
||||
@@ -8,9 +8,7 @@ import {
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-attribute-icon";
|
||||
import "../../../components/ha-control-select-menu";
|
||||
import "../../../components/ha-icon-button-group";
|
||||
import "../../../components/ha-icon-button-toggle";
|
||||
@@ -29,6 +27,7 @@ import "../../../state-control/climate/ha-state-control-climate-temperature";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/ha-more-info-control-select-container";
|
||||
import { moreInfoControlStyle } from "../components/more-info-control-style";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
type MainControl = "temperature" | "humidity";
|
||||
|
||||
@@ -169,182 +168,112 @@ class MoreInfoClimate extends LitElement {
|
||||
</div>
|
||||
<ha-more-info-control-select-container>
|
||||
<ha-control-select-menu
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.card.climate.mode")}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleOperationModeChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${climateHvacModeIcon(stateObj.state)}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
${stateObj.attributes.hvac_modes
|
||||
.options=${stateObj.attributes.hvac_modes
|
||||
.concat()
|
||||
.sort(compareClimateHvacModes)
|
||||
.map(
|
||||
(mode) => html`
|
||||
<ha-list-item .value=${mode} graphic="icon">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${climateHvacModeIcon(mode)}
|
||||
></ha-svg-icon>
|
||||
${this.hass.formatEntityState(stateObj, mode)}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
.map((mode) => ({
|
||||
value: mode,
|
||||
iconPath: climateHvacModeIcon(mode),
|
||||
label: this.hass.formatEntityState(stateObj, mode),
|
||||
}))}
|
||||
@wa-select=${this._handleOperationModeChanged}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${climateHvacModeIcon(stateObj.state)}
|
||||
></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
${supportPresetMode && stateObj.attributes.preset_modes
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
stateObj,
|
||||
"preset_mode"
|
||||
)}
|
||||
.value=${stateObj.attributes.preset_mode}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handlePresetmodeChanged}
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handlePresetmodeChanged}
|
||||
.options=${stateObj.attributes.preset_modes.map((mode) => ({
|
||||
value: mode,
|
||||
label: this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"preset_mode",
|
||||
mode
|
||||
),
|
||||
attributeIcon: {
|
||||
stateObj,
|
||||
attribute: "preset_mode",
|
||||
attributeValue: mode,
|
||||
},
|
||||
}))}
|
||||
>
|
||||
${stateObj.attributes.preset_mode
|
||||
? html`
|
||||
<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
attribute="preset_mode"
|
||||
.attributeValue=${stateObj.attributes.preset_mode}
|
||||
></ha-attribute-icon>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiTuneVariant}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
${stateObj.attributes.preset_modes!.map(
|
||||
(mode) => html`
|
||||
<ha-list-item .value=${mode} graphic="icon">
|
||||
<ha-attribute-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
attribute="preset_mode"
|
||||
.attributeValue=${mode}
|
||||
></ha-attribute-icon>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"preset_mode",
|
||||
mode
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
: nothing}
|
||||
${supportFanMode && stateObj.attributes.fan_modes
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
stateObj,
|
||||
"fan_mode"
|
||||
)}
|
||||
.value=${stateObj.attributes.fan_mode}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleFanModeChanged}
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleFanModeChanged}
|
||||
.options=${stateObj.attributes.fan_modes.map((mode) => ({
|
||||
value: mode,
|
||||
label: this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"fan_mode",
|
||||
mode
|
||||
),
|
||||
attributeIcon: {
|
||||
stateObj,
|
||||
attribute: "fan_mode",
|
||||
attributeValue: mode,
|
||||
},
|
||||
}))}
|
||||
>
|
||||
${stateObj.attributes.fan_mode
|
||||
? html`
|
||||
<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
attribute="fan_mode"
|
||||
.attributeValue=${stateObj.attributes.fan_mode}
|
||||
></ha-attribute-icon>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
|
||||
`}
|
||||
${stateObj.attributes.fan_modes!.map(
|
||||
(mode) => html`
|
||||
<ha-list-item .value=${mode} graphic="icon">
|
||||
<ha-attribute-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
attribute="fan_mode"
|
||||
.attributeValue=${mode}
|
||||
></ha-attribute-icon>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"fan_mode",
|
||||
mode
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
: nothing}
|
||||
${supportSwingMode && stateObj.attributes.swing_modes
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
stateObj,
|
||||
"swing_mode"
|
||||
)}
|
||||
.value=${stateObj.attributes.swing_mode}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleSwingmodeChanged}
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleSwingmodeChanged}
|
||||
.options=${stateObj.attributes.swing_modes.map((mode) => ({
|
||||
value: mode,
|
||||
label: this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"swing_mode",
|
||||
mode
|
||||
),
|
||||
attributeIcon: {
|
||||
stateObj,
|
||||
attribute: "swing_mode",
|
||||
attributeValue: mode,
|
||||
},
|
||||
}))}
|
||||
>
|
||||
${stateObj.attributes.swing_mode
|
||||
? html`
|
||||
<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
attribute="swing_mode"
|
||||
.attributeValue=${stateObj.attributes.swing_mode}
|
||||
></ha-attribute-icon>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiArrowOscillating}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
${stateObj.attributes.swing_modes!.map(
|
||||
(mode) => html`
|
||||
<ha-list-item .value=${mode} graphic="icon">
|
||||
<ha-attribute-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
attribute="swing_mode"
|
||||
.attributeValue=${mode}
|
||||
></ha-attribute-icon>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"swing_mode",
|
||||
mode
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiArrowOscillating}
|
||||
></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
: nothing}
|
||||
@@ -352,52 +281,34 @@ class MoreInfoClimate extends LitElement {
|
||||
stateObj.attributes.swing_horizontal_modes
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
stateObj,
|
||||
"swing_horizontal_mode"
|
||||
)}
|
||||
.value=${stateObj.attributes.swing_horizontal_mode}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleSwingHorizontalmodeChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${stateObj.attributes.swing_horizontal_mode
|
||||
? html`
|
||||
<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
attribute="swing_horizontal_mode"
|
||||
.attributeValue=${stateObj.attributes
|
||||
.swing_horizontal_mode}
|
||||
></ha-attribute-icon>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiArrowOscillating}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
${stateObj.attributes.swing_horizontal_modes!.map(
|
||||
(mode) => html`
|
||||
<ha-list-item .value=${mode} graphic="icon">
|
||||
<ha-attribute-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
attribute="swing_horizontal_mode"
|
||||
.attributeValue=${mode}
|
||||
></ha-attribute-icon>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"swing_horizontal_mode",
|
||||
mode
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
@wa-select=${this._handleSwingHorizontalmodeChanged}
|
||||
.options=${stateObj.attributes.swing_horizontal_modes.map(
|
||||
(mode) => ({
|
||||
value: mode,
|
||||
label: this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"swing_horizontal_mode",
|
||||
mode
|
||||
),
|
||||
attributeIcon: {
|
||||
stateObj,
|
||||
attribute: "swing_horizontal_mode",
|
||||
attributeValue: mode,
|
||||
},
|
||||
})
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiArrowOscillating}
|
||||
></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
: nothing}
|
||||
@@ -410,8 +321,8 @@ class MoreInfoClimate extends LitElement {
|
||||
this._mainControl = ev.currentTarget.control;
|
||||
}
|
||||
|
||||
private _handleFanModeChanged(ev) {
|
||||
const newVal = ev.target.value;
|
||||
private _handleFanModeChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.attributes.fan_mode,
|
||||
newVal,
|
||||
@@ -420,15 +331,15 @@ class MoreInfoClimate extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _handleOperationModeChanged(ev) {
|
||||
const newVal = ev.target.value;
|
||||
private _handleOperationModeChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
this._callServiceHelper(this.stateObj!.state, newVal, "set_hvac_mode", {
|
||||
hvac_mode: newVal,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSwingmodeChanged(ev) {
|
||||
const newVal = ev.target.value;
|
||||
private _handleSwingmodeChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.attributes.swing_mode,
|
||||
newVal,
|
||||
@@ -437,8 +348,8 @@ class MoreInfoClimate extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _handleSwingHorizontalmodeChanged(ev) {
|
||||
const newVal = ev.target.value;
|
||||
private _handleSwingHorizontalmodeChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.attributes.swing_horizontal_mode,
|
||||
newVal,
|
||||
@@ -447,8 +358,8 @@ class MoreInfoClimate extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _handlePresetmodeChanged(ev) {
|
||||
const newVal = ev.target.value || null;
|
||||
private _handlePresetmodeChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value || null;
|
||||
if (newVal) {
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.attributes.preset_mode,
|
||||
|
||||
@@ -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 } from "../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
|
||||
@customElement("more-info-date")
|
||||
class MoreInfoDate extends LitElement {
|
||||
@@ -31,7 +31,7 @@ class MoreInfoDate extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
private _dateChanged(ev: ValueChangedEvent<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 } from "../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
|
||||
@customElement("more-info-datetime")
|
||||
class MoreInfoDatetime extends LitElement {
|
||||
@@ -45,7 +45,7 @@ class MoreInfoDatetime extends LitElement {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
private _timeChanged(ev: ValueChangedEvent<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: CustomEvent<{ value: string }>): void {
|
||||
private _dateChanged(ev: ValueChangedEvent<string>): void {
|
||||
if (ev.detail.value) {
|
||||
const dateObj = new Date(this.stateObj!.state);
|
||||
const newDate = ev.detail.value.split("-").map(Number);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-attribute-icon";
|
||||
@@ -31,6 +30,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
import "../components/ha-more-info-control-select-container";
|
||||
import "../components/ha-more-info-state-header";
|
||||
import { moreInfoControlStyle } from "../components/more-info-control-style";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
@customElement("more-info-fan")
|
||||
class MoreInfoFan extends LitElement {
|
||||
@@ -48,8 +48,8 @@ class MoreInfoFan extends LitElement {
|
||||
});
|
||||
};
|
||||
|
||||
private _handleDirection(ev) {
|
||||
const newVal = ev.target.value;
|
||||
private _handleDirection(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
const oldVal = this.stateObj?.attributes.direction;
|
||||
|
||||
if (!newVal || oldVal === newVal) return;
|
||||
@@ -60,8 +60,8 @@ class MoreInfoFan extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handlePresetMode(ev) {
|
||||
const newVal = ev.target.value;
|
||||
private _handlePresetMode(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
const oldVal = this._presetMode;
|
||||
|
||||
if (!newVal || oldVal === newVal) return;
|
||||
@@ -73,8 +73,8 @@ class MoreInfoFan extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleOscillating(ev) {
|
||||
const newVal = ev.target.value === "true";
|
||||
private _handleOscillating(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value === "true";
|
||||
const oldVal = this.stateObj?.attributes.oscillating;
|
||||
|
||||
if (oldVal === newVal) return;
|
||||
@@ -176,65 +176,64 @@ class MoreInfoFan extends LitElement {
|
||||
${supportsPresetMode && this.stateObj.attributes.preset_modes
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"preset_mode"
|
||||
)}
|
||||
.value=${this.stateObj.attributes.preset_mode}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handlePresetMode}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${this.stateObj.attributes.preset_mode
|
||||
? html`<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="preset_mode"
|
||||
.attributeValue=${this.stateObj.attributes.preset_mode}
|
||||
></ha-attribute-icon>`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiTuneVariant}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
${this.stateObj.attributes.preset_modes?.map(
|
||||
(mode) => html`
|
||||
<ha-list-item .value=${mode} graphic="icon">
|
||||
<ha-attribute-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="preset_mode"
|
||||
.attributeValue=${mode}
|
||||
></ha-attribute-icon>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
"preset_mode",
|
||||
mode
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
@wa-select=${this._handlePresetMode}
|
||||
.options=${this.stateObj.attributes.preset_modes.map(
|
||||
(mode) => ({
|
||||
value: mode,
|
||||
label: this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
"preset_mode",
|
||||
mode
|
||||
),
|
||||
attributeIcon: this.stateObj
|
||||
? {
|
||||
stateObj: this.stateObj,
|
||||
attribute: "preset_mode",
|
||||
attributeValue: mode,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
: nothing}
|
||||
${supportsDirection
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"direction"
|
||||
)}
|
||||
.value=${this.stateObj.attributes.direction}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleDirection}
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleDirection}
|
||||
.options=${["forward", "reverse"].map((direction) => ({
|
||||
value: direction,
|
||||
label: this.stateObj
|
||||
? this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"direction",
|
||||
direction
|
||||
)
|
||||
: direction,
|
||||
attributeIcon: this.stateObj
|
||||
? {
|
||||
stateObj: this.stateObj,
|
||||
attribute: "direction",
|
||||
attributeValue: direction,
|
||||
}
|
||||
: undefined,
|
||||
}))}
|
||||
>
|
||||
<ha-attribute-icon
|
||||
slot="icon"
|
||||
@@ -243,40 +242,13 @@ class MoreInfoFan extends LitElement {
|
||||
attribute="direction"
|
||||
.attributeValue=${this.stateObj.attributes.direction}
|
||||
></ha-attribute-icon>
|
||||
<ha-list-item value="forward" graphic="icon">
|
||||
<ha-attribute-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="direction"
|
||||
attributeValue="forward"
|
||||
></ha-attribute-icon>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"direction",
|
||||
"forward"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="reverse" graphic="icon">
|
||||
<ha-attribute-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="direction"
|
||||
attributeValue="reverse"
|
||||
></ha-attribute-icon>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"direction",
|
||||
"reverse"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
: nothing}
|
||||
${supportsOscillate
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"oscillating"
|
||||
@@ -285,37 +257,26 @@ class MoreInfoFan extends LitElement {
|
||||
? "true"
|
||||
: "false"}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleOscillating}
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleOscillating}
|
||||
.options=${["true", "false"].map((val) => ({
|
||||
value: val,
|
||||
iconPath:
|
||||
val === "true"
|
||||
? mdiArrowOscillating
|
||||
: mdiArrowOscillatingOff,
|
||||
label: this.stateObj
|
||||
? this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"oscillating",
|
||||
val === "true"
|
||||
)
|
||||
: val,
|
||||
}))}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiArrowOscillatingOff}
|
||||
></ha-svg-icon>
|
||||
<ha-list-item value="true" graphic="icon">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiArrowOscillating}
|
||||
></ha-svg-icon>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"oscillating",
|
||||
true
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="false" graphic="icon">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiArrowOscillatingOff}
|
||||
></ha-svg-icon>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"oscillating",
|
||||
false
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { mdiPower, mdiTuneVariant } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-attribute-icon";
|
||||
import "../../../components/ha-control-select-menu";
|
||||
@@ -14,6 +13,7 @@ import "../../../state-control/humidifier/ha-state-control-humidifier-humidity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/ha-more-info-control-select-container";
|
||||
import { moreInfoControlStyle } from "../components/more-info-control-style";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
@customElement("more-info-humidifier")
|
||||
class MoreInfoHumidifier extends LitElement {
|
||||
@@ -74,68 +74,48 @@ class MoreInfoHumidifier extends LitElement {
|
||||
|
||||
<ha-more-info-control-select-container>
|
||||
<ha-control-select-menu
|
||||
.hass=${hass}
|
||||
.label=${this.hass.localize("ui.card.humidifier.state")}
|
||||
.value=${this.stateObj.state}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleStateChanged}
|
||||
@closed=${stopPropagation}
|
||||
@wa-select=${this._handleStateChanged}
|
||||
.options=${["off", "on"].map((fanState) => ({
|
||||
value: fanState,
|
||||
label: this.stateObj
|
||||
? this.hass.formatEntityState(this.stateObj, fanState)
|
||||
: fanState,
|
||||
}))}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPower}></ha-svg-icon>
|
||||
<ha-list-item value="off">
|
||||
${this.hass.formatEntityState(this.stateObj, "off")}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="on">
|
||||
${this.hass.formatEntityState(this.stateObj, "on")}
|
||||
</ha-list-item>
|
||||
</ha-control-select-menu>
|
||||
|
||||
${supportModes
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.hass=${hass}
|
||||
.label=${hass.localize("ui.card.humidifier.mode")}
|
||||
.value=${stateObj.attributes.mode}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleModeChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${stateObj.attributes.mode
|
||||
? html`
|
||||
<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
attribute="mode"
|
||||
.attributeValue=${stateObj.attributes.mode}
|
||||
></ha-attribute-icon>
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiTuneVariant}
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
${stateObj.attributes.available_modes!.map(
|
||||
(mode) => html`
|
||||
<ha-list-item .value=${mode} graphic="icon">
|
||||
<ha-attribute-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
attribute="mode"
|
||||
.attributeValue=${mode}
|
||||
></ha-attribute-icon>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
stateObj!,
|
||||
@wa-select=${this._handleModeChanged}
|
||||
.options=${stateObj.attributes.available_modes?.map((mode) => ({
|
||||
value: mode,
|
||||
label: stateObj
|
||||
? this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"mode",
|
||||
mode
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
)
|
||||
: mode,
|
||||
attributeIcon: stateObj
|
||||
? {
|
||||
stateObj,
|
||||
attribute: "mode",
|
||||
attributeValue: mode,
|
||||
}
|
||||
: undefined,
|
||||
})) || []}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
: nothing}
|
||||
@@ -143,8 +123,8 @@ class MoreInfoHumidifier extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleStateChanged(ev) {
|
||||
const newVal = ev.target.value || null;
|
||||
private _handleStateChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value || null;
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.state,
|
||||
newVal,
|
||||
@@ -153,8 +133,8 @@ class MoreInfoHumidifier extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _handleModeChanged(ev) {
|
||||
const newVal = ev.target.value || null;
|
||||
private _handleModeChanged(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
this._mode = newVal;
|
||||
this._callServiceHelper(
|
||||
this.stateObj!.attributes.mode,
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
setInputDateTimeValue,
|
||||
stateToIsoDateString,
|
||||
} from "../../../data/input_datetime";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
|
||||
@customElement("more-info-input_datetime")
|
||||
class MoreInfoInputDatetime extends LitElement {
|
||||
@@ -55,7 +55,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
private _timeChanged(ev: ValueChangedEvent<string>): void {
|
||||
setInputDateTimeValue(
|
||||
this.hass!,
|
||||
this.stateObj!.entity_id,
|
||||
@@ -66,7 +66,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
private _dateChanged(ev: ValueChangedEvent<string>): void {
|
||||
setInputDateTimeValue(
|
||||
this.hass!,
|
||||
this.stateObj!.entity_id,
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-attribute-icon";
|
||||
import "../../../components/ha-control-select-menu";
|
||||
@@ -38,6 +37,7 @@ import "../components/lights/ha-more-info-light-favorite-colors";
|
||||
import "../components/lights/light-color-rgb-picker";
|
||||
import "../components/lights/light-color-temp-picker";
|
||||
import { moreInfoControlStyle } from "../components/more-info-control-style";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
type MainControl = "brightness" | "color_temp" | "color";
|
||||
|
||||
@@ -253,47 +253,35 @@ class MoreInfoLight extends LitElement {
|
||||
${supportsEffects && this.stateObj.attributes.effect_list
|
||||
? html`
|
||||
<ha-control-select-menu
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"effect"
|
||||
)}
|
||||
.value=${this.stateObj.attributes.effect}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@selected=${this._handleEffect}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${this.stateObj.attributes.effect
|
||||
? html`<ha-attribute-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="effect"
|
||||
.attributeValue=${this.stateObj.attributes.effect}
|
||||
></ha-attribute-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiCreation}
|
||||
></ha-svg-icon>`}
|
||||
${this.stateObj.attributes.effect_list?.map(
|
||||
(effect) => html`
|
||||
<ha-list-item .value=${effect} graphic="icon">
|
||||
<ha-attribute-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
attribute="effect"
|
||||
.attributeValue=${effect}
|
||||
></ha-attribute-icon>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
"effect",
|
||||
effect
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
@wa-select=${this._handleEffect}
|
||||
.options=${this.stateObj.attributes.effect_list.map(
|
||||
(effect) => ({
|
||||
value: effect,
|
||||
label: this.stateObj
|
||||
? this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"effect",
|
||||
effect
|
||||
)
|
||||
: effect,
|
||||
attributeIcon: this.stateObj
|
||||
? {
|
||||
stateObj: this.stateObj,
|
||||
attribute: "effect",
|
||||
attributeValue: effect,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiCreation}></ha-svg-icon>
|
||||
</ha-control-select-menu>
|
||||
`
|
||||
: nothing}
|
||||
@@ -317,8 +305,8 @@ class MoreInfoLight extends LitElement {
|
||||
});
|
||||
};
|
||||
|
||||
private _handleEffect(ev) {
|
||||
const newVal = ev.target.value;
|
||||
private _handleEffect(ev: HaDropdownSelectEvent) {
|
||||
const newVal = ev.detail.item.value;
|
||||
const oldVal = this._effect;
|
||||
|
||||
if (!newVal || oldVal === newVal) return;
|
||||
|
||||
@@ -25,8 +25,8 @@ import { VolumeSliderController } from "../../../common/util/volume-slider";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-marquee-text";
|
||||
@@ -202,12 +202,11 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-dropdown>
|
||||
return html`<ha-dropdown @wa-select=${this._handleSourceChange}>
|
||||
<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(
|
||||
@@ -743,7 +742,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSourceChange(e: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
private _handleSourceChange(e: HaDropdownSelectEvent) {
|
||||
const source = e.detail.item.value;
|
||||
if (!source || this.stateObj!.attributes.source === source) {
|
||||
return;
|
||||
@@ -755,7 +754,7 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSoundModeChange(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
private _handleSoundModeChange(ev: HaDropdownSelectEvent) {
|
||||
const soundMode = ev.detail.item.value;
|
||||
if (!soundMode || this.stateObj!.attributes.sound_mode === soundMode) {
|
||||
return;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user