mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-03 06:51:48 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 081a13b3d7 |
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/devcontainers/python:3.14
|
||||
FROM mcr.microsoft.com/devcontainers/python:1-3.14
|
||||
|
||||
ENV \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
|
||||
@@ -21,14 +21,6 @@
|
||||
-->
|
||||
|
||||
|
||||
## Screenshots
|
||||
<!--
|
||||
If your PR includes visual changes, please add screenshots or a short video
|
||||
showing the before and after. This helps reviewers understand the impact of
|
||||
your changes.
|
||||
Note: Remove this section if this PR has no visual changes.
|
||||
-->
|
||||
|
||||
## Type of change
|
||||
<!--
|
||||
What type of change does your PR introduce to the Home Assistant frontend?
|
||||
@@ -43,6 +35,16 @@
|
||||
- [ ] Breaking change (fix/feature causing existing functionality to break)
|
||||
- [ ] Code quality improvements to existing code or addition of tests
|
||||
|
||||
## Example configuration
|
||||
<!--
|
||||
Supplying a configuration snippet, makes it easier for a maintainer to test
|
||||
your PR.
|
||||
-->
|
||||
|
||||
```yaml
|
||||
|
||||
```
|
||||
|
||||
## Additional information
|
||||
<!--
|
||||
Details are important, and help maintainers processing your PR.
|
||||
@@ -52,8 +54,6 @@
|
||||
- This PR fixes or closes issue: fixes #
|
||||
- This PR is related to issue or discussion:
|
||||
- Link to documentation pull request:
|
||||
- Link to developer documentation pull request:
|
||||
- Link to backend pull request:
|
||||
|
||||
## Checklist
|
||||
<!--
|
||||
@@ -61,50 +61,18 @@
|
||||
creating the PR. If you're unsure about any of them, don't hesitate to ask.
|
||||
We're here to help! This is simply a reminder of what we are going to look
|
||||
for before merging your code.
|
||||
|
||||
AI tools are welcome, but contributors are responsible for *fully*
|
||||
understanding the code before submitting a PR.
|
||||
-->
|
||||
|
||||
- [ ] I understand the code I am submitting and can explain how it works.
|
||||
- [ ] The code change is tested and works locally.
|
||||
- [ ] There is no commented out code in this PR.
|
||||
- [ ] I have followed the [development checklist][dev-checklist]
|
||||
- [ ] I have followed the [perfect PR recommendations][perfect-pr]
|
||||
- [ ] Any generated code has been carefully reviewed for correctness and compliance with project standards.
|
||||
- [ ] Tests have been added to verify that the new code works.
|
||||
|
||||
If user exposed functionality or configuration variables are added/changed:
|
||||
|
||||
- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]
|
||||
|
||||
<!--
|
||||
This project is very active and we have a high turnover of pull requests.
|
||||
|
||||
Unfortunately, the number of incoming pull requests is higher than what our
|
||||
reviewers can review and merge so there is a long backlog of pull requests
|
||||
waiting for review. You can help here!
|
||||
|
||||
By reviewing another pull request, you will help raise the code quality of
|
||||
that pull request and the final review will be faster. This way the general
|
||||
pace of pull request reviews will go up and your wait time will go down.
|
||||
|
||||
When picking a pull request to review, try to choose one that hasn't yet
|
||||
been reviewed.
|
||||
|
||||
Thanks for helping out!
|
||||
-->
|
||||
|
||||
To help with the load of incoming pull requests:
|
||||
|
||||
- [ ] I have reviewed two other [open pull requests][prs] in this repository.
|
||||
|
||||
[prs]: https://github.com/home-assistant/frontend/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+sort%3Acreated-desc+review%3Anone+-status%3Afailure
|
||||
|
||||
<!--
|
||||
Thank you for contributing <3
|
||||
|
||||
Below, some useful links you could explore:
|
||||
-->
|
||||
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
|
||||
|
||||
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
||||
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr
|
||||
|
||||
@@ -251,6 +251,7 @@ For browser support, API details, and current specifications, refer to these aut
|
||||
**Available Dialog Types:**
|
||||
|
||||
- `ha-wa-dialog` - Preferred for new dialogs (Web Awesome based)
|
||||
- `ha-md-dialog` - Material Design 3 dialog component
|
||||
- `ha-dialog` - Legacy component (still widely used)
|
||||
|
||||
**Opening Dialogs (Fire Event Pattern - Recommended):**
|
||||
@@ -558,10 +559,6 @@ this.hass.localize("ui.panel.config.updates.update_available", {
|
||||
- Use HTTPS - All external resources must use HTTPS
|
||||
- CSP compliance - Ensure code works with Content Security Policy
|
||||
|
||||
### Pull Requests
|
||||
|
||||
When creating a pull request, you **must** use the PR template located at `.github/PULL_REQUEST_TEMPLATE.md`. Read the template file and use its full content as the PR body, filling in each section appropriately. Do not omit, reorder, or rewrite the template sections. Do not check the checklist items on behalf of the user — those are the user's responsibility to review and check. If the PR includes UI changes, remind the user to add screenshots or a short video to the PR after creating it.
|
||||
|
||||
### Text and Copy Guidelines
|
||||
|
||||
#### Terminology Standards
|
||||
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
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@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
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@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
let sidebarChangeCallback;
|
||||
let changeFunction;
|
||||
|
||||
export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("frontend/get_user_data", () => ({ value: null }));
|
||||
hass.mockWS("frontend/get_user_data", () => ({
|
||||
value: null,
|
||||
}));
|
||||
hass.mockWS("frontend/set_user_data", ({ key, value }) => {
|
||||
if (key === "sidebar") {
|
||||
sidebarChangeCallback?.({
|
||||
changeFunction?.({
|
||||
value: {
|
||||
panelOrder: value.panelOrder || [],
|
||||
hiddenPanels: value.hiddenPanels || [],
|
||||
@@ -14,11 +16,14 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
hass.mockWS("frontend/subscribe_user_data", (msg, _hass, onChange) => {
|
||||
if (msg.key === "sidebar") {
|
||||
sidebarChangeCallback = onChange;
|
||||
}
|
||||
onChange?.({ value: null });
|
||||
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
|
||||
changeFunction = onChange;
|
||||
onChange?.({
|
||||
value: {
|
||||
panelOrder: [],
|
||||
hiddenPanels: [],
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return () => {};
|
||||
});
|
||||
@@ -43,5 +48,4 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||
return () => {};
|
||||
});
|
||||
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
|
||||
hass.mockWS("frontend/get_themes", (_msg, currentHass) => currentHass.themes);
|
||||
};
|
||||
|
||||
@@ -29,7 +29,6 @@ export const mockLovelace = (
|
||||
|
||||
hass.mockWS("lovelace/config/save", () => Promise.resolve());
|
||||
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
|
||||
hass.mockWS("lovelace/dashboards/list", () => Promise.resolve([]));
|
||||
};
|
||||
|
||||
customElements.whenDefined("hui-root").then(() => {
|
||||
|
||||
@@ -10,9 +10,7 @@ As a community, we are proud of our logo. Follow these guidelines to ensure it a
|
||||
|
||||

|
||||
|
||||
<ha-alert alert-type="info">
|
||||
This logo is trademarked and the property of the Open Home Foundation. This means it is not available for commercial use without express written permission from the foundation. We regard commercial use as anything designed to market or promote a product, software or service that is for sale. Please contact <a href="mailto:partner@openhomefoundation.org">partner@openhomefoundation.org</a> for further information
|
||||
</ha-alert>
|
||||
Please note that this logo is not released under the CC license. All rights reserved.
|
||||
|
||||
# Design
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
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-design" rel="noopener noreferrer" target="_blank">Discord #designers channel</a>. If you can't see the channel, make sure you set the correct role in Channels & Roles.
|
||||
- Meet us at <a href="https://www.home-assistant.io/join-chat" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
|
||||
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
button-style
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
|
||||
+23
-22
@@ -29,7 +29,7 @@
|
||||
"@babel/runtime": "7.28.6",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.2",
|
||||
"@codemirror/commands": "6.10.1",
|
||||
"@codemirror/language": "6.12.1",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
@@ -37,22 +37,22 @@
|
||||
"@codemirror/view": "6.39.12",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.2.1",
|
||||
"@formatjs/intl-displaynames": "7.2.1",
|
||||
"@formatjs/intl-durationformat": "0.10.1",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.1",
|
||||
"@formatjs/intl-listformat": "8.2.1",
|
||||
"@formatjs/intl-locale": "5.2.1",
|
||||
"@formatjs/intl-numberformat": "9.2.2",
|
||||
"@formatjs/intl-pluralrules": "6.2.2",
|
||||
"@formatjs/intl-relativetimeformat": "12.2.2",
|
||||
"@formatjs/intl-datetimeformat": "7.2.0",
|
||||
"@formatjs/intl-displaynames": "7.2.0",
|
||||
"@formatjs/intl-durationformat": "0.10.0",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.0",
|
||||
"@formatjs/intl-listformat": "8.2.0",
|
||||
"@formatjs/intl-locale": "5.2.0",
|
||||
"@formatjs/intl-numberformat": "9.2.1",
|
||||
"@formatjs/intl-pluralrules": "6.2.1",
|
||||
"@formatjs/intl-relativetimeformat": "12.2.1",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.0",
|
||||
"@home-assistant/webawesome": "3.0.0-ha.2",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -71,6 +71,7 @@
|
||||
"@material/mwc-icon-button": "0.27.0",
|
||||
"@material/mwc-linear-progress": "0.27.0",
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"@material/mwc-menu": "0.27.0",
|
||||
"@material/mwc-radio": "0.27.0",
|
||||
"@material/mwc-select": "0.27.0",
|
||||
"@material/mwc-snackbar": "0.27.0",
|
||||
@@ -111,7 +112,7 @@
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.1.2",
|
||||
"intl-messageformat": "11.1.1",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -132,7 +133,7 @@
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"ua-parser-js": "2.0.9",
|
||||
"ua-parser-js": "2.0.8",
|
||||
"vue": "2.7.16",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
@@ -145,17 +146,17 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/core": "7.28.6",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.6",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@babel/plugin-transform-runtime": "7.28.5",
|
||||
"@babel/preset-env": "7.28.6",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.9",
|
||||
"@lokalise/node-api": "15.6.1",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.2",
|
||||
"@rspack/core": "1.7.5",
|
||||
"@rsdoctor/rspack-plugin": "1.5.1",
|
||||
"@rspack/core": "1.7.4",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@@ -191,14 +192,14 @@
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"glob": "13.0.1",
|
||||
"glob": "13.0.0",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "28.0.0",
|
||||
"jsdom": "27.4.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@@ -228,13 +229,13 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"globals": "17.3.0",
|
||||
"globals": "17.2.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
},
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"volta": {
|
||||
"node": "24.13.1"
|
||||
"node": "24.13.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,6 @@ if [[ -n "$DEVCONTAINER" ]]; then
|
||||
nvm install --reinstall-packages-from="$nodeCurrent" --default
|
||||
nvm uninstall "$nodeCurrent"
|
||||
fi
|
||||
|
||||
# install yarn if not already available
|
||||
if ! command -v yarn &> /dev/null; then
|
||||
npm install -g corepack
|
||||
yes | yarn
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v yarn &> /dev/null; then
|
||||
|
||||
@@ -194,6 +194,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
button-style
|
||||
native-name
|
||||
@value-changed=${this._languageChanged}
|
||||
inline-arrow
|
||||
></ha-language-picker>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
|
||||
@@ -116,6 +116,3 @@ export const UNIT_F = "°F";
|
||||
|
||||
/** Entity ID of the default view. */
|
||||
export const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
|
||||
|
||||
/** String to visually separate labels on UI */
|
||||
export const STRINGS_SEPARATOR_DOT = " · ";
|
||||
|
||||
@@ -3,14 +3,13 @@ import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
import type { HomeAssistant, ValuePart } from "../../types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { DURATION_UNITS, formatDuration } from "../datetime/format_duration";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
import {
|
||||
formatNumber,
|
||||
formatNumberToParts,
|
||||
getNumberFormatOptions,
|
||||
isNumericFromAttributes,
|
||||
} from "../number/format_number";
|
||||
@@ -52,36 +51,8 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
attributes: any,
|
||||
state: string
|
||||
): string => {
|
||||
const parts = computeStateToPartsFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
sensorNumericDeviceClasses,
|
||||
config,
|
||||
entity,
|
||||
entityId,
|
||||
attributes,
|
||||
state
|
||||
);
|
||||
return parts.map((part) => part.value).join("");
|
||||
};
|
||||
|
||||
const computeStateToPartsFromEntityAttributes = (
|
||||
localize: LocalizeFunc,
|
||||
locale: FrontendLocaleData,
|
||||
sensorNumericDeviceClasses: string[],
|
||||
config: HassConfig,
|
||||
entity: EntityRegistryDisplayEntry | undefined,
|
||||
entityId: string,
|
||||
attributes: any,
|
||||
state: string
|
||||
): ValuePart[] => {
|
||||
if (state === UNKNOWN || state === UNAVAILABLE) {
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: localize(`state.default.${state}`),
|
||||
},
|
||||
];
|
||||
return localize(`state.default.${state}`);
|
||||
}
|
||||
|
||||
const domain = computeDomain(entityId);
|
||||
@@ -102,27 +73,19 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
DURATION_UNITS.includes(attributes.unit_of_measurement)
|
||||
) {
|
||||
try {
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDuration(
|
||||
locale,
|
||||
state,
|
||||
attributes.unit_of_measurement,
|
||||
entity?.display_precision
|
||||
),
|
||||
},
|
||||
];
|
||||
return formatDuration(
|
||||
locale,
|
||||
state,
|
||||
attributes.unit_of_measurement,
|
||||
entity?.display_precision
|
||||
);
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
}
|
||||
|
||||
// state is monetary
|
||||
if (attributes.device_class === "monetary") {
|
||||
let parts: Record<string, string>[] = [];
|
||||
try {
|
||||
parts = formatNumberToParts(state, locale, {
|
||||
return formatNumber(state, locale, {
|
||||
style: "currency",
|
||||
currency: attributes.unit_of_measurement,
|
||||
minimumFractionDigits: 2,
|
||||
@@ -135,34 +98,8 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
} catch (_err) {
|
||||
// fallback to default
|
||||
}
|
||||
|
||||
const TYPE_MAP: Record<string, ValuePart["type"]> = {
|
||||
integer: "value",
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
};
|
||||
|
||||
const valueParts: ValuePart[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
const type = TYPE_MAP[part.type];
|
||||
if (!type) continue;
|
||||
const last = valueParts[valueParts.length - 1];
|
||||
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
|
||||
if (type === "value" && last?.type === "value") {
|
||||
last.value += part.value;
|
||||
} else {
|
||||
valueParts.push({ type, value: part.value });
|
||||
}
|
||||
}
|
||||
|
||||
return valueParts;
|
||||
}
|
||||
|
||||
// default processing of numeric values
|
||||
const value = formatNumber(
|
||||
state,
|
||||
locale,
|
||||
@@ -177,14 +114,10 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
attributes.unit_of_measurement;
|
||||
|
||||
if (unit) {
|
||||
return [
|
||||
{ type: "value", value: value },
|
||||
{ type: "literal", value: blankBeforeUnit(unit, locale) },
|
||||
{ type: "unit", value: unit },
|
||||
];
|
||||
return `${value}${blankBeforeUnit(unit, locale)}${unit}`;
|
||||
}
|
||||
|
||||
return [{ type: "value", value: value }];
|
||||
return value;
|
||||
}
|
||||
|
||||
if (["date", "input_datetime", "time"].includes(domain)) {
|
||||
@@ -196,51 +129,36 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
const components = state.split(" ");
|
||||
if (components.length === 2) {
|
||||
// Date and time.
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDateTime(
|
||||
new Date(components.join("T")),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
),
|
||||
},
|
||||
];
|
||||
return formatDateTime(
|
||||
new Date(components.join("T")),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
);
|
||||
}
|
||||
if (components.length === 1) {
|
||||
if (state.includes("-")) {
|
||||
// Date only.
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDate(
|
||||
new Date(`${state}T00:00`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
),
|
||||
},
|
||||
];
|
||||
return formatDate(
|
||||
new Date(`${state}T00:00`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
);
|
||||
}
|
||||
if (state.includes(":")) {
|
||||
// Time only.
|
||||
const now = new Date();
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatTime(
|
||||
new Date(`${now.toISOString().split("T")[0]}T${state}`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
),
|
||||
},
|
||||
];
|
||||
return formatTime(
|
||||
new Date(`${now.toISOString().split("T")[0]}T${state}`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
return [{ type: "value", value: state }];
|
||||
return state;
|
||||
} catch (_e) {
|
||||
// Formatting methods may throw error if date parsing doesn't go well,
|
||||
// just return the state string in that case.
|
||||
return [{ type: "value", value: state }];
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,58 +182,25 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||
) {
|
||||
try {
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value: formatDateTime(new Date(state), locale, config),
|
||||
},
|
||||
];
|
||||
return formatDateTime(new Date(state), locale, config);
|
||||
} catch (_err) {
|
||||
return [{ type: "value", value: state }];
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: "value",
|
||||
value:
|
||||
(entity?.translation_key &&
|
||||
localize(
|
||||
`component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}`
|
||||
)) ||
|
||||
// Return device class translation
|
||||
(attributes.device_class &&
|
||||
localize(
|
||||
`component.${domain}.entity_component.${attributes.device_class}.state.${state}`
|
||||
)) ||
|
||||
// Return default translation
|
||||
localize(`component.${domain}.entity_component._.state.${state}`) ||
|
||||
// We don't know! Return the raw state.
|
||||
state,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const computeStateToParts = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
locale: FrontendLocaleData,
|
||||
sensorNumericDeviceClasses: string[],
|
||||
config: HassConfig,
|
||||
entities: HomeAssistant["entities"],
|
||||
state?: string
|
||||
): ValuePart[] => {
|
||||
const entity = entities?.[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
return computeStateToPartsFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
sensorNumericDeviceClasses,
|
||||
config,
|
||||
entity,
|
||||
stateObj.entity_id,
|
||||
stateObj.attributes,
|
||||
state !== undefined ? state : stateObj.state
|
||||
return (
|
||||
(entity?.translation_key &&
|
||||
localize(
|
||||
`component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}`
|
||||
)) ||
|
||||
// Return device class translation
|
||||
(attributes.device_class &&
|
||||
localize(
|
||||
`component.${domain}.entity_component.${attributes.device_class}.state.${state}`
|
||||
)) ||
|
||||
// Return default translation
|
||||
localize(`component.${domain}.entity_component._.state.${state}`) ||
|
||||
// We don't know! Return the raw state.
|
||||
state
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { NumberFormat } from "../../data/translation";
|
||||
import { round } from "./round";
|
||||
|
||||
/**
|
||||
* Returns true if the entity is considered numeric based on the attributes it has
|
||||
@@ -51,22 +52,7 @@ export const formatNumber = (
|
||||
num: string | number,
|
||||
localeOptions?: FrontendLocaleData,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): string =>
|
||||
formatNumberToParts(num, localeOptions, options)
|
||||
.map((part) => part.value)
|
||||
.join("");
|
||||
|
||||
/**
|
||||
* Returns an array of objects containing the formatted number in parts
|
||||
* Similar to Intl.NumberFormat.prototype.formatToParts()
|
||||
*
|
||||
* Input params - same as for formatNumber()
|
||||
*/
|
||||
export const formatNumberToParts = (
|
||||
num: string | number,
|
||||
localeOptions?: FrontendLocaleData,
|
||||
options?: Intl.NumberFormatOptions
|
||||
): any[] => {
|
||||
): string => {
|
||||
const locale = localeOptions
|
||||
? numberFormatToLocale(localeOptions)
|
||||
: undefined;
|
||||
@@ -85,7 +71,7 @@ export const formatNumberToParts = (
|
||||
return new Intl.NumberFormat(
|
||||
locale,
|
||||
getDefaultFormatOptions(num, options)
|
||||
).formatToParts(Number(num));
|
||||
).format(Number(num));
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -100,10 +86,15 @@ export const formatNumberToParts = (
|
||||
...options,
|
||||
useGrouping: false,
|
||||
})
|
||||
).formatToParts(Number(num));
|
||||
).format(Number(num));
|
||||
}
|
||||
|
||||
return [{ type: "literal", value: num }];
|
||||
if (typeof num === "string") {
|
||||
return num;
|
||||
}
|
||||
return `${round(num, options?.maximumFractionDigits).toString()}${
|
||||
options?.style === "currency" ? ` ${options.currency}` : ""
|
||||
}`;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
const SI_PREFIX_MULTIPLIERS: Record<string, number> = {
|
||||
T: 1e12,
|
||||
G: 1e9,
|
||||
M: 1e6,
|
||||
k: 1e3,
|
||||
m: 1e-3,
|
||||
"\u00B5": 1e-6, // µ (micro sign)
|
||||
"\u03BC": 1e-6, // μ (greek small letter mu)
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize a numeric value by detecting SI unit prefixes (T, G, M, k, m, µ).
|
||||
* Only applies when the unit is longer than 1 character and starts with a
|
||||
* recognized prefix, avoiding false positives on standalone units like "m" (meters).
|
||||
*/
|
||||
export const normalizeValueBySIPrefix = (
|
||||
value: number,
|
||||
unit: string | undefined
|
||||
): number => {
|
||||
if (!unit || unit.length <= 1) {
|
||||
return value;
|
||||
}
|
||||
const prefix = unit[0];
|
||||
if (prefix in SI_PREFIX_MULTIPLIERS) {
|
||||
return value * SI_PREFIX_MULTIPLIERS[prefix];
|
||||
}
|
||||
return value;
|
||||
};
|
||||
@@ -12,10 +12,6 @@ export type FormatEntityStateFunc = (
|
||||
stateObj: HassEntity,
|
||||
state?: string
|
||||
) => string;
|
||||
export type FormatEntityStateToPartsFunc = (
|
||||
stateObj: HassEntity,
|
||||
state?: string
|
||||
) => ValuePart[];
|
||||
export type FormatEntityAttributeValueFunc = (
|
||||
stateObj: HassEntity,
|
||||
attribute: string,
|
||||
@@ -50,13 +46,12 @@ export const computeFormatFunctions = async (
|
||||
sensorNumericDeviceClasses: string[]
|
||||
): Promise<{
|
||||
formatEntityState: FormatEntityStateFunc;
|
||||
formatEntityStateToParts: FormatEntityStateToPartsFunc;
|
||||
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
|
||||
formatEntityAttributeValueToParts: FormatEntityAttributeValueToPartsFunc;
|
||||
formatEntityAttributeName: FormatEntityAttributeNameFunc;
|
||||
formatEntityName: FormatEntityNameFunc;
|
||||
}> => {
|
||||
const { computeStateDisplay, computeStateToParts } =
|
||||
const { computeStateDisplay } =
|
||||
await import("../entity/compute_state_display");
|
||||
const {
|
||||
computeAttributeValueDisplay,
|
||||
@@ -75,16 +70,6 @@ export const computeFormatFunctions = async (
|
||||
entities,
|
||||
state
|
||||
),
|
||||
formatEntityStateToParts: (stateObj, state) =>
|
||||
computeStateToParts(
|
||||
localize,
|
||||
stateObj,
|
||||
locale,
|
||||
sensorNumericDeviceClasses,
|
||||
config,
|
||||
entities,
|
||||
state
|
||||
),
|
||||
formatEntityAttributeValue: (stateObj, attribute, value) =>
|
||||
computeAttributeValueDisplay(
|
||||
localize,
|
||||
|
||||
@@ -19,7 +19,6 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { getAllGraphColors } from "../../common/color/colors";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import { themesContext } from "../../data/context";
|
||||
import type { Themes } from "../../data/ws-themes";
|
||||
@@ -28,7 +27,6 @@ import type { HomeAssistant } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../ha-icon-button";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { formatTimeLabel } from "./axis-label";
|
||||
import { downSampleLineData } from "./down-sample";
|
||||
@@ -94,18 +92,10 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _resizeAnimationDuration?: number;
|
||||
|
||||
private _suspendResize = false;
|
||||
|
||||
private _layoutTransitionActive = false;
|
||||
|
||||
// @ts-ignore
|
||||
private _resizeController = new ResizeController(this, {
|
||||
callback: () => {
|
||||
if (this.chart) {
|
||||
if (this._suspendResize) {
|
||||
this._shouldResizeChart = true;
|
||||
return;
|
||||
}
|
||||
if (!this.chart.getZr().animation.isFinished()) {
|
||||
this._shouldResizeChart = true;
|
||||
} else {
|
||||
@@ -123,11 +113,8 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _originalZrFlush?: () => void;
|
||||
|
||||
private _pendingSetup = false;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._pendingSetup = false;
|
||||
while (this._listeners.length) {
|
||||
this._listeners.pop()!();
|
||||
}
|
||||
@@ -139,13 +126,7 @@ export class HaChartBase extends LitElement {
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._pendingSetup = true;
|
||||
afterNextRender(() => {
|
||||
if (this.isConnected && this._pendingSetup) {
|
||||
this._pendingSetup = false;
|
||||
this._setupChart();
|
||||
}
|
||||
});
|
||||
this._setupChart();
|
||||
}
|
||||
|
||||
this._listeners.push(
|
||||
@@ -200,26 +181,6 @@ export class HaChartBase extends LitElement {
|
||||
() => window.removeEventListener("keyup", handleKeyUp)
|
||||
);
|
||||
}
|
||||
|
||||
const handleLayoutTransition: EventListener = (ev) => {
|
||||
const event = ev as HASSDomEvent<HASSDomEvents["hass-layout-transition"]>;
|
||||
this._layoutTransitionActive = Boolean(event.detail?.active);
|
||||
this.toggleAttribute(
|
||||
"layout-transition-active",
|
||||
this._layoutTransitionActive
|
||||
);
|
||||
this._suspendResize = this._layoutTransitionActive;
|
||||
if (!this._suspendResize) {
|
||||
this._resizeChartIfNeeded();
|
||||
}
|
||||
};
|
||||
window.addEventListener("hass-layout-transition", handleLayoutTransition);
|
||||
this._listeners.push(() =>
|
||||
window.removeEventListener(
|
||||
"hass-layout-transition",
|
||||
handleLayoutTransition
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
@@ -1027,29 +988,19 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
private _handleChartRenderFinished = () => {
|
||||
this._resizeChartIfNeeded();
|
||||
if (this._shouldResizeChart) {
|
||||
this.chart?.resize({
|
||||
animation:
|
||||
this._reducedMotion ||
|
||||
typeof this._resizeAnimationDuration !== "number"
|
||||
? undefined
|
||||
: { duration: this._resizeAnimationDuration },
|
||||
});
|
||||
this._shouldResizeChart = false;
|
||||
this._resizeAnimationDuration = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
private _resizeChartIfNeeded() {
|
||||
if (!this.chart || !this._shouldResizeChart) {
|
||||
return;
|
||||
}
|
||||
if (this._suspendResize) {
|
||||
return;
|
||||
}
|
||||
if (!this.chart.getZr().animation.isFinished()) {
|
||||
return;
|
||||
}
|
||||
this.chart.resize({
|
||||
animation:
|
||||
this._reducedMotion || typeof this._resizeAnimationDuration !== "number"
|
||||
? undefined
|
||||
: { duration: this._resizeAnimationDuration },
|
||||
});
|
||||
this._shouldResizeChart = false;
|
||||
this._resizeAnimationDuration = undefined;
|
||||
}
|
||||
|
||||
private _compareCustomLegendOptions(
|
||||
oldOptions: ECOption | undefined,
|
||||
newOptions: ECOption | undefined
|
||||
@@ -1071,18 +1022,11 @@ export class HaChartBase extends LitElement {
|
||||
display: block;
|
||||
position: relative;
|
||||
letter-spacing: normal;
|
||||
overflow: visible;
|
||||
}
|
||||
:host([layout-transition-active]),
|
||||
:host([layout-transition-active]) .container,
|
||||
:host([layout-transition-active]) .chart-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
.container.has-height {
|
||||
max-height: var(--chart-max-height, 350px);
|
||||
@@ -1090,7 +1034,6 @@ export class HaChartBase extends LitElement {
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
max-height: var(--chart-max-height, 350px);
|
||||
overflow: visible;
|
||||
}
|
||||
.has-height .chart-container {
|
||||
flex: 1;
|
||||
|
||||
@@ -572,7 +572,6 @@ export class StatisticsChart extends LitElement {
|
||||
let firstSum: number | null | undefined = null;
|
||||
stats.forEach((stat) => {
|
||||
const startDate = new Date(stat.start);
|
||||
const endDate = new Date(stat.end);
|
||||
if (prevDate === startDate) {
|
||||
return;
|
||||
}
|
||||
@@ -602,25 +601,10 @@ export class StatisticsChart extends LitElement {
|
||||
dataValues.push(val);
|
||||
});
|
||||
if (!this._hiddenStats.has(statistic_id)) {
|
||||
pushData(
|
||||
startDate,
|
||||
endDate.getTime() < endTime.getTime() ? endDate : endTime,
|
||||
dataValues
|
||||
);
|
||||
pushData(startDate, new Date(stat.end), dataValues);
|
||||
}
|
||||
});
|
||||
|
||||
// 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]!])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Append current state if viewing recent data
|
||||
const now = new Date();
|
||||
// allow 10m of leeway for "now", because stats are 5 minute aggregated
|
||||
@@ -635,6 +619,16 @@ export class StatisticsChart extends LitElement {
|
||||
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)[] = [];
|
||||
|
||||
@@ -10,11 +10,9 @@ import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button";
|
||||
import { createCloseHeading } from "../ha-dialog";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-list";
|
||||
import "../ha-list-item";
|
||||
import "../ha-sortable";
|
||||
import "../ha-svg-icon";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
DataTableColumnData,
|
||||
|
||||
@@ -20,7 +20,6 @@ import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { groupBy } from "../../common/util/group-by";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../common/const";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -637,7 +636,7 @@ export class HaDataTable extends LitElement {
|
||||
.map(
|
||||
([key2, column2], i) =>
|
||||
html`${i !== 0
|
||||
? STRINGS_SEPARATOR_DOT
|
||||
? " · "
|
||||
: nothing}${column2.template
|
||||
? column2.template(row)
|
||||
: row[key2]}`
|
||||
@@ -1193,7 +1192,6 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
.mdc-data-table__cell--numeric {
|
||||
text-align: var(--float-end);
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell--icon {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -11,8 +11,10 @@ import {
|
||||
sortDeviceAutomations,
|
||||
} from "../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import "../ha-md-select";
|
||||
import "../ha-md-select-option";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
||||
@@ -190,7 +192,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
this._renderEmpty = false;
|
||||
}
|
||||
|
||||
private _automationChanged(ev: ValueChangedEvent<string>) {
|
||||
private _automationChanged(ev: CustomEvent<{ value: string }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (!value || NO_AUTOMATION_KEY === value) {
|
||||
@@ -215,4 +217,10 @@ export abstract class HaDeviceAutomationPicker<
|
||||
delete value.metadata;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-select {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,16 @@ import secondsToDuration from "../../common/datetime/seconds_to_duration";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
isNumericState,
|
||||
} from "../../common/number/format_number";
|
||||
import {
|
||||
isUnavailableState,
|
||||
UNAVAILABLE,
|
||||
UNKNOWN,
|
||||
} from "../../data/entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import { timerTimeRemaining } from "../../data/timer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -171,11 +180,16 @@ export class HaStateLabelBadge extends LitElement {
|
||||
}
|
||||
// eslint-disable-next-line: disable=no-fallthrough
|
||||
default:
|
||||
return isUnavailableState(entityState.state)
|
||||
return entityState.state === UNKNOWN ||
|
||||
entityState.state === UNAVAILABLE
|
||||
? "—"
|
||||
: this.hass!.formatEntityStateToParts(entityState).find(
|
||||
(part) => part.type === "value"
|
||||
)?.value;
|
||||
: isNumericState(entityState)
|
||||
? formatNumber(
|
||||
entityState.state,
|
||||
this.hass!.locale,
|
||||
getNumberFormatOptions(entityState, entry)
|
||||
)
|
||||
: this.hass!.formatEntityState(entityState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,11 +238,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
if (domain === "timer") {
|
||||
return secondsToDuration(_timerTimeRemaining);
|
||||
}
|
||||
return (
|
||||
this.hass!.formatEntityStateToParts(entityState).find(
|
||||
(part) => part.type === "unit"
|
||||
)?.value || null
|
||||
);
|
||||
return entityState.attributes.unit_of_measurement || null;
|
||||
}
|
||||
|
||||
private _clearInterval() {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { Analytics, AnalyticsPreferences } from "../data/analytics";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import "./ha-md-list-item";
|
||||
import "./ha-settings-row";
|
||||
import "./ha-switch";
|
||||
import "./ha-tooltip";
|
||||
import type { HaSwitch } from "./ha-switch";
|
||||
@@ -33,80 +33,105 @@ export class HaAnalytics extends LitElement {
|
||||
const baseEnabled = !loading && this.analytics!.preferences.base;
|
||||
|
||||
return html`
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.localize(
|
||||
<ha-settings-row>
|
||||
<span slot="heading" data-for="base">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.title`
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.localize(
|
||||
)}
|
||||
</span>
|
||||
<span slot="description" data-for="base">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.description`
|
||||
)}</span
|
||||
>
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${!!baseEnabled}
|
||||
.preference=${"base"}
|
||||
.disabled=${loading}
|
||||
name="base"
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
${ADDITIONAL_PREFERENCES.map(
|
||||
(preference) => html`
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.localize(
|
||||
<ha-settings-row>
|
||||
<span slot="heading" data-for=${preference}>
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.title`
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.localize(
|
||||
)}
|
||||
</span>
|
||||
<span slot="description" data-for=${preference}>
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.description`
|
||||
)}</span
|
||||
>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.id="switch-${preference}"
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${!!this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
></ha-switch>
|
||||
${baseEnabled
|
||||
? nothing
|
||||
: html`<ha-tooltip .for="switch-${preference}" placement="right">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
</ha-tooltip>`}
|
||||
</ha-md-list-item>
|
||||
)}
|
||||
</span>
|
||||
<span>
|
||||
<ha-switch
|
||||
.id="switch-${preference}"
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${!!this.analytics?.preferences[preference]}
|
||||
.preference=${preference}
|
||||
name=${preference}
|
||||
>
|
||||
</ha-switch>
|
||||
${baseEnabled
|
||||
? nothing
|
||||
: html`<ha-tooltip
|
||||
.for="switch-${preference}"
|
||||
placement="right"
|
||||
>
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
</ha-tooltip>`}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
`
|
||||
)}
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.localize(
|
||||
<ha-settings-row>
|
||||
<span slot="heading" data-for="diagnostics">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.title`
|
||||
)}</span
|
||||
>
|
||||
<span slot="supporting-text"
|
||||
>${this.localize(
|
||||
)}
|
||||
</span>
|
||||
<span slot="description" data-for="diagnostics">
|
||||
${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.description`
|
||||
)}</span
|
||||
>
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
@change=${this._handleRowClick}
|
||||
.checked=${!!this.analytics?.preferences.diagnostics}
|
||||
.preference=${"diagnostics"}
|
||||
.disabled=${loading}
|
||||
name="diagnostics"
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
|
||||
this.shadowRoot!.querySelectorAll("*[data-for]").forEach((el) => {
|
||||
const forEl = (el as HTMLElement).dataset.for;
|
||||
delete (el as HTMLElement).dataset.for;
|
||||
|
||||
el.addEventListener("click", () => {
|
||||
const toFocus = this.shadowRoot!.querySelector(
|
||||
`*[name=${forEl}]`
|
||||
) as HTMLElement | null;
|
||||
|
||||
if (toFocus) {
|
||||
toFocus.focus();
|
||||
toFocus.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _handleRowClick(ev: Event) {
|
||||
const target = ev.currentTarget as HaSwitch;
|
||||
const preference = (target as any).preference;
|
||||
@@ -139,10 +164,13 @@ export class HaAnalytics extends LitElement {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-item-overflow: visible;
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
span[slot="heading"],
|
||||
span[slot="description"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -48,6 +48,7 @@ export class HaAnsiToHtml extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
pre.wrap {
|
||||
|
||||
@@ -163,7 +163,7 @@ export class HaAreaPicker extends LitElement {
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.area-picker.add_new_suggestion",
|
||||
"ui.components.area-picker.add_new_sugestion",
|
||||
{
|
||||
name: searchString,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import type { FloorRegistryEntry } from "../data/floor_registry";
|
||||
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-items-display-editor";
|
||||
@@ -200,7 +200,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
private async _areaDisplayChanged(ev: ValueChangedEvent<DisplayValue>) {
|
||||
private async _areaDisplayChanged(ev: CustomEvent<{ value: DisplayValue }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
const currentFloorId = (ev.currentTarget as any).floorId;
|
||||
|
||||
@@ -2,12 +2,14 @@ import type { PropertyValueMap } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { formatLanguageCode } from "../common/language/format_language";
|
||||
import type { AssistPipeline } from "../data/assist_pipeline";
|
||||
import { listAssistPipelines } from "../data/assist_pipeline";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const PREFERRED = "preferred";
|
||||
const LAST_USED = "last_used";
|
||||
@@ -39,31 +41,6 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
const value = this.value ?? this._default;
|
||||
const options: HaSelectOption[] = [
|
||||
{
|
||||
value: PREFERRED,
|
||||
label: this.hass.localize("ui.components.pipeline-picker.preferred", {
|
||||
preferred: this._pipelines.find(
|
||||
(pipeline) => pipeline.id === this._preferredPipeline
|
||||
)?.name,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
if (this.includeLastUsed) {
|
||||
options.unshift({
|
||||
value: LAST_USED,
|
||||
label: this.hass.localize("ui.components.pipeline-picker.last_used"),
|
||||
});
|
||||
}
|
||||
|
||||
options.push(
|
||||
...this._pipelines.map((pipeline) => ({
|
||||
value: pipeline.id,
|
||||
label: `${pipeline.name} (${formatLanguageCode(pipeline.language, this.hass.locale)})`,
|
||||
}))
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
@@ -72,8 +49,33 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
.options=${options}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${this.includeLastUsed
|
||||
? html`
|
||||
<ha-list-item .value=${LAST_USED}>
|
||||
${this.hass!.localize(
|
||||
"ui.components.pipeline-picker.last_used"
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
: null}
|
||||
<ha-list-item .value=${PREFERRED}>
|
||||
${this.hass!.localize("ui.components.pipeline-picker.preferred", {
|
||||
preferred: this._pipelines.find(
|
||||
(pipeline) => pipeline.id === this._preferredPipeline
|
||||
)?.name,
|
||||
})}
|
||||
</ha-list-item>
|
||||
${this._pipelines.map(
|
||||
(pipeline) =>
|
||||
html`<ha-list-item .value=${pipeline.id}>
|
||||
${pipeline.name}
|
||||
(${formatLanguageCode(pipeline.language, this.hass.locale)})
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@@ -94,17 +96,17 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
if (
|
||||
!this.hass ||
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === this._default)
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === this._default)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = value === this._default ? undefined : value;
|
||||
this.value = target.value === this._default ? undefined : target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
@@ -259,10 +260,14 @@ export class HaBaseTimeInput extends LitElement {
|
||||
.required=${this.required}
|
||||
.value=${this.amPm}
|
||||
.disabled=${this.disabled}
|
||||
.name=${"amPm"}
|
||||
name="amPm"
|
||||
naturalMenuWidth
|
||||
fixedMenuPosition
|
||||
@selected=${this._valueChanged}
|
||||
.options=${["AM", "PM"]}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
<ha-list-item value="AM">AM</ha-list-item>
|
||||
<ha-list-item value="PM">PM</ha-list-item>
|
||||
</ha-select>`}
|
||||
</div>
|
||||
${this.helper
|
||||
@@ -277,12 +282,10 @@ export class HaBaseTimeInput extends LitElement {
|
||||
fireEvent(this, "value-changed");
|
||||
}
|
||||
|
||||
private _valueChanged(ev: InputEvent | HaSelectSelectEvent): void {
|
||||
private _valueChanged(ev: InputEvent) {
|
||||
const textField = ev.currentTarget as HaTextField;
|
||||
this[textField.name] =
|
||||
textField.name === "amPm"
|
||||
? (ev as HaSelectSelectEvent).detail.value
|
||||
: Number(textField.value);
|
||||
textField.name === "amPm" ? textField.value : Number(textField.value);
|
||||
const value: TimeChangedEvent = {
|
||||
hours: this.hours,
|
||||
minutes: this.minutes,
|
||||
@@ -363,6 +366,10 @@ export class HaBaseTimeInput extends LitElement {
|
||||
ha-textfield:last-child {
|
||||
--text-field-border-top-right-radius: var(--mdc-shape-medium);
|
||||
}
|
||||
ha-select {
|
||||
--mdc-shape-small: 0;
|
||||
width: 85px;
|
||||
}
|
||||
:host([clearable]) .mdc-select__anchor {
|
||||
padding-inline-end: var(--select-selected-text-padding-end, 12px);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { Blueprint, BlueprintDomain, Blueprints } from "../data/blueprint";
|
||||
import { fetchBlueprints } from "../data/blueprint";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
|
||||
@customElement("ha-blueprint-picker")
|
||||
@@ -54,16 +55,20 @@ class HaBluePrintPicker extends LitElement {
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass.localize("ui.components.blueprint-picker.select_blueprint")}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._blueprintChanged}
|
||||
.options=${this._processedBlueprints(this.blueprints).map(
|
||||
(blueprint) => ({
|
||||
value: blueprint.path,
|
||||
label: blueprint.name,
|
||||
})
|
||||
)}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${this._processedBlueprints(this.blueprints).map(
|
||||
(blueprint) => html`
|
||||
<ha-list-item .value=${blueprint.path}>
|
||||
${blueprint.name}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@@ -77,8 +82,8 @@ class HaBluePrintPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _blueprintChanged(ev: HaSelectSelectEvent) {
|
||||
const newValue = ev.detail.value;
|
||||
private _blueprintChanged(ev) {
|
||||
const newValue = ev.target.value;
|
||||
|
||||
if (newValue !== this.value) {
|
||||
this.value = newValue;
|
||||
|
||||
@@ -6,7 +6,7 @@ import memoizeOne from "memoize-one";
|
||||
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||
@@ -224,7 +224,7 @@ export class HaColorPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
|
||||
private _valueChanged(ev: CustomEvent<{ value?: string }>) {
|
||||
ev.stopPropagation();
|
||||
const selected = ev.detail.value;
|
||||
const normalized =
|
||||
|
||||
@@ -89,7 +89,7 @@ export class HaControlSelectMenu extends LitElement {
|
||||
private _renderOption = (option: SelectOption) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${option.value}
|
||||
.selected=${this.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
|
||||
@@ -263,6 +263,15 @@ export class HaControlSelectMenu extends LitElement {
|
||||
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);
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { ConfigEntry, SubEntry } from "../data/config_entries";
|
||||
import { getConfigEntry, getSubEntries } from "../data/config_entries";
|
||||
@@ -13,8 +14,9 @@ import { fetchIntegrationManifest } from "../data/integration";
|
||||
import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow";
|
||||
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@@ -71,35 +73,37 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
value = NONE;
|
||||
}
|
||||
|
||||
const options: HaSelectOption[] = this._agents.map((agent) => ({
|
||||
value: agent.id,
|
||||
label: agent.name,
|
||||
disabled:
|
||||
agent.supported_languages !== "*" &&
|
||||
agent.supported_languages.length === 0,
|
||||
}));
|
||||
|
||||
if (!this.required) {
|
||||
options.unshift({
|
||||
value: NONE,
|
||||
label: this.hass.localize(
|
||||
"ui.components.conversation-agent-picker.none"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
this.hass!.localize(
|
||||
"ui.components.conversation-agent-picker.conversation_agent"
|
||||
"ui.components.coversation-agent-picker.conversation_agent"
|
||||
)}
|
||||
.value=${value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
.options=${options}
|
||||
></ha-select
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${!this.required
|
||||
? html`<ha-list-item .value=${NONE}>
|
||||
${this.hass!.localize(
|
||||
"ui.components.coversation-agent-picker.none"
|
||||
)}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
${this._agents.map(
|
||||
(agent) =>
|
||||
html`<ha-list-item
|
||||
.value=${agent.id}
|
||||
.disabled=${agent.supported_languages !== "*" &&
|
||||
agent.supported_languages.length === 0}
|
||||
>
|
||||
${agent.name}
|
||||
</ha-list-item>`
|
||||
)}</ha-select
|
||||
>${(this._subConfigEntry &&
|
||||
this._configEntry?.supported_subentry_types[
|
||||
this._subConfigEntry.subentry_type
|
||||
@@ -234,17 +238,17 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
if (
|
||||
!this.hass ||
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = value === NONE ? undefined : value;
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._agents!.find((agent) => agent.id === this.value)
|
||||
|
||||
@@ -65,10 +65,6 @@ export class HaDateRangePicker extends LitElement {
|
||||
@property({ attribute: "time-picker", type: Boolean })
|
||||
public timePicker = false;
|
||||
|
||||
public open(): void {
|
||||
this._openPicker();
|
||||
}
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public minimal = false;
|
||||
@@ -310,15 +306,6 @@ export class HaDateRangePicker extends LitElement {
|
||||
return dateRangePicker.vueComponent.$children[0];
|
||||
}
|
||||
|
||||
private _openPicker() {
|
||||
if (!this._dateRangePicker.open) {
|
||||
const datePicker = this.shadowRoot!.querySelector(
|
||||
"date-range-picker div.date-range-inputs"
|
||||
) as any;
|
||||
datePicker?.click();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleInputClick() {
|
||||
// close the date picker, so it will open again on the click event
|
||||
if (this._dateRangePicker.open) {
|
||||
|
||||
@@ -7,9 +7,8 @@ import { nextRender } from "../common/util/render-status";
|
||||
import { haStyleDialog } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { DatePickerDialogParams } from "./ha-date-input";
|
||||
import "./ha-dialog";
|
||||
import "./ha-button";
|
||||
import "./ha-dialog-footer";
|
||||
import "./ha-wa-dialog";
|
||||
|
||||
@customElement("ha-dialog-date-picker")
|
||||
export class HaDialogDatePicker extends LitElement {
|
||||
@@ -23,8 +22,6 @@ export class HaDialogDatePicker extends LitElement {
|
||||
|
||||
@state() private _params?: DatePickerDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _value?: string;
|
||||
|
||||
public async showDialog(params: DatePickerDialogParams): Promise<void> {
|
||||
@@ -33,14 +30,9 @@ export class HaDialogDatePicker extends LitElement {
|
||||
await nextRender();
|
||||
this._params = params;
|
||||
this._value = params.value;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -49,13 +41,7 @@ export class HaDialogDatePicker extends LitElement {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
width="small"
|
||||
without-header
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
return html`<ha-dialog open @closed=${this.closeDialog}>
|
||||
<app-datepicker
|
||||
.value=${this._value}
|
||||
.min=${this._params.min}
|
||||
@@ -64,40 +50,35 @@ export class HaDialogDatePicker extends LitElement {
|
||||
@datepicker-value-updated=${this._valueChanged}
|
||||
.firstDayOfWeek=${this._params.firstWeekday}
|
||||
></app-datepicker>
|
||||
|
||||
<div class="bottom-actions">
|
||||
${this._params.canClear
|
||||
? html`<ha-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._clear}
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.clear")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this._setToday}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.today")}
|
||||
</ha-button>
|
||||
</div>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._setValue}>
|
||||
${this.hass.localize("ui.common.ok")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>`;
|
||||
${this._params.canClear
|
||||
? html`<ha-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._clear}
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.clear")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this._setToday}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.date-picker.today")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="primaryAction"
|
||||
dialogaction="cancel"
|
||||
class="cancel-btn"
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._setValue}>
|
||||
${this.hass.localize("ui.common.ok")}
|
||||
</ha-button>
|
||||
</ha-dialog>`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
@@ -127,20 +108,11 @@ export class HaDialogDatePicker extends LitElement {
|
||||
static styles = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-wa-dialog {
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
.bottom-actions {
|
||||
display: flex;
|
||||
gap: var(--ha-space-4);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: var(--ha-space-1);
|
||||
--justify-action-buttons: space-between;
|
||||
}
|
||||
app-datepicker {
|
||||
display: block;
|
||||
margin-inline: auto;
|
||||
--app-datepicker-accent-color: var(--primary-color);
|
||||
--app-datepicker-bg-color: transparent;
|
||||
--app-datepicker-color: var(--primary-text-color);
|
||||
@@ -157,6 +129,11 @@ export class HaDialogDatePicker extends LitElement {
|
||||
app-datepicker::part(body) {
|
||||
direction: ltr;
|
||||
}
|
||||
@media all and (min-width: 450px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: 300px;
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
app-datepicker {
|
||||
width: 100%;
|
||||
|
||||
@@ -76,18 +76,6 @@ export class HaDialog extends DialogBase {
|
||||
var(--divider-color)
|
||||
);
|
||||
z-index: var(--dialog-z-index, 8);
|
||||
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
--mdc-typography-headline6-font-size: 1.574rem;
|
||||
}
|
||||
.mdc-dialog::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
-webkit-backdrop-filter: var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
@@ -96,6 +84,9 @@ export class HaDialog extends DialogBase {
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
);
|
||||
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
--mdc-typography-headline6-font-size: 1.574rem;
|
||||
}
|
||||
.mdc-dialog .mdc-dialog__scrim {
|
||||
background-color: var(--mdc-dialog-scrim-color, none);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-divider")
|
||||
export class HaMdDivider extends LitElement {
|
||||
@property() public label?: string;
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<div
|
||||
role=${ifDefined(this.label ? "separator" : undefined)}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
>
|
||||
<span class="line"></span>
|
||||
${this.label
|
||||
? html`
|
||||
<span class="label">${this.label}</span>
|
||||
<span class="line"></span>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
width: var(--ha-divider-width, 100%);
|
||||
}
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.label {
|
||||
padding: var(--ha-divider-label-padding, 0 16px);
|
||||
}
|
||||
.line {
|
||||
flex: 1;
|
||||
background-color: var(--divider-color);
|
||||
height: var(--ha-divider-line-height, 1px);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-divider": HaMdDivider;
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,6 @@ import type { PropertyValues } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"hass-layout-transition": { active: boolean; reason?: string };
|
||||
}
|
||||
interface HTMLElementEventMap {
|
||||
"hass-layout-transition": HASSDomEvent<
|
||||
HASSDomEvents["hass-layout-transition"]
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
const blockingElements = (document as any).$blockingElements;
|
||||
|
||||
@@ -27,30 +15,6 @@ export class HaDrawer extends DrawerBase {
|
||||
|
||||
private _rtlStyle?: HTMLElement;
|
||||
|
||||
private _sidebarTransitionActive = false;
|
||||
|
||||
private _handleDrawerTransitionStart = (ev: TransitionEvent) => {
|
||||
if (ev.propertyName !== "width" || this._sidebarTransitionActive) {
|
||||
return;
|
||||
}
|
||||
this._sidebarTransitionActive = true;
|
||||
fireEvent(window, "hass-layout-transition", {
|
||||
active: true,
|
||||
reason: "sidebar",
|
||||
});
|
||||
};
|
||||
|
||||
private _handleDrawerTransitionEnd = (ev: TransitionEvent) => {
|
||||
if (ev.propertyName !== "width" || !this._sidebarTransitionActive) {
|
||||
return;
|
||||
}
|
||||
this._sidebarTransitionActive = false;
|
||||
fireEvent(window, "hass-layout-transition", {
|
||||
active: false,
|
||||
reason: "sidebar",
|
||||
});
|
||||
};
|
||||
|
||||
protected createAdapter() {
|
||||
return {
|
||||
...super.createAdapter(),
|
||||
@@ -99,38 +63,6 @@ export class HaDrawer extends DrawerBase {
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
super.firstUpdated();
|
||||
this.mdcRoot?.addEventListener(
|
||||
"transitionstart",
|
||||
this._handleDrawerTransitionStart
|
||||
);
|
||||
this.mdcRoot?.addEventListener(
|
||||
"transitionend",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
this.mdcRoot?.addEventListener(
|
||||
"transitioncancel",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.mdcRoot?.removeEventListener(
|
||||
"transitionstart",
|
||||
this._handleDrawerTransitionStart
|
||||
);
|
||||
this.mdcRoot?.removeEventListener(
|
||||
"transitionend",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
this.mdcRoot?.removeEventListener(
|
||||
"transitioncancel",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
}
|
||||
|
||||
private async _setupSwipe() {
|
||||
const hammer = await import("../resources/hammer");
|
||||
this._mc = new hammer.Manager(document, {
|
||||
@@ -158,16 +90,6 @@ export class HaDrawer extends DrawerBase {
|
||||
border-color: var(--divider-color, rgba(0, 0, 0, 0.12));
|
||||
inset-inline-start: 0 !important;
|
||||
inset-inline-end: initial !important;
|
||||
transition-property: transform, width;
|
||||
transition-duration:
|
||||
var(--mdc-drawer-transition-duration, 0.2s),
|
||||
var(--ha-animation-duration-normal);
|
||||
transition-timing-function:
|
||||
var(
|
||||
--mdc-drawer-transition-timing-function,
|
||||
cubic-bezier(0.4, 0, 0.2, 1)
|
||||
),
|
||||
ease;
|
||||
}
|
||||
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
|
||||
z-index: 200;
|
||||
@@ -181,15 +103,6 @@ export class HaDrawer extends DrawerBase {
|
||||
direction: var(--direction);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
padding-left var(--ha-animation-duration-normal) ease,
|
||||
padding-inline-start var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mdc-drawer,
|
||||
.mdc-drawer-app-content {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -2,7 +2,7 @@ import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-it
|
||||
import "@home-assistant/webawesome/dist/components/icon/icon";
|
||||
import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
|
||||
import { css, type CSSResultGroup, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
/**
|
||||
@@ -17,8 +17,6 @@ import "./ha-svg-icon";
|
||||
*/
|
||||
@customElement("ha-dropdown-item")
|
||||
export class HaDropdownItem extends DropdownItem {
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
|
||||
protected renderCheckboxIcon() {
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
@@ -39,7 +37,6 @@ export class HaDropdownItem extends DropdownItem {
|
||||
|
||||
#check {
|
||||
visibility: visible;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#icon ::slotted(*) {
|
||||
@@ -49,16 +46,6 @@ export class HaDropdownItem extends DropdownItem {
|
||||
:host([variant="danger"]) #icon ::slotted(*) {
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
}
|
||||
|
||||
:host([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);
|
||||
}
|
||||
:host([selected]:hover) {
|
||||
background-color: var(--ha-color-fill-primary-quiet-hover);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import type WaButton from "@home-assistant/webawesome/dist/components/button/button";
|
||||
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 };
|
||||
}>;
|
||||
export type HaDropdownSelectEvent = CustomEvent<{ item: HaDropdownItem }>;
|
||||
|
||||
/**
|
||||
* Home Assistant dropdown component
|
||||
@@ -23,37 +16,11 @@ export type HaDropdownSelectEvent<T = string> = CustomEvent<{
|
||||
*
|
||||
*/
|
||||
@customElement("ha-dropdown")
|
||||
// @ts-ignore Allow to set an alternative anchor element
|
||||
export class HaDropdown extends Dropdown {
|
||||
@property({ attribute: false }) dropdownTag = "ha-dropdown";
|
||||
|
||||
@property({ attribute: false }) dropdownItemTag = "ha-dropdown-item";
|
||||
|
||||
public get anchorElement(): HTMLButtonElement | WaButton | undefined {
|
||||
// @ts-ignore Allow to set an anchor element on popup
|
||||
return this.popup?.anchor as HTMLButtonElement | WaButton | undefined;
|
||||
}
|
||||
|
||||
public set anchorElement(element: HTMLButtonElement | WaButton | undefined) {
|
||||
// @ts-ignore Allow to get the current anchor element from popup
|
||||
if (!this.popup) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore Allow to get the current anchor element from popup
|
||||
this.popup.anchor = element;
|
||||
}
|
||||
|
||||
/** Get the slotted trigger button, a <wa-button> or <button> element */
|
||||
// @ts-ignore Override parent method to be able to use alternative anchor
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override getTrigger(): HTMLButtonElement | WaButton | null {
|
||||
if (this.anchorElement) {
|
||||
return this.anchorElement;
|
||||
}
|
||||
// @ts-ignore fallback to default trigger slot if no anchorElement is set
|
||||
return super.getTrigger();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Dropdown.styles,
|
||||
|
||||
@@ -6,7 +6,6 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
import "./ha-button-toggle-group";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
|
||||
export interface HaDurationData {
|
||||
days?: number;
|
||||
@@ -153,9 +152,7 @@ class HaDurationInput extends LitElement {
|
||||
: NaN;
|
||||
}
|
||||
|
||||
private _durationChanged(
|
||||
ev: ValueChangedEvent<TimeChangedEvent | undefined>
|
||||
) {
|
||||
private _durationChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value ? { ...ev.detail.value } : undefined;
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export class HaFab extends FabBase {
|
||||
}
|
||||
:disabled {
|
||||
--mdc-theme-secondary: var(--disabled-text-color);
|
||||
cursor: not-allowed !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
`,
|
||||
// safari workaround - must be explicit
|
||||
|
||||
@@ -315,13 +315,9 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
ha-list {
|
||||
--mdc-list-item-meta-size: auto;
|
||||
--mdc-list-side-padding-right: var(--ha-space-1);
|
||||
--mdc-list-side-padding-left: var(--ha-space-4);
|
||||
--mdc-list-side-padding-right: 4px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
ha-dropdown-item {
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
@@ -179,9 +179,6 @@ export class HaFilterDomains extends LitElement {
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -199,9 +199,6 @@ export class HaFilterIntegrations extends LitElement {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -164,9 +164,6 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: var(--ha-space-4);
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -359,7 +359,7 @@ export class HaFloorPicker extends LitElement {
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.floor-picker.add_new_suggestion",
|
||||
"ui.components.floor-picker.add_new_sugestion",
|
||||
{
|
||||
name: searchString,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { HomeAssistant } from "../types";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-md-divider";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tooltip";
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
{
|
||||
id: ADD_NEW_ID + searchString,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.label-picker.add_new_suggestion",
|
||||
"ui.components.label-picker.add_new_sugestion",
|
||||
{
|
||||
name: searchString,
|
||||
}
|
||||
|
||||
@@ -107,6 +107,9 @@ export class HaLanguagePicker extends LitElement {
|
||||
|
||||
@property({ attribute: "no-sort", type: Boolean }) public noSort = false;
|
||||
|
||||
@property({ attribute: "inline-arrow", type: Boolean })
|
||||
public inlineArrow = false;
|
||||
|
||||
@state() _defaultLanguages: string[] = [];
|
||||
|
||||
@query("ha-generic-picker", true) public genericPicker!: HaGenericPicker;
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
import { Dialog } from "@material/web/dialog/internal/dialog";
|
||||
import { styles } from "@material/web/dialog/internal/dialog-styles";
|
||||
import {
|
||||
type DialogAnimation,
|
||||
DIALOG_DEFAULT_CLOSE_ANIMATION,
|
||||
DIALOG_DEFAULT_OPEN_ANIMATION,
|
||||
} from "@material/web/dialog/internal/animations";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
// workaround to be able to overlay a dialog with another dialog
|
||||
Dialog.addInitializer(async (instance) => {
|
||||
await instance.updateComplete;
|
||||
|
||||
const dialogInstance = instance as HaMdDialog;
|
||||
|
||||
// @ts-expect-error dialog is private
|
||||
dialogInstance.dialog.prepend(dialogInstance.scrim);
|
||||
// @ts-expect-error scrim is private
|
||||
dialogInstance.scrim.style.inset = 0;
|
||||
// @ts-expect-error scrim is private
|
||||
dialogInstance.scrim.style.zIndex = 0;
|
||||
|
||||
const { getOpenAnimation, getCloseAnimation } = dialogInstance;
|
||||
dialogInstance.getOpenAnimation = () => {
|
||||
const animations = getOpenAnimation.call(this);
|
||||
animations.container = [
|
||||
...(animations.container ?? []),
|
||||
...(animations.dialog ?? []),
|
||||
];
|
||||
animations.dialog = [];
|
||||
return animations;
|
||||
};
|
||||
dialogInstance.getCloseAnimation = () => {
|
||||
const animations = getCloseAnimation.call(this);
|
||||
animations.container = [
|
||||
...(animations.container ?? []),
|
||||
...(animations.dialog ?? []),
|
||||
];
|
||||
animations.dialog = [];
|
||||
return animations;
|
||||
};
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>;
|
||||
|
||||
/**
|
||||
* Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs
|
||||
*
|
||||
*/
|
||||
@customElement("ha-md-dialog")
|
||||
export class HaMdDialog extends Dialog {
|
||||
/**
|
||||
* When true the dialog will not close when the user presses the esc key or press out of the dialog.
|
||||
*/
|
||||
@property({ attribute: "disable-cancel-action", type: Boolean })
|
||||
public disableCancelAction = false;
|
||||
|
||||
private _polyfillDialogRegistered = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener("cancel", this._handleCancel);
|
||||
|
||||
if (typeof HTMLDialogElement !== "function") {
|
||||
this.addEventListener("open", this._handleOpen);
|
||||
|
||||
if (!DIALOG_POLYFILL) {
|
||||
DIALOG_POLYFILL = import("dialog-polyfill");
|
||||
}
|
||||
}
|
||||
|
||||
// if browser doesn't support animate API disable open/close animations
|
||||
if (this.animate === undefined) {
|
||||
this.quick = true;
|
||||
}
|
||||
|
||||
// if browser doesn't support animate API disable open/close animations
|
||||
if (this.animate === undefined) {
|
||||
this.quick = true;
|
||||
}
|
||||
}
|
||||
|
||||
// prevent open in older browsers and wait for polyfill to load
|
||||
private async _handleOpen(openEvent: Event) {
|
||||
openEvent.preventDefault();
|
||||
|
||||
if (this._polyfillDialogRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._polyfillDialogRegistered = true;
|
||||
this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css");
|
||||
const dialog = this.shadowRoot?.querySelector(
|
||||
"dialog"
|
||||
) as HTMLDialogElement;
|
||||
|
||||
const dialogPolyfill = await DIALOG_POLYFILL;
|
||||
dialogPolyfill.default.registerDialog(dialog);
|
||||
this.removeEventListener("open", this._handleOpen);
|
||||
|
||||
this.show();
|
||||
}
|
||||
|
||||
private async _loadPolyfillStylesheet(href) {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = href;
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
link.onload = () => resolve();
|
||||
link.onerror = () =>
|
||||
reject(new Error(`Stylesheet failed to load: ${href}`));
|
||||
|
||||
this.shadowRoot?.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
private _handleCancel(closeEvent: Event) {
|
||||
if (this.disableCancelAction) {
|
||||
closeEvent.preventDefault();
|
||||
const dialogElement = this.shadowRoot?.querySelector("dialog .container");
|
||||
if (this.animate !== undefined) {
|
||||
dialogElement?.animate(
|
||||
[
|
||||
{
|
||||
transform: "rotate(-1deg)",
|
||||
"animation-timing-function": "ease-in",
|
||||
},
|
||||
{
|
||||
transform: "rotate(1.5deg)",
|
||||
"animation-timing-function": "ease-out",
|
||||
},
|
||||
{
|
||||
transform: "rotate(0deg)",
|
||||
"animation-timing-function": "ease-in",
|
||||
},
|
||||
],
|
||||
{
|
||||
duration: 200,
|
||||
iterations: 2,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-dialog-container-color: var(--card-background-color);
|
||||
--md-dialog-headline-color: var(--primary-text-color);
|
||||
--md-dialog-supporting-text-color: var(--primary-text-color);
|
||||
--md-sys-color-scrim: #000000;
|
||||
|
||||
--md-dialog-headline-weight: var(--ha-font-weight-normal);
|
||||
--md-dialog-headline-size: var(--ha-font-size-xl);
|
||||
--md-dialog-supporting-text-size: var(--ha-font-size-m);
|
||||
--md-dialog-supporting-text-line-height: var(--ha-line-height-normal);
|
||||
--md-divider-color: var(--divider-color);
|
||||
}
|
||||
|
||||
:host([type="alert"]) {
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host(:not([type="alert"])) {
|
||||
min-width: var(--mdc-dialog-min-width, 100vw);
|
||||
min-height: 100%;
|
||||
max-height: 100%;
|
||||
--md-dialog-container-shape: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: var(--safe-area-inset-top, 0);
|
||||
margin-bottom: var(--safe-area-inset-bottom, 0);
|
||||
margin-left: var(--safe-area-inset-left, 0);
|
||||
margin-right: var(--safe-area-inset-right, 0);
|
||||
}
|
||||
}
|
||||
|
||||
::slotted(ha-dialog-header[slot="headline"]) {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
slot[name="actions"]::slotted(*) {
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.scroller {
|
||||
overflow: var(--dialog-content-overflow, auto);
|
||||
}
|
||||
|
||||
slot[name="content"]::slotted(*) {
|
||||
padding: var(--dialog-content-padding, var(--ha-space-6));
|
||||
}
|
||||
.scrim {
|
||||
z-index: 10; /* overlay navigation */
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
// by default the dialog open/close animation will be from/to the top
|
||||
// but if we have a special mobile dialog which is at the bottom of the screen, a from bottom animation can be used:
|
||||
const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = {
|
||||
...DIALOG_DEFAULT_OPEN_ANIMATION,
|
||||
dialog: [
|
||||
[
|
||||
// Dialog slide up
|
||||
[{ transform: "translateY(50px)" }, { transform: "translateY(0)" }],
|
||||
{ duration: 500, easing: "cubic-bezier(.3,0,0,1)" },
|
||||
],
|
||||
],
|
||||
container: [
|
||||
[
|
||||
// Container fade in
|
||||
[{ opacity: 0 }, { opacity: 1 }],
|
||||
{ duration: 50, easing: "linear", pseudoElement: "::before" },
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const CLOSE_TO_BOTTOM_ANIMATION: DialogAnimation = {
|
||||
...DIALOG_DEFAULT_CLOSE_ANIMATION,
|
||||
dialog: [
|
||||
[
|
||||
// Dialog slide down
|
||||
[{ transform: "translateY(0)" }, { transform: "translateY(50px)" }],
|
||||
{ duration: 150, easing: "cubic-bezier(.3,0,0,1)" },
|
||||
],
|
||||
],
|
||||
container: [
|
||||
[
|
||||
// Container fade out
|
||||
[{ opacity: "1" }, { opacity: "0" }],
|
||||
{ delay: 100, duration: 50, easing: "linear", pseudoElement: "::before" },
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
export const getMobileOpenFromBottomAnimation = () => {
|
||||
const matches = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
return matches ? OPEN_FROM_BOTTOM_ANIMATION : DIALOG_DEFAULT_OPEN_ANIMATION;
|
||||
};
|
||||
|
||||
export const getMobileCloseToBottomAnimation = () => {
|
||||
const matches = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
return matches ? CLOSE_TO_BOTTOM_ANIMATION : DIALOG_DEFAULT_CLOSE_ANIMATION;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-dialog": HaMdDialog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Divider } from "@material/web/divider/internal/divider";
|
||||
import { styles } from "@material/web/divider/internal/divider-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-divider")
|
||||
export class HaMdDivider extends Divider {
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-divider-color: var(--divider-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-divider": HaMdDivider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { MenuItemEl } from "@material/web/menu/internal/menuitem/menu-item";
|
||||
import { styles } from "@material/web/menu/internal/menuitem/menu-item-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-menu-item")
|
||||
export class HaMdMenuItem extends MenuItemEl {
|
||||
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
--md-sys-color-secondary-container: rgba(
|
||||
var(--rgb-primary-color),
|
||||
0.15
|
||||
);
|
||||
--md-sys-color-on-secondary-container: var(--text-primary-color);
|
||||
--mdc-icon-size: 16px;
|
||||
|
||||
--md-sys-color-on-primary-container: var(--primary-text-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
--md-menu-item-label-text-font: Roboto, sans-serif;
|
||||
}
|
||||
:host(.warning) {
|
||||
--md-menu-item-label-text-color: var(--error-color);
|
||||
--md-menu-item-leading-icon-color: var(--error-color);
|
||||
}
|
||||
::slotted([slot="headline"]) {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
:host([disabled]) {
|
||||
opacity: 1;
|
||||
--md-menu-item-label-text-color: var(--disabled-text-color);
|
||||
--md-menu-item-leading-icon-color: var(--disabled-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-menu-item": HaMdMenuItem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Menu } from "@material/web/menu/internal/menu";
|
||||
import { styles } from "@material/web/menu/internal/menu-styles";
|
||||
import type { CloseMenuEvent } from "@material/web/menu/menu";
|
||||
import {
|
||||
CloseReason,
|
||||
KeydownCloseKey,
|
||||
} from "@material/web/menu/internal/controllers/shared";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { HaMdMenuItem } from "./ha-md-menu-item";
|
||||
|
||||
@customElement("ha-md-menu")
|
||||
export class HaMdMenu extends Menu {
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("close-menu", this._handleCloseMenu);
|
||||
}
|
||||
|
||||
private _handleCloseMenu(ev: CloseMenuEvent) {
|
||||
if (
|
||||
ev.detail.reason.kind === CloseReason.KEYDOWN &&
|
||||
ev.detail.reason.key === KeydownCloseKey.ESCAPE
|
||||
) {
|
||||
return;
|
||||
}
|
||||
(ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator);
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-surface-container: var(--card-background-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-menu": HaMdMenu;
|
||||
}
|
||||
|
||||
interface HTMLElementEventMap {
|
||||
"close-menu": CloseMenuEvent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { SelectOptionEl } from "@material/web/select/internal/selectoption/select-option";
|
||||
import { styles } from "@material/web/menu/internal/menuitem/menu-item-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-select-option")
|
||||
export class HaMdSelectOption extends SelectOptionEl {
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-select-option": HaMdSelectOption;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { FilledSelect } from "@material/web/select/internal/filled-select";
|
||||
import { styles as sharedStyles } from "@material/web/select/internal/shared-styles";
|
||||
import { styles } from "@material/web/select/internal/filled-select-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-md-select")
|
||||
export class HaMdSelect extends FilledSelect {
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
|
||||
--md-sys-color-surface-container-highest: var(--input-fill-color);
|
||||
--md-sys-color-on-surface: var(--input-ink-color);
|
||||
|
||||
--md-sys-color-surface-container: var(--input-fill-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
--md-sys-color-secondary-container: var(--input-fill-color);
|
||||
--md-menu-container-color: var(--card-background-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-md-select": HaMdSelect;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { MenuBase } from "@material/mwc-menu/mwc-menu-base";
|
||||
import { styles } from "@material/mwc-menu/mwc-menu.css";
|
||||
import { html } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "./ha-list";
|
||||
|
||||
@customElement("ha-menu")
|
||||
export class HaMenu extends MenuBase {
|
||||
protected get listElement() {
|
||||
if (!this.listElement_) {
|
||||
this.listElement_ = this.renderRoot.querySelector("ha-list");
|
||||
return this.listElement_;
|
||||
}
|
||||
|
||||
return this.listElement_;
|
||||
}
|
||||
|
||||
protected renderList() {
|
||||
const itemRoles = this.innerRole === "menu" ? "menuitem" : "option";
|
||||
const classes = this.renderListClasses();
|
||||
|
||||
return html`<ha-list
|
||||
rootTabbable
|
||||
.innerAriaLabel=${this.innerAriaLabel}
|
||||
.innerRole=${this.innerRole}
|
||||
.multi=${this.multi}
|
||||
class=${classMap(classes)}
|
||||
.itemRoles=${itemRoles}
|
||||
.wrapFocus=${this.wrapFocus}
|
||||
.activatable=${this.activatable}
|
||||
@action=${this.onAction}
|
||||
>
|
||||
<slot></slot>
|
||||
</ha-list>`;
|
||||
}
|
||||
|
||||
static styles = styles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-menu": HaMenu;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { SupervisorMounts } from "../data/supervisor/mounts";
|
||||
import {
|
||||
@@ -14,9 +15,9 @@ import {
|
||||
} from "../data/supervisor/mounts";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const _BACKUP_DATA_DISK_ = "/backup";
|
||||
|
||||
@@ -51,54 +52,60 @@ class HaMountPicker extends LitElement {
|
||||
if (!this._mounts) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const options: HaSelectOption[] = this._filterMounts(
|
||||
this._mounts,
|
||||
this.usage
|
||||
).map((mount) => ({
|
||||
value: mount.name,
|
||||
label: mount.name,
|
||||
secondary: `${mount.server}${mount.port ? `:${mount.port}` : ""}${
|
||||
mount.type === SupervisorMountType.NFS ? mount.path : `:${mount.share}`
|
||||
}`,
|
||||
iconPath:
|
||||
mount.usage === SupervisorMountUsage.MEDIA
|
||||
? mdiPlayBox
|
||||
: mount.usage === SupervisorMountUsage.SHARE
|
||||
? mdiFolder
|
||||
: mdiBackupRestore,
|
||||
}));
|
||||
|
||||
if (this.usage === SupervisorMountUsage.BACKUP) {
|
||||
const dataDiskOption = {
|
||||
value: _BACKUP_DATA_DISK_,
|
||||
iconPath: mdiHarddisk,
|
||||
label:
|
||||
this.hass.localize("ui.components.mount-picker.use_datadisk") ||
|
||||
"Use data disk for backup",
|
||||
};
|
||||
if (
|
||||
!this._mounts.default_backup_mount ||
|
||||
this._mounts.default_backup_mount === _BACKUP_DATA_DISK_
|
||||
) {
|
||||
options.unshift(dataDiskOption);
|
||||
} else {
|
||||
options.push(dataDiskOption);
|
||||
}
|
||||
}
|
||||
|
||||
const dataDiskOption = html`<ha-list-item
|
||||
graphic="icon"
|
||||
.value=${_BACKUP_DATA_DISK_}
|
||||
>
|
||||
<span>
|
||||
${this.hass.localize("ui.components.mount-picker.use_datadisk") ||
|
||||
"Use data disk for backup"}
|
||||
</span>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiHarddisk}></ha-svg-icon>
|
||||
</ha-list-item>`;
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.mount-picker.mount")
|
||||
: this.label}
|
||||
.value=${this.value}
|
||||
.value=${this._value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
@selected=${this._mountChanged}
|
||||
.options=${options}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${this.usage === SupervisorMountUsage.BACKUP &&
|
||||
(!this._mounts.default_backup_mount ||
|
||||
this._mounts.default_backup_mount === _BACKUP_DATA_DISK_)
|
||||
? dataDiskOption
|
||||
: nothing}
|
||||
${this._filterMounts(this._mounts, this.usage).map(
|
||||
(mount) =>
|
||||
html`<ha-list-item twoline graphic="icon" .value=${mount.name}>
|
||||
<span>${mount.name}</span>
|
||||
<span slot="secondary"
|
||||
>${mount.server}${mount.port
|
||||
? `:${mount.port}`
|
||||
: nothing}${mount.type === SupervisorMountType.NFS
|
||||
? mount.path
|
||||
: `:${mount.share}`}</span
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mount.usage === SupervisorMountUsage.MEDIA
|
||||
? mdiPlayBox
|
||||
: mount.usage === SupervisorMountUsage.SHARE
|
||||
? mdiFolder
|
||||
: mdiBackupRestore}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>`
|
||||
)}
|
||||
${this.usage === SupervisorMountUsage.BACKUP &&
|
||||
this._mounts.default_backup_mount
|
||||
? dataDiskOption
|
||||
: nothing}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@@ -146,10 +153,16 @@ class HaMountPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _mountChanged(ev: HaSelectSelectEvent) {
|
||||
const newValue = ev.detail.value;
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
if (newValue !== this.value) {
|
||||
private _mountChanged(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HaSelect;
|
||||
const newValue = target.value;
|
||||
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +262,12 @@ export class HaNavigationPicker extends LitElement {
|
||||
|
||||
const viewConfigs = await Promise.all(
|
||||
lovelacePanels.map((panel) =>
|
||||
fetchConfig(this.hass!.connection, panel.url_path, true)
|
||||
fetchConfig(
|
||||
this.hass!.connection,
|
||||
// path should be null to fetch default lovelace panel
|
||||
panel.url_path === "lovelace" ? null : panel.url_path,
|
||||
true
|
||||
)
|
||||
.then((config) => [panel.id, config] as [string, typeof config])
|
||||
.catch((_) => [panel.id, undefined] as [string, undefined])
|
||||
)
|
||||
|
||||
@@ -55,7 +55,6 @@ export interface PickerComboBoxItem {
|
||||
sorting_label?: string;
|
||||
icon_path?: string;
|
||||
icon?: string;
|
||||
isRelated?: boolean;
|
||||
}
|
||||
|
||||
export interface PickerComboBoxIndexSelectedDetail {
|
||||
|
||||
@@ -135,7 +135,9 @@ class HaQrScanner extends LitElement {
|
||||
(camera) => html`
|
||||
<ha-dropdown-item
|
||||
.value=${camera.id}
|
||||
.selected=${this._selectedCamera === camera.id}
|
||||
class=${this._selectedCamera === camera.id
|
||||
? "selected"
|
||||
: ""}
|
||||
>
|
||||
${camera.label}
|
||||
</ha-dropdown-item>
|
||||
@@ -378,6 +380,9 @@ class HaQrScanner extends LitElement {
|
||||
color: white;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
}
|
||||
ha-dropdown-item.selected {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -61,7 +61,6 @@ class HaSegmentedBar extends LitElement {
|
||||
: html`
|
||||
<ha-tooltip for="segment-${index}" placement="top">
|
||||
${segment.label}
|
||||
(${((segment.value / totalValue) * 100).toFixed(1)}%)
|
||||
</ha-tooltip>
|
||||
`}
|
||||
<div
|
||||
|
||||
+158
-178
@@ -1,207 +1,187 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-picker-field";
|
||||
import type { HaPickerField } from "./ha-picker-field";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
export interface HaSelectOption {
|
||||
value: string;
|
||||
label?: string;
|
||||
secondary?: string;
|
||||
iconPath?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event type for the ha-select component when an item is selected.
|
||||
* @param T - The type of the value of the selected item.
|
||||
* @param Clearable - Whether the select is clearable (allows undefined values).
|
||||
*/
|
||||
export type HaSelectSelectEvent<
|
||||
T = string,
|
||||
Clearable extends boolean = false,
|
||||
> = CustomEvent<{
|
||||
value: Clearable extends true ? T | undefined : T;
|
||||
}>;
|
||||
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
||||
import { styles } from "@material/mwc-select/mwc-select.css";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-menu";
|
||||
|
||||
@customElement("ha-select")
|
||||
export class HaSelect extends LitElement {
|
||||
@property({ type: Boolean }) public clearable = false;
|
||||
export class HaSelect extends SelectBase {
|
||||
// @ts-ignore
|
||||
@property({ type: Boolean }) public icon = false;
|
||||
|
||||
@property({ attribute: false }) public options?: HaSelectOption[] | string[];
|
||||
@property({ type: Boolean, reflect: true }) public clearable = false;
|
||||
|
||||
@property() public label?: string;
|
||||
@property({ attribute: "inline-arrow", type: Boolean })
|
||||
public inlineArrow = false;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@query("ha-picker-field") private _triggerField!: HaPickerField;
|
||||
|
||||
private _getValueLabel = memoizeOne(
|
||||
(
|
||||
options: HaSelectOption[] | string[] | undefined,
|
||||
value: string | undefined
|
||||
) => {
|
||||
if (!options || !value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
for (const option of options) {
|
||||
if (
|
||||
(typeof option === "string" && option === value) ||
|
||||
(typeof option !== "string" && option.value === value)
|
||||
) {
|
||||
return typeof option === "string"
|
||||
? option
|
||||
: option.label || option.value;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
);
|
||||
@property() public options;
|
||||
|
||||
protected override render() {
|
||||
if (this.disabled) {
|
||||
return this._renderField();
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dropdown
|
||||
placement="bottom"
|
||||
@wa-select=${this._handleSelect}
|
||||
@wa-show=${this._handleShow}
|
||||
@wa-hide=${this._handleHide}
|
||||
>
|
||||
${this._renderField()}
|
||||
${this.options
|
||||
? this.options.map(
|
||||
(option) => html`
|
||||
<ha-dropdown-item
|
||||
.value=${typeof option === "string" ? option : option.value}
|
||||
.disabled=${typeof option === "string"
|
||||
? false
|
||||
: (option.disabled ?? false)}
|
||||
.selected=${this.value ===
|
||||
(typeof option === "string" ? option : option.value)}
|
||||
>
|
||||
${option.iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${option.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
<div class="content">
|
||||
${typeof option === "string"
|
||||
? option
|
||||
: option.label || option.value}
|
||||
${option.secondary
|
||||
? html`<div class="secondary">${option.secondary}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)
|
||||
: html`<slot></slot>`}
|
||||
</ha-dropdown>
|
||||
${super.render()}
|
||||
${this.clearable && !this.required && !this.disabled && this.value
|
||||
? html`<ha-icon-button
|
||||
label="clear"
|
||||
@click=${this._clearValue}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderField() {
|
||||
const valueLabel = this._getValueLabel(this.options, this.value);
|
||||
|
||||
return html`
|
||||
<ha-picker-field
|
||||
slot="trigger"
|
||||
type="button"
|
||||
class=${this._opened ? "opened" : ""}
|
||||
compact
|
||||
aria-label=${ifDefined(this.label)}
|
||||
@clear=${this._clearValue}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.value=${valueLabel}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.hideClearIcon=${!this.clearable ||
|
||||
this.required ||
|
||||
this.disabled ||
|
||||
!this.value}
|
||||
>
|
||||
</ha-picker-field>
|
||||
`;
|
||||
protected override renderMenu() {
|
||||
const classes = this.getMenuClasses();
|
||||
return html`<ha-menu
|
||||
innerRole="listbox"
|
||||
wrapFocus
|
||||
class=${classMap(classes)}
|
||||
activatable
|
||||
.fullwidth=${this.fixedMenuPosition ? false : !this.naturalMenuWidth}
|
||||
.open=${this.menuOpen}
|
||||
.anchor=${this.anchorElement}
|
||||
.fixed=${this.fixedMenuPosition}
|
||||
@selected=${this.onSelected}
|
||||
@opened=${this.onOpened}
|
||||
@closed=${this.onClosed}
|
||||
@items-updated=${this.onItemsUpdated}
|
||||
@keydown=${this.handleTypeahead}
|
||||
>
|
||||
${this.renderMenuContent()}
|
||||
</ha-menu>`;
|
||||
}
|
||||
|
||||
private _handleSelect(ev: CustomEvent<{ item: { value: string } }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.item.value;
|
||||
if (value === this.value) {
|
||||
return;
|
||||
protected override renderLeadingIcon() {
|
||||
if (!this.icon) {
|
||||
return nothing;
|
||||
}
|
||||
fireEvent(this, "selected", { value });
|
||||
|
||||
return html`<span class="mdc-select__icon"
|
||||
><slot name="icon"></slot
|
||||
></span>`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("translations-updated", this._translationsUpdated);
|
||||
}
|
||||
|
||||
protected async firstUpdated() {
|
||||
super.firstUpdated();
|
||||
|
||||
if (this.inlineArrow) {
|
||||
this.shadowRoot
|
||||
?.querySelector(".mdc-select__selected-text-container")
|
||||
?.classList.add("inline-arrow");
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("inlineArrow")) {
|
||||
const textContainerElement = this.shadowRoot?.querySelector(
|
||||
".mdc-select__selected-text-container"
|
||||
);
|
||||
if (this.inlineArrow) {
|
||||
textContainerElement?.classList.add("inline-arrow");
|
||||
} else {
|
||||
textContainerElement?.classList.remove("inline-arrow");
|
||||
}
|
||||
}
|
||||
if (changedProperties.get("options")) {
|
||||
this.layoutOptions();
|
||||
this.selectByValue(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener(
|
||||
"translations-updated",
|
||||
this._translationsUpdated
|
||||
);
|
||||
}
|
||||
|
||||
private _clearValue(): void {
|
||||
if (this.disabled || !this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "selected", { value: undefined });
|
||||
this.valueSetDirectly = true;
|
||||
this.select(-1);
|
||||
this.mdcFoundation.handleChange();
|
||||
}
|
||||
|
||||
private _handleShow() {
|
||||
this.style.setProperty(
|
||||
"--select-menu-width",
|
||||
`${this._triggerField.offsetWidth}px`
|
||||
);
|
||||
this._opened = true;
|
||||
}
|
||||
private _translationsUpdated = debounce(async () => {
|
||||
await nextRender();
|
||||
this.layoutOptions();
|
||||
}, 500);
|
||||
|
||||
private _handleHide() {
|
||||
this._opened = false;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
ha-picker-field.opened {
|
||||
--mdc-text-field-idle-line-color: var(--primary-color);
|
||||
}
|
||||
ha-dropdown-item .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);
|
||||
}
|
||||
`;
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
:host([clearable]) {
|
||||
position: relative;
|
||||
}
|
||||
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.mdc-select__anchor {
|
||||
width: var(--ha-select-min-width, 200px);
|
||||
}
|
||||
.mdc-select--filled .mdc-select__anchor {
|
||||
height: var(--ha-select-height, 56px);
|
||||
}
|
||||
.mdc-select--filled .mdc-floating-label {
|
||||
inset-inline-start: var(--ha-space-4);
|
||||
inset-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label {
|
||||
inset-inline-start: 48px;
|
||||
inset-inline-end: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-select .mdc-select__anchor {
|
||||
padding-inline-start: var(--ha-space-4);
|
||||
padding-inline-end: 0px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-select__anchor .mdc-floating-label--float-above {
|
||||
transform-origin: var(--float-start);
|
||||
}
|
||||
.mdc-select__selected-text-container {
|
||||
padding-inline-end: var(--select-selected-text-padding-end, 0px);
|
||||
}
|
||||
:host([clearable]) .mdc-select__selected-text-container {
|
||||
padding-inline-end: var(
|
||||
--select-selected-text-padding-end,
|
||||
var(--ha-space-4)
|
||||
);
|
||||
}
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 28px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 28px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.inline-arrow {
|
||||
flex-grow: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-select": HaSelect;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
selected: { value: string | undefined };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,17 @@ import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import type { SelectOption, SelectSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-checkbox";
|
||||
import "../ha-dropdown-item";
|
||||
import "../ha-formfield";
|
||||
import "../ha-generic-picker";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-list-item";
|
||||
import "../ha-radio";
|
||||
import "../ha-select";
|
||||
import "../ha-select-box";
|
||||
@@ -230,15 +231,24 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
.label=${this.label ?? ""}
|
||||
.value=${(this.value as string) ?? ""}
|
||||
.value=${this.value ?? ""}
|
||||
.helper=${this.helper ?? ""}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
clearable
|
||||
@closed=${stopPropagation}
|
||||
@selected=${this._valueChanged}
|
||||
.options=${options}
|
||||
>
|
||||
${options.map(
|
||||
(item: SelectOption) => html`
|
||||
<ha-list-item .value=${item.value} .disabled=${!!item.disabled}
|
||||
>${item.label}</ha-list-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@@ -285,7 +295,7 @@ export class HaSelectSelector extends LitElement {
|
||||
private _valueChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (ev.detail?.value === undefined && this.value !== undefined) {
|
||||
if (ev.detail?.index === -1 && this.value !== undefined) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: undefined,
|
||||
});
|
||||
@@ -375,7 +385,7 @@ export class HaSelectSelector extends LitElement {
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
ha-dropdown-item[disabled] {
|
||||
ha-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
ha-chip-set {
|
||||
|
||||
@@ -108,7 +108,6 @@ export class HaSettingsRow extends LitElement {
|
||||
white-space: normal;
|
||||
}
|
||||
.prefix-wrap {
|
||||
flex: 1;
|
||||
display: var(--settings-row-prefix-display);
|
||||
}
|
||||
:host([narrow]) .prefix-wrap {
|
||||
|
||||
@@ -492,22 +492,19 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
|
||||
${this._updatesCount > 0 || this._issuesCount > 0
|
||||
? html`
|
||||
<span class="badge" slot="start">
|
||||
${this._updatesCount + this._issuesCount}
|
||||
</span>
|
||||
`
|
||||
${!this.alwaysExpand &&
|
||||
(this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`<span class="badge" slot="start"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>`
|
||||
: nothing}
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("panel.config")}</span
|
||||
>
|
||||
${this._updatesCount > 0 || this._issuesCount > 0
|
||||
? html`
|
||||
<span class="badge" slot="end"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>
|
||||
`
|
||||
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`<span class="badge" slot="end"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
@@ -527,15 +524,13 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
type="button"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiBell}></ha-svg-icon>
|
||||
${notificationCount > 0
|
||||
? html`
|
||||
<span class="badge" slot="start"> ${notificationCount} </span>
|
||||
`
|
||||
${!this.alwaysExpand && notificationCount > 0
|
||||
? html`<span class="badge" slot="start">${notificationCount}</span>`
|
||||
: nothing}
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("ui.notification_drawer.title")}</span
|
||||
>
|
||||
${notificationCount > 0
|
||||
${this.alwaysExpand && notificationCount > 0
|
||||
? html`<span class="badge" slot="end">${notificationCount}</span>`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
@@ -744,8 +739,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
);
|
||||
font-size: var(--ha-font-size-xl);
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
width: calc(56px + var(--safe-area-inset-left, 0px));
|
||||
padding-left: calc(
|
||||
var(--ha-space-1) + var(--safe-area-inset-left, 0px)
|
||||
);
|
||||
@@ -754,7 +747,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
);
|
||||
padding-inline-end: initial;
|
||||
padding-top: var(--safe-area-inset-top, 0px);
|
||||
transition: width var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([expanded]) .menu {
|
||||
width: calc(256px + var(--safe-area-inset-left, 0px));
|
||||
@@ -769,22 +761,15 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
margin-left: 3px;
|
||||
margin-inline-start: 3px;
|
||||
margin-inline-end: initial;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
transition:
|
||||
max-width var(--ha-animation-duration-normal) ease,
|
||||
opacity var(--ha-animation-duration-normal) ease;
|
||||
width: 100%;
|
||||
display: none;
|
||||
}
|
||||
:host([narrow]) .title {
|
||||
margin: 0;
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
:host([expanded]) .title {
|
||||
max-width: 100%;
|
||||
opacity: 1;
|
||||
transition-delay: 0ms, 80ms;
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.panels-list {
|
||||
@@ -842,7 +827,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
--md-list-item-leading-space: var(--ha-space-3);
|
||||
--md-list-item-trailing-space: var(--ha-space-3);
|
||||
--md-list-item-leading-icon-size: var(--ha-space-6);
|
||||
transition: width var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([expanded]) ha-md-list-item {
|
||||
width: 248px;
|
||||
@@ -883,22 +867,11 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
}
|
||||
|
||||
ha-md-list-item .item-text {
|
||||
display: block;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: none;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
transition:
|
||||
max-width var(--ha-animation-duration-normal) ease,
|
||||
opacity var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([expanded]) ha-md-list-item .item-text {
|
||||
max-width: 100%;
|
||||
opacity: 1;
|
||||
transition-delay: 0ms, 80ms;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -916,9 +889,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
background-color: var(--accent-color);
|
||||
padding: 2px 6px;
|
||||
color: var(--text-accent-color, var(--text-primary-color));
|
||||
transition:
|
||||
opacity var(--ha-animation-duration-normal) ease,
|
||||
transform var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
|
||||
ha-svg-icon + .badge {
|
||||
@@ -930,12 +900,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
padding: 0 var(--ha-space-1);
|
||||
}
|
||||
:host([expanded]) .badge[slot="start"],
|
||||
:host(:not([expanded])) .badge[slot="end"] {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ha-md-list-item.user {
|
||||
--md-list-item-leading-icon-size: var(--ha-space-10);
|
||||
@@ -974,15 +938,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
-webkit-transform: scaleX(var(--scale-direction));
|
||||
transform: scaleX(var(--scale-direction));
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.menu,
|
||||
ha-md-list-item,
|
||||
ha-md-list-item .item-text,
|
||||
.title {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,14 +2,16 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { STTEngine } from "../data/stt";
|
||||
import { listSTTEngines } from "../data/stt";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@@ -59,30 +61,6 @@ export class HaSTTPicker extends LitElement {
|
||||
value = NONE;
|
||||
}
|
||||
|
||||
const options: HaSelectOption[] = this._engines
|
||||
.filter((engine) => !engine.deprecated || engine.engine_id !== value)
|
||||
.map((engine) => {
|
||||
let label: string;
|
||||
if (engine.engine_id.includes(".")) {
|
||||
const stateObj = this.hass.states[engine.engine_id];
|
||||
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
||||
} else {
|
||||
label = engine.name || engine.engine_id;
|
||||
}
|
||||
return {
|
||||
value: engine.engine_id,
|
||||
label,
|
||||
disabled: engine.supported_languages?.length === 0,
|
||||
};
|
||||
});
|
||||
|
||||
if (this.required || value === NONE) {
|
||||
options.unshift({
|
||||
value: NONE,
|
||||
label: this.hass.localize("ui.components.stt-picker.none") || "None",
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
@@ -91,8 +69,33 @@ export class HaSTTPicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
.options=${options}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${!this.required
|
||||
? html`<ha-list-item .value=${NONE}>
|
||||
${this.hass!.localize("ui.components.stt-picker.none")}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
${this._engines.map((engine) => {
|
||||
if (engine.deprecated && engine.engine_id !== value) {
|
||||
return nothing;
|
||||
}
|
||||
let label: string;
|
||||
if (engine.engine_id.includes(".")) {
|
||||
const stateObj = this.hass!.states[engine.engine_id];
|
||||
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
||||
} else {
|
||||
label = engine.name || engine.engine_id;
|
||||
}
|
||||
return html`<ha-list-item
|
||||
.value=${engine.engine_id}
|
||||
.disabled=${engine.supported_languages?.length === 0}
|
||||
>
|
||||
${label}
|
||||
</ha-list-item>`;
|
||||
})}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@@ -141,17 +144,17 @@ export class HaSTTPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
if (
|
||||
!this.hass ||
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = value === NONE ? undefined : value;
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._engines!.find((engine) => engine.engine_id === this.value)
|
||||
|
||||
@@ -53,7 +53,7 @@ import {
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
} from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-generic-picker";
|
||||
@@ -403,7 +403,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _targetPicked(ev: ValueChangedEvent<string>) {
|
||||
private _targetPicked(ev: CustomEvent<{ value: string }>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
if (value.startsWith(CREATE_ID)) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, nothing, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-select";
|
||||
import "./ha-list-item";
|
||||
|
||||
const DEFAULT_THEME = "default";
|
||||
|
||||
@@ -24,26 +25,6 @@ export class HaThemePicker extends LitElement {
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const options: HaSelectOption[] = Object.keys(
|
||||
this.hass?.themes.themes || {}
|
||||
).map((theme) => ({
|
||||
value: theme,
|
||||
}));
|
||||
|
||||
if (this.includeDefault) {
|
||||
options.unshift({
|
||||
value: DEFAULT_THEME,
|
||||
label: "Home Assistant",
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.required) {
|
||||
options.unshift({
|
||||
value: "remove",
|
||||
label: this.hass!.localize("ui.components.theme-picker.no_theme"),
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
@@ -52,8 +33,31 @@ export class HaThemePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
.options=${options}
|
||||
></ha-select>
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${!this.required
|
||||
? html`
|
||||
<ha-list-item value="remove">
|
||||
${this.hass!.localize("ui.components.theme-picker.no_theme")}
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${this.includeDefault
|
||||
? html`
|
||||
<ha-list-item .value=${DEFAULT_THEME}>
|
||||
Home Assistant
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${Object.keys(this.hass!.themes.themes)
|
||||
.sort()
|
||||
.map(
|
||||
(theme) =>
|
||||
html`<ha-list-item .value=${theme}>${theme}</ha-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -63,11 +67,11 @@ export class HaThemePicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
if (!this.hass || ev.detail.value === "") {
|
||||
private _changed(ev): void {
|
||||
if (!this.hass || ev.target.value === "") {
|
||||
return;
|
||||
}
|
||||
this.value = ev.detail.value === "remove" ? undefined : ev.detail.value;
|
||||
this.value = ev.target.value === "remove" ? undefined : ev.target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { FrontendLocaleData } from "../data/translation";
|
||||
import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
|
||||
@customElement("ha-time-input")
|
||||
export class HaTimeInput extends LitElement {
|
||||
@@ -70,7 +69,7 @@ export class HaTimeInput extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _timeChanged(ev: ValueChangedEvent<TimeChangedEvent | undefined>) {
|
||||
private _timeChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
|
||||
ev.stopPropagation();
|
||||
const eventValue = ev.detail.value;
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const haTopAppBarFixedSharedStyles = css`
|
||||
header {
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
}
|
||||
|
||||
.mdc-top-app-bar__row {
|
||||
height: var(--header-height);
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
}
|
||||
|
||||
.mdc-top-app-bar--fixed-adjust {
|
||||
padding-top: calc(
|
||||
var(--header-height, 0px) + var(--safe-area-inset-top, 0px)
|
||||
);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
|
||||
:host([narrow]) .mdc-top-app-bar--fixed-adjust {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
|
||||
.mdc-top-app-bar {
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--app-header-text-color, var(--mdc-theme-on-primary, #fff));
|
||||
background-color: var(
|
||||
--app-header-background-color,
|
||||
var(--mdc-theme-primary)
|
||||
);
|
||||
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -2,7 +2,6 @@ import { TopAppBarFixedBase } from "@material/mwc-top-app-bar-fixed/mwc-top-app-
|
||||
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { haTopAppBarFixedSharedStyles } from "./ha-top-app-bar-fixed-shared-styles";
|
||||
|
||||
@customElement("ha-top-app-bar-fixed")
|
||||
export class HaTopAppBarFixed extends TopAppBarFixedBase {
|
||||
@@ -10,8 +9,37 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
haTopAppBarFixedSharedStyles,
|
||||
css`
|
||||
header {
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
}
|
||||
.mdc-top-app-bar__row {
|
||||
height: var(--header-height);
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
}
|
||||
.mdc-top-app-bar--fixed-adjust {
|
||||
padding-top: calc(
|
||||
var(--header-height, 0px) + var(--safe-area-inset-top, 0px)
|
||||
);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar--fixed-adjust {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
.mdc-top-app-bar {
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--app-header-text-color, var(--mdc-theme-on-primary, #fff));
|
||||
background-color: var(
|
||||
--app-header-background-color,
|
||||
var(--mdc-theme-primary)
|
||||
);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
.mdc-top-app-bar__title {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
padding-inline-start: var(--ha-space-6);
|
||||
|
||||
@@ -2,14 +2,16 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { TTSEngine } from "../data/tts";
|
||||
import { listTTSEngines } from "../data/tts";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@@ -59,30 +61,6 @@ export class HaTTSPicker extends LitElement {
|
||||
value = NONE;
|
||||
}
|
||||
|
||||
const options: HaSelectOption[] = this._engines
|
||||
.filter((engine) => !engine.deprecated || engine.engine_id === value)
|
||||
.map((engine) => {
|
||||
let label: string;
|
||||
if (engine.engine_id.includes(".")) {
|
||||
const stateObj = this.hass.states[engine.engine_id];
|
||||
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
||||
} else {
|
||||
label = engine.name || engine.engine_id;
|
||||
}
|
||||
return {
|
||||
value: engine.engine_id,
|
||||
label,
|
||||
disabled: engine.supported_languages?.length === 0,
|
||||
};
|
||||
});
|
||||
|
||||
if (!this.required || value === NONE) {
|
||||
options.unshift({
|
||||
value: NONE,
|
||||
label: this.hass.localize("ui.components.tts-picker.none"),
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
@@ -91,8 +69,33 @@ export class HaTTSPicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
.options=${options}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${!this.required
|
||||
? html`<ha-list-item .value=${NONE}>
|
||||
${this.hass!.localize("ui.components.tts-picker.none")}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
${this._engines.map((engine) => {
|
||||
if (engine.deprecated && engine.engine_id !== value) {
|
||||
return nothing;
|
||||
}
|
||||
let label: string;
|
||||
if (engine.engine_id.includes(".")) {
|
||||
const stateObj = this.hass!.states[engine.engine_id];
|
||||
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
||||
} else {
|
||||
label = engine.name || engine.engine_id;
|
||||
}
|
||||
return html`<ha-list-item
|
||||
.value=${engine.engine_id}
|
||||
.disabled=${engine.supported_languages?.length === 0}
|
||||
>
|
||||
${label}
|
||||
</ha-list-item>`;
|
||||
})}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@@ -141,17 +144,17 @@ export class HaTTSPicker extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
if (
|
||||
!this.hass ||
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = value === NONE ? undefined : value;
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
fireEvent(this, "supported-languages-changed", {
|
||||
value: this._engines!.find((engine) => engine.engine_id === this.value)
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { TTSVoice } from "../data/tts";
|
||||
import { listTTSVoices } from "../data/tts";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
@@ -29,25 +31,14 @@ export class HaTTSVoicePicker extends LitElement {
|
||||
|
||||
@state() _voices?: TTSVoice[] | null;
|
||||
|
||||
@query("ha-select") private _select?: HaSelect;
|
||||
|
||||
protected render() {
|
||||
if (!this._voices) {
|
||||
return nothing;
|
||||
}
|
||||
const value =
|
||||
this.value ?? (this.required ? this._voices[0]?.voice_id : NONE);
|
||||
|
||||
const options: HaSelectOption[] = (this._voices || []).map((voice) => ({
|
||||
value: voice.voice_id,
|
||||
label: voice.name,
|
||||
}));
|
||||
|
||||
if (!this.required || !this.value) {
|
||||
options.unshift({
|
||||
value: NONE,
|
||||
label: this.hass!.localize("ui.components.tts-voice-picker.none"),
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
@@ -56,8 +47,21 @@ export class HaTTSVoicePicker extends LitElement {
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
@selected=${this._changed}
|
||||
.options=${options}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
>
|
||||
${!this.required
|
||||
? html`<ha-list-item .value=${NONE}>
|
||||
${this.hass!.localize("ui.components.tts-voice-picker.none")}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
${this._voices.map(
|
||||
(voice) =>
|
||||
html`<ha-list-item .value=${voice.voice_id}>
|
||||
${voice.name}
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
@@ -98,25 +102,34 @@ export class HaTTSVoicePicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has("_voices") &&
|
||||
this._select?.value !== this.value
|
||||
) {
|
||||
this._select?.layoutOptions();
|
||||
fireEvent(this, "value-changed", { value: this._select?.value });
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-select {
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
private _changed(ev: HaSelectSelectEvent): void {
|
||||
const value = ev.detail.value;
|
||||
private _changed(ev): void {
|
||||
const target = ev.target as HaSelect;
|
||||
if (
|
||||
!this.hass ||
|
||||
value === "" ||
|
||||
value === this.value ||
|
||||
(this.value === undefined && value === NONE)
|
||||
target.value === "" ||
|
||||
target.value === this.value ||
|
||||
(this.value === undefined && target.value === NONE)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.value = value === NONE ? undefined : value;
|
||||
this.value = target.value === NONE ? undefined : target.value;
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import { property, query, customElement } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { haTopAppBarFixedSharedStyles } from "./ha-top-app-bar-fixed-shared-styles";
|
||||
|
||||
export const passiveEventOptionsIfSupported = supportsPassiveEventListener
|
||||
? { passive: true }
|
||||
@@ -246,8 +245,24 @@ export class TopAppBarBaseBase extends BaseElement {
|
||||
static override styles = [
|
||||
styles,
|
||||
haStyleScrollbar,
|
||||
haTopAppBarFixedSharedStyles,
|
||||
css`
|
||||
header {
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
}
|
||||
.mdc-top-app-bar__row {
|
||||
height: var(--header-height);
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
}
|
||||
.mdc-top-app-bar--fixed-adjust {
|
||||
padding-top: calc(
|
||||
var(--header-height, 0px) + var(--safe-area-inset-top, 0px)
|
||||
);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar--fixed-adjust {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
.shadow-container {
|
||||
position: absolute;
|
||||
top: calc(-1 * var(--header-height));
|
||||
@@ -264,6 +279,19 @@ export class TopAppBarBaseBase extends BaseElement {
|
||||
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
|
||||
);
|
||||
}
|
||||
.mdc-top-app-bar {
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--app-header-text-color, var(--mdc-theme-on-primary, #fff));
|
||||
background-color: var(
|
||||
--app-header-background-color,
|
||||
var(--mdc-theme-primary)
|
||||
);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@@ -75,7 +74,7 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
*/
|
||||
@customElement("ha-wa-dialog")
|
||||
export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "aria-labelledby" })
|
||||
public ariaLabelledBy?: string;
|
||||
@@ -198,21 +197,22 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (this.hass && isIosApp(this.hass)) {
|
||||
const element = this.querySelector("[autofocus]");
|
||||
if (element !== null) {
|
||||
if (!element.id) {
|
||||
element.id = "ha-wa-dialog-autofocus";
|
||||
}
|
||||
this.hass?.auth.external?.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: element.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
// temporary disabled because of issues with focus in iOS app, can be reenabled in 2026.2.0
|
||||
// if (isIosApp(this.hass)) {
|
||||
// const element = this.querySelector("[autofocus]");
|
||||
// if (element !== null) {
|
||||
// if (!element.id) {
|
||||
// element.id = "ha-wa-dialog-autofocus";
|
||||
// }
|
||||
// this.hass.auth.external!.fireMessage({
|
||||
// type: "focus_element",
|
||||
// payload: {
|
||||
// element_id: element.id,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import { extractApiErrorMessage } from "../../data/hassio/common";
|
||||
@@ -17,9 +19,8 @@ import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-alert";
|
||||
import "../ha-button";
|
||||
import "../ha-dialog-footer";
|
||||
import "../ha-dialog";
|
||||
import "../ha-dialog-header";
|
||||
import "../ha-wa-dialog";
|
||||
import "./ha-media-player-toggle";
|
||||
import type { JoinMediaPlayersDialogParams } from "./show-join-media-players-dialog";
|
||||
|
||||
@@ -37,11 +38,8 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(params: JoinMediaPlayersDialogParams): void {
|
||||
this._entityId = params.entityId;
|
||||
this._open = true;
|
||||
|
||||
const stateObj = this.hass.states[params.entityId] as
|
||||
| MediaPlayerEntity
|
||||
@@ -56,11 +54,6 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._entityId = undefined;
|
||||
this._selectedEntities = [];
|
||||
this._groupMembers = [];
|
||||
@@ -75,18 +68,23 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
}
|
||||
|
||||
const entityId = this._entityId;
|
||||
const stateObj = this.hass.states[entityId] as HassEntity | undefined;
|
||||
const name = (stateObj && computeStateName(stateObj)) || entityId;
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
flexcontent
|
||||
@closed=${this._dialogClosed}
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
flexContent
|
||||
.heading=${name}
|
||||
@closed=${this.closeDialog}
|
||||
>
|
||||
<ha-dialog-header show-border slot="header">
|
||||
<ha-dialog-header show-border slot="heading">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
data-dialog="close"
|
||||
dialogAction="close"
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button>
|
||||
<span slot="title"
|
||||
@@ -120,23 +118,21 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
></ha-media-player-toggle>`
|
||||
)}
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${!!this._submitting}
|
||||
slot="primaryAction"
|
||||
@click=${this._submit}
|
||||
>
|
||||
${this.hass.localize("ui.common.apply")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
.disabled=${!!this._submitting}
|
||||
slot="primaryAction"
|
||||
@click=${this._submit}
|
||||
>
|
||||
${this.hass.localize("ui.common.apply")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -221,7 +217,6 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: var(--ha-space-6);
|
||||
row-gap: var(--ha-space-4);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ import "../ha-icon-button";
|
||||
import "./hat-logbook-note";
|
||||
import type { NodeInfo } from "./hat-script-graph";
|
||||
import { traceTabStyles } from "./trace-tab-styles";
|
||||
import type { Trigger } from "../../data/automation";
|
||||
import { migrateAutomationTrigger } from "../../data/automation";
|
||||
|
||||
const TRACE_PATH_TABS = [
|
||||
"step_config",
|
||||
@@ -168,9 +166,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
: selectedType === "trigger"
|
||||
? html`<h2>
|
||||
${describeTrigger(
|
||||
migrateAutomationTrigger({
|
||||
...currentDetail,
|
||||
}) as Trigger,
|
||||
currentDetail,
|
||||
this.hass,
|
||||
this._entityReg
|
||||
)}
|
||||
|
||||
@@ -32,13 +32,12 @@ export class VoiceAssistantBrandicon extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
display: inline;
|
||||
}
|
||||
.logo {
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
height: 24px;
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
+20
-5
@@ -14,7 +14,6 @@ import {
|
||||
import type { Collection, HassEntity } from "home-assistant-js-websocket";
|
||||
import { getCollection } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { normalizeValueBySIPrefix } from "../common/number/normalize-by-si-prefix";
|
||||
import {
|
||||
calcDate,
|
||||
calcDateProperty,
|
||||
@@ -1432,10 +1431,26 @@ export const getPowerFromState = (stateObj: HassEntity): number | undefined => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return normalizeValueBySIPrefix(
|
||||
value,
|
||||
stateObj.attributes.unit_of_measurement
|
||||
);
|
||||
// Normalize to watts (W) based on unit of measurement (case-sensitive)
|
||||
// Supported units: GW, kW, MW, mW, TW, W
|
||||
const unit = stateObj.attributes.unit_of_measurement;
|
||||
switch (unit) {
|
||||
case "W":
|
||||
return value;
|
||||
case "kW":
|
||||
return value * 1000;
|
||||
case "mW":
|
||||
return value / 1000;
|
||||
case "MW":
|
||||
return value * 1_000_000;
|
||||
case "GW":
|
||||
return value * 1_000_000_000;
|
||||
case "TW":
|
||||
return value * 1_000_000_000_000;
|
||||
default:
|
||||
// Assume value is in watts (W) if no unit or an unsupported unit is provided
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+1
-2
@@ -142,7 +142,7 @@ export const subscribeHistory = (
|
||||
);
|
||||
};
|
||||
|
||||
export class HistoryStream {
|
||||
class HistoryStream {
|
||||
hass: HomeAssistant;
|
||||
|
||||
hoursToShow?: number;
|
||||
@@ -221,7 +221,6 @@ export class HistoryStream {
|
||||
// only expire the rest of the history as it ages.
|
||||
const lastExpiredState = expiredStates[expiredStates.length - 1];
|
||||
lastExpiredState.lu = purgeBeforePythonTime;
|
||||
delete lastExpiredState.lc;
|
||||
newHistory[entityId].unshift(lastExpiredState);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-6
@@ -41,16 +41,12 @@ export const enum TodoListEntityFeature {
|
||||
SET_DESCRIPTION_ON_ITEM = 64,
|
||||
}
|
||||
|
||||
export const getTodoLists = (
|
||||
hass: HomeAssistant,
|
||||
includeHidden = true
|
||||
): TodoList[] =>
|
||||
export const getTodoLists = (hass: HomeAssistant): TodoList[] =>
|
||||
Object.keys(hass.states)
|
||||
.filter(
|
||||
(entityId) =>
|
||||
computeDomain(entityId) === "todo" &&
|
||||
!isUnavailableState(hass.states[entityId].state) &&
|
||||
(includeHidden || hass.entities[entityId]?.hidden !== true)
|
||||
!isUnavailableState(hass.states[entityId].state)
|
||||
)
|
||||
.map((entityId) => ({
|
||||
...hass.states[entityId],
|
||||
|
||||
@@ -6,7 +6,8 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-wa-dialog";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-dialog-header";
|
||||
import "../../components/ha-icon-button";
|
||||
import type { DataEntryFlowStep } from "../../data/data_entry_flow";
|
||||
import {
|
||||
@@ -63,8 +64,6 @@ class DataEntryFlowDialog extends LitElement {
|
||||
|
||||
private _instance = instance;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _step:
|
||||
| DataEntryFlowStep
|
||||
| undefined
|
||||
@@ -78,7 +77,6 @@ class DataEntryFlowDialog extends LitElement {
|
||||
public async showDialog(params: DataEntryFlowDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._instance = instance++;
|
||||
this._open = true;
|
||||
|
||||
const curInstance = this._instance;
|
||||
let step: DataEntryFlowStep;
|
||||
@@ -148,17 +146,6 @@ class DataEntryFlowDialog extends LitElement {
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
if (!this._open) {
|
||||
this._dialogClosed();
|
||||
return;
|
||||
}
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
@@ -187,7 +174,6 @@ class DataEntryFlowDialog extends LitElement {
|
||||
this._unsubDataEntryFlowProgress();
|
||||
this._unsubDataEntryFlowProgress = undefined;
|
||||
}
|
||||
this._open = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -303,53 +289,56 @@ class DataEntryFlowDialog extends LitElement {
|
||||
const dialogSubtitle = this._getDialogSubtitle();
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
prevent-scrim-close
|
||||
@after-show=${this._focusFormStep}
|
||||
@closed=${this._dialogClosed}
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${dialogTitle || true}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="headerNavigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
data-dialog="close"
|
||||
></ha-icon-button>
|
||||
<ha-dialog-header slot="heading">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
dialogAction="close"
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button>
|
||||
|
||||
<div
|
||||
slot="headerTitle"
|
||||
class="dialog-title${this._step?.type === "form" ? " form" : ""}"
|
||||
title=${dialogTitle}
|
||||
>
|
||||
${dialogTitle}
|
||||
</div>
|
||||
<div
|
||||
slot="title"
|
||||
class="dialog-title${this._step?.type === "form" ? " form" : ""}"
|
||||
title=${dialogTitle}
|
||||
>
|
||||
${dialogTitle}
|
||||
</div>
|
||||
|
||||
${dialogSubtitle
|
||||
? html` <div slot="headerSubtitle">${dialogSubtitle}</div>`
|
||||
: nothing}
|
||||
${showDocumentationLink && !this._loading && this._step
|
||||
? html`
|
||||
<a
|
||||
slot="headerActionItems"
|
||||
class="help"
|
||||
href=${this._params.manifest!.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this._params.manifest!.domain}`
|
||||
)
|
||||
: this._params.manifest!.documentation}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.help")}
|
||||
.path=${mdiHelpCircle}
|
||||
${dialogSubtitle
|
||||
? html` <div slot="subtitle">${dialogSubtitle}</div>`
|
||||
: nothing}
|
||||
${showDocumentationLink && !this._loading && this._step
|
||||
? html`
|
||||
<a
|
||||
slot="actionItems"
|
||||
class="help"
|
||||
href=${this._params.manifest!.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this._params.manifest!.domain}`
|
||||
)
|
||||
: this._params.manifest!.documentation}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
</ha-icon-button
|
||||
></a>
|
||||
`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.help")}
|
||||
.path=${mdiHelpCircle}
|
||||
>
|
||||
</ha-icon-button
|
||||
></a>
|
||||
`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
<div>
|
||||
${this._loading || this._step === null
|
||||
? html`
|
||||
@@ -369,7 +358,6 @@ class DataEntryFlowDialog extends LitElement {
|
||||
${this._step.type === "form"
|
||||
? html`
|
||||
<step-flow-form
|
||||
autofocus
|
||||
narrow
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
@@ -429,7 +417,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
`}
|
||||
`}
|
||||
</div>
|
||||
</ha-wa-dialog>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -559,24 +547,11 @@ class DataEntryFlowDialog extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _focusFormStep = async (): Promise<void> => {
|
||||
if (this._step?.type !== "form" || !this._open) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.updateComplete;
|
||||
(
|
||||
this.renderRoot.querySelector(
|
||||
"step-flow-form[autofocus]"
|
||||
) as HTMLElement | null
|
||||
)?.focus();
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-wa-dialog {
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
.dialog-title {
|
||||
|
||||
@@ -13,6 +13,7 @@ import "../../../components/ha-cover-controls";
|
||||
import "../../../components/ha-cover-tilt-controls";
|
||||
import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-humidifier-state";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-slider";
|
||||
import "../../../components/ha-time-input";
|
||||
@@ -295,11 +296,17 @@ class EntityPreviewRow extends LitElement {
|
||||
.label=${computeStateName(stateObj)}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.options=${stateObj.attributes.options?.map((option) => ({
|
||||
value: option,
|
||||
label: this.hass!.formatEntityState(stateObj, option),
|
||||
})) || []}
|
||||
naturalMenuWidth
|
||||
>
|
||||
${stateObj.attributes.options
|
||||
? stateObj.attributes.options.map(
|
||||
(option) => html`
|
||||
<ha-list-item .value=${option}>
|
||||
${this.hass!.formatEntityState(stateObj, option)}
|
||||
</ha-list-item>
|
||||
`
|
||||
)
|
||||
: ""}
|
||||
</ha-select>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -29,8 +29,6 @@ class StepFlowForm extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "autofocus" }) public autoFocus = false;
|
||||
|
||||
@property({ attribute: false }) public step!: DataEntryFlowStepForm;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -47,11 +45,6 @@ class StepFlowForm extends LitElement {
|
||||
|
||||
private _errors?: Record<string, string>;
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
@@ -90,7 +83,6 @@ class StepFlowForm extends LitElement {
|
||||
? html`<ha-alert alert-type="error">${this._errorMsg}</ha-alert>`
|
||||
: ""}
|
||||
<ha-form
|
||||
?autofocus=${this.autoFocus}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.data=${stepData}
|
||||
@@ -141,13 +133,10 @@ class StepFlowForm extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
setTimeout(() => this.shadowRoot!.querySelector("ha-form")!.focus(), 0);
|
||||
this.addEventListener("keydown", this._handleKeyDown);
|
||||
}
|
||||
|
||||
public override focus(_options?: FocusOptions): void {
|
||||
this.renderRoot.querySelector("ha-form")?.focus();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("step") && this.step?.preview) {
|
||||
|
||||
@@ -5,9 +5,8 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-control-button";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-textfield";
|
||||
import "../../components/ha-wa-dialog";
|
||||
import type { HaTextField } from "../../components/ha-textfield";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HassDialog } from "../make-dialog-manager";
|
||||
@@ -37,51 +36,34 @@ export class DialogEnterCode
|
||||
|
||||
@state() private _dialogParams?: EnterCodeDialogParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@query("#code") private _input?: HaTextField;
|
||||
|
||||
@state() private _showClearButton = false;
|
||||
|
||||
@state() private _narrow = false;
|
||||
|
||||
private _closeAction?: "submit" | "cancel";
|
||||
|
||||
public async showDialog(dialogParams: EnterCodeDialogParams): Promise<void> {
|
||||
this._dialogParams = dialogParams;
|
||||
this._open = true;
|
||||
this._showClearButton = false;
|
||||
this._closeAction = undefined;
|
||||
this._narrow = matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
if (!this._closeAction) {
|
||||
this._dialogParams?.cancel?.();
|
||||
}
|
||||
public closeDialog() {
|
||||
this._dialogParams = undefined;
|
||||
this._showClearButton = false;
|
||||
this._closeAction = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
return true;
|
||||
}
|
||||
|
||||
private _submit(): void {
|
||||
this._dialogParams?.submit?.(this._input?.value ?? "");
|
||||
this._closeAction = "submit";
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _cancel(): void {
|
||||
this._dialogParams?.cancel?.();
|
||||
this._closeAction = "cancel";
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
@@ -111,17 +93,15 @@ export class DialogEnterCode
|
||||
|
||||
if (isText) {
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._dialogParams.title ??
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this._cancel}
|
||||
.heading=${this._dialogParams.title ??
|
||||
this.hass.localize("ui.dialogs.enter_code.title")}
|
||||
width="small"
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-textfield
|
||||
class="input"
|
||||
?autofocus=${!this._narrow}
|
||||
?dialogInitialFocus=${!this._narrow}
|
||||
id="code"
|
||||
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
||||
type="password"
|
||||
@@ -130,31 +110,31 @@ export class DialogEnterCode
|
||||
pattern=${ifDefined(this._dialogParams.codePattern)}
|
||||
inputmode="text"
|
||||
></ha-textfield>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this._cancel}
|
||||
>
|
||||
${this._dialogParams.cancelText ??
|
||||
this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._submit}>
|
||||
${this._dialogParams.submitText ??
|
||||
this.hass.localize("ui.common.submit")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
dialogAction="cancel"
|
||||
>
|
||||
${this._dialogParams.cancelText ??
|
||||
this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button @click=${this._submit} slot="primaryAction">
|
||||
${this._dialogParams.submitText ??
|
||||
this.hass.localize("ui.common.submit")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._dialogParams.title ?? "Enter code"}
|
||||
width="small"
|
||||
@closed=${this._dialogClosed}
|
||||
<ha-dialog
|
||||
open
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._dialogParams.title ?? "Enter code"
|
||||
)}
|
||||
@closed=${this._cancel}
|
||||
hideActions
|
||||
>
|
||||
<div class="container">
|
||||
<ha-textfield
|
||||
@@ -163,7 +143,7 @@ export class DialogEnterCode
|
||||
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
||||
type="password"
|
||||
inputmode="numeric"
|
||||
?autofocus=${!this._narrow}
|
||||
?dialogInitialFocus=${!this._narrow}
|
||||
></ha-textfield>
|
||||
<div class="keypad">
|
||||
${BUTTONS.map((value) =>
|
||||
@@ -203,17 +183,18 @@ export class DialogEnterCode
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-wa-dialog>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-wa-dialog {
|
||||
ha-dialog {
|
||||
/* Place above other dialogs */
|
||||
--dialog-z-index: 104;
|
||||
}
|
||||
ha-textfield {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
margin: auto;
|
||||
}
|
||||
.container {
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button-toggle";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import {
|
||||
getMobileCloseToBottomAnimation,
|
||||
getMobileOpenFromBottomAnimation,
|
||||
} from "../../../../components/ha-md-dialog";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { LightColor, LightEntity } from "../../../../data/light";
|
||||
import {
|
||||
@@ -36,7 +41,7 @@ class DialogLightColorFavorite extends LitElement {
|
||||
|
||||
@state() private _modes: LightPickerMode[] = [];
|
||||
|
||||
@state() private _open = false;
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public async showDialog(
|
||||
dialogParams: LightColorFavoriteDialogParams
|
||||
@@ -45,11 +50,10 @@ class DialogLightColorFavorite extends LitElement {
|
||||
this._dialogParams = dialogParams;
|
||||
this._color = dialogParams.initialColor ?? this._computeCurrentColor();
|
||||
this._updateModes();
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
this._dialog?.close();
|
||||
}
|
||||
|
||||
private _updateModes() {
|
||||
@@ -116,8 +120,16 @@ class DialogLightColorFavorite extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private async _cancel() {
|
||||
this._dialogParams?.cancel?.();
|
||||
}
|
||||
|
||||
private _cancelDialog() {
|
||||
this._cancel();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._dialogParams = undefined;
|
||||
this._entry = undefined;
|
||||
this._color = undefined;
|
||||
@@ -126,7 +138,7 @@ class DialogLightColorFavorite extends LitElement {
|
||||
|
||||
private async _save() {
|
||||
if (!this._color) {
|
||||
this.closeDialog();
|
||||
this._cancel();
|
||||
return;
|
||||
}
|
||||
this._dialogParams?.submit?.(this._color);
|
||||
@@ -147,76 +159,83 @@ class DialogLightColorFavorite extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
.headerTitle=${this._dialogParams?.title}
|
||||
<ha-md-dialog
|
||||
open
|
||||
@cancel=${this._cancel}
|
||||
@closed=${this._dialogClosed}
|
||||
aria-labelledby="dialog-light-color-favorite-title"
|
||||
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
|
||||
.getCloseAnimation=${getMobileCloseToBottomAnimation}
|
||||
>
|
||||
<div class="header">
|
||||
${this._modes.length > 1
|
||||
? html`
|
||||
<div class="modes">
|
||||
${this._modes.map(
|
||||
(value) => html`
|
||||
<ha-icon-button-toggle
|
||||
border-only
|
||||
.selected=${value === this._mode}
|
||||
.label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
|
||||
)}
|
||||
.mode=${value}
|
||||
@click=${this._modeChanged}
|
||||
>
|
||||
<span
|
||||
class="wheel ${classMap({ [value]: true })}"
|
||||
></span>
|
||||
</ha-icon-button-toggle>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="content">
|
||||
${this._mode === "color_temp"
|
||||
? html`
|
||||
<light-color-temp-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
</light-color-temp-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._mode === "color"
|
||||
? html`
|
||||
<light-color-rgb-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
</light-color-rgb-picker>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@click=${this.closeDialog}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title" id="dialog-light-color-favorite-title"
|
||||
>${this._dialogParams?.title}</span
|
||||
>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
<div class="header">
|
||||
${this._modes.length > 1
|
||||
? html`
|
||||
<div class="modes">
|
||||
${this._modes.map(
|
||||
(value) => html`
|
||||
<ha-icon-button-toggle
|
||||
border-only
|
||||
.selected=${value === this._mode}
|
||||
.label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
|
||||
)}
|
||||
.mode=${value}
|
||||
@click=${this._modeChanged}
|
||||
>
|
||||
<span
|
||||
class="wheel ${classMap({ [value]: true })}"
|
||||
></span>
|
||||
</ha-icon-button-toggle>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="content">
|
||||
${this._mode === "color_temp"
|
||||
? html`
|
||||
<light-color-temp-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
</light-color-temp-picker>
|
||||
`
|
||||
: nothing}
|
||||
${this._mode === "color"
|
||||
? html`
|
||||
<light-color-rgb-picker
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
@color-changed=${this._colorChanged}
|
||||
>
|
||||
</light-color-rgb-picker>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<ha-button appearance="plain" @click=${this._cancelDialog}>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${!this._color}
|
||||
<ha-button @click=${this._save} .disabled=${!this._color}
|
||||
>${this.hass.localize("ui.common.save")}</ha-button
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -224,18 +243,23 @@ class DialogLightColorFavorite extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-wa-dialog {
|
||||
--ha-dialog-width-md: 420px; /* prevent width jumps when switching modes */
|
||||
--ha-dialog-max-height: min(
|
||||
ha-md-dialog {
|
||||
min-width: 420px; /* prevent width jumps when switching modes */
|
||||
max-height: min(
|
||||
600px,
|
||||
100% - 48px
|
||||
); /* prevent scrolling on desktop */
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-wa-dialog {
|
||||
--ha-dialog-width-md: 100vw;
|
||||
--ha-dialog-max-height: calc(100% - 100px);
|
||||
ha-md-dialog {
|
||||
min-width: 100%;
|
||||
min-height: auto;
|
||||
max-height: calc(100% - 100px);
|
||||
margin-bottom: 0;
|
||||
|
||||
--md-dialog-container-shape-start-start: 28px;
|
||||
--md-dialog-container-shape-start-end: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface LightColorFavoriteDialogParams {
|
||||
title: string;
|
||||
initialColor?: LightColor;
|
||||
submit?: (color?: LightColor) => void;
|
||||
cancel?: () => void;
|
||||
}
|
||||
|
||||
export const loadLightColorFavoriteDialog = () =>
|
||||
@@ -17,6 +18,7 @@ export const showLightColorFavoriteDialog = (
|
||||
dialogParams: LightColorFavoriteDialogParams
|
||||
) =>
|
||||
new Promise<LightColor | null>((resolve) => {
|
||||
const origCancel = dialogParams.cancel;
|
||||
const origSubmit = dialogParams.submit;
|
||||
|
||||
fireEvent(element, "show-dialog", {
|
||||
@@ -24,6 +26,12 @@ export const showLightColorFavoriteDialog = (
|
||||
dialogImport: loadLightColorFavoriteDialog,
|
||||
dialogParams: {
|
||||
...dialogParams,
|
||||
cancel: () => {
|
||||
resolve(null);
|
||||
if (origCancel) {
|
||||
origCancel();
|
||||
}
|
||||
},
|
||||
submit: (color: LightColor) => {
|
||||
resolve(color);
|
||||
if (origSubmit) {
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { mdiPlay, mdiStop } from "@mdi/js";
|
||||
import { mdiClose, mdiPlay, mdiStop } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-control-button";
|
||||
import type { HaSelectSelectEvent } from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-list-item";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import {
|
||||
getMobileCloseToBottomAnimation,
|
||||
getMobileOpenFromBottomAnimation,
|
||||
} from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-select";
|
||||
import "../../../../components/ha-textfield";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import { SirenEntityFeature } from "../../../../data/siren";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -24,25 +28,23 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
|
||||
@state() _stateObj?: HassEntity;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() _tone?: string;
|
||||
|
||||
@state() _volume?: number;
|
||||
|
||||
@state() _duration?: number;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
public showDialog({ stateObj }: { stateObj: HassEntity }) {
|
||||
this._stateObj = stateObj;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._open = false;
|
||||
this._dialog?.close();
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._open = false;
|
||||
this._stateObj = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -63,33 +65,50 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
SirenEntityFeature.DURATION
|
||||
);
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.open=${this._open}
|
||||
.hass=${this.hass}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.components.siren.advanced_controls"
|
||||
)}
|
||||
<ha-md-dialog
|
||||
open
|
||||
@closed=${this._dialogClosed}
|
||||
aria-labelledby="dialog-light-color-favorite-title"
|
||||
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
|
||||
.getCloseAnimation=${getMobileCloseToBottomAnimation}
|
||||
>
|
||||
<div>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@click=${this.closeDialog}
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title" id="dialog-light-color-favorite-title"
|
||||
>${this.hass.localize(
|
||||
"ui.components.siren.advanced_controls"
|
||||
)}</span
|
||||
>
|
||||
</ha-dialog-header>
|
||||
<div slot="content">
|
||||
<div class="options">
|
||||
${supportsTones
|
||||
? html`
|
||||
<ha-select
|
||||
.label=${this.hass.localize("ui.components.siren.tone")}
|
||||
@selected=${this._handleToneChange}
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleToneChange}
|
||||
.value=${this._tone}
|
||||
.options=${Object.entries(
|
||||
this._stateObj!.attributes.available_tones
|
||||
).map(([toneId, toneName]) => ({
|
||||
value: Array.isArray(
|
||||
this._stateObj!.attributes.available_tones
|
||||
)
|
||||
? toneName
|
||||
: toneId,
|
||||
label: toneName,
|
||||
}))}
|
||||
>
|
||||
${Object.entries(
|
||||
this._stateObj.attributes.available_tones
|
||||
).map(
|
||||
([toneId, toneName]) => html`
|
||||
<ha-list-item
|
||||
.value=${Array.isArray(
|
||||
this._stateObj!.attributes.available_tones
|
||||
)
|
||||
? toneName
|
||||
: toneId}
|
||||
>${toneName}</ha-list-item
|
||||
>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: nothing}
|
||||
@@ -134,21 +153,17 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
</ha-control-button>
|
||||
</div>
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
<div slot="actions">
|
||||
<ha-button @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleToneChange(ev: HaSelectSelectEvent) {
|
||||
this._tone = ev.detail.value;
|
||||
private _handleToneChange(ev) {
|
||||
this._tone = ev.target.value;
|
||||
}
|
||||
|
||||
private _handleVolumeChange(ev) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-time-input";
|
||||
import { setDateValue } from "../../../data/date";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("more-info-date")
|
||||
class MoreInfoDate extends LitElement {
|
||||
@@ -31,7 +31,7 @@ class MoreInfoDate extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _dateChanged(ev: ValueChangedEvent<string>): void {
|
||||
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
if (ev.detail.value) {
|
||||
setDateValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-time-input";
|
||||
import { setDateTimeValue } from "../../../data/datetime";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("more-info-datetime")
|
||||
class MoreInfoDatetime extends LitElement {
|
||||
@@ -45,7 +45,7 @@ class MoreInfoDatetime extends LitElement {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _timeChanged(ev: ValueChangedEvent<string>): void {
|
||||
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
if (ev.detail.value) {
|
||||
const dateObj = new Date(this.stateObj!.state);
|
||||
const newTime = ev.detail.value.split(":").map(Number);
|
||||
@@ -55,7 +55,7 @@ class MoreInfoDatetime extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _dateChanged(ev: ValueChangedEvent<string>): void {
|
||||
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
if (ev.detail.value) {
|
||||
const dateObj = new Date(this.stateObj!.state);
|
||||
const newDate = ev.detail.value.split("-").map(Number);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
setInputDateTimeValue,
|
||||
stateToIsoDateString,
|
||||
} from "../../../data/input_datetime";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("more-info-input_datetime")
|
||||
class MoreInfoInputDatetime extends LitElement {
|
||||
@@ -55,7 +55,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _timeChanged(ev: ValueChangedEvent<string>): void {
|
||||
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
setInputDateTimeValue(
|
||||
this.hass!,
|
||||
this.stateObj!.entity_id,
|
||||
@@ -66,7 +66,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _dateChanged(ev: ValueChangedEvent<string>): void {
|
||||
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
setInputDateTimeValue(
|
||||
this.hass!,
|
||||
this.stateObj!.entity_id,
|
||||
|
||||
@@ -202,18 +202,21 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-dropdown @wa-select=${this._handleSourceChange}>
|
||||
return html`<ha-dropdown>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(`ui.card.media_player.source`)}
|
||||
.path=${mdiLoginVariant}
|
||||
@wa-select=${this._handleSourceChange}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${this.stateObj.attributes.source_list!.map(
|
||||
(source) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${source}
|
||||
.selected=${source === this.stateObj?.attributes.source}
|
||||
class=${source === this.stateObj?.attributes.source
|
||||
? "selected"
|
||||
: ""}
|
||||
>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
@@ -248,7 +251,9 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
(soundMode) =>
|
||||
html`<ha-dropdown-item
|
||||
.value=${soundMode}
|
||||
.selected=${soundMode === this.stateObj?.attributes.sound_mode}
|
||||
class=${soundMode === this.stateObj?.attributes.sound_mode
|
||||
? "selected"
|
||||
: ""}
|
||||
>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj!,
|
||||
@@ -674,6 +679,13 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
align-self: center;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
`;
|
||||
|
||||
private _handleClick(e: MouseEvent): void {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import type { HaSelectSelectEvent } from "../../../components/ha-select";
|
||||
import "../../../components/ha-select";
|
||||
import type { RemoteEntity } from "../../../data/remote";
|
||||
import { REMOTE_SUPPORT_ACTIVITY } from "../../../data/remote";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-list-item";
|
||||
|
||||
@customElement("more-info-remote")
|
||||
class MoreInfoRemote extends LitElement {
|
||||
@@ -29,24 +30,30 @@ class MoreInfoRemote extends LitElement {
|
||||
)}
|
||||
.value=${stateObj.attributes.current_activity || ""}
|
||||
@selected=${this._handleActivityChanged}
|
||||
.options=${stateObj.attributes.activity_list?.map((activity) => ({
|
||||
value: activity,
|
||||
label: this.hass!.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"activity",
|
||||
activity
|
||||
),
|
||||
}))}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${stateObj.attributes.activity_list?.map(
|
||||
(activity) => html`
|
||||
<ha-list-item .value=${activity}>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"activity",
|
||||
activity
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleActivityChanged(ev: HaSelectSelectEvent) {
|
||||
private _handleActivityChanged(ev) {
|
||||
const oldVal = this.stateObj!.attributes.current_activity;
|
||||
const newVal = ev.detail.value;
|
||||
const newVal = ev.target.value;
|
||||
|
||||
if (!newVal || oldVal === newVal) {
|
||||
return;
|
||||
|
||||
@@ -5,7 +5,7 @@ import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-time-input";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import { setTimeValue } from "../../../data/time";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("more-info-time")
|
||||
class MoreInfoTime extends LitElement {
|
||||
@@ -35,7 +35,7 @@ class MoreInfoTime extends LitElement {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _timeChanged(ev: ValueChangedEvent<string>): void {
|
||||
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
|
||||
if (ev.detail.value) {
|
||||
setTimeValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
|
||||
}
|
||||
|
||||
@@ -11,12 +11,13 @@ import {
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/entity/ha-battery-icon";
|
||||
import type { HaSelectSelectEvent } from "../../../components/ha-select";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-select";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../../data/entity/entity_registry";
|
||||
@@ -171,17 +172,21 @@ class MoreInfoVacuum extends LitElement {
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
.value=${stateObj.attributes.fan_speed}
|
||||
@selected=${this._handleFanSpeedChanged}
|
||||
.options=${stateObj.attributes.fan_speed_list!.map(
|
||||
(mode) => ({
|
||||
value: mode,
|
||||
label: this.hass!.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"fan_speed",
|
||||
mode
|
||||
),
|
||||
})
|
||||
)}
|
||||
fixedMenuPosition
|
||||
naturalMenuWidth
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${stateObj.attributes.fan_speed_list!.map(
|
||||
(mode) => html`
|
||||
<ha-list-item .value=${mode}>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"fan_speed",
|
||||
mode
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
<div
|
||||
style="justify-content: center; align-self: center; padding-top: 1.3em"
|
||||
@@ -286,9 +291,9 @@ class MoreInfoVacuum extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleFanSpeedChanged(ev: HaSelectSelectEvent) {
|
||||
private _handleFanSpeedChanged(ev) {
|
||||
const oldVal = this.stateObj!.attributes.fan_speed;
|
||||
const newVal = ev.detail.value;
|
||||
const newVal = ev.target.value;
|
||||
|
||||
if (!newVal || oldVal === newVal) {
|
||||
return;
|
||||
|
||||
@@ -313,119 +313,113 @@ class MoreInfoWeather extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${supportedForecasts?.length
|
||||
? html`
|
||||
<div class="section">
|
||||
${this.hass.localize("ui.card.weather.forecast")}:
|
||||
</div>
|
||||
${supportedForecasts?.length > 1
|
||||
? html`<ha-tab-group
|
||||
@wa-tab-show=${this._handleForecastTypeChanged}
|
||||
|
||||
<div class="section">
|
||||
${this.hass.localize("ui.card.weather.forecast")}:
|
||||
</div>
|
||||
${supportedForecasts?.length > 1
|
||||
? html`<ha-tab-group @wa-tab-show=${this._handleForecastTypeChanged}>
|
||||
${supportedForecasts.map(
|
||||
(forecastType) =>
|
||||
html`<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${forecastType}
|
||||
.active=${this._forecastType === forecastType}
|
||||
>
|
||||
${supportedForecasts.map(
|
||||
(forecastType) =>
|
||||
html`<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${forecastType}
|
||||
.active=${this._forecastType === forecastType}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.card.weather.${forecastType}`
|
||||
)}
|
||||
</ha-tab-group-tab>`
|
||||
)}
|
||||
</ha-tab-group>`
|
||||
: nothing}
|
||||
<div class="forecast">
|
||||
${forecast?.length
|
||||
? this._groupForecastByDay(forecast).map((dayForecast) => {
|
||||
const showDayHeader = hourly || dayNight;
|
||||
return html`
|
||||
<div class="forecast-day">
|
||||
${showDayHeader
|
||||
? html`<div class="forecast-day-header">
|
||||
${formatDateWeekdayShort(
|
||||
new Date(dayForecast[0].datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="forecast-day-content">
|
||||
${dayForecast.map((item) =>
|
||||
this._showValue(item.templow) ||
|
||||
this._showValue(item.temperature)
|
||||
? html`
|
||||
<div class="forecast-item">
|
||||
<div
|
||||
class="forecast-item-label ${showDayHeader
|
||||
? ""
|
||||
: "no-header"}"
|
||||
>
|
||||
${hourly
|
||||
? formatTime(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)
|
||||
: dayNight
|
||||
? html`<div class="daynight">
|
||||
${item.is_daytime !== false
|
||||
? this.hass!.localize(
|
||||
"ui.card.weather.day"
|
||||
)
|
||||
: this.hass!.localize(
|
||||
"ui.card.weather.night"
|
||||
)}
|
||||
</div>`
|
||||
: formatDateWeekdayShort(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
</div>
|
||||
${this._showValue(item.condition)
|
||||
? html`
|
||||
<div class="forecast-image-icon">
|
||||
${getWeatherStateIcon(
|
||||
item.condition!,
|
||||
this,
|
||||
!(
|
||||
item.is_daytime ||
|
||||
item.is_daytime === undefined
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="temp">
|
||||
${this._showValue(item.temperature)
|
||||
? html`${formatNumber(
|
||||
item.temperature,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="templow">
|
||||
${this._showValue(item.templow)
|
||||
? html`${formatNumber(
|
||||
item.templow!,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
: html`<ha-spinner size="medium"></ha-spinner>`}
|
||||
</div>
|
||||
`
|
||||
${this.hass!.localize(`ui.card.weather.${forecastType}`)}
|
||||
</ha-tab-group-tab>`
|
||||
)}
|
||||
</ha-tab-group>`
|
||||
: nothing}
|
||||
<div class="forecast">
|
||||
${forecast?.length
|
||||
? this._groupForecastByDay(forecast).map((dayForecast) => {
|
||||
const showDayHeader = hourly || dayNight;
|
||||
return html`
|
||||
<div class="forecast-day">
|
||||
${showDayHeader
|
||||
? html`<div class="forecast-day-header">
|
||||
${formatDateWeekdayShort(
|
||||
new Date(dayForecast[0].datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="forecast-day-content">
|
||||
${dayForecast.map((item) =>
|
||||
this._showValue(item.templow) ||
|
||||
this._showValue(item.temperature)
|
||||
? html`
|
||||
<div class="forecast-item">
|
||||
<div
|
||||
class="forecast-item-label ${showDayHeader
|
||||
? ""
|
||||
: "no-header"}"
|
||||
>
|
||||
${hourly
|
||||
? formatTime(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)
|
||||
: dayNight
|
||||
? html`<div class="daynight">
|
||||
${item.is_daytime !== false
|
||||
? this.hass!.localize(
|
||||
"ui.card.weather.day"
|
||||
)
|
||||
: this.hass!.localize(
|
||||
"ui.card.weather.night"
|
||||
)}
|
||||
</div>`
|
||||
: formatDateWeekdayShort(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
</div>
|
||||
${this._showValue(item.condition)
|
||||
? html`
|
||||
<div class="forecast-image-icon">
|
||||
${getWeatherStateIcon(
|
||||
item.condition!,
|
||||
this,
|
||||
!(
|
||||
item.is_daytime ||
|
||||
item.is_daytime === undefined
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="temp">
|
||||
${this._showValue(item.temperature)
|
||||
? html`${formatNumber(
|
||||
item.temperature,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="templow">
|
||||
${this._showValue(item.templow)
|
||||
? html`${formatNumber(
|
||||
item.templow!,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
: html`<ha-spinner size="medium"></ha-spinner>`}
|
||||
</div>
|
||||
|
||||
${this.stateObj.attributes.attribution
|
||||
? html`
|
||||
<div class="attribution">
|
||||
|
||||
@@ -96,6 +96,7 @@ interface ChildView {
|
||||
viewTitle?: string;
|
||||
viewImport?: () => Promise<unknown>;
|
||||
viewParams?: any;
|
||||
keepHeader?: boolean;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -349,6 +350,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
this._childView = {
|
||||
viewTag: "ha-more-info-attributes",
|
||||
viewParams: { entityId: this._entityId },
|
||||
keepHeader: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -404,9 +406,12 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
const deviceType =
|
||||
(deviceId && this.hass.devices[deviceId].entry_type) || "device";
|
||||
|
||||
const isDefaultView = this._currView === DEFAULT_VIEW && !this._childView;
|
||||
const isDefaultView =
|
||||
this._currView === DEFAULT_VIEW &&
|
||||
(!this._childView || this._childView.keepHeader);
|
||||
const isSpecificInitialView =
|
||||
this._initialView !== DEFAULT_VIEW && !this._childView;
|
||||
this._initialView !== DEFAULT_VIEW &&
|
||||
(!this._childView || this._childView.keepHeader);
|
||||
const showCloseIcon =
|
||||
(isDefaultView &&
|
||||
this._parentEntityIds.length === 0 &&
|
||||
@@ -445,7 +450,12 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
const breadcrumb = [areaName, deviceName, entityName].filter(
|
||||
(v): v is string => Boolean(v)
|
||||
);
|
||||
const title = this._childView?.viewTitle || breadcrumb.pop() || entityId;
|
||||
const title =
|
||||
(this._childView && !this._childView.keepHeader
|
||||
? this._childView.viewTitle
|
||||
: undefined) ||
|
||||
breadcrumb.pop() ||
|
||||
entityId;
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user