Compare commits

...

64 Commits

Author SHA1 Message Date
Aidan Timson 3bf5833262 Benchmark script 2026-02-11 02:14:56 +00:00
Aidan Timson 8c9da19935 SWC 2026-02-11 02:12:54 +00:00
Aidan Timson 4b1eeb0eb1 Use scrollbar styles on host 2026-02-11 02:12:54 +00:00
Aidan Timson f7ffdabe5d Move scrolling for dashboards inside view container 2026-02-11 02:12:54 +00:00
Aidan Timson 678ee7e82a Migrate todo dialog to wa (#29527)
Migrate todo dialog(s) to wa
2026-02-10 20:38:45 +01:00
uptimeZERO_ ee72f4818d Add guard for failed JSON parsing of localStorage entries (#29502)
* Added guard for failed json parsing of selectedTheme key

* deleting corrupted keys and logging
2026-02-10 20:30:28 +01:00
karwosts ccbd9c1f24 Don't show hidden todolists on todo panel (#29510) 2026-02-10 19:21:53 +01:00
ildar170975 84135a9424 hui-entity-editor: fix padding-top for ha-md-list (#29537)
fix padding-top for ha-md-list
2026-02-10 19:17:10 +01:00
Petar Petrov 4ebc334298 Normalize SI unit prefixes in distribution card proportions (#29539)
* Normalize SI unit prefixes in distribution card proportions

* Extract SI prefix normalization to shared utility with tests

Moves normalizeValueBySIPrefix to src/common/number/ so it can be
reused. Replaces the inline method in the distribution card and the
switch statement in getPowerFromState (energy.ts).
2026-02-10 19:16:02 +01:00
Petar Petrov 3c4c3e39e5 Fix storage space calculations to account for reserved system space (#29540) 2026-02-10 19:13:58 +01:00
Aidan Timson 1267003b42 Migrate local backup location dialog to wa (#29543)
Migrate config-backup dialog(s) to wa
2026-02-10 19:11:44 +01:00
Aidan Timson 733359c869 Fix position of 2 extra actions for date picker dialog (#29541)
Fix position of 2 extra actions for date picker
2026-02-10 19:08:55 +01:00
Aidan Timson 50b6e07ae5 Migrate import blueprint dialog to wa (#29544)
* Migrate config-blueprint dialog(s) to wa

* Add vt
2026-02-10 19:00:58 +01:00
Wendelin 1b62a7cff8 Dropdown item add selected prop (#29553)
Refactor dropdown item selection handling to use property binding for selected state
2026-02-10 18:55:38 +01:00
Wendelin c293cf56f6 Migrate from ha-md-menu to ha-dropdown (#29548) 2026-02-10 15:33:49 +00:00
Aidan Timson 2ec6f3615d Use ValueChangedEvent for more CustomEvents (#29547) 2026-02-10 16:26:19 +01:00
Wendelin d3b92059e5 Re-enable autofocus for iOS in ha-wa-dialog (#29534) 2026-02-10 15:09:33 +01:00
ildar170975 72e69f6291 Entities card: add “area” for “secondary_info” (#29268)
* add "area/floor" labels for secondary-info for Entities card

* use STRINGS_SEPARATOR_DOT

* add "area/floor" for secondary-info

* add "area/floor" for secondary-info

* add "area/floor" for secondary-info

* use STRINGS_SEPARATOR_DOT

* use STRINGS_SEPARATOR_DOT

* use STRINGS_SEPARATOR_DOT

* add STRINGS_SEPARATOR_DOT

* changed an order & renamed an option

* fixed name for "area with floor"

* chaged an order & renamed an entry

* renamed "area with floor" entry

* add STRINGS_SEPARATOR_DOT

* Delete src/common/strings-separator.ts

* change import

* change import

* change import

* change import

* change import

* fix import

* fix import

* fix import

* remove "area-with-floor"

* remove "area-with-floor"

* remove "area-with-floor"

* remove unneeded comma

* remove "area-with-floor"

* clean up

* typo

* Apply suggestion from @MindFreeze

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* move area definition into a separate method

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-10 16:06:00 +02:00
Aidan Timson 3bff97595f Migrate date picker dialog to wa (#29506) 2026-02-10 14:52:01 +01:00
renovate[bot] 19b1d03cd1 Update Node.js to v24.13.1 (#29538)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 13:34:20 +00:00
Petar Petrov f04557688c Use unique SVG mask IDs in graph base to fix rendering on Samsung WebView (#29525)
* Use unique SVG mask IDs in graph base to fix rendering on Samsung WebView

* Remove unused IDs
2026-02-10 14:23:52 +01:00
Matthias de Baat d6953ea1bc Make storage charts consistent with lifetime chart (#29526)
* Make storage charts consistent with lifetime chart

* Update src/panels/config/storage/storage-breakdown-chart.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-10 13:12:53 +00:00
ildar170975 4501db18c7 Entities card editor: fix margin-top for ha-entity-picker (#29536)
* fix margin-top for ha-entity-picker

* Apply suggestion from @MindFreeze

* use ha-space var

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-10 13:10:40 +00:00
ildar170975 c68bcc5b32 Entity card: fix unit for monetary (#29406)
* fix unit for monetary

* fix for span

* formatEntityStateToParts() does not return "order"

* get unit from formatEntityStateToParts()

* resolving conflicts

* resolving conflicts

* some styling
2026-02-10 15:09:43 +02:00
Aidan Timson 3753c7d313 Migrate helper dialogs to wa (#29507)
* Migrate config-helpers dialog(s) to wa

* Stop secondary dialogs from automatically closing

* Styles

* Fix initial focus
2026-02-10 14:54:52 +02:00
Aidan Timson 4a2ef18375 Migrate media browser dialog to wa (#29529) 2026-02-10 13:28:48 +01:00
Wendelin 051da41eec Update @home-assistant/webawesome to version 3.2.1-ha.0 (#29533)
update @home-assistant/webawesome to version 3.2.1-ha.0
2026-02-10 13:25:53 +01:00
Aidan Timson 9b1a679f21 Add missing AI tasks item to quick search (#29531) 2026-02-10 13:10:35 +01:00
Aidan Timson 905a0f957c Migrate system information and startup time dialogs to wa (#29532) 2026-02-10 13:08:08 +01:00
Wendelin d9a687b79c Hardware panel imporve chart loading UI (#29528) 2026-02-10 10:42:57 +00:00
karwosts 3b56497134 Hide more info weather forecast when unsupported (#29517) 2026-02-10 09:28:52 +00:00
Paul Bottein ed0ec871ce Don't close automation and script sidebar on save (#29434) 2026-02-10 10:07:31 +01:00
ildar170975 29f5362182 hui-graph-header-footer: add own action handler (#29522)
* add action handler

* set action handler for ":host"
2026-02-10 11:03:31 +02:00
Matthias de Baat 5096bab26c Move AI task to its own page and change General into Home information (#29458)
* Move AI task to its own page and change General into Home information

* Fixed unused state properties for form submission states, removed unused imports and obsolete CSS, replaced hardcoded pixel values with spacing tokens, and added error handling for the map component.

* Update src/panels/config/core/ha-config-section-general.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-10 06:45:26 +00:00
Yosi Levy ce2892cab9 Various RTL fixes (#29520) 2026-02-10 06:59:18 +01:00
renovate[bot] 57748c15de Update dependency @codemirror/commands to v6.10.2 (#29518)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 06:58:46 +01:00
Paul Bottein ecb6a33c86 Add color settings for entity in distribution card (#29436)
* Add color settings for entity in distribution card

* Fix name

* Fix name again...
2026-02-09 16:15:14 +01:00
ildar170975 d34921ff6d state-card-input_number: fix for narrow viewport (#29327)
state-info: fix for narrow viewport
2026-02-09 15:30:40 +01:00
ildar170975 e6c9e81082 Add formatEntityStateToParts() + use it for hui-entity-card & ha-state-label-badge (#29239)
* add formatEntityStateToParts

* add formatEntityStateToParts

* use formatEntityStateToParts

* add formatEntityStateToParts

* use formatEntityStateToParts

* add formatEntityStateToParts

* add formatEntityStateToParts

* add computeStateDisplayToParts

* update for monetary

* fix a test for monetary

* fixed test for monetary

* do not include "order" into result

* do not include "order" into result

* do not include "order" into result

* do not include "order" into result

* do not include "order" into result

* do not include "order" into result

* do not include "order" into result

* simplify

* ensure less conflicts in future merges

* Refactor monetary computing

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2026-02-09 15:11:05 +01:00
Petar Petrov aa13c6fa53 Add tests for energy chart functions (#29504) 2026-02-09 15:07:11 +01:00
ildar170975 d47738aa24 developer-tools-debug: migrate "ha-settings-row" to "ha-md-list-item" (#29501)
* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* background: 0 -> background: none
2026-02-09 14:25:23 +01:00
ildar170975 d473ee1084 User profile: migrate toggle rows "ha-settings-row" to "ha-md-list-item" (#29497)
* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* remove unneeded "narrow"

* add import

* background: 0 -> background: none
2026-02-09 13:52:19 +01:00
Aidan Timson 0372ed932f Migrate new dashboard dialog to wa (#29438)
* Migrate config-dashboard dialog(s) to wa

* Restructure for scrolling
2026-02-09 14:08:04 +02:00
Aidan Timson 2baa044db5 Migrate profile dialogs to wa, refactor LL access token dialog (#29440)
* Migrate profile dialog(s) to wa

* Make sure code is entered before submit is allowed

* Refactor dialog

* Remove unused params

* Pass existing names and validate name is not already used

* Reduce cleanup on show

* Make QR image larger

* max width

* Fix

* Remove extra event fire

* Make params required

* cleanup

* Cleanup

* Fix

* Fix
2026-02-09 12:54:02 +01:00
Wendelin 3d04046bcc Migrate automation picker row to ha-dropdown (#29428)
* Update @home-assistant/webawesome to version 3.2.1 and refactor ha-dropdown integration in automation picker

* review

* revert wa update

* Update @home-assistant/webawesome to version 3.0.0-ha.2 in yarn.lock
2026-02-09 12:35:49 +01:00
Tom Carpenter 8e860cb17d Improve energy dashboard monthly/this-quarter chart time axes (#29435)
* Add splitNumber option to monthly ECharts

When there are a small number of bars (<=3) for monthly data, set the splitNumber parameter to force the date x-axis to show whole months.

* Add axis tick fomratting for short months

This ensures that the month format is consistent between 2/3 month and longer ranges.

* Avoid calling getSuggestedMax twice

* Fix another case of power chart cutting off last hour of data

The previous fix only solved the problem for 5-minute data, not hourly or daily. This should solve the issue regardless, and allows the energy chart to have other line-based plots in the future.

* Update other uses of getSuggestedMax()

* Fix statistics-chart Last Period Rendering

1. When appending the "current state" value, if the current time intersects with the final period, we can end up with the chart folding back on itself. This is fixed by ensuring for the final period we push the earlier of the statistic end time and the display end time (which is in turn limited to now).

2. Always close off the last data point at the chart end time. Otherwise for line charts, the final period doesn't get rendered.

* Remove unused monthStyle formatter.

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Rename getSuggestedMax function parameter in energy chart

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* Document magic numbers in montly energy chart

* Make padding a constant for clarity.
* Explain the purpose of splitNumber.

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-09 13:05:42 +02:00
Petar Petrov c41d7ff923 Fix history-graph card rendering stale data point on left edge (#29499)
When HistoryStream.processMessage() prunes expired history and preserves
the last expired state as a boundary marker, it updates lu (last_updated)
but not lc (last_changed). Chart components use lc preferentially, so
when lc is present the boundary point gets plotted at the original stale
timestamp far to the left of the visible window. Delete lc from the
boundary state so the chart uses the corrected lu timestamp.
2026-02-09 10:13:06 +01:00
Matthias de Baat c22fc1021a Counter gravity effect on the Matter icon (#29459) 2026-02-09 10:05:06 +01:00
dependabot[bot] 6344233934 Bump github/codeql-action from 4.32.0 to 4.32.2 (#29498)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.0 to 4.32.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b20883b0cd1f46c72ae0ba6d1090936928f9fa30...45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-09 10:22:39 +02:00
ildar170975 23441d593b Data tables: filters: fix a placement for icon & text (#29488)
* fix padding & margin

* fix margin for icon

* fix margin for icon

* fix margin for image

* use ha-space-4

* use --ha-space-4

* use ha-space-1

* use ha-space-4
2026-02-09 08:57:00 +01:00
ildar170975 8393ed5fd4 voice-assistant-brand-icon: fixes for margin & alignment (#29493)
* fix styles

* fix a gap between logos

* fix a gap between logos

* fix right margin for logo

* fix right margin for logo

* show icons in flex

* remove unneeded style

* add right margin for logo
2026-02-09 08:54:45 +01:00
TheJulianJES 09afe9bb51 Fix ZHA dashboard using disabled and ignored config entries (#29494) 2026-02-09 08:18:43 +01:00
Benedikt Johannes fb8d6062c5 Sugestion -> Suggestion (#29490)
* Update en.json

* Update ha-area-picker.ts

* Update ha-label-picker.ts

* Update ha-floor-picker.ts

* Update ha-category-picker.ts
2026-02-09 08:14:15 +01:00
karwosts f93ae58b83 Fix dupl. id error in water-sankey (#29489) 2026-02-08 19:06:37 +01:00
karwosts 7626b26b2d No FAB in calendar-card (#29487) 2026-02-08 19:06:31 +01:00
renovate[bot] a1bf30e501 Update dependency @rsdoctor/rspack-plugin to v1.5.2 (#29481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 08:41:36 +00:00
ildar170975 37a45d1729 voice-assistants-expose-assistant-icon: fix tooltip (#29469)
* fix tooltip

* provide id for tooltip for assistant icon
2026-02-08 09:32:12 +01:00
karwosts 6962a915a3 Fix describe legacy triggers in traces (#29473)
* Fix describe legacy triggers in traces

* remove unnecessary type
2026-02-07 22:05:05 +01:00
renovate[bot] de3e2bcafa Update dependency glob to v13.0.1 (#29462)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 21:44:45 +01:00
Matthias de Baat e732280b70 Add safe space at the bottom (#29454)
* Add safe space at the bottom

* Move margin-bottom from ha-config-analytics component to its parent wrapper in ha-config-section-analytics
2026-02-06 17:47:40 +01:00
renovate[bot] 4fb3453f73 Update dependency ua-parser-js to v2.0.9 (#29456)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 16:45:18 +00:00
renovate[bot] 0f9cb9c13e Update dependency @rspack/core to v1.7.5 (#29447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 12:23:27 +01:00
Matthias de Baat 7aa235c6af Fix Discord link for designers (#29393)
* Fix Discord link for designers

Updated Discord link for designers to the correct channel.

* Update Discord link for designers in home.markdown

* Update gallery/src/pages/concepts/home.markdown

Co-authored-by: Aidan Timson <aidan@timmo.dev>

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Aidan Timson <aidan@timmo.dev>
2026-02-05 21:09:28 +01:00
Aidan Timson e8ddae8189 Migrate join beta dialog to wa (#29439)
Migrate config-updates dialog(s) to wa
2026-02-05 21:06:38 +01:00
139 changed files with 3448 additions and 2143 deletions
+3 -3
View File
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
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@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
# ️ 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@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
+1 -1
View File
@@ -1 +1 @@
24.13.0
24.13.1
+3
View File
@@ -31,4 +31,7 @@ module.exports = {
isDevContainer() {
return isTrue(process.env.DEV_CONTAINER);
},
jsMinifier() {
return (process.env.JS_MINIFIER || "swc").toLowerCase();
},
};
+7 -1
View File
@@ -80,7 +80,13 @@ const doneHandler = (done) => (err, stats) => {
console.log(stats.toString("minimal"));
}
log(`Build done @ ${new Date().toLocaleTimeString()}`);
const durationMs =
stats?.startTime && stats?.endTime ? stats.endTime - stats.startTime : 0;
const durationLabel = durationMs
? ` (${(durationMs / 1000).toFixed(1)}s, minifier: ${env.jsMinifier()})`
: ` (minifier: ${env.jsMinifier()})`;
log(`Build done @ ${new Date().toLocaleTimeString()}${durationLabel}`);
if (done) {
done();
+15 -5
View File
@@ -13,6 +13,7 @@ const { WebpackManifestPlugin } = require("rspack-manifest-plugin");
const log = require("fancy-log");
// eslint-disable-next-line @typescript-eslint/naming-convention
const WebpackBar = require("webpackbar/rspack");
const env = require("./env.cjs");
const paths = require("./paths.cjs");
const bundle = require("./bundle.cjs");
@@ -100,11 +101,20 @@ const createRspackConfig = ({
},
optimization: {
minimizer: [
new TerserPlugin({
parallel: true,
extractComments: true,
terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }),
}),
env.jsMinifier() === "terser"
? new TerserPlugin({
parallel: true,
extractComments: true,
terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }),
})
: new rspack.SwcJsMinimizerRspackPlugin({
extractComments: true,
minimizerOptions: {
ecma: latestBuild ? 2015 : 5,
module: latestBuild,
format: { comments: false },
},
}),
],
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
+1 -1
View File
@@ -18,7 +18,7 @@ The Home Assistant interface is based on Material Design. It's a design system c
We want to make it as easy for designers to contribute as it is for developers. Theres a lot a designer can contribute to:
- Meet us at <a href="https://www.home-assistant.io/join-chat" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
- Meet us at <a href="https://www.home-assistant.io/join-chat-design" rel="noopener noreferrer" target="_blank">Discord #designers channel</a>. If you can't see the channel, make sure you set the correct role in Channels & Roles.
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
+7 -7
View File
@@ -29,7 +29,7 @@
"@babel/runtime": "7.28.6",
"@braintree/sanitize-url": "7.1.2",
"@codemirror/autocomplete": "6.20.0",
"@codemirror/commands": "6.10.1",
"@codemirror/commands": "6.10.2",
"@codemirror/language": "6.12.1",
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.6.0",
@@ -52,7 +52,7 @@
"@fullcalendar/list": "6.1.20",
"@fullcalendar/luxon3": "6.1.20",
"@fullcalendar/timegrid": "6.1.20",
"@home-assistant/webawesome": "3.0.0-ha.2",
"@home-assistant/webawesome": "3.2.1-ha.0",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.1.0",
"@lit-labs/observers": "2.1.0",
@@ -132,7 +132,7 @@
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
"ua-parser-js": "2.0.8",
"ua-parser-js": "2.0.9",
"vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
@@ -154,8 +154,8 @@
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.1",
"@rspack/core": "1.7.4",
"@rsdoctor/rspack-plugin": "1.5.2",
"@rspack/core": "1.7.5",
"@rspack/dev-server": "1.2.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.25",
@@ -191,7 +191,7 @@
"eslint-plugin-wc": "3.0.2",
"fancy-log": "2.0.0",
"fs-extra": "11.3.3",
"glob": "13.0.0",
"glob": "13.0.1",
"gulp": "5.0.1",
"gulp-brotli": "3.0.0",
"gulp-json-transform": "0.5.0",
@@ -235,6 +235,6 @@
},
"packageManager": "yarn@4.12.0",
"volta": {
"node": "24.13.0"
"node": "24.13.1"
}
}
+43
View File
@@ -0,0 +1,43 @@
#!/bin/sh
set -e
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
OUT_ROOT="$ROOT_DIR/hass_frontend"
OUT_LATEST="$OUT_ROOT/frontend_latest"
OUT_ES5="$OUT_ROOT/frontend_es5"
bytes_dir() {
if [ -d "$1" ]; then
du -sb "$1" | cut -f1
else
echo 0
fi
}
run_build() {
minifier="$1"
printf "\n==> Building with %s\n" "$minifier"
start_time=$(date +%s)
JS_MINIFIER="$minifier" "$ROOT_DIR/script/build_frontend"
end_time=$(date +%s)
duration=$((end_time - start_time))
latest_size=$(bytes_dir "$OUT_LATEST")
es5_size=$(bytes_dir "$OUT_ES5")
total_size=$(bytes_dir "$OUT_ROOT")
printf "%s|%s|%s|%s\n" "$minifier" "$duration" "$latest_size" "$es5_size" >> "$ROOT_DIR/temp/minifier_benchmark.tsv"
printf " duration: %ss\n" "$duration"
printf " frontend_latest: %s bytes\n" "$latest_size"
printf " frontend_es5: %s bytes\n" "$es5_size"
printf " hass_frontend: %s bytes\n" "$total_size"
}
mkdir -p "$ROOT_DIR/temp"
rm -f "$ROOT_DIR/temp/minifier_benchmark.tsv"
run_build swc
run_build terser
printf "\n==> Summary (minifier | seconds | latest bytes | es5 bytes)\n"
cat "$ROOT_DIR/temp/minifier_benchmark.tsv"
+3
View File
@@ -116,3 +116,6 @@ 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 = " · ";
+159 -44
View File
@@ -3,13 +3,14 @@ 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 } from "../../types";
import type { HomeAssistant, ValuePart } 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";
@@ -51,8 +52,36 @@ 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 localize(`state.default.${state}`);
return [
{
type: "value",
value: localize(`state.default.${state}`),
},
];
}
const domain = computeDomain(entityId);
@@ -73,19 +102,27 @@ export const computeStateDisplayFromEntityAttributes = (
DURATION_UNITS.includes(attributes.unit_of_measurement)
) {
try {
return formatDuration(
locale,
state,
attributes.unit_of_measurement,
entity?.display_precision
);
return [
{
type: "value",
value: 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 {
return formatNumber(state, locale, {
parts = formatNumberToParts(state, locale, {
style: "currency",
currency: attributes.unit_of_measurement,
minimumFractionDigits: 2,
@@ -98,8 +135,34 @@ export const computeStateDisplayFromEntityAttributes = (
} 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,
@@ -114,10 +177,14 @@ export const computeStateDisplayFromEntityAttributes = (
attributes.unit_of_measurement;
if (unit) {
return `${value}${blankBeforeUnit(unit, locale)}${unit}`;
return [
{ type: "value", value: value },
{ type: "literal", value: blankBeforeUnit(unit, locale) },
{ type: "unit", value: unit },
];
}
return value;
return [{ type: "value", value: value }];
}
if (["date", "input_datetime", "time"].includes(domain)) {
@@ -129,36 +196,51 @@ export const computeStateDisplayFromEntityAttributes = (
const components = state.split(" ");
if (components.length === 2) {
// Date and time.
return formatDateTime(
new Date(components.join("T")),
{ ...locale, time_zone: TimeZone.local },
config
);
return [
{
type: "value",
value: formatDateTime(
new Date(components.join("T")),
{ ...locale, time_zone: TimeZone.local },
config
),
},
];
}
if (components.length === 1) {
if (state.includes("-")) {
// Date only.
return formatDate(
new Date(`${state}T00:00`),
{ ...locale, time_zone: TimeZone.local },
config
);
return [
{
type: "value",
value: formatDate(
new Date(`${state}T00:00`),
{ ...locale, time_zone: TimeZone.local },
config
),
},
];
}
if (state.includes(":")) {
// Time only.
const now = new Date();
return formatTime(
new Date(`${now.toISOString().split("T")[0]}T${state}`),
{ ...locale, time_zone: TimeZone.local },
config
);
return [
{
type: "value",
value: formatTime(
new Date(`${now.toISOString().split("T")[0]}T${state}`),
{ ...locale, time_zone: TimeZone.local },
config
),
},
];
}
}
return state;
return [{ type: "value", value: state }];
} catch (_e) {
// Formatting methods may throw error if date parsing doesn't go well,
// just return the state string in that case.
return state;
return [{ type: "value", value: state }];
}
}
@@ -182,25 +264,58 @@ export const computeStateDisplayFromEntityAttributes = (
(domain === "sensor" && attributes.device_class === "timestamp")
) {
try {
return formatDateTime(new Date(state), locale, config);
return [
{
type: "value",
value: formatDateTime(new Date(state), locale, config),
},
];
} catch (_err) {
return state;
return [{ type: "value", value: 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
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
);
};
+19 -10
View File
@@ -5,7 +5,6 @@ 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
@@ -52,7 +51,22 @@ export const formatNumber = (
num: string | number,
localeOptions?: FrontendLocaleData,
options?: Intl.NumberFormatOptions
): string => {
): 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[] => {
const locale = localeOptions
? numberFormatToLocale(localeOptions)
: undefined;
@@ -71,7 +85,7 @@ export const formatNumber = (
return new Intl.NumberFormat(
locale,
getDefaultFormatOptions(num, options)
).format(Number(num));
).formatToParts(Number(num));
}
if (
@@ -86,15 +100,10 @@ export const formatNumber = (
...options,
useGrouping: false,
})
).format(Number(num));
).formatToParts(Number(num));
}
if (typeof num === "string") {
return num;
}
return `${round(num, options?.maximumFractionDigits).toString()}${
options?.style === "currency" ? ` ${options.currency}` : ""
}`;
return [{ type: "literal", value: num }];
};
/**
@@ -0,0 +1,28 @@
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;
};
+16 -1
View File
@@ -12,6 +12,10 @@ export type FormatEntityStateFunc = (
stateObj: HassEntity,
state?: string
) => string;
export type FormatEntityStateToPartsFunc = (
stateObj: HassEntity,
state?: string
) => ValuePart[];
export type FormatEntityAttributeValueFunc = (
stateObj: HassEntity,
attribute: string,
@@ -46,12 +50,13 @@ export const computeFormatFunctions = async (
sensorNumericDeviceClasses: string[]
): Promise<{
formatEntityState: FormatEntityStateFunc;
formatEntityStateToParts: FormatEntityStateToPartsFunc;
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
formatEntityAttributeValueToParts: FormatEntityAttributeValueToPartsFunc;
formatEntityAttributeName: FormatEntityAttributeNameFunc;
formatEntityName: FormatEntityNameFunc;
}> => {
const { computeStateDisplay } =
const { computeStateDisplay, computeStateToParts } =
await import("../entity/compute_state_display");
const {
computeAttributeValueDisplay,
@@ -70,6 +75,16 @@ export const computeFormatFunctions = async (
entities,
state
),
formatEntityStateToParts: (stateObj, state) =>
computeStateToParts(
localize,
stateObj,
locale,
sensorNumericDeviceClasses,
config,
entities,
state
),
formatEntityAttributeValue: (stateObj, attribute, value) =>
computeAttributeValueDisplay(
localize,
+17 -11
View File
@@ -572,6 +572,7 @@ 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;
}
@@ -601,10 +602,25 @@ export class StatisticsChart extends LitElement {
dataValues.push(val);
});
if (!this._hiddenStats.has(statistic_id)) {
pushData(startDate, new Date(stat.end), dataValues);
pushData(
startDate,
endDate.getTime() < endTime.getTime() ? endDate : endTime,
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
@@ -619,16 +635,6 @@ 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)[] = [];
+3 -1
View File
@@ -20,6 +20,7 @@ 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";
@@ -636,7 +637,7 @@ export class HaDataTable extends LitElement {
.map(
([key2, column2], i) =>
html`${i !== 0
? " · "
? STRINGS_SEPARATOR_DOT
: nothing}${column2.template
? column2.template(row)
: row[key2]}`
@@ -1192,6 +1193,7 @@ export class HaDataTable extends LitElement {
.mdc-data-table__cell--numeric {
text-align: var(--float-end);
direction: ltr;
}
.mdc-data-table__cell--icon {
+10 -20
View File
@@ -9,16 +9,7 @@ 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 {
formatNumber,
getNumberFormatOptions,
isNumericState,
} from "../../common/number/format_number";
import {
isUnavailableState,
UNAVAILABLE,
UNKNOWN,
} from "../../data/entity/entity";
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import { timerTimeRemaining } from "../../data/timer";
import type { HomeAssistant } from "../../types";
@@ -180,16 +171,11 @@ export class HaStateLabelBadge extends LitElement {
}
// eslint-disable-next-line: disable=no-fallthrough
default:
return entityState.state === UNKNOWN ||
entityState.state === UNAVAILABLE
return isUnavailableState(entityState.state)
? "—"
: isNumericState(entityState)
? formatNumber(
entityState.state,
this.hass!.locale,
getNumberFormatOptions(entityState, entry)
)
: this.hass!.formatEntityState(entityState);
: this.hass!.formatEntityStateToParts(entityState).find(
(part) => part.type === "value"
)?.value;
}
}
@@ -238,7 +224,11 @@ export class HaStateLabelBadge extends LitElement {
if (domain === "timer") {
return secondsToDuration(_timerTimeRemaining);
}
return entityState.attributes.unit_of_measurement || null;
return (
this.hass!.formatEntityStateToParts(entityState).find(
(part) => part.type === "unit"
)?.value || null
);
}
private _clearInterval() {
+1 -1
View File
@@ -163,7 +163,7 @@ export class HaAreaPicker extends LitElement {
{
id: ADD_NEW_ID + searchString,
primary: this.hass.localize(
"ui.components.area-picker.add_new_sugestion",
"ui.components.area-picker.add_new_suggestion",
{
name: searchString,
}
+2 -2
View File
@@ -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 } from "../types";
import type { HomeAssistant, ValueChangedEvent } 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: CustomEvent<{ value?: string }>) {
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
ev.stopPropagation();
const selected = ev.detail.value;
const normalized =
+1 -10
View File
@@ -89,7 +89,7 @@ export class HaControlSelectMenu extends LitElement {
private _renderOption = (option: SelectOption) =>
html`<ha-dropdown-item
.value=${option.value}
class=${this.value === option.value ? "selected" : ""}
.selected=${this.value === option.value}
>${option.iconPath
? html`<ha-svg-icon slot="icon" .path=${option.iconPath}></ha-svg-icon>`
: option.icon
@@ -263,15 +263,6 @@ 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);
+61 -38
View File
@@ -7,8 +7,9 @@ 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 {
@@ -22,6 +23,8 @@ export class HaDialogDatePicker extends LitElement {
@state() private _params?: DatePickerDialogParams;
@state() private _open = false;
@state() private _value?: string;
public async showDialog(params: DatePickerDialogParams): Promise<void> {
@@ -30,9 +33,14 @@ 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 });
}
@@ -41,7 +49,13 @@ export class HaDialogDatePicker extends LitElement {
if (!this._params) {
return nothing;
}
return html`<ha-dialog open @closed=${this.closeDialog}>
return html`<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
width="small"
without-header
@closed=${this._dialogClosed}
>
<app-datepicker
.value=${this._value}
.min=${this._params.min}
@@ -50,35 +64,40 @@ export class HaDialogDatePicker extends LitElement {
@datepicker-value-updated=${this._valueChanged}
.firstDayOfWeek=${this._params.firstWeekday}
></app-datepicker>
${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>`;
<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>`;
}
private _valueChanged(ev: CustomEvent) {
@@ -108,11 +127,20 @@ export class HaDialogDatePicker extends LitElement {
static styles = [
haStyleDialog,
css`
ha-dialog {
ha-wa-dialog {
--dialog-content-padding: 0;
--justify-action-buttons: space-between;
}
.bottom-actions {
display: flex;
gap: var(--ha-space-4);
justify-content: center;
align-items: center;
width: 100%;
margin-bottom: var(--ha-space-1);
}
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);
@@ -129,11 +157,6 @@ 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%;
+10 -1
View File
@@ -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 } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import "./ha-svg-icon";
/**
@@ -17,6 +17,8 @@ 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
@@ -47,6 +49,13 @@ 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);
}
`,
];
}
+27
View File
@@ -1,3 +1,4 @@
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";
@@ -22,11 +23,37 @@ 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,
+4 -1
View File
@@ -6,6 +6,7 @@ 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;
@@ -152,7 +153,9 @@ class HaDurationInput extends LitElement {
: NaN;
}
private _durationChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
private _durationChanged(
ev: ValueChangedEvent<TimeChangedEvent | undefined>
) {
ev.stopPropagation();
const value = ev.detail.value ? { ...ev.detail.value } : undefined;
+5 -1
View File
@@ -315,9 +315,13 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
}
ha-list {
--mdc-list-item-meta-size: auto;
--mdc-list-side-padding-right: 4px;
--mdc-list-side-padding-right: var(--ha-space-1);
--mdc-list-side-padding-left: var(--ha-space-4);
--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);
}
+3
View File
@@ -179,6 +179,9 @@ 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;
+3
View File
@@ -199,6 +199,9 @@ 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,6 +164,9 @@ 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;
+1 -1
View File
@@ -359,7 +359,7 @@ export class HaFloorPicker extends LitElement {
{
id: ADD_NEW_ID + searchString,
primary: this.hass.localize(
"ui.components.floor-picker.add_new_sugestion",
"ui.components.floor-picker.add_new_suggestion",
{
name: searchString,
}
-1
View File
@@ -9,7 +9,6 @@ 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";
+1 -1
View File
@@ -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_sugestion",
"ui.components.label-picker.add_new_suggestion",
{
name: searchString,
}
-22
View File
@@ -1,22 +0,0 @@
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;
}
}
-52
View File
@@ -1,52 +0,0 @@
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;
}
}
-47
View File
@@ -1,47 +0,0 @@
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;
}
}
+1 -6
View File
@@ -135,9 +135,7 @@ class HaQrScanner extends LitElement {
(camera) => html`
<ha-dropdown-item
.value=${camera.id}
class=${this._selectedCamera === camera.id
? "selected"
: ""}
.selected=${this._selectedCamera === camera.id}
>
${camera.label}
</ha-dropdown-item>
@@ -380,9 +378,6 @@ 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;
+2 -16
View File
@@ -94,10 +94,8 @@ export class HaSelect extends LitElement {
.disabled=${typeof option === "string"
? false
: (option.disabled ?? false)}
class=${this.value ===
(typeof option === "string" ? option : option.value)
? "selected"
: ""}
.selected=${this.value ===
(typeof option === "string" ? option : option.value)}
>
${option.iconPath
? html`<ha-svg-icon
@@ -182,10 +180,6 @@ export class HaSelect extends LitElement {
ha-picker-field.opened {
--mdc-text-field-idle-line-color: var(--primary-color);
}
ha-dropdown-item.selected:hover {
background-color: var(--ha-color-fill-primary-quiet-hover);
}
ha-dropdown-item .content {
display: flex;
gap: var(--ha-space-1);
@@ -200,14 +194,6 @@ export class HaSelect extends LitElement {
ha-dropdown::part(menu) {
min-width: var(--select-menu-width);
}
:host ::slotted(ha-dropdown-item.selected),
ha-dropdown-item.selected {
font-weight: var(--ha-font-weight-medium);
color: var(--primary-color);
background-color: var(--ha-color-fill-primary-quiet-resting);
--icon-primary-color: var(--primary-color);
}
`;
}
declare global {
+2 -1
View File
@@ -5,6 +5,7 @@ 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 {
@@ -69,7 +70,7 @@ export class HaTimeInput extends LitElement {
`;
}
private _timeChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
private _timeChanged(ev: ValueChangedEvent<TimeChangedEvent | undefined>) {
ev.stopPropagation();
const eventValue = ev.detail.value;
+16 -16
View File
@@ -14,6 +14,7 @@ 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";
@@ -197,22 +198,21 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
await this.updateComplete;
requestAnimationFrame(() => {
// 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;
// }
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();
});
};
@@ -24,6 +24,8 @@ import "../ha-icon-button";
import "./hat-logbook-note";
import type { NodeInfo } from "./hat-script-graph";
import { traceTabStyles } from "./trace-tab-styles";
import type { Trigger } from "../../data/automation";
import { migrateAutomationTrigger } from "../../data/automation";
const TRACE_PATH_TABS = [
"step_config",
@@ -166,7 +168,9 @@ export class HaTracePathDetails extends LitElement {
: selectedType === "trigger"
? html`<h2>
${describeTrigger(
currentDetail,
migrateAutomationTrigger({
...currentDetail,
}) as Trigger,
this.hass,
this._entityReg
)}
+4 -3
View File
@@ -32,12 +32,13 @@ 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;
}
`,
];
+5 -20
View File
@@ -14,6 +14,7 @@ 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,
@@ -1431,26 +1432,10 @@ export const getPowerFromState = (stateObj: HassEntity): number | undefined => {
return undefined;
}
// 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;
}
return normalizeValueBySIPrefix(
value,
stateObj.attributes.unit_of_measurement
);
};
/**
+2 -1
View File
@@ -142,7 +142,7 @@ export const subscribeHistory = (
);
};
class HistoryStream {
export class HistoryStream {
hass: HomeAssistant;
hoursToShow?: number;
@@ -221,6 +221,7 @@ 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);
}
}
+6 -2
View File
@@ -41,12 +41,16 @@ export const enum TodoListEntityFeature {
SET_DESCRIPTION_ON_ITEM = 64,
}
export const getTodoLists = (hass: HomeAssistant): TodoList[] =>
export const getTodoLists = (
hass: HomeAssistant,
includeHidden = true
): TodoList[] =>
Object.keys(hass.states)
.filter(
(entityId) =>
computeDomain(entityId) === "todo" &&
!isUnavailableState(hass.states[entityId].state)
!isUnavailableState(hass.states[entityId].state) &&
(includeHidden || hass.entities[entityId]?.hidden !== true)
)
.map((entityId) => ({
...hass.states[entityId],
@@ -213,9 +213,7 @@ class MoreInfoMediaPlayer extends LitElement {
(source) =>
html`<ha-dropdown-item
.value=${source}
class=${source === this.stateObj?.attributes.source
? "selected"
: ""}
.selected=${source === this.stateObj?.attributes.source}
>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
@@ -250,9 +248,7 @@ class MoreInfoMediaPlayer extends LitElement {
(soundMode) =>
html`<ha-dropdown-item
.value=${soundMode}
class=${soundMode === this.stateObj?.attributes.sound_mode
? "selected"
: ""}
.selected=${soundMode === this.stateObj?.attributes.sound_mode}
>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
@@ -678,13 +674,6 @@ 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 {
@@ -313,113 +313,119 @@ class MoreInfoWeather extends LitElement {
</div>
`
: nothing}
<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?.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}
>
${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
${supportedForecasts.map(
(forecastType) =>
html`<ha-tab-group-tab
slot="nav"
.panel=${forecastType}
.active=${this._forecastType === forecastType}
>
${this.hass!.localize(
`ui.card.weather.${forecastType}`
)}
</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"
</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>`
: 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._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>
`
: nothing}
${this.stateObj.attributes.attribution
? html`
<div class="attribution">
@@ -129,11 +129,12 @@ export class CloudStepIntro extends LitElement {
}
.feature .logos {
margin-bottom: 16px;
display: flex;
gap: var(--ha-space-4);
}
.feature .logos > * {
width: 40px;
height: 40px;
margin: 0 4px;
}
.round-icon {
border-radius: var(--ha-border-radius-circle);
@@ -196,7 +196,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
(lang) =>
html`<ha-dropdown-item
.value=${lang.id}
class=${this._language === lang.id ? "selected" : ""}
.selected=${this._language === lang.id}
>
${lang.primary}
</ha-dropdown-item>`
@@ -407,13 +407,6 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
margin-inline-end: 12px;
margin-inline-start: initial;
}
ha-dropdown-item.selected {
border: 1px solid var(--primary-color);
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);
}
`,
];
}
+9
View File
@@ -52,6 +52,7 @@ export interface MockHomeAssistant extends HomeAssistant {
mockEvent(event);
mockTheme(theme: Record<string, string> | null);
formatEntityState(stateObj: HassEntity, state?: string): string;
formatEntityStateToParts(stateObj: HassEntity, state?: string): ValuePart[];
formatEntityAttributeValue(
stateObj: HassEntity,
attribute: string,
@@ -117,6 +118,7 @@ export const provideHass = (
async function updateFormatFunctions() {
const {
formatEntityState,
formatEntityStateToParts,
formatEntityAttributeName,
formatEntityAttributeValue,
formatEntityAttributeValueToParts,
@@ -133,6 +135,7 @@ export const provideHass = (
);
hass().updateHass({
formatEntityState,
formatEntityStateToParts,
formatEntityAttributeName,
formatEntityAttributeValue,
formatEntityAttributeValueToParts,
@@ -375,6 +378,12 @@ export const provideHass = (
floors: {},
formatEntityState: (stateObj, state) =>
(state !== null ? state : stateObj.state) ?? "",
formatEntityStateToParts: (stateObj, state) => [
{
type: "value",
value: (state !== null ? state : stateObj.state) ?? "",
},
],
formatEntityAttributeName: (_stateObj, attribute) => attribute,
formatEntityAttributeValue: (stateObj, attribute, value) =>
value !== null ? value : (stateObj.attributes[attribute] ?? ""),
+3 -1
View File
@@ -74,6 +74,8 @@ export class HAFullCalendar extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ attribute: "add-fab", type: Boolean }) public addFab = false;
@property({ attribute: false }) public events: CalendarEvent[] = [];
@property({ attribute: false }) public calendars: CalendarData[] = [];
@@ -208,7 +210,7 @@ export class HAFullCalendar extends LitElement {
: ""}
<div id="calendar"></div>
${this._hasMutableCalendars
${this.addFab && this._hasMutableCalendars
? html`<ha-fab
slot="fab"
.label=${this.hass.localize("ui.components.calendar.event.add")}
+5
View File
@@ -193,6 +193,7 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
</ha-list-item>`
: nothing}
<ha-full-calendar
add-fab
.events=${this._events}
.calendars=${this._calendars}
.narrow=${this.narrow}
@@ -330,6 +331,8 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
ha-dropdown-item {
padding-left: 32px;
padding-inline-start: 32px;
padding-inline-end: initial;
--icon-primary-color: var(--ha-color-fill-neutral-loud-resting);
}
@@ -339,6 +342,8 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
:host([mobile]) {
padding-left: unset;
padding-inline-start: unset;
padding-inline-end: initial;
}
.loading {
display: flex;
@@ -63,6 +63,7 @@ class SupervisorAppDocumentationDashboard extends LitElement {
margin: auto;
padding: var(--ha-space-2);
max-width: 1024px;
direction: ltr;
}
ha-markdown {
padding: var(--ha-space-4);
@@ -102,7 +102,7 @@ export class HaConditionAction
}
return html`
<ha-dropdown-item .value=${opt} class=${selected ? "selected" : ""}>
<ha-dropdown-item .value=${opt} .selected=${selected}>
<ha-condition-icon
.hass=${this.hass}
slot="icon"
@@ -1,3 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
@@ -42,7 +43,6 @@ import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-button-prev";
import "../../../components/ha-icon-next";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
@@ -657,10 +657,7 @@ class DialogAddAutomationElement
.path=${mdiPlus}
></ha-svg-icon>
</ha-md-list-item>
<ha-md-divider
role="separator"
tabindex="-1"
></ha-md-divider>`
<wa-divider></wa-divider>`
: nothing}
${collections.map(
(collection, index) => html`
@@ -2177,8 +2174,8 @@ class DialogAddAutomationElement
width: var(--ha-space-6);
}
ha-md-list-item.paste {
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
wa-divider {
--spacing: 0;
}
ha-svg-icon.plus {
@@ -47,6 +47,10 @@ import type {
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-dropdown";
import type {
HaDropdown,
HaDropdownSelectEvent,
} from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-filter-blueprints";
@@ -57,10 +61,6 @@ import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-menu";
import type { HaMdMenu } from "../../../components/ha-md-menu";
import "../../../components/ha-md-menu-item";
import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
@@ -84,9 +84,9 @@ import { fullEntitiesContext } from "../../../data/context";
import type { DataTableFilters } from "../../../data/data_table_filters";
import {
deserializeFilters,
serializeFilters,
isFilterUsed,
isRelatedItemsFilterUsed,
serializeFilters,
} from "../../../data/data_table_filters";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type {
@@ -111,16 +111,16 @@ import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route, ServiceCallResponse } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity";
import {
getEntityIdHiddenTableColumn,
getAreaTableColumn,
getCategoryTableColumn,
getLabelsTableColumn,
getTriggeredAtTableColumn,
} from "../common/data-table-columns";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import {
getAreaTableColumn,
getCategoryTableColumn,
getEntityIdHiddenTableColumn,
getLabelsTableColumn,
getTriggeredAtTableColumn,
} from "../common/data-table-columns";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import {
@@ -129,7 +129,6 @@ import {
} from "../voice-assistants/expose/assistants-table-column";
import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants";
import { showNewAutomationDialog } from "./show-dialog-new-automation";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
type AutomationItem = AutomationEntity & {
name: string;
@@ -223,7 +222,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
})
private _activeHiddenColumns?: string[];
@query("#overflow-menu") private _overflowMenu!: HaMdMenu;
@query("#overflow-menu") private _overflowMenu!: HaDropdown;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
@@ -233,6 +232,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
return getAvailableAssistants(this.cloudStatus, this.hass);
}
private _openingOverflow = false;
private _automations = memoizeOne(
(
automations: AutomationEntity[],
@@ -371,16 +372,27 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
);
private _showOverflowMenu = (ev) => {
if (
this._overflowMenu.open &&
ev.target === this._overflowMenu.anchorElement
) {
this._overflowMenu.close();
if (this._overflowMenu.anchorElement === ev.target) {
this._overflowMenu.anchorElement = undefined;
return;
}
this._overflowAutomation = ev.target.automation;
this._openingOverflow = true;
this._overflowMenu.anchorElement = ev.target;
this._overflowMenu.show();
this._overflowAutomation = ev.target.automation;
this._overflowMenu.open = true;
};
private _overflowMenuOpened = () => {
this._openingOverflow = false;
};
private _overflowMenuClosed = () => {
// changing the anchorElement triggers a close event, ignore it
if (this._openingOverflow) {
return;
}
this._overflowMenu.anchorElement = undefined;
};
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
@@ -697,74 +709,58 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage-data-table>
<ha-md-menu id="overflow-menu" positioning="fixed">
<ha-md-menu-item .clickAction=${this._showInfo}>
<ha-svg-icon
.path=${mdiInformationOutline}
slot="start"
></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
</div>
</ha-md-menu-item>
<ha-dropdown
id="overflow-menu"
@wa-select=${this._handleOverflowAction}
@wa-after-show=${this._overflowMenuOpened}
@wa-after-hide=${this._overflowMenuClosed}
>
<ha-dropdown-item value="show_info">
<ha-svg-icon .path=${mdiInformationOutline} slot="icon"></ha-svg-icon>
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
</ha-dropdown-item>
<ha-md-menu-item .clickAction=${this._showSettings}>
<ha-svg-icon .path=${mdiCog} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.show_settings"
)}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._editCategory}>
<ha-svg-icon .path=${mdiTag} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}`
)}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._runActions}>
<ha-svg-icon .path=${mdiPlay} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.editor.run")}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._showTrace}>
<ha-svg-icon .path=${mdiTransitConnection} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.editor.show_trace"
)}
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._duplicate}>
<ha-svg-icon .path=${mdiContentDuplicate} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.picker.duplicate")}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._toggle}>
<ha-dropdown-item value="show_settings">
<ha-svg-icon .path=${mdiCog} slot="icon"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.picker.show_settings"
)}
</ha-dropdown-item>
<ha-dropdown-item value="edit_category">
<ha-svg-icon .path=${mdiTag} slot="icon"></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}`
)}
</ha-dropdown-item>
<ha-dropdown-item value="run_actions">
<ha-svg-icon .path=${mdiPlay} slot="icon"></ha-svg-icon>
${this.hass.localize("ui.panel.config.automation.editor.run")}
</ha-dropdown-item>
<ha-dropdown-item value="show_trace">
<ha-svg-icon .path=${mdiTransitConnection} slot="icon"></ha-svg-icon>
${this.hass.localize("ui.panel.config.automation.editor.show_trace")}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="duplicate">
<ha-svg-icon .path=${mdiContentDuplicate} slot="icon"></ha-svg-icon>
${this.hass.localize("ui.panel.config.automation.picker.duplicate")}
</ha-dropdown-item>
<ha-dropdown-item value="toggle">
<ha-svg-icon
.path=${this._overflowAutomation?.state === "off"
? mdiToggleSwitch
: mdiToggleSwitchOffOutline}
slot="start"
slot="icon"
></ha-svg-icon>
<div slot="headline">
${this._overflowAutomation?.state === "off"
? this.hass.localize("ui.panel.config.automation.editor.enable")
: this.hass.localize("ui.panel.config.automation.editor.disable")}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._deleteConfirm} class="warning">
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.picker.delete")}
</div>
</ha-md-menu-item>
</ha-md-menu>
${this._overflowAutomation?.state === "off"
? this.hass.localize("ui.panel.config.automation.editor.enable")
: this.hass.localize("ui.panel.config.automation.editor.disable")}
</ha-dropdown-item>
<ha-dropdown-item value="delete" variant="danger">
<ha-svg-icon .path=${mdiDelete} slot="icon"></ha-svg-icon>
${this.hass.localize("ui.panel.config.automation.picker.delete")}
</ha-dropdown-item>
</ha-dropdown>
`;
}
@@ -901,33 +897,59 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this._applyFilters();
}
private _showInfo = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
fireEvent(this, "hass-more-info", { entityId: automation.entity_id });
private _handleOverflowAction = (ev: HaDropdownSelectEvent) => {
const action = ev.detail.item.value;
if (!action || !this._overflowAutomation) {
return;
}
switch (action) {
case "show_info":
this._showInfo(this._overflowAutomation);
break;
case "show_settings":
this._showSettings(this._overflowAutomation);
break;
case "edit_category":
this._editCategory(this._overflowAutomation);
break;
case "run_actions":
this._runActions(this._overflowAutomation);
break;
case "show_trace":
this._showTrace(this._overflowAutomation);
break;
case "toggle":
this._toggle(this._overflowAutomation);
break;
case "delete":
this._deleteConfirm(this._overflowAutomation);
break;
case "duplicate":
this._duplicate(this._overflowAutomation);
break;
}
};
private _showSettings = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _showInfo = (automation: AutomationItem) => {
fireEvent(this, "hass-more-info", {
entityId: automation.entity_id,
});
};
private _showSettings = (automation: AutomationItem) => {
fireEvent(this, "hass-more-info", {
entityId: automation.entity_id,
view: "settings",
});
};
private _runActions = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _runActions = (automation: AutomationItem) => {
triggerAutomationActions(this.hass, automation.entity_id);
};
private _editCategory = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _editCategory = (automation: AutomationItem) => {
const entityReg = this._entityReg.find(
(reg) => reg.entity_id === automation.entity_id
);
@@ -948,10 +970,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
});
};
private _showTrace = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _showTrace = (automation: AutomationItem) => {
if (!automation.attributes.id) {
showAlertDialog(this, {
text: this.hass.localize(
@@ -965,20 +984,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
);
};
private _toggle = async (item: HaMdMenuItem): Promise<void> => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _toggle = async (automation: AutomationItem): Promise<void> => {
const service = automation.state === "off" ? "turn_on" : "turn_off";
await this.hass.callService("automation", service, {
entity_id: automation.entity_id,
});
};
private _deleteConfirm = async (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _deleteConfirm = async (automation: AutomationItem) => {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.picker.delete_confirm_title"
@@ -994,9 +1007,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
});
};
private async _delete(automation) {
private async _delete(automation: AutomationItem) {
try {
await deleteAutomation(this.hass, automation.attributes.id);
await deleteAutomation(this.hass, automation.attributes.id!);
this._selected = this._selected.filter(
(entityId) => entityId !== automation.entity_id
);
@@ -1015,14 +1028,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
}
}
private _duplicate = async (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _duplicate = async (automation: AutomationItem) => {
try {
const config = await fetchAutomationFileConfig(
this.hass,
automation.attributes.id
automation.attributes.id!
);
duplicateAutomation(config);
} catch (err: any) {
@@ -438,7 +438,6 @@ export class HaManualAutomationEditor extends SubscribeMixin(LitElement) {
}
private _saveAutomation() {
this.triggerCloseSidebar();
fireEvent(this, "save-automation");
}
-6
View File
@@ -40,9 +40,6 @@ export const rowStyles = css`
.warning ul {
margin: 4px 0;
}
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
}
ha-tooltip {
cursor: default;
}
@@ -272,7 +269,4 @@ export const overflowStyles = css`
display: none;
}
}
ha-md-menu-item {
--mdc-icon-size: 24px;
}
`;
@@ -4,7 +4,8 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
@@ -36,13 +37,20 @@ class LocalBackupLocationDialog extends LitElement {
@state() private _error?: string;
@state() private _open = false;
public async showDialog(
dialogParams: LocalBackupLocationDialogParams
): Promise<void> {
this._dialogParams = dialogParams;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._data = undefined;
this._error = undefined;
this._waiting = undefined;
@@ -55,17 +63,13 @@ class LocalBackupLocationDialog extends LitElement {
return nothing;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
`ui.panel.config.backup.dialogs.local_backup_location.title`
)
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize(
`ui.panel.config.backup.dialogs.local_backup_location.title`
)}
@closed=${this.closeDialog}
@closed=${this._dialogClosed}
>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
@@ -77,34 +81,35 @@ class LocalBackupLocationDialog extends LitElement {
)}
</p>
<ha-form
autofocus
.hass=${this.hass}
.data=${this._data}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
dialogInitialFocus
></ha-form>
<ha-alert alert-type="info">
${this.hass.localize(
`ui.panel.config.backup.dialogs.local_backup_location.note`
)}
</ha-alert>
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
dialogInitialFocus
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
.disabled=${this._waiting || !this._data}
slot="primaryAction"
@click=${this._changeMount}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
.disabled=${this._waiting || !this._data}
slot="primaryAction"
@click=${this._changeMount}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
@@ -143,9 +148,6 @@ class LocalBackupLocationDialog extends LitElement {
haStyle,
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
}
ha-form {
display: block;
margin-bottom: 16px;
@@ -26,15 +26,16 @@ import type {
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import type {
HaDropdown,
HaDropdownSelectEvent,
} from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-filter-states";
import "../../../components/ha-icon";
import "../../../components/ha-icon-next";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-menu";
import type { HaMdMenu } from "../../../components/ha-md-menu";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import type {
@@ -73,7 +74,6 @@ import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
import { downloadBackup } from "./helper/download_backup";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
interface BackupRow extends DataTableRowData, BackupContent {
formatted_type: string;
@@ -123,7 +123,11 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
@query("#overflow-menu") private _overflowMenu?: HaMdMenu;
@query("#overflow-menu") private _overflowMenu?: HaDropdown;
private _openingOverflow = false;
private _overflowBackup?: BackupRow;
public connectedCallback() {
super.connectedCallback();
@@ -287,12 +291,27 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
return;
}
if (this._overflowMenu.open) {
this._overflowMenu.close();
if (this._overflowMenu.anchorElement === ev.target) {
this._overflowMenu.anchorElement = undefined;
return;
}
this._openingOverflow = true;
this._overflowMenu.anchorElement = ev.target;
this._overflowMenu.show();
this._overflowBackup = ev.target.backup;
this._overflowMenu.open = true;
};
private _overflowMenuOpened = () => {
this._openingOverflow = false;
};
private _overflowMenuClosed = () => {
// changing the anchorElement triggers a close event, ignore it
if (this._openingOverflow || !this._overflowMenu) {
return;
}
this._overflowMenu.anchorElement = undefined;
};
private _handleGroupingChanged(ev: CustomEvent) {
@@ -477,16 +496,21 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
`
: nothing}
</hass-tabs-subpage-data-table>
<ha-md-menu id="overflow-menu" positioning="fixed">
<ha-md-menu-item .clickAction=${this._downloadBackup}>
<ha-svg-icon slot="start" .path=${mdiDownload}></ha-svg-icon>
<ha-dropdown
id="overflow-menu"
@wa-select=${this._handleOverflowAction}
@wa-after-show=${this._overflowMenuOpened}
@wa-after-hide=${this._overflowMenuClosed}
>
<ha-dropdown-item value="download">
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
${this.hass.localize("ui.common.download")}
</ha-md-menu-item>
<ha-md-menu-item class="warning" .clickAction=${this._deleteBackup}>
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
</ha-dropdown-item>
<ha-dropdown-item variant="danger" value="delete">
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
${this.hass.localize("ui.common.delete")}
</ha-md-menu-item>
</ha-md-menu>
</ha-dropdown-item>
</ha-dropdown>
`;
}
@@ -556,16 +580,29 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
navigate(`/config/backup/details/${id}`);
}
private _downloadBackup = async (ev): Promise<void> => {
const backup = ev.parentElement.anchorElement.backup;
private _handleOverflowAction = (ev: HaDropdownSelectEvent) => {
const action = ev.detail.item.value;
if (action === "download") {
this._downloadBackup();
return;
}
if (action === "delete") {
this._deleteBackup();
}
};
private _downloadBackup = async (): Promise<void> => {
const backup = this._overflowBackup;
if (!backup) {
return;
}
downloadBackup(this.hass, this, backup, this.config);
};
private _deleteBackup = async (ev): Promise<void> => {
const backup = ev.parentElement.anchorElement.backup;
private _deleteBackup = async (): Promise<void> => {
const backup = this._overflowBackup;
if (!backup) {
return;
}
@@ -6,17 +6,19 @@ import { documentationUrl } from "../../../util/documentation-url";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-code-editor";
import "../../../components/ha-dialog";
import "../../../components/ha-dialog-header";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-markdown";
import "../../../components/ha-spinner";
import "../../../components/ha-textfield";
import "../../../components/ha-wa-dialog";
import type { HaTextField } from "../../../components/ha-textfield";
import type { BlueprintImportResult } from "../../../data/blueprint";
import { importBlueprint, saveBlueprint } from "../../../data/blueprint";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { withViewTransition } from "../../../common/util/view-transition";
@customElement("ha-dialog-import-blueprint")
class DialogImportBlueprint extends LitElement {
@@ -26,6 +28,8 @@ class DialogImportBlueprint extends LitElement {
@state() private _params?;
@state() private _open = false;
@state() private _importing = false;
@state() private _saving = false;
@@ -43,9 +47,14 @@ class DialogImportBlueprint extends LitElement {
this._error = undefined;
this._url = this._params.url;
this.large = false;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._error = undefined;
this._result = undefined;
this._params = undefined;
@@ -59,11 +68,16 @@ class DialogImportBlueprint extends LitElement {
}
const heading = this.hass.localize("ui.panel.config.blueprint.add.header");
return html`
<ha-dialog open .heading=${heading} @closed=${this.closeDialog}>
<ha-dialog-header slot="heading">
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
width=${this.large ? "full" : "medium"}
@closed=${this._dialogClosed}
>
<ha-dialog-header slot="header">
<ha-icon-button
slot="navigationIcon"
dialogAction="cancel"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
@@ -104,6 +118,7 @@ class DialogImportBlueprint extends LitElement {
.label=${this.hass.localize(
"ui.panel.config.blueprint.add.file_name"
)}
autofocus
></ha-textfield>
`}
<ha-expansion-panel
@@ -157,59 +172,63 @@ class DialogImportBlueprint extends LitElement {
"ui.panel.config.blueprint.add.url"
)}
.value=${this._url || ""}
dialogInitialFocus
autofocus
></ha-textfield>
`}
</div>
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._saving}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
${!this._result
? html`
<ha-button
slot="primaryAction"
@click=${this._import}
.disabled=${this._importing}
.loading=${this._importing}
.ariaLabel=${this.hass.localize(
`ui.panel.config.blueprint.add.${this._importing ? "importing" : "import_btn"}`
)}
>
${this.hass.localize(
"ui.panel.config.blueprint.add.import_btn"
)}
</ha-button>
`
: html`
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${this._saving || !!this._result.validation_errors}
.loading=${this._saving}
.ariaLabel=${this.hass.localize(
`ui.panel.config.blueprint.add.${this._saving ? "saving" : this._result.exists ? "save_btn_override" : "save_btn"}`
)}
>
${this._result.exists
? this.hass.localize(
"ui.panel.config.blueprint.add.save_btn_override"
)
: this.hass.localize(
"ui.panel.config.blueprint.add.save_btn"
)}
</ha-button>
`}
</ha-dialog>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._saving}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
${!this._result
? html`
<ha-button
slot="primaryAction"
@click=${this._import}
.disabled=${this._importing}
.loading=${this._importing}
.ariaLabel=${this.hass.localize(
`ui.panel.config.blueprint.add.${this._importing ? "importing" : "import_btn"}`
)}
>
${this.hass.localize(
"ui.panel.config.blueprint.add.import_btn"
)}
</ha-button>
`
: html`
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${this._saving || !!this._result.validation_errors}
.loading=${this._saving}
.ariaLabel=${this.hass.localize(
`ui.panel.config.blueprint.add.${this._saving ? "saving" : this._result.exists ? "save_btn_override" : "save_btn"}`
)}
>
${this._result.exists
? this.hass.localize(
"ui.panel.config.blueprint.add.save_btn_override"
)
: this.hass.localize(
"ui.panel.config.blueprint.add.save_btn"
)}
</ha-button>
`}
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
private _enlarge() {
this.large = !this.large;
withViewTransition(() => {
this.large = !this.large;
});
}
private async _import() {
@@ -273,10 +292,6 @@ class DialogImportBlueprint extends LitElement {
a ha-svg-icon {
--mdc-icon-size: 16px;
}
:host([large]) ha-dialog {
--mdc-dialog-min-width: 90vw;
--mdc-dialog-max-width: 90vw;
}
ha-expansion-panel {
--expansion-panel-content-padding: 0px;
}
@@ -153,7 +153,7 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
{
id: ADD_NEW_ID + searchString,
primary: this.hass.localize(
"ui.components.category-picker.add_new_sugestion",
"ui.components.category-picker.add_new_suggestion",
{
name: searchString,
}
@@ -0,0 +1,49 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../layouts/hass-subpage";
import "./ai-task-pref";
import type { HomeAssistant } from "../../../types";
@customElement("ha-config-section-ai-tasks")
class HaConfigSectionAITasks extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
protected render() {
return html`
<hass-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.header=${this.hass.localize("ui.panel.config.ai_tasks.caption")}
>
<div class="content">
<ai-task-pref
.hass=${this.hass}
.narrow=${this.narrow}
></ai-task-pref>
</div>
</hass-subpage>
`;
}
static styles = css`
.content {
padding: var(--ha-space-7) var(--ha-space-5) 0;
max-width: 1040px;
margin: 0 auto;
}
ai-task-pref {
max-width: 600px;
margin: 0 auto;
display: block;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-config-section-ai-tasks": HaConfigSectionAITasks;
}
}
@@ -71,6 +71,7 @@ class HaConfigSectionAnalytics extends LitElement {
display: block;
max-width: 600px;
margin: 0 auto;
margin-bottom: 24px;
}
`;
}
@@ -1,6 +1,6 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { UNIT_C } from "../../../common/const";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { navigate } from "../../../common/navigate";
@@ -17,8 +17,6 @@ import "../../../components/ha-formfield";
import "../../../components/ha-language-picker";
import "../../../components/ha-radio";
import type { HaRadio } from "../../../components/ha-radio";
import "../../../components/ha-select";
import "../../../components/ha-settings-row";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import "../../../components/ha-timezone-picker";
@@ -26,8 +24,7 @@ import type { ConfigUpdateValues } from "../../../data/core";
import { saveCoreConfig } from "../../../data/core";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-subpage";
import "./ai-task-pref";
import type { AITaskPref } from "./ai-task-pref";
import "../../../components/map/ha-map";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@@ -37,7 +34,9 @@ class HaConfigSectionGeneral extends LitElement {
@property({ type: Boolean }) public narrow = false;
@state() private _submitting = false;
@state() private _submittingName = false;
@state() private _submittingRegional = false;
@state() private _unitSystem?: ConfigUpdateValues["unit_system"];
@@ -59,13 +58,10 @@ class HaConfigSectionGeneral extends LitElement {
@state() private _updateUnits?: boolean;
@query("ai-task-pref") private _aiTaskPref!: AITaskPref;
protected render(): TemplateResult {
const canEdit = ["storage", "default"].includes(
this.hass.config.config_source
);
const disabled = this._submitting || !canEdit;
return html`
<hass-subpage
back-path="/config/system"
@@ -77,211 +73,269 @@ class HaConfigSectionGeneral extends LitElement {
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-card outlined>
<div class="card-content">
${!canEdit
? html`
<ha-alert>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
)}
</ha-alert>
`
: nothing}
<ha-textfield
name="name"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.location_name"
)}
.disabled=${disabled}
.value=${this._name}
@change=${this._handleChange}
></ha-textfield>
<ha-timezone-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.time_zone"
)}
name="timeZone"
.disabled=${disabled}
.value=${this._timeZone}
@value-changed=${this._handleValueChanged}
hide-clear-icon
></ha-timezone-picker>
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation"
)}
name="elevation"
type="number"
.disabled=${disabled}
.value=${this._elevation}
.suffix=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_meters"
)}
@change=${this._handleChange}
>
</ha-textfield>
<div>
<div>
${!canEdit
? html`
<ha-alert>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system"
"ui.panel.config.core.section.core.core_config.edit_requires_storage"
)}
</div>
<ha-formfield
.label=${html`
<span style="font-size: 14px">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.metric_example"
)}
</span>
<div style="color: var(--secondary-text-color)">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_metric"
)}
</div>
`}
>
<ha-radio
name="unit_system"
value="metric"
.checked=${this._unitSystem === "metric"}
@change=${this._unitSystemChanged}
.disabled=${disabled}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`
<span style="font-size: 14px">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.us_customary_example"
)}
</span>
<div style="color: var(--secondary-text-color)">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_us_customary"
)}
</div>
`}
>
<ha-radio
name="unit_system"
value="us_customary"
.checked=${this._unitSystem === "us_customary"}
@change=${this._unitSystemChanged}
.disabled=${disabled}
></ha-radio>
</ha-formfield>
${this._unitSystem !== this._configuredUnitSystem()
? html`
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_label"
)}
>
<ha-checkbox
.checked=${this._updateUnits}
.disabled=${this._submitting}
@change=${this._updateUnitsChanged}
></ha-checkbox>
</ha-formfield>
<div class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_text_1"
)}
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_text_2"
)} <br /><br />
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_text_3"
)}
</div>
`
: ""}
</div>
<div>
<ha-currency-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.currency"
)}
name="currency"
.disabled=${disabled}
.value=${this._currency}
@value-changed=${this._handleValueChanged}
></ha-currency-picker>
<a
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
target="_blank"
rel="noopener noreferrer"
class="find-value"
>${this.hass.localize(
"ui.panel.config.core.section.core.core_config.find_currency_value"
)}</a
>
</div>
<ha-country-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.country"
)}
name="country"
.disabled=${disabled}
.value=${this._country}
@value-changed=${this._handleValueChanged}
></ha-country-picker>
<ha-language-picker
.hass=${this.hass}
native-name
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.language"
)}
name="language"
.value=${this._language}
.disabled=${disabled}
@closed=${stopPropagation}
@value-changed=${this._handleValueChanged}
>
</ha-language-picker>
</div>
<ha-settings-row>
<div slot="heading">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.edit_location"
)}
</div>
<div slot="description" class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.edit_location_description"
)}
</div>
<ha-button
appearance="plain"
size="small"
@click=${this._editLocation}
.disabled=${disabled}
>${this.hass.localize("ui.common.edit")}</ha-button
>
</ha-settings-row>
<div class="card-actions">
<ha-progress-button
@click=${this._updateEntry}
.disabled=${disabled}
>
${this.hass!.localize("ui.common.save")}
</ha-progress-button>
</div>
</ha-card>
<ai-task-pref
.hass=${this.hass}
.narrow=${this.narrow}
></ai-task-pref>
</ha-alert>
`
: nothing}
${this._renderHomeNameCard(canEdit)}
${this._renderLocationCard(canEdit)}
${this._renderRegionalSettingsCard(canEdit)}
</div>
</hass-subpage>
`;
}
private _renderHomeNameCard(canEdit: boolean): TemplateResult {
const disabled = this._submittingName || !canEdit;
return html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.core.section.core.home_name_card.header"
)}
>
<div class="card-content">
<ha-textfield
name="name"
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.location_name"
)}
.disabled=${disabled}
.value=${this._name}
@change=${this._handleChange}
></ha-textfield>
</div>
<div class="card-actions">
<ha-progress-button
appearance="filled"
@click=${this._updateHomeName}
.disabled=${disabled}
>
${this.hass.localize("ui.common.save")}
</ha-progress-button>
</div>
</ha-card>
`;
}
private _renderLocationCard(canEdit: boolean): TemplateResult {
const hasHomeZone = "zone.home" in this.hass.states;
return html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.core.section.core.location_card.header"
)}
>
${hasHomeZone
? html`
<div class="card-content">
<ha-map
.hass=${this.hass}
.entities=${["zone.home"]}
.zoom=${14}
.autoFit=${true}
.fitZones=${true}
.themeMode=${"auto"}
.renderPassive=${false}
.interactiveZones=${false}
class="map-preview"
></ha-map>
</div>
`
: nothing}
<div class="card-actions">
<ha-button
appearance="filled"
@click=${this._editLocation}
.disabled=${!canEdit}
>
${this.hass.localize("ui.common.edit")}
</ha-button>
</div>
</ha-card>
`;
}
private _renderRegionalSettingsCard(canEdit: boolean): TemplateResult {
const disabled = this._submittingRegional || !canEdit;
return html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.core.section.core.regional_settings_card.header"
)}
>
<div class="card-content">
<ha-timezone-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.time_zone"
)}
name="timeZone"
.disabled=${disabled}
.value=${this._timeZone}
@value-changed=${this._handleValueChanged}
hide-clear-icon
></ha-timezone-picker>
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation"
)}
name="elevation"
type="number"
.disabled=${disabled}
.value=${this._elevation}
.suffix=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.elevation_meters"
)}
@change=${this._handleChange}
>
</ha-textfield>
<div>
<div>
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system"
)}
</div>
<ha-formfield
.label=${html`
<span style="font-size: 14px">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.metric_example"
)}
</span>
<div style="color: var(--secondary-text-color)">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_metric"
)}
</div>
`}
>
<ha-radio
name="unit_system"
value="metric"
.checked=${this._unitSystem === "metric"}
@change=${this._unitSystemChanged}
.disabled=${disabled}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`
<span style="font-size: 14px">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.us_customary_example"
)}
</span>
<div style="color: var(--secondary-text-color)">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.unit_system_us_customary"
)}
</div>
`}
>
<ha-radio
name="unit_system"
value="us_customary"
.checked=${this._unitSystem === "us_customary"}
@change=${this._unitSystemChanged}
.disabled=${disabled}
></ha-radio>
</ha-formfield>
${this._unitSystem !== this._configuredUnitSystem()
? html`
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_label"
)}
>
<ha-checkbox
.checked=${this._updateUnits}
.disabled=${this._submittingRegional}
@change=${this._updateUnitsChanged}
></ha-checkbox>
</ha-formfield>
<div class="secondary">
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_text_1"
)}
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_text_2"
)}
<br /><br />
${this.hass.localize(
"ui.panel.config.core.section.core.core_config.update_units_text_3"
)}
</div>
`
: ""}
</div>
<div>
<ha-currency-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.currency"
)}
name="currency"
.disabled=${disabled}
.value=${this._currency}
@value-changed=${this._handleValueChanged}
></ha-currency-picker>
<a
href="https://en.wikipedia.org/wiki/ISO_4217#Active_codes"
target="_blank"
rel="noopener noreferrer"
class="find-value"
>${this.hass.localize(
"ui.panel.config.core.section.core.core_config.find_currency_value"
)}</a
>
</div>
<ha-country-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.country"
)}
name="country"
.disabled=${disabled}
.value=${this._country}
@value-changed=${this._handleValueChanged}
></ha-country-picker>
<ha-language-picker
.hass=${this.hass}
native-name
.label=${this.hass.localize(
"ui.panel.config.core.section.core.core_config.language"
)}
name="language"
.value=${this._language}
.disabled=${disabled}
@closed=${stopPropagation}
@value-changed=${this._handleValueChanged}
>
</ha-language-picker>
</div>
<div class="card-actions">
<ha-progress-button
appearance="filled"
@click=${this._updateRegionalSettings}
.disabled=${disabled}
>
${this.hass.localize("ui.common.save")}
</ha-progress-button>
</div>
</ha-card>
`;
}
private _configuredUnitSystem() {
return this.hass.config.unit_system.temperature === UNIT_C
? "metric"
@@ -297,12 +351,6 @@ class HaConfigSectionGeneral extends LitElement {
this._timeZone = this.hass.config.time_zone || "Etc/GMT";
this._name = this.hass.config.location_name;
this._updateUnits = true;
if (window.location.hash === "#ai-task") {
this._aiTaskPref.updateComplete.then(() => {
this._aiTaskPref.scrollIntoView();
});
}
}
private _handleValueChanged(ev: ValueChangedEvent<string>) {
@@ -325,7 +373,31 @@ class HaConfigSectionGeneral extends LitElement {
this._updateUnits = (ev.target as HaCheckbox).checked;
}
private async _updateEntry(ev: CustomEvent) {
private async _updateHomeName(ev: CustomEvent) {
const button = ev.target as HaProgressButton;
if (button.progress) {
return;
}
button.progress = true;
this._submittingName = true;
this._error = undefined;
try {
await saveCoreConfig(this.hass, {
location_name: this._name,
});
button.actionSuccess();
} catch (err: any) {
button.actionError();
this._error = err.message;
} finally {
button.progress = false;
this._submittingName = false;
}
}
private async _updateRegionalSettings(ev: CustomEvent) {
const button = ev.target as HaProgressButton;
if (button.progress) {
return;
@@ -350,6 +422,8 @@ class HaConfigSectionGeneral extends LitElement {
}
}
button.progress = true;
this._submittingRegional = true;
this._error = undefined;
let locationConfig;
@@ -364,14 +438,13 @@ class HaConfigSectionGeneral extends LitElement {
try {
await saveCoreConfig(this.hass, {
currency: this._currency,
time_zone: this._timeZone,
elevation: Number(this._elevation),
unit_system: this._unitSystem,
update_units: this._updateUnits && unitSystemChanged,
time_zone: this._timeZone,
location_name: this._name,
language: this._language,
currency: this._currency,
country: this._country,
language: this._language,
...locationConfig,
});
button.actionSuccess();
@@ -380,6 +453,7 @@ class HaConfigSectionGeneral extends LitElement {
this._error = err.message;
} finally {
button.progress = false;
this._submittingRegional = false;
}
}
@@ -391,48 +465,39 @@ class HaConfigSectionGeneral extends LitElement {
haStyle,
css`
.content {
padding: 28px 20px 0;
padding: var(--ha-space-7) var(--ha-space-5) 0;
max-width: 1040px;
margin: 0 auto;
}
ha-card,
ai-task-pref {
ha-card {
max-width: 600px;
margin: 0 auto;
height: 100%;
justify-content: space-between;
flex-direction: column;
display: flex;
}
ha-card,
ai-task-pref {
margin-bottom: 24px;
margin: 0 auto var(--ha-space-6);
}
.card-content {
display: flex;
justify-content: space-between;
flex-direction: column;
padding: 16px 16px 0 16px;
}
.card-actions {
text-align: right;
height: 48px;
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 16px;
}
.card-content > * {
display: block;
margin-top: 16px;
}
ha-select {
display: block;
.card-content > *:not(:first-child) {
margin-top: var(--ha-space-4);
}
.card-actions {
display: flex;
justify-content: flex-end;
}
a.find-value {
margin-top: 8px;
margin-top: var(--ha-space-2);
display: inline-block;
}
.map-preview {
height: 200px;
width: 100%;
display: block;
border-radius: var(--ha-card-border-radius, 8px);
overflow: hidden;
}
`,
];
}
@@ -87,7 +87,9 @@ class HaConfigSystemNavigation extends LitElement {
description = this._storageInfo
? this.hass.localize("ui.panel.config.storage.description", {
percent_used: `${Math.round(
(this._storageInfo.used / this._storageInfo.total) * 100
((this._storageInfo.total - this._storageInfo.free) /
this._storageInfo.total) *
100
)}${blankBeforePercent(this.hass.locale)}%`,
free_space: `${this._storageInfo.free} GB`,
})
@@ -5,7 +5,8 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-wa-dialog";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@@ -21,14 +22,21 @@ export class DialogJoinBeta
@state() private _dialogParams?: JoinBetaDialogParams;
@state() private _open = false;
public showDialog(dialogParams: JoinBetaDialogParams): void {
this._dialogParams = dialogParams;
this._open = true;
}
public closeDialog() {
this._open = false;
return true;
}
private _dialogClosed() {
this._dialogParams = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
protected render() {
@@ -37,13 +45,11 @@ export class DialogJoinBeta
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.join_beta_channel.title")
)}
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize("ui.dialogs.join_beta_channel.title")}
@closed=${this._dialogClosed}
>
<ha-alert alert-type="warning">
${this.hass.localize("ui.dialogs.join_beta_channel.backup")}
@@ -67,17 +73,19 @@ export class DialogJoinBeta
)}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>
<ha-button
appearance="plain"
slot="primaryAction"
@click=${this._cancel}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._join}>
${this.hass.localize("ui.dialogs.join_beta_channel.join")}
</ha-button>
</ha-dialog>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this._cancel}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._join}>
${this.hass.localize("ui.dialogs.join_beta_channel.join")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
@@ -9,11 +9,11 @@ import type {
LocalizeFunc,
LocalizeKeys,
} from "../../../common/translations/localize";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-wa-dialog";
import "../../../components/search-input";
import type { LovelaceConfig } from "../../../data/lovelace/config/types";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import { haStyleScrollbar } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { generateDefaultView } from "../../lovelace/views/default-view";
import "./dashboard-card";
@@ -65,7 +65,7 @@ const STRATEGIES = [
class DialogNewDashboard extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false;
@state() private _open = false;
@state() private _params?: NewDashboardDialogParams;
@@ -77,7 +77,7 @@ class DialogNewDashboard extends LitElement implements HassDialog {
})[] = [];
public showDialog(params: NewDashboardDialogParams): void {
this._opened = true;
this._open = true;
this._params = params;
this._localizedStrategies = STRATEGIES.map((strategy) => ({
...strategy,
@@ -89,14 +89,15 @@ class DialogNewDashboard extends LitElement implements HassDialog {
}
public closeDialog() {
if (this._opened) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._opened = false;
this._params = undefined;
this._open = false;
return true;
}
private _dialogClosed(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _generateDefaultConfig = memoizeOne(
(localize: LocalizeFunc): LovelaceConfig => ({
views: [generateDefaultView(localize, true)],
@@ -104,98 +105,100 @@ class DialogNewDashboard extends LitElement implements HassDialog {
);
protected render() {
if (!this._opened) {
if (!this._params) {
return nothing;
}
const defaultConfig = this._generateDefaultConfig(this.hass.localize);
return html`
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.header`
)
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
flexcontent
width="large"
header-title=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.header`
)}
@closed=${this._dialogClosed}
>
<search-input
.hass=${this.hass}
.label=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.search_dashboards`
)}
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
></search-input>
<div class="content">
${this._filter
? html`
<div class="cards-container">
${this._filterStrategies(
this._localizedStrategies,
this._filter
).map(
(strategy) => html`
<dashboard-card
.name=${strategy.localizedName}
.description=${strategy.localizedDescription}
.img=${this.hass.themes.darkMode
? strategy.images.dark
: strategy.images.light}
.alt=${strategy.localizedName}
@click=${this._selected}
.strategy=${strategy.type}
></dashboard-card>
`
)}
</div>
`
: html`
<div class="cards-container">
<dashboard-card
.name=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty`
)}
.description=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty_description`
)}
.img=${this.hass.themes.darkMode
? "/static/images/dashboard-options/dark/icon-dashboard-new.svg"
: "/static/images/dashboard-options/light/icon-dashboard-new.svg"}
.alt=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty`
)}
@click=${this._selected}
.config=${defaultConfig}
></dashboard-card>
</div>
<div class="cards-container">
<div class="cards-container-header">
${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.heading.default`
<div class="content-wrapper">
<search-input
autofocus
.hass=${this.hass}
.label=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.search_dashboards`
)}
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
></search-input>
<div class="content ha-scrollbar">
${this._filter
? html`
<div class="cards-container">
${this._filterStrategies(
this._localizedStrategies,
this._filter
).map(
(strategy) => html`
<dashboard-card
.name=${strategy.localizedName}
.description=${strategy.localizedDescription}
.img=${this.hass.themes.darkMode
? strategy.images.dark
: strategy.images.light}
.alt=${strategy.localizedName}
@click=${this._selected}
.strategy=${strategy.type}
></dashboard-card>
`
)}
</div>
${this._localizedStrategies.map(
(strategy) => html`
<dashboard-card
.name=${strategy.localizedName}
.description=${strategy.localizedDescription}
.img=${this.hass.themes.darkMode
? strategy.images.dark
: strategy.images.light}
.alt=${strategy.localizedName}
@click=${this._selected}
.strategy=${strategy.type}
></dashboard-card>
`
)}
</div>
`}
`
: html`
<div class="cards-container">
<dashboard-card
.name=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty`
)}
.description=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty_description`
)}
.img=${this.hass.themes.darkMode
? "/static/images/dashboard-options/dark/icon-dashboard-new.svg"
: "/static/images/dashboard-options/light/icon-dashboard-new.svg"}
.alt=${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.create_empty`
)}
@click=${this._selected}
.config=${defaultConfig}
></dashboard-card>
</div>
<div class="cards-container">
<div class="cards-container-header">
${this.hass.localize(
`ui.panel.config.lovelace.dashboards.dialog_new.heading.default`
)}
</div>
${this._localizedStrategies.map(
(strategy) => html`
<dashboard-card
.name=${strategy.localizedName}
.description=${strategy.localizedDescription}
.img=${this.hass.themes.darkMode
? strategy.images.dark
: strategy.images.light}
.alt=${strategy.localizedName}
@click=${this._selected}
.strategy=${strategy.type}
></dashboard-card>
`
)}
</div>
`}
</div>
</div>
</ha-dialog>
</ha-wa-dialog>
`;
}
@@ -253,33 +256,16 @@ class DialogNewDashboard extends LitElement implements HassDialog {
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
haStyleScrollbar,
css`
@media all and (max-width: 450px), all and (max-height: 500px) {
/* overrule the ha-style-dialog max-height on small screens */
ha-dialog {
--mdc-dialog-max-height: 100%;
height: 100%;
}
}
@media all and (min-width: 850px) {
ha-dialog {
--mdc-dialog-min-width: 845px;
--mdc-dialog-min-height: calc(
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
);
--mdc-dialog-max-height: calc(
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
);
}
}
ha-dialog {
--mdc-dialog-max-width: 845px;
ha-wa-dialog {
--dialog-content-padding: 0;
--dialog-z-index: 6;
--ha-dialog-min-height: 60svh;
}
ha-wa-dialog::part(body) {
overflow: hidden;
min-height: 0;
}
.cards-container-header {
font-size: var(--ha-font-size-l);
@@ -315,8 +301,17 @@ class DialogNewDashboard extends LitElement implements HassDialog {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
margin-top: 20px;
}
.content-wrapper {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.content {
padding: 0 24px 0 24px;
padding: 0 var(--ha-space-6) var(--ha-space-6) var(--ha-space-6);
flex: 1;
min-height: 0;
overflow: auto;
}
`,
];
@@ -2,6 +2,7 @@ import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../components/ha-card";
import "../../../../components/ha-button";
import "../../../../components/ha-md-list";
import "../../../../components/entity/ha-entity-picker";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../resources/styles";
@@ -22,8 +23,6 @@ import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_regi
class HaPanelDevDebug extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@state() private _entityId?: string;
protected render() {
@@ -34,14 +33,14 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
"ui.panel.config.developer-tools.tabs.debug.title"
)}
>
<ha-debug-connection-row
.hass=${this.hass}
.narrow=${this.narrow}
></ha-debug-connection-row>
<ha-debug-disable-view-transition-row
.hass=${this.hass}
.narrow=${this.narrow}
></ha-debug-disable-view-transition-row>
<ha-md-list>
<ha-debug-connection-row
.hass=${this.hass}
></ha-debug-connection-row>
<ha-debug-disable-view-transition-row
.hass=${this.hass}
></ha-debug-disable-view-transition-row>
</ha-md-list>
</ha-card>
<ha-card
.header=${this.hass.localize(
@@ -128,6 +127,11 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
max-width: 600px;
margin: 0 auto;
}
ha-md-list {
padding-top: 0;
padding-bottom: 0;
background: none;
}
`,
];
}
@@ -1,7 +1,7 @@
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-settings-row";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-switch";
import type { HaSwitch } from "../../../../components/ha-switch";
import type { HomeAssistant } from "../../../../types";
@@ -11,26 +11,25 @@ import { storeState } from "../../../../util/ha-pref-storage";
class HaDebugConnectionRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
protected render(): TemplateResult {
return html`
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
${this.hass.localize(
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.debug_connection.title"
)}
</span>
<span slot="description">
${this.hass.localize(
)}</span
>
<span slot="supporting-text"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.debug_connection.description"
)}
</span>
)}</span
>
<ha-switch
slot="end"
.checked=${this.hass.debugConnection}
@change=${this._checkedChanged}
></ha-switch>
</ha-settings-row>
</ha-md-list-item>
`;
}
@@ -3,7 +3,7 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { storage } from "../../../../common/decorators/storage";
import { setViewTransitionDisabled } from "../../../../common/util/view-transition";
import "../../../../components/ha-settings-row";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-switch";
import type { HaSwitch } from "../../../../components/ha-switch";
import type { HomeAssistant } from "../../../../types";
@@ -12,29 +12,28 @@ import type { HomeAssistant } from "../../../../types";
class HaDebugDisableViewTransitionRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@storage({ key: "disableViewTransition", state: true, subscribe: false })
private _disabled = false;
protected render(): TemplateResult {
return html`
<ha-settings-row .narrow=${this.narrow}>
<span slot="heading">
${this.hass.localize(
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.disable_view_transition.title"
)}
</span>
<span slot="description">
${this.hass.localize(
)}</span
>
<span slot="supporting-text"
>${this.hass.localize(
"ui.panel.config.developer-tools.tabs.debug.disable_view_transition.description"
)}
</span>
)}</span
>
<ha-switch
slot="end"
.checked=${this._disabled}
@change=${this._checkedChanged}
></ha-switch>
</ha-settings-row>
</ha-md-list-item>
`;
}
@@ -10,10 +10,10 @@ import {
mdiUnfoldMoreHorizontal,
} from "@mdi/js";
import "@home-assistant/webawesome/dist/components/divider/divider";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, type CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
@@ -28,15 +28,11 @@ import type {
SortingDirection,
} from "../../../../components/data-table/ha-data-table";
import { showDataTableSettingsDialog } from "../../../../components/data-table/show-dialog-data-table-settings";
import "@home-assistant/webawesome/dist/components/divider/divider";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import type { HaMdMenu } from "../../../../components/ha-md-menu";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-md-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/search-input-outlined";
import type {
StatisticsMetaData,
@@ -112,20 +108,8 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
@query("ha-data-table", true) private _dataTable!: HaDataTable;
@query("#group-by-menu") private _groupByMenu!: HaMdMenu;
@query("#sort-by-menu") private _sortByMenu!: HaMdMenu;
@query("search-input-outlined") private _searchInput!: HTMLElement;
private _toggleGroupBy() {
this._groupByMenu.open = !this._groupByMenu.open;
}
private _toggleSortBy() {
this._sortByMenu.open = !this._sortByMenu.open;
}
protected firstUpdated() {
this._validateStatistics();
}
@@ -278,37 +262,106 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
const sortByMenu = Object.values(columns).find((col) => col.sortable)
? html`
<ha-assist-chip
.label=${localize("ui.components.subpage-data-table.sort_by", {
sortColumn: this._sortColumn
? ` ${columns[this._sortColumn]?.title || columns[this._sortColumn]?.label}` ||
""
: "",
})}
id="sort-by-anchor"
@click=${this._toggleSortBy}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
<ha-dropdown @wa-select=${this._handleSortBy}>
<ha-assist-chip
slot="trigger"
.label=${localize("ui.components.subpage-data-table.sort_by", {
sortColumn: this._sortColumn
? ` ${columns[this._sortColumn]?.title || columns[this._sortColumn]?.label}` ||
""
: "",
})}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${Object.entries(columns).map(([id, column]) =>
column.sortable
? html`
<ha-dropdown-item
.value=${id}
.selected=${id === this._sortColumn}
>
${this._sortColumn === id
? html`
<ha-svg-icon
slot="details"
.path=${this._sortDirection === "desc"
? mdiArrowDown
: mdiArrowUp}
></ha-svg-icon>
`
: nothing}
${column.title || column.label}
</ha-dropdown-item>
`
: nothing
)}
</ha-dropdown>
`
: nothing;
const groupByMenu = Object.values(columns).find((col) => col.groupable)
? html`
<ha-assist-chip
.label=${localize("ui.components.subpage-data-table.group_by", {
groupColumn: this._groupColumn
? ` ${columns[this._groupColumn].title || columns[this._groupColumn].label}`
: "",
})}
id="group-by-anchor"
@click=${this._toggleGroupBy}
>
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon
></ha-assist-chip>
<ha-dropdown @wa-select=${this._handleOverflowGroupBy}>
<ha-assist-chip
slot="trigger"
.label=${localize("ui.components.subpage-data-table.group_by", {
groupColumn: this._groupColumn
? ` ${columns[this._groupColumn].title || columns[this._groupColumn].label}`
: "",
})}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
${Object.entries(columns).map(([id, column]) =>
column.groupable
? html`
<ha-dropdown-item
.value=${id}
.selected=${id === this._groupColumn}
>
${column.title || column.label}
</ha-dropdown-item>
`
: nothing
)}
<ha-dropdown-item
value="none"
.selected=${this._groupColumn === undefined}
>
${localize("ui.components.subpage-data-table.dont_group_by")}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item
value="collapse_all"
@click=${this._collapseAllGroups}
.disabled=${this._groupColumn === undefined}
>
<ha-svg-icon
slot="icon"
.path=${mdiUnfoldLessHorizontal}
></ha-svg-icon>
${localize(
"ui.components.subpage-data-table.collapse_all_groups"
)}
</ha-dropdown-item>
<ha-dropdown-item
@click=${this._expandAllGroups}
.disabled=${this._groupColumn === undefined}
>
<ha-svg-icon
slot="icon"
.path=${mdiUnfoldMoreHorizontal}
></ha-svg-icon>
${localize("ui.components.subpage-data-table.expand_all_groups")}
</ha-dropdown-item>
</ha-dropdown>
`
: nothing;
@@ -417,6 +470,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
.hiddenColumns=${this.hiddenColumns}
@row-click=${this._rowClicked}
@selection-changed=${this._handleSelectionChanged}
@sorting-changed=${this._handleTableSortingChanged}
>
${!this.narrow
? html`
@@ -434,82 +488,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
</div>`}
</ha-data-table>
</div>
<ha-md-menu
anchor="group-by-anchor"
id="group-by-menu"
positioning="fixed"
>
${Object.entries(columns).map(([id, column]) =>
column.groupable
? html`
<ha-md-menu-item
.value=${id}
@click=${this._handleGroupBy}
.selected=${id === this._groupColumn}
class=${classMap({ selected: id === this._groupColumn })}
>
${column.title || column.label}
</ha-md-menu-item>
`
: nothing
)}
<ha-md-menu-item
.value=${undefined}
@click=${this._handleGroupBy}
.selected=${this._groupColumn === undefined}
class=${classMap({ selected: this._groupColumn === undefined })}
>
${localize("ui.components.subpage-data-table.dont_group_by")}
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
@click=${this._collapseAllGroups}
.disabled=${this._groupColumn === undefined}
>
<ha-svg-icon
slot="start"
.path=${mdiUnfoldLessHorizontal}
></ha-svg-icon>
${localize("ui.components.subpage-data-table.collapse_all_groups")}
</ha-md-menu-item>
<ha-md-menu-item
@click=${this._expandAllGroups}
.disabled=${this._groupColumn === undefined}
>
<ha-svg-icon
slot="start"
.path=${mdiUnfoldMoreHorizontal}
></ha-svg-icon>
${localize("ui.components.subpage-data-table.expand_all_groups")}
</ha-md-menu-item>
</ha-md-menu>
<ha-md-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed">
${Object.entries(columns).map(([id, column]) =>
column.sortable
? html`
<ha-md-menu-item
.value=${id}
@click=${this._handleSortBy}
keep-open
.selected=${id === this._sortColumn}
class=${classMap({ selected: id === this._sortColumn })}
>
${this._sortColumn === id
? html`
<ha-svg-icon
slot="end"
.path=${this._sortDirection === "desc"
? mdiArrowDown
: mdiArrowUp}
></ha-svg-icon>
`
: nothing}
${column.title || column.label}
</ha-md-menu-item>
`
: nothing
)}
</ha-md-menu>
`;
}
@@ -526,8 +504,17 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
this._selected = ev.detail.value;
}
private _handleSortBy(ev) {
const columnId = ev.currentTarget.value;
private _handleTableSortingChanged(
ev: CustomEvent<{ column: string; direction: SortingDirection }>
) {
const { column, direction } = ev.detail;
this._sortColumn = column;
this._sortDirection = direction;
}
private _handleSortBy(ev: HaDropdownSelectEvent) {
ev.preventDefault(); // keep dropdown open
const columnId = ev.detail.item.value;
if (!this._sortDirection || this._sortColumn !== columnId) {
this._sortDirection = "asc";
} else if (this._sortDirection === "asc") {
@@ -538,11 +525,29 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
this._sortColumn = columnId;
}
private _handleGroupBy(ev) {
this._setGroupColumn(ev.currentTarget.value);
}
private _handleOverflowGroupBy = (ev: HaDropdownSelectEvent) => {
const action = ev.detail.item.value;
private _setGroupColumn(columnId: string) {
if (!action) {
return;
}
switch (action) {
case "collapse_all":
this._collapseAllGroups();
return;
case "expand_all":
this._expandAllGroups();
return;
case "none":
this._setGroupColumn();
return;
default:
this._setGroupColumn(action);
}
};
private _setGroupColumn(columnId?: string) {
this._groupColumn = columnId;
}
@@ -797,8 +802,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
--dialog-content-padding: 0;
}
#sort-by-anchor,
#group-by-anchor,
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
@@ -30,12 +30,12 @@ import "../../../components/ha-labels-picker";
import "../../../components/ha-list-item";
import "../../../components/ha-radio";
import "../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../components/ha-select";
import "../../../components/ha-settings-row";
import "../../../components/ha-state-icon";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-textfield";
import type { HaSelectSelectEvent } from "../../../components/ha-select";
import {
CAMERA_ORIENTATIONS,
CAMERA_SUPPORT_STREAM,
@@ -434,19 +434,15 @@ export class EntityRegistrySettingsEditor extends LitElement {
>
<ha-dropdown-item
value="switch"
class=${this._switchAsDomain === "switch" &&
(!this._deviceClass || this._deviceClass === "switch")
? "selected"
: ""}
.selected=${this._switchAsDomain === "switch" &&
(!this._deviceClass || this._deviceClass === "switch")}
>
${domainToName(this.hass.localize, "switch")}
</ha-dropdown-item>
<ha-dropdown-item
value="outlet"
class=${this._switchAsDomain === "switch" &&
this._deviceClass === "outlet"
? "selected"
: ""}
.selected=${this._switchAsDomain === "switch" &&
this._deviceClass === "outlet"}
>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_classes.switch.outlet"
@@ -460,9 +456,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
(entry) => html`
<ha-dropdown-item
.value=${entry.domain}
class=${this._switchAsDomain === entry.domain
? "selected"
: ""}
.selected=${this._switchAsDomain === entry.domain}
>
${entry.label}
</ha-dropdown-item>
@@ -479,13 +473,13 @@ export class EntityRegistrySettingsEditor extends LitElement {
>
<ha-dropdown-item
value="switch"
class=${this._switchAsDomain === "switch" ? "selected" : ""}
.selected=${this._switchAsDomain === "switch"}
>
${domainToName(this.hass.localize, "switch")}
</ha-dropdown-item>
<ha-dropdown-item
.value=${domain}
class=${this._switchAsDomain === domain ? "selected" : ""}
.selected=${this._switchAsDomain === domain}
>
${domainToName(this.hass.localize, domain)}
</ha-dropdown-item>
@@ -499,9 +493,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
: html`
<ha-dropdown-item
.value=${entry.domain}
class=${this._switchAsDomain === entry.domain
? "selected"
: ""}
.selected=${this._switchAsDomain === entry.domain}
>
${entry.label}
</ha-dropdown-item>
@@ -551,9 +543,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
(entry) => html`
<ha-dropdown-item
.value=${entry.deviceClass}
class=${entry.deviceClass === this._deviceClass
? "selected"
: ""}
.selected=${entry.deviceClass === this._deviceClass}
>
${entry.label}
</ha-dropdown-item>
@@ -571,9 +561,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
(entry) => html`
<ha-dropdown-item
.value=${entry.deviceClass}
class=${entry.deviceClass === this._deviceClass
? "selected"
: ""}
.selected=${entry.deviceClass === this._deviceClass}
>
${entry.label}
</ha-dropdown-item>
+13
View File
@@ -27,6 +27,7 @@ import {
mdiScriptText,
mdiShape,
mdiSofa,
mdiStarFourPoints,
mdiTextBoxOutline,
mdiTools,
mdiUpdate,
@@ -118,6 +119,7 @@ export const configSections: Record<string, PageNavigation[]> = {
path: "/config/matter",
iconPath:
"M7.228375 6.41685c0.98855 0.80195 2.16365 1.3412 3.416275 1.56765V1.30093l1.3612 -0.7854275 1.360125 0.7854275V7.9845c1.252875 -0.226675 2.4283 -0.765875 3.41735 -1.56765l2.471225 1.4293c-4.019075 3.976275 -10.490025 3.976275 -14.5091 0l2.482925 -1.4293Zm3.00335 17.067575c1.43325 -5.47035 -1.8052 -11.074775 -7.2604 -12.564675v2.859675c1.189125 0.455 2.244125 1.202875 3.0672 2.174275L0.25 19.2955v1.5719l1.3611925 0.781175L7.39865 18.3068c0.430175 1.19825 0.550625 2.48575 0.35015 3.743l2.482925 1.434625ZM21.034 10.91975c-5.452225 1.4932 -8.6871 7.09635 -7.254025 12.564675l2.47655 -1.43035c-0.200025 -1.257275 -0.079575 -2.544675 0.35015 -3.743025l5.7832 3.337525L23.75 20.86315V19.2955L17.961475 15.9537c0.8233 -0.97115 1.878225 -1.718975 3.0672 -2.174275l0.005325 -2.859675Z",
iconViewBox: "0 1 24 24",
iconColor: "#2458B3",
component: "matter",
translationKey: "matter",
@@ -398,6 +400,13 @@ export const configSections: Record<string, PageNavigation[]> = {
iconPath: mdiShape,
iconColor: "#f1c447",
},
{
path: "/config/ai-tasks",
translationKey: "ai_tasks",
iconPath: mdiStarFourPoints,
iconColor: "#8B69E3",
core: true,
},
{
path: "/config/labs",
translationKey: "labs",
@@ -601,6 +610,10 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
tag: "ha-config-labs",
load: () => import("./labs/ha-config-labs"),
},
"ai-tasks": {
tag: "ha-config-section-ai-tasks",
load: () => import("./core/ha-config-section-ai-tasks"),
},
zha: {
tag: "zha-config-dashboard-router",
load: () =>
@@ -12,10 +12,11 @@ import "../../../components/chart/ha-chart-base";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-fade-in";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-md-list-item";
import "../../../components/ha-settings-row";
import "../../../components/ha-spinner";
import type { ConfigEntry } from "../../../data/config_entries";
import { subscribeConfigEntries } from "../../../data/config_entries";
import type {
@@ -365,7 +366,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
})}
</ha-card>`
: nothing}
${this._systemStatusData
${isComponentLoaded(this.hass, "hardware")
? html`<ha-card outlined>
<div class="header">
<div class="title">
@@ -374,16 +375,25 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
)}
</div>
<div class="value">
${this._systemStatusData.cpu_percent ||
"-"}${blankBeforePercent(this.hass.locale)}%
${this._systemStatusData
? html`${this._systemStatusData
.cpu_percent}${blankBeforePercent(
this.hass.locale
)}%`
: "-"}
</div>
</div>
<div class="card-content">
<div class="card-content loading-container">
<ha-chart-base
.hass=${this.hass}
.data=${this._getChartData(this._cpuEntries)}
.options=${this._chartOptions}
></ha-chart-base>
${!this._systemStatusData
? html` <ha-fade-in delay="1000" class="loading-overlay">
<ha-spinner size="large"></ha-spinner>
</ha-fade-in>`
: nothing}
</div>
</ha-card>
<ha-card outlined>
@@ -392,37 +402,38 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.hardware.memory")}
</div>
<div class="value">
${round(this._systemStatusData.memory_used_mb / 1024, 1)}
GB /
${round(
(this._systemStatusData.memory_used_mb! +
this._systemStatusData.memory_free_mb!) /
1024,
0
)}
GB
${this._systemStatusData
? html`${round(
this._systemStatusData.memory_used_mb / 1024,
1
)}
GB /
${round(
(this._systemStatusData.memory_used_mb +
this._systemStatusData.memory_free_mb) /
1024,
0
)}
GB`
: "-"}
</div>
</div>
<div class="card-content">
<div class="card-content loading-container">
<ha-chart-base
.hass=${this.hass}
.data=${this._getChartData(this._memoryEntries)}
.options=${this._chartOptions}
></ha-chart-base>
${!this._systemStatusData
? html`
<ha-fade-in delay="1000" class="loading-overlay">
<ha-spinner size="large"></ha-spinner>
</ha-fade-in>
`
: nothing}
</div>
</ha-card>`
: isComponentLoaded(this.hass, "hardware")
? html`<ha-card outlined>
<div class="card-content">
<ha-alert alert-type="info">
<ha-spinner slot="icon"></ha-spinner>
${this.hass.localize(
"ui.panel.config.hardware.loading_system_data"
)}
</ha-alert>
</div>
</ha-card>`
: nothing}
: nothing}
</div>
</hass-subpage>
`;
@@ -502,6 +513,22 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
flex-direction: column;
padding: 16px;
}
.loading-container {
position: relative;
}
.loading-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(var(--rgb-card-background-color), 0.75);
display: flex;
justify-content: center;
align-items: center;
}
.card-content img {
max-width: 300px;
margin: auto;
@@ -548,10 +575,6 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
ha-alert {
--ha-alert-icon-size: 24px;
}
ha-alert ha-spinner {
--ha-spinner-size: 24px;
}
`,
];
}
+100 -77
View File
@@ -1,21 +1,22 @@
import { mdiAlertOutline } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { stringCompare } from "../../../common/string/compare";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-list";
import "../../../components/ha-button";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import "../../../components/ha-wa-dialog";
import "../../../components/search-input";
import { getConfigFlowHandlers } from "../../../data/config_flow";
import { createCounter } from "../../../data/counter";
import { createInputBoolean } from "../../../data/input_boolean";
@@ -27,6 +28,7 @@ import { createInputText } from "../../../data/input_text";
import {
domainToName,
fetchIntegrationManifest,
type IntegrationManifest,
} from "../../../data/integration";
import { createSchedule } from "../../../data/schedule";
import { createTimer } from "../../../data/timer";
@@ -103,7 +105,7 @@ export class DialogHelperDetail extends LitElement {
@state() private _item?: Helper;
@state() private _opened = false;
@state() private _open = false;
@state() private _domain?: string;
@@ -111,14 +113,18 @@ export class DialogHelperDetail extends LitElement {
@state() private _submitting = false;
@query(".form") private _form?: HTMLDivElement;
@state() private _helperFlows?: string[];
@state() private _loading = false;
@state() private _filter?: string;
private _pendingConfigFlow?: {
startFlowHandler: string;
manifest: IntegrationManifest;
dialogClosedCallback?: ShowDialogHelperDetailParams["dialogClosedCallback"];
};
private _params?: ShowDialogHelperDetailParams;
public async showDialog(params: ShowDialogHelperDetailParams): Promise<void> {
@@ -128,29 +134,47 @@ export class DialogHelperDetail extends LitElement {
if (this._domain && this._domain in HELPERS) {
await HELPERS[this._domain].import();
}
this._opened = true;
this._open = true;
await this.updateComplete;
this.hass.loadFragmentTranslation("config");
const flows = await getConfigFlowHandlers(this.hass, ["helper"]);
await this.hass.loadBackendTranslation("title", flows, true);
// Ensure the titles are loaded before we render the flows.
this._helperFlows = flows;
await this.updateComplete;
await this._focusSearchInput();
}
public closeDialog(): void {
this._opened = false;
this._open = false;
}
private _dialogClosed(): void {
this._open = false;
this._error = undefined;
this._domain = undefined;
this._params = undefined;
this._filter = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
if (this._pendingConfigFlow) {
const pendingConfigFlow = this._pendingConfigFlow;
this._pendingConfigFlow = undefined;
showConfigFlowDialog(this, {
startFlowHandler: pendingConfigFlow.startFlowHandler,
manifest: pendingConfigFlow.manifest,
dialogClosedCallback: pendingConfigFlow.dialogClosedCallback,
});
}
}
protected render() {
if (!this._opened) {
if (!this._params) {
return nothing;
}
let content: TemplateResult;
let footer: TemplateResult | typeof nothing = nothing;
if (this._domain) {
content = html`
@@ -160,25 +184,30 @@ export class DialogHelperDetail extends LitElement {
hass: this.hass,
item: this._item,
new: true,
autofocus: true,
})}
</div>
<ha-button
slot="primaryAction"
@click=${this._createItem}
.disabled=${this._submitting}
>
${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
</ha-button>
${this._params?.domain
? nothing
: html`<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this._goBack}
.disabled=${this._submitting}
>
${this.hass!.localize("ui.common.back")}
</ha-button>`}
`;
footer = html`
<ha-dialog-footer slot="footer">
${this._params?.domain
? nothing
: html`<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this._goBack}
.disabled=${this._submitting}
>
${this.hass!.localize("ui.common.back")}
</ha-button>`}
<ha-button
slot="primaryAction"
@click=${this._createItem}
.disabled=${this._submitting}
>
${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
</ha-button>
</ha-dialog-footer>
`;
} else if (this._loading || this._helperFlows === undefined) {
content = html`<ha-spinner></ha-spinner>`;
@@ -191,8 +220,8 @@ export class DialogHelperDetail extends LitElement {
content = html`
<search-input
autofocus
.hass=${this.hass}
dialogInitialFocus="true"
.filter=${this._filter}
@value-changed=${this._filterChanged}
.label=${this.hass.localize(
@@ -207,7 +236,6 @@ export class DialogHelperDetail extends LitElement {
"ui.panel.config.helpers.dialog.create_helper"
)}
rootTabbable
dialogInitialFocus
>
${items.map(([domain, label]) => {
// Only OG helpers need to be loaded prior adding one
@@ -255,32 +283,28 @@ export class DialogHelperDetail extends LitElement {
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
class=${classMap({ "button-left": !this._domain })}
.hideActions=${!this._domain}
.heading=${createCloseHeading(
this.hass,
this._domain
? this.hass.localize(
"ui.panel.config.helpers.dialog.create_platform",
{
platform:
(isHelperDomain(this._domain) &&
this.hass.localize(
`ui.panel.config.helpers.types.${
this._domain as HelperDomain
}`
)) ||
this._domain,
}
)
: this.hass.localize("ui.panel.config.helpers.dialog.create_helper")
)}
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this._domain
? this.hass.localize(
"ui.panel.config.helpers.dialog.create_platform",
{
platform:
(isHelperDomain(this._domain) &&
this.hass.localize(
`ui.panel.config.helpers.types.${
this._domain as HelperDomain
}`
)) ||
this._domain,
}
)
: this.hass.localize("ui.panel.config.helpers.dialog.create_helper")}
@closed=${this._dialogClosed}
>
${content}
</ha-dialog>
${content} ${footer}
</ha-wa-dialog>
`;
}
@@ -381,26 +405,35 @@ export class DialogHelperDetail extends LitElement {
} finally {
this._loading = false;
}
this._focusForm();
} else {
showConfigFlowDialog(this, {
this._pendingConfigFlow = {
startFlowHandler: domain,
manifest: await fetchIntegrationManifest(this.hass, domain),
dialogClosedCallback: this._params!.dialogClosedCallback,
});
dialogClosedCallback: this._params?.dialogClosedCallback,
};
this.closeDialog();
}
}
private async _focusForm(): Promise<void> {
await this.updateComplete;
(this._form?.lastElementChild as HTMLElement).focus();
}
private _goBack() {
private async _goBack() {
this._domain = undefined;
this._item = undefined;
this._error = undefined;
await this.updateComplete;
await this._focusSearchInput();
}
private async _focusSearchInput() {
const searchInput = this.shadowRoot?.querySelector("search-input") as
| (HTMLElement & { updateComplete?: Promise<unknown> })
| null;
if (!searchInput) {
return;
}
await searchInput.updateComplete;
searchInput.focus();
}
static get styles(): CSSResultGroup {
@@ -408,31 +441,21 @@ export class DialogHelperDetail extends LitElement {
haStyleScrollbar,
haStyleDialog,
css`
ha-dialog.button-left {
--justify-action-buttons: flex-start;
}
ha-dialog {
ha-wa-dialog {
--dialog-content-padding: 0;
--dialog-scroll-divider-color: transparent;
--mdc-dialog-max-height: 90vh;
}
@media all and (min-width: 550px) {
ha-dialog {
--mdc-dialog-min-width: 500px;
}
}
ha-icon-next {
width: 24px;
width: var(--ha-space-6);
}
ha-tooltip {
pointer-events: auto;
}
.form {
padding: 24px;
padding: var(--ha-space-6);
}
search-input {
display: block;
margin: 16px 16px 0;
margin: 0 var(--ha-space-4) 0;
}
ha-list {
height: calc(60vh - 184px);
@@ -3,7 +3,8 @@ import { html, LitElement, nothing } from "lit";
import memoizeOne from "memoize-one";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-form/ha-form";
import "../../../../components/ha-button";
import { haStyleDialog } from "../../../../resources/styles";
@@ -24,6 +25,8 @@ class DialogScheduleBlockInfo extends LitElement {
@state() private _params?: ScheduleBlockInfoDialogParams;
@state() private _open = false;
private _expand = false;
private _schema = memoizeOne((expand: boolean) => [
@@ -57,9 +60,14 @@ class DialogScheduleBlockInfo extends LitElement {
this._error = undefined;
this._data = params.block;
this._expand = !!params.block?.data;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._params = undefined;
this._data = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -71,18 +79,17 @@ class DialogScheduleBlockInfo extends LitElement {
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass!.localize(
"ui.dialogs.helper_settings.schedule.edit_schedule_block"
)
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass!.localize(
"ui.dialogs.helper_settings.schedule.edit_schedule_block"
)}
@closed=${this._dialogClosed}
>
<div>
<ha-form
autofocus
.hass=${this.hass}
.schema=${this._schema(this._expand)}
.data=${this._data}
@@ -91,18 +98,20 @@ class DialogScheduleBlockInfo extends LitElement {
@value-changed=${this._valueChanged}
></ha-form>
</div>
<ha-button
slot="secondaryAction"
@click=${this._deleteBlock}
appearance="plain"
variant="danger"
>
${this.hass!.localize("ui.common.delete")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._updateBlock}>
${this.hass!.localize("ui.common.save")}
</ha-button>
</ha-dialog>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
@click=${this._deleteBlock}
appearance="filled"
variant="danger"
>
${this.hass!.localize("ui.common.delete")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._updateBlock}>
${this.hass!.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
@@ -369,7 +369,6 @@ export class BluetoothConfigDashboard extends LitElement {
padding: 24px 0 32px;
max-width: 600px;
margin: 0 auto;
direction: ltr;
}
ha-card {
margin-bottom: 16px;
@@ -323,9 +323,9 @@ class ZHAConfigDashboard extends LitElement {
const configEntries = await getConfigEntries(this.hass, {
domain: "zha",
});
if (configEntries.length) {
this._configEntry = configEntries[0];
}
this._configEntry = configEntries.find(
(entry) => entry.disabled_by === null && entry.source !== "ignore"
);
}
private async _fetchConfiguration(): Promise<void> {
+72 -32
View File
@@ -1,3 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import {
mdiDelete,
mdiDevices,
@@ -20,13 +21,16 @@ import type {
RowClickedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/ha-dropdown";
import type {
HaDropdown,
HaDropdownSelectEvent,
} from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import { renderLabelColorBadge } from "../../../components/ha-label-picker";
import "../../../components/ha-md-menu";
import type { HaMdMenu } from "../../../components/ha-md-menu";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-svg-icon";
import type {
LabelRegistryEntry,
@@ -44,12 +48,12 @@ import {
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage-data-table";
import type { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "./show-dialog-label-detail";
import {
getCreatedAtTableColumn,
getModifiedAtTableColumn,
} from "../common/data-table-columns";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "./show-dialog-label-detail";
@customElement("ha-config-labels")
export class HaConfigLabels extends LitElement {
@@ -93,10 +97,12 @@ export class HaConfigLabels extends LitElement {
})
private _activeHiddenColumns?: string[];
@query("#overflow-menu") private _overflowMenu?: HaMdMenu;
@query("#overflow-menu") private _overflowMenu?: HaDropdown;
private _overflowLabel!: LabelRegistryEntry;
private _openingOverflow = false;
private _columns = memoizeOne((localize: LocalizeFunc, narrow: boolean) => {
const columns: DataTableColumnContainer<LabelRegistryEntry> = {
icon: {
@@ -172,13 +178,27 @@ export class HaConfigLabels extends LitElement {
return;
}
if (this._overflowMenu.open) {
this._overflowMenu.close();
if (this._overflowMenu.anchorElement === ev.target) {
this._overflowMenu.anchorElement = undefined;
return;
}
this._overflowLabel = ev.target.selected;
this._openingOverflow = true;
this._overflowMenu.anchorElement = ev.target;
this._overflowMenu.show();
this._overflowLabel = ev.target.selected;
this._overflowMenu.open = true;
};
private _overflowMenuOpened = () => {
this._openingOverflow = false;
};
private _overflowMenuClosed = () => {
// changing the anchorElement triggers a close event, ignore it
if (this._openingOverflow || !this._overflowMenu) {
return;
}
this._overflowMenu.anchorElement = undefined;
};
protected firstUpdated(changedProperties: PropertyValues) {
@@ -224,32 +244,30 @@ export class HaConfigLabels extends LitElement {
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage-data-table>
<ha-md-menu id="overflow-menu" positioning="fixed">
<ha-md-menu-item .clickAction=${this._navigateEntities}>
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
<ha-dropdown
id="overflow-menu"
@wa-select=${this._handleOverflowAction}
@wa-after-show=${this._overflowMenuOpened}
@wa-after-hide=${this._overflowMenuClosed}
>
<ha-dropdown-item value="navigate-entities">
<ha-svg-icon slot="icon" .path=${mdiShape}></ha-svg-icon>
${this.hass.localize("ui.panel.config.entities.caption")}
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._navigateDevices}>
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
</ha-dropdown-item>
<ha-dropdown-item value="navigate-devices">
<ha-svg-icon slot="icon" .path=${mdiDevices}></ha-svg-icon>
${this.hass.localize("ui.panel.config.devices.caption")}
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._navigateAutomations}>
<ha-svg-icon slot="start" .path=${mdiRobot}></ha-svg-icon>
</ha-dropdown-item>
<ha-dropdown-item value="navigate-automations">
<ha-svg-icon slot="icon" .path=${mdiRobot}></ha-svg-icon>
${this.hass.localize("ui.panel.config.automation.caption")}
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
class="warning"
.clickAction=${this._handleRemoveLabelClick}
>
<ha-svg-icon
slot="start"
class="warning"
.path=${mdiDelete}
></ha-svg-icon>
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item variant="danger" value="remove">
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
${this.hass.localize("ui.common.delete")}
</ha-md-menu-item>
</ha-md-menu>
</ha-dropdown-item>
</ha-dropdown>
`;
}
@@ -341,6 +359,28 @@ export class HaConfigLabels extends LitElement {
}
}
private _handleOverflowAction = (ev: HaDropdownSelectEvent) => {
const action = ev.detail.item.value;
if (!action) {
return;
}
switch (action) {
case "navigate-entities":
this._navigateEntities();
break;
case "navigate-devices":
this._navigateDevices();
break;
case "navigate-automations":
this._navigateAutomations();
break;
case "remove":
this._handleRemoveLabelClick();
break;
}
};
private _navigateEntities = () => {
navigate(
`/config/entities?historyBack=1&label=${this._overflowLabel.label_id}`
+1 -7
View File
@@ -169,7 +169,7 @@ class ErrorLogCard extends LitElement {
(boot) => html`
<ha-dropdown-item
.value=${`boot_${boot}`}
class=${boot === this._boot ? "selected" : ""}
.selected=${boot === this._boot}
>
${boot === 0
? localize("ui.panel.config.logs.current")
@@ -846,12 +846,6 @@ class ErrorLogCard extends LitElement {
.download-link {
color: var(--text-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);
}
`;
}
@@ -2,8 +2,7 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-card";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-wa-dialog";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import "./integrations-startup-time";
@@ -12,44 +11,47 @@ import "./integrations-startup-time";
class DialogIntegrationStartup extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false;
@state() private _open = false;
public showDialog(): void {
this._opened = true;
this._open = true;
}
public closeDialog() {
this._opened = false;
this._open = false;
}
private _dialogClosed(): void {
this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._opened) {
if (!this._open) {
return nothing;
}
return html`
<ha-dialog
open
hideActions
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.repairs.integration_startup_time")
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize(
"ui.panel.config.repairs.integration_startup_time"
)}
@closed=${this.closeDialog}
@closed=${this._dialogClosed}
>
<integrations-startup-time
.hass=${this.hass}
narrow
></integrations-startup-time>
</ha-dialog>
</ha-wa-dialog>
`;
}
static styles: CSSResultGroup = [
haStyleDialog,
css`
ha-dialog {
ha-wa-dialog {
--dialog-content-padding: 0;
}
`,
@@ -9,8 +9,8 @@ import { copyToClipboard } from "../../../common/util/copy-clipboard";
import { subscribePollingCollection } from "../../../common/util/subscribe-polling";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-card";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-wa-dialog";
import "../../../components/ha-metric";
import "../../../components/ha-spinner";
import type { HassioStats } from "../../../data/hassio/common";
@@ -62,20 +62,24 @@ class DialogSystemInformation extends LitElement {
@state() private _coreStats?: HassioStats;
@state() private _opened = false;
@state() private _open = false;
private _systemHealthSubscription?: Promise<UnsubscribeFunc>;
private _hassIOSubscription?: UnsubscribeFunc;
public showDialog(): void {
this._opened = true;
this._open = true;
this.hass!.loadBackendTranslation("system_health");
this._subscribe();
}
public closeDialog() {
this._opened = false;
this._open = false;
}
private _dialogClosed(): void {
this._open = false;
this._unsubscribe();
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -126,20 +130,20 @@ class DialogSystemInformation extends LitElement {
}
protected render() {
if (!this._opened) {
if (!this._open) {
return nothing;
}
const sections = this._getSections();
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.repairs.system_information")
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize(
"ui.panel.config.repairs.system_information"
)}
@closed=${this._dialogClosed}
>
<div>
${this._resolutionInfo
@@ -224,10 +228,12 @@ class DialogSystemInformation extends LitElement {
</div>
`}
</div>
<ha-button slot="primaryAction" @click=${this._copyInfo}>
${this.hass.localize("ui.panel.config.repairs.copy")}
</ha-button>
</ha-dialog>
<ha-dialog-footer slot="footer">
<ha-button slot="primaryAction" @click=${this._copyInfo}>
${this.hass.localize("ui.panel.config.repairs.copy")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
@@ -2,6 +2,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { relativeTime } from "../../../common/datetime/relative_time";
import { capitalizeFirstLetter } from "../../../common/string/capitalize-first-letter";
import { STRINGS_SEPARATOR_DOT } from "../../../common/const";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import { domainToName } from "../../../data/integration";
@@ -99,7 +100,7 @@ class HaConfigRepairs extends LitElement {
${(issue.severity === "critical" ||
issue.severity === "error") &&
issue.created
? " · "
? STRINGS_SEPARATOR_DOT
: ""}
${createdBy
? html`<span .title=${createdBy}>${createdBy}</span>`
@@ -52,7 +52,6 @@ import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
import "../../../components/ha-state-icon";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
@@ -54,7 +54,6 @@ import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
@@ -571,7 +571,6 @@ export class HaManualScriptEditor extends SubscribeMixin(LitElement) {
}
private _saveScript() {
this.triggerCloseSidebar();
fireEvent(this, "save-script");
}
@@ -55,17 +55,16 @@ export class StorageBreakdownChart extends LitElement {
<span class="heading">${heading}</span>
<span class="description">${description}</span>
</div>
${hasChildren
? html`<ha-icon-button
.path=${this._chartType === "sunburst"
? mdiViewArray
: mdiChartDonutVariant}
.label=${this.hass.localize(
"ui.panel.config.storage.change_chart_type"
)}
@click=${this._handleChartTypeChange}
></ha-icon-button>`
: nothing}
<ha-icon-button
.path=${this._chartType === "sunburst"
? mdiViewArray
: mdiChartDonutVariant}
.label=${this.hass.localize(
"ui.panel.config.storage.change_chart_type"
)}
.disabled=${!hasChildren}
@click=${this._handleChartTypeChange}
></ha-icon-button>
</div>
<div class="chart-container ${this._chartType}">
@@ -106,9 +105,11 @@ export class StorageBreakdownChart extends LitElement {
storageInfo: HostDisksUsage | null | undefined
) => {
let totalSpaceGB = hostInfo.disk_total;
let usedSpaceGB = hostInfo.disk_used;
let freeSpaceGB =
hostInfo.disk_free || hostInfo.disk_total - hostInfo.disk_used;
// hostInfo.disk_used doesn't include system reserved space,
// so we calculate used space based on total and free space
let usedSpaceGB = totalSpaceGB - freeSpaceGB;
if (storageInfo) {
const totalSpace =
@@ -213,26 +214,24 @@ export class StorageBreakdownChart extends LitElement {
static styles = css`
.header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: var(--ha-space-2);
align-items: flex-end;
gap: var(--ha-space-2);
}
.heading-text {
display: flex;
flex-direction: column;
gap: var(--ha-space-1);
flex: 1;
}
.heading {
font-weight: 500;
font-size: var(--ha-font-size-m);
color: var(--primary-text-color);
line-height: var(--ha-line-height-expanded);
margin-right: var(--ha-space-2);
}
.description {
font-size: var(--ha-font-size-s);
color: var(--secondary-text-color);
line-height: var(--ha-line-height-expanded);
}
ha-icon-button {
@@ -454,7 +454,7 @@ export class AssistPref extends LitElement {
align-items: center;
padding-bottom: 0;
}
img {
voice-assistant-brand-icon {
height: 28px;
margin-right: 16px;
margin-inline-end: 16px;
@@ -280,7 +280,7 @@ export class CloudAlexaPref extends LitElement {
display: flex;
align-items: center;
}
img {
voice-assistant-brand-icon {
height: 28px;
margin-right: 16px;
margin-inline-end: 16px;
@@ -152,11 +152,12 @@ export class CloudDiscover extends LitElement {
}
.feature .logos {
margin-bottom: 16px;
display: flex;
gap: var(--ha-space-4);
}
.feature .logos > * {
width: 40px;
height: 40px;
margin: 0 4px;
}
.round-icon {
border-radius: var(--ha-border-radius-circle);
@@ -350,7 +350,7 @@ export class CloudGooglePref extends LitElement {
display: flex;
align-items: center;
}
img {
voice-assistant-brand-icon {
height: 28px;
margin-right: 16px;
margin-inline-end: 16px;
@@ -26,25 +26,29 @@ export function getAssistantsTableColumn<T>(
valueColumn: "assistants_sortable_key",
template: (entry: any) =>
html`${entry.assistants.length !== 0
? availableAssistants.map((vaId) => {
const supported =
!supportedEntities?.[vaId] ||
supportedEntities[vaId].includes(entry.entity_id);
const manual = entry.manAssistants?.includes(vaId);
return getAssistantsTableColumnIcon(
entry.assistants.includes(vaId),
vaId,
hass,
entitiesToCheck,
manual,
!supported
);
})
? html`<div style="display: flex; gap: var(--ha-space-4);">
${availableAssistants.map((vaId) => {
const supported =
!supportedEntities?.[vaId] ||
supportedEntities[vaId].includes(entry.entity_id);
const manual = entry.manAssistants?.includes(vaId);
return getAssistantsTableColumnIcon(
entry.entity_id,
entry.assistants.includes(vaId),
vaId,
hass,
entitiesToCheck,
manual,
!supported
);
})}
</div>`
: nothing}`,
};
}
export const getAssistantsTableColumnIcon = (
id: string,
show: boolean,
vaId: string,
hass: HomeAssistant,
@@ -57,6 +61,7 @@ export const getAssistantsTableColumnIcon = (
);
return show
? html`<voice-assistants-expose-assistant-icon
.id=${id}
.assistant=${vaId}
.hass=${hass}
.manual=${manual ?? false}
@@ -2,6 +2,7 @@ import { mdiAlertCircle } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { slugify } from "../../../../common/string/slugify";
import { voiceAssistants } from "../../../../data/expose";
import type { HomeAssistant } from "../../../../types";
import "../../../../components/ha-svg-icon";
@@ -23,8 +24,9 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement {
render() {
if (!this.assistant || !voiceAssistants[this.assistant]) return nothing;
const id = slugify(this.id) + "-" + this.assistant;
return html`
<div class="container" id="container">
<div class="container" id=${id}>
<voice-assistant-brand-icon
style=${styleMap({
filter: this.manual ? "grayscale(100%)" : undefined,
@@ -43,7 +45,7 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement {
: nothing}
</div>
<ha-tooltip
for="container"
for=${id}
placement="left"
.disabled=${!this.unsupported && !this.manual}
>
@@ -66,13 +68,6 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement {
.container {
position: relative;
}
.logo {
position: relative;
height: 24px;
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
}
.unsupported {
color: var(--error-color);
position: absolute;
@@ -1,8 +1,9 @@
import type { HassConfig } from "home-assistant-js-websocket";
import {
differenceInMonths,
subHours,
differenceInDays,
differenceInMonths,
differenceInCalendarMonths,
differenceInYears,
startOfYear,
addMilliseconds,
@@ -12,6 +13,7 @@ import {
addHours,
startOfDay,
addDays,
subDays,
} from "date-fns";
import type {
BarSeriesOption,
@@ -33,10 +35,22 @@ import { filterXSS } from "../../../../../common/util/xss";
import type { StatisticPeriod } from "../../../../../data/recorder";
import { getSuggestedPeriod } from "../../../../../data/energy";
export function getSuggestedMax(period: StatisticPeriod, end: Date): Date {
// Number of days of padding when showing time axis in months
const MONTH_TIME_AXIS_PADDING = 5;
export function getSuggestedMax(
period: StatisticPeriod,
end: Date,
noRounding: boolean
): Date {
// Maximum period depends on whether plotting a line chart or discrete bars.
// - For line charts we must be plotting all the way to end of a given period,
// otherwise we cut off the last period of data.
// - For bar charts we need to round down to the start of the final bars period
// to avoid unnecessary padding of the chart.
let suggestedMax = new Date(end);
if (period === "5minute") {
if (noRounding || period === "5minute") {
return suggestedMax;
}
suggestedMax.setMinutes(0, 0, 0);
@@ -82,17 +96,44 @@ export function getCommonOptions(
detailedDailyData = false
): ECOption {
const suggestedPeriod = getSuggestedPeriod(start, end, detailedDailyData);
const suggestedMax = getSuggestedMax(suggestedPeriod, end, detailedDailyData);
const compare = compareStart !== undefined && compareEnd !== undefined;
const showCompareYear =
compare && start.getFullYear() !== compareStart.getFullYear();
const options: ECOption = {
const monthTimeAxis: ECOption = {
xAxis: {
type: "time",
min: subDays(start, MONTH_TIME_AXIS_PADDING),
max: addDays(suggestedMax, MONTH_TIME_AXIS_PADDING),
axisLabel: {
formatter: {
year: "{yearStyle|{MMMM} {yyyy}}",
month: "{MMMM}",
},
rich: {
yearStyle: {
fontWeight: "bold",
},
},
},
// For shorter month ranges, force splitting to ensure time axis renders
// as whole month intervals. Limit the number of forced ticks to 6 months
// (so a max calendar difference of 5) to reduce clutter.
splitNumber: Math.min(differenceInCalendarMonths(end, start), 5),
},
};
const normalTimeAxis: ECOption = {
xAxis: {
type: "time",
min: start,
max: getSuggestedMax(suggestedPeriod, end),
max: suggestedMax,
},
};
const options: ECOption = {
...(suggestedPeriod === "month" ? monthTimeAxis : normalTimeAxis),
yAxis: {
type: "value",
name: unit,
+2 -2
View File
@@ -13,7 +13,7 @@ import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { BINARY_STATE_ON } from "../../../common/const";
import { BINARY_STATE_ON, STRINGS_SEPARATOR_DOT } from "../../../common/const";
import { computeAreaName } from "../../../common/entity/compute_area_name";
import { generateEntityFilter } from "../../../common/entity/entity_filter";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
@@ -522,7 +522,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
return `${formattedValue}${formattedUnit}`;
})
.filter(Boolean)
.join(" · ");
.join(STRINGS_SEPARATOR_DOT);
return sensorStates;
}
@@ -7,7 +7,9 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { getGraphColorByIndex } from "../../../common/color/colors";
import { computeCssColor } from "../../../common/color/compute-color";
import { computeDomain } from "../../../common/entity/compute_domain";
import { normalizeValueBySIPrefix } from "../../../common/number/normalize-by-si-prefix";
import { MobileAwareMixin } from "../../../mixins/mobile-aware-mixin";
import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
@@ -33,6 +35,7 @@ const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
interface ProcessedEntity {
entity: string;
name?: string | EntityNameItem | EntityNameItem[];
color?: string;
}
interface LegendItem {
@@ -147,6 +150,7 @@ export class HuiDistributionCard
this._configEntities = entities.map((entity) => ({
entity: entity.entity,
name: entity.name,
color: entity.color,
}));
}
@@ -227,10 +231,16 @@ export class HuiDistributionCard
const stateObj = this.hass!.states[entity.entity];
if (!stateObj) return;
const value = Number(stateObj.state);
if (value <= 0 || isNaN(value)) return;
const rawValue = Number(stateObj.state);
if (rawValue <= 0 || isNaN(rawValue)) return;
const value = normalizeValueBySIPrefix(
rawValue,
stateObj.attributes.unit_of_measurement
);
const color = getGraphColorByIndex(entity.originalIndex, computedStyles);
const color = entity.color
? computeCssColor(entity.color)
: getGraphColorByIndex(entity.originalIndex, computedStyles);
const name = computeLovelaceEntityName(this.hass!, stateObj, entity.name);
const formattedValue = this.hass!.formatEntityState(stateObj);
@@ -279,7 +289,9 @@ export class HuiDistributionCard
name: name,
value: value,
formattedValue: formattedValue,
color: getGraphColorByIndex(index, computedStyles),
color: entity.color
? computeCssColor(entity.color)
: getGraphColorByIndex(index, computedStyles),
isHidden: isHidden,
isDisabled: isZeroOrNegative,
};
+5 -19
View File
@@ -13,11 +13,6 @@ import {
stateColorCss,
} from "../../../common/entity/state_color";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import {
formatNumber,
getNumberFormatOptions,
isNumericState,
} from "../../../common/number/format_number";
import { iconColorCSS } from "../../../common/style/icon_color_css";
import "../../../components/ha-attribute-value";
import "../../../components/ha-card";
@@ -125,6 +120,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
}
const domain = computeStateDomain(stateObj);
const stateParts = this.hass.formatEntityStateToParts(stateObj);
let unit;
if (
@@ -134,9 +130,9 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
) {
unit = this._config.unit;
if (!unit) {
if (!this._config.attribute)
unit = stateObj.attributes.unit_of_measurement;
else {
if (!this._config.attribute) {
unit = stateParts.find((part) => part.type === "unit")?.value;
} else {
const parts = this.hass.formatEntityAttributeValueToParts(
stateObj,
this._config.attribute
@@ -205,17 +201,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
>
</ha-attribute-value>`
: this.hass.localize("state.default.unknown")
: (isNumericState(stateObj) || this._config.unit) &&
stateObj.attributes.device_class !== "duration"
? formatNumber(
stateObj.state,
this.hass.locale,
getNumberFormatOptions(
stateObj,
this.hass.entities[this._config.entity]
)
)
: this.hass.formatEntityState(stateObj)}</span
: stateParts.find((part) => part.type === "value")?.value}</span
>
${unit ? html`<span class="measurement">${unit}</span>` : nothing}
</div>
@@ -332,7 +332,11 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
.maxYAxis=${this._config.max_y_axis}
.startTime=${this._energyStart}
.endTime=${this._energyEnd && this._energyStart
? getSuggestedMax(this._period!, this._energyEnd)
? getSuggestedMax(
this._period!,
this._energyEnd,
(this._config.chart_type ?? "line") === "line"
)
: undefined}
.fitYData=${this._config.fit_y_data || false}
.hideLegend=${this._config.hide_legend || false}
+4 -1
View File
@@ -94,6 +94,7 @@ export interface EntitiesCardEntityConfig extends EntityConfig {
| "last-changed"
| "last-triggered"
| "last-updated"
| "area"
| "position"
| "state"
| "tilt-position"
@@ -664,7 +665,9 @@ export interface HomeSummaryCard extends LovelaceCardConfig {
double_tap_action?: ActionConfig;
}
export interface DistributionEntityConfig extends EntityConfig {}
export interface DistributionEntityConfig extends EntityConfig {
color?: string;
}
export interface DistributionCardConfig extends LovelaceCardConfig {
type: "distribution";
@@ -157,7 +157,7 @@ class HuiWaterSankeyCard
}
nodes.push({
id: source.stat_energy_from,
id: `source-${source.stat_energy_from}`,
label: getStatisticLabel(
this.hass,
source.stat_energy_from,
@@ -169,7 +169,7 @@ class HuiWaterSankeyCard
});
links.push({
source: source.stat_energy_from,
source: `source-${source.stat_energy_from}`,
target: "home",
value,
});
@@ -246,6 +246,7 @@ export class HuiEntityEditor extends LitElement {
}
ha-md-list {
gap: 8px;
padding-top: 0;
}
ha-md-list-item {
border: 1px solid var(--divider-color);
@@ -8,6 +8,8 @@ import { uid } from "../../../common/util/uid";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeAreaName } from "../../../common/entity/compute_area_name";
import { getEntityContext } from "../../../common/entity/context/get_entity_context";
import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time";
import "../../../components/entity/state-badge";
import "../../../components/ha-relative-time";
@@ -199,7 +201,9 @@ export class HuiGenericEntityRow extends LitElement {
? html`${this.hass.formatEntityState(
stateObj
)}`
: nothing)}
: this.config.secondary_info === "area"
? (this._getArea(stateObj) ?? nothing)
: nothing)}
</div>
`
: nothing}
@@ -235,6 +239,17 @@ export class HuiGenericEntityRow extends LitElement {
handleAction(this, this.hass!, this.config!, ev.detail.action!);
}
private _getArea(stateObj) {
const context = getEntityContext(
stateObj,
this.hass!.entities,
this.hass!.devices,
this.hass!.areas,
this.hass!.floors
);
return context.area ? computeAreaName(context.area) : undefined;
}
static styles = css`
:host {
display: flex;

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