Compare commits

...

145 Commits

Author SHA1 Message Date
Aidan Timson
d5b87878ec Revert "Add basic view transitions between tab UIs (#28374)"
This reverts commit e7b0b9a090.
2025-12-09 09:05:14 +00:00
Aidan Timson
4030ce3f88 Migrate dialog-upload-backup to ha-wa-dialog (#28444)
* Migrate dialog-upload-backup.ts from ha-md-dialog to ha-wa-dialog

* Remove custom css, space tokens

* Restore
2025-12-09 09:32:28 +02:00
Aidan Timson
41cabde393 Migrate dialog-download-decrypted-backup to ha-wa-dialog (#28442)
* Migrate dialog-download-decrypted-backup.ts from ha-md-dialog to ha-wa-dialog

* Fixes from other migrations
2025-12-09 09:28:44 +02:00
Aidan Timson
47d1fdf673 Migrate change/show backup encryption key to ha-wa-dialog (#28428)
* Migrate dialog-change-backup-encryption-key.ts from ha-md-dialog to ha-wa-dialog

* Remove open render nothing, remove custom css size

* Migrate show backup key

* Apply suggestion from @MindFreeze

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-09 07:24:43 +00:00
Aidan Timson
e59e83fffe Migrate dialog-areas-floors-order to ha-wa-dialog (#28424)
* Migrate dialog-areas-floors-order.ts from ha-md-dialog to ha-wa-dialog

* Fix saving

* Remove render nothing if dialog false

* Remove custom width
2025-12-09 09:12:29 +02:00
Aidan Timson
b896b78876 Migrate labs dialogs to ha-wa-dialog (#28429)
* Migrate dialog-labs-preview-feature-enable.ts from ha-md-dialog to ha-wa-dialog

* Migrate dialog-labs-progress.ts from ha-md-dialog to ha-wa-dialog

* Restore

* Remove use of slots

* Fix

* Remove header
2025-12-09 08:55:28 +02:00
Copilot
6e180c9fb4 Migrate ha-button-menu to ha-dropdown in 3 files (#28337)
* Initial plan

* Migrate ha-button-menu to ha-dropdown in 3 files

- Migrate ha-config-logs.ts: Replace ha-button-menu with ha-dropdown for log provider selection
- Migrate ha-qr-scanner.ts: Replace ha-button-menu with ha-dropdown for camera selection
- Migrate ha-data-table-labels.ts: Replace ha-button-menu with ha-dropdown for overflow labels

Following the migration pattern from PR #28293

Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>

* Fix ha-data-table-labels migration to use proper @wa-select pattern

- Add HaDropdownItem type import
- Use @wa-show instead of @click for menu opening
- Use @wa-select for item selection with proper event handler
- Update comment to reference ha-dropdown instead of ha-button-menu

Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>

* Fixes

* Remove unused code

* Remove unused styles

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
Co-authored-by: Wendelin <w@pe8.at>
2025-12-08 21:30:35 +01:00
Aidan Timson
b50daecdec Create an adaptive dialog and bottom sheet component, migrate restart (#28344)
* Create a switchable dialog and bottom sheet component

* Migrate dialog restart

* Fix close action

* Docstring

* Rename type

* Rename func

* Document

* Remove flex, use flex

* Cleanup docstring

* Update bodyScrolled

* Cleanup

* Remove

* Rename to ha-adaptive-dialog

* Cleanup

* Format

* Fix forwarding of slots

* Fix style

* listenMediaQuery, simplify and remove double scroll handling

* Add slotted footer to bottom sheet for compat

* Add block-mode-change to stop mode switches for forms etc.

* Document block-mode-change

* Update gallery

* Fix gallery doc for wa-dialog (caught by agent review)

* Update src/components/ha-adaptive-dialog.ts

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

* Cleanup

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-08 17:32:31 +02:00
Aidan Timson
f5bd0816a8 Fix more info entity settings bottom fade (#28437)
* Use space tokens

* Fix settings bottom fade issue

* Space token

* Restore
2025-12-08 16:05:49 +02:00
Aidan Timson
b6d1e65044 Migrate dialog-edit-sidebar to ha-wa-dialog, fix dialog padding (#28426)
* Migrate dialog-edit-sidebar.ts from ha-md-dialog to ha-wa-dialog

* Format

* Fix wa dialog padding impl

* Remove custom size

* Use space tokens

* Remove render nothing

* Fix default top padding
2025-12-08 14:47:10 +02:00
Aidan Timson
e2312e52e3 Remove z-index from scrollable fades (#28434) 2025-12-08 14:33:30 +02:00
renovate[bot]
46a6da33d9 Update dependency terser-webpack-plugin to v5.3.15 (#28436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-08 14:32:39 +02:00
Aidan Timson
e7b0b9a090 Add basic view transitions between tab UIs (#28374)
* Add basic view transitions between tab UIs

* Remove debug

* Add to debug navigation action
2025-12-08 14:22:05 +02:00
Petar Petrov
63ffc3b7f9 Restrict row resizing for stack and entities cards (#28422)
* Restrict row resizing for stack and entities cards

* Fix disabled state for grid layout slider
2025-12-08 12:53:37 +02:00
Nils Schönwald
ebfceae38c Update snowflake to 6 sides (#28406) 2025-12-08 09:16:55 +00:00
Aidan Timson
cccfc1716b Add scrollable fade mixin to bottom sheet, picker combo box (#28347)
* Add scrollable fade mixin to ha-bottom-sheet

* Wrap picker combobox (used inside of sheet with virtualizer)
2025-12-08 08:58:19 +02:00
dcapslock
6abf0dfcb9 Energy strategies to refresh energy collection which allows to be used in custom dashboards (#28400)
* Energy strategies to refresh energy collection which allows to be used in custom dashboards

* Update src/panels/energy/strategies/energy-overview-view-strategy.ts

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

* Only refresh if no prefs

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-08 08:17:19 +02:00
dependabot[bot]
9d2404763f Bump home-assistant/wheels from 2025.11.0 to 2025.12.0 (#28421)
Bumps [home-assistant/wheels](https://github.com/home-assistant/wheels) from 2025.11.0 to 2025.12.0.
- [Release notes](https://github.com/home-assistant/wheels/releases)
- [Commits](https://github.com/home-assistant/wheels/compare/2025.11.0...2025.12.0)

---
updated-dependencies:
- dependency-name: home-assistant/wheels
  dependency-version: 2025.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 06:11:02 +00:00
dependabot[bot]
6c84c9b44e Bump github/codeql-action from 4.31.5 to 4.31.7 (#28417)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.5 to 4.31.7.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](fdbfb4d275...cf1bb45a27)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.7
  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>
2025-12-08 06:10:48 +00:00
dependabot[bot]
edb2172bdd Bump actions/checkout from 6.0.0 to 6.0.1 (#28419)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](1af3b93b68...8e8c483db8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.1
  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>
2025-12-08 06:10:44 +00:00
dependabot[bot]
067e7645ac Bump actions/stale from 10.1.0 to 10.1.1 (#28420)
Bumps [actions/stale](https://github.com/actions/stale) from 10.1.0 to 10.1.1.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](5f858e3efb...997185467f)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: 10.1.1
  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>
2025-12-08 06:10:35 +00:00
dependabot[bot]
c3e4af3db8 Bump actions/setup-node from 6.0.0 to 6.1.0 (#28418)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](2028fbc5c2...395ad32622)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-08 06:10:25 +00:00
karwosts
f44be2d3b9 More unsafe description_placeholders fixes (#28416) 2025-12-08 08:05:35 +02:00
karwosts
cff7ed9b05 Fix for undefined description_placeholders (#28395)
Another fix for undefined description_placeholders
2025-12-07 09:21:08 +02:00
Norbert Rittel
c4e5f1dba6 Fix wording for use with both energy and power sensors (#28392) 2025-12-06 13:41:56 +00:00
Bram Kragten
d1011d691f Handle search params changed after first updated for dashboards and … (#28375)
handle search params changed after first updated for dashboards and integrations
2025-12-06 13:45:44 +02:00
renovate[bot]
7dda8c36bc Update dependency prettier to v3.7.4 (#28388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-06 09:32:57 +01:00
renovate[bot]
45b2376616 Update vitest monorepo to v4.0.15 (#28379)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-05 17:51:51 +01:00
Paul Bottein
2f70a82d02 Use non-admin endpoint to subscribe to one lab feature (#28352) 2025-12-05 17:34:22 +01:00
Aidan Timson
00868b2450 Add scrollable fade to more info dialog (#28314)
* Add scrollable fade to more info dialog

* Introduce scroll threshold (default 4px, more info 24px)

* Docstrings

* Remove getter for numeric values in mixin

* Update src/dialogs/more-info/ha-more-info-dialog.ts

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

* Fix lint

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-12-05 15:43:47 +01:00
Timothy
e573a726aa Add add to button in more info topbar for non admin users (#28365) 2025-12-05 15:05:50 +01:00
Petar Petrov
ef82bc2abb Fix calendar card not showing different colors for multiple calendars (#28338) 2025-12-05 13:39:51 +01:00
renovate[bot]
92e3864f63 Update dependency typescript-eslint to v8.48.1 (#28371)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-05 13:17:53 +01:00
renovate[bot]
a4af975bb3 Update dependency @rspack/core to v1.6.6 (#28370)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-05 13:17:47 +01:00
Aidan Timson
025ffd7b56 Revert "Fix underflowing text in ha-settings-row (#28339)" (#28369) 2025-12-05 11:50:37 +00:00
Jan-Philipp Benecke
3ea4a28931 Fix underflowing text in ha-settings-row (#28339) 2025-12-05 09:08:42 +00:00
Benjamin
3b092b834e Filter out hidden entities in map configuration (#28320) 2025-12-05 08:44:07 +01:00
Aidan Timson
ed8ccbe12c Add scrollable fade mixin to ha-wa-dialog (#28346) 2025-12-05 08:43:43 +01:00
karwosts
420f88f73a Fix incorrect water & gas price hints (#28357)
* Fix incorrect water price hint

* Gas
2025-12-05 08:44:23 +02:00
karwosts
086aa5fa28 Delete stop response variable on empty (#28362) 2025-12-05 08:38:39 +02:00
renovate[bot]
cca4cc512b Update dependency @rsdoctor/rspack-plugin to v1.3.12 (#28350)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-04 14:54:27 +00:00
Petar Petrov
8eb65f327a Append current state to power-sources-graph (#28330) 2025-12-04 10:18:48 +01:00
Petar Petrov
f3495feacb Fix markdown sections and styling (#28333) 2025-12-04 10:15:11 +01:00
Petar Petrov
2161bcfa3f Fix gauge severity using entity state instead of attribute value (#28331) 2025-12-04 10:13:41 +01:00
Paul Bottein
1400398422 Move reorder areas and floors to floor overflow (#28335) 2025-12-04 10:58:27 +02:00
Preet Patel
506d466c03 Fix energy dashboard redirect for device-consumption-only configs (#28322)
When users configure energy with only device consumption (no
grid/solar/battery/gas/water sources), the dashboard would redirect
to /config/energy instead of displaying. This occurred because
_generateLovelaceConfig() returned an empty views array.

The fix adds hasDeviceConsumption check and includes ENERGY_VIEW
when device consumption is configured, since energy-view-strategy
already supports device consumption cards.
2025-12-04 06:38:43 +00:00
Bram Kragten
46735c72ed Add more info to the energy demo (#28316)
* Add more info to the energy demo

* Also add battery power
2025-12-03 20:46:59 +01:00
Copilot
c43d41053b Migrate ha-button-menu to ha-dropdown in 4 files (#28300)
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Wendelin <w@pe8.at>
Co-authored-by: uptimeZERO_ <pavilionsahota@gmail.com>
2025-12-03 16:43:07 +00:00
Petar Petrov
844d53a0ba Always show energy-sources-table in overview (#28315) 2025-12-03 17:18:07 +01:00
Bram Kragten
1c8b78eae9 Bumped version to 20251203.0 2025-12-03 15:31:03 +01:00
Bram Kragten
a918e878fa Fix add matter device my link (#28313) 2025-12-03 15:30:26 +01:00
Petar Petrov
ebc354bf55 Fix label filter losing selections when searching (#28312) 2025-12-03 15:29:48 +01:00
Wendelin
98a1f5ca3a Use ha-dropdown for automations/scripts (#28293)
Co-authored-by: uptimeZERO_ <pavilionsahota@gmail.com>
2025-12-03 13:33:48 +00:00
Bram Kragten
48015ab312 Fix sticky headers in TCA dialog when target is selected (#28310) 2025-12-03 14:24:29 +01:00
Aidan Timson
09515b1937 Add subscribeLabFeature function (#28309)
* Add subscribe to lab feature function

* Add docstrings to exported functions
2025-12-03 14:16:09 +01:00
Aidan Timson
5a40627676 Add small rotation to snowflakes (#28308) 2025-12-03 14:12:08 +01:00
Wendelin
cd34447603 Hide disabled devices in automation target tree (#28307) 2025-12-03 14:02:25 +01:00
Paul Bottein
803fabbf64 Use svg for snowflakes (#28306) 2025-12-03 11:46:07 +00:00
Paul Bottein
78c4dc48d0 Rename unassigned areas to other areas (#28305) 2025-12-03 12:42:14 +01:00
Paul Bottein
147600ea43 Use core area sorting everywhere (#28304) 2025-12-03 12:28:55 +01:00
Wendelin
2f91f0dd15 Revert "Migrate updates dropdown to ha-dropdown" (#28303)
Revert "Migrate updates dropdown to ha-dropdown (#28039)"

This reverts commit ba9bab38c9.
2025-12-03 12:27:39 +01:00
ildar170975
3fa330acfb computeLovelaceEntityName(): allow "number" names to be processed (#28231)
* allow "number" names to be processed

* Apply suggestion from @MindFreeze

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-03 10:40:29 +00:00
Paul Bottein
e0a6b671ce Always set ha-wa-dialog position to fixed (#28301) 2025-12-03 10:11:54 +00:00
Wendelin
d6edd150a8 Fix filtering of floors in getAreasAndFloorsItems function (#28302) 2025-12-03 10:45:18 +01:00
Petar Petrov
2c00889921 Add Y-axis label formatter to energy charts (#28298) 2025-12-03 10:33:00 +01:00
Petar Petrov
0447d87f18 Hide empty System message in assist debug view (#28296) 2025-12-03 10:29:07 +01:00
Petar Petrov
d7e18b0520 Fix binary sensor history timeline not rendering properly (#28297) 2025-12-03 10:18:32 +01:00
Luca Félix
e7254b1587 feat: round_temperature on weather forecast card (#28103)
* feat: round_temperature on weather forecast card

* fix: use round util function

* refactor: applied comments from review
2025-12-03 10:02:25 +02:00
renovate[bot]
8681a7d450 Update dependency prettier to v3.7.3 (#28295)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 07:19:24 +01:00
Paul Bottein
fff12acb6b Handle not existing panels in dashboard config (#28292) 2025-12-02 17:23:09 +01:00
Petar Petrov
3d327ed628 Update Energy dashboard layout (#28283) 2025-12-02 16:01:17 +01:00
Wendelin
0d51648de1 Use history to manage back button click in automations add TCA (#28289) 2025-12-02 15:43:13 +01:00
Wendelin
c5642c15b8 Automation add TCA: fix narrow subtitles & icons (#28291) 2025-12-02 14:17:55 +00:00
Paul Bottein
7f885010de Add dialog to reorder areas and floors (#28272) 2025-12-02 15:12:36 +01:00
Paul Bottein
356d51f974 Only show current weather in home overview (#28288) 2025-12-02 15:38:34 +02:00
Dave T
38a907e51e Separate action field YAML examples (#27218)
* Comma separate field examples if it is a list

* Remove prettier ignore and json.stringify all examples

* Use YAML format

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-02 13:34:25 +00:00
Bram Kragten
87c0b1d887 fix paste in add tca dialog (#28286) 2025-12-02 14:30:16 +01:00
Paul Bottein
0f195015b7 Fix container alignment in section view (#28287) 2025-12-02 15:29:11 +02:00
Petar Petrov
17a976af67 Fix index value for grid return in power sankey card (#28281) 2025-12-02 14:47:15 +02:00
Aidan Timson
a41a7e822a Remove placeholder for non device area picker in entity settings (#28285)
Remove placeholder for non device area picker
2025-12-02 11:52:02 +00:00
dependabot[bot]
5473bf56c6 Bump express from 4.21.2 to 4.22.1 (#28280)
Bumps [express](https://github.com/expressjs/express) from 4.21.2 to 4.22.1.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/v4.22.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.2...v4.22.1)

---
updated-dependencies:
- dependency-name: express
  dependency-version: 4.22.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 08:17:59 +02:00
karwosts
029eba7ab8 Safer lookup of description_placeholders when service is invalid (#28273) 2025-12-02 08:08:36 +02:00
Silas Krause
824a3f288d Revert custom markdown styles (#28277) 2025-12-02 08:07:45 +02:00
renovate[bot]
fdd89c05d3 Update dependency prettier to v3.7.2 (#28276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 08:06:30 +02:00
Paul Bottein
c33cb7fff9 Clean reference to floor compare (#28269)
Fix floor compare
2025-12-01 16:47:09 +02:00
Paul Bottein
de53ad8dce Add helper for floor level (#28268)
* Add helper for floor level

* Update src/translations/en.json

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

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-12-01 15:43:57 +01:00
Simon Lamon
2dec7490b6 Include background in light, climate and security views (#28264)
* Include background

* Remove background key

* Add imports
2025-12-01 16:20:50 +02:00
Aidan Timson
6ed4bd5ce8 Match more-info-update backup preferences (#28266) 2025-12-01 15:51:31 +02:00
Petar Petrov
4e899c56ed Reduce the duration of init animation for charts to 500ms (#28262)
Reduce the duration of init animation for charts
2025-12-01 15:43:20 +02:00
Petar Petrov
53c20a0493 Add power view and restructure energy dashboard layout (#28240) 2025-12-01 14:16:11 +01:00
Wendelin
be2d6e9212 Fix automation trigger ha icon (#28265) 2025-12-01 13:05:36 +00:00
Wendelin
915442c571 Respect system area sort in automation target tree (#28263) 2025-12-01 14:01:06 +01:00
Aidan Timson
de6ebc2d0a Add missing key for labs to show in quick bar (#28261) 2025-12-01 12:26:41 +00:00
Bram Kragten
bcb12fa062 Use name instead of description_configured for triggers and conditions (#28260) 2025-12-01 13:19:19 +01:00
Aidan Timson
078915743d Make labs toolbar icon use default color (#28255) 2025-12-01 12:08:25 +01:00
Petar Petrov
54c524127f Fix refresh in energy panel subviews (#28252) 2025-12-01 12:07:45 +01:00
Wendelin
334e1c35e1 Fix ha-bottom-sheet closed event (#28257) 2025-12-01 12:04:15 +01:00
Aidan Timson
528c7727e2 Fix 1px padding for subpage titles (#28256) 2025-12-01 12:03:38 +01:00
Aidan Timson
d1043e33df Fix subpage layout icon alignment (#28254) 2025-12-01 09:54:25 +00:00
dependabot[bot]
b088f2c0e5 Bump home-assistant/wheels from 2025.10.0 to 2025.11.0 (#28251)
Bumps [home-assistant/wheels](https://github.com/home-assistant/wheels) from 2025.10.0 to 2025.11.0.
- [Release notes](https://github.com/home-assistant/wheels/releases)
- [Commits](https://github.com/home-assistant/wheels/compare/2025.10.0...2025.11.0)

---
updated-dependencies:
- dependency-name: home-assistant/wheels
  dependency-version: 2025.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 08:53:24 +02:00
dependabot[bot]
b33f407493 Bump relative-ci/agent-action from 3.2.0 to 3.2.1 (#28250)
Bumps [relative-ci/agent-action](https://github.com/relative-ci/agent-action) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/relative-ci/agent-action/releases)
- [Commits](feb19ddc69...c45aaa919e)

---
updated-dependencies:
- dependency-name: relative-ci/agent-action
  dependency-version: 3.2.1
  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>
2025-12-01 08:48:44 +02:00
dependabot[bot]
502d76b316 Bump actions/setup-python from 6.0.0 to 6.1.0 (#28249)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](e797f83bcb...83679a892e)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 08:48:24 +02:00
dependabot[bot]
1ddc07c215 Bump softprops/action-gh-release from 2.4.2 to 2.5.0 (#28248)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.4.2 to 2.5.0.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](5be0e66d93...a06a81a03e)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 2.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 08:47:32 +02:00
dependabot[bot]
a611a5fc4e Bump github/codeql-action from 4.31.4 to 4.31.5 (#28247)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.4 to 4.31.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](e12f017898...fdbfb4d275)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.5
  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>
2025-12-01 08:46:51 +02:00
eringerli
c13bece6d0 fix stacking of multiple power sources (#28243) 2025-12-01 06:25:47 +00:00
Paulus Schoutsen
28a89ff9e6 Add custom element decorators instead of customElements.define (#28235)
* Add custom element decorators instead of customElements.define

* Ignore

* prettier
2025-12-01 06:38:31 +01:00
karwosts
81b5ddec9d Add water devices to energy data download (#28242) 2025-11-30 16:00:14 +01:00
renovate[bot]
ce86aabe32 Update dependency prettier to v3.7.1 (#28239)
* Update dependency prettier to v3.7.1

* format

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-30 13:33:41 +00:00
Silas Krause
a8910bcbe4 Fix markdown rendering for cached html (#28229)
* Render markdown table in wrapper.

* Fix markdown styles

* Fix formatting

* fix rendering for cache
2025-11-30 15:21:39 +02:00
ildar170975
0e4cf9f62d ha-picker-field: change left padding to align with other controls (#28217)
change --md-list-item-leading-space & --md-list-item-trailing-space
2025-11-29 14:00:27 +02:00
renovate[bot]
506635d649 Update vaadinWebComponents monorepo to v24.9.6 (#28225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-29 11:44:28 +00:00
karwosts
27e24ee49b Add missing helper to language selector (#28218) 2025-11-29 12:35:04 +01:00
renovate[bot]
bcd712b48c Update vitest monorepo to v4.0.14 (#28215)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 20:23:38 +01:00
renovate[bot]
461ef9b916 Update dependency @rspack/core to v1.6.5 (#28207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 11:14:57 +01:00
renovate[bot]
b794989daa Update dependency @bundle-stats/plugin-webpack-filter to v4.21.7 (#28205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-28 08:29:33 +02:00
Silas Krause
6727ffaae0 Fix markdown styles regression (#28202)
* Render markdown table in wrapper.

* Fix markdown styles

* Fix formatting
2025-11-28 08:28:57 +02:00
Paul Bottein
4df8501b20 Fix ha icon size (#28201) 2025-11-27 22:54:44 +01:00
Aidan Timson
539d0e443f Fix ha-wa-dialog fullscreen and make alerts not fullscreen (#28175) 2025-11-27 22:01:30 +01:00
renovate[bot]
9c9d274b5c Update dependency typescript-eslint to v8.48.0 (#28196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-27 19:20:07 +01:00
Paul Bottein
d6e6bc0e80 Fix safe area for sidebar section views in Android (#28194) 2025-11-27 19:46:56 +02:00
ildar170975
530a70b168 Automations, scripts, scenes: add a tooltip for relative time (#28158)
* add a tooltip for last_triggered

* add a tooltip for last_triggered

* add a tooltip for last_activated

* Apply suggestions from code review

* Apply suggestion from @MindFreeze

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

* Apply suggestion from @MindFreeze

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

* Apply suggestion from @MindFreeze

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

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-27 19:44:11 +02:00
Wendelin
23137500f8 Add TCA by target sort like item collections (#28192) 2025-11-27 17:03:27 +01:00
Wendelin
63e7ed21a4 Fix lab automations icons and sidebar width (#28184)
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-27 16:57:42 +01:00
Petar Petrov
92611f46f4 Fix water sankey calculation to include total supply from sources (#28191) 2025-11-27 16:44:56 +01:00
Bram Kragten
9bc896241d Always store token when using develop and serve (#28179) 2025-11-27 16:43:36 +01:00
Paul Bottein
2baafe620c Add hint to reorder areas and floors (#28189) 2025-11-27 16:32:23 +01:00
Wendelin
ce52bbaf8c Show hidden entities in target tree (#28181)
* Show hidden entities in target tree

* Fix types
2025-11-27 15:44:50 +02:00
Wendelin
0b4b8d9082 "Add TCA" dialog desktop height to 800px (#28182) 2025-11-27 14:42:18 +01:00
Petar Petrov
bddbb773b7 Fix sankey chart resizing (#28180) 2025-11-27 15:41:46 +02:00
Paul Bottein
d52e1e8835 Use hui-root for panel energy (#28149)
* Use hui-root for panel energy

* Review feedback

* Set empty prefs
2025-11-27 15:35:36 +02:00
Petar Petrov
0a9dccfd19 Refactor power sankey hierarchy to handle devices with not power sensor (#28164) 2025-11-27 12:21:55 +01:00
Petar Petrov
bfd78670cc Disable axis pointer on the energy devices bar chart to fix refresh issues on touch devices (#28163) 2025-11-27 12:20:06 +01:00
Petar Petrov
11276af1a0 Handle grouping by floor and area in power sankey card (#28162) 2025-11-27 12:19:42 +01:00
Paul Bottein
d7be46c00b Fix box shadow for sidebar tabs (#28170) 2025-11-27 12:19:15 +01:00
Paul Bottein
94f32ce242 Fix disabled dashboard picker when no custom dashboard (#28172) 2025-11-27 12:18:48 +01:00
Wendelin
ef3e8186bc Fix add condition default tab and blank styles (#28166) 2025-11-27 12:18:21 +01:00
Paul Bottein
50fcf622aa Fix labs back button (#28174) 2025-11-27 12:42:55 +02:00
Paul Bottein
77c2444be8 Restore sidebar view when clicking back (#28167) 2025-11-27 12:42:36 +02:00
Wendelin
e5cb26cd3d Fix automation add TCA autofocus (#28168)
Fix automation add tca autofocus
2025-11-27 11:28:18 +01:00
Simon Lamon
2896519bfd Don't show more info for untracked consumption (#28151) 2025-11-27 09:55:23 +02:00
ildar170975
0b6e35eb53 Data tables: make sorting direction 2-state instead of 3-state (#28160)
* sorting is 2-state

* sorting is 2-state

* sorting is 2-state
2025-11-27 09:54:38 +02:00
Iván Pereira
e80a855f87 Fix hide sidebar tooltip on touchend events (#28042)
* fix: hide sidebar tooltip on touchend events

* Add a comment recommended by Copilot

* Clear timeouts id in disconnectedCallback
2025-11-27 09:03:23 +02:00
dependabot[bot]
7c88cf4e30 Bump node-forge from 1.3.1 to 1.3.2 (#28157)
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.3.1 to 1.3.2.
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.1...v1.3.2)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-version: 1.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-27 06:49:09 +01:00
Petar Petrov
9001cd3e65 Replace gauges with energy usage graph in energy overview (#28150) 2025-11-26 17:37:18 +01:00
renovate[bot]
ca8923d8f4 Update dependency glob to v13 (#28135)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-26 18:13:05 +02:00
237 changed files with 6448 additions and 4143 deletions

View File

@@ -21,12 +21,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -56,12 +56,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: master
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -24,9 +24,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -58,9 +58,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -76,9 +76,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -100,9 +100,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
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@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
# 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@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7

View File

@@ -22,12 +22,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,12 +57,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: master
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -16,10 +16,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -21,10 +21,10 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -20,15 +20,15 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@feb19ddc698445db27401f1490f6ac182da0816f # v3.2.0
uses: relative-ci/agent-action@c45aaa919ef85620af54242a241ac17a8fa35983 # v3.2.1
with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}

View File

@@ -23,10 +23,10 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -34,7 +34,7 @@ jobs:
uses: home-assistant/actions/helpers/verify-version@master
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -55,7 +55,7 @@ jobs:
script/release
- name: Upload release assets
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
files: |
dist/*.whl
@@ -75,7 +75,7 @@ jobs:
# home-assistant/wheels doesn't support SHA pinning
- name: Build wheels
uses: home-assistant/wheels@2025.10.0
uses: home-assistant/wheels@2025.12.0
with:
abi: cp313
tag: musllinux_1_2
@@ -91,9 +91,9 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -108,7 +108,7 @@ jobs:
- name: Tar folder
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
- name: Upload release asset
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
@@ -120,9 +120,9 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -137,6 +137,6 @@ jobs:
- name: Tar folder
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
- name: Upload release asset
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Upload Translations
run: |

View File

@@ -156,7 +156,9 @@ const createTestTranslation = () =>
*/
const createMasterTranslation = () =>
gulp
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])], {
allowEmpty: true,
})
.pipe(new CustomJSON(lokaliseTransform))
.pipe(new MergeJSON("en"))
.pipe(gulp.dest(workDir));

View File

@@ -305,9 +305,8 @@ export class HcMain extends HassElement {
await llColl.refresh();
this._unsubLovelace = llColl.subscribe(async (rawConfig) => {
if (isStrategyDashboard(rawConfig)) {
const { generateLovelaceDashboardStrategy } = await import(
"../../../../src/panels/lovelace/strategies/get-strategy"
);
const { generateLovelaceDashboardStrategy } =
await import("../../../../src/panels/lovelace/strategies/get-strategy");
const config = await generateLovelaceDashboardStrategy(
rawConfig,
this.hass!
@@ -347,9 +346,8 @@ export class HcMain extends HassElement {
}
private async _generateDefaultLovelaceConfig() {
const { generateLovelaceDashboardStrategy } = await import(
"../../../../src/panels/lovelace/strategies/get-strategy"
);
const { generateLovelaceDashboardStrategy } =
await import("../../../../src/panels/lovelace/strategies/get-strategy");
this._handleNewLovelaceConfig(
await generateLovelaceDashboardStrategy(DEFAULT_CONFIG, this.hass!)
);

View File

@@ -44,18 +44,24 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
number_energy_price: null,
},
],
power: [
{ stat_rate: "sensor.power_grid" },
{ stat_rate: "sensor.power_grid_return" },
],
cost_adjustment_day: 0,
},
{
type: "solar",
stat_energy_from: "sensor.solar_production",
stat_rate: "sensor.power_solar",
config_entry_solar_forecast: ["solar_forecast"],
},
/* {
{
type: "battery",
stat_energy_from: "sensor.battery_output",
stat_energy_to: "sensor.battery_input",
}, */
stat_rate: "sensor.power_battery",
},
{
type: "gas",
stat_energy_from: "sensor.energy_gas",
@@ -63,28 +69,48 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
entity_energy_price: null,
number_energy_price: null,
},
{
type: "water",
stat_energy_from: "sensor.energy_water",
stat_cost: "sensor.energy_water_cost",
entity_energy_price: null,
number_energy_price: null,
},
],
device_consumption: [
{
stat_consumption: "sensor.energy_car",
stat_rate: "sensor.power_car",
},
{
stat_consumption: "sensor.energy_ac",
stat_rate: "sensor.power_ac",
},
{
stat_consumption: "sensor.energy_washing_machine",
stat_rate: "sensor.power_washing_machine",
},
{
stat_consumption: "sensor.energy_dryer",
stat_rate: "sensor.power_dryer",
},
{
stat_consumption: "sensor.energy_heat_pump",
stat_rate: "sensor.power_heat_pump",
},
{
stat_consumption: "sensor.energy_boiler",
stat_rate: "sensor.power_boiler",
},
],
device_consumption_water: [
{
stat_consumption: "sensor.water_kitchen",
},
{
stat_consumption: "sensor.water_garden",
},
],
device_consumption_water: [],
})
);
hass.mockWS(

View File

@@ -154,6 +154,38 @@ export const energyEntities = () =>
unit_of_measurement: "EUR",
},
},
"sensor.power_grid": {
entity_id: "sensor.power_grid",
state: "500",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.power_grid_return": {
entity_id: "sensor.power_grid_return",
state: "-100",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.power_solar": {
entity_id: "sensor.power_solar",
state: "200",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.power_battery": {
entity_id: "sensor.power_battery",
state: "100",
attributes: {
state_class: "measurement",
unit_of_measurement: "W",
},
},
"sensor.energy_gas_cost": {
entity_id: "sensor.energy_gas_cost",
state: "2",
@@ -171,6 +203,15 @@ export const energyEntities = () =>
unit_of_measurement: "m³",
},
},
"sensor.energy_water": {
entity_id: "sensor.energy_water",
state: "4000",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Water",
unit_of_measurement: "L",
},
},
"sensor.energy_car": {
entity_id: "sensor.energy_car",
state: "4",
@@ -225,4 +266,58 @@ export const energyEntities = () =>
unit_of_measurement: "kWh",
},
},
"sensor.power_car": {
entity_id: "sensor.power_car",
state: "40",
attributes: {
state_class: "measurement",
friendly_name: "Electric car",
unit_of_measurement: "W",
},
},
"sensor.power_ac": {
entity_id: "sensor.power_ac",
state: "30",
attributes: {
state_class: "measurement",
friendly_name: "Air conditioning",
unit_of_measurement: "W",
},
},
"sensor.power_washing_machine": {
entity_id: "sensor.power_washing_machine",
state: "60",
attributes: {
state_class: "measurement",
friendly_name: "Washing machine",
unit_of_measurement: "W",
},
},
"sensor.power_dryer": {
entity_id: "sensor.power_dryer",
state: "55",
attributes: {
state_class: "measurement",
friendly_name: "Dryer",
unit_of_measurement: "W",
},
},
"sensor.power_heat_pump": {
entity_id: "sensor.power_heat_pump",
state: "60",
attributes: {
state_class: "measurement",
friendly_name: "Heat pump",
unit_of_measurement: "W",
},
},
"sensor.power_boiler": {
entity_id: "sensor.power_boiler",
state: "70",
attributes: {
state_class: "measurement",
friendly_name: "Boiler",
unit_of_measurement: "W",
},
},
});

View File

@@ -17,17 +17,15 @@ const generateMeanStatistics = (
end: Date,
// eslint-disable-next-line default-param-last
period: "5minute" | "hour" | "day" | "month" = "hour",
initValue: number,
maxDiff: number
): StatisticValue[] => {
const statistics: StatisticValue[] = [];
let currentDate = new Date(start);
currentDate.setMinutes(0, 0, 0);
let lastVal = initValue;
const now = new Date();
while (end > currentDate && currentDate < now) {
const delta = Math.random() * maxDiff;
const mean = lastVal + delta;
const mean = delta;
statistics.push({
start: currentDate.getTime(),
end: currentDate.getTime(),
@@ -38,7 +36,6 @@ const generateMeanStatistics = (
state: mean,
sum: null,
});
lastVal = mean;
currentDate =
period === "day"
? addDays(currentDate, 1)
@@ -336,7 +333,6 @@ export const mockRecorder = (mockHass: MockHomeAssistant) => {
start,
end,
period,
state,
state * (state > 80 ? 0.05 : 0.1)
);
}

View File

@@ -0,0 +1,3 @@
---
title: Adaptive dialog (ha-adaptive-dialog)
---

View File

@@ -0,0 +1,732 @@
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { mdiCog, mdiHelp } from "@mdi/js";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-dialog-footer";
import "../../../../src/components/ha-adaptive-dialog";
import "../../../../src/components/ha-form/ha-form";
import "../../../../src/components/ha-icon-button";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import type { HomeAssistant } from "../../../../src/types";
const SCHEMA: HaFormSchema[] = [
{ type: "string", name: "Name", default: "", autofocus: true },
{ type: "string", name: "Email", default: "" },
];
type DialogType =
| false
| "basic"
| "basic-subtitle-below"
| "basic-subtitle-above"
| "form"
| "form-block-mode"
| "actions"
| "large"
| "small";
@customElement("demo-components-ha-adaptive-dialog")
export class DemoHaAdaptiveDialog extends LitElement {
@state() private _openDialog: DialogType = false;
@state() private _hass?: HomeAssistant;
protected firstUpdated() {
const hass = provideHass(this);
this._hass = hass;
}
protected render() {
return html`
<div class="content">
<h1>Adaptive dialog <code>&lt;ha-adaptive-dialog&gt;</code></h1>
<p class="subtitle">
Responsive dialog component that automatically switches between a full
dialog and bottom sheet based on screen size.
</p>
<h2>Demos</h2>
<div class="buttons">
<ha-button @click=${this._handleOpenDialog("basic")}
>Basic adaptive dialog</ha-button
>
<ha-button @click=${this._handleOpenDialog("basic-subtitle-below")}
>Adaptive dialog with subtitle below</ha-button
>
<ha-button @click=${this._handleOpenDialog("basic-subtitle-above")}
>Adaptive dialog with subtitle above</ha-button
>
<ha-button @click=${this._handleOpenDialog("small")}
>Small width adaptive dialog</ha-button
>
<ha-button @click=${this._handleOpenDialog("large")}
>Large width adaptive dialog</ha-button
>
<ha-button @click=${this._handleOpenDialog("form")}
>Adaptive dialog with form</ha-button
>
<ha-button @click=${this._handleOpenDialog("form-block-mode")}
>Adaptive dialog with form (block mode change)</ha-button
>
<ha-button @click=${this._handleOpenDialog("actions")}
>Adaptive dialog with actions</ha-button
>
</div>
<ha-card>
<div class="card-content">
<p>
<strong>Tip:</strong> Resize your browser window to see the
responsive behavior. The dialog automatically switches to a bottom
sheet on narrow screens (&lt;870px width) or short screens
(&lt;500px height).
</p>
</div>
</ha-card>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "basic"}
header-title="Basic adaptive dialog"
@closed=${this._handleClosed}
>
<div>Adaptive dialog content</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "basic-subtitle-below"}
header-title="Adaptive dialog with subtitle"
header-subtitle="This is an adaptive dialog with a subtitle below"
@closed=${this._handleClosed}
>
<div>Adaptive dialog content</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "basic-subtitle-above"}
header-title="Adaptive dialog with subtitle above"
header-subtitle="This is an adaptive dialog with a subtitle above"
header-subtitle-position="above"
@closed=${this._handleClosed}
>
<div>Adaptive dialog content</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "small"}
width="small"
header-title="Small adaptive dialog"
@closed=${this._handleClosed}
>
<div>This dialog uses the small width preset (320px).</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "large"}
width="large"
header-title="Large adaptive dialog"
@closed=${this._handleClosed}
>
<div>This dialog uses the large width preset (1024px).</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "form"}
header-title="Adaptive dialog with form"
header-subtitle="This is an adaptive dialog with a form"
@closed=${this._handleClosed}
>
<ha-form autofocus .schema=${SCHEMA}></ha-form>
<ha-dialog-footer slot="footer">
<ha-button
@click=${this._handleClosed}
slot="secondaryAction"
variant="plain"
>Cancel</ha-button
>
<ha-button
@click=${this._handleClosed}
slot="primaryAction"
variant="accent"
>Submit</ha-button
>
</ha-dialog-footer>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "form-block-mode"}
header-title="Adaptive dialog with form (block mode change)"
header-subtitle="This form will not reset when the viewport size changes"
block-mode-change
@closed=${this._handleClosed}
>
<ha-form autofocus .schema=${SCHEMA}></ha-form>
<ha-dialog-footer slot="footer">
<ha-button
@click=${this._handleClosed}
slot="secondaryAction"
variant="plain"
>Cancel</ha-button
>
<ha-button
@click=${this._handleClosed}
slot="primaryAction"
variant="accent"
>Submit</ha-button
>
</ha-dialog-footer>
</ha-adaptive-dialog>
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "actions"}
header-title="Adaptive dialog with actions"
header-subtitle="This is an adaptive dialog with header actions"
@closed=${this._handleClosed}
>
<div slot="headerActionItems">
<ha-icon-button label="Settings" path=${mdiCog}></ha-icon-button>
<ha-icon-button label="Help" path=${mdiHelp}></ha-icon-button>
</div>
<div>Adaptive dialog content</div>
</ha-adaptive-dialog>
<h2>Design</h2>
<h3>Responsive behavior</h3>
<p>
The <code>ha-adaptive-dialog</code> component automatically switches
between two modes based on screen size:
</p>
<ul>
<li>
<strong>Dialog mode:</strong> Used on larger screens (width &gt;
870px and height &gt; 500px). Renders as a centered dialog using
<code>ha-wa-dialog</code>.
</li>
<li>
<strong>Bottom sheet mode:</strong> Used on mobile devices and
smaller screens (width ≤ 870px or height ≤ 500px). Renders as a
drawer from the bottom using <code>ha-bottom-sheet</code>.
</li>
</ul>
<p>
The mode is determined automatically and updates when the window is
resized. To prevent mode changes after the initial mount (useful for
preventing form resets), use the <code>block-mode-change</code>
attribute.
</p>
<h3>Width</h3>
<p>
In dialog mode, there are multiple width presets available. These are
ignored in bottom sheet mode.
</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>small</code></td>
<td><code>min(320px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>medium</code></td>
<td><code>min(580px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>large</code></td>
<td><code>min(1024px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>full</code></td>
<td><code>var(--full-width)</code></td>
</tr>
</tbody>
</table>
<p>Adaptive dialogs have a default width of <code>medium</code>.</p>
<h3>Header</h3>
<p>
The header contains a navigation icon, title, subtitle, and action
items.
</p>
<table>
<thead>
<tr>
<th>Slot</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>headerNavigationIcon</code></td>
<td>
Leading header action (e.g., close/back button). In bottom sheet
mode, defaults to a close button if not provided.
</td>
</tr>
<tr>
<td><code>headerTitle</code></td>
<td>The header title content.</td>
</tr>
<tr>
<td><code>headerSubtitle</code></td>
<td>The header subtitle content.</td>
</tr>
<tr>
<td><code>headerActionItems</code></td>
<td>Trailing header actions (e.g., icon buttons, menus).</td>
</tr>
</tbody>
</table>
<h4>Header title</h4>
<p>
The header title can be set using the <code>header-title</code>
attribute or by providing custom content in the
<code>headerTitle</code> slot.
</p>
<h4>Header subtitle</h4>
<p>
The header subtitle can be set using the
<code>header-subtitle</code> attribute or by providing custom content
in the <code>headerSubtitle</code> slot. The subtitle position
relative to the title can be controlled with the
<code>header-subtitle-position</code> attribute.
</p>
<h4>Header navigation icon</h4>
<p>
In bottom sheet mode, a close button is automatically provided if no
custom navigation icon is specified. In dialog mode, the dialog can be
closed via the standard dialog close button.
</p>
<h4>Header action items</h4>
<p>
The header action items usually contain icon buttons and/or menu
buttons.
</p>
<h3>Body</h3>
<p>The body is the content of the adaptive dialog.</p>
<h3>Footer</h3>
<p>The footer is the footer of the adaptive dialog.</p>
<p>
It is recommended to use the <code>ha-dialog-footer</code> component
for the footer and to style the buttons inside the footer as follows:
</p>
<table>
<thead>
<tr>
<th>Slot</th>
<th>Description</th>
<th>Variant to use</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>secondaryAction</code></td>
<td>The secondary action button(s).</td>
<td><code>plain</code></td>
</tr>
<tr>
<td><code>primaryAction</code></td>
<td>The primary action button(s).</td>
<td><code>accent</code></td>
</tr>
</tbody>
</table>
<h2>Implementation</h2>
<h3>When to use</h3>
<p>
Use <code>ha-adaptive-dialog</code> when you need a dialog that should
adapt to different screen sizes automatically. This is particularly
useful for:
</p>
<ul>
<li>Forms and data entry that need to work well on mobile devices</li>
<li>
Content that benefits from full-screen presentation on small devices
</li>
<li>
Interfaces that need consistent behavior across desktop and mobile
</li>
</ul>
<p>
If you don't need responsive behavior, use
<code>ha-wa-dialog</code> directly for desktop-only dialogs or
<code>ha-bottom-sheet</code> for mobile-only sheets.
</p>
<p>
Use the <code>block-mode-change</code> attribute when you want to
prevent the dialog from switching modes after it's opened. This is
especially useful for forms, as it prevents form data from being lost
when users resize their browser window.
</p>
<h3>Example usage</h3>
<pre><code>&lt;ha-adaptive-dialog
.hass=\${this.hass}
open
width="medium"
header-title="Dialog title"
header-subtitle="Dialog subtitle"
&gt;
&lt;div slot="headerActionItems"&gt;
&lt;ha-icon-button label="Settings" path="mdiCog"&gt;&lt;/ha-icon-button&gt;
&lt;ha-icon-button label="Help" path="mdiHelp"&gt;&lt;/ha-icon-button&gt;
&lt;/div&gt;
&lt;div&gt;Dialog content&lt;/div&gt;
&lt;ha-dialog-footer slot="footer"&gt;
&lt;ha-button slot="secondaryAction" variant="plain"
&gt;Cancel&lt;/ha-button
&gt;
&lt;ha-button slot="primaryAction" variant="accent"&gt;Submit&lt;/ha-button&gt;
&lt;/ha-dialog-footer&gt;
&lt;/ha-adaptive-dialog&gt;</code></pre>
<p>Example with <code>block-mode-change</code> for forms:</p>
<pre><code>&lt;ha-adaptive-dialog
.hass=\${this.hass}
open
header-title="Edit configuration"
block-mode-change
&gt;
&lt;ha-form .schema=\${schema} .data=\${data}&gt;&lt;/ha-form&gt;
&lt;ha-dialog-footer slot="footer"&gt;
&lt;ha-button slot="secondaryAction" variant="plain"
&gt;Cancel&lt;/ha-button
&gt;
&lt;ha-button slot="primaryAction" variant="accent"&gt;Save&lt;/ha-button&gt;
&lt;/ha-dialog-footer&gt;
&lt;/ha-adaptive-dialog&gt;</code></pre>
<h3>API</h3>
<p>
This component combines <code>ha-wa-dialog</code> and
<code>ha-bottom-sheet</code> with automatic mode switching based on
screen size.
</p>
<h4>Attributes</h4>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
<th>Options</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>open</code></td>
<td>Controls the adaptive dialog open state.</td>
<td><code>false</code></td>
<td><code>false</code>, <code>true</code></td>
</tr>
<tr>
<td><code>width</code></td>
<td>
Preferred dialog width preset (dialog mode only, ignored in
bottom sheet mode).
</td>
<td><code>medium</code></td>
<td>
<code>small</code>, <code>medium</code>, <code>large</code>,
<code>full</code>
</td>
</tr>
<tr>
<td><code>header-title</code></td>
<td>Header title text when no custom title slot is provided.</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>header-subtitle</code></td>
<td>
Header subtitle text when no custom subtitle slot is provided.
</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>header-subtitle-position</code></td>
<td>Position of the subtitle relative to the title.</td>
<td><code>below</code></td>
<td><code>above</code>, <code>below</code></td>
</tr>
<tr>
<td><code>aria-labelledby</code></td>
<td>
The ID of the element that labels the dialog (for
accessibility).
</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>aria-describedby</code></td>
<td>
The ID of the element that describes the dialog (for
accessibility).
</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>block-mode-change</code></td>
<td>
When set, the mode is determined at mount time based on the
current screen size, but subsequent mode changes are blocked.
Useful for preventing forms from resetting when the viewport
size changes.
</td>
<td><code>false</code></td>
<td><code>false</code>, <code>true</code></td>
</tr>
</tbody>
</table>
<h4>CSS custom properties</h4>
<table>
<thead>
<tr>
<th>CSS Property</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--ha-dialog-surface-background</code></td>
<td>Dialog/sheet background color.</td>
</tr>
<tr>
<td><code>--ha-dialog-border-radius</code></td>
<td>Border radius of the dialog surface (dialog mode only).</td>
</tr>
<tr>
<td><code>--ha-dialog-show-duration</code></td>
<td>Show animation duration (dialog mode only).</td>
</tr>
<tr>
<td><code>--ha-dialog-hide-duration</code></td>
<td>Hide animation duration (dialog mode only).</td>
</tr>
</tbody>
</table>
<h4>Events</h4>
<table>
<thead>
<tr>
<th>Event</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>opened</code></td>
<td>
Fired when the adaptive dialog is shown (dialog mode only).
</td>
</tr>
<tr>
<td><code>closed</code></td>
<td>
Fired after the adaptive dialog is hidden (dialog mode only).
</td>
</tr>
<tr>
<td><code>after-show</code></td>
<td>Fired after show animation completes (dialog mode only).</td>
</tr>
</tbody>
</table>
<h3>Focus management</h3>
<p>
To automatically focus an element when the adaptive dialog opens, add
the
<code>autofocus</code> attribute to it. Components with
<code>delegatesFocus: true</code> (like <code>ha-form</code>) will
forward focus to their first focusable child.
</p>
<p>Example:</p>
<pre><code>&lt;ha-adaptive-dialog .hass=\${this.hass} open&gt;
&lt;ha-form autofocus .schema=\${schema}&gt;&lt;/ha-form&gt;
&lt;/ha-adaptive-dialog&gt;</code></pre>
</div>
`;
}
private _handleOpenDialog = (dialog: DialogType) => () => {
this._openDialog = dialog;
};
private _handleClosed = () => {
this._openDialog = false;
};
static styles = [
css`
:host {
display: block;
padding: var(--ha-space-4);
}
.content {
max-width: 1000px;
margin: 0 auto;
}
h1 {
margin-top: 0;
margin-bottom: var(--ha-space-2);
}
h2 {
margin-top: var(--ha-space-6);
margin-bottom: var(--ha-space-3);
}
h3,
h4 {
margin-top: var(--ha-space-4);
margin-bottom: var(--ha-space-2);
}
p {
margin: var(--ha-space-2) 0;
line-height: 1.6;
}
ul {
margin: var(--ha-space-2) 0;
padding-left: var(--ha-space-5);
}
li {
margin: var(--ha-space-1) 0;
line-height: 1.6;
}
.subtitle {
color: var(--secondary-text-color);
font-size: 1.1em;
margin-bottom: var(--ha-space-4);
}
table {
width: 100%;
border-collapse: collapse;
margin: var(--ha-space-3) 0;
}
th,
td {
text-align: left;
padding: var(--ha-space-2);
border-bottom: 1px solid var(--divider-color);
}
th {
font-weight: 500;
}
code {
background-color: var(--secondary-background-color);
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
}
pre {
background-color: var(--secondary-background-color);
padding: var(--ha-space-3);
border-radius: 8px;
overflow-x: auto;
margin: var(--ha-space-3) 0;
}
pre code {
background-color: transparent;
padding: 0;
}
.buttons {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: var(--ha-space-2);
margin: var(--ha-space-4) 0;
}
.card-content {
padding: var(--ha-space-3);
}
a {
color: var(--primary-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-adaptive-dialog": DemoHaAdaptiveDialog;
}
}

View File

@@ -139,7 +139,7 @@ export class DemoHaWaDialog extends LitElement {
</tr>
<tr>
<td><code>large</code></td>
<td><code>min(720px, var(--full-width))</code></td>
<td><code>min(1024px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>full</code></td>
@@ -381,10 +381,6 @@ export class DemoHaWaDialog extends LitElement {
<td><code>--dialog-z-index</code></td>
<td>Z-index for the dialog.</td>
</tr>
<tr>
<td><code>--dialog-surface-position</code></td>
<td>CSS position of the dialog surface.</td>
</tr>
<tr>
<td><code>--dialog-surface-margin-top</code></td>
<td>Top margin for the dialog surface.</td>

View File

@@ -88,8 +88,8 @@ class HassioRegistriesDialog extends LitElement {
<ha-button
?disabled=${Boolean(
!this._input.registry ||
!this._input.username ||
!this._input.password
!this._input.username ||
!this._input.password
)}
@click=${this._addNewRegistry}
appearance="filled"

View File

@@ -52,7 +52,7 @@
"@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@home-assistant/webawesome": "3.0.0-ha.0",
"@home-assistant/webawesome": "3.0.0-ha.1",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6",
@@ -89,8 +89,8 @@
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.9.5",
"@vaadin/vaadin-themable-mixin": "24.9.5",
"@vaadin/combo-box": "24.9.6",
"@vaadin/vaadin-themable-mixin": "24.9.6",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
@@ -152,13 +152,13 @@
"@babel/helper-define-polyfill-provider": "0.6.5",
"@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.5",
"@bundle-stats/plugin-webpack-filter": "4.21.6",
"@bundle-stats/plugin-webpack-filter": "4.21.7",
"@lokalise/node-api": "15.4.0",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.3.11",
"@rspack/core": "1.6.4",
"@rsdoctor/rspack-plugin": "1.3.12",
"@rspack/core": "1.6.6",
"@rspack/dev-server": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.22",
@@ -178,7 +178,7 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.13",
"@vitest/coverage-v8": "4.0.15",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
@@ -194,7 +194,7 @@
"eslint-plugin-wc": "3.0.2",
"fancy-log": "2.0.0",
"fs-extra": "11.3.2",
"glob": "12.0.0",
"glob": "13.0.0",
"gulp": "5.0.1",
"gulp-brotli": "3.0.0",
"gulp-json-transform": "0.5.0",
@@ -209,17 +209,17 @@
"lodash.template": "4.5.0",
"map-stream": "0.0.7",
"pinst": "3.0.0",
"prettier": "3.6.2",
"prettier": "3.7.4",
"rspack-manifest-plugin": "5.2.0",
"serve": "14.2.5",
"sinon": "21.0.0",
"tar": "7.5.2",
"terser-webpack-plugin": "5.3.14",
"terser-webpack-plugin": "5.3.15",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.47.0",
"typescript-eslint": "8.48.1",
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.13",
"vitest": "4.0.15",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20251029.0"
version = "20251203.0"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"

View File

@@ -1,5 +1,6 @@
import type { AuthData } from "home-assistant-js-websocket";
import { extractSearchParam } from "../url/search-params";
import { hassUrl } from "../../data/auth";
declare global {
interface Window {
@@ -30,7 +31,11 @@ export function askWrite() {
export function saveTokens(tokens: AuthData | null) {
tokenCache.tokens = tokens;
if (!tokenCache.writeEnabled && extractSearchParam("storeToken") === "true") {
if (
!tokenCache.writeEnabled &&
(extractSearchParam("storeToken") === "true" ||
hassUrl !== `${location.protocol}//${location.host}`)
) {
tokenCache.writeEnabled = true;
}

View File

@@ -45,9 +45,8 @@ export const computeFormatFunctions = async (
formatEntityAttributeName: FormatEntityAttributeNameFunc;
formatEntityName: FormatEntityNameFunc;
}> => {
const { computeStateDisplay } = await import(
"../entity/compute_state_display"
);
const { computeStateDisplay } =
await import("../entity/compute_state_display");
const { computeAttributeValueDisplay, computeAttributeNameDisplay } =
await import("../entity/compute_attribute_display");

View File

@@ -593,6 +593,7 @@ export class HaChartBase extends LitElement {
}
const options = {
animation: !this._reducedMotion,
animationDuration: 500,
darkMode: this._themes.darkMode ?? false,
aria: { show: true },
dataZoom: this._getDataZoomConfig(),

View File

@@ -167,6 +167,7 @@ export class HaSankeyChart extends LitElement {
curveness: 0.5,
},
layoutIterations: 0,
animationDuration: 500,
label: {
formatter: (params) =>
data.nodes.find((node) => node.id === (params.data as Node).id)
@@ -279,6 +280,7 @@ export class HaSankeyChart extends LitElement {
:host {
display: block;
flex: 1;
max-width: 100%;
background: var(--ha-card-background, var(--card-background-color));
}
ha-chart-base {

View File

@@ -1,6 +1,6 @@
import type { PropertyValues } from "lit";
import { html, LitElement } from "lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import type { VisualMapComponentOption } from "echarts/components";
import type { LineSeriesOption } from "echarts/charts";
import type { YAXisOption } from "echarts/types/dist/shared";
@@ -27,6 +27,7 @@ const safeParseFloat = (value) => {
return isFinite(parsed) ? parsed : null;
};
@customElement("state-history-chart-line")
export class StateHistoryChartLine extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -795,7 +796,6 @@ export class StateHistoryChartLine extends LitElement {
return Math.abs(value) < 1 ? value : roundingFn(value);
}
}
customElements.define("state-history-chart-line", StateHistoryChartLine);
declare global {
interface HTMLElementTagNameMap {

View File

@@ -373,6 +373,7 @@ export class StateHistoryChartTimeline extends LitElement {
itemName: 3,
},
renderItem: this._renderItem,
progressive: 0,
});
});

View File

@@ -2,15 +2,17 @@ import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import type { LabelRegistryEntry } from "../../data/label_registry";
import { computeCssColor } from "../../common/color/compute-color";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-label";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { stringCompare } from "../../common/string/compare";
import type { LabelRegistryEntry } from "../../data/label_registry";
import "../chips/ha-chip-set";
import "../ha-button-menu";
import "../ha-dropdown";
import "../ha-dropdown-item";
import type { HaDropdownItem } from "../ha-dropdown-item";
import "../ha-icon";
import "../ha-list-item";
import "../ha-label";
@customElement("ha-data-table-labels")
class HaDataTableLabels extends LitElement {
@@ -26,12 +28,11 @@ class HaDataTableLabels extends LitElement {
(label) => this._renderLabel(label, true)
)}
${labels.length > 2
? html`<ha-button-menu
absolute
? html`<ha-dropdown
role="button"
tabindex="0"
@click=${this._handleIconOverflowMenuOpened}
@closed=${this._handleIconOverflowMenuClosed}
@click=${stopPropagation}
@wa-select=${this._handleDropdownSelect}
>
<ha-label slot="trigger" class="plus" dense>
+${labels.length - 2}
@@ -40,12 +41,12 @@ class HaDataTableLabels extends LitElement {
labels.slice(2),
(label) => label.label_id,
(label) => html`
<ha-list-item @click=${this._labelClicked} .item=${label}>
<ha-dropdown-item .value=${label.label_id} .item=${label}>
${this._renderLabel(label, false)}
</ha-list-item>
</ha-dropdown-item>
`
)}
</ha-button-menu>`
</ha-dropdown>`
: nothing}
</ha-chip-set>
`;
@@ -81,21 +82,12 @@ class HaDataTableLabels extends LitElement {
fireEvent(this, "label-clicked", { label });
}
protected _handleIconOverflowMenuOpened(e) {
e.stopPropagation();
// If this component is used inside a data table, the z-index of the row
// needs to be increased. Otherwise the ha-button-menu would be displayed
// underneath the next row in the table.
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
if (row) {
row.style.zIndex = "1";
}
}
protected _handleIconOverflowMenuClosed() {
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
if (row) {
row.style.zIndex = "";
private _handleDropdownSelect(
ev: CustomEvent<{ item: HaDropdownItem & { item?: LabelRegistryEntry } }>
) {
const label = ev.detail?.item?.item;
if (label) {
fireEvent(this, "label-clicked", { label });
}
}
@@ -114,9 +106,6 @@ class HaDataTableLabels extends LitElement {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
ha-button-menu {
border-radius: 10px;
}
.plus {
--ha-label-background-color: transparent;
border: 1px solid var(--divider-color);

View File

@@ -838,10 +838,10 @@ export class HaDataTable extends LitElement {
} else if (this.sortDirection === "asc") {
this.sortDirection = "desc";
} else {
this.sortDirection = null;
this.sortDirection = "asc";
}
this.sortColumn = this.sortDirection === null ? undefined : columnId;
this.sortColumn = columnId;
fireEvent(this, "sorting-changed", {
column: columnId,

View File

@@ -2,7 +2,7 @@ import { mdiAlert } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../common/entity/compute_domain";
@@ -17,6 +17,7 @@ import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
import type { HomeAssistant } from "../../types";
import "../ha-state-icon";
@customElement("state-badge")
export class StateBadge extends LitElement {
public hass?: HomeAssistant;
@@ -265,5 +266,3 @@ declare global {
"state-badge": StateBadge;
}
}
customElements.define("state-badge", StateBadge);

View File

@@ -0,0 +1,188 @@
import { mdiClose } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { HomeAssistant } from "../types";
import { listenMediaQuery } from "../common/dom/media_query";
import "./ha-bottom-sheet";
import "./ha-dialog-header";
import "./ha-icon-button";
import "./ha-wa-dialog";
import type { DialogWidth } from "./ha-wa-dialog";
type DialogSheetMode = "dialog" | "bottom-sheet";
/**
* Home Assistant adaptive dialog component
*
* @element ha-adaptive-dialog
* @extends {LitElement}
*
* @summary
* A responsive dialog component that automatically switches between a full dialog (ha-wa-dialog)
* and a bottom sheet (ha-bottom-sheet) based on screen size. Uses dialog mode on larger screens
* (>870px width and >500px height) and bottom sheet mode on smaller screens or mobile devices.
*
* @slot headerNavigationIcon - Leading header action (e.g. close/back button).
* @slot headerTitle - Custom title content (used when header-title is not set).
* @slot headerSubtitle - Custom subtitle content (used when header-subtitle is not set).
* @slot headerActionItems - Trailing header actions (e.g. buttons, menus).
* @slot - Dialog/sheet content body.
* @slot footer - Dialog/sheet footer content.
*
* @cssprop --ha-dialog-surface-background - Dialog/sheet background color.
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface (dialog mode only).
* @cssprop --ha-dialog-show-duration - Show animation duration (dialog mode only).
* @cssprop --ha-dialog-hide-duration - Hide animation duration (dialog mode only).
*
* @attr {boolean} open - Controls the dialog/sheet open state.
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset (dialog mode only). Defaults to "medium".
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used.
* @attr {string} header-subtitle - Header subtitle text. If not set, the headerSubtitle slot is used.
* @attr {("above"|"below")} header-subtitle-position - Position of the subtitle relative to the title. Defaults to "below".
* @attr {boolean} block-mode-change - When set, the mode is determined at mount time based on the current screen size, but subsequent mode changes are blocked. Useful for preventing forms from resetting when the viewport size changes.
*
* @event opened - Fired when the dialog/sheet is shown (dialog mode only).
* @event closed - Fired after the dialog/sheet is hidden.
* @event after-show - Fired after show animation completes (dialog mode only).
*
* @remarks
* **Responsive Behavior:**
* The component automatically switches between dialog and bottom sheet modes based on viewport size.
* Dialog mode is used for screens wider than 870px and taller than 500px.
* Bottom sheet mode is used for mobile devices and smaller screens.
*
* When `block-mode-change` is set, the mode is determined once at mount time based on the initial
* screen size. Subsequent viewport size changes will not trigger mode switches, which is useful
* for preventing form resets or other state loss when users resize their browser window.
*
* **Focus Management:**
* To automatically focus an element when opened, add the `autofocus` attribute to it.
* Components with `delegatesFocus: true` (like `ha-form`) will forward focus to their first focusable child.
* Example: `<ha-form autofocus .schema=${schema}></ha-form>`
*/
@customElement("ha-adaptive-dialog")
export class HaAdaptiveDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "aria-labelledby" })
public ariaLabelledBy?: string;
@property({ attribute: "aria-describedby" })
public ariaDescribedBy?: string;
@property({ type: Boolean, reflect: true })
public open = false;
@property({ type: String, reflect: true, attribute: "width" })
public width: DialogWidth = "medium";
@property({ attribute: "header-title" })
public headerTitle?: string;
@property({ attribute: "header-subtitle" })
public headerSubtitle?: string;
@property({ type: String, attribute: "header-subtitle-position" })
public headerSubtitlePosition: "above" | "below" = "below";
@property({ type: Boolean, attribute: "block-mode-change" })
public blockModeChange = false;
@state() private _mode: DialogSheetMode = "dialog";
private _unsubMediaQuery?: () => void;
private _modeSet = false;
connectedCallback() {
super.connectedCallback();
this._unsubMediaQuery = listenMediaQuery(
"(max-width: 870px), (max-height: 500px)",
(matches) => {
if (!this._modeSet || !this.blockModeChange) {
this._mode = matches ? "bottom-sheet" : "dialog";
this._modeSet = true;
}
}
);
}
disconnectedCallback() {
super.disconnectedCallback();
this._unsubMediaQuery?.();
this._unsubMediaQuery = undefined;
this._modeSet = false;
}
render() {
if (this._mode === "bottom-sheet") {
return html`
<ha-bottom-sheet .open=${this.open} flexcontent>
<ha-dialog-header
slot="header"
.subtitlePosition=${this.headerSubtitlePosition}
>
<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-drawer="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button>
</slot>
${this.headerTitle !== undefined
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
${this.headerTitle}
</span>`
: html`<slot name="headerTitle" slot="title"></slot>`}
${this.headerSubtitle !== undefined
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
<slot name="headerActionItems" slot="actionItems"></slot>
</ha-dialog-header>
<slot></slot>
<slot name="footer" slot="footer"></slot>
</ha-bottom-sheet>
`;
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this.open}
.width=${this.width}
.ariaLabelledBy=${this.ariaLabelledBy}
.ariaDescribedBy=${this.ariaDescribedBy}
.headerTitle=${this.headerTitle}
.headerSubtitle=${this.headerSubtitle}
.headerSubtitlePosition=${this.headerSubtitlePosition}
flexcontent
>
<slot name="headerNavigationIcon" slot="headerNavigationIcon"></slot>
<slot name="headerTitle" slot="headerTitle"></slot>
<slot name="headerSubtitle" slot="headerSubtitle"></slot>
<slot name="headerActionItems" slot="headerActionItems"></slot>
<slot></slot>
<slot name="footer" slot="footer"></slot>
</ha-wa-dialog>
`;
}
static get styles() {
return [
css`
ha-bottom-sheet {
--ha-bottom-sheet-surface-background: var(
--ha-dialog-surface-background,
var(--card-background-color, var(--ha-color-surface-default))
);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-adaptive-dialog": HaAdaptiveDialog;
}
}

View File

@@ -4,7 +4,6 @@ import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { getAreaContext } from "../common/entity/context/get_area_context";
import { areaCompare } from "../data/area_registry";
import type { HomeAssistant } from "../types";
import "./ha-expansion-panel";
import "./ha-items-display-editor";
@@ -37,11 +36,7 @@ export class HaAreasDisplayEditor extends LitElement {
public showNavigationButton = false;
protected render(): TemplateResult {
const compare = areaCompare(this.hass.areas);
const areas = Object.values(this.hass.areas).sort((areaA, areaB) =>
compare(areaA.area_id, areaB.area_id)
);
const areas = Object.values(this.hass.areas);
const items: DisplayItem[] = areas.map((area) => {
const { floor } = getAreaContext(area, this.hass.floors);

View File

@@ -7,7 +7,6 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeFloorName } from "../common/entity/compute_floor_name";
import { getAreaContext } from "../common/entity/context/get_area_context";
import { areaCompare } from "../data/area_registry";
import type { FloorRegistryEntry } from "../data/floor_registry";
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
import type { HomeAssistant } from "../types";
@@ -131,11 +130,8 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
// update items if floors change
_hassFloors: HomeAssistant["floors"]
): Record<string, DisplayItem[]> => {
const compare = areaCompare(hassAreas);
const areas = Object.values(hassAreas);
const areas = Object.values(hassAreas).sort((areaA, areaB) =>
compare(areaA.area_id, areaB.area_id)
);
const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
(acc, area) => {
const { floor } = getAreaContext(area, this.hass.floors);

View File

@@ -659,6 +659,7 @@ export class HaAssistChat extends LitElement {
--markdown-table-border-color: var(--divider-color);
--markdown-code-background-color: var(--primary-background-color);
--markdown-code-text-color: var(--primary-text-color);
--markdown-list-indent: 1rem;
&:not(:has(ha-markdown-element)) {
min-height: 1lh;
min-width: 1lh;

View File

@@ -2,12 +2,13 @@ import "@home-assistant/webawesome/dist/components/drawer/drawer";
import { css, html, LitElement, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { haStyleScrollbar } from "../resources/styles";
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
@customElement("ha-bottom-sheet")
export class HaBottomSheet extends LitElement {
export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
@property({ type: Boolean }) public open = false;
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
@@ -17,11 +18,18 @@ export class HaBottomSheet extends LitElement {
@query("#drawer") private _drawer!: HTMLElement;
@query("#body") private _bodyElement!: HTMLDivElement;
protected get scrollableElement(): HTMLElement | null {
return this._bodyElement;
}
private _gestureRecognizer = new SwipeGestureRecognizer();
private _isDragging = false;
private _handleAfterHide() {
private _handleAfterHide(afterHideEvent: Event) {
afterHideEvent.stopPropagation();
this.open = false;
const ev = new Event("closed", {
bubbles: true,
@@ -48,9 +56,13 @@ export class HaBottomSheet extends LitElement {
@touchstart=${this._handleTouchStart}
>
<slot name="header"></slot>
<div id="body" class="body ha-scrollbar">
<slot></slot>
<div class="content-wrapper">
<div id="body" class="body ha-scrollbar">
<slot></slot>
</div>
${this.renderScrollableFades()}
</div>
<slot name="footer"></slot>
</wa-drawer>
`;
}
@@ -166,60 +178,87 @@ export class HaBottomSheet extends LitElement {
this._isDragging = false;
}
static styles = [
haStyleScrollbar,
css`
wa-drawer {
--wa-color-surface-raised: transparent;
--spacing: 0;
--size: var(--ha-bottom-sheet-height, auto);
--show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
--hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
}
wa-drawer::part(dialog) {
max-height: var(--ha-bottom-sheet-max-height, 90vh);
align-items: center;
transform: var(--dialog-transform);
transition: var(--dialog-transition);
}
wa-drawer::part(body) {
max-width: var(--ha-bottom-sheet-max-width);
width: 100%;
border-top-left-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
border-top-right-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
background-color: var(
--ha-bottom-sheet-surface-background,
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
);
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
}
:host([flexcontent]) wa-drawer::part(body) {
display: flex;
flex-direction: column;
}
:host([flexcontent]) .body {
flex: 1;
max-width: 100%;
display: flex;
flex-direction: column;
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
}
`,
];
static get styles() {
return [
...super.styles,
haStyleScrollbar,
css`
wa-drawer {
--wa-color-surface-raised: transparent;
--spacing: 0;
--size: var(--ha-bottom-sheet-height, auto);
--show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
--hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
}
wa-drawer::part(dialog) {
max-height: var(--ha-bottom-sheet-max-height, 90vh);
align-items: center;
transform: var(--dialog-transform);
transition: var(--dialog-transition);
}
wa-drawer::part(body) {
max-width: var(--ha-bottom-sheet-max-width);
width: 100%;
border-top-left-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
border-top-right-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
);
background-color: var(
--ha-bottom-sheet-surface-background,
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
);
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
}
:host([flexcontent]) wa-drawer::part(body) {
display: flex;
flex-direction: column;
}
.content-wrapper {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
:host([flexcontent]) .body {
flex: 1;
max-width: 100%;
display: flex;
flex-direction: column;
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
}
slot[name="footer"] {
display: block;
padding: var(--ha-space-0);
}
::slotted([slot="footer"]) {
display: flex;
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
var(--ha-space-4);
gap: var(--ha-space-3);
justify-content: flex-end;
align-items: center;
width: 100%;
box-sizing: border-box;
}
:host([flexcontent]) slot[name="footer"] {
flex-shrink: 0;
}
`,
];
}
}
declare global {

View File

@@ -202,6 +202,7 @@ export class HaControlSelect extends LitElement {
color: var(--primary-text-color);
user-select: none;
-webkit-tap-highlight-color: transparent;
border-radius: var(--control-select-border-radius);
}
:host([vertical]) {
width: var(--control-select-thickness);
@@ -211,7 +212,6 @@ export class HaControlSelect extends LitElement {
position: relative;
height: 100%;
width: 100%;
border-radius: var(--control-select-border-radius);
transform: translateZ(0);
display: flex;
flex-direction: row;

View File

@@ -167,30 +167,33 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
}
private async _labelSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
if (!ev.detail.index.size) {
fireEvent(this, "data-table-filter-changed", {
value: [],
items: undefined,
});
this.value = [];
return;
}
const value: string[] = [];
const filteredLabels = this._filteredLabels(
this._labels,
this._filter,
this.value
);
const filteredLabelIds = new Set(filteredLabels.map((l) => l.label_id));
// Keep previously selected labels that are not in the current filtered view
const preservedLabels = (this.value || []).filter(
(id) => !filteredLabelIds.has(id)
);
// Build the new selection from the filtered labels based on selected indices
const newlySelectedLabels: string[] = [];
for (const index of ev.detail.index) {
const labelId = filteredLabels[index].label_id;
value.push(labelId);
const labelId = filteredLabels[index]?.label_id;
if (labelId) {
newlySelectedLabels.push(labelId);
}
}
this.value = value;
const value = [...preservedLabels, ...newlySelectedLabels];
this.value = value.length ? value : [];
fireEvent(this, "data-table-filter-changed", {
value,
value: value.length ? value : undefined,
items: undefined,
});
}

View File

@@ -248,7 +248,7 @@ export class HaGenericPicker extends LitElement {
});
};
private _hidePicker(ev) {
private _hidePicker(ev: Event) {
ev.stopPropagation();
if (this._newValue) {
fireEvent(this, "value-changed", { value: this._newValue });

View File

@@ -32,6 +32,12 @@ export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public step = 1;
@property({ type: Boolean, attribute: "rows-disabled" })
public rowsDisabled?: boolean;
@property({ type: Boolean, attribute: "columns-disabled" })
public columnsDisabled?: boolean;
@state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
protected willUpdate(changedProperties) {
@@ -42,9 +48,11 @@ export class HaGridSizeEditor extends LitElement {
protected render() {
const disabledColumns =
this.columnMin !== undefined && this.columnMin === this.columnMax;
this.columnsDisabled ||
(this.columnMin !== undefined && this.columnMin === this.columnMax);
const disabledRows =
this.rowMin !== undefined && this.rowMin === this.rowMax;
this.rowsDisabled ||
(this.rowMin !== undefined && this.rowMin === this.rowMax);
const autoHeight = this._localValue?.rows === "auto";
const fullWidth = this._localValue?.columns === "full";
@@ -72,7 +80,7 @@ export class HaGridSizeEditor extends LitElement {
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
.disabled=${disabledColumns}
tooltip-mode="always"
tooltip-mode=${disabledColumns ? "never" : "always"}
></ha-grid-layout-slider>
<ha-grid-layout-slider
@@ -88,7 +96,7 @@ export class HaGridSizeEditor extends LitElement {
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
.disabled=${disabledRows}
tooltip-mode="always"
tooltip-mode=${disabledRows ? "never" : "always"}
></ha-grid-layout-slider>
${!this.isDefault
? html`

View File

@@ -73,6 +73,8 @@ export class HaLanguagePicker extends LitElement {
@property({ type: Boolean }) public required = false;
@property() public helper?: string;
@property({ attribute: "native-name", type: Boolean })
public nativeName = false;
@@ -135,6 +137,7 @@ export class HaLanguagePicker extends LitElement {
.value=${value}
.valueRenderer=${this._valueRenderer}
.disabled=${this.disabled}
.helper=${this.helper}
.getItems=${this._getItems}
@value-changed=${this._changed}
hide-clear-icon

View File

@@ -71,7 +71,7 @@ class HaMarkdownElement extends ReactiveElement {
if (!this.innerHTML && this.cache) {
const key = this._computeCacheKey();
if (markdownCache.has(key)) {
render(markdownCache.get(key)!, this.renderRoot);
render(h(unsafeHTML(markdownCache.get(key))), this.renderRoot);
this._resize();
}
}
@@ -99,10 +99,7 @@ class HaMarkdownElement extends ReactiveElement {
}
);
render(
elements.map((e) => h(unsafeHTML(e))),
this.renderRoot
);
render(h(unsafeHTML(elements.join(""))), this.renderRoot);
this._resize();

View File

@@ -25,11 +25,11 @@ export class HaMarkdown extends LitElement {
@property({ type: Boolean }) public cache = false;
@query("ha-markdown-element") private _markdownElement!: ReactiveElement;
@query("ha-markdown-element") private _markdownElement?: ReactiveElement;
protected async getUpdateComplete() {
const result = await super.getUpdateComplete();
await this._markdownElement.updateComplete;
await this._markdownElement?.updateComplete;
return result;
}
@@ -71,13 +71,11 @@ export class HaMarkdown extends LitElement {
color: var(--markdown-link-color, var(--primary-color));
}
img {
background-color: rgba(10, 10, 10, 0.15);
background-color: var(--markdown-image-background-color);
border-radius: var(--markdown-image-border-radius);
max-width: 100%;
min-height: 2lh;
height: auto;
width: auto;
text-indent: 4px;
transition: height 0.2s ease-in-out;
}
p:first-child > img:first-child {
@@ -86,10 +84,9 @@ export class HaMarkdown extends LitElement {
p:first-child > img:last-child {
vertical-align: top;
}
ol,
ul {
list-style-position: inside;
padding-inline-start: 0;
:host > ul,
:host > ol {
padding-inline-start: var(--markdown-list-indent, revert);
}
li {
&:has(input[type="checkbox"]) {
@@ -140,16 +137,19 @@ export class HaMarkdown extends LitElement {
margin: var(--ha-space-4) 0;
}
table {
border-collapse: collapse;
display: block;
overflow-x: auto;
border-collapse: var(--markdown-table-border-collapse, collapse);
}
div:has(> table) {
overflow: auto;
}
th {
text-align: start;
}
td,
th {
border: 1px solid var(--markdown-table-border-color, transparent);
border-width: var(--markdown-table-border-width, 1px);
border-style: var(--markdown-table-border-style, solid);
border-color: var(--markdown-table-border-color, var(--divider-color));
padding: 0.25em 0.5em;
}
blockquote {

View File

@@ -14,6 +14,7 @@ import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { HaFuse } from "../resources/fuse";
import { haStyleScrollbar } from "../resources/styles";
import { loadVirtualizer } from "../resources/virtualizer";
@@ -59,7 +60,7 @@ export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
) => T[];
@customElement("ha-picker-combo-box")
export class HaPickerComboBox extends LitElement {
export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@property({ attribute: false }) public hass?: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@@ -126,6 +127,10 @@ export class HaPickerComboBox extends LitElement {
@state() private _items: (PickerComboBoxItem | string)[] = [];
protected get scrollableElement(): HTMLElement | null {
return this._virtualizerElement as HTMLElement | null;
}
@state() private _sectionTitle?: string;
private _allItems: (PickerComboBoxItem | string)[] = [];
@@ -180,19 +185,22 @@ export class HaPickerComboBox extends LitElement {
</div>
`
: nothing}
<lit-virtualizer
.keyFunction=${this._keyFunction}
tabindex="0"
scroller
.items=${this._items}
.renderItem=${this._renderItem}
style="min-height: 36px;"
class=${this._listScrolled ? "scrolled" : ""}
@scroll=${this._onScrollList}
@focus=${this._focusList}
@visibilityChanged=${this._visibilityChanged}
>
</lit-virtualizer>`;
<div class="virtualizer-wrapper">
<lit-virtualizer
.keyFunction=${this._keyFunction}
tabindex="0"
scroller
.items=${this._items}
.renderItem=${this._renderItem}
style="min-height: 36px;"
class=${this._listScrolled ? "scrolled" : ""}
@scroll=${this._onScrollList}
@focus=${this._focusList}
@visibilityChanged=${this._visibilityChanged}
>
</lit-virtualizer>
${this.renderScrollableFades()}
</div>`;
}
private _renderSectionButtons() {
@@ -584,156 +592,167 @@ export class HaPickerComboBox extends LitElement {
private _keyFunction = (item: PickerComboBoxItem | string) =>
typeof item === "string" ? item : item.id;
static styles = [
haStyleScrollbar,
css`
:host {
display: flex;
flex-direction: column;
padding-top: var(--ha-space-3);
flex: 1;
}
static get styles() {
return [
...super.styles,
haStyleScrollbar,
css`
:host {
display: flex;
flex-direction: column;
padding-top: var(--ha-space-3);
flex: 1;
}
ha-textfield {
padding: 0 var(--ha-space-3);
margin-bottom: var(--ha-space-3);
}
ha-textfield {
padding: 0 var(--ha-space-3);
margin-bottom: var(--ha-space-3);
}
:host([mode="dialog"]) ha-textfield {
padding: 0 var(--ha-space-4);
}
:host([mode="dialog"]) ha-textfield {
padding: 0 var(--ha-space-4);
}
ha-combo-box-item {
width: 100%;
}
ha-combo-box-item {
width: 100%;
}
ha-combo-box-item.selected {
background-color: var(--ha-color-fill-neutral-quiet-hover);
}
@media (prefers-color-scheme: dark) {
ha-combo-box-item.selected {
background-color: var(--ha-color-fill-neutral-normal-hover);
background-color: var(--ha-color-fill-neutral-quiet-hover);
}
}
lit-virtualizer {
flex: 1;
}
@media (prefers-color-scheme: dark) {
ha-combo-box-item.selected {
background-color: var(--ha-color-fill-neutral-normal-hover);
}
}
lit-virtualizer:focus-visible {
outline: none;
}
.virtualizer-wrapper {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
lit-virtualizer.scrolled {
border-top: 1px solid var(--ha-color-border-neutral-quiet);
}
lit-virtualizer {
flex: 1;
}
.bottom-padding {
height: max(var(--safe-area-inset-bottom, 0px), var(--ha-space-8));
width: 100%;
}
lit-virtualizer:focus-visible {
outline: none;
}
.empty {
text-align: center;
}
lit-virtualizer.scrolled {
border-top: 1px solid var(--ha-color-border-neutral-quiet);
}
.combo-box-row {
display: flex;
width: 100%;
align-items: center;
box-sizing: border-box;
min-height: 36px;
}
.combo-box-row.current-value {
background-color: var(--ha-color-fill-primary-quiet-resting);
}
.bottom-padding {
height: max(var(--safe-area-inset-bottom, 0px), var(--ha-space-8));
width: 100%;
}
.combo-box-row.selected {
background-color: var(--ha-color-fill-neutral-quiet-hover);
}
.empty {
text-align: center;
}
.combo-box-row {
display: flex;
width: 100%;
align-items: center;
box-sizing: border-box;
min-height: 36px;
}
.combo-box-row.current-value {
background-color: var(--ha-color-fill-primary-quiet-resting);
}
@media (prefers-color-scheme: dark) {
.combo-box-row.selected {
background-color: var(--ha-color-fill-neutral-normal-hover);
background-color: var(--ha-color-fill-neutral-quiet-hover);
}
}
.sections {
display: flex;
flex-wrap: nowrap;
gap: var(--ha-space-2);
padding: var(--ha-space-3) var(--ha-space-3);
overflow: auto;
}
@media (prefers-color-scheme: dark) {
.combo-box-row.selected {
background-color: var(--ha-color-fill-neutral-normal-hover);
}
}
:host([mode="dialog"]) .sections {
padding: var(--ha-space-3) var(--ha-space-4);
}
.sections {
display: flex;
flex-wrap: nowrap;
gap: var(--ha-space-2);
padding: var(--ha-space-3) var(--ha-space-3);
overflow: auto;
}
.sections ha-filter-chip {
flex-shrink: 0;
--md-filter-chip-selected-container-color: var(
--ha-color-fill-primary-normal-hover
);
color: var(--primary-color);
}
:host([mode="dialog"]) .sections {
padding: var(--ha-space-3) var(--ha-space-4);
}
.sections .separator {
height: var(--ha-space-8);
width: 0;
border: 1px solid var(--ha-color-border-neutral-quiet);
}
.sections ha-filter-chip {
flex-shrink: 0;
--md-filter-chip-selected-container-color: var(
--ha-color-fill-primary-normal-hover
);
color: var(--primary-color);
}
.section-title,
.title {
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
display: flex;
align-items: center;
}
.sections .separator {
height: var(--ha-space-8);
width: 0;
border: 1px solid var(--ha-color-border-neutral-quiet);
}
.title {
width: 100%;
}
.section-title,
.title {
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
display: flex;
align-items: center;
}
:host([mode="dialog"]) .title {
padding: var(--ha-space-1) var(--ha-space-4);
}
.title {
width: 100%;
}
:host([mode="dialog"]) ha-textfield {
padding: 0 var(--ha-space-4);
}
:host([mode="dialog"]) .title {
padding: var(--ha-space-1) var(--ha-space-4);
}
.section-title-wrapper {
height: 0;
position: relative;
}
:host([mode="dialog"]) ha-textfield {
padding: 0 var(--ha-space-4);
}
.section-title {
opacity: 0;
position: absolute;
top: 1px;
width: calc(100% - var(--ha-space-8));
}
.section-title-wrapper {
height: 0;
position: relative;
}
.section-title.show {
opacity: 1;
z-index: 1;
}
.section-title {
opacity: 0;
position: absolute;
top: 1px;
width: calc(100% - var(--ha-space-8));
}
.empty-search {
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
padding: var(--ha-space-3);
}
`,
];
.section-title.show {
opacity: 1;
z-index: 1;
}
.empty-search {
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
padding: var(--ha-space-3);
}
`,
];
}
}
declare global {

View File

@@ -103,8 +103,8 @@ export class HaPickerField extends LitElement {
--md-list-item-two-line-container-height: 56px;
--md-list-item-top-space: 0px;
--md-list-item-bottom-space: 0px;
--md-list-item-leading-space: 8px;
--md-list-item-trailing-space: 8px;
--md-list-item-leading-space: var(--ha-space-4);
--md-list-item-trailing-space: var(--ha-space-2);
--ha-md-list-item-gap: var(--ha-space-2);
/* Remove the default focus ring */
--md-focus-ring-width: 0px;

View File

@@ -9,13 +9,13 @@ import { customElement, property, query, state } from "lit/decorators";
import { prepareZXingModule } from "barcode-detector";
import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { addExternalBarCodeListener } from "../external_app/external_app_entrypoint";
import type { HomeAssistant } from "../types";
import "./ha-alert";
import "./ha-button";
import "./ha-button-menu";
import "./ha-list-item";
import "./ha-dropdown";
import "./ha-dropdown-item";
import type { HaDropdownItem } from "./ha-dropdown-item";
import "./ha-spinner";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@@ -52,6 +52,8 @@ class HaQrScanner extends LitElement {
@state() private _warning?: string;
@state() private _selectedCamera?: string;
private _qrScanner?: QrScanner;
private _qrNotFoundCount = 0;
@@ -121,7 +123,7 @@ class HaQrScanner extends LitElement {
!this._error &&
this._cameras &&
this._cameras.length > 1
? html`<ha-button-menu fixed @closed=${stopPropagation}>
? html`<ha-dropdown @wa-select=${this._handleDropdownSelect}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize(
@@ -131,15 +133,17 @@ class HaQrScanner extends LitElement {
></ha-icon-button>
${this._cameras!.map(
(camera) => html`
<ha-list-item
<ha-dropdown-item
.value=${camera.id}
@click=${this._cameraChanged}
class=${this._selectedCamera === camera.id
? "selected"
: ""}
>
${camera.label}
</ha-list-item>
</ha-dropdown-item>
`
)}
</ha-button-menu>`
</ha-dropdown>`
: nothing}
</div>`
: html`<ha-alert alert-type="warning">
@@ -205,6 +209,9 @@ class HaQrScanner extends LitElement {
private async _listCameras(qrScanner: typeof QrScanner): Promise<void> {
this._cameras = await qrScanner.listCameras(true);
if (this._cameras.length > 0) {
this._selectedCamera = this._cameras[0].id;
}
}
private _qrCodeError = (err: any) => {
@@ -252,8 +259,12 @@ class HaQrScanner extends LitElement {
this._qrCodeScanned(this._manualInput!.value);
}
private _cameraChanged(ev: CustomEvent): void {
this._qrScanner?.setCamera((ev.target as any).value);
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
const cameraId = ev.detail?.item?.value;
if (cameraId) {
this._selectedCamera = cameraId;
this._qrScanner?.setCamera(cameraId);
}
}
private _openExternalScanner() {
@@ -359,7 +370,7 @@ class HaQrScanner extends LitElement {
#canvas-container {
position: relative;
}
ha-button-menu {
ha-icon-button {
position: absolute;
bottom: 8px;
right: 8px;
@@ -369,6 +380,9 @@ class HaQrScanner extends LitElement {
color: white;
border-radius: var(--ha-border-radius-circle);
}
ha-dropdown-item.selected {
font-weight: var(--ha-font-weight-bold);
}
.row {
display: flex;
align-items: center;

View File

@@ -450,7 +450,7 @@ export class HaServiceControl extends LitElement {
const hasOptional = Boolean(
!shouldRenderServiceDataYaml &&
serviceData?.flatFields.some((field) => showOptionalToggle(field))
serviceData?.flatFields.some((field) => showOptionalToggle(field))
);
const targetEntities = this._getTargetedEntities(
@@ -467,7 +467,7 @@ export class HaServiceControl extends LitElement {
const descriptionPlaceholders =
domain && serviceName
? this.hass.services[domain][serviceName].description_placeholders
? this.hass.services[domain]?.[serviceName]?.description_placeholders
: undefined;
const description =

View File

@@ -197,6 +197,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _mouseLeaveTimeout?: number;
private _touchendTimeout?: number;
private _tooltipHideTimeout?: number;
private _recentKeydownActiveUntil = 0;
@@ -237,6 +239,18 @@ class HaSidebar extends SubscribeMixin(LitElement) {
];
}
public disconnectedCallback() {
super.disconnectedCallback();
// clear timeouts
clearTimeout(this._mouseLeaveTimeout);
clearTimeout(this._tooltipHideTimeout);
clearTimeout(this._touchendTimeout);
// set undefined values
this._mouseLeaveTimeout = undefined;
this._tooltipHideTimeout = undefined;
this._touchendTimeout = undefined;
}
protected render() {
if (!this.hass) {
return nothing;
@@ -406,6 +420,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
class="ha-scrollbar"
@focusin=${this._listboxFocusIn}
@focusout=${this._listboxFocusOut}
@touchend=${this._listboxTouchend}
@scroll=${this._listboxScroll}
@keydown=${this._listboxKeydown}
>
@@ -620,6 +635,14 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._hideTooltip();
}
private _listboxTouchend() {
clearTimeout(this._touchendTimeout);
this._touchendTimeout = window.setTimeout(() => {
// Allow 1 second for users to read the tooltip on touch devices
this._hideTooltip();
}, 1000);
}
@eventOptions({
passive: true,
})

View File

@@ -1,7 +1,7 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { HomeAssistant } from "../types";
import { subscribeLabFeatures } from "../data/labs";
import { subscribeLabFeature } from "../data/labs";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
interface Snowflake {
@@ -10,7 +10,7 @@ interface Snowflake {
size: number;
duration: number;
delay: number;
blur: number;
rotation: number;
}
@customElement("ha-snowflakes")
@@ -27,13 +27,14 @@ export class HaSnowflakes extends SubscribeMixin(LitElement) {
public hassSubscribe() {
return [
subscribeLabFeatures(this.hass!.connection, (features) => {
this._enabled =
features.find(
(f) =>
f.domain === "frontend" && f.preview_feature === "winter_mode"
)?.enabled ?? false;
}),
subscribeLabFeature(
this.hass!.connection,
"frontend",
"winter_mode",
(feature) => {
this._enabled = feature.enabled;
}
),
];
}
@@ -51,7 +52,7 @@ export class HaSnowflakes extends SubscribeMixin(LitElement) {
size: Math.random() * 12 + 8, // Random size between 8-20px
duration: Math.random() * 8 + 8, // Random duration between 8-16s
delay: Math.random() * 8, // Random delay between 0-8s
blur: Math.random() * 1, // Random blur between 0-1px
rotation: Math.random() * 720 - 360, // Random starting rotation -360 to 360deg
});
}
this._snowflakes = snowflakes;
@@ -75,20 +76,27 @@ export class HaSnowflakes extends SubscribeMixin(LitElement) {
<div class="snowflakes ${isDark ? "dark" : "light"}" aria-hidden="true">
${this._snowflakes.map(
(flake) => html`
<div
<svg
class="snowflake ${this.narrow && flake.id >= 30
? "hide-narrow"
: ""}"
style="
left: ${flake.left}%;
font-size: ${flake.size}px;
width: ${flake.size}px;
height: ${flake.size}px;
animation-duration: ${flake.duration}s;
animation-delay: ${flake.delay}s;
filter: blur(${flake.blur}px);
--rotation: ${flake.rotation}deg;
"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
</div>
<path
d="M7.991 0a.644.644 0 0 1 .283 1.221v2.553l.986-.988a.645.645 0 0 1 .612-.839.644.644 0 1 1-.222 1.247l-1.376 1.38V7.52l1.65-.954.466-1.879a.645.645 0 0 1 .1-1.042.643.643 0 1 1 .445 1.189l-.363 1.356 3.145-1.82a.643.643 0 1 1 .282.49l-2.205 1.277 1.347.361a.643.643 0 1 1-.158.543l-1.88-.505L8.573 8l1.632.945 1.858-.535a.64.64 0 0 1 .95-.434.643.643 0 1 1-.805.98l-1.354.364L14 11.14a.641.641 0 0 1 .914.855.643.643 0 0 1-1.197-.366l-2.205-1.276.36 1.35a.642.642 0 0 1 .419.95.643.643 0 1 1-.967-.816l-.503-1.884L8.273 8.48v1.909l1.39 1.344a.644.644 0 1 1 .208 1.252.644.644 0 0 1-.606-.852l-.991-.994v3.64A.644.644 0 0 1 7.99 16a.644.644 0 0 1-.282-1.221v-2.553l-.986.988a.645.645 0 0 1-.612.839.644.644 0 1 1 .222-1.247l1.376-1.38V8.5l-1.632.945-.467 1.879q.079.068.134.163a.643.643 0 1 1-.68-.31l.364-1.357-3.145 1.82A.643.643 0 1 1 2 11.15l2.205-1.276-1.347-.361a.643.643 0 1 1 .158-.543l1.88.505L7.444 8l-1.65-.954-1.857.534a.64.64 0 0 1-.95.434.643.643 0 1 1 .805-.98l1.354-.364L2 4.85a.641.641 0 0 1-.914-.855.643.643 0 0 1 1.197.366l2.205 1.276-.36-1.35a.642.642 0 0 1-.419-.95.643.643 0 1 1 .967.816l.503 1.884L7.71 7.5V5.611L6.32 4.267a.644.644 0 1 1-.208-1.252.644.644 0 0 1 .607.852l.991.994V1.22A.644.644 0 0 1 7.991 0"
fill="currentColor"
/>
</svg>
`
)}
</div>
@@ -128,16 +136,10 @@ export class HaSnowflakes extends SubscribeMixin(LitElement) {
.light .snowflake {
color: #00bcd4;
text-shadow:
0 0 5px #00bcd4,
0 0 10px #00e5ff;
}
.dark .snowflake {
color: #fff;
text-shadow:
0 0 5px rgba(255, 255, 255, 0.8),
0 0 10px rgba(255, 255, 255, 0.5);
}
.snowflake.hide-narrow {
@@ -146,19 +148,23 @@ export class HaSnowflakes extends SubscribeMixin(LitElement) {
@keyframes fall {
0% {
transform: translateY(-10vh) translateX(0);
transform: translateY(-10vh) translateX(0) rotate(var(--rotation));
}
25% {
transform: translateY(30vh) translateX(10px);
transform: translateY(30vh) translateX(10px)
rotate(calc(var(--rotation) + 25deg));
}
50% {
transform: translateY(60vh) translateX(-10px);
transform: translateY(60vh) translateX(-10px)
rotate(calc(var(--rotation) + 50deg));
}
75% {
transform: translateY(85vh) translateX(10px);
transform: translateY(85vh) translateX(10px)
rotate(calc(var(--rotation) + 75deg));
}
100% {
transform: translateY(120vh) translateX(0);
transform: translateY(120vh) translateX(0)
rotate(calc(var(--rotation) + 100deg));
}
}

View File

@@ -6,7 +6,6 @@ import {
mdiDevices,
mdiFormatListBulleted,
mdiGestureDoubleTap,
mdiHomeAssistant,
mdiMapMarker,
mdiMapMarkerRadius,
mdiMessageAlert,
@@ -23,6 +22,7 @@ import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { computeDomain } from "../common/entity/compute_domain";
import { FALLBACK_DOMAIN_ICONS, triggerIcon } from "../data/icons";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";

View File

@@ -1,3 +1,5 @@
import "@home-assistant/webawesome/dist/components/dialog/dialog";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement } from "lit";
import {
customElement,
@@ -7,9 +9,8 @@ import {
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { mdiClose } from "@mdi/js";
import "@home-assistant/webawesome/dist/components/dialog/dialog";
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 "./ha-dialog-header";
@@ -49,10 +50,10 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
* @cssprop --ha-dialog-surface-background - Dialog background color.
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface.
* @cssprop --dialog-z-index - Z-index for the dialog.
* @cssprop --dialog-surface-position - CSS position of the dialog surface.
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
*
* @attr {boolean} open - Controls the dialog open state.
* @attr {("alert"|"standard")} type - Dialog type. Defaults to "standard".
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset. Defaults to "medium".
* @attr {boolean} prevent-scrim-close - Prevents closing the dialog by clicking the scrim/overlay. Defaults to false.
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used.
@@ -72,7 +73,7 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
* @see https://github.com/home-assistant/frontend/issues/27143
*/
@customElement("ha-wa-dialog")
export class HaWaDialog extends LitElement {
export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "aria-labelledby" })
@@ -84,6 +85,9 @@ export class HaWaDialog extends LitElement {
@property({ type: Boolean, reflect: true })
public open = false;
@property({ reflect: true })
public type: "alert" | "standard" = "standard";
@property({ type: String, reflect: true, attribute: "width" })
public width: DialogWidth = "medium";
@@ -110,6 +114,10 @@ export class HaWaDialog extends LitElement {
@state()
private _bodyScrolled = false;
protected get scrollableElement(): HTMLElement | null {
return this.bodyContainer;
}
protected updated(
changedProperties: Map<string | number | symbol, unknown>
): void {
@@ -158,8 +166,11 @@ export class HaWaDialog extends LitElement {
<slot name="headerActionItems" slot="actionItems"></slot>
</ha-dialog-header>
</slot>
<div class="body ha-scrollbar" @scroll=${this._handleBodyScroll}>
<slot></slot>
<div class="content-wrapper">
<div class="body ha-scrollbar" @scroll=${this._handleBodyScroll}>
<slot></slot>
</div>
${this.renderScrollableFades()}
</div>
<slot name="footer" slot="footer"></slot>
</wa-dialog>
@@ -172,7 +183,9 @@ export class HaWaDialog extends LitElement {
await this.updateComplete;
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
requestAnimationFrame(() => {
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
});
};
private _handleAfterShow = () => {
@@ -194,155 +207,180 @@ export class HaWaDialog extends LitElement {
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
}
static styles = [
haStyleScrollbar,
css`
wa-dialog {
--full-width: var(
--ha-dialog-width-full,
min(
95vw,
calc(
100vw - var(--safe-area-inset-left, var(--ha-space-0)) - var(
--safe-area-inset-right,
var(--ha-space-0)
)
)
)
);
--width: min(var(--ha-dialog-width-md, 580px), var(--full-width));
--spacing: var(--dialog-content-padding, var(--ha-space-6));
--show-duration: var(--ha-dialog-show-duration, 200ms);
--hide-duration: var(--ha-dialog-hide-duration, 200ms);
--ha-dialog-surface-background: var(
--card-background-color,
var(--ha-color-surface-default)
);
--wa-color-surface-raised: var(
--ha-dialog-surface-background,
var(--card-background-color, var(--ha-color-surface-default))
);
--wa-panel-border-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-3xl)
);
max-width: var(--ha-dialog-max-width, 100vw);
max-width: var(--ha-dialog-max-width, 100svw);
}
:host([width="small"]) wa-dialog {
--width: min(var(--ha-dialog-width-sm, 320px), var(--full-width));
}
:host([width="large"]) wa-dialog {
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
}
:host([width="full"]) wa-dialog {
--width: var(--full-width);
}
wa-dialog::part(dialog) {
min-width: var(--width, var(--full-width));
max-width: var(--width, var(--full-width));
max-height: var(
--ha-dialog-max-height,
calc(100% - var(--ha-space-20))
);
min-height: var(--ha-dialog-min-height);
position: var(--dialog-surface-position, relative);
margin-top: var(--dialog-surface-margin-top, auto);
display: flex;
flex-direction: column;
overflow: hidden;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
:host {
--ha-dialog-border-radius: var(--ha-space-0);
static get styles() {
return [
...super.styles,
haStyleScrollbar,
css`
wa-dialog {
--full-width: var(
--ha-dialog-width-full,
min(95vw, var(--safe-width))
);
--width: min(var(--ha-dialog-width-md, 580px), var(--full-width));
--spacing: var(--dialog-content-padding, var(--ha-space-6));
--show-duration: var(--ha-dialog-show-duration, 200ms);
--hide-duration: var(--ha-dialog-hide-duration, 200ms);
--ha-dialog-surface-background: var(
--card-background-color,
var(--ha-color-surface-default)
);
--wa-color-surface-raised: var(
--ha-dialog-surface-background,
var(--card-background-color, var(--ha-color-surface-default))
);
--wa-panel-border-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-3xl)
);
max-width: var(--ha-dialog-max-width, var(--safe-width));
}
wa-dialog {
--full-width: var(--ha-dialog-width-full, 100vw);
:host([width="small"]) wa-dialog {
--width: min(var(--ha-dialog-width-sm, 320px), var(--full-width));
}
:host([width="large"]) wa-dialog {
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
}
:host([width="full"]) wa-dialog {
--width: var(--full-width);
}
wa-dialog::part(dialog) {
min-height: var(--ha-dialog-min-height, 100vh);
min-height: var(--ha-dialog-min-height, 100svh);
max-height: var(--ha-dialog-max-height, 100vh);
max-height: var(--ha-dialog-max-height, 100svh);
padding-top: var(--safe-area-inset-top, var(--ha-space-0));
padding-bottom: var(--safe-area-inset-bottom, var(--ha-space-0));
padding-left: var(--safe-area-inset-left, var(--ha-space-0));
padding-right: var(--safe-area-inset-right, var(--ha-space-0));
min-width: var(--width, var(--full-width));
max-width: var(--width, var(--full-width));
max-height: var(
--ha-dialog-max-height,
calc(var(--safe-height) - var(--ha-space-20))
);
min-height: var(--ha-dialog-min-height);
margin-top: var(--dialog-surface-margin-top, auto);
/* Used to offset the dialog from the safe areas when space is limited */
transform: translate(
calc(
var(--safe-area-offset-left, var(--ha-space-0)) - var(
--safe-area-offset-right,
var(--ha-space-0)
)
),
calc(
var(--safe-area-offset-top, var(--ha-space-0)) - var(
--safe-area-offset-bottom,
var(--ha-space-0)
)
)
);
display: flex;
flex-direction: column;
overflow: hidden;
}
}
.header-title-container {
display: flex;
align-items: center;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
:host([type="standard"]) {
--ha-dialog-border-radius: var(--ha-space-0);
.header-title {
margin: 0;
margin-bottom: 0;
color: var(--ha-dialog-header-title-color, var(--primary-text-color));
font-size: var(
--ha-dialog-header-title-font-size,
var(--ha-font-size-2xl)
);
line-height: var(
--ha-dialog-header-title-line-height,
var(--ha-line-height-condensed)
);
font-weight: var(
--ha-dialog-header-title-font-weight,
var(--ha-font-weight-normal)
);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: var(--ha-space-3);
}
wa-dialog {
/* Make the container fill the whole screen width and not the safe width */
--full-width: var(--ha-dialog-width-full, 100vw);
--width: var(--full-width);
}
wa-dialog::part(body) {
padding: 0;
display: flex;
flex-direction: column;
max-width: 100%;
overflow: hidden;
}
wa-dialog::part(dialog) {
/* Make the dialog fill the whole screen height and not the safe height */
min-height: var(--ha-dialog-min-height, 100vh);
min-height: var(--ha-dialog-min-height, 100dvh);
max-height: var(--ha-dialog-max-height, 100vh);
max-height: var(--ha-dialog-max-height, 100dvh);
margin-top: 0;
margin-bottom: 0;
/* Use safe area as padding instead of the container size */
padding-top: var(--safe-area-inset-top);
padding-bottom: var(--safe-area-inset-bottom);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
/* Reset the transform to center the dialog */
transform: none;
}
}
}
.body {
position: var(--dialog-content-position, relative);
padding: 0 var(--dialog-content-padding, var(--ha-space-6))
var(--dialog-content-padding, var(--ha-space-6))
var(--dialog-content-padding, var(--ha-space-6));
overflow: auto;
flex-grow: 1;
}
:host([flexcontent]) .body {
max-width: 100%;
flex: 1;
display: flex;
flex-direction: column;
}
.header-title-container {
display: flex;
align-items: center;
}
wa-dialog::part(footer) {
padding: var(--ha-space-0);
}
.header-title {
margin: 0;
margin-bottom: 0;
color: var(--ha-dialog-header-title-color, var(--primary-text-color));
font-size: var(
--ha-dialog-header-title-font-size,
var(--ha-font-size-2xl)
);
line-height: var(
--ha-dialog-header-title-line-height,
var(--ha-line-height-condensed)
);
font-weight: var(
--ha-dialog-header-title-font-weight,
var(--ha-font-weight-normal)
);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: var(--ha-space-3);
}
::slotted([slot="footer"]) {
display: flex;
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
var(--ha-space-4);
gap: var(--ha-space-3);
justify-content: flex-end;
align-items: center;
width: 100%;
}
`,
];
wa-dialog::part(body) {
padding: 0;
display: flex;
flex-direction: column;
max-width: 100%;
overflow: hidden;
}
.content-wrapper {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.body {
position: var(--dialog-content-position, relative);
padding: var(
--dialog-content-padding,
0 var(--ha-space-6) var(--ha-space-6) var(--ha-space-6)
);
overflow: auto;
flex-grow: 1;
}
:host([flexcontent]) .body {
max-width: 100%;
flex: 1;
display: flex;
flex-direction: column;
}
wa-dialog::part(footer) {
padding: var(--ha-space-0);
}
::slotted([slot="footer"]) {
display: flex;
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
var(--ha-space-4);
gap: var(--ha-space-3);
justify-content: flex-end;
align-items: center;
width: 100%;
}
`,
];
}
}
declare global {

View File

@@ -1,10 +1,11 @@
import { LitElement, html, css } from "lit";
import { property } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import type { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-state-icon";
@customElement("ha-entity-marker")
class HaEntityMarker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -89,8 +90,6 @@ class HaEntityMarker extends LitElement {
`;
}
customElements.define("ha-entity-marker", HaEntityMarker);
declare global {
interface HTMLElementTagNameMap {
"ha-entity-marker": HaEntityMarker;

View File

@@ -223,6 +223,7 @@ const getAreasAndFloorsItems = (
}
let outputAreas = areas;
let outputFloors = floors;
let areaIds: string[] | undefined;
@@ -254,9 +255,29 @@ const getAreasAndFloorsItems = (
outputAreas = outputAreas.filter(
(area) => !area.floor_id || !excludeFloors!.includes(area.floor_id)
);
outputFloors = outputFloors.filter(
(floor) => !excludeFloors!.includes(floor.floor_id)
);
}
const hierarchy = getAreasFloorHierarchy(floors, outputAreas);
if (
entityFilter ||
deviceFilter ||
includeDomains ||
excludeDomains ||
includeDeviceClasses
) {
// Ensure we only include floors that have areas with the filtered entities/devices
const validFloorIds = new Set(
outputAreas.map((area) => area.floor_id).filter((id) => id)
);
outputFloors = outputFloors.filter((floor) =>
validFloorIds.has(floor.floor_id)
);
}
const hierarchy = getAreasFloorHierarchy(outputFloors, outputAreas);
const items: (
| FloorComboBoxItem

View File

@@ -1,4 +1,3 @@
import { stringCompare } from "../common/string/compare";
import type { HomeAssistant } from "../types";
import type { DeviceRegistryEntry } from "./device_registry";
import type {
@@ -75,17 +74,11 @@ export const reorderAreaRegistryEntries = (
});
export const getAreaEntityLookup = (
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[],
filterHidden = false
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[]
): AreaEntityLookup => {
const areaEntityLookup: AreaEntityLookup = {};
for (const entity of entities) {
if (
!entity.area_id ||
(filterHidden &&
((entity as EntityRegistryDisplayEntry).hidden ||
(entity as EntityRegistryEntry).hidden_by))
) {
if (!entity.area_id) {
continue;
}
if (!(entity.area_id in areaEntityLookup)) {
@@ -111,22 +104,3 @@ export const getAreaDeviceLookup = (
}
return areaDeviceLookup;
};
export const areaCompare =
(entries?: HomeAssistant["areas"], order?: string[]) =>
(a: string, b: string) => {
const indexA = order ? order.indexOf(a) : -1;
const indexB = order ? order.indexOf(b) : -1;
if (indexA === -1 && indexB === -1) {
const nameA = entries?.[a]?.name ?? a;
const nameB = entries?.[b]?.name ?? b;
return stringCompare(nameA, nameB);
}
if (indexA === -1) {
return 1;
}
if (indexB === -1) {
return -1;
}
return indexA - indexB;
};

View File

@@ -144,9 +144,7 @@ const tryDescribeTrigger = (
const type = getTriggerObjectId(trigger.trigger);
return (
hass.localize(
`component.${domain}.triggers.${type}.description_configured`
) ||
hass.localize(`component.${domain}.triggers.${type}.name`) ||
hass.localize(
`ui.panel.config.automation.editor.triggers.type.${triggerType as LegacyTrigger["trigger"]}.label`
) ||
@@ -919,9 +917,7 @@ const tryDescribeCondition = (
const type = getConditionObjectId(condition.condition);
return (
hass.localize(
`component.${domain}.conditions.${type}.description_configured`
) ||
hass.localize(`component.${domain}.conditions.${type}.name`) ||
hass.localize(
`ui.panel.config.automation.editor.conditions.type.${conditionType as LegacyCondition["condition"]}.label`
) ||

View File

@@ -111,17 +111,11 @@ export const sortDeviceRegistryByName = (
);
export const getDeviceEntityLookup = (
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[],
filterHidden = false
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[]
): DeviceEntityLookup => {
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (
!entity.device_id ||
(filterHidden &&
((entity as EntityRegistryDisplayEntry).hidden ||
(entity as EntityRegistryEntry).hidden_by))
) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {

View File

@@ -11,7 +11,7 @@ import {
isLastDayOfMonth,
addYears,
} from "date-fns";
import type { Collection } from "home-assistant-js-websocket";
import type { Collection, HassEntity } from "home-assistant-js-websocket";
import { getCollection } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import {
@@ -1361,3 +1361,37 @@ export const calculateSolarConsumedGauge = (
}
return undefined;
};
/**
* Get current power value from entity state, normalized to kW
* @param stateObj - The entity state object to get power value from
* @returns Power value in kW, or 0 if entity not found or invalid
*/
export const getPowerFromState = (stateObj: HassEntity): number | undefined => {
if (!stateObj) {
return undefined;
}
const value = parseFloat(stateObj.state);
if (isNaN(value)) {
return undefined;
}
// Normalize to kW 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 / 1000;
case "mW":
return value / 1000000;
case "MW":
return value * 1000;
case "GW":
return value * 1000000;
case "TW":
return value * 1000000000;
default:
// Assume kW if no unit or unit is kW
return value;
}
};

View File

@@ -1,4 +1,3 @@
import { stringCompare } from "../common/string/compare";
import type { HomeAssistant } from "../types";
import type { AreaRegistryEntry } from "./area_registry";
import type { RegistryEntry } from "./registry";
@@ -75,27 +74,3 @@ export const getFloorAreaLookup = (
}
return floorAreaLookup;
};
export const floorCompare =
(entries?: HomeAssistant["floors"], order?: string[]) =>
(a: string, b: string) => {
const indexA = order ? order.indexOf(a) : -1;
const indexB = order ? order.indexOf(b) : -1;
if (indexA === -1 && indexB === -1) {
const floorA = entries?.[a];
const floorB = entries?.[b];
if (floorA && floorB && floorA.level !== floorB.level) {
return (floorB.level ?? -9999) - (floorA.level ?? -9999);
}
const nameA = floorA?.name ?? a;
const nameB = floorB?.name ?? b;
return stringCompare(nameA, nameB);
}
if (indexA === -1) {
return 1;
}
if (indexB === -1) {
return -1;
}
return indexA - indexB;
};

View File

@@ -47,8 +47,7 @@ export interface HassioFullBackupCreateParams {
confirm_password?: string;
background?: boolean;
}
export interface HassioPartialBackupCreateParams
extends HassioFullBackupCreateParams {
export interface HassioPartialBackupCreateParams extends HassioFullBackupCreateParams {
folders?: string[];
addons?: string[];
homeassistant?: boolean;

View File

@@ -18,6 +18,11 @@ export interface LabPreviewFeaturesResponse {
features: LabPreviewFeature[];
}
/**
* Fetch all lab features
* @param hass - The Home Assistant instance
* @returns A promise to fetch the lab features
*/
export const fetchLabFeatures = async (
hass: HomeAssistant
): Promise<LabPreviewFeature[]> => {
@@ -27,6 +32,15 @@ export const fetchLabFeatures = async (
return response.features;
};
/**
* Update a specific lab feature
* @param hass - The Home Assistant instance
* @param domain - The domain of the lab feature
* @param preview_feature - The preview feature of the lab feature
* @param enabled - Whether the lab feature is enabled
* @param create_backup - Whether to create a backup of the lab feature
* @returns A promise to update the lab feature
*/
export const labsUpdatePreviewFeature = (
hass: HomeAssistant,
domain: string,
@@ -65,6 +79,12 @@ const subscribeLabUpdates = (
"labs_updated"
);
/**
* Subscribe to a collection of lab features
* @param conn - The connection to the Home Assistant instance
* @param onChange - The function to call when the lab features change
* @returns The unsubscribe function
*/
export const subscribeLabFeatures = (
conn: Connection,
onChange: (features: LabPreviewFeature[]) => void
@@ -76,3 +96,23 @@ export const subscribeLabFeatures = (
conn,
onChange
);
/**
* Subscribe to a specific lab feature
* @param conn - The connection to the Home Assistant instance
* @param domain - The domain of the lab feature
* @param previewFeature - The preview feature identifier
* @param onChange - The function to call when the lab feature changes
* @returns A promise that resolves to the unsubscribe function
*/
export const subscribeLabFeature = (
conn: Connection,
domain: string,
previewFeature: string,
onChange: (feature: LabPreviewFeature) => void
): Promise<() => void> =>
conn.subscribeMessage<LabPreviewFeature>(onChange, {
type: "labs/subscribe",
domain,
preview_feature: previewFeature,
});

View File

@@ -18,8 +18,7 @@ export const enum LawnMowerEntityFeature {
}
interface LawnMowerEntityAttributes
extends HassEntityAttributeBase,
Record<string, any> {}
extends HassEntityAttributeBase, Record<string, any> {}
export interface LawnMowerEntity extends HassEntityBase {
attributes: LawnMowerEntityAttributes;

View File

@@ -18,8 +18,7 @@ export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
cards?: LovelaceCardConfig[];
}
export interface LovelaceStrategySectionConfig
extends LovelaceBaseSectionConfig {
export interface LovelaceStrategySectionConfig extends LovelaceBaseSectionConfig {
strategy: LovelaceStrategyConfig;
}

View File

@@ -11,8 +11,7 @@ export interface LovelaceConfig extends LovelaceDashboardBaseConfig {
views: LovelaceViewRawConfig[];
}
export interface LovelaceDashboardStrategyConfig
extends LovelaceDashboardBaseConfig {
export interface LovelaceDashboardStrategyConfig extends LovelaceDashboardBaseConfig {
strategy: LovelaceStrategyConfig;
}

View File

@@ -29,8 +29,7 @@ export interface LovelaceDashboardMutableParams {
title: string;
}
export interface LovelaceDashboardCreateParams
extends LovelaceDashboardMutableParams {
export interface LovelaceDashboardCreateParams extends LovelaceDashboardMutableParams {
url_path: string;
mode: "storage";
}

View File

@@ -220,12 +220,12 @@ const tryDescribeAction = <T extends ActionType>(
if (config.action) {
const [domain, serviceName] = config.action.split(".", 2);
const descriptionPlaceholders =
hass.services[domain][serviceName].description_placeholders;
hass.services[domain]?.[serviceName]?.description_placeholders;
const service =
hass.localize(
`component.${domain}.services.${serviceName}.name`,
descriptionPlaceholders
) || hass.services[domain][serviceName]?.name;
) || hass.services[domain]?.[serviceName]?.name;
if (config.metadata) {
return hass.localize(

View File

@@ -106,8 +106,7 @@ export interface AutomationTrace extends BaseTrace {
}
export interface AutomationTraceExtended
extends AutomationTrace,
BaseTraceExtended {
extends AutomationTrace, BaseTraceExtended {
config: ManualAutomationConfig;
blueprint_inputs?: BlueprintAutomationConfig;
}

View File

@@ -82,6 +82,12 @@ export interface WeatherEntity extends HassEntityBase {
attributes: WeatherEntityAttributes;
}
export const WEATHER_TEMPERATURE_ATTRIBUTES = new Set<string>([
"temperature",
"apparent_temperature",
"dew_point",
]);
export const weatherSVGs = new Set<string>([
"clear-night",
"cloudy",
@@ -256,9 +262,15 @@ export const getWeatherUnit = (
export const getSecondaryWeatherAttribute = (
hass: HomeAssistant,
stateObj: WeatherEntity,
forecast: ForecastAttribute[]
forecast: ForecastAttribute[],
temperatureFractionDigits?: number
): TemplateResult | undefined => {
const extrema = getWeatherExtrema(hass, stateObj, forecast);
const extrema = getWeatherExtrema(
hass,
stateObj,
forecast,
temperatureFractionDigits
);
if (extrema) {
return extrema;
@@ -298,7 +310,8 @@ export const getSecondaryWeatherAttribute = (
const getWeatherExtrema = (
hass: HomeAssistant,
stateObj: WeatherEntity,
forecast: ForecastAttribute[]
forecast: ForecastAttribute[],
temperatureFractionDigits?: number
): TemplateResult | undefined => {
if (!forecast?.length) {
return undefined;
@@ -313,13 +326,22 @@ const getWeatherExtrema = (
break;
}
if (!tempHigh || fc.temperature > tempHigh) {
tempHigh = fc.temperature;
tempHigh =
temperatureFractionDigits === undefined
? fc.temperature
: round(fc.temperature, temperatureFractionDigits);
}
if (!tempLow || (fc.templow && fc.templow < tempLow)) {
tempLow = fc.templow;
if (fc.templow !== undefined && (!tempLow || fc.templow < tempLow)) {
tempLow =
temperatureFractionDigits === undefined
? fc.templow
: round(fc.templow, temperatureFractionDigits);
}
if (!fc.templow && (!tempLow || fc.temperature < tempLow)) {
tempLow = fc.temperature;
tempLow =
temperatureFractionDigits === undefined
? fc.temperature
: round(fc.temperature, temperatureFractionDigits);
}
}

View File

@@ -1,6 +1,7 @@
import { mdiAlertOutline, mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
@@ -64,6 +65,7 @@ class DialogBox extends LitElement {
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
type=${confirmPrompt ? "alert" : "standard"}
?prevent-scrim-close=${confirmPrompt}
@closed=${this._dialogClosed}
aria-labelledby="dialog-box-title"
@@ -79,7 +81,11 @@ class DialogBox extends LitElement {
></ha-icon-button
></slot>`
: nothing}
<span slot="title" id="dialog-box-title">
<span
class=${classMap({ title: true, alert: confirmPrompt })}
slot="title"
id="dialog-box-title"
>
${this._params.warning
? html`<ha-svg-icon
.path=${mdiAlertOutline}
@@ -199,6 +205,14 @@ class DialogBox extends LitElement {
ha-textfield {
width: 100%;
}
.title.alert {
padding: 0 var(--ha-space-2);
}
@media all and (min-width: 450px) and (min-height: 500px) {
.title.alert {
padding: 0 var(--ha-space-1);
}
}
`;
}

View File

@@ -32,8 +32,7 @@ export interface PromptDialogParams extends BaseDialogBoxParams {
}
export interface DialogBoxParams
extends ConfirmationDialogParams,
PromptDialogParams {
extends ConfirmationDialogParams, PromptDialogParams {
confirm?: (out?: string) => void;
confirmation?: boolean;
prompt?: boolean;

View File

@@ -1,9 +1,9 @@
import type { HASSDomEvent, ValidHassDomEvent } from "../common/dom/fire_event";
import { mainWindow } from "../common/dom/get_main_window";
import type { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
import { ancestorsWithProperty } from "../common/dom/ancestors-with-property";
import { deepActiveElement } from "../common/dom/deep-active-element";
import type { HASSDomEvent, ValidHassDomEvent } from "../common/dom/fire_event";
import { mainWindow } from "../common/dom/get_main_window";
import { nextRender } from "../common/util/render-status";
import type { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
declare global {
// for fire event
@@ -19,10 +19,11 @@ declare global {
}
}
export interface HassDialog<T = HASSDomEvents[ValidHassDomEvent]>
extends HTMLElement {
export interface HassDialog<
T = HASSDomEvents[ValidHassDomEvent],
> extends HTMLElement {
showDialog(params: T);
closeDialog?: () => boolean;
closeDialog?: (historyState?: any) => boolean;
}
interface ShowDialogParams<T> {
@@ -143,27 +144,32 @@ export const showDialog = async (
return true;
};
export const closeDialog = async (dialogTag: string): Promise<boolean> => {
export const closeDialog = async (
dialogTag: string,
historyState?: any
): Promise<boolean> => {
if (!(dialogTag in LOADED)) {
return true;
}
const dialogElement = await LOADED[dialogTag].element;
if (dialogElement.closeDialog) {
return dialogElement.closeDialog() !== false;
return dialogElement.closeDialog(historyState) !== false;
}
return true;
};
// called on back()
export const closeLastDialog = async () => {
export const closeLastDialog = async (historyState?: any) => {
if (OPEN_DIALOG_STACK.length) {
const lastDialog = OPEN_DIALOG_STACK.pop();
const closed = await closeDialog(lastDialog!.dialogTag);
const lastDialog = OPEN_DIALOG_STACK.pop() as DialogState;
const closed = await closeDialog(lastDialog.dialogTag, historyState);
if (!closed) {
// if the dialog was not closed, put it back on the stack
OPEN_DIALOG_STACK.push(lastDialog!);
}
if (OPEN_DIALOG_STACK.length && mainWindow.history.state?.opensDialog) {
OPEN_DIALOG_STACK.push(lastDialog);
} else if (
OPEN_DIALOG_STACK.length &&
mainWindow.history.state?.opensDialog
) {
// if there are more dialogs open, push a new state so back() will close the next top dialog
mainWindow.history.pushState(
{ dialog: OPEN_DIALOG_STACK[OPEN_DIALOG_STACK.length - 1].dialogTag },

View File

@@ -1,5 +1,5 @@
import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { slugify } from "../../../common/string/slugify";
import "../../../components/buttons/ha-progress-button";
import "../../../components/ha-camera-stream";
@@ -9,6 +9,7 @@ import type { HomeAssistant } from "../../../types";
import { fileDownload } from "../../../util/file_download";
import { showToast } from "../../../util/toast";
@customElement("more-info-camera")
class MoreInfoCamera extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -112,8 +113,6 @@ class MoreInfoCamera extends LitElement {
`;
}
customElements.define("more-info-camera", MoreInfoCamera);
declare global {
interface HTMLElementTagNameMap {
"more-info-camera": MoreInfoCamera;

View File

@@ -7,7 +7,7 @@ import {
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
@@ -32,6 +32,7 @@ import { moreInfoControlStyle } from "../components/more-info-control-style";
type MainControl = "temperature" | "humidity";
@customElement("more-info-climate")
class MoreInfoClimate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -567,8 +568,6 @@ class MoreInfoClimate extends LitElement {
}
}
customElements.define("more-info-climate", MoreInfoClimate);
declare global {
interface HTMLElementTagNameMap {
"more-info-climate": MoreInfoClimate;

View File

@@ -1,7 +1,7 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
import type { GroupEntity } from "../../../data/group";
import { computeGroupDomain } from "../../../data/group";
@@ -13,6 +13,7 @@ import {
importMoreInfoControl,
} from "../state_more_info_control";
@customElement("more-info-group")
class MoreInfoGroup extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -106,8 +107,6 @@ class MoreInfoGroup extends LitElement {
}
}
customElements.define("more-info-group", MoreInfoGroup);
declare global {
interface HTMLElementTagNameMap {
"more-info-group": MoreInfoGroup;

View File

@@ -1,7 +1,7 @@
import { mdiPower, mdiTuneVariant } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-select-menu";
@@ -15,6 +15,7 @@ import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-control-select-container";
import { moreInfoControlStyle } from "../components/more-info-control-style";
@customElement("more-info-humidifier")
class MoreInfoHumidifier extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -249,8 +250,6 @@ class MoreInfoHumidifier extends LitElement {
}
}
customElements.define("more-info-humidifier", MoreInfoHumidifier);
declare global {
interface HTMLElementTagNameMap {
"more-info-humidifier": MoreInfoHumidifier;

View File

@@ -14,8 +14,9 @@ import {
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import { classMap } from "lit/directives/class-map";
import { keyed } from "lit/directives/keyed";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
@@ -50,7 +51,12 @@ import { lightSupportsFavoriteColors } from "../../data/light";
import type { ItemType } from "../../data/search";
import { SearchableDomains } from "../../data/search";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
import { ScrollableFadeMixin } from "../../mixins/scrollable-fade-mixin";
import {
haStyleDialog,
haStyleDialogFixedTop,
haStyleScrollbar,
} from "../../resources/styles";
import "../../state-summary/state-card-content";
import type { HomeAssistant } from "../../types";
import {
@@ -96,13 +102,15 @@ declare global {
const DEFAULT_VIEW: View = "info";
@customElement("ha-more-info-dialog")
export class MoreInfoDialog extends LitElement {
export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public large = false;
@state() private _parentEntityIds: string[] = [];
@query(".content") private _contentElement?: HTMLDivElement;
@state() private _entityId?: string | null;
@state() private _data?: Record<string, any>;
@@ -121,6 +129,12 @@ export class MoreInfoDialog extends LitElement {
@state() private _sensorNumericDeviceClasses?: string[] = [];
protected scrollFadeThreshold = 24;
protected get scrollableElement(): HTMLElement | null {
return this._contentElement || null;
}
public showDialog(params: MoreInfoDialogParams) {
this._entityId = params.entityId;
if (!this._entityId) {
@@ -302,7 +316,9 @@ export class MoreInfoDialog extends LitElement {
}
private _goToAddEntityTo(ev) {
if (!shouldHandleRequestSelectedEvent(ev)) return;
// Only check for request-selected events (from menu items), not regular clicks (from icon button)
if (ev.type === "request-selected" && !shouldHandleRequestSelectedEvent(ev))
return;
this._setView("add_to");
}
@@ -550,7 +566,18 @@ export class MoreInfoDialog extends LitElement {
: nothing}
</ha-button-menu>
`
: nothing}
: !__DEMO__ && this._shouldShowAddEntityTo()
? html`
<ha-icon-button
slot="actionItems"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.add_entity_to"
)}
.path=${mdiPlusBoxMultipleOutline}
@click=${this._goToAddEntityTo}
></ha-icon-button>
`
: nothing}
`
: isSpecificInitialView
? html`
@@ -581,78 +608,86 @@ export class MoreInfoDialog extends LitElement {
`
: nothing}
</ha-dialog-header>
${keyed(
this._entityId,
html`
<div
class="content"
tabindex="-1"
dialogInitialFocus
@show-child-view=${this._showChildView}
@entity-entry-updated=${this._entryUpdated}
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
@hass-more-info=${this._handleMoreInfoEvent}
>
${cache(
this._childView
? html`
<div class="child-view">
${dynamicElement(this._childView.viewTag, {
hass: this.hass,
entry: this._entry,
params: this._childView.viewParams,
})}
</div>
`
: this._currView === "info"
<div
class=${classMap({
"content-wrapper": true,
"settings-view": this._currView === "settings",
})}
>
${keyed(
this._entityId,
html`
<div
class="content ha-scrollbar"
tabindex="-1"
dialogInitialFocus
@show-child-view=${this._showChildView}
@entity-entry-updated=${this._entryUpdated}
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
@hass-more-info=${this._handleMoreInfoEvent}
>
${cache(
this._childView
? html`
<ha-more-info-info
dialogInitialFocus
.hass=${this.hass}
.entityId=${this._entityId}
.entry=${this._entry}
.editMode=${this._infoEditMode}
.data=${this._data}
></ha-more-info-info>
<div class="child-view">
${dynamicElement(this._childView.viewTag, {
hass: this.hass,
entry: this._entry,
params: this._childView.viewParams,
})}
</div>
`
: this._currView === "history"
: this._currView === "info"
? html`
<ha-more-info-history-and-logbook
<ha-more-info-info
dialogInitialFocus
.hass=${this.hass}
.entityId=${this._entityId}
></ha-more-info-history-and-logbook>
.entry=${this._entry}
.editMode=${this._infoEditMode}
.data=${this._data}
></ha-more-info-info>
`
: this._currView === "settings"
: this._currView === "history"
? html`
<ha-more-info-settings
<ha-more-info-history-and-logbook
.hass=${this.hass}
.entityId=${this._entityId}
.entry=${this._entry}
></ha-more-info-settings>
></ha-more-info-history-and-logbook>
`
: this._currView === "related"
: this._currView === "settings"
? html`
<ha-related-items
<ha-more-info-settings
.hass=${this.hass}
.itemId=${entityId}
.itemType=${SearchableDomains.has(domain)
? (domain as ItemType)
: "entity"}
></ha-related-items>
.entityId=${this._entityId}
.entry=${this._entry}
></ha-more-info-settings>
`
: this._currView === "add_to"
: this._currView === "related"
? html`
<ha-more-info-add-to
<ha-related-items
.hass=${this.hass}
.entityId=${entityId}
@add-to-action-selected=${this._goBack}
></ha-more-info-add-to>
.itemId=${entityId}
.itemType=${SearchableDomains.has(domain)
? (domain as ItemType)
: "entity"}
></ha-related-items>
`
: nothing
)}
</div>
`
)}
: this._currView === "add_to"
? html`
<ha-more-info-add-to
.hass=${this.hass}
.entityId=${entityId}
@add-to-action-selected=${this._goBack}
></ha-more-info-add-to>
`
: nothing
)}
</div>
`
)}
${this.renderScrollableFades()}
</div>
</ha-dialog>
`;
}
@@ -707,18 +742,31 @@ export class MoreInfoDialog extends LitElement {
static get styles() {
return [
...super.styles,
haStyleDialog,
haStyleDialogFixedTop,
haStyleScrollbar,
css`
ha-dialog {
--dialog-content-padding: 0;
}
.content {
.content-wrapper {
flex: 1 1 auto;
min-height: 0;
position: relative;
display: flex;
flex-direction: column;
}
.content {
outline: none;
flex: 1;
overflow: auto;
}
.content-wrapper.settings-view .fade-bottom {
bottom: var(--ha-space-18);
}
.child-view {

View File

@@ -15,7 +15,7 @@ import "../../components/ha-alert";
import "../../components/ha-expansion-panel";
import "../../components/ha-fade-in";
import "../../components/ha-icon-next";
import "../../components/ha-wa-dialog";
import "../../components/ha-adaptive-dialog";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/ha-spinner";
@@ -109,7 +109,7 @@ class DialogRestart extends LitElement {
const dialogTitle = this.hass.localize("ui.dialogs.restart.heading");
return html`
<ha-wa-dialog
<ha-adaptive-dialog
.hass=${this.hass}
.open=${this._dialogOpen}
header-title=${dialogTitle}
@@ -257,7 +257,7 @@ class DialogRestart extends LitElement {
</ha-expansion-panel>
`}
</div>
</ha-wa-dialog>
</ha-adaptive-dialog>
`;
}
@@ -405,7 +405,7 @@ class DialogRestart extends LitElement {
haStyle,
haStyleDialog,
css`
ha-wa-dialog {
ha-adaptive-dialog {
--dialog-content-padding: 0;
}

View File

@@ -1,11 +1,12 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { mdiClose, mdiDotsVertical, mdiRestart } from "@mdi/js";
import { css, html, LitElement, nothing, type TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { mdiDotsVertical, mdiRestart } from "@mdi/js";
import { css, html, LitElement, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-alert";
import "../../components/ha-dialog-header";
import "../../components/ha-button";
import "../../components/ha-dialog-footer";
import "../../components/ha-fade-in";
import "../../components/ha-icon-button";
import "../../components/ha-items-display-editor";
@@ -14,9 +15,8 @@ import type {
DisplayValue,
} from "../../components/ha-items-display-editor";
import "../../components/ha-md-button-menu";
import "../../components/ha-md-dialog";
import type { HaMdDialog } from "../../components/ha-md-dialog";
import "../../components/ha-md-menu-item";
import "../../components/ha-wa-dialog";
import { computePanels } from "../../components/ha-sidebar";
import "../../components/ha-spinner";
import "../../components/ha-svg-icon";
@@ -40,8 +40,6 @@ class DialogEditSidebar extends LitElement {
@state() private _open = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
@state() private _order?: string[];
@state() private _hidden?: string[];
@@ -55,7 +53,6 @@ class DialogEditSidebar extends LitElement {
public async showDialog(): Promise<void> {
this._open = true;
this._getData();
}
@@ -87,7 +84,7 @@ class DialogEditSidebar extends LitElement {
}
public closeDialog(): void {
this._dialog?.close();
this._open = false;
}
private _panels = memoizeOne((panels: HomeAssistant["panels"]) =>
@@ -167,57 +164,52 @@ class DialogEditSidebar extends LitElement {
}
protected render() {
if (!this._open) {
return nothing;
}
const dialogTitle = this.hass.localize("ui.sidebar.edit_sidebar");
return html`
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${dialogTitle}
header-subtitle=${!this._migrateToUserData
? this.hass.localize("ui.sidebar.edit_subtitle")
: ""}
@closed=${this._dialogClosed}
>
<ha-md-button-menu
slot="headerActionItems"
positioning="popover"
anchor-corner="end-end"
menu-corner="start-end"
>
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
@click=${this.closeDialog}
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<span slot="title" .title=${dialogTitle}>${dialogTitle}</span>
${!this._migrateToUserData
? html`<span slot="subtitle"
>${this.hass.localize("ui.sidebar.edit_subtitle")}</span
>`
: nothing}
<ha-md-button-menu
slot="actionItems"
positioning="popover"
anchor-corner="end-end"
menu-corner="start-end"
<ha-md-menu-item .clickAction=${this._resetToDefaults}>
<ha-svg-icon slot="start" .path=${mdiRestart}></ha-svg-icon>
${this.hass.localize("ui.sidebar.reset_to_defaults")}
</ha-md-menu-item>
</ha-md-button-menu>
<div class="content">${this._renderContent()}</div>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item .clickAction=${this._resetToDefaults}>
<ha-svg-icon slot="start" .path=${mdiRestart}></ha-svg-icon>
${this.hass.localize("ui.sidebar.reset_to_defaults")}
</ha-md-menu-item>
</ha-md-button-menu>
</ha-dialog-header>
<div slot="content" class="content">${this._renderContent()}</div>
<div slot="actions">
<ha-button appearance="plain" @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
.disabled=${!this._order || !this._hidden}
@click=${this._save}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</div>
</ha-md-dialog>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
@@ -272,15 +264,13 @@ class DialogEditSidebar extends LitElement {
}
static styles = css`
ha-md-dialog {
min-width: 600px;
ha-wa-dialog {
max-height: 90%;
--dialog-content-padding: 8px 24px;
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
}
@media all and (max-width: 600px), all and (max-height: 500px) {
ha-md-dialog {
--md-dialog-container-shape: 0;
@media all and (max-width: 580px), all and (max-height: 500px) {
ha-wa-dialog {
min-width: 100%;
min-height: 100%;
}

View File

@@ -1,9 +1,10 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import "../components/ha-spinner";
import "../components/ha-button";
@customElement("ha-init-page")
class HaInitPage extends LitElement {
@property({ type: Boolean }) public error = false;
@@ -120,8 +121,6 @@ class HaInitPage extends LitElement {
`;
}
customElements.define("ha-init-page", HaInitPage);
declare global {
interface HTMLElementTagNameMap {
"ha-init-page": HaInitPage;

View File

@@ -128,6 +128,8 @@ class HassSubpage extends LitElement {
ha-menu-button,
ha-icon-button-arrow-prev,
::slotted([slot="toolbar-icon"]) {
display: flex;
align-items: center;
pointer-events: auto;
color: var(--sidebar-icon-color);
}
@@ -143,7 +145,6 @@ class HassSubpage extends LitElement {
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
padding-bottom: 1px;
}
.content {

View File

@@ -621,9 +621,9 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
} else if (this._sortDirection === "asc") {
this._sortDirection = "desc";
} else {
this._sortDirection = null;
this._sortDirection = "asc";
}
this._sortColumn = this._sortDirection === null ? undefined : columnId;
this._sortColumn = columnId;
fireEvent(this, "sorting-changed", {
column: columnId,

View File

@@ -1,6 +1,6 @@
import { mdiClose } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { property, query, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import type { LocalizeKeys } from "../common/translations/localize";
import "../components/ha-button";
import "../components/ha-icon-button";
@@ -26,6 +26,7 @@ export interface ToastActionParams {
| { translationKey: LocalizeKeys; args?: Record<string, string> };
}
@customElement("notification-manager")
class NotificationManager extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -115,8 +116,6 @@ class NotificationManager extends LitElement {
}
}
customElements.define("notification-manager", NotificationManager);
declare global {
interface HTMLElementTagNameMap {
"notification-manager": NotificationManager;

View File

@@ -13,19 +13,27 @@ import type { Constructor } from "../types";
const stylesArray = (styles?: CSSResultGroup | CSSResultGroup[]) =>
styles === undefined ? [] : Array.isArray(styles) ? styles : [styles];
/**
* Mixin that adds top and bottom fade overlays for scrollable content.
* @param superClass - The LitElement class to extend.
* @returns Extended class with scrollable fade functionality.
*/
export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
superClass: T
) => {
class ScrollableFadeClass extends superClass {
/** Whether content has scrolled past the threshold. Controls top fade visibility. */
@state() protected _contentScrolled = false;
/** Whether content extends beyond the viewport. Controls bottom fade visibility. */
@state() protected _contentScrollable = false;
private _scrollTarget?: HTMLElement | null;
private _onScroll = (ev: Event) => {
const target = ev.currentTarget as HTMLElement;
this._contentScrolled = (target.scrollTop ?? 0) > 0;
this._contentScrolled =
(target.scrollTop ?? 0) > this.scrollFadeThreshold;
this._updateScrollableState(target);
};
@@ -39,15 +47,26 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
},
});
private static readonly DEFAULT_SAFE_AREA_PADDING = 16;
/**
* Safe area padding in pixels for the scrollable element.
*/
protected scrollFadeSafeAreaPadding = 16;
/**
* Scroll threshold in pixels for showing the fades.
*/
protected scrollFadeThreshold = 4;
/**
* Default scrollable element value.
*/
private static readonly DEFAULT_SCROLLABLE_ELEMENT: HTMLElement | null =
null;
protected get scrollFadeSafeAreaPadding() {
return ScrollableFadeClass.DEFAULT_SAFE_AREA_PADDING;
}
/**
* Element to observe for scroll and resize events. Override with a getter to specify target.
* Kept as a getter to allow subclasses to return query results.
*/
protected get scrollableElement(): HTMLElement | null {
return ScrollableFadeClass.DEFAULT_SCROLLABLE_ELEMENT;
}
@@ -67,6 +86,11 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
super.disconnectedCallback();
}
/**
* Renders top and bottom fade overlays. Call in render method.
* @param rounded - Whether to apply rounded corners.
* @returns Template containing fade elements.
*/
protected renderScrollableFades(rounded = false): TemplateResult {
return html`
<div
@@ -110,7 +134,6 @@ export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
transparent
);
border-radius: var(--ha-border-radius-square);
z-index: 100;
opacity: 0;
}
.fade-top {

View File

@@ -90,9 +90,7 @@ class OnboardingRestoreBackupCloudLogin extends LitElement {
this._email = this._cloudLoginElement.emailField.value;
}
await import(
"../../panels/config/cloud/forgot-password/cloud-forgot-password-card"
);
await import("../../panels/config/cloud/forgot-password/cloud-forgot-password-card");
this._view = "forgot-password";
}

View File

@@ -3,7 +3,7 @@ import { TZDate } from "@date-fns/tz";
import { addDays, isSameDay } from "date-fns";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { formatDate } from "../../common/datetime/format_date";
import { formatDateTime } from "../../common/datetime/format_date_time";
import { formatTime } from "../../common/datetime/format_time";
@@ -26,6 +26,7 @@ import type { CalendarEventDetailDialogParams } from "./show-dialog-calendar-eve
import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor";
import { resolveTimeZone } from "../../common/datetime/resolve-time-zone";
@customElement("dialog-calendar-event-detail")
class DialogCalendarEventDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -271,8 +272,3 @@ declare global {
"dialog-calendar-event-detail": DialogCalendarEventDetail;
}
}
customElements.define(
"dialog-calendar-event-detail",
DialogCalendarEventDetail
);

View File

@@ -13,6 +13,7 @@ import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strateg
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import "../lovelace/views/hui-view-background";
const CLIMATE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
strategy: {
@@ -115,6 +116,7 @@ class PanelClimate extends LitElement {
this._lovelace
? html`
<hui-view-container .hass=${this.hass}>
<hui-view-background .hass=${this.hass}> </hui-view-background>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}

View File

@@ -1,7 +1,7 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
@@ -40,6 +40,7 @@ const SENSOR_DOMAINS = ["sensor"];
const TEMPERATURE_DEVICE_CLASSES = [SENSOR_DEVICE_CLASS_TEMPERATURE];
const HUMIDITY_DEVICE_CLASSES = [SENSOR_DEVICE_CLASS_HUMIDITY];
@customElement("dialog-area-registry-detail")
class DialogAreaDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -426,5 +427,3 @@ declare global {
"dialog-area-registry-detail": DialogAreaDetail;
}
}
customElements.define("dialog-area-registry-detail", DialogAreaDetail);

View File

@@ -0,0 +1,495 @@
import { mdiDragHorizontalVariant, mdiTextureBox } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import {
type AreasFloorHierarchy,
getAreasFloorHierarchy,
getAreasOrder,
getFloorOrder,
} from "../../../common/areas/areas-floor-hierarchy";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-floor-icon";
import "../../../components/ha-icon";
import "../../../components/ha-wa-dialog";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import {
reorderAreaRegistryEntries,
updateAreaRegistryEntry,
} from "../../../data/area_registry";
import { reorderFloorRegistryEntries } from "../../../data/floor_registry";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { showToast } from "../../../util/toast";
import type { AreasFloorsOrderDialogParams } from "./show-dialog-areas-floors-order";
const UNASSIGNED_FLOOR = "__unassigned__";
interface FloorChange {
areaId: string;
floorId: string | null;
}
@customElement("dialog-areas-floors-order")
class DialogAreasFloorsOrder extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@state() private _hierarchy?: AreasFloorHierarchy;
@state() private _saving = false;
public async showDialog(
_params: AreasFloorsOrderDialogParams
): Promise<void> {
this._open = true;
this._computeHierarchy();
}
private _computeHierarchy(): void {
this._hierarchy = getAreasFloorHierarchy(
Object.values(this.hass.floors),
Object.values(this.hass.areas)
);
}
public closeDialog(): void {
this._saving = false;
this._open = false;
}
private _dialogClosed(): void {
this._open = false;
this._hierarchy = undefined;
this._saving = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._hierarchy) {
return nothing;
}
const hasFloors = this._hierarchy.floors.length > 0;
const dialogTitle = this.hass.localize(
hasFloors
? "ui.panel.config.areas.dialog.reorder_floors_areas_title"
: "ui.panel.config.areas.dialog.reorder_areas_title"
);
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${dialogTitle}
@closed=${this._dialogClosed}
>
<div class="content">
<ha-sortable
handle-selector=".floor-handle"
draggable-selector=".floor"
@item-moved=${this._floorMoved}
invert-swap
>
<div class="floors">
${repeat(
this._hierarchy.floors,
(floor) => floor.id,
(floor) => this._renderFloor(floor)
)}
</div>
</ha-sortable>
${this._renderUnassignedAreas()}
</div>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
@click=${this.closeDialog}
appearance="plain"
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${this._saving}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
private _renderFloor(floor: { id: string; areas: string[] }) {
const floorEntry = this.hass.floors[floor.id];
if (!floorEntry) {
return nothing;
}
return html`
<div class="floor">
<div class="floor-header">
<ha-floor-icon .floor=${floorEntry}></ha-floor-icon>
<span class="floor-name">${floorEntry.name}</span>
<ha-svg-icon
class="floor-handle"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</div>
<ha-sortable
handle-selector=".area-handle"
draggable-selector="ha-md-list-item"
@item-moved=${this._areaMoved}
@item-added=${this._areaAdded}
group="areas"
.floor=${floor.id}
>
<ha-md-list>
${floor.areas.length > 0
? floor.areas.map((areaId) => this._renderArea(areaId))
: html`<p class="empty">
${this.hass.localize(
"ui.panel.config.areas.dialog.empty_floor"
)}
</p>`}
</ha-md-list>
</ha-sortable>
</div>
`;
}
private _renderUnassignedAreas() {
const hasFloors = this._hierarchy!.floors.length > 0;
return html`
<div class="floor unassigned">
${hasFloors
? html`<div class="floor-header">
<span class="floor-name">
${this.hass.localize(
"ui.panel.config.areas.dialog.other_areas"
)}
</span>
</div>`
: nothing}
<ha-sortable
handle-selector=".area-handle"
draggable-selector="ha-md-list-item"
@item-moved=${this._areaMoved}
@item-added=${this._areaAdded}
group="areas"
.floor=${UNASSIGNED_FLOOR}
>
<ha-md-list>
${this._hierarchy!.areas.length > 0
? this._hierarchy!.areas.map((areaId) => this._renderArea(areaId))
: html`<p class="empty">
${this.hass.localize(
"ui.panel.config.areas.dialog.empty_unassigned"
)}
</p>`}
</ha-md-list>
</ha-sortable>
</div>
`;
}
private _renderArea(areaId: string) {
const area = this.hass.areas[areaId];
if (!area) {
return nothing;
}
return html`
<ha-md-list-item .sortableData=${area}>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<span slot="headline">${area.name}</span>
<ha-svg-icon
class="area-handle"
slot="end"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</ha-md-list-item>
`;
}
private _floorMoved(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._hierarchy) {
return;
}
const { oldIndex, newIndex } = ev.detail;
const newFloors = [...this._hierarchy.floors];
const [movedFloor] = newFloors.splice(oldIndex, 1);
newFloors.splice(newIndex, 0, movedFloor);
this._hierarchy = {
...this._hierarchy,
floors: newFloors,
};
}
private _areaMoved(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._hierarchy) {
return;
}
const { floor } = ev.currentTarget as HTMLElement & { floor: string };
const { oldIndex, newIndex } = ev.detail;
const floorId = floor === UNASSIGNED_FLOOR ? null : floor;
if (floorId === null) {
// Reorder unassigned areas
const newAreas = [...this._hierarchy.areas];
const [movedArea] = newAreas.splice(oldIndex, 1);
newAreas.splice(newIndex, 0, movedArea);
this._hierarchy = {
...this._hierarchy,
areas: newAreas,
};
} else {
// Reorder areas within a floor
this._hierarchy = {
...this._hierarchy,
floors: this._hierarchy.floors.map((f) => {
if (f.id === floorId) {
const newAreas = [...f.areas];
const [movedArea] = newAreas.splice(oldIndex, 1);
newAreas.splice(newIndex, 0, movedArea);
return { ...f, areas: newAreas };
}
return f;
}),
};
}
}
private _areaAdded(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._hierarchy) {
return;
}
const { floor } = ev.currentTarget as HTMLElement & { floor: string };
const { data: area, index } = ev.detail as {
data: AreaRegistryEntry;
index: number;
};
const newFloorId = floor === UNASSIGNED_FLOOR ? null : floor;
// Update hierarchy
const newUnassignedAreas = this._hierarchy.areas.filter(
(id) => id !== area.area_id
);
if (newFloorId === null) {
// Add to unassigned at the specified index
newUnassignedAreas.splice(index, 0, area.area_id);
}
this._hierarchy = {
...this._hierarchy,
floors: this._hierarchy.floors.map((f) => {
if (f.id === newFloorId) {
// Add to new floor at the specified index
const newAreas = [...f.areas];
newAreas.splice(index, 0, area.area_id);
return { ...f, areas: newAreas };
}
// Remove from old floor
return {
...f,
areas: f.areas.filter((id) => id !== area.area_id),
};
}),
areas: newUnassignedAreas,
};
}
private _computeFloorChanges(): FloorChange[] {
if (!this._hierarchy) {
return [];
}
const changes: FloorChange[] = [];
// Check areas assigned to floors
for (const floor of this._hierarchy.floors) {
for (const areaId of floor.areas) {
const originalFloorId = this.hass.areas[areaId]?.floor_id ?? null;
if (floor.id !== originalFloorId) {
changes.push({ areaId, floorId: floor.id });
}
}
}
// Check unassigned areas
for (const areaId of this._hierarchy.areas) {
const originalFloorId = this.hass.areas[areaId]?.floor_id ?? null;
if (originalFloorId !== null) {
changes.push({ areaId, floorId: null });
}
}
return changes;
}
private async _save(): Promise<void> {
if (!this._hierarchy || this._saving) {
return;
}
this._saving = true;
try {
const areaOrder = getAreasOrder(this._hierarchy);
const floorOrder = getFloorOrder(this._hierarchy);
// Update floor assignments for areas that changed floors
const floorChanges = this._computeFloorChanges();
const floorChangePromises = floorChanges.map(({ areaId, floorId }) =>
updateAreaRegistryEntry(this.hass, areaId, {
floor_id: floorId,
})
);
await Promise.all(floorChangePromises);
// Reorder areas and floors
await reorderAreaRegistryEntries(this.hass, areaOrder);
await reorderFloorRegistryEntries(this.hass, floorOrder);
this.closeDialog();
} catch (err: any) {
showToast(this, {
message:
err.message ||
this.hass.localize("ui.panel.config.areas.dialog.reorder_failed"),
});
this._saving = false;
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-wa-dialog {
max-height: 90%;
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
}
@media all and (max-width: 580px), all and (max-height: 500px) {
ha-wa-dialog {
min-width: 100%;
min-height: 100%;
}
}
.floors {
display: flex;
flex-direction: column;
gap: 16px;
}
.floor {
border: 1px solid var(--divider-color);
border-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
overflow: hidden;
}
.floor.unassigned {
margin-top: 16px;
}
.floor-header {
display: flex;
align-items: center;
padding: 12px 16px;
background-color: var(--secondary-background-color);
gap: 12px;
}
.floor-name {
flex: 1;
font-weight: var(--ha-font-weight-medium);
}
.floor-handle {
cursor: grab;
color: var(--secondary-text-color);
}
ha-md-list {
padding: 0;
--md-list-item-leading-space: 16px;
--md-list-item-trailing-space: 16px;
display: flex;
flex-direction: column;
}
ha-md-list-item {
--md-list-item-one-line-container-height: 48px;
--md-list-item-container-shape: 0;
}
ha-md-list-item.sortable-ghost {
border-radius: calc(
var(--ha-card-border-radius, var(--ha-border-radius-lg)) - 1px
);
box-shadow: inset 0 0 0 2px var(--primary-color);
}
.area-handle {
cursor: grab;
color: var(--secondary-text-color);
}
.empty {
text-align: center;
color: var(--secondary-text-color);
font-style: italic;
margin: 0;
padding: 12px 16px;
order: 1;
}
ha-md-list:has(ha-md-list-item) .empty {
display: none;
}
.content {
padding-top: 16px;
padding-bottom: 16px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-areas-floors-order": DialogAreasFloorsOrder;
}
}

View File

@@ -1,7 +1,7 @@
import { mdiTextureBox } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -27,6 +27,7 @@ import type { HomeAssistant } from "../../../types";
import { showAreaRegistryDetailDialog } from "./show-dialog-area-registry-detail";
import type { FloorRegistryDetailDialogParams } from "./show-dialog-floor-registry-detail";
@customElement("dialog-floor-registry-detail")
class DialogFloorDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -144,6 +145,10 @@ class DialogFloorDetail extends LitElement {
"ui.panel.config.floors.editor.level"
)}
type="number"
.helper=${this.hass.localize(
"ui.panel.config.floors.editor.level_helper"
)}
helperPersistent
></ha-textfield>
<ha-icon-picker
@@ -357,5 +362,3 @@ declare global {
"dialog-floor-registry-detail": DialogFloorDetail;
}
}
customElements.define("dialog-floor-registry-detail", DialogFloorDetail);

View File

@@ -2,10 +2,10 @@ import type { ActionDetail } from "@material/mwc-list";
import {
mdiDelete,
mdiDotsVertical,
mdiDragHorizontalVariant,
mdiHelpCircle,
mdiPencil,
mdiPlus,
mdiSort,
} from "@mdi/js";
import {
css,
@@ -21,7 +21,6 @@ import memoizeOne from "memoize-one";
import {
getAreasFloorHierarchy,
getAreasOrder,
getFloorOrder,
type AreasFloorHierarchy,
} from "../../../common/areas/areas-floor-hierarchy";
import { formatListWithAnds } from "../../../common/string/format-list";
@@ -42,7 +41,6 @@ import type { FloorRegistryEntry } from "../../../data/floor_registry";
import {
createFloorRegistryEntry,
deleteFloorRegistryEntry,
reorderFloorRegistryEntries,
updateFloorRegistryEntry,
} from "../../../data/floor_registry";
import {
@@ -58,6 +56,7 @@ import {
loadAreaRegistryDetailDialog,
showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail";
import { showAreasFloorsOrderDialog } from "./show-dialog-areas-floors-order";
import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-detail";
const UNASSIGNED_FLOOR = "__unassigned__";
@@ -84,6 +83,8 @@ export class HaConfigAreasDashboard extends LitElement {
@property({ attribute: false }) public route!: Route;
private _searchParms = new URLSearchParams(window.location.search);
@state() private _hierarchy?: AreasFloorHierarchy;
private _blockHierarchyUpdate = false;
@@ -167,7 +168,9 @@ export class HaConfigAreasDashboard extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
back-path="/config"
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.tabs=${configSections.areas}
.route=${this.route}
has-fab
@@ -179,87 +182,84 @@ export class HaConfigAreasDashboard extends LitElement {
@click=${this._showHelp}
></ha-icon-button>
<div class="container">
<ha-sortable
handle-selector=".handle"
draggable-selector=".floor"
@item-moved=${this._floorMoved}
.options=${SORT_OPTIONS}
group="floors"
invert-swap
>
<div class="floors">
${this._hierarchy.floors.map(({ areas, id }) => {
const floor = this.hass.floors[id];
if (!floor) {
return nothing;
}
return html`
<div class="floor">
<div class="header">
<h2>
<ha-floor-icon .floor=${floor}></ha-floor-icon>
${floor.name}
</h2>
<div class="actions">
<ha-svg-icon
class="handle"
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
<ha-button-menu
.floor=${floor}
@action=${this._handleFloorAction}
<div class="floors">
${this._hierarchy.floors.map(({ areas, id }) => {
const floor = this.hass.floors[id];
if (!floor) {
return nothing;
}
return html`
<div class="floor">
<div class="header">
<h2>
<ha-floor-icon .floor=${floor}></ha-floor-icon>
${floor.name}
</h2>
<div class="actions">
<ha-button-menu
.floor=${floor}
@action=${this._handleFloorAction}
>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon"
><ha-svg-icon
.path=${mdiSort}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.reorder"
)}</ha-list-item
>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon"
><ha-svg-icon
.path=${mdiPencil}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.floor.edit_floor"
)}</ha-list-item
>
<ha-list-item class="warning" graphic="icon"
><ha-svg-icon
class="warning"
.path=${mdiDelete}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.floor.delete_floor"
)}</ha-list-item
>
</ha-button-menu>
</div>
<li divider role="separator"></li>
<ha-list-item graphic="icon"
><ha-svg-icon
.path=${mdiPencil}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.floor.edit_floor"
)}</ha-list-item
>
<ha-list-item class="warning" graphic="icon"
><ha-svg-icon
class="warning"
.path=${mdiDelete}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.floor.delete_floor"
)}</ha-list-item
>
</ha-button-menu>
</div>
<ha-sortable
handle-selector="a"
draggable-selector="a"
@item-added=${this._areaAdded}
@item-moved=${this._areaMoved}
group="areas"
.options=${SORT_OPTIONS}
.floor=${floor.floor_id}
>
<div class="areas">
${areas.map((areaId) => {
const area = this.hass.areas[areaId];
if (!area) {
return nothing;
}
const stats = areasStats.get(area.area_id);
return this._renderArea(area, stats);
})}
</div>
</ha-sortable>
</div>
`;
})}
</div>
</ha-sortable>
<ha-sortable
handle-selector="a"
draggable-selector="a"
@item-added=${this._areaAdded}
@item-moved=${this._areaMoved}
group="areas"
.options=${SORT_OPTIONS}
.floor=${floor.floor_id}
>
<div class="areas">
${areas.map((areaId) => {
const area = this.hass.areas[areaId];
if (!area) {
return nothing;
}
const stats = areasStats.get(area.area_id);
return this._renderArea(area, stats);
})}
</div>
</ha-sortable>
</div>
`;
})}
</div>
${this._hierarchy.areas.length
? html`
@@ -267,9 +267,30 @@ export class HaConfigAreasDashboard extends LitElement {
<div class="header">
<h2>
${this.hass.localize(
"ui.panel.config.areas.picker.unassigned_areas"
this._hierarchy.floors.length
? "ui.panel.config.areas.picker.other_areas"
: "ui.panel.config.areas.picker.header"
)}
</h2>
<div class="actions">
<ha-button-menu
@action=${this._handleUnassignedAreasAction}
>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon"
><ha-svg-icon
.path=${mdiSort}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.reorder"
)}</ha-list-item
>
</ha-button-menu>
</div>
</div>
<ha-sortable
handle-selector="a"
@@ -391,51 +412,6 @@ export class HaConfigAreasDashboard extends LitElement {
});
}
private async _floorMoved(ev) {
ev.stopPropagation();
if (!this.hass || !this._hierarchy) {
return;
}
const { oldIndex, newIndex } = ev.detail;
const reorderFloors = (
floors: AreasFloorHierarchy["floors"],
oldIdx: number,
newIdx: number
) => {
const newFloors = [...floors];
const [movedFloor] = newFloors.splice(oldIdx, 1);
newFloors.splice(newIdx, 0, movedFloor);
return newFloors;
};
// Optimistically update UI
this._hierarchy = {
...this._hierarchy,
floors: reorderFloors(this._hierarchy.floors, oldIndex, newIndex),
};
const areaOrder = getAreasOrder(this._hierarchy);
const floorOrder = getFloorOrder(this._hierarchy);
// Block hierarchy updates for 500ms to avoid flickering
// because of multiple async updates
this._blockHierarchyUpdateFor(500);
try {
await reorderAreaRegistryEntries(this.hass, areaOrder);
await reorderFloorRegistryEntries(this.hass, floorOrder);
} catch {
showToast(this, {
message: this.hass.localize(
"ui.panel.config.areas.picker.floor_reorder_failed"
),
});
// Revert on error
this._computeHierarchy();
}
}
private async _areaMoved(ev) {
ev.stopPropagation();
if (!this.hass || !this._hierarchy) {
@@ -561,14 +537,23 @@ export class HaConfigAreasDashboard extends LitElement {
const floor = (ev.currentTarget as any).floor;
switch (ev.detail.index) {
case 0:
this._editFloor(floor);
this._showReorderDialog();
break;
case 1:
this._editFloor(floor);
break;
case 2:
this._deleteFloor(floor);
break;
}
}
private _handleUnassignedAreasAction(ev: CustomEvent<ActionDetail>) {
if (ev.detail.index === 0) {
this._showReorderDialog();
}
}
private _createFloor() {
this._openFloorDialog();
}
@@ -598,6 +583,10 @@ export class HaConfigAreasDashboard extends LitElement {
this._openAreaDialog();
}
private _showReorderDialog() {
showAreasFloorsOrderDialog(this, {});
}
private _showHelp() {
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.areas.caption"),

View File

@@ -0,0 +1,17 @@
import { fireEvent } from "../../../common/dom/fire_event";
export interface AreasFloorsOrderDialogParams {}
export const loadAreasFloorsOrderDialog = () =>
import("./dialog-areas-floors-order");
export const showAreasFloorsOrderDialog = (
element: HTMLElement,
params: AreasFloorsOrderDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-areas-floors-order",
dialogImport: loadAreasFloorsOrderDialog,
dialogParams: params,
});
};

View File

@@ -1,3 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiAlertCircleCheck,
@@ -32,11 +33,11 @@ import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-automation-row";
import type { HaAutomationRow } from "../../../../components/ha-automation-row";
import "../../../../components/ha-card";
import "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-service-icon";
import "../../../../components/ha-tooltip";
import {
@@ -288,15 +289,12 @@ export default class HaAutomationActionRow extends LitElement {
</ha-tooltip>`
: nothing}
<ha-md-button-menu
quick
<ha-dropdown
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
anchor-corner="end-end"
menu-corner="start-end"
@wa-select=${this._handleDropdownSelect}
placement="bottom-end"
>
<ha-icon-button
slot="trigger"
@@ -304,30 +302,24 @@ export default class HaAutomationActionRow extends LitElement {
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item .clickAction=${this._runAction}>
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
<ha-dropdown-item value="run">
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize("ui.panel.config.automation.editor.actions.run")
)}
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._renameAction}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-dropdown-item>
<ha-dropdown-item value="rename" .disabled=${this.disabled}>
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)
)}
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateAction}
.disabled=${this.disabled}
>
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="duplicate" .disabled=${this.disabled}>
<ha-svg-icon
slot="start"
slot="icon"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
@@ -336,13 +328,10 @@ export default class HaAutomationActionRow extends LitElement {
"ui.panel.config.automation.editor.actions.duplicate"
)
)}
</ha-md-menu-item>
</ha-dropdown-item>
<ha-md-menu-item
.clickAction=${this._copyAction}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
<ha-dropdown-item value="copy" .disabled=${this.disabled}>
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
@@ -351,7 +340,6 @@ export default class HaAutomationActionRow extends LitElement {
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
@@ -362,13 +350,10 @@ export default class HaAutomationActionRow extends LitElement {
<span>C</span>
</span>`
)}
</ha-md-menu-item>
</ha-dropdown-item>
<ha-md-menu-item
.clickAction=${this._cutAction}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
<ha-dropdown-item value="cut" .disabled=${this.disabled}>
<ha-svg-icon slot="icon" .path=${mdiContentCut}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
@@ -377,7 +362,6 @@ export default class HaAutomationActionRow extends LitElement {
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
@@ -388,51 +372,48 @@ export default class HaAutomationActionRow extends LitElement {
<span>X</span>
</span>`
)}
</ha-md-menu-item>
</ha-dropdown-item>
${!this.optionsInSidebar
? html`
<ha-md-menu-item
.clickAction=${this._moveUp}
<ha-dropdown-item
value="move_up"
.disabled=${this.disabled || !!this.first}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_up"
)}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
<ha-svg-icon slot="icon" .path=${mdiArrowUp}></ha-svg-icon
></ha-dropdown-item>
<ha-dropdown-item
value="move_down"
.disabled=${this.disabled || !!this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
<ha-svg-icon slot="icon" .path=${mdiArrowDown}></ha-svg-icon
></ha-dropdown-item>
`
: nothing}
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
<ha-dropdown-item
value="toggle_yaml_mode"
.disabled=${!this._uiModeAvailable || !!this._warnings}
>
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)
)}
</ha-md-menu-item>
</ha-dropdown-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<wa-divider></wa-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
<ha-dropdown-item value="disable" .disabled=${this.disabled}>
<ha-svg-icon
slot="start"
slot="icon"
.path=${this.action.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
@@ -443,15 +424,15 @@ export default class HaAutomationActionRow extends LitElement {
`ui.panel.config.automation.editor.actions.${this.action.enabled === false ? "enable" : "disable"}`
)
)}
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
</ha-dropdown-item>
<ha-dropdown-item
value="delete"
variant="danger"
.disabled=${this.disabled}
>
<ha-svg-icon
class="warning"
slot="start"
slot="icon"
.path=${mdiDelete}
></ha-svg-icon>
@@ -463,7 +444,6 @@ export default class HaAutomationActionRow extends LitElement {
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
@@ -478,8 +458,8 @@ export default class HaAutomationActionRow extends LitElement {
>
</span>`
)}
</ha-md-menu-item>
</ha-md-button-menu>
</ha-dropdown-item>
</ha-dropdown>
${!this.optionsInSidebar
? html`${this._warnings
@@ -890,6 +870,47 @@ export default class HaAutomationActionRow extends LitElement {
this._automationRowElement?.focus();
}
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
const action = ev.detail?.item?.value;
if (!action) {
return;
}
switch (action) {
case "run":
this._runAction();
break;
case "rename":
this._renameAction();
break;
case "duplicate":
this._duplicateAction();
break;
case "copy":
this._copyAction();
break;
case "cut":
this._cutAction();
break;
case "move_up":
this._moveUp();
break;
case "move_down":
this._moveDown();
break;
case "toggle_yaml_mode":
this._toggleYamlMode(ev.target as HTMLElement);
break;
case "disable":
this._onDisable();
break;
case "delete":
this._onDelete();
break;
}
}
static styles = [rowStyles, overflowStyles];
}

View File

@@ -64,8 +64,15 @@ export class HaStopAction extends LitElement implements ActionElement {
private _responseChanged(ev: Event) {
ev.stopPropagation();
const newAction = { ...this.action };
const newValue = (ev.target as any).value;
if (newValue) {
newAction.response_variable = newValue;
} else {
delete newAction.response_variable;
}
fireEvent(this, "value-changed", {
value: { ...this.action, response_variable: (ev.target as any).value },
value: newAction,
});
}

View File

@@ -16,6 +16,7 @@ import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { mainWindow } from "../../../common/dom/get_main_window";
import { computeAreaName } from "../../../common/entity/compute_area_name";
import { computeDeviceName } from "../../../common/entity/compute_device_name";
import { computeDomain } from "../../../common/entity/compute_domain";
@@ -96,7 +97,7 @@ import {
fetchIntegrationManifests,
} from "../../../data/integration";
import type { LabelRegistryEntry } from "../../../data/label_registry";
import { subscribeLabFeatures } from "../../../data/labs";
import { subscribeLabFeature } from "../../../data/labs";
import {
TARGET_SEPARATOR,
getConditionsForTarget,
@@ -118,7 +119,6 @@ import type { HomeAssistant } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import "./add-automation-element/ha-automation-add-from-target";
import type HaAutomationAddFromTarget from "./add-automation-element/ha-automation-add-from-target";
import "./add-automation-element/ha-automation-add-items";
import "./add-automation-element/ha-automation-add-search";
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
@@ -216,10 +216,6 @@ class DialogAddAutomationElement
// #endregion state
// #region queries
@query("ha-automation-add-from-target")
private _targetPickerElement?: HaAutomationAddFromTarget;
@query("ha-automation-add-items")
private _itemsListElement?: HTMLDivElement;
@@ -232,7 +228,7 @@ class DialogAddAutomationElement
private _unsub?: Promise<UnsubscribeFunc>;
private _unsubscribeLabFeatures?: UnsubscribeFunc;
private _unsubscribeLabFeatures?: Promise<UnsubscribeFunc>;
private _configEntryLookup: Record<string, ConfigEntry> = {};
@@ -285,22 +281,24 @@ class DialogAddAutomationElement
this._fetchManifests();
this._calculateUsedDomains();
this._unsubscribeLabFeatures = subscribeLabFeatures(
this._unsubscribeLabFeatures = subscribeLabFeature(
this.hass.connection,
(features) => {
this._newTriggersAndConditions =
features.find(
(feature) =>
feature.domain === "automation" &&
feature.preview_feature === "new_triggers_conditions"
)?.enabled ?? false;
this._tab =
this._newTriggersAndConditions && this._params?.type !== "condition"
? "targets"
: "groups";
"automation",
"new_triggers_conditions",
(feature) => {
this._newTriggersAndConditions = feature.enabled;
this._tab = this._newTriggersAndConditions ? "targets" : "groups";
}
);
// add initial dialog view state to history
mainWindow.history.pushState(
{
dialogData: {},
},
""
);
if (this._params?.type === "action") {
this.hass.loadBackendTranslation("services");
getServiceIcons(this.hass);
@@ -321,7 +319,41 @@ class DialogAddAutomationElement
this._bottomSheetMode = this._narrow;
}
public closeDialog() {
public closeDialog(historyState?: any) {
// prevent closing when come from popstate event and root level isn't active
if (
this._open &&
historyState &&
(this._selectedTarget || this._selectedGroup)
) {
if (historyState.dialogData?.target) {
this._selectedTarget = historyState.dialogData.target;
this._getItemsByTarget();
this._tab = "targets";
return false;
}
if (historyState.dialogData?.group) {
this._selectedCollectionIndex = historyState.dialogData.collectionIndex;
this._selectedGroup = historyState.dialogData.group;
this._tab = "groups";
return false;
}
// return to home on mobile
if (this._narrow) {
this._selectedTarget = undefined;
this._selectedGroup = undefined;
return false;
}
}
// if dialog is closed, but root level isn't active, clean up history state
if (mainWindow.history.state?.dialogData) {
this._open = false;
mainWindow.history.back();
return false;
}
this.removeKeyboardShortcuts();
this._unsubscribe();
if (this._params) {
@@ -390,7 +422,7 @@ class DialogAddAutomationElement
this._unsub = undefined;
}
if (this._unsubscribeLabFeatures) {
this._unsubscribeLabFeatures();
this._unsubscribeLabFeatures.then((unsub) => unsub());
this._unsubscribeLabFeatures = undefined;
}
}
@@ -408,7 +440,7 @@ class DialogAddAutomationElement
return html`
<ha-bottom-sheet
.open=${this._open}
@closed=${this.closeDialog}
@closed=${this._handleClosed}
flexcontent
>
${this._renderContent()}
@@ -420,7 +452,7 @@ class DialogAddAutomationElement
<ha-wa-dialog
width="large"
.open=${this._open}
@closed=${this.closeDialog}
@closed=${this._handleClosed}
flexcontent
>
${this._renderContent()}
@@ -558,8 +590,7 @@ class DialogAddAutomationElement
interactive
type="button"
class="paste"
.value=${PASTE_VALUE}
@click=${this._selected}
@click=${this._paste}
>
<div class="shortcut-label">
<div class="label">
@@ -652,6 +683,7 @@ class DialogAddAutomationElement
<ha-automation-add-items
.hass=${this.hass}
.items=${this._getItems()}
.scrollable=${!this._narrow}
.error=${this._tab === "targets" && this._loadItemsError
? this.hass.localize(
"ui.panel.config.automation.editor.load_target_items_failed"
@@ -730,15 +762,26 @@ class DialogAddAutomationElement
);
if (targetId) {
if (targetType === "area" && this.hass.areas[targetId]?.floor_id) {
const floorId = this.hass.areas[targetId].floor_id;
subtitle = computeFloorName(this.hass.floors[floorId]) || floorId;
}
if (targetType === "device" && this.hass.devices[targetId]?.area_id) {
const areaId = this.hass.devices[targetId].area_id;
subtitle = computeAreaName(this.hass.areas[areaId]) || areaId;
}
if (targetType === "entity" && this.hass.states[targetId]) {
if (targetType === "area") {
const floorId = this.hass.areas[targetId]?.floor_id;
if (floorId) {
subtitle = computeFloorName(this.hass.floors[floorId]) || floorId;
} else {
subtitle = this.hass.localize(
"ui.panel.config.automation.editor.other_areas"
);
}
} else if (targetType === "device") {
const areaId = this.hass.devices[targetId]?.area_id;
if (areaId) {
subtitle = computeAreaName(this.hass.areas[areaId]) || areaId;
} else {
const device = this.hass.devices[targetId];
subtitle = this.hass.localize(
`ui.panel.config.automation.editor.${device?.entry_type === "service" ? "services" : "unassigned_devices"}`
);
}
} else if (targetType === "entity" && this.hass.states[targetId]) {
const entity = this.hass.entities[targetId];
if (entity && !entity.device_id && !entity.area_id) {
const domain = targetId.split(".", 2)[0];
@@ -763,10 +806,10 @@ class DialogAddAutomationElement
.join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
}
}
}
if (subtitle) {
return html`<span slot="subtitle">${subtitle}</span>`;
if (subtitle) {
return html`<span slot="subtitle">${subtitle}</span>`;
}
}
}
@@ -1353,6 +1396,61 @@ class DialogAddAutomationElement
this._labelRegistry?.find(({ label_id }) => label_id === labelId)
);
private _getDomainType(domain: string) {
return ENTITY_DOMAINS_MAIN.has(domain) ||
(this._manifests?.[domain].integration_type === "entity" &&
!ENTITY_DOMAINS_OTHER.has(domain))
? "dynamicGroups"
: this._manifests?.[domain].integration_type === "helper"
? "helpers"
: "other";
}
private _sortDomainsByCollection(
type: AddAutomationElementDialogParams["type"],
entries: [
string,
{ title: string; items: AddAutomationElementListItem[] },
][]
): { title: string; items: AddAutomationElementListItem[] }[] {
const order: string[] = [];
TYPES[type].collections.forEach((collection) => {
order.push(...Object.keys(collection.groups));
});
return entries
.sort((a, b) => {
const domainA = a[0];
const domainB = b[0];
if (order.includes(domainA) && order.includes(domainB)) {
return order.indexOf(domainA) - order.indexOf(domainB);
}
let typeA = domainA;
let typeB = domainB;
if (!order.includes(domainA)) {
typeA = this._getDomainType(domainA);
}
if (!order.includes(domainB)) {
typeB = this._getDomainType(domainB);
}
if (typeA === typeB) {
return stringCompare(
a[1].title,
b[1].title,
this.hass.locale.language
);
}
return order.indexOf(typeA) - order.indexOf(typeB);
})
.map((entry) => entry[1]);
}
// #endregion data
// #region data memoize
@@ -1368,12 +1466,12 @@ class DialogAddAutomationElement
private _getAreaEntityLookupMemoized = memoizeOne(
(entities: HomeAssistant["entities"]) =>
getAreaEntityLookup(Object.values(entities), true)
getAreaEntityLookup(Object.values(entities))
);
private _getDeviceEntityLookupMemoized = memoizeOne(
(entities: HomeAssistant["entities"]) =>
getDeviceEntityLookup(Object.values(entities), true)
getDeviceEntityLookup(Object.values(entities))
);
private _extractTypeAndIdFromTarget = memoizeOne(
@@ -1438,8 +1536,9 @@ class DialogAddAutomationElement
);
});
return Object.values(items).sort((a, b) =>
stringCompare(a.title, b.title, this.hass.locale.language)
return this._sortDomainsByCollection(
this._params!.type,
Object.entries(items)
);
}
@@ -1548,8 +1647,9 @@ class DialogAddAutomationElement
);
});
return Object.values(items).sort((a, b) =>
stringCompare(a.title, b.title, this.hass.locale.language)
return this._sortDomainsByCollection(
this._params!.type,
Object.entries(items)
);
}
@@ -1580,8 +1680,9 @@ class DialogAddAutomationElement
);
});
return Object.values(items).sort((a, b) =>
stringCompare(a.title, b.title, this.hass.locale.language)
return this._sortDomainsByCollection(
this._params!.type,
Object.entries(items)
);
}
@@ -1594,11 +1695,7 @@ class DialogAddAutomationElement
}
private _back() {
if (this._selectedTarget) {
this._targetPickerElement?.navigateBack();
return;
}
this._selectedGroup = undefined;
mainWindow.history.back();
}
private _groupSelected(ev) {
@@ -1610,11 +1707,26 @@ class DialogAddAutomationElement
}
this._selectedGroup = group.value;
this._selectedCollectionIndex = ev.currentTarget.index;
mainWindow.history.pushState(
{
dialogData: {
group: this._selectedGroup,
collectionIndex: this._selectedCollectionIndex,
},
},
""
);
requestAnimationFrame(() => {
this._itemsListElement?.scrollTo(0, 0);
});
}
private _paste() {
this._params!.add(PASTE_VALUE);
this.closeDialog();
}
private _selected(ev: CustomEvent<{ value: string }>) {
let target: HassServiceTarget | undefined;
if (
@@ -1634,6 +1746,14 @@ class DialogAddAutomationElement
this._targetItems = undefined;
this._loadItemsError = false;
this._selectedTarget = ev.detail.value;
mainWindow.history.pushState(
{
dialogData: {
target: this._selectedTarget,
},
},
""
);
requestAnimationFrame(() => {
if (this._narrow) {
@@ -1678,14 +1798,19 @@ class DialogAddAutomationElement
}
if (this._params!.type === "action") {
const items = await getServicesForTarget(
const items: string[] = await getServicesForTarget(
this.hass.callWS,
this._selectedTarget
);
const filteredItems = items.filter(
// homeassistant services are too generic to be applied on the selected target
(service) => !service.startsWith("homeassistant.")
);
this._targetItems = this._getDomainGroupedActionListItems(
this.hass.localize,
items
filteredItems
);
}
} catch (err) {
@@ -1748,6 +1873,10 @@ class DialogAddAutomationElement
this._tab = "targets";
}
private _handleClosed() {
this.closeDialog();
}
// #region interaction
// #region render helpers
@@ -1913,7 +2042,7 @@ class DialogAddAutomationElement
ha-wa-dialog {
--dialog-content-padding: var(--ha-space-0);
--ha-dialog-min-height: min(
648px,
800px,
calc(
100vh - max(
var(--safe-area-inset-bottom),
@@ -1922,7 +2051,7 @@ class DialogAddAutomationElement
)
);
--ha-dialog-min-height: min(
648px,
800px,
calc(
100dvh - max(
var(--safe-area-inset-bottom),
@@ -2015,7 +2144,7 @@ class DialogAddAutomationElement
min-height: 160px;
}
.content.column ha-automation-add-from-target {
overflow: hidden;
overflow: clip;
}
ha-wa-dialog ha-automation-add-items {

View File

@@ -553,9 +553,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
area.icon,
] as [string, string, string | undefined, string | undefined];
})
.sort(([, nameA], [, nameB]) =>
stringCompare(nameA, nameB, this.hass.locale.language)
)
.map(([areaTargetId, areaName, floorId, areaIcon]) => {
const { open, devices, entities } =
this._entries[`floor${TARGET_SEPARATOR}${floorId || ""}`].areas![
@@ -708,7 +705,11 @@ export default class HaAutomationAddFromTarget extends LitElement {
this.floors
);
const label = entityName || deviceName || entityId;
let label = entityName || deviceName || entityId;
if (this.entities[entityId]?.hidden) {
label += ` (${this.localize("ui.panel.config.automation.editor.entity_hidden")})`;
}
return [entityId, label, stateObj] as [string, string, HassEntity];
})
@@ -837,12 +838,12 @@ export default class HaAutomationAddFromTarget extends LitElement {
private _getAreaEntityLookupMemoized = memoizeOne(
(entities: HomeAssistant["entities"]) =>
getAreaEntityLookup(Object.values(entities), true)
getAreaEntityLookup(Object.values(entities))
);
private _getDeviceEntityLookupMemoized = memoizeOne(
(entities: HomeAssistant["entities"]) =>
getDeviceEntityLookup(Object.values(entities), true)
getDeviceEntityLookup(Object.values(entities))
);
private _getSelectedTargetId = memoizeOne(
@@ -910,6 +911,10 @@ export default class HaAutomationAddFromTarget extends LitElement {
const services: Record<string, Level3Entries> = {};
unassignedDevices.forEach(({ id: deviceId, entry_type }) => {
const device = this.devices[deviceId];
if (!device || device.disabled_by) {
return;
}
const deviceEntry = {
open: false,
entities:
@@ -1011,6 +1016,10 @@ export default class HaAutomationAddFromTarget extends LitElement {
const devices: Record<string, Level3Entries> = {};
referenced_devices.forEach(({ id: deviceId }) => {
const device = this.devices[deviceId];
if (!device || device.disabled_by) {
return;
}
devices[deviceId] = {
open: false,
entities:
@@ -1382,92 +1391,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
);
}
public navigateBack() {
if (!this.value) {
return;
}
const valueType = Object.keys(this.value)[0].replace("_id", "");
const valueId = this.value[`${valueType}_id`];
if (
valueType === "floor" ||
valueType === "label" ||
(!valueId &&
(valueType === "device" ||
valueType === "helper" ||
valueType === "service" ||
valueType === "area"))
) {
fireEvent(this, "value-changed", { value: undefined });
return;
}
if (valueType === "area") {
fireEvent(this, "value-changed", {
value: { floor_id: this.areas[valueId].floor_id || undefined },
});
return;
}
if (valueType === "device") {
if (
!this.devices[valueId].area_id &&
this.devices[valueId].entry_type === "service"
) {
fireEvent(this, "value-changed", {
value: { service_id: undefined },
});
return;
}
fireEvent(this, "value-changed", {
value: { area_id: this.devices[valueId].area_id || undefined },
});
return;
}
if (valueType === "entity" && valueId) {
const deviceId = this.entities[valueId].device_id;
if (deviceId) {
fireEvent(this, "value-changed", {
value: { device_id: deviceId },
});
return;
}
const areaId = this.entities[valueId].area_id;
if (areaId) {
fireEvent(this, "value-changed", {
value: { area_id: areaId },
});
return;
}
const domain = valueId.split(".", 2)[0];
const manifest = this.manifests ? this.manifests[domain] : undefined;
if (manifest?.integration_type === "helper") {
fireEvent(this, "value-changed", {
value: { [`helper_${domain}_id`]: undefined },
});
return;
}
fireEvent(this, "value-changed", {
value: { [`entity_${domain}_id`]: undefined },
});
}
if (valueType.startsWith("helper_") || valueType.startsWith("entity_")) {
fireEvent(this, "value-changed", {
value: {
[`${valueType.startsWith("helper_") ? "helper" : "device"}_id`]:
undefined,
},
});
}
}
private _expandHeight() {
this._fullHeight = true;
this.style.setProperty("--max-height", "none");

View File

@@ -60,6 +60,8 @@ export class HaAutomationAddItems extends LitElement {
@property({ type: Boolean, attribute: "tooltip-description" })
public tooltipDescription = false;
@property({ type: Boolean, reflect: true }) scrollable = false;
@state() private _itemsScrolled = false;
@query(".items")
@@ -260,11 +262,12 @@ export class HaAutomationAddItems extends LitElement {
:host {
display: flex;
}
:host([scrollable]) .items {
overflow: auto;
}
.items {
display: flex;
flex-direction: column;
overflow: auto;
flex: 1;
}
.items.blank {
@@ -273,7 +276,7 @@ export class HaAutomationAddItems extends LitElement {
align-items: center;
color: var(--ha-color-text-secondary);
padding: var(--ha-space-0);
margin: var(--ha-space-3) var(--ha-space-4)
margin: var(--ha-space-0) var(--ha-space-4)
max(var(--safe-area-inset-bottom), var(--ha-space-3));
line-height: var(--ha-line-height-expanded);
justify-content: center;
@@ -306,7 +309,7 @@ export class HaAutomationAddItems extends LitElement {
.items .item-headline {
display: flex;
align-items: center;
gap: var(--ha-space-1);
gap: var(--ha-space-2);
min-height: var(--ha-space-9);
flex-wrap: wrap;
}
@@ -366,12 +369,16 @@ export class HaAutomationAddItems extends LitElement {
}
.selected-target state-badge {
--mdc-icon-size: 20px;
--mdc-icon-size: 24px;
}
.selected-target state-badge,
.selected-target ha-floor-icon {
display: flex;
height: 32px;
width: 32px;
align-items: center;
}
.selected-target ha-domain-icon {
width: 24px;
height: 24px;
filter: grayscale(100%);
}
`;

View File

@@ -1,3 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
@@ -33,11 +34,11 @@ import "../../../../components/ha-automation-row";
import type { HaAutomationRow } from "../../../../components/ha-automation-row";
import "../../../../components/ha-card";
import "../../../../components/ha-condition-icon";
import "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import type {
AutomationClipboard,
Condition,
@@ -194,15 +195,12 @@ export default class HaAutomationConditionRow extends LitElement {
<slot name="icons" slot="icons"></slot>
<ha-md-button-menu
quick
<ha-dropdown
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
anchor-corner="end-end"
menu-corner="start-end"
@wa-select=${this._handleDropdownSelect}
placement="bottom-end"
>
<ha-icon-button
slot="trigger"
@@ -211,34 +209,28 @@ export default class HaAutomationConditionRow extends LitElement {
>
</ha-icon-button>
<ha-md-menu-item .clickAction=${this._testCondition}>
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
<ha-dropdown-item value="test">
<ha-svg-icon slot="icon" .path=${mdiFlask}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.conditions.test"
)
)}
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._renameCondition}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-dropdown-item>
<ha-dropdown-item value="rename" .disabled=${this.disabled}>
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.conditions.rename"
)
)}
</ha-md-menu-item>
</ha-dropdown-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<wa-divider></wa-divider>
<ha-md-menu-item
.clickAction=${this._duplicateCondition}
.disabled=${this.disabled}
>
<ha-dropdown-item value="duplicate" .disabled=${this.disabled}>
<ha-svg-icon
slot="start"
slot="icon"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
${this._renderOverflowLabel(
@@ -246,13 +238,10 @@ export default class HaAutomationConditionRow extends LitElement {
"ui.panel.config.automation.editor.actions.duplicate"
)
)}
</ha-md-menu-item>
</ha-dropdown-item>
<ha-md-menu-item
.clickAction=${this._copyCondition}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon
<ha-dropdown-item value="copy" .disabled=${this.disabled}>
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon
>${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
@@ -261,7 +250,6 @@ export default class HaAutomationConditionRow extends LitElement {
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
@@ -272,13 +260,10 @@ export default class HaAutomationConditionRow extends LitElement {
<span>C</span>
</span>`
)}
</ha-md-menu-item>
</ha-dropdown-item>
<ha-md-menu-item
.clickAction=${this._cutCondition}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon
<ha-dropdown-item value="cut" .disabled=${this.disabled}>
<ha-svg-icon slot="icon" .path=${mdiContentCut}></ha-svg-icon
>${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
@@ -287,7 +272,6 @@ export default class HaAutomationConditionRow extends LitElement {
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
@@ -298,48 +282,45 @@ export default class HaAutomationConditionRow extends LitElement {
<span>X</span>
</span>`
)}
</ha-md-menu-item>
</ha-dropdown-item>
${!this.optionsInSidebar
? html`
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || this.first}
<ha-dropdown-item
value="move_up"
.disabled=${this.disabled || !!this.first}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_up"
)}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || this.last}
<ha-svg-icon slot="icon" .path=${mdiArrowUp}></ha-svg-icon
></ha-dropdown-item>
<ha-dropdown-item
value="move_down"
.disabled=${this.disabled || !!this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
<ha-svg-icon slot="icon" .path=${mdiArrowDown}></ha-svg-icon
></ha-dropdown-item>
`
: nothing}
<ha-md-menu-item .clickAction=${this._toggleYamlMode}>
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
<ha-dropdown-item value="toggle_yaml_mode">
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)
)}
</ha-md-menu-item>
</ha-dropdown-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<wa-divider></wa-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
<ha-dropdown-item value="disable" .disabled=${this.disabled}>
<ha-svg-icon
slot="start"
slot="icon"
.path=${this.condition.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
@@ -350,15 +331,15 @@ export default class HaAutomationConditionRow extends LitElement {
`ui.panel.config.automation.editor.actions.${this.condition.enabled === false ? "enable" : "disable"}`
)
)}
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
</ha-dropdown-item>
<ha-dropdown-item
variant="danger"
value="delete"
.disabled=${this.disabled}
>
<ha-svg-icon
class="warning"
slot="start"
slot="icon"
.path=${mdiDelete}
></ha-svg-icon>
${this._renderOverflowLabel(
@@ -369,7 +350,6 @@ export default class HaAutomationConditionRow extends LitElement {
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
@@ -384,8 +364,8 @@ export default class HaAutomationConditionRow extends LitElement {
>
</span>`
)}
</ha-md-menu-item>
</ha-md-button-menu>
</ha-dropdown-item>
</ha-dropdown>
${!this.optionsInSidebar
? html`${this._warnings
? html`<ha-automation-editor-warning
@@ -837,6 +817,47 @@ export default class HaAutomationConditionRow extends LitElement {
this._automationRowElement?.focus();
}
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
const action = ev.detail?.item?.value;
if (!action) {
return;
}
switch (action) {
case "test":
this._testCondition();
break;
case "rename":
this._renameCondition();
break;
case "duplicate":
this._duplicateCondition();
break;
case "copy":
this._copyCondition();
break;
case "cut":
this._cutCondition();
break;
case "move_up":
this._moveUp();
break;
case "move_down":
this._moveDown();
break;
case "toggle_yaml_mode":
this._toggleYamlMode(ev.target as HTMLElement);
break;
case "disable":
this._onDisable();
break;
case "delete":
this._onDelete();
break;
}
}
static get styles(): CSSResultGroup {
return [
rowStyles,

View File

@@ -28,7 +28,7 @@ import {
CONDITION_BUILDING_BLOCKS,
subscribeConditions,
} from "../../../../data/condition";
import { subscribeLabFeatures } from "../../../../data/labs";
import { subscribeLabFeature } from "../../../../data/labs";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types";
import {
@@ -90,14 +90,14 @@ export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
protected hassSubscribe() {
return [
subscribeLabFeatures(this.hass!.connection, (features) => {
this._newTriggersAndConditions =
features.find(
(feature) =>
feature.domain === "automation" &&
feature.preview_feature === "new_triggers_conditions"
)?.enabled ?? false;
}),
subscribeLabFeature(
this.hass!.connection,
"automation",
"new_triggers_conditions",
(feature) => {
this._newTriggersAndConditions = feature.enabled;
}
),
];
}

View File

@@ -124,9 +124,9 @@ export class HaPlatformCondition extends LitElement {
const hasOptional = Boolean(
conditionDesc?.fields &&
Object.values(conditionDesc.fields).some((field) =>
showOptionalToggle(field)
)
Object.values(conditionDesc.fields).some((field) =>
showOptionalToggle(field)
)
);
return html`
@@ -393,6 +393,10 @@ export class HaPlatformCondition extends LitElement {
}
static styles = css`
:host {
display: block;
margin: 0px calc(-1 * var(--ha-space-4));
}
ha-settings-row {
padding: 0 var(--ha-space-4);
}

View File

@@ -1,3 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
@@ -23,20 +24,22 @@ import {
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { property, query, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
import { transform } from "../../../common/decorators/transform";
import { fireEvent } from "../../../common/dom/fire_event";
import { goBack, navigate } from "../../../common/navigate";
import { promiseTimeout } from "../../../common/util/promise-timeout";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-fade-in";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
@@ -74,7 +77,6 @@ import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info
import "../../../layouts/hass-subpage";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
import { haStyle } from "../../../resources/styles";
import type { Entries, HomeAssistant, Route } from "../../../types";
import { isMac } from "../../../util/is_mac";
@@ -86,10 +88,10 @@ import {
type EntityRegistryUpdate,
showAutomationSaveDialog,
} from "./automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveTimeoutDialog } from "./automation-save-timeout-dialog/show-dialog-automation-save-timeout";
import "./blueprint-automation-editor";
import "./manual-automation-editor";
import type { HaManualAutomationEditor } from "./manual-automation-editor";
import { showAutomationSaveTimeoutDialog } from "./automation-save-timeout-dialog/show-dialog-automation-save-timeout";
declare global {
interface HTMLElementTagNameMap {
@@ -112,6 +114,7 @@ declare global {
}
}
@customElement("ha-automation-editor")
export class HaAutomationEditor extends PreventUnsavedMixin(
KeyboardShortcutMixin(LitElement)
) {
@@ -291,7 +294,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
</ha-button>
`
: ""}
<ha-button-menu slot="toolbar-icon">
<ha-dropdown
slot="toolbar-icon"
@wa-select=${this._handleDropdownSelect}
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
@@ -299,99 +305,73 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
></ha-icon-button>
${this._mode === "gui" && this.narrow
? html`<ha-list-item
graphic="icon"
@click=${this._undo}
? html`<ha-dropdown-item
value="undo"
.disabled=${!this._undoRedoController.canUndo}
>
${this.hass.localize("ui.common.undo")}
<ha-svg-icon slot="graphic" .path=${mdiUndo}></ha-svg-icon>
</ha-list-item>
<ha-list-item
graphic="icon"
@click=${this._redo}
<ha-svg-icon slot="icon" .path=${mdiUndo}></ha-svg-icon>
</ha-dropdown-item>
<ha-dropdown-item
value="redo"
.disabled=${!this._undoRedoController.canRedo}
>
${this.hass.localize("ui.common.redo")}
<ha-svg-icon slot="graphic" .path=${mdiRedo}></ha-svg-icon>
</ha-list-item>`
<ha-svg-icon slot="icon" .path=${mdiRedo}></ha-svg-icon>
</ha-dropdown-item>`
: nothing}
<ha-list-item
graphic="icon"
.disabled=${!stateObj}
@click=${this._showInfo}
>
<ha-dropdown-item .disabled=${!stateObj} value="info">
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
<ha-svg-icon
slot="graphic"
slot="icon"
.path=${mdiInformationOutline}
></ha-svg-icon>
</ha-list-item>
</ha-dropdown-item>
<ha-list-item
graphic="icon"
.disabled=${!stateObj}
@click=${this._showSettings}
>
<ha-dropdown-item .disabled=${!stateObj} value="settings">
${this.hass.localize(
"ui.panel.config.automation.picker.show_settings"
)}
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
</ha-dropdown-item>
<ha-list-item
graphic="icon"
.disabled=${!stateObj}
@click=${this._editCategory}
>
<ha-dropdown-item .disabled=${!stateObj} value="category">
${this.hass.localize(
`ui.panel.config.scene.picker.${this._registryEntry?.categories?.automation ? "edit_category" : "assign_category"}`
)}
<ha-svg-icon slot="graphic" .path=${mdiTag}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
</ha-dropdown-item>
<ha-list-item
graphic="icon"
.disabled=${!stateObj}
@click=${this._runActions}
>
<ha-dropdown-item .disabled=${!stateObj} value="run">
${this.hass.localize("ui.panel.config.automation.editor.run")}
<ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
</ha-dropdown-item>
${stateObj && this.narrow
? html`<a
href="/config/automation/trace/${encodeURIComponent(
this._config.id!
)}"
>
<ha-list-item graphic="icon">
${this.hass.localize(
"ui.panel.config.automation.editor.show_trace"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiTransitConnection}
></ha-svg-icon>
</ha-list-item>
</a>`
? html`<ha-dropdown-item value="trace">
${this.hass.localize(
"ui.panel.config.automation.editor.show_trace"
)}
<ha-svg-icon
slot="icon"
.path=${mdiTransitConnection}
></ha-svg-icon>
</ha-dropdown-item>`
: nothing}
<ha-list-item
graphic="icon"
@click=${this._promptAutomationAlias}
<ha-dropdown-item
value="rename"
.disabled=${this._readOnly ||
!this.automationId ||
this._mode === "yaml"}
>
${this.hass.localize("ui.panel.config.automation.editor.rename")}
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
</ha-dropdown-item>
${!useBlueprint
? html`
<ha-list-item
graphic="icon"
<ha-dropdown-item
@click=${this._promptAutomationMode}
.disabled=${this._readOnly || this._mode === "yaml"}
>
@@ -399,18 +379,17 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
"ui.panel.config.automation.editor.change_mode"
)}
<ha-svg-icon
slot="graphic"
slot="icon"
.path=${mdiDebugStepOver}
></ha-svg-icon>
</ha-list-item>
</ha-dropdown-item>
`
: nothing}
<ha-list-item
.disabled=${this._blueprintConfig ||
<ha-dropdown-item
.disabled=${!!this._blueprintConfig ||
(!this._readOnly && !this.automationId)}
graphic="icon"
@click=${this._duplicate}
value="duplicate"
>
${this.hass.localize(
this._readOnly
@@ -418,74 +397,60 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
: "ui.panel.config.automation.editor.duplicate"
)}
<ha-svg-icon
slot="graphic"
slot="icon"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
</ha-list-item>
</ha-dropdown-item>
${useBlueprint
? html`
<ha-list-item
graphic="icon"
@click=${this._takeControl}
<ha-dropdown-item
value="take_control"
.disabled=${this._readOnly}
>
${this.hass.localize(
"ui.panel.config.automation.editor.take_control"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiFileEdit}
></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="icon" .path=${mdiFileEdit}></ha-svg-icon>
</ha-dropdown-item>
`
: nothing}
<ha-list-item
graphic="icon"
@click=${this._mode === "gui"
? this._switchYamlMode
: this._switchUiMode}
>
<ha-dropdown-item value="toggle_yaml_mode">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${this._mode === "gui" ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="graphic" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-list-item>
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-dropdown-item>
<li divider role="separator"></li>
<wa-divider></wa-divider>
<ha-list-item
graphic="icon"
.disabled=${!stateObj}
@click=${this._toggle}
>
<ha-dropdown-item .disabled=${!stateObj} value="disable">
${stateObj?.state === "off"
? this.hass.localize("ui.panel.config.automation.editor.enable")
: this.hass.localize("ui.panel.config.automation.editor.disable")}
<ha-svg-icon
slot="graphic"
slot="icon"
.path=${stateObj?.state === "off"
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-list-item>
</ha-dropdown-item>
<ha-list-item
<ha-dropdown-item
.disabled=${!this.automationId}
class=${classMap({ warning: Boolean(this.automationId) })}
graphic="icon"
@click=${this._deleteConfirm}
.variant=${this.automationId ? "danger" : "default"}
value="delete"
>
${this.hass.localize("ui.panel.config.automation.picker.delete")}
<ha-svg-icon
class=${classMap({ warning: Boolean(this.automationId) })}
slot="graphic"
slot="icon"
.path=${mdiDelete}
>
</ha-svg-icon>
</ha-list-item>
</ha-button-menu>
</ha-dropdown-item>
</ha-dropdown>
<div
class=${this._mode === "yaml" ? "yaml-mode" : ""}
@subscribe-automation-config=${this._subscribeAutomationConfig}
@@ -1248,6 +1213,63 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this._undoRedoController.redo();
}
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
const action = ev.detail?.item?.value;
if (!action) {
return;
}
switch (action) {
case "undo":
this._undo();
break;
case "redo":
this._redo();
break;
case "info":
this._showInfo();
break;
case "settings":
this._showSettings();
break;
case "category":
this._editCategory();
break;
case "run":
this._runActions();
break;
case "rename":
this._promptAutomationAlias();
break;
case "change_mode":
this._promptAutomationMode();
break;
case "duplicate":
this._duplicate();
break;
case "take_control":
this._takeControl();
break;
case "toggle_yaml_mode":
if (this._mode === "gui") {
this._switchYamlMode();
break;
}
this._switchUiMode();
break;
case "disable":
this._toggle();
break;
case "delete":
this._deleteConfirm();
break;
case "trace":
this._showTrace();
break;
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -1300,13 +1322,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
margin-inline-end: 8px;
margin-inline-start: initial;
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
ha-button-menu a {
text-decoration: none;
color: var(--primary-color);
}
h1 {
margin: 0;
}
@@ -1339,5 +1354,3 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
];
}
}
customElements.define("ha-automation-editor", HaAutomationEditor);

View File

@@ -35,6 +35,8 @@ import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify";
import "../../../components/ha-tooltip";
import type { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
@@ -327,14 +329,19 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
const date = new Date(automation.last_triggered);
const now = new Date();
const dayDifference = differenceInDays(now, date);
const formattedTime = formatShortDateTimeWithConditionalYear(
date,
this.hass.locale,
this.hass.config
);
const elementId = "last-triggered-" + slugify(automation.entity_id);
return html`
${dayDifference > 3
? formatShortDateTimeWithConditionalYear(
date,
this.hass.locale,
this.hass.config
)
: relativeTime(date, locale)}
? formattedTime
: html`
<ha-tooltip for=${elementId}>${formattedTime}</ha-tooltip>
<span id=${elementId}>${relativeTime(date, locale)}</span>
`}
`;
},
},

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