Compare commits

...

113 Commits

Author SHA1 Message Date
Petar Petrov
a6e2befcb7 Use entity icon in area card if there is only 1 entity 2025-09-08 14:42:53 +03:00
Aidan Timson
62714b2b68 Safe area: dialogs (#26814)
* dialogs: apply safe-area insets (content padding, header mobile insets, more-info top margin)

* Set default (40px prio if not set)

* Set default (default padding prio if not set)

* Set default to avoid issues

* Set on container

* Sort

* No longer needed

* No longer needed

* No longer needed

* Remove

* Restore

* Restore

* Move to padding

* Switch to margins, set min and max height

* Set default

* Account for insets to remove extra scrollbars

* Fix content for filter dialog

* Move margins outside of media check

* Use padding instead

* use min-width instead

* Use padding for just top and bottom

* Calculate lit-virtualizer to include safe areas

* Calculate lit-virtualizer to include safe areas

* Fix double scrollbar from previous

* Remove calculation

* Default

* Remove double calculation

* Remove double calculation
2025-09-08 09:39:27 +02:00
renovate[bot]
07fdd5b7af Update vaadinWebComponents monorepo to v24.8.7 (#26936)
* Update vaadinWebComponents monorepo to v24.8.7

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-08 07:35:30 +00:00
Phil White
720f435987 Fix refresh button appending logs instead of clearing them (#26875) 2025-09-08 09:18:05 +02:00
Yosi Levy
52061d6c1a Variour RTL fixes for automation and others (#26891) 2025-09-08 09:13:10 +02:00
Paulus Schoutsen
ae35164a57 Update clipboard copy template message in actions dev tool (#26925) 2025-09-08 08:59:11 +02:00
karwosts
d1c814bd6b Fix condition action in config flow dialog (#26929) 2025-09-08 08:33:02 +02:00
dependabot[bot]
bb50512c89 Bump softprops/action-gh-release from 2.3.2 to 2.3.3 (#26935)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.3.2 to 2.3.3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2.3.2...v2.3.3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 2.3.3
  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-09-08 08:20:26 +02:00
dependabot[bot]
0fae45edc9 Bump actions/setup-node from 4.4.0 to 5.0.0 (#26934)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.4.0 to 5.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.4.0...v5.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 08:20:00 +02:00
dependabot[bot]
0a8d3cc8fa Bump actions/setup-python from 5 to 6 (#26933)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 08:19:11 +02:00
dependabot[bot]
db09947a67 Bump actions/labeler from 5.0.0 to 6.0.1 (#26932)
Bumps [actions/labeler](https://github.com/actions/labeler) from 5.0.0 to 6.0.1.
- [Release notes](https://github.com/actions/labeler/releases)
- [Commits](https://github.com/actions/labeler/compare/v5.0.0...v6.0.1)

---
updated-dependencies:
- dependency-name: actions/labeler
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 08:18:47 +02:00
dependabot[bot]
5eb600726f Bump actions/stale from 9.1.0 to 10.0.0 (#26931)
Bumps [actions/stale](https://github.com/actions/stale) from 9.1.0 to 10.0.0.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v9.1.0...v10.0.0)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: 10.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 08:18:07 +02:00
dependabot[bot]
17a2e6e1f6 Bump actions/github-script from 7 to 8 (#26930)
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-08 08:17:35 +02:00
renovate[bot]
53e7959d54 Update dependency serve to v14.2.5 (#26922)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-07 20:21:02 +02:00
Aidan Timson
5f140c5fc4 Safe area: main content and onboarding (#26813)
* foundation: define --safe-area-content-inset-*; adjust drawer width; apply top inset to pre-login templates

* Set default for var

* Update demo template

* Set default to avoid issues

* Allow for left and right insets or 560px

* Remove
2025-09-07 13:25:29 +03:00
renovate[bot]
688b8e5229 Update dependency @types/leaflet.markercluster to v1.5.6 (#26911)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-06 14:59:59 +00:00
renovate[bot]
34b50b45a3 Update dependency @types/leaflet-draw to v1.0.13 (#26910)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-06 16:49:58 +02:00
karwosts
c17c9c6cc9 Remove an unused translation (#26908) 2025-09-05 22:39:01 -04:00
renovate[bot]
c9d72b5253 Update dependency @types/culori to v4.0.1 (#26899)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 23:22:30 +02:00
renovate[bot]
f5dbb28fb2 Update dependency typescript-eslint to v8.42.0 (#26897)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 23:22:22 +02:00
renovate[bot]
9acfe5c1cc Update dependency @codemirror/autocomplete to v6.18.7 (#26896)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 23:22:15 +02:00
Jan-Philipp Benecke
701cbcfbad Do not show state-card-content in new more-info dialog in gallery (#26889)
* Do not show `state-card-content` in new more-info dialog in gallery

* Use `computeShowNewMoreInfo`
2025-09-05 14:13:28 -04:00
renovate[bot]
38685127d2 Update dependency @codemirror/view to v6.38.2 (#26890)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-05 08:59:04 +03:00
renovate[bot]
4275f6c6b6 Update dependency lint-staged to v16.1.6 (#26885)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 19:33:55 +02:00
renovate[bot]
bfc186b612 Update dependency @rspack/core to v1.5.2 (#26880)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 16:41:29 +02:00
Paul Bottein
cb4d92ccf4 Add UI editor for trend graph feature (#26872) 2025-09-04 16:28:02 +02:00
Wendelin
1dc7256fb5 Translate del in shortcut dialog (#26865) 2025-09-04 16:11:38 +02:00
Wendelin
012e710e45 Test-device-tracker-entity-filter (#26881) 2025-09-04 16:08:38 +02:00
Paul Bottein
5abb7d0286 Fix testing condition in iOS (#26879) 2025-09-04 15:50:39 +02:00
Petar Petrov
ce74946706 Fix highlighting issue in Energy Sankey card (#26878) 2025-09-04 15:42:39 +02:00
Bram Kragten
bf351d67e9 Revert "Rename "Logbook" to "Activity" in user-facing strings" (#26867)
Revert "Rename "Logbook" to "Activity" in user-facing strings (#26619)"

This reverts commit 057fad55e8.
2025-09-04 11:14:16 +02:00
Wendelin
b75fa013d2 Fix script with fields fields in tile card button feature (#26866)
Check script fields in tile card button feature
2025-09-04 11:42:41 +03:00
Paul Bottein
2601b0d89c Display area with only sensors in climate view in home dashboard (#26863)
* Display area with only sensors in climate view in home dashboard

* Update home-climate-view-strategy.ts

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2025-09-04 10:11:35 +02:00
Mike Kelly
ef8410e121 Add MCF as another volume unit (#26400) 2025-09-04 09:30:13 +03:00
karwosts
37610703c8 Pass first weekday to recorder/statistic_during_period (#26229)
* Pass first weekday to recorder/statistic_during_period

* switch order
2025-09-04 09:27:40 +03:00
Aidan Timson
4efd9bba8a Fix weather more info forecast layout jump when loading data (#26764)
* Fix weather more info forecast layout jump when loading data

* Tabs arent always populated

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-04 06:04:56 +00:00
Paulus Schoutsen
e1fe7976d8 Do not allow filtering by automation labels on mobile (#26861) 2025-09-04 08:47:52 +03:00
Simon Lamon
53b96107d9 Home dashboard: Climate fix (#26856)
* Climate dashboard fix

* Update home-climate-view-strategy.ts
2025-09-03 18:55:33 +02:00
Bram Kragten
dcbad9e798 Dont align with clipboard on copy/cut automation item (#26855) 2025-09-03 14:19:02 +00:00
karwosts
26b3212c7e Enable sorting step flow menu options in user language (#26845)
* Enable sorting step flow menu options in user language

* Remove menu_sort_values

* Update src/dialogs/config-flow/step-flow-menu.ts

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

* prettier

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-09-03 17:03:09 +03:00
Paul Bottein
f3f4bcfe45 Rename history chart to trend graph (#26854)
* Rename history chart to trend graph

* rename element and prettier

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-09-03 10:56:05 +00:00
Bram Kragten
7cfa9de75f Bumped version to 20250903.0 2025-09-03 12:05:19 +02:00
Paul Bottein
b66dc8894d Improve responsive support for history graph feature (#26851) 2025-09-03 12:00:49 +02:00
Wendelin
14a7813ab0 Fix automation narrow bottom sheet close animation (#26850) 2025-09-03 09:45:13 +00:00
Bram Kragten
70a2ca281f Update shortcuts dialog (#26849) 2025-09-03 10:01:10 +02:00
Wendelin
d982f042fc Update device action button variant based on warning class (#26848) 2025-09-03 07:06:18 +00:00
J. Nick Koston
e60f9e326b Show scanner-type-specific power cycle instructions in Bluetooth UI (#26847)
* Show scanner-type-specific power cycle instructions in Bluetooth UI

* preen
2025-09-03 10:03:03 +03:00
J. Nick Koston
ba39d189e7 Show scanner current state in the Bluetooth UI (#26792)
* Show scanner current state in the Bluetooth UI

The state updates live as the scanner changes mode, and provides
a warning on what to do if the scanner gets in a bad state

* Show scanner current state in the Bluetooth UI

The state updates live as the scanner changes mode, and provides
a warning on what to do if the scanner gets in a bad state

* cleanup

* switch
2025-09-02 15:20:06 -03:00
Paul Bottein
78867b2cd9 Remove non numerical sensor and binary-sensor for history chart feature (#26838)
* Use hui-graph-base for chart history feature and remove non numerical sensor support

* Fix chart opacity
2025-09-02 14:38:06 -03:00
Paul Bottein
1dff42dc00 Change home summaries color (#26839) 2025-09-02 18:14:46 +02:00
Bram Kragten
0c9b3a0765 Remove expand all/collapse all options from overflow (#26837)
* remove expand all/collapse all options from overflow

* ignore
2025-09-02 16:26:44 +02:00
Bram Kragten
5a109c0ba8 Align box shadows (#26835)
* Align box shadows

* update colors
2025-09-02 14:15:48 +00:00
Bram Kragten
f3b214c30a Add new shortcuts to shortcuts dialog (#26836) 2025-09-02 17:08:32 +03:00
Petar Petrov
c49d2a0be6 Add support for option descriptions in config flows (#26825)
* Add support for option descriptions in config flows

* use `twoline`

* remove unused classes
2025-09-02 14:03:35 +00:00
Bram Kragten
c6c3170c1b Tweak automation row (#26834)
tweak automation row
2025-09-02 13:50:05 +00:00
Paul Bottein
0abb958aea Clean graph in security and climate view in home dashboard (#26833) 2025-09-02 14:58:56 +02:00
Bram Kragten
9d55843629 Fix scrolling items in the bottom into view (#26830) 2025-09-02 14:57:11 +02:00
Bram Kragten
b70d309297 add shadow when scrollable in automation bottom sheet, min height 50vh (#26828) 2025-09-02 14:12:48 +02:00
Simon Lamon
5961b71562 Home dashboard: Ensure temperature sensor entity exists (#26831) 2025-09-02 13:03:18 +02:00
karwosts
6942626f60 Differentate service vs device in more-info link (#26823) 2025-09-02 12:44:16 +02:00
Aidan Timson
069c0acdff Fix capitalization on data table filter button (#26829) 2025-09-02 10:44:01 +00:00
Bram Kragten
1f0d83190d prevent keyboard shortcuts with more modifier keys (#26826) 2025-09-02 09:39:54 +02:00
Wendelin
7c6c92c856 Automation-keybindings (#26762)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-09-01 21:41:50 +02:00
Robert Resch
eff352cde1 Warn about system performance when setting camera stream orientation (#26616) 2025-09-01 20:54:48 +02:00
Bram Kragten
62a75c188c Change drag selected styling (#26822) 2025-09-01 20:26:25 +02:00
Paul Bottein
4ffa6b6186 Force energy distribution to display the data of today (#26811) 2025-09-01 18:12:34 +02:00
Paul Bottein
25173cf605 Don't use ha-automation-row-selected to know if the item was selected (#26812) 2025-09-01 18:12:08 +02:00
Paul Bottein
3277d8e80b Add automation testing logic to sidebar (#26815) 2025-09-01 18:08:59 +02:00
Bram Kragten
55864fdc82 Update hover and hightlight states for automation rows (#26820) 2025-09-01 17:40:03 +02:00
Paul Bottein
d4d662ba46 Fix automation sidebar z index (#26810) 2025-09-01 17:21:36 +02:00
Paul Bottein
3ea5f508bb Remove box shadow for ha card in automation bottom sheet on mobile (#26817) 2025-09-01 17:19:09 +02:00
Paul Bottein
902a5dd678 Fix tag trigger when using it inside sidebar (#26819) 2025-09-01 17:18:18 +02:00
renovate[bot]
4a3ed62583 Update dependency @types/chromecast-caf-receiver to v6.0.24 (#26500)
* Update dependency @types/chromecast-caf-receiver to v6.0.24

* Use enum strings directly to make TS happy

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-09-01 14:53:10 +00:00
Paul Bottein
b4223e9e92 Use summary card in home dashboard (#26775) 2025-09-01 15:13:53 +02:00
Paul Bottein
99955d7818 Fix add dialog on dashboards on mobile (#26807) 2025-09-01 15:39:52 +03:00
Paul Bottein
f66768726c Update heading subtitle height to better fix grid (#26808) 2025-09-01 15:39:03 +03:00
Michel van de Wetering
0e4be02b2c Remove Hue bridge v1 image (#26674) 2025-09-01 15:27:50 +03:00
Bram Kragten
6daea23b3c Enable drag and drop on mobile for automations (#26805) 2025-09-01 14:09:13 +02:00
Petar Petrov
e21ddcb1e5 Handle negative values in History chart card feature (#26806) 2025-09-01 11:45:52 +00:00
Wendelin
ded85d9f27 Automation editor: fix yaml editor and editor switch (#26772) 2025-09-01 13:26:17 +02:00
Petar Petrov
eea43494da Use feature-color in History chart feature (#26802) 2025-09-01 11:34:44 +02:00
Norbert Rittel
9cf9ef927d Expand description for disabled services (#26782)
* Expand description for disabled services

* Drop "not added to Home Assistant"
2025-09-01 09:51:22 +03:00
Paulus Schoutsen
779ec4f583 Do not add graphs to every sensor tile card (#26793) 2025-08-31 15:11:41 +00:00
renovate[bot]
c541831cd2 Update dependency marked to v16.2.1 (#26785)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-31 17:07:02 +02:00
renovate[bot]
fd20c2a554 Update dependency @rspack/core to v1.5.1 (#26790)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-31 17:06:55 +02:00
uptimeZERO_
14fd29808c Bugfix: Fixed column widths in the dashboard config page (#26777)
Fixed column widths in dashboard config
2025-08-30 11:54:37 +00:00
Aidan Timson
7132ee157f Adjust states set state layout (#26765) 2025-08-30 13:11:41 +02:00
J. Nick Koston
1596b313d5 Add RSSI color gradient to Bluetooth visualization to identify connection problems (#26778) 2025-08-29 14:15:00 -05:00
karwosts
70cd68ded7 Don't use context for media selector with 'accept' (#26773) 2025-08-29 14:08:11 +00:00
renovate[bot]
cc91a6185e Update dependency @rspack/core to v1.5.0 (#26768)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-29 17:05:14 +03:00
Paul Bottein
1fd7c84583 Use fixed layout for automation sidebar to have scrollbar on the side (#26751)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-08-29 14:19:13 +02:00
Paul Bottein
0269540ee9 Add translations for home dashboard (#26763) 2025-08-29 14:17:55 +02:00
renovate[bot]
98390b3843 Update Yarn to v4.9.4 (#26760)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-29 08:12:28 +00:00
Paul Bottein
269628929c Fix alert z-index for automation and script (#26759) 2025-08-29 08:05:05 +00:00
Norbert Rittel
21fcc84afd Improve OAuth setup explanation (#26758)
* Improve OAuth setup explanation

* Add "your"

* Include "application" in headline
2025-08-29 09:59:31 +02:00
Wendelin
b86c1db83d Automation editor: fix focus handling (#26755) 2025-08-29 08:39:06 +02:00
renovate[bot]
a376670478 Update dependency typescript-eslint to v8.41.0 (#26757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-29 08:21:34 +02:00
renovate[bot]
72c62079aa Update dependency hls.js to v1.6.11 (#26756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-29 08:21:28 +02:00
Surya Prakash
9baf875585 Fix: Keep buttons active in picture-glance-card regardless of state (#26718)
* Fix: Keep buttons active in picture-glance-card regardless of state


Fixes #26683

**Problem:**
Buttons in the picture-glance-card were incorrectly disabled when their state was 'unknown', making them unclickable.

**Root Cause:**
The `disabled` property was being set based on the state value, but buttons should remain clickable regardless of their state since their state only reflects the last time they were pressed.

**Solution:**
Set `disabled=${false}` to ensure buttons are always active and clickable.

**Testing:**
Verified that buttons remain active and functional even when state is 'unknown'.

* Fix: Ensure buttons get state-on class when state is unknown

Fixes #26683

The "state-on" class was not being applied when button state was
"unknown" because "unknown" was included in the STATES_OFF set.
This ensures buttons appear active regardless of their state.

* Update hui-picture-glance-card.ts

* Update hui-picture-glance-card.ts

* Update hui-picture-glance-card.ts

* Update hui-picture-glance-card.ts

* Update hui-picture-glance-card.ts

* Update hui-picture-glance-card.ts

* Update hui-picture-glance-card.ts
2025-08-29 08:20:39 +02:00
Jonathan Keslin
175915218f Hide leading zero in clock card hour when using 12-hour time format (#26669)
Remove leading zero on hour on Clock card when displayed with 12-hour time format
2025-08-29 08:42:07 +03:00
Wendelin
25f25243bd Automation editor: overflow changes and style fixes (#26744)
* Fix for width also for blueprint editor

* Fix overflow menus

* Fix option icons

* Fix iOS bottom sheet flickering and drag handle

* Fix mobile padding

* Fix padding in sidebar

* Fix overflow placement

* Add new a11y sort

* Remove overflow in rows

* Fix a11y select row

* Revert "Fix a11y select row"

This reverts commit 54260c4a37.

* Fix option padding on blueprint

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2025-08-28 16:42:45 +00:00
karwosts
cf8d36b1f3 Hide 'options' from enum more info (#26736)
* Hide 'options' from enum more info

* restrict to specific domain and class
2025-08-28 18:08:33 +02:00
Aidan Timson
e3a9d754df Change loading detailed storage to use ha-alert with spinner (#26749)
* Change spinner overlay to use `ha-alert` with messaging

* Use spinner for icon slot
2025-08-28 18:05:53 +02:00
Petar Petrov
7b303a699b Increase disk usage request timeout (#26748) 2025-08-28 16:32:52 +03:00
renovate[bot]
ee45eb00f7 Update vaadinWebComponents monorepo to v24.8.6 (#26746)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-28 15:38:03 +03:00
Norbert Rittel
24a6aa2669 Different spelling fixes of user-facing strings (#26745)
* Different spelling fixes of user-facing strings

* Fix menu "Application credentials" menu item name
2025-08-28 15:37:06 +03:00
Aidan Timson
66d011cfb9 Move automation and script ha-alerts to main flow (#26735)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-08-28 13:53:15 +02:00
Wendelin
35895735cc Fix automation editor drag selected row in/out nested (#26740)
Fix nested sort
2025-08-28 13:30:14 +02:00
Norbert Rittel
e71df0b71a Improve section descriptions in Automation editor (#26741)
Replace "listed here" or "list of" with "added here"
2025-08-28 11:05:31 +02:00
Paulus Schoutsen
2a9846c598 Show configured area sensors on climate domain dashboard (#26737) 2025-08-28 08:34:45 +03:00
Paulus Schoutsen
b243d56bee Show binary sensors with graphs on the security dashboard (#26738) 2025-08-28 08:26:44 +03:00
J. Nick Koston
6a372a165e Improve network adapter configuration discoverability (#26734) 2025-08-27 09:59:23 -05:00
Paul Bottein
a5dad9bc22 Use entity picture for home favorite and update home dashboard icon (#26732)
* Add strategy icon

* Use entity picture for favorite
2025-08-27 16:33:48 +02:00
144 changed files with 5397 additions and 2540 deletions

View File

@@ -26,7 +26,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v5.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -61,7 +61,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v5.0.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -26,7 +26,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v5.0.0
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v5.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -60,7 +60,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v5.0.0
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v5.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -78,7 +78,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v5.0.0
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v5.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -102,7 +102,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v5.0.0
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v5.0.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -27,7 +27,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v5.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -62,7 +62,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v5.0.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v5.0.0
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v5.0.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v5.0.0
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v5.0.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Apply labels
uses: actions/labeler@v5.0.0
uses: actions/labeler@v6.0.1
with:
sync-labels: true

View File

@@ -23,12 +23,12 @@ jobs:
uses: actions/checkout@v5.0.0
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v5.0.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v5.0.0
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
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@v4.4.0
uses: actions/setup-node@v5.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -55,7 +55,7 @@ jobs:
script/release
- name: Upload release assets
uses: softprops/action-gh-release@v2.3.2
uses: softprops/action-gh-release@v2.3.3
with:
files: |
dist/*.whl
@@ -92,7 +92,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v5.0.0
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v5.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -107,7 +107,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@v2.3.2
uses: softprops/action-gh-release@v2.3.3
with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
@@ -121,7 +121,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v5.0.0
- name: Setup Node
uses: actions/setup-node@v4.4.0
uses: actions/setup-node@v5.0.0
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -136,6 +136,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@v2.3.2
uses: softprops/action-gh-release@v2.3.3
with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz

View File

@@ -12,7 +12,7 @@ jobs:
if: github.event.issue.type.name == 'Task'
steps:
- name: Check if user is authorized
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const issueAuthor = context.payload.issue.user.login;

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.9.3.cjs
yarnPath: .yarn/releases/yarn-4.9.4.cjs

View File

@@ -5,17 +5,17 @@ const castContext = framework.CastReceiverContext.getInstance();
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
framework.messages.MessageType.LOAD,
"LOAD" as framework.messages.MessageType.LOAD,
(loadRequestData) => {
const media = loadRequestData.media;
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = framework.messages.StreamType.LIVE;
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
media.hlsVideoSegmentFormat =
framework.messages.HlsVideoSegmentFormat.FMP4;
"fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}

View File

@@ -40,7 +40,8 @@ const playDummyMedia = (viewTitle?: string) => {
loadRequestData.media.contentId =
"https://cast.home-assistant.io/images/google-nest-hub.png";
loadRequestData.media.contentType = "image/jpeg";
loadRequestData.media.streamType = framework.messages.StreamType.NONE;
loadRequestData.media.streamType =
"NONE" as framework.messages.StreamType.NONE;
const metadata = new framework.messages.GenericMediaMetadata();
metadata.title = viewTitle;
loadRequestData.media.metadata = metadata;
@@ -89,7 +90,7 @@ const showMediaPlayer = () => {
const options = new framework.CastReceiverOptions();
options.disableIdleTimeout = true;
options.customNamespaces = {
[CAST_NS]: framework.system.MessageType.JSON,
[CAST_NS]: "json" as framework.system.MessageType.JSON,
};
castContext.addCustomMessageListener(
@@ -97,9 +98,7 @@ castContext.addCustomMessageListener(
// @ts-ignore
(ev: ReceivedMessage<HassMessage>) => {
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
if (
playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE
) {
if (playerManager.getPlayerState() !== "IDLE") {
playerManager.stop();
} else {
showLovelaceController();
@@ -113,7 +112,7 @@ castContext.addCustomMessageListener(
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
framework.messages.MessageType.LOAD,
"LOAD" as framework.messages.MessageType.LOAD,
(loadRequestData) => {
if (
loadRequestData.media.contentId ===
@@ -127,24 +126,23 @@ playerManager.setMessageInterceptor(
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = framework.messages.StreamType.LIVE;
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
media.hlsVideoSegmentFormat =
framework.messages.HlsVideoSegmentFormat.FMP4;
"fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}
);
playerManager.addEventListener(
framework.events.EventType.MEDIA_STATUS,
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS,
(event) => {
if (
event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
event.mediaStatus?.playerState === "IDLE" &&
event.mediaStatus?.idleReason &&
event.mediaStatus?.idleReason !==
framework.messages.IdleReason.INTERRUPTED
event.mediaStatus?.idleReason !== "INTERRUPTED"
) {
// media finished or stopped, return to default Lovelace
showLovelaceController();

View File

@@ -68,7 +68,7 @@
}
#ha-launch-screen .ha-launch-screen-spacer-top {
flex: 1;
margin-top: calc( 2 * max(var(--safe-area-inset-bottom, 0px), 48px) + 46px );
margin-top: calc( 2 * max(var(--safe-area-inset-top, 0px), 48px) + 46px );
padding-top: 48px;
}
#ha-launch-screen .ha-launch-screen-spacer-bottom {

View File

@@ -1,10 +1,11 @@
import { LitElement, css, html } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../src/components/ha-card";
import "../../../src/dialogs/more-info/more-info-content";
import "../../../src/state-summary/state-card-content";
import "../ha-demo-options";
import type { HomeAssistant } from "../../../src/types";
import { computeShowNewMoreInfo } from "../../../src/dialogs/more-info/const";
@customElement("demo-more-info")
class DemoMoreInfo extends LitElement {
@@ -21,11 +22,13 @@ class DemoMoreInfo extends LitElement {
<div class="root">
<div id="card">
<ha-card>
<state-card-content
.stateObj=${state}
.hass=${this.hass}
in-dialog
></state-card-content>
${!computeShowNewMoreInfo(state)
? html`<state-card-content
.stateObj=${state}
.hass=${this.hass}
in-dialog
></state-card-content>`
: nothing}
<more-info-content
.hass=${this.hass}

View File

@@ -1106,7 +1106,7 @@ export default {
friendly_name: "Philips Hue",
entity_picture: null,
description:
"Press the button on the bridge to register Philips Hue with Home Assistant.\n\n![Description image](/static/images/config_philips_hue.jpg)",
"Press the button on the bridge to register Philips Hue with Home Assistant.",
submit_caption: "I have pressed the button",
},
last_changed: "2018-07-19T10:44:46.515160+00:00",

View File

@@ -781,7 +781,7 @@ class HassioAddonInfo extends LitElement {
${this.addon.long_description
? html`
<ha-card outlined>
<ha-card class="long-description" outlined>
<div class="card-content">
<ha-markdown
.content=${this.addon.long_description}
@@ -1333,6 +1333,9 @@ class HassioAddonInfo extends LitElement {
.description a {
color: var(--primary-color);
}
.long-description {
direction: ltr;
}
ha-assist-chip {
--md-sys-color-primary: var(--text-primary-color);
--md-sys-color-on-surface: var(--text-primary-color);

View File

@@ -19,8 +19,9 @@
height: auto;
padding: 32px 0;
}
.content {
max-width: 560px;
max-width: min(560px, calc(100vw - var(--safe-area-inset-right, 0px) - var(--safe-area-inset-left, 0px)));
margin: 0 auto;
padding: 0 16px;
box-sizing: content-box;

View File

@@ -29,13 +29,13 @@
"@awesome.me/webawesome": "3.0.0-beta.4",
"@babel/runtime": "7.28.3",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/autocomplete": "6.18.7",
"@codemirror/commands": "6.8.1",
"@codemirror/language": "6.11.3",
"@codemirror/legacy-modes": "6.5.1",
"@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.38.1",
"@codemirror/view": "6.38.2",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.0",
"@formatjs/intl-displaynames": "6.8.11",
@@ -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.8.5",
"@vaadin/vaadin-themable-mixin": "24.8.5",
"@vaadin/combo-box": "24.8.7",
"@vaadin/vaadin-themable-mixin": "24.8.7",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
@@ -112,7 +112,7 @@
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"hls.js": "1.6.10",
"hls.js": "1.6.11",
"home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "10.7.16",
@@ -123,7 +123,7 @@
"lit": "3.3.1",
"lit-html": "3.3.1",
"luxon": "3.7.1",
"marked": "16.2.0",
"marked": "16.2.1",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
@@ -159,18 +159,18 @@
"@octokit/plugin-retry": "8.0.1",
"@octokit/rest": "22.0.0",
"@rsdoctor/rspack-plugin": "1.2.3",
"@rspack/core": "1.4.11",
"@rspack/core": "1.5.2",
"@rspack/dev-server": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.22",
"@types/chromecast-caf-receiver": "6.0.24",
"@types/chromecast-caf-sender": "1.0.11",
"@types/color-name": "2.0.0",
"@types/culori": "4.0.0",
"@types/culori": "4.0.1",
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.20",
"@types/leaflet-draw": "1.0.12",
"@types/leaflet.markercluster": "1.5.5",
"@types/leaflet-draw": "1.0.13",
"@types/leaflet.markercluster": "1.5.6",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.7.1",
"@types/mocha": "10.0.10",
@@ -204,7 +204,7 @@
"husky": "9.1.7",
"jsdom": "26.1.0",
"jszip": "3.10.1",
"lint-staged": "16.1.5",
"lint-staged": "16.1.6",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
@@ -212,13 +212,13 @@
"pinst": "3.0.0",
"prettier": "3.6.2",
"rspack-manifest-plugin": "5.0.3",
"serve": "14.2.4",
"serve": "14.2.5",
"sinon": "21.0.0",
"tar": "7.4.3",
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.2",
"typescript-eslint": "8.40.0",
"typescript-eslint": "8.42.0",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.2.4",
"webpack-stats-plugin": "1.1.3",
@@ -234,8 +234,7 @@
"@fullcalendar/daygrid": "6.1.19",
"globals": "16.3.0",
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"@vaadin/vaadin-themable-mixin": "24.8.5"
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
},
"packageManager": "yarn@4.9.3"
"packageManager": "yarn@4.9.4"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1,76 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4744_40067)">
<path d="M0 6C0 2.68629 2.68629 0 6 0H28C31.3137 0 34 2.68629 34 6C34 9.31371 31.3137 12 28 12H6C2.68629 12 0 9.31371 0 6Z" fill="white" fill-opacity="0.48"/>
<path d="M0 28C0 23.5817 3.58172 20 8 20H42.6667C47.0849 20 50.6667 23.5817 50.6667 28V36C50.6667 40.4183 47.0849 44 42.6667 44H8.00001C3.58173 44 0 40.4183 0 36V28Z" fill="#1C1C1C"/>
<path d="M8 20.5H42.667C46.809 20.5002 50.167 23.858 50.167 28V36C50.167 40.142 46.809 43.4998 42.667 43.5H8C3.85787 43.5 0.5 40.1421 0.5 36V28C0.5 23.8579 3.85786 20.5 8 20.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M6 32C6 28.6863 8.68629 26 12 26C15.3137 26 18 28.6863 18 32C18 35.3137 15.3137 38 12 38C8.68629 38 6 35.3137 6 32Z" fill="white" fill-opacity="0.24"/>
<path d="M24 31C24 29.3431 25.3431 28 27 28H39.6667C41.3235 28 42.6667 29.3431 42.6667 31V33C42.6667 34.6569 41.3235 36 39.6667 36H27C25.3431 36 24 34.6569 24 33V31Z" fill="white" fill-opacity="0.24"/>
<path d="M54.6666 28C54.6666 23.5817 58.2483 20 62.6666 20H97.3333C101.752 20 105.333 23.5817 105.333 28V36C105.333 40.4183 101.752 44 97.3333 44H62.6666C58.2484 44 54.6666 40.4183 54.6666 36V28Z" fill="#1C1C1C"/>
<path d="M62.6666 20.5H97.3336C101.476 20.5002 104.834 23.858 104.834 28V36C104.834 40.142 101.476 43.4998 97.3336 43.5H62.6666C58.5245 43.5 55.1666 40.1421 55.1666 36V28C55.1666 23.8579 58.5245 20.5 62.6666 20.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M60.6666 32C60.6666 28.6863 63.3529 26 66.6666 26C69.9803 26 72.6666 28.6863 72.6666 32C72.6666 35.3137 69.9803 38 66.6666 38C63.3529 38 60.6666 35.3137 60.6666 32Z" fill="white" fill-opacity="0.24"/>
<path d="M78.6666 31C78.6666 29.3431 80.0098 28 81.6666 28H94.3333C95.9901 28 97.3333 29.3431 97.3333 31V33C97.3333 34.6569 95.9901 36 94.3333 36H81.6666C80.0098 36 78.6666 34.6569 78.6666 33V31Z" fill="white" fill-opacity="0.24"/>
<path d="M109.333 28C109.333 23.5817 112.915 20 117.333 20H152C156.418 20 160 23.5817 160 28V36C160 40.4183 156.418 44 152 44H117.333C112.915 44 109.333 40.4183 109.333 36V28Z" fill="#1C1C1C"/>
<path d="M117.333 20.5H152C156.142 20.5002 159.5 23.858 159.5 28V36C159.5 40.142 156.142 43.4998 152 43.5H117.333C113.191 43.5 109.833 40.1421 109.833 36V28C109.833 23.8579 113.191 20.5 117.333 20.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M115.333 32C115.333 28.6863 118.02 26 121.333 26C124.647 26 127.333 28.6863 127.333 32C127.333 35.3137 124.647 38 121.333 38C118.02 38 115.333 35.3137 115.333 32Z" fill="white" fill-opacity="0.24"/>
<path d="M133.333 31C133.333 29.3431 134.677 28 136.333 28H149C150.657 28 152 29.3431 152 31V33C152 34.6569 150.657 36 149 36H136.333C134.677 36 133.333 34.6569 133.333 33V31Z" fill="white" fill-opacity="0.24"/>
<path d="M0 56C0 53.7909 1.79086 52 4 52H29C31.2091 52 33 53.7909 33 56C33 58.2091 31.2091 60 29 60H4C1.79086 60 0 58.2091 0 56Z" fill="white" fill-opacity="0.48"/>
<path d="M0 72C0 67.5817 3.58172 64 8 64H29C33.4183 64 37 67.5817 37 72V96C37 100.418 33.4183 104 29 104H8C3.58172 104 0 100.418 0 96V72Z" fill="#1C1C1C"/>
<path d="M8 64.5H29C33.1421 64.5 36.5 67.8579 36.5 72V96C36.5 100.142 33.1421 103.5 29 103.5H8C3.85786 103.5 0.5 100.142 0.5 96V72C0.5 67.8579 3.85786 64.5 8 64.5Z" stroke="white" stroke-opacity="0.24"/>
<mask id="mask0_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="6" y="72" width="25" height="24">
<path d="M18.5 74C16.6435 74 14.863 74.7375 13.5503 76.0503C12.2375 77.363 11.5 79.1435 11.5 81C11.5 83.38 12.69 85.47 14.5 86.74V89C14.5 89.2652 14.6054 89.5196 14.7929 89.7071C14.9804 89.8946 15.2348 90 15.5 90H21.5C21.7652 90 22.0196 89.8946 22.2071 89.7071C22.3946 89.5196 22.5 89.2652 22.5 89V86.74C24.31 85.47 25.5 83.38 25.5 81C25.5 79.1435 24.7625 77.363 23.4497 76.0503C22.137 74.7375 20.3565 74 18.5 74ZM15.5 93C15.5 93.2652 15.6054 93.5196 15.7929 93.7071C15.9804 93.8946 16.2348 94 16.5 94H20.5C20.7652 94 21.0196 93.8946 21.2071 93.7071C21.3946 93.5196 21.5 93.2652 21.5 93V92H15.5V93Z" fill="black"/>
</mask>
<g mask="url(#mask0_4744_40067)">
<rect x="6.5" y="72" width="24" height="24" fill="#03A9F4"/>
</g>
<path d="M41 72C41 67.5817 44.5817 64 49 64H70C74.4183 64 78 67.5817 78 72V96C78 100.418 74.4183 104 70 104H49C44.5817 104 41 100.418 41 96V72Z" fill="#1C1C1C"/>
<path d="M49 64.5H70C74.1421 64.5 77.5 67.8579 77.5 72V96C77.5 100.142 74.1421 103.5 70 103.5H49C44.8579 103.5 41.5 100.142 41.5 96V72C41.5 67.8579 44.8579 64.5 49 64.5Z" stroke="white" stroke-opacity="0.24"/>
<mask id="mask1_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="47" y="72" width="25" height="24">
<path d="M66.5 80C67.61 80 68.5 80.9 68.5 82V88.76C69.11 89.31 69.5 90.11 69.5 91C69.5 92.66 68.16 94 66.5 94C64.84 94 63.5 92.66 63.5 91C63.5 90.11 63.89 89.31 64.5 88.76V82C64.5 80.9 65.4 80 66.5 80ZM66.5 81C65.95 81 65.5 81.45 65.5 82V83H67.5V82C67.5 81.45 67.05 81 66.5 81ZM52.5 92V84H49.5L59.5 75L63.9 78.96C63.04 79.69 62.5 80.78 62.5 82V88C61.87 88.83 61.5 89.87 61.5 91L61.6 92H52.5Z" fill="black"/>
</mask>
<g mask="url(#mask1_4744_40067)">
<rect x="47.5" y="72" width="24" height="24" fill="#03A9F4"/>
</g>
<path d="M82 72C82 67.5817 85.5817 64 90 64H111C115.418 64 119 67.5817 119 72V96C119 100.418 115.418 104 111 104H90C85.5817 104 82 100.418 82 96V72Z" fill="#1C1C1C"/>
<path d="M90 64.5H111C115.142 64.5 118.5 67.8579 118.5 72V96C118.5 100.142 115.142 103.5 111 103.5H90C85.8579 103.5 82.5 100.142 82.5 96V72C82.5 67.8579 85.8579 64.5 90 64.5Z" stroke="white" stroke-opacity="0.24"/>
<mask id="mask2_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="88" y="72" width="25" height="24">
<path d="M100.5 84H107.5C106.97 88.11 104.22 91.78 100.5 92.92V84H93.5V78.3L100.5 75.19M100.5 73L91.5 77V83C91.5 88.55 95.34 93.73 100.5 95C105.66 93.73 109.5 88.55 109.5 83V77L100.5 73Z" fill="black"/>
</mask>
<g mask="url(#mask2_4744_40067)">
<rect x="88.5" y="72" width="24" height="24" fill="#03A9F4"/>
</g>
<path d="M123 72C123 67.5817 126.582 64 131 64H152C156.418 64 160 67.5817 160 72V96C160 100.418 156.418 104 152 104H131C126.582 104 123 100.418 123 96V72Z" fill="#1C1C1C"/>
<path d="M131 64.5H152C156.142 64.5 159.5 67.8579 159.5 72V96C159.5 100.142 156.142 103.5 152 103.5H131C126.858 103.5 123.5 100.142 123.5 96V72C123.5 67.8579 126.858 64.5 131 64.5Z" stroke="white" stroke-opacity="0.24"/>
<mask id="mask3_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="129" y="72" width="25" height="24">
<path d="M145.5 84C145.5 83.4696 145.711 82.9609 146.086 82.5858C146.461 82.2107 146.97 82 147.5 82C148.03 82 148.539 82.2107 148.914 82.5858C149.289 82.9609 149.5 83.4696 149.5 84C149.5 84.5304 149.289 85.0391 148.914 85.4142C148.539 85.7893 148.03 86 147.5 86C146.97 86 146.461 85.7893 146.086 85.4142C145.711 85.0391 145.5 84.5304 145.5 84ZM139.5 84C139.5 83.4696 139.711 82.9609 140.086 82.5858C140.461 82.2107 140.97 82 141.5 82C142.03 82 142.539 82.2107 142.914 82.5858C143.289 82.9609 143.5 83.4696 143.5 84C143.5 84.5304 143.289 85.0391 142.914 85.4142C142.539 85.7893 142.03 86 141.5 86C140.97 86 140.461 85.7893 140.086 85.4142C139.711 85.0391 139.5 84.5304 139.5 84ZM133.5 84C133.5 83.4696 133.711 82.9609 134.086 82.5858C134.461 82.2107 134.97 82 135.5 82C136.03 82 136.539 82.2107 136.914 82.5858C137.289 82.9609 137.5 83.4696 137.5 84C137.5 84.5304 137.289 85.0391 136.914 85.4142C136.539 85.7893 136.03 86 135.5 86C134.97 86 134.461 85.7893 134.086 85.4142C133.711 85.0391 133.5 84.5304 133.5 84Z" fill="black"/>
</mask>
<g mask="url(#mask3_4744_40067)">
<rect x="129.5" y="72" width="24" height="24" fill="#03A9F4"/>
</g>
<path d="M0 116C0 113.791 1.79086 112 4 112H29C31.2091 112 33 113.791 33 116C33 118.209 31.2091 120 29 120H4C1.79086 120 0 118.209 0 116Z" fill="white" fill-opacity="0.48"/>
<path d="M0 132C0 127.582 3.58172 124 8 124H70C74.4183 124 78 127.582 78 132V160H0V132Z" fill="url(#paint0_linear_4744_40067)"/>
<path d="M8 124.5H70C74.1421 124.5 77.5 127.858 77.5 132V159.5H0.5V132C0.5 127.858 3.85786 124.5 8 124.5Z" stroke="url(#paint1_linear_4744_40067)" stroke-opacity="0.12"/>
<path d="M82 132C82 127.582 85.5817 124 90 124H152C156.418 124 160 127.582 160 132V160H82V132Z" fill="url(#paint2_linear_4744_40067)"/>
<path d="M90 124.5H152C156.142 124.5 159.5 127.858 159.5 132V159.5H82.5V132C82.5 127.858 85.8579 124.5 90 124.5Z" stroke="url(#paint3_linear_4744_40067)" stroke-opacity="0.12"/>
</g>
<defs>
<linearGradient id="paint0_linear_4744_40067" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-color="#1C1C1C"/>
<stop offset="1" stop-color="#1C1C1C" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_4744_40067" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-color="white" stop-opacity="0.24"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_4744_40067" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-color="#1C1C1C"/>
<stop offset="1" stop-color="#1C1C1C" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint3_linear_4744_40067" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-color="white" stop-opacity="0.24"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0_4744_40067">
<rect width="160" height="160" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1,76 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4744_39984)">
<path d="M0 6C0 2.68629 2.68629 0 6 0H28C31.3137 0 34 2.68629 34 6C34 9.31371 31.3137 12 28 12H6C2.68629 12 0 9.31371 0 6Z" fill="black" fill-opacity="0.32"/>
<path d="M0 28C0 23.5817 3.58172 20 8 20H42.6667C47.0849 20 50.6667 23.5817 50.6667 28V36C50.6667 40.4183 47.0849 44 42.6667 44H8.00001C3.58173 44 0 40.4183 0 36V28Z" fill="white"/>
<path d="M8 20.5H42.667C46.809 20.5002 50.167 23.858 50.167 28V36C50.167 40.142 46.809 43.4998 42.667 43.5H8C3.85787 43.5 0.5 40.1421 0.5 36V28C0.5 23.8579 3.85786 20.5 8 20.5Z" stroke="black" stroke-opacity="0.12"/>
<rect x="6" y="26" width="12" height="12" rx="6" fill="black" fill-opacity="0.12"/>
<path d="M24 31C24 29.3431 25.3431 28 27 28H39.6667C41.3235 28 42.6667 29.3431 42.6667 31V33C42.6667 34.6569 41.3235 36 39.6667 36H27C25.3431 36 24 34.6569 24 33V31Z" fill="black" fill-opacity="0.12"/>
<path d="M54.6667 28C54.6667 23.5817 58.2484 20 62.6667 20H97.3333C101.752 20 105.333 23.5817 105.333 28V36C105.333 40.4183 101.752 44 97.3334 44H62.6667C58.2484 44 54.6667 40.4183 54.6667 36V28Z" fill="white"/>
<path d="M62.6667 20.5H97.3337C101.476 20.5002 104.834 23.858 104.834 28V36C104.834 40.142 101.476 43.4998 97.3337 43.5H62.6667C58.5246 43.5 55.1667 40.1421 55.1667 36V28C55.1667 23.8579 58.5246 20.5 62.6667 20.5Z" stroke="black" stroke-opacity="0.12"/>
<rect x="60.6667" y="26" width="12" height="12" rx="6" fill="black" fill-opacity="0.12"/>
<path d="M78.6667 31C78.6667 29.3431 80.0098 28 81.6667 28H94.3334C95.9902 28 97.3334 29.3431 97.3334 31V33C97.3334 34.6569 95.9902 36 94.3334 36H81.6667C80.0098 36 78.6667 34.6569 78.6667 33V31Z" fill="black" fill-opacity="0.12"/>
<path d="M109.333 28C109.333 23.5817 112.915 20 117.333 20H152C156.418 20 160 23.5817 160 28V36C160 40.4183 156.418 44 152 44H117.333C112.915 44 109.333 40.4183 109.333 36V28Z" fill="white"/>
<path d="M117.333 20.5H152C156.142 20.5002 159.5 23.858 159.5 28V36C159.5 40.142 156.142 43.4998 152 43.5H117.333C113.191 43.5 109.833 40.1421 109.833 36V28C109.833 23.8579 113.191 20.5 117.333 20.5Z" stroke="black" stroke-opacity="0.12"/>
<rect x="115.333" y="26" width="12" height="12" rx="6" fill="black" fill-opacity="0.12"/>
<path d="M133.333 31C133.333 29.3431 134.676 28 136.333 28H149C150.657 28 152 29.3431 152 31V33C152 34.6569 150.657 36 149 36H136.333C134.676 36 133.333 34.6569 133.333 33V31Z" fill="black" fill-opacity="0.12"/>
<path d="M0 56C0 53.7909 1.79086 52 4 52H29C31.2091 52 33 53.7909 33 56C33 58.2091 31.2091 60 29 60H4C1.79086 60 0 58.2091 0 56Z" fill="black" fill-opacity="0.32"/>
<path d="M0 72C0 67.5817 3.58172 64 8 64H29C33.4183 64 37 67.5817 37 72V96C37 100.418 33.4183 104 29 104H8C3.58172 104 0 100.418 0 96V72Z" fill="white"/>
<path d="M8 64.5H29C33.1421 64.5 36.5 67.8579 36.5 72V96C36.5 100.142 33.1421 103.5 29 103.5H8C3.85786 103.5 0.5 100.142 0.5 96V72C0.5 67.8579 3.85786 64.5 8 64.5Z" stroke="black" stroke-opacity="0.12"/>
<mask id="mask0_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="6" y="72" width="25" height="24">
<path d="M18.5 74C16.6435 74 14.863 74.7375 13.5503 76.0503C12.2375 77.363 11.5 79.1435 11.5 81C11.5 83.38 12.69 85.47 14.5 86.74V89C14.5 89.2652 14.6054 89.5196 14.7929 89.7071C14.9804 89.8946 15.2348 90 15.5 90H21.5C21.7652 90 22.0196 89.8946 22.2071 89.7071C22.3946 89.5196 22.5 89.2652 22.5 89V86.74C24.31 85.47 25.5 83.38 25.5 81C25.5 79.1435 24.7625 77.363 23.4497 76.0503C22.137 74.7375 20.3565 74 18.5 74ZM15.5 93C15.5 93.2652 15.6054 93.5196 15.7929 93.7071C15.9804 93.8946 16.2348 94 16.5 94H20.5C20.7652 94 21.0196 93.8946 21.2071 93.7071C21.3946 93.5196 21.5 93.2652 21.5 93V92H15.5V93Z" fill="black"/>
</mask>
<g mask="url(#mask0_4744_39984)">
<rect x="6.5" y="72" width="24" height="24" fill="#03A9F4"/>
</g>
<path d="M41 72C41 67.5817 44.5817 64 49 64H70C74.4183 64 78 67.5817 78 72V96C78 100.418 74.4183 104 70 104H49C44.5817 104 41 100.418 41 96V72Z" fill="white"/>
<path d="M49 64.5H70C74.1421 64.5 77.5 67.8579 77.5 72V96C77.5 100.142 74.1421 103.5 70 103.5H49C44.8579 103.5 41.5 100.142 41.5 96V72C41.5 67.8579 44.8579 64.5 49 64.5Z" stroke="black" stroke-opacity="0.12"/>
<mask id="mask1_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="47" y="72" width="25" height="24">
<path d="M66.5 80C67.61 80 68.5 80.9 68.5 82V88.76C69.11 89.31 69.5 90.11 69.5 91C69.5 92.66 68.16 94 66.5 94C64.84 94 63.5 92.66 63.5 91C63.5 90.11 63.89 89.31 64.5 88.76V82C64.5 80.9 65.4 80 66.5 80ZM66.5 81C65.95 81 65.5 81.45 65.5 82V83H67.5V82C67.5 81.45 67.05 81 66.5 81ZM52.5 92V84H49.5L59.5 75L63.9 78.96C63.04 79.69 62.5 80.78 62.5 82V88C61.87 88.83 61.5 89.87 61.5 91L61.6 92H52.5Z" fill="black"/>
</mask>
<g mask="url(#mask1_4744_39984)">
<rect x="47.5" y="72" width="24" height="24" fill="#03A9F4"/>
</g>
<path d="M82 72C82 67.5817 85.5817 64 90 64H111C115.418 64 119 67.5817 119 72V96C119 100.418 115.418 104 111 104H90C85.5817 104 82 100.418 82 96V72Z" fill="white"/>
<path d="M90 64.5H111C115.142 64.5 118.5 67.8579 118.5 72V96C118.5 100.142 115.142 103.5 111 103.5H90C85.8579 103.5 82.5 100.142 82.5 96V72C82.5 67.8579 85.8579 64.5 90 64.5Z" stroke="black" stroke-opacity="0.12"/>
<mask id="mask2_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="88" y="72" width="25" height="24">
<path d="M100.5 84H107.5C106.97 88.11 104.22 91.78 100.5 92.92V84H93.5V78.3L100.5 75.19M100.5 73L91.5 77V83C91.5 88.55 95.34 93.73 100.5 95C105.66 93.73 109.5 88.55 109.5 83V77L100.5 73Z" fill="black"/>
</mask>
<g mask="url(#mask2_4744_39984)">
<rect x="88.5" y="72" width="24" height="24" fill="#03A9F4"/>
</g>
<path d="M123 72C123 67.5817 126.582 64 131 64H152C156.418 64 160 67.5817 160 72V96C160 100.418 156.418 104 152 104H131C126.582 104 123 100.418 123 96V72Z" fill="white"/>
<path d="M131 64.5H152C156.142 64.5 159.5 67.8579 159.5 72V96C159.5 100.142 156.142 103.5 152 103.5H131C126.858 103.5 123.5 100.142 123.5 96V72C123.5 67.8579 126.858 64.5 131 64.5Z" stroke="black" stroke-opacity="0.12"/>
<mask id="mask3_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="129" y="72" width="25" height="24">
<path d="M145.5 84C145.5 83.4696 145.711 82.9609 146.086 82.5858C146.461 82.2107 146.97 82 147.5 82C148.03 82 148.539 82.2107 148.914 82.5858C149.289 82.9609 149.5 83.4696 149.5 84C149.5 84.5304 149.289 85.0391 148.914 85.4142C148.539 85.7893 148.03 86 147.5 86C146.97 86 146.461 85.7893 146.086 85.4142C145.711 85.0391 145.5 84.5304 145.5 84ZM139.5 84C139.5 83.4696 139.711 82.9609 140.086 82.5858C140.461 82.2107 140.97 82 141.5 82C142.03 82 142.539 82.2107 142.914 82.5858C143.289 82.9609 143.5 83.4696 143.5 84C143.5 84.5304 143.289 85.0391 142.914 85.4142C142.539 85.7893 142.03 86 141.5 86C140.97 86 140.461 85.7893 140.086 85.4142C139.711 85.0391 139.5 84.5304 139.5 84ZM133.5 84C133.5 83.4696 133.711 82.9609 134.086 82.5858C134.461 82.2107 134.97 82 135.5 82C136.03 82 136.539 82.2107 136.914 82.5858C137.289 82.9609 137.5 83.4696 137.5 84C137.5 84.5304 137.289 85.0391 136.914 85.4142C136.539 85.7893 136.03 86 135.5 86C134.97 86 134.461 85.7893 134.086 85.4142C133.711 85.0391 133.5 84.5304 133.5 84Z" fill="black"/>
</mask>
<g mask="url(#mask3_4744_39984)">
<rect x="129.5" y="72" width="24" height="24" fill="#18BCF2"/>
</g>
<path d="M0 116C0 113.791 1.79086 112 4 112H29C31.2091 112 33 113.791 33 116C33 118.209 31.2091 120 29 120H4C1.79086 120 0 118.209 0 116Z" fill="black" fill-opacity="0.32"/>
<path d="M0 132C0 127.582 3.58172 124 8 124H70C74.4183 124 78 127.582 78 132V160H0V132Z" fill="url(#paint0_linear_4744_39984)"/>
<path d="M8 124.5H70C74.1421 124.5 77.5 127.858 77.5 132V159.5H0.5V132C0.5 127.858 3.85786 124.5 8 124.5Z" stroke="url(#paint1_linear_4744_39984)" stroke-opacity="0.12"/>
<path d="M82 132C82 127.582 85.5817 124 90 124H152C156.418 124 160 127.582 160 132V160H82V132Z" fill="url(#paint2_linear_4744_39984)"/>
<path d="M90 124.5H152C156.142 124.5 159.5 127.858 159.5 132V159.5H82.5V132C82.5 127.858 85.8579 124.5 90 124.5Z" stroke="url(#paint3_linear_4744_39984)" stroke-opacity="0.12"/>
</g>
<defs>
<linearGradient id="paint0_linear_4744_39984" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_4744_39984" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-opacity="0.12"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_4744_39984" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint3_linear_4744_39984" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
<stop offset="0.5" stop-opacity="0.12"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0_4744_39984">
<rect width="160" height="160" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

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

View File

@@ -67,10 +67,7 @@ export const generateEntityFilter = (
}
if (floors) {
if (!floor) {
return false;
}
if (!floors) {
if (!floor || !floors.has(floor.floor_id)) {
return false;
}
}

View File

@@ -119,10 +119,11 @@ class HaAlert extends LitElement {
.main-content {
overflow-wrap: anywhere;
word-break: break-word;
line-height: normal;
margin-left: 8px;
margin-right: 0;
margin-inline-start: 8px;
margin-inline-end: 0;
margin-inline-end: 8px;
}
.title {
margin-top: 2px;

View File

@@ -3,11 +3,15 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
import { STATE_ATTRIBUTES } from "../data/entity_attributes";
import {
STATE_ATTRIBUTES,
STATE_ATTRIBUTES_DOMAIN_CLASS,
} from "../data/entity_attributes";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-attribute-value";
import "./ha-expansion-panel";
import { computeStateDomain } from "../common/entity/compute_state_domain";
@customElement("ha-attributes")
class HaAttributes extends LitElement {
@@ -22,7 +26,12 @@ class HaAttributes extends LitElement {
private get _filteredAttributes() {
return this._computeDisplayAttributes(
STATE_ATTRIBUTES.concat(
this.extraFilters ? this.extraFilters.split(",") : []
this.extraFilters ? this.extraFilters.split(",") : [],
(this.stateObj &&
STATE_ATTRIBUTES_DOMAIN_CLASS[computeStateDomain(this.stateObj)]?.[
this.stateObj.attributes?.device_class
]) ||
[]
)
);
}

View File

@@ -1,7 +1,7 @@
import { mdiChevronUp } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-icon-button";
@@ -16,12 +16,20 @@ export class HaAutomationRow extends LitElement {
@property({ type: Boolean, reflect: true })
public selected = false;
@property({ type: Boolean, reflect: true, attribute: "sort-selected" })
public sortSelected = false;
@property({ type: Boolean, reflect: true })
public disabled = false;
@property({ type: Boolean, reflect: true, attribute: "building-block" })
public buildingBlock = false;
@property({ type: Boolean, reflect: true }) public highlight?: boolean;
@query(".row")
private _rowElement?: HTMLDivElement;
protected render(): TemplateResult {
return html`
<div
@@ -66,15 +74,69 @@ export class HaAutomationRow extends LitElement {
if (ev.defaultPrevented) {
return;
}
if (ev.key !== "Enter" && ev.key !== " ") {
if (
ev.key !== "Enter" &&
ev.key !== " " &&
!(
(this.sortSelected || ev.altKey) &&
!(ev.ctrlKey || ev.metaKey) &&
!ev.shiftKey &&
(ev.key === "ArrowUp" || ev.key === "ArrowDown")
) &&
!(
(ev.ctrlKey || ev.metaKey) &&
!ev.shiftKey &&
!ev.altKey &&
(ev.key === "c" ||
ev.key === "x" ||
ev.key === "Delete" ||
ev.key === "Backspace")
)
) {
return;
}
ev.preventDefault();
ev.stopPropagation();
if (ev.key === "ArrowUp" || ev.key === "ArrowDown") {
if (ev.key === "ArrowUp") {
fireEvent(this, "move-up");
return;
}
fireEvent(this, "move-down");
return;
}
if (this.sortSelected && (ev.key === "Enter" || ev.key === " ")) {
fireEvent(this, "stop-sort-selection");
return;
}
if (ev.ctrlKey || ev.metaKey) {
if (ev.key === "c") {
fireEvent(this, "copy-row");
return;
}
if (ev.key === "x") {
fireEvent(this, "cut-row");
return;
}
if (ev.key === "Delete" || ev.key === "Backspace") {
fireEvent(this, "delete-row");
return;
}
}
this.click();
}
public focus() {
requestAnimationFrame(() => {
this._rowElement?.focus();
});
}
static styles = css`
:host {
display: block;
@@ -97,6 +159,7 @@ export class HaAutomationRow extends LitElement {
.expand-button {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
color: var(--ha-color-on-neutral-quiet);
margin-left: -8px;
}
:host([building-block]) .leading-icon-wrapper {
background-color: var(--ha-color-fill-neutral-loud-resting);
@@ -134,6 +197,22 @@ export class HaAutomationRow extends LitElement {
overflow-wrap: anywhere;
margin: 0 12px;
}
:host([sort-selected]) .row {
outline: solid;
outline-color: rgba(var(--rgb-accent-color), 0.6);
outline-offset: -2px;
outline-width: 2px;
background-color: rgba(var(--rgb-accent-color), 0.08);
}
.row:hover {
background-color: rgba(var(--rgb-primary-text-color), 0.04);
}
:host([highlight]) .row {
background-color: rgba(var(--rgb-primary-color), 0.08);
}
:host([highlight]) .row:hover {
background-color: rgba(var(--rgb-primary-color), 0.16);
}
`;
}
@@ -144,5 +223,9 @@ declare global {
interface HASSDomEvents {
"toggle-collapsed": undefined;
"stop-sort-selection": undefined;
"copy-row": undefined;
"cut-row": undefined;
"delete-row": undefined;
}
}

View File

@@ -30,6 +30,8 @@ export class HaBottomSheet extends LitElement {
@state() private _dialogMaxViewpointHeight = 70;
@state() private _dialogMinViewpointHeight = 55;
@state() private _dialogViewportHeight?: number;
render() {
@@ -41,6 +43,7 @@ export class HaBottomSheet extends LitElement {
? `${this._dialogViewportHeight}vh`
: "auto",
maxHeight: `${this._dialogMaxViewpointHeight}vh`,
minHeight: `${this._dialogMinViewpointHeight}vh`,
})}
>
<div class="handle-wrapper">
@@ -80,6 +83,7 @@ export class HaBottomSheet extends LitElement {
this._dialogViewportHeight =
(this._dialog.offsetHeight / window.innerHeight) * 100;
this._dialogMaxViewpointHeight = 90;
this._dialogMinViewpointHeight = 20;
} else {
// after close animation is done close dialog element and fire closed event
this._dialog.close();
@@ -197,6 +201,7 @@ export class HaBottomSheet extends LitElement {
justify-content: center;
align-items: center;
z-index: 7;
padding-bottom: 76px;
}
.handle-wrapper .handle::after {
content: "";
@@ -227,7 +232,6 @@ export class HaBottomSheet extends LitElement {
box-shadow: var(--wa-shadow-l);
padding: 0;
margin: 0;
top: auto;
inset-inline-end: auto;
bottom: 0;

View File

@@ -262,10 +262,10 @@ export class HaButton extends Button {
}
.button.has-start {
padding-left: 8px;
padding-inline-start: 8px;
}
.button.has-end {
padding-right: 8px;
padding-inline-end: 8px;
}
`,
];

View File

@@ -90,7 +90,7 @@ export class HaDialog extends DialogBase {
}
.mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end);
padding: 12px 16px max(var(--safe-area-inset-bottom), 16px) 16px;
padding: 12px 16px 16px 16px;
}
.mdc-dialog__actions span:nth-child(1) {
flex: var(--secondary-action-button-flex, unset);
@@ -100,6 +100,8 @@ export class HaDialog extends DialogBase {
}
.mdc-dialog__container {
align-items: var(--vertical-align-dialog, center);
padding-top: var(--safe-area-inset-top);
padding-bottom: var(--safe-area-inset-bottom);
}
.mdc-dialog__title {
padding: 16px 16px 0 16px;
@@ -115,15 +117,17 @@ export class HaDialog extends DialogBase {
padding: var(--dialog-content-padding, 24px);
}
:host([hideactions]) .mdc-dialog .mdc-dialog__content {
padding-bottom: max(
var(--dialog-content-padding, 24px),
var(--safe-area-inset-bottom)
);
padding-bottom: var(--dialog-content-padding, 24px);
}
.mdc-dialog .mdc-dialog__surface {
position: var(--dialog-surface-position, relative);
top: var(--dialog-surface-top);
margin-top: var(--dialog-surface-margin-top);
min-width: calc(
var(--mdc-dialog-min-width, 100vw) - var(
--safe-area-inset-left
) - var(--safe-area-inset-right)
);
min-height: var(--mdc-dialog-min-height, auto);
border-radius: var(--ha-dialog-border-radius, 24px);
-webkit-backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
@@ -137,6 +141,24 @@ export class HaDialog extends DialogBase {
display: flex;
flex-direction: column;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.mdc-dialog .mdc-dialog__surface {
min-height: calc(
100vh - var(--safe-area-inset-top, 0px) - var(
--safe-area-inset-bottom,
0px
)
);
max-height: calc(
100vh - var(--safe-area-inset-top, 0px) - var(
--safe-area-inset-bottom,
0px
)
);
}
}
.header_title {
display: flex;
align-items: center;

View File

@@ -393,10 +393,13 @@ export class HaItemDisplayEditor extends LitElement {
--md-list-item-one-line-container-height: 48px;
}
ha-md-list-item.drag-selected {
box-shadow:
0px 0px 8px 4px rgba(var(--rgb-accent-color), 0.8),
inset 0px 2px 8px 4px rgba(var(--rgb-accent-color), 0.4);
--md-focus-ring-color: rgba(var(--rgb-accent-color), 0.6);
border-radius: 8px;
outline: solid;
outline-color: rgba(var(--rgb-accent-color), 0.6);
outline-offset: -2px;
outline-width: 2px;
background-color: rgba(var(--rgb-accent-color), 0.08);
}
ha-md-list-item ha-icon-button {
margin-left: -12px;

View File

@@ -16,9 +16,23 @@ export class HaMdButtonMenu extends LitElement {
@property() public positioning?: "fixed" | "absolute" | "popover";
@property({ attribute: "anchor-corner" }) public anchorCorner:
| "start-start"
| "start-end"
| "end-start"
| "end-end" = "end-start";
@property({ attribute: "menu-corner" }) public menuCorner:
| "start-start"
| "start-end"
| "end-start"
| "end-end" = "start-start";
@property({ type: Boolean, attribute: "has-overflow" }) public hasOverflow =
false;
@property({ type: Boolean }) public quick = false;
@query("ha-md-menu", true) private _menu!: HaMdMenu;
public get items() {
@@ -39,8 +53,11 @@ export class HaMdButtonMenu extends LitElement {
<slot name="trigger" @slotchange=${this._setTriggerAria}></slot>
</div>
<ha-md-menu
.quick=${this.quick}
.positioning=${this.positioning}
.hasOverflow=${this.hasOverflow}
.anchorCorner=${this.anchorCorner}
.menuCorner=${this.menuCorner}
@opening=${this._handleOpening}
@closing=${this._handleClosing}
>

View File

@@ -159,6 +159,7 @@ export class HaMdDialog extends Dialog {
--md-dialog-headline-size: var(--ha-font-size-xl);
--md-dialog-supporting-text-size: var(--ha-font-size-m);
--md-dialog-supporting-text-line-height: var(--ha-line-height-normal);
--md-divider-color: var(--divider-color);
}
:host([type="alert"]) {

View File

@@ -1,4 +1,4 @@
import { mdiStar } from "@mdi/js";
import { mdiInformationOutline, mdiStar } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -71,6 +71,17 @@ export class HaNetwork extends LitElement {
<span slot="description" data-for="auto_configure">
${this.hass.localize("ui.panel.config.network.adapter.detected")}:
${format_auto_detected_interfaces(this.networkConfig.adapters)}
${!configured_adapters.length
? html`<div class="info-text">
<ha-svg-icon
.path=${mdiInformationOutline}
class="info-icon"
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.network.adapter.auto_configure_manual_hint"
)}
</div>`
: nothing}
</span>
</ha-settings-row>
${configured_adapters.length || this._expanded
@@ -171,6 +182,21 @@ export class HaNetwork extends LitElement {
span[slot="description"] {
cursor: pointer;
}
.info-text {
display: flex;
align-items: center;
margin-top: 8px;
color: var(--secondary-text-color);
}
.info-icon {
width: 18px;
height: 18px;
color: var(--info-color, var(--primary-color));
margin-right: 8px;
flex-shrink: 0;
}
`,
];
}

View File

@@ -3,6 +3,7 @@ import {
mdiDevices,
mdiPaletteSwatch,
mdiTextureBox,
mdiTransitConnectionVariant,
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
@@ -266,7 +267,9 @@ export class HaRelatedItems extends LitElement {
<a href="/config/devices/device/${relatedDeviceId}">
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${mdiDevices}
.path=${device.entry_type === "service"
? mdiTransitConnectionVariant
: mdiDevices}
slot="graphic"
></ha-svg-icon>
${device.name_by_user || device.name}

View File

@@ -53,9 +53,15 @@ export class HaMediaSelector extends LitElement {
private _contextEntities: string[] | undefined;
private get _hasAccept(): boolean {
return !!this.selector?.media?.accept?.length;
}
willUpdate(changedProps: PropertyValues<this>) {
if (changedProps.has("context")) {
this._contextEntities = ensureArray(this.context?.filter_entity);
if (!this._hasAccept) {
this._contextEntities = ensureArray(this.context?.filter_entity);
}
}
if (changedProps.has("value")) {
@@ -99,10 +105,8 @@ export class HaMediaSelector extends LitElement {
(stateObj &&
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
const hasAccept = this.selector?.media?.accept?.length;
return html`
${hasAccept ||
${this._hasAccept ||
(this._contextEntities && this._contextEntities.length <= 1)
? nothing
: html`
@@ -148,7 +152,7 @@ export class HaMediaSelector extends LitElement {
: this.value.metadata?.title || this.value.media_content_id}
@click=${this._pickMedia}
@keydown=${this._handleKeyDown}
class=${this.disabled || (!entityId && !hasAccept)
class=${this.disabled || (!entityId && !this._hasAccept)
? "disabled"
: ""}
>
@@ -215,7 +219,7 @@ export class HaMediaSelector extends LitElement {
private _entityChanged(ev: CustomEvent) {
ev.stopPropagation();
if (this.context?.filter_entity) {
if (!this._hasAccept && this.context?.filter_entity) {
fireEvent(this, "value-changed", {
value: {
media_content_id: "",
@@ -257,7 +261,7 @@ export class HaMediaSelector extends LitElement {
media_content_type: id.media_content_type,
media_content_id: id.media_content_id,
})),
...(this.context?.filter_entity
...(!this._hasAccept && this.context?.filter_entity
? { browse_entity_id: this._getActiveEntityId() }
: {}),
},

View File

@@ -15,6 +15,7 @@ declare global {
"item-added": {
index: number;
data: any;
item: any;
};
"item-removed": {
index: number;
@@ -180,6 +181,7 @@ export class HaSortable extends LitElement {
fireEvent(this, "item-added", {
index: evt.newIndex,
data: evt.item.sortableData,
item: evt.item,
});
};

View File

@@ -556,18 +556,18 @@ export interface AutomationClipboard {
}
export interface BaseSidebarConfig {
toggleYamlMode: () => boolean;
delete: () => void;
close: (focus?: boolean) => void;
}
export interface TriggerSidebarConfig extends BaseSidebarConfig {
save: (value: Trigger) => void;
close: () => void;
rename: () => void;
disable: () => void;
duplicate: () => void;
cut: () => void;
copy: () => void;
toggleYamlMode: () => void;
config: Trigger;
yamlMode: boolean;
uiSupported: boolean;
@@ -575,13 +575,13 @@ export interface TriggerSidebarConfig extends BaseSidebarConfig {
export interface ConditionSidebarConfig extends BaseSidebarConfig {
save: (value: Condition) => void;
close: () => void;
rename: () => void;
disable: () => void;
test: () => void;
duplicate: () => void;
cut: () => void;
copy: () => void;
toggleYamlMode: () => void;
config: Condition;
yamlMode: boolean;
uiSupported: boolean;
@@ -589,13 +589,13 @@ export interface ConditionSidebarConfig extends BaseSidebarConfig {
export interface ActionSidebarConfig extends BaseSidebarConfig {
save: (value: Action) => void;
close: () => void;
rename: () => void;
disable: () => void;
duplicate: () => void;
cut: () => void;
copy: () => void;
run: () => void;
toggleYamlMode: () => void;
config: {
action: Action;
};
@@ -604,7 +604,6 @@ export interface ActionSidebarConfig extends BaseSidebarConfig {
}
export interface OptionSidebarConfig extends BaseSidebarConfig {
close: () => void;
rename: () => void;
duplicate: () => void;
defaultOption?: boolean;
@@ -612,13 +611,13 @@ export interface OptionSidebarConfig extends BaseSidebarConfig {
export interface ScriptFieldSidebarConfig extends BaseSidebarConfig {
save: (value: Field) => void;
close: () => void;
config: {
field: Field;
selector: boolean;
key: string;
excludeKeys: string[];
};
toggleYamlMode: () => void;
yamlMode: boolean;
}

View File

@@ -24,11 +24,14 @@ export interface BluetoothConnectionData extends DataTableRowData {
source: string;
}
export type HaScannerType = "usb" | "uart" | "remote" | "unknown";
export interface BluetoothScannerDetails {
source: string;
connectable: boolean;
name: string;
adapter: string;
scanner_type?: HaScannerType;
}
export type BluetoothScannersDetails = Record<string, BluetoothScannerDetails>;
@@ -55,6 +58,13 @@ export interface BluetoothAllocationsData {
allocated: string[];
}
export interface BluetoothScannerState {
source: string;
adapter: string;
current_mode: "active" | "passive" | null;
requested_mode: "active" | "passive" | null;
}
export const subscribeBluetoothScannersDetailsUpdates = (
conn: Connection,
store: Store<BluetoothScannersDetails>
@@ -170,3 +180,20 @@ export const subscribeBluetoothConnectionAllocations = (
params
);
};
export const subscribeBluetoothScannerState = (
conn: Connection,
callbackFunction: (scannerState: BluetoothScannerState) => void,
configEntryId?: string
): Promise<() => Promise<void>> => {
const params: { type: string; config_entry_id?: string } = {
type: "bluetooth/subscribe_scanner_state",
};
if (configEntryId) {
params.config_entry_id = configEntryId;
}
return conn.subscribeMessage<BluetoothScannerState>(
(scannerState) => callbackFunction(scannerState),
params
);
};

View File

@@ -97,6 +97,7 @@ export interface DataEntryFlowStepMenu {
step_id: string;
/** If array, use value to lookup translations in strings.json */
menu_options: string[] | Record<string, string>;
sort?: boolean;
description_placeholders?: Record<string, string>;
translation_domain?: string;
}

View File

@@ -1,6 +1,7 @@
import { formatDurationDigital } from "../common/datetime/format_duration";
import type { FrontendLocaleData } from "./translation";
// These attributes are hidden from the more-info window for all entities.
export const STATE_ATTRIBUTES = [
"entity_id",
"assumed_state",
@@ -26,6 +27,14 @@ export const STATE_ATTRIBUTES = [
"available_tones",
];
// These attributes are hidden from the more-info window for entities of the
// matching domain and device_class.
export const STATE_ATTRIBUTES_DOMAIN_CLASS = {
sensor: {
enum: ["options"],
},
};
export const TEMPERATURE_ATTRIBUTES = new Set([
"temperature",
"current_temperature",

View File

@@ -1 +1 @@
export const strokeWidth = 5;
export const strokeWidth = 2;

View File

@@ -195,6 +195,7 @@ export const fetchHostDisksUsage = async (hass: HomeAssistant) => {
type: "supervisor/api",
endpoint: "/host/disks/default/usage",
method: "get",
timeout: 3600, // seconds. This can take a while
});
}

View File

@@ -2,6 +2,7 @@ import type { Connection } from "home-assistant-js-websocket";
import { computeStateName } from "../common/entity/compute_state_name";
import type { HaDurationData } from "../components/ha-duration-input";
import type { HomeAssistant } from "../types";
import { firstWeekday } from "../common/datetime/first_weekday";
export interface RecorderInfo {
backlog: number | null;
@@ -108,7 +109,7 @@ export interface StatisticsValidationResultMeanTypeChanged {
};
}
export const VOLUME_UNITS = ["L", "gal", "ft³", "m³", "CCF"] as const;
export const VOLUME_UNITS = ["L", "gal", "ft³", "m³", "CCF", "MCF"] as const;
export interface StatisticsUnitConfiguration {
energy?: "Wh" | "kWh" | "MWh" | "GJ";
@@ -211,7 +212,14 @@ export const fetchStatistic = (
: period.fixed_period.end,
}
: undefined,
calendar: period.calendar,
calendar: period.calendar
? {
...(period.calendar.period === "week"
? { first_weekday: firstWeekday(hass.locale).substring(0, 3) }
: {}),
...period.calendar,
}
: undefined,
rolling_window: period.rolling_window,
});

View File

@@ -256,6 +256,13 @@ export const showConfigFlowDialog = (
);
},
renderMenuOptionDescription(hass, step, option) {
return hass.localize(
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.menu_option_descriptions.${option}`,
step.description_placeholders
);
},
renderLoadingDescription(hass, reason, handler, step) {
if (reason !== "loading_flow" && reason !== "loading_step") {
return "";

View File

@@ -137,6 +137,12 @@ export interface FlowConfig {
option: string
): string;
renderMenuOptionDescription(
hass: HomeAssistant,
step: DataEntryFlowStepMenu,
option: string
): string;
renderLoadingDescription(
hass: HomeAssistant,
loadingReason: LoadingReason,

View File

@@ -225,6 +225,13 @@ export const showOptionsFlowDialog = (
);
},
renderMenuOptionDescription(hass, step, option) {
return hass.localize(
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.menu_option_descriptions.${option}`,
step.description_placeholders
);
},
renderLoadingDescription(hass, reason) {
return (
hass.localize(`component.${configEntry.domain}.options.loading`) ||

View File

@@ -252,6 +252,13 @@ export const showSubConfigFlowDialog = (
);
},
renderMenuOptionDescription(hass, step, option) {
return hass.localize(
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.menu_option_descriptions.${option}`,
step.description_placeholders
);
},
renderLoadingDescription(hass, reason, handler, step) {
if (reason !== "loading_flow" && reason !== "loading_step") {
return "";

View File

@@ -1,5 +1,5 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-icon-next";
@@ -8,6 +8,7 @@ import type { DataEntryFlowStepMenu } from "../../data/data_entry_flow";
import type { HomeAssistant } from "../../types";
import type { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
import { stringCompare } from "../../common/string/compare";
@customElement("step-flow-menu")
class StepFlowMenu extends LitElement {
@@ -17,9 +18,18 @@ class StepFlowMenu extends LitElement {
@property({ attribute: false }) public step!: DataEntryFlowStepMenu;
protected shouldUpdate(changedProps: PropertyValues): boolean {
return (
changedProps.size > 1 ||
!changedProps.has("hass") ||
this.hass.localize !== changedProps.get("hass")?.localize
);
}
protected render(): TemplateResult {
let options: string[];
let translations: Record<string, string>;
let optionDescriptions: Record<string, string> = {};
if (Array.isArray(this.step.menu_options)) {
options = this.step.menu_options;
@@ -30,10 +40,36 @@ class StepFlowMenu extends LitElement {
this.step,
option
);
optionDescriptions[option] =
this.flowConfig.renderMenuOptionDescription(
this.hass,
this.step,
option
);
}
} else {
options = Object.keys(this.step.menu_options);
translations = this.step.menu_options;
optionDescriptions = Object.fromEntries(
options.map((key) => [
key,
this.flowConfig.renderMenuOptionDescription(
this.hass,
this.step,
key
),
])
);
}
if (this.step.sort) {
options = options.sort((a, b) =>
stringCompare(
translations[a]!,
translations[b]!,
this.hass.locale.language
)
);
}
const description = this.flowConfig.renderMenuDescription(
@@ -46,8 +82,18 @@ class StepFlowMenu extends LitElement {
<div class="options">
${options.map(
(option) => html`
<ha-list-item hasMeta .step=${option} @click=${this._handleStep}>
<ha-list-item
hasMeta
.step=${option}
@click=${this._handleStep}
?twoline=${optionDescriptions[option]}
>
<span>${translations[option]}</span>
${optionDescriptions[option]
? html`<span slot="secondary">
${optionDescriptions[option]}
</span>`
: nothing}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
@@ -73,11 +119,10 @@ class StepFlowMenu extends LitElement {
css`
.options {
margin-top: 20px;
margin-bottom: 8px;
margin-bottom: 16px;
}
.content {
padding-bottom: 16px;
border-bottom: 1px solid var(--divider-color);
}
.content + .options {
margin-top: 8px;

View File

@@ -3,11 +3,11 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import "../../components/ha-dialog-header";
import "../../components/ha-md-dialog";
import type { HaMdDialog } from "../../components/ha-md-dialog";
import "../../components/ha-dialog-header";
import "../../components/ha-svg-icon";
import "../../components/ha-button";
import "../../components/ha-textfield";
import type { HaTextField } from "../../components/ha-textfield";
import type { HomeAssistant } from "../../types";
@@ -52,7 +52,7 @@ class DialogBox extends LitElement {
return nothing;
}
const confirmPrompt = this._params.confirmation || this._params.prompt;
const confirmPrompt = this._params.confirmation || !!this._params.prompt;
const dialogTitle =
this._params.title ||
@@ -62,7 +62,7 @@ class DialogBox extends LitElement {
return html`
<ha-md-dialog
open
.disableCancelAction=${confirmPrompt || false}
.disableCancelAction=${confirmPrompt}
@closed=${this._dialogClosed}
type="alert"
aria-labelledby="dialog-box-title"
@@ -100,23 +100,22 @@ class DialogBox extends LitElement {
: ""}
</div>
<div slot="actions">
${confirmPrompt &&
html`
<ha-button
@click=${this._dismiss}
?dialogInitialFocus=${!this._params.prompt &&
this._params.destructive}
appearance="plain"
>
${this._params.dismissText
? this._params.dismissText
: this.hass.localize("ui.common.cancel")}
</ha-button>
`}
${confirmPrompt
? html`
<ha-button
@click=${this._dismiss}
?autofocus=${!this._params.prompt && this._params.destructive}
appearance="plain"
>
${this._params.dismissText
? this._params.dismissText
: this.hass.localize("ui.common.cancel")}
</ha-button>
`
: nothing}
<ha-button
@click=${this._confirm}
?dialogInitialFocus=${!this._params.prompt &&
!this._params.destructive}
?autofocus=${!this._params.prompt && !this._params.destructive}
variant=${this._params.destructive ? "danger" : "brand"}
>
${this._params.confirmText

View File

@@ -475,7 +475,6 @@ class MoreInfoUpdate extends LitElement {
bottom: 0;
margin: 0 -24px 0 -24px;
margin-bottom: calc(-1 * max(var(--safe-area-inset-bottom), 24px));
padding-bottom: var(--safe-area-inset-bottom);
box-sizing: border-box;
display: flex;
flex-direction: column;

View File

@@ -6,7 +6,9 @@ import memoizeOne from "memoize-one";
import { formatDateWeekdayShort } from "../../../common/datetime/format_date";
import { formatTime } from "../../../common/datetime/format_time";
import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-alert";
import "../../../components/ha-relative-time";
import "../../../components/ha-spinner";
import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
@@ -292,106 +294,101 @@ class MoreInfoWeather extends LitElement {
</div>
`
: nothing}
${forecast
? html`
<div class="section">
${this.hass.localize("ui.card.weather.forecast")}:
</div>
${supportedForecasts.length > 1
? html`<sl-tab-group
@sl-tab-show=${this._handleForecastTypeChanged}
<div class="section">
${this.hass.localize("ui.card.weather.forecast")}:
</div>
${supportedForecasts?.length > 1
? html`<sl-tab-group @sl-tab-show=${this._handleForecastTypeChanged}>
${supportedForecasts.map(
(forecastType) =>
html`<sl-tab
slot="nav"
.panel=${forecastType}
.active=${this._forecastType === forecastType}
>
${supportedForecasts.map(
(forecastType) =>
html`<sl-tab
slot="nav"
.panel=${forecastType}
.active=${this._forecastType === forecastType}
>
${this.hass!.localize(
`ui.card.weather.${forecastType}`
)}
</sl-tab>`
)}
</sl-tab-group>`
: nothing}
<div class="forecast">
${forecast.map((item) =>
this._showValue(item.templow) ||
this._showValue(item.temperature)
? html`
${this.hass!.localize(`ui.card.weather.${forecastType}`)}
</sl-tab>`
)}
</sl-tab-group>`
: nothing}
<div class="forecast">
${forecast?.length
? forecast.map((item) =>
this._showValue(item.templow) || this._showValue(item.temperature)
? html`
<div>
<div>
<div>
${dayNight
${dayNight
? html`
${formatDateWeekdayShort(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
<div class="daynight">
${item.is_daytime !== false
? this.hass!.localize("ui.card.weather.day")
: this.hass!.localize(
"ui.card.weather.night"
)}<br />
</div>
`
: hourly
? html`
${formatTime(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
`
: html`
${formatDateWeekdayShort(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
<div class="daynight">
${item.is_daytime !== false
? this.hass!.localize("ui.card.weather.day")
: this.hass!.localize(
"ui.card.weather.night"
)}<br />
</div>
`
: hourly
? html`
${formatTime(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
`
: html`
${formatDateWeekdayShort(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
`}
</div>
${this._showValue(item.condition)
? html`
<div class="forecast-image-icon">
${getWeatherStateIcon(
item.condition!,
this,
!(
item.is_daytime ||
item.is_daytime === undefined
)
)}
</div>
`
: nothing}
<div class="temp">
${this._showValue(item.temperature)
? html`${formatNumber(
item.temperature,
this.hass!.locale
)}°`
: "—"}
</div>
<div class="templow">
${this._showValue(item.templow)
? html`${formatNumber(
item.templow!,
this.hass!.locale
)}°`
: hourly
? nothing
: "—"}
</div>
`}
</div>
`
: nothing
)}
</div>
`
: nothing}
${this._showValue(item.condition)
? html`
<div class="forecast-image-icon">
${getWeatherStateIcon(
item.condition!,
this,
!(
item.is_daytime ||
item.is_daytime === undefined
)
)}
</div>
`
: nothing}
<div class="temp">
${this._showValue(item.temperature)
? html`${formatNumber(
item.temperature,
this.hass!.locale
)}°`
: "—"}
</div>
<div class="templow">
${this._showValue(item.templow)
? html`${formatNumber(
item.templow!,
this.hass!.locale
)}°`
: hourly
? nothing
: "—"}
</div>
</div>
`
: nothing
)
: html`<ha-spinner size="medium"></ha-spinner>`}
</div>
${this.stateObj.attributes.attribution
? html`
<div class="attribution">
@@ -589,6 +586,10 @@ class MoreInfoWeather extends LitElement {
.forecast-icon {
--mdc-icon-size: 40px;
}
.forecast ha-spinner {
height: 120px;
}
`,
];
}

View File

@@ -8,6 +8,7 @@ import {
mdiPencil,
mdiPencilOff,
mdiPencilOutline,
mdiTransitConnectionVariant,
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
@@ -311,6 +312,8 @@ export class MoreInfoDialog extends LitElement {
const isAdmin = this.hass.user!.is_admin;
const deviceId = this._getDeviceId();
const deviceType =
(deviceId && this.hass.devices[deviceId].entry_type) || "device";
const isDefaultView = this._currView === DEFAULT_VIEW && !this._childView;
const isSpecificInitialView =
@@ -434,11 +437,18 @@ export class MoreInfoDialog extends LitElement {
@request-selected=${this._goToDevice}
>
${this.hass.localize(
"ui.dialogs.more_info_control.device_info"
"ui.dialogs.more_info_control.device_or_service_info",
{
type: this.hass.localize(
`ui.dialogs.more_info_control.device_type.${deviceType}`
),
}
)}
<ha-svg-icon
slot="graphic"
.path=${mdiDevices}
.path=${deviceType === "service"
? mdiTransitConnectionVariant
: mdiDevices}
></ha-svg-icon>
</ha-list-item>
`
@@ -651,7 +661,10 @@ export class MoreInfoDialog extends LitElement {
ha-dialog {
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
--vertical-align-dialog: flex-start;
--dialog-surface-margin-top: 40px;
--dialog-surface-margin-top: max(
40px,
var(--safe-area-inset-top, 0px)
);
--dialog-content-padding: 0;
}

View File

@@ -281,11 +281,11 @@ export class QuickBar extends LitElement {
class="ha-scrollbar"
style=${styleMap({
height: this._narrow
? "calc(100vh - 56px)"
: `${Math.min(
? "calc(100vh - 56px - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))"
: `calc(${Math.min(
items.length * (commandMode ? 56 : 72) + 26,
500
)}px`,
)}px - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))`,
})}
.items=${items}
.renderItem=${this._renderItem}

View File

@@ -1,121 +1,148 @@
import { mdiAppleKeyboardCommand } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import { createCloseHeading } from "../../components/ha-dialog";
import type { HomeAssistant } from "../../types";
import { haStyleDialog } from "../../resources/styles";
import "../../components/ha-alert";
import "../../components/chips/ha-assist-chip";
import type { LocalizeKeys } from "../../common/translations/localize";
import "../../components/ha-alert";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-svg-icon";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { isMac } from "../../util/is_mac";
interface Text {
type: "text";
key: LocalizeKeys;
textTranslationKey: LocalizeKeys;
}
type ShortcutString = string | { key: LocalizeKeys };
interface LocalizedShortcut {
shortcutTranslationKey: LocalizeKeys;
}
type ShortcutString = string | LocalizedShortcut;
interface Shortcut {
type: "shortcut";
shortcut: ShortcutString[];
key: LocalizeKeys;
descriptionTranslationKey: LocalizeKeys;
}
interface Section {
key: LocalizeKeys;
titleTranslationKey: LocalizeKeys;
items: (Text | Shortcut)[];
}
const CTRL_CMD = "__CTRL_CMD__";
const _SHORTCUTS: Section[] = [
{
key: "ui.dialogs.shortcuts.searching.title",
titleTranslationKey: "ui.dialogs.shortcuts.searching.title",
items: [
{ type: "text", key: "ui.dialogs.shortcuts.searching.on_any_page" },
{
type: "shortcut",
textTranslationKey: "ui.dialogs.shortcuts.searching.on_any_page",
},
{
shortcut: ["C"],
key: "ui.dialogs.shortcuts.searching.search_command",
descriptionTranslationKey:
"ui.dialogs.shortcuts.searching.search_command",
},
{
type: "shortcut",
shortcut: ["E"],
key: "ui.dialogs.shortcuts.searching.search_entities",
descriptionTranslationKey:
"ui.dialogs.shortcuts.searching.search_entities",
},
{
type: "shortcut",
shortcut: ["D"],
key: "ui.dialogs.shortcuts.searching.search_devices",
descriptionTranslationKey:
"ui.dialogs.shortcuts.searching.search_devices",
},
{
type: "text",
key: "ui.dialogs.shortcuts.searching.on_pages_with_tables",
textTranslationKey:
"ui.dialogs.shortcuts.searching.on_pages_with_tables",
},
{
type: "shortcut",
shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "F"],
key: "ui.dialogs.shortcuts.searching.search_in_table",
shortcut: [CTRL_CMD, "F"],
descriptionTranslationKey:
"ui.dialogs.shortcuts.searching.search_in_table",
},
],
},
{
key: "ui.dialogs.shortcuts.assist.title",
titleTranslationKey: "ui.dialogs.shortcuts.assist.title",
items: [
{
type: "shortcut",
shortcut: ["A"],
key: "ui.dialogs.shortcuts.assist.open_assist",
descriptionTranslationKey: "ui.dialogs.shortcuts.assist.open_assist",
},
],
},
{
key: "ui.dialogs.shortcuts.automation_script.title",
titleTranslationKey: "ui.dialogs.shortcuts.automation_script.title",
items: [
{
type: "shortcut",
shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "V"],
key: "ui.dialogs.shortcuts.automation_script.paste",
shortcut: [CTRL_CMD, "C"],
descriptionTranslationKey:
"ui.dialogs.shortcuts.automation_script.copy",
},
{
type: "shortcut",
shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "S"],
key: "ui.dialogs.shortcuts.automation_script.save",
shortcut: [CTRL_CMD, "X"],
descriptionTranslationKey: "ui.dialogs.shortcuts.automation_script.cut",
},
],
},
{
key: "ui.dialogs.shortcuts.charts.title",
items: [
{
type: "shortcut",
shortcut: [
{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" },
{ key: "ui.dialogs.shortcuts.shortcuts.drag" },
CTRL_CMD,
{ shortcutTranslationKey: "ui.dialogs.shortcuts.keys.del" },
],
key: "ui.dialogs.shortcuts.charts.drag_to_zoom",
descriptionTranslationKey:
"ui.dialogs.shortcuts.automation_script.delete",
},
{
type: "shortcut",
shortcut: [
{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" },
{ key: "ui.dialogs.shortcuts.shortcuts.scroll_wheel" },
],
key: "ui.dialogs.shortcuts.charts.scroll_to_zoom",
shortcut: [CTRL_CMD, "V"],
descriptionTranslationKey:
"ui.dialogs.shortcuts.automation_script.paste",
},
{
type: "shortcut",
shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.double_click" }],
key: "ui.dialogs.shortcuts.charts.double_click",
shortcut: [CTRL_CMD, "S"],
descriptionTranslationKey:
"ui.dialogs.shortcuts.automation_script.save",
},
],
},
{
key: "ui.dialogs.shortcuts.other.title",
titleTranslationKey: "ui.dialogs.shortcuts.charts.title",
items: [
{
shortcut: [
CTRL_CMD,
{ shortcutTranslationKey: "ui.dialogs.shortcuts.shortcuts.drag" },
],
descriptionTranslationKey: "ui.dialogs.shortcuts.charts.drag_to_zoom",
},
{
shortcut: [
CTRL_CMD,
{
shortcutTranslationKey:
"ui.dialogs.shortcuts.shortcuts.scroll_wheel",
},
],
descriptionTranslationKey: "ui.dialogs.shortcuts.charts.scroll_to_zoom",
},
{
shortcut: [
{
shortcutTranslationKey:
"ui.dialogs.shortcuts.shortcuts.double_click",
},
],
descriptionTranslationKey: "ui.dialogs.shortcuts.charts.double_click",
},
],
},
{
titleTranslationKey: "ui.dialogs.shortcuts.other.title",
items: [
{
type: "shortcut",
shortcut: ["M"],
key: "ui.dialogs.shortcuts.other.my_link",
descriptionTranslationKey: "ui.dialogs.shortcuts.other.my_link",
},
],
},
@@ -137,17 +164,28 @@ class DialogShortcuts extends LitElement {
}
private _renderShortcut(
shortcuts: ShortcutString[],
translationKey: LocalizeKeys
shortcutKeys: ShortcutString[],
descriptionKey: LocalizeKeys
) {
const keys = shortcuts.map((shortcut) =>
typeof shortcut === "string" ? shortcut : this.hass.localize(shortcut.key)
);
return html`
<div class="shortcut">
${keys.map((key) => html` <span>${key.toUpperCase()}</span>`)}
${this.hass.localize(translationKey)}
${shortcutKeys.map(
(shortcutKey) =>
html`<span
>${shortcutKey === CTRL_CMD
? isMac
? html`<ha-svg-icon
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize("ui.panel.config.automation.editor.ctrl")
: typeof shortcutKey === "string"
? shortcutKey
: this.hass.localize(
shortcutKey.shortcutTranslationKey
)}</span
>`
)}
${this.hass.localize(descriptionKey)}
</div>
`;
}
@@ -171,16 +209,18 @@ class DialogShortcuts extends LitElement {
<div class="content">
${_SHORTCUTS.map(
(section) => html`
<h3>${this.hass.localize(section.key)}</h3>
<h3>${this.hass.localize(section.titleTranslationKey)}</h3>
<div class="items">
${section.items.map((item) => {
if (item.type === "text") {
return html`<p>${this.hass.localize(item.key)}</p>`;
if ("shortcut" in item) {
return this._renderShortcut(
(item as Shortcut).shortcut,
(item as Shortcut).descriptionTranslationKey
);
}
if (item.type === "shortcut") {
return this._renderShortcut(item.shortcut, item.key);
}
return nothing;
return html`<p>
${this.hass.localize((item as Text).textTranslationKey)}
</p>`;
})}
</div>
`
@@ -232,6 +272,10 @@ class DialogShortcuts extends LitElement {
.items p {
margin-bottom: 8px;
}
ha-svg-icon {
width: 12px;
}
`,
];
}

View File

@@ -35,6 +35,7 @@
align-items: center;
justify-content: center;
margin-bottom: 32px;
padding-top: var(--safe-area-inset-top);
}
.header img {

View File

@@ -44,7 +44,7 @@
}
#ha-launch-screen .ha-launch-screen-spacer-top {
flex: 1;
margin-top: calc( 2 * max(var(--safe-area-inset-bottom, 0px), 48px) + 46px );
margin-top: calc( 2 * max(var(--safe-area-inset-top, 0px), 48px) + 46px );
padding-top: 48px;
}
#ha-launch-screen .ha-launch-screen-spacer-bottom {

View File

@@ -19,8 +19,9 @@
height: auto;
padding: 32px 0;
}
.content {
max-width: 560px;
max-width: min(560px, calc(100vw - var(--safe-area-inset-right, 0px) - var(--safe-area-inset-left, 0px)));
margin: 0 auto;
padding: 0 16px;
box-sizing: content-box;
@@ -32,6 +33,7 @@
justify-content: flex-start;
margin-bottom: 32px;
margin-left: 32px;
padding-top: var(--safe-area-inset-top);
}
.header img {

View File

@@ -884,7 +884,13 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
}
.filter-dialog-content {
height: calc(100vh - 1px - 61px - var(--header-height));
height: calc(
100vh -
70px - var(--header-height, 0px) - var(
--safe-area-inset-top,
0px
) - var(--safe-area-inset-bottom, 0px)
);
display: flex;
flex-direction: column;
}

View File

@@ -146,6 +146,8 @@ export class HomeAssistantMain extends LitElement {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
--mdc-drawer-width: 56px;
--mdc-top-app-bar-width: calc(100% - var(--mdc-drawer-width));
--safe-area-content-inset-left: 0px;
--safe-area-content-inset-right: var(--safe-area-inset-right);
}
:host([expanded]) {
--mdc-drawer-width: calc(256px + var(--safe-area-inset-left));
@@ -153,6 +155,7 @@ export class HomeAssistantMain extends LitElement {
:host([modal]) {
--mdc-drawer-width: unset;
--mdc-top-app-bar-width: unset;
--safe-area-content-inset-left: var(--safe-area-inset-left);
}
partial-panel-resolver,
ha-sidebar {

View File

@@ -11,23 +11,54 @@ export const KeyboardShortcutMixin = <T extends Constructor<LitElement>>(
class extends superClass {
private _keydownEvent = (event: KeyboardEvent) => {
const supportedShortcuts = this.supportedShortcuts();
if ((event.ctrlKey || event.metaKey) && event.key in supportedShortcuts) {
if (
(event.ctrlKey || event.metaKey) &&
!event.shiftKey &&
!event.altKey &&
event.key in supportedShortcuts
) {
event.preventDefault();
supportedShortcuts[event.key]();
return;
}
const supportedSingleKeyShortcuts = this.supportedSingleKeyShortcuts();
if (event.key in supportedSingleKeyShortcuts) {
event.preventDefault();
supportedSingleKeyShortcuts[event.key]();
}
};
private _listenersAdded = false;
public connectedCallback() {
super.connectedCallback();
window.addEventListener("keydown", this._keydownEvent);
this.addKeyboardShortcuts();
}
public disconnectedCallback() {
window.removeEventListener("keydown", this._keydownEvent);
this.removeKeyboardShortcuts();
super.disconnectedCallback();
}
public addKeyboardShortcuts() {
if (this._listenersAdded) {
return;
}
this._listenersAdded = true;
window.addEventListener("keydown", this._keydownEvent);
}
public removeKeyboardShortcuts() {
this._listenersAdded = false;
window.removeEventListener("keydown", this._keydownEvent);
}
protected supportedShortcuts(): SupportedShortcuts {
return {};
}
protected supportedSingleKeyShortcuts(): SupportedShortcuts {
return {};
}
};

View File

@@ -53,6 +53,7 @@ export default class HaAutomationActionEditor extends LitElement {
this.disabled || (this.action.enabled === false && !this.yamlMode),
yaml: yamlMode,
indent: this.indent,
card: !this.inSidebar,
})}
>
${yamlMode
@@ -96,7 +97,7 @@ export default class HaAutomationActionEditor extends LitElement {
if (!ev.detail.isValid) {
return;
}
fireEvent(this, "value-changed", {
fireEvent(this, "yaml-changed", {
value: migrateAutomationAction(ev.detail.value),
});
}

View File

@@ -5,12 +5,12 @@ import {
mdiArrowUp,
mdiContentCopy,
mdiContentCut,
mdiContentDuplicate,
mdiDelete,
mdiDotsVertical,
mdiPlay,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiPlusCircleMultipleOutline,
mdiRenameBox,
mdiStopCircleOutline,
} from "@mdi/js";
@@ -26,6 +26,7 @@ import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-automation-row";
import type { HaAutomationRow } from "../../../../components/ha-automation-row";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
@@ -150,9 +151,14 @@ export default class HaAutomationActionRow extends LitElement {
@property({ type: Boolean }) public last?: boolean;
@property({ type: Boolean }) public highlight?: boolean;
@property({ type: Boolean, attribute: "sidebar" })
public optionsInSidebar = false;
@property({ type: Boolean, attribute: "sort-selected" })
public sortSelected = false;
@storage({
key: "automationClipboard",
state: false,
@@ -186,6 +192,13 @@ export default class HaAutomationActionRow extends LitElement {
@query("ha-automation-action-editor")
private _actionEditor?: HaAutomationActionEditor;
@query("ha-automation-row")
private _automationRowElement?: HaAutomationRow;
get selected() {
return this._selected;
}
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
@@ -254,138 +267,136 @@ export default class HaAutomationActionRow extends LitElement {
<ha-svg-icon .path=${mdiAlertCircleCheck}></ha-svg-icon>
</ha-tooltip>`
: nothing}
${!this.optionsInSidebar
? html`<ha-md-button-menu
quick
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
anchor-corner="end-end"
menu-corner="start-end"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-button-menu
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item .clickAction=${this._runAction}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.run"
)}
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._renameAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="start"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
</ha-md-menu-item>
${!this.optionsInSidebar
? html`<ha-md-menu-item .clickAction=${this._runAction}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.run"
)}
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._renameAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="start"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>`
: nothing}
<ha-md-menu-item
.clickAction=${this._moveUp}
.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._moveUp}
.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}
>
${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-md-menu-item
.clickAction=${this._moveDown}
.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-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!this._uiModeAvailable || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
${!this.optionsInSidebar
? html`<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!this._uiModeAvailable || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon
slot="start"
.path=${mdiPlaylistEdit}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
${this.action.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${this.action.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
${this.action.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${this.action.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>`
: nothing}
</ha-md-button-menu>
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>`
: nothing}
${!this.optionsInSidebar
? html`${this._warnings
? html`<ha-automation-editor-warning
@@ -431,7 +442,6 @@ export default class HaAutomationActionRow extends LitElement {
${this.optionsInSidebar
? html`<ha-automation-row
.disabled=${this.action.enabled === false}
@click=${this._toggleSidebar}
.leftChevron=${[
...ACTION_BUILDING_BLOCKS,
...ACTION_COMBINED_BLOCKS,
@@ -442,11 +452,17 @@ export default class HaAutomationActionRow extends LitElement {
))}
.collapsed=${this._collapsed}
.selected=${this._selected}
@toggle-collapsed=${this._toggleCollapse}
.highlight=${this.highlight}
.buildingBlock=${[
...ACTION_BUILDING_BLOCKS,
...ACTION_COMBINED_BLOCKS,
].includes(blockType!)}
.sortSelected=${this.sortSelected}
@click=${this._toggleSidebar}
@toggle-collapsed=${this._toggleCollapse}
@copy-row=${this._copyAction}
@cut-row=${this._cutAction}
@delete-row=${this._onDelete}
>${this._renderRow()}</ha-automation-row
>`
: html`
@@ -505,6 +521,15 @@ export default class HaAutomationActionRow extends LitElement {
};
private _runAction = async () => {
requestAnimationFrame(() => {
// @ts-ignore is supported in all browsers except firefox
if (this.scrollIntoViewIfNeeded) {
// @ts-ignore is supported in all browsers except firefox
this.scrollIntoViewIfNeeded();
return;
}
this.scrollIntoView();
});
const validated = await validateConfig(this.hass, {
actions: this.action,
});
@@ -614,6 +639,12 @@ export default class HaAutomationActionRow extends LitElement {
private _copyAction = () => {
this._setClipboard();
showToast(this, {
message: this.hass.localize(
"ui.panel.config.automation.editor.actions.copied_to_clipboard"
),
duration: 2000,
});
};
private _cutAction = () => {
@@ -622,6 +653,12 @@ export default class HaAutomationActionRow extends LitElement {
if (this._selected) {
fireEvent(this, "close-sidebar");
}
showToast(this, {
message: this.hass.localize(
"ui.panel.config.automation.editor.actions.cut_to_clipboard"
),
duration: 2000,
});
};
private _moveUp = () => {
@@ -655,8 +692,7 @@ export default class HaAutomationActionRow extends LitElement {
ev?.stopPropagation();
if (this._selected) {
this._selected = false;
fireEvent(this, "close-sidebar");
fireEvent(this, "request-close-sidebar");
return;
}
this.openSidebar();
@@ -670,16 +706,19 @@ export default class HaAutomationActionRow extends LitElement {
save: (value) => {
fireEvent(this, "value-changed", { value });
},
close: () => {
close: (focus?: boolean) => {
this._selected = false;
fireEvent(this, "close-sidebar");
if (focus) {
this.focus();
}
},
rename: () => {
this._renameAction();
},
toggleYamlMode: () => {
this._toggleYamlMode();
return this._yamlMode;
this.openSidebar();
},
disable: this._onDisable,
delete: this._onDelete,
@@ -697,10 +736,12 @@ export default class HaAutomationActionRow extends LitElement {
this._collapsed = false;
if (this.narrow) {
this.scrollIntoView({
block: "start",
behavior: "smooth",
});
window.setTimeout(() => {
this.scrollIntoView({
block: "start",
behavior: "smooth",
});
}, 180); // duration of transition of added padding for bottom sheet
}
}
@@ -747,6 +788,10 @@ export default class HaAutomationActionRow extends LitElement {
this._collapsed = !this._collapsed;
}
public focus() {
this._automationRowElement?.focus();
}
static styles = rowStyles;
}

View File

@@ -1,12 +1,11 @@
import { mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { listenMediaQuery } from "../../../../common/dom/media_query";
import { nextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button";
import "../../../../components/ha-sortable";
@@ -45,7 +44,7 @@ export default class HaAutomationAction extends LitElement {
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
false;
@state() private _showReorder = false;
@state() private _rowSortSelected?: number;
@state()
@storage({
@@ -61,36 +60,23 @@ export default class HaAutomationAction extends LitElement {
private _focusLastActionOnChange = false;
private _focusActionIndexOnChange?: number;
private _actionKeys = new WeakMap<Action, string>();
private _unsubMql?: () => void;
public connectedCallback() {
super.connectedCallback();
this._unsubMql = listenMediaQuery("(min-width: 600px)", (matches) => {
this._showReorder = matches;
});
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubMql?.();
this._unsubMql = undefined;
}
protected render() {
return html`
<ha-sortable
handle-selector=".handle"
draggable-selector="ha-automation-action-row"
.disabled=${!this._showReorder || this.disabled}
.disabled=${this.disabled}
group="actions"
invert-swap
@item-moved=${this._actionMoved}
@item-added=${this._actionAdded}
@item-removed=${this._actionRemoved}
>
<div class="rows">
<div class="rows ${!this.optionsInSidebar ? "no-sidebar" : ""}">
${repeat(
this.actions,
(action) => this._getKey(action),
@@ -109,12 +95,22 @@ export default class HaAutomationAction extends LitElement {
@move-up=${this._moveUp}
@value-changed=${this._actionChanged}
.hass=${this.hass}
?highlight=${this.highlightedActions?.includes(action)}
.highlight=${this.highlightedActions?.includes(action)}
.optionsInSidebar=${this.optionsInSidebar}
.sortSelected=${this._rowSortSelected === idx}
@stop-sort-selection=${this._stopSortSelection}
>
${this._showReorder && !this.disabled
${!this.disabled
? html`
<div class="handle" slot="icons">
<div
tabindex="0"
class="handle ${this._rowSortSelected === idx
? "active"
: ""}"
slot="icons"
@keydown=${this._handleDragKeydown}
.index=${idx}
>
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
`
@@ -154,19 +150,27 @@ export default class HaAutomationAction extends LitElement {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("actions") && this._focusLastActionOnChange) {
this._focusLastActionOnChange = false;
if (
changedProps.has("actions") &&
(this._focusLastActionOnChange ||
this._focusActionIndexOnChange !== undefined)
) {
const mode = this._focusLastActionOnChange ? "new" : "moved";
const row = this.shadowRoot!.querySelector<HaAutomationActionRow>(
"ha-automation-action-row:last-of-type"
`ha-automation-action-row:${mode === "new" ? "last-of-type" : `nth-of-type(${this._focusActionIndexOnChange! + 1})`}`
)!;
this._focusLastActionOnChange = false;
this._focusActionIndexOnChange = undefined;
row.updateComplete.then(() => {
// on new condition open the settings in the sidebar, except for building blocks
const type = getAutomationActionType(row.action);
if (
type &&
this.optionsInSidebar &&
!ACTION_BUILDING_BLOCKS.includes(type)
(!ACTION_BUILDING_BLOCKS.includes(type) || mode === "moved")
) {
row.openSidebar();
if (this.narrow) {
@@ -176,8 +180,14 @@ export default class HaAutomationAction extends LitElement {
});
}
}
row.expand();
row.focus();
if (mode === "new") {
row.expand();
}
if (!this.optionsInSidebar) {
row.focus();
}
});
}
}
@@ -246,18 +256,30 @@ export default class HaAutomationAction extends LitElement {
return this._actionKeys.get(action)!;
}
private _moveUp(ev) {
private async _moveUp(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index - 1;
this._move(index, newIndex);
if (!(ev.target as HaAutomationActionRow).first) {
const newIndex = index - 1;
this._move(index, newIndex);
if (this._rowSortSelected === index) {
this._rowSortSelected = newIndex;
}
ev.target.focus();
}
}
private _moveDown(ev) {
private async _moveDown(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index + 1;
this._move(index, newIndex);
if (!(ev.target as HaAutomationActionRow).last) {
const newIndex = index + 1;
this._move(index, newIndex);
if (this._rowSortSelected === index) {
this._rowSortSelected = newIndex;
}
ev.target.focus();
}
}
private _move(oldIndex: number, newIndex: number) {
@@ -277,6 +299,9 @@ export default class HaAutomationAction extends LitElement {
private async _actionAdded(ev: CustomEvent): Promise<void> {
ev.stopPropagation();
const { index, data } = ev.detail;
const item = ev.detail.item as HaAutomationActionRow;
const selected = item.selected;
let actions = [
...this.actions.slice(0, index),
data,
@@ -284,6 +309,9 @@ export default class HaAutomationAction extends LitElement {
];
// Add action locally to avoid UI jump
this.actions = actions;
if (selected) {
this._focusActionIndexOnChange = actions.length === 1 ? 0 : index;
}
await nextRender();
if (this.actions !== actions) {
// Ensure action is added even after update
@@ -292,6 +320,9 @@ export default class HaAutomationAction extends LitElement {
data,
...this.actions.slice(index),
];
if (selected) {
this._focusActionIndexOnChange = actions.length === 1 ? 0 : index;
}
}
fireEvent(this, "value-changed", { value: actions });
}
@@ -335,14 +366,21 @@ export default class HaAutomationAction extends LitElement {
});
}
static styles = [
automationRowsStyles,
css`
:host([root]) .rows {
padding-right: 8px;
}
`,
];
private _handleDragKeydown(ev: KeyboardEvent) {
if (ev.key === "Enter" || ev.key === " ") {
ev.stopPropagation();
this._rowSortSelected =
this._rowSortSelected === undefined
? (ev.target as any).index
: undefined;
}
}
private _stopSortSelection() {
this._rowSortSelected = undefined;
}
static styles = automationRowsStyles;
}
declare global {

View File

@@ -3,6 +3,7 @@ import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { stringCompare } from "../../../../../common/string/compare";
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-list-item";
import "../../../../../components/ha-select";
@@ -66,6 +67,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
.value=${this.action.condition}
naturalMenuWidth
@selected=${this._typeChanged}
@closed=${stopPropagation}
>
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`

View File

@@ -1,4 +1,9 @@
import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js";
import {
mdiAppleKeyboardCommand,
mdiClose,
mdiContentPaste,
mdiPlus,
} from "@mdi/js";
import Fuse from "fuse.js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
@@ -41,11 +46,14 @@ import {
} from "../../../data/integration";
import { TRIGGER_GROUPS, TRIGGER_ICONS } from "../../../data/trigger";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { HaFuse } from "../../../resources/fuse";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
import { HaFuse } from "../../../resources/fuse";
const TYPES = {
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },
@@ -85,7 +93,10 @@ const ENTITY_DOMAINS_OTHER = new Set([
const ENTITY_DOMAINS_MAIN = new Set(["notify"]);
@customElement("add-automation-element-dialog")
class DialogAddAutomationElement extends LitElement implements HassDialog {
class DialogAddAutomationElement
extends KeyboardShortcutMixin(LitElement)
implements HassDialog
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: AddAutomationElementDialogParams;
@@ -108,9 +119,14 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
@state() private _height?: number;
@state() private _narrow = false;
public showDialog(params): void {
this._params = params;
this._group = params.group;
this.addKeyboardShortcuts();
if (this._params?.type === "action") {
this.hass.loadBackendTranslation("services");
this._fetchManifests();
@@ -120,9 +136,12 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
this._fullScreen = matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
this._narrow = matchMedia("(max-width: 870px)").matches;
}
public closeDialog() {
this.removeKeyboardShortcuts();
if (this._params) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -555,15 +574,37 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
.value=${PASTE_VALUE}
@click=${this._selected}
>
${this.hass.localize(
`ui.panel.config.automation.editor.${this._params.type}s.paste`
)}
<span slot="supporting-text"
>${this.hass.localize(
// @ts-ignore
`ui.panel.config.automation.editor.${this._params.type}s.type.${this._params.clipboardItem}.label`
)}</span
>
<div class="shortcut-label">
<div class="label">
<div>
${this.hass.localize(
`ui.panel.config.automation.editor.${this._params.type}s.paste`
)}
</div>
<div class="supporting-text">
${this.hass.localize(
// @ts-ignore
`ui.panel.config.automation.editor.${this._params.type}s.type.${this._params.clipboardItem}.label`
)}
</div>
</div>
${!this._narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span>V</span>
</span>`
: nothing}
</div>
<ha-svg-icon
slot="start"
.path=${mdiContentPaste}
@@ -571,7 +612,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
><ha-svg-icon slot="end" .path=${mdiPlus}></ha-svg-icon>
</ha-md-list-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
: ""}
: nothing}
${repeat(
items,
(item) => item.key,
@@ -637,6 +678,30 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
this._filter = ev.detail.value;
}
private _addClipboard = () => {
if (this._params?.clipboardItem) {
this._params!.add(PASTE_VALUE);
showToast(this, {
message: this.hass.localize(
"ui.panel.config.automation.editor.item_pasted",
{
item: this.hass.localize(
// @ts-ignore
`ui.panel.config.automation.editor.${this._params.type}s.type.${this._params.clipboardItem}.label`
),
}
),
});
this.closeDialog();
}
};
protected supportedShortcuts(): SupportedShortcuts {
return {
v: () => this._addClipboard(),
};
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -660,6 +725,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
max-width: 100vw;
--md-list-item-leading-space: 24px;
--md-list-item-trailing-space: 24px;
--md-list-item-supporting-text-font: var(--ha-font-size-s);
}
ha-md-list-item img {
width: 24px;
@@ -668,6 +734,27 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
display: block;
margin: 0 16px;
}
.shortcut-label {
display: flex;
gap: 12px;
justify-content: space-between;
}
.shortcut-label .supporting-text {
color: var(--secondary-text-color);
font-size: var(--ha-font-size-s);
}
.shortcut-label .shortcut {
--mdc-icon-size: 12px;
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
.shortcut-label .shortcut span {
font-size: var(--ha-font-size-s);
font-family: var(--ha-font-family-code);
color: var(--ha-color-text-secondary);
}
`,
];
}

View File

@@ -28,6 +28,8 @@ export default class HaAutomationConditionEditor extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
@property({ type: Boolean, reflect: true }) public selected = false;
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
@@ -55,6 +57,7 @@ export default class HaAutomationConditionEditor extends LitElement {
(this.condition.enabled === false && !this.yamlMode),
yaml: yamlMode,
indent: this.indent,
card: !this.inSidebar,
})}
>
${yamlMode
@@ -100,8 +103,7 @@ export default class HaAutomationConditionEditor extends LitElement {
if (!ev.detail.isValid) {
return;
}
// @ts-ignore
fireEvent(this, "value-changed", { value: ev.detail.value, yaml: true });
fireEvent(this, "yaml-changed", { value: ev.detail.value });
}
private _onUiChanged(ev: CustomEvent) {

View File

@@ -4,12 +4,12 @@ import {
mdiArrowUp,
mdiContentCopy,
mdiContentCut,
mdiContentDuplicate,
mdiDelete,
mdiDotsVertical,
mdiFlask,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiPlusCircleMultipleOutline,
mdiRenameBox,
mdiStopCircleOutline,
} from "@mdi/js";
@@ -26,6 +26,7 @@ import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-automation-row";
import type { HaAutomationRow } from "../../../../components/ha-automation-row";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
@@ -52,6 +53,7 @@ import {
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import "../ha-automation-editor-warning";
import { rowStyles } from "../styles";
import "./ha-automation-condition-editor";
@@ -115,6 +117,11 @@ export default class HaAutomationConditionRow extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean }) public highlight?: boolean;
@property({ type: Boolean, attribute: "sort-selected" })
public sortSelected = false;
@state() private _collapsed = true;
@state() private _warnings?: string[];
@@ -145,6 +152,13 @@ export default class HaAutomationConditionRow extends LitElement {
@query("ha-automation-condition-editor")
public conditionEditor?: HaAutomationConditionEditor;
@query("ha-automation-row")
private _automationRowElement?: HaAutomationRow;
get selected() {
return this._selected;
}
private _renderRow() {
return html`
<ha-svg-icon
@@ -160,143 +174,140 @@ export default class HaAutomationConditionRow extends LitElement {
<slot name="icons" slot="icons"></slot>
<ha-md-button-menu
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
>
</ha-icon-button>
${!this.optionsInSidebar
? html`<ha-md-button-menu
quick
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
anchor-corner="end-end"
menu-corner="start-end"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
>
</ha-icon-button>
${!this.optionsInSidebar
? html`
<ha-md-menu-item .clickAction=${this._testCondition}>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.test"
)}
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._renameCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._testCondition}>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.test"
)}
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._renameCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="start"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._duplicateCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="start"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
`
: nothing}
<ha-md-menu-item
.clickAction=${this._cutCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
.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._moveUp}
.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}
>
${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-md-menu-item
.clickAction=${this._moveDown}
.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>
${!this.optionsInSidebar
? html`<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${this._uiSupported(this.condition.condition) ||
!!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon
slot="start"
.path=${mdiPlaylistEdit}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${this._uiSupported(this.condition.condition) ||
!!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
${this.condition.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${this.condition.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
${this.condition.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${this.condition.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>`
: nothing}
</ha-md-button-menu>
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>`
: nothing}
${!this.optionsInSidebar
? html`${this._warnings
? html`<ha-automation-editor-warning
@@ -351,11 +362,16 @@ export default class HaAutomationConditionRow extends LitElement {
)}
.collapsed=${this._collapsed}
.selected=${this._selected}
@click=${this._toggleSidebar}
@toggle-collapsed=${this._toggleCollapse}
.highlight=${this.highlight}
.buildingBlock=${CONDITION_BUILDING_BLOCKS.includes(
this.condition.condition
)}
.sortSelected=${this.sortSelected}
@click=${this._toggleSidebar}
@toggle-collapsed=${this._toggleCollapse}
@copy-row=${this._copyCondition}
@cut-row=${this._cutCondition}
@delete-row=${this._onDelete}
>${this._renderRow()}</ha-automation-row
>`
: html`
@@ -472,6 +488,15 @@ export default class HaAutomationConditionRow extends LitElement {
this._testingResult = undefined;
this._testing = true;
const condition = this.condition;
requestAnimationFrame(() => {
// @ts-ignore is supported in all browsers expect firefox
if (this.scrollIntoViewIfNeeded) {
// @ts-ignore is supported in all browsers expect firefox
this.scrollIntoViewIfNeeded();
return;
}
this.scrollIntoView();
});
try {
const validateResult = await validateConfig(this.hass, {
@@ -562,6 +587,12 @@ export default class HaAutomationConditionRow extends LitElement {
private _copyCondition = () => {
this._setClipboard();
showToast(this, {
message: this.hass.localize(
"ui.panel.config.automation.editor.conditions.copied_to_clipboard"
),
duration: 2000,
});
};
private _cutCondition = () => {
@@ -570,6 +601,12 @@ export default class HaAutomationConditionRow extends LitElement {
if (this._selected) {
fireEvent(this, "close-sidebar");
}
showToast(this, {
message: this.hass.localize(
"ui.panel.config.automation.editor.conditions.cut_to_clipboard"
),
duration: 2000,
});
};
private _moveUp = () => {
@@ -630,8 +667,7 @@ export default class HaAutomationConditionRow extends LitElement {
ev?.stopPropagation();
if (this._selected) {
this._selected = false;
fireEvent(this, "close-sidebar");
fireEvent(this, "request-close-sidebar");
return;
}
this.openSidebar();
@@ -643,16 +679,19 @@ export default class HaAutomationConditionRow extends LitElement {
save: (value) => {
fireEvent(this, "value-changed", { value });
},
close: () => {
close: (focus?: boolean) => {
this._selected = false;
fireEvent(this, "close-sidebar");
if (focus) {
this.focus();
}
},
rename: () => {
this._renameCondition();
},
toggleYamlMode: () => {
this._toggleYamlMode();
return this._yamlMode;
this.openSidebar();
},
disable: this._onDisable,
delete: this._onDelete,
@@ -668,10 +707,12 @@ export default class HaAutomationConditionRow extends LitElement {
this._collapsed = false;
if (this.narrow) {
this.scrollIntoView({
block: "start",
behavior: "smooth",
});
window.setTimeout(() => {
this.scrollIntoView({
block: "start",
behavior: "smooth",
});
}, 180); // duration of transition of added padding for bottom sheet
}
}
@@ -684,6 +725,10 @@ export default class HaAutomationConditionRow extends LitElement {
this._collapsed = !this._collapsed;
}
public focus() {
this._automationRowElement?.focus();
}
static get styles(): CSSResultGroup {
return [
rowStyles,

View File

@@ -1,12 +1,11 @@
import { mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { listenMediaQuery } from "../../../../common/dom/media_query";
import { nextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
@@ -43,7 +42,7 @@ export default class HaAutomationCondition extends LitElement {
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
false;
@state() private _showReorder = false;
@state() private _rowSortSelected?: number;
@state()
@storage({
@@ -59,23 +58,10 @@ export default class HaAutomationCondition extends LitElement {
private _focusLastConditionOnChange = false;
private _focusConditionIndexOnChange?: number;
private _conditionKeys = new WeakMap<Condition, string>();
private _unsubMql?: () => void;
public connectedCallback() {
super.connectedCallback();
this._unsubMql = listenMediaQuery("(min-width: 600px)", (matches) => {
this._showReorder = matches;
});
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubMql?.();
this._unsubMql = undefined;
}
protected updated(changedProperties: PropertyValues) {
if (!changedProperties.has("conditions")) {
return;
@@ -100,16 +86,25 @@ export default class HaAutomationCondition extends LitElement {
fireEvent(this, "value-changed", {
value: updatedConditions,
});
} else if (this._focusLastConditionOnChange) {
this._focusLastConditionOnChange = false;
} else if (
this._focusLastConditionOnChange ||
this._focusConditionIndexOnChange !== undefined
) {
const mode = this._focusLastConditionOnChange ? "new" : "moved";
const row = this.shadowRoot!.querySelector<HaAutomationConditionRow>(
"ha-automation-condition-row:last-of-type"
`ha-automation-condition-row:${mode === "new" ? "last-of-type" : `nth-of-type(${this._focusConditionIndexOnChange! + 1})`}`
)!;
this._focusLastConditionOnChange = false;
this._focusConditionIndexOnChange = undefined;
row.updateComplete.then(() => {
// on new condition open the settings in the sidebar, except for building blocks
if (
this.optionsInSidebar &&
!CONDITION_BUILDING_BLOCKS.includes(row.condition.condition)
(!CONDITION_BUILDING_BLOCKS.includes(row.condition.condition) ||
mode === "moved")
) {
row.openSidebar();
if (this.narrow) {
@@ -119,8 +114,14 @@ export default class HaAutomationCondition extends LitElement {
});
}
}
row.expand();
row.focus();
if (mode === "new") {
row.expand();
}
if (!this.optionsInSidebar) {
row.focus();
}
});
}
}
@@ -145,14 +146,14 @@ export default class HaAutomationCondition extends LitElement {
<ha-sortable
handle-selector=".handle"
draggable-selector="ha-automation-condition-row"
.disabled=${!this._showReorder || this.disabled}
.disabled=${this.disabled}
group="conditions"
invert-swap
@item-moved=${this._conditionMoved}
@item-added=${this._conditionAdded}
@item-removed=${this._conditionRemoved}
>
<div class="rows">
<div class="rows ${!this.optionsInSidebar ? "no-sidebar" : ""}">
${repeat(
this.conditions.filter((c) => typeof c === "object"),
(condition) => this._getKey(condition),
@@ -172,12 +173,22 @@ export default class HaAutomationCondition extends LitElement {
@move-up=${this._moveUp}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
?highlight=${this.highlightedConditions?.includes(cond)}
.highlight=${this.highlightedConditions?.includes(cond)}
.optionsInSidebar=${this.optionsInSidebar}
.sortSelected=${this._rowSortSelected === idx}
@stop-sort-selection=${this._stopSortSelection}
>
${this._showReorder && !this.disabled
${!this.disabled
? html`
<div class="handle" slot="icons">
<div
tabindex="0"
class="handle ${this._rowSortSelected === idx
? "active"
: ""}"
slot="icons"
@keydown=${this._handleDragKeydown}
.index=${idx}
>
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
`
@@ -266,15 +277,27 @@ export default class HaAutomationCondition extends LitElement {
private _moveUp(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index - 1;
this._move(index, newIndex);
if (!(ev.target as HaAutomationConditionRow).first) {
const newIndex = index - 1;
this._move(index, newIndex);
if (this._rowSortSelected === index) {
this._rowSortSelected = newIndex;
}
ev.target.focus();
}
}
private _moveDown(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index + 1;
this._move(index, newIndex);
if (!(ev.target as HaAutomationConditionRow).last) {
const newIndex = index + 1;
this._move(index, newIndex);
if (this._rowSortSelected === index) {
this._rowSortSelected = newIndex;
}
ev.target.focus();
}
}
private _move(oldIndex: number, newIndex: number) {
@@ -294,6 +317,8 @@ export default class HaAutomationCondition extends LitElement {
private async _conditionAdded(ev: CustomEvent): Promise<void> {
ev.stopPropagation();
const { index, data } = ev.detail;
const item = ev.detail.item as HaAutomationConditionRow;
const selected = item.selected;
let conditions = [
...this.conditions.slice(0, index),
data,
@@ -301,6 +326,9 @@ export default class HaAutomationCondition extends LitElement {
];
// Add condition locally to avoid UI jump
this.conditions = conditions;
if (selected) {
this._focusConditionIndexOnChange = conditions.length === 1 ? 0 : index;
}
await nextRender();
if (this.conditions !== conditions) {
// Ensure condition is added even after update
@@ -309,6 +337,9 @@ export default class HaAutomationCondition extends LitElement {
data,
...this.conditions.slice(index),
];
if (selected) {
this._focusConditionIndexOnChange = conditions.length === 1 ? 0 : index;
}
}
fireEvent(this, "value-changed", { value: conditions });
}
@@ -354,14 +385,21 @@ export default class HaAutomationCondition extends LitElement {
});
}
static styles = [
automationRowsStyles,
css`
:host([root]) .rows {
padding-right: 8px;
}
`,
];
private _handleDragKeydown(ev: KeyboardEvent) {
if (ev.key === "Enter" || ev.key === " ") {
ev.stopPropagation();
this._rowSortSelected =
this._rowSortSelected === undefined
? (ev.target as any).index
: undefined;
}
}
private _stopSortSelection() {
this._rowSortSelected = undefined;
}
static styles = automationRowsStyles;
}
declare global {

View File

@@ -1,7 +1,6 @@
import { consume } from "@lit/context";
import {
mdiCog,
mdiContentDuplicate,
mdiContentSave,
mdiDebugStepOver,
mdiDelete,
@@ -11,13 +10,12 @@ import {
mdiPlay,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiPlusCircleMultipleOutline,
mdiRenameBox,
mdiRobotConfused,
mdiStopCircleOutline,
mdiTag,
mdiTransitConnection,
mdiUnfoldLessHorizontal,
mdiUnfoldMoreHorizontal,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
@@ -337,7 +335,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
)}
<ha-svg-icon
slot="graphic"
.path=${mdiContentDuplicate}
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
</ha-list-item>
@@ -371,30 +369,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
<ha-svg-icon slot="graphic" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-list-item>
${!useBlueprint
? html`
<ha-list-item graphic="icon" @click=${this._collapseAll}>
<ha-svg-icon
slot="graphic"
.path=${mdiUnfoldLessHorizontal}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.editor.collapse_all"
)}
</ha-list-item>
<ha-list-item graphic="icon" @click=${this._expandAll}>
<ha-svg-icon
slot="graphic"
.path=${mdiUnfoldMoreHorizontal}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.editor.expand_all"
)}
</ha-list-item>
`
: nothing}
<li divider role="separator"></li>
<ha-list-item
@@ -432,62 +406,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
class=${this._mode === "yaml" ? "yaml-mode" : ""}
@subscribe-automation-config=${this._subscribeAutomationConfig}
>
<div class="error-wrapper">
${this._errors || stateObj?.state === UNAVAILABLE
? html`<ha-alert
alert-type="error"
.title=${stateObj?.state === UNAVAILABLE
? this.hass.localize(
"ui.panel.config.automation.editor.unavailable"
)
: undefined}
>
${this._errors || this._validationErrors}
${stateObj?.state === UNAVAILABLE
? html`<ha-svg-icon
slot="icon"
.path=${mdiRobotConfused}
></ha-svg-icon>`
: nothing}
</ha-alert>`
: ""}
${this._blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.automation.editor.confirm_take_control"
)}
<div slot="action" style="display: flex;">
<ha-button
appearance="plain"
@click=${this._takeControlSave}
>${this.hass.localize("ui.common.yes")}</ha-button
>
<ha-button
appearance="plain"
@click=${this._revertBlueprint}
>${this.hass.localize("ui.common.no")}</ha-button
>
</div>
</ha-alert>`
: this._readOnly
? html`<ha-alert alert-type="warning" dismissable
>${this.hass.localize(
"ui.panel.config.automation.editor.read_only"
)}
<ha-button
appearance="filled"
size="small"
variant="warning"
slot="action"
@click=${this._duplicate}
>
${this.hass.localize(
"ui.panel.config.automation.editor.migrate"
)}
</ha-button>
</ha-alert>`
: nothing}
</div>
${this._mode === "gui"
? html`
<div>
@@ -499,7 +417,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.isWide=${this.isWide}
.stateObj=${stateObj}
.config=${this._config}
.disabled=${Boolean(this._readOnly)}
.disabled=${this._readOnly}
.saving=${this._saving}
.dirty=${this._dirty}
@value-changed=${this._valueChanged}
@@ -513,13 +431,94 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.isWide=${this.isWide}
.stateObj=${stateObj}
.config=${this._config}
.disabled=${Boolean(this._readOnly)}
.disabled=${this._readOnly}
.dirty=${this._dirty}
.saving=${this._saving}
@value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
@editor-save=${this._handleSaveAutomation}
></manual-automation-editor>
>
<div class="alert-wrapper" slot="alerts">
${this._errors || stateObj?.state === UNAVAILABLE
? html`<ha-alert
alert-type="error"
.title=${stateObj?.state === UNAVAILABLE
? this.hass.localize(
"ui.panel.config.automation.editor.unavailable"
)
: undefined}
>
${this._errors || this._validationErrors}
${stateObj?.state === UNAVAILABLE
? html`<ha-svg-icon
slot="icon"
.path=${mdiRobotConfused}
></ha-svg-icon>`
: nothing}
</ha-alert>`
: nothing}
${this._blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.automation.editor.confirm_take_control"
)}
<div slot="action" style="display: flex;">
<ha-button
appearance="plain"
@click=${this._takeControlSave}
>${this.hass.localize(
"ui.common.yes"
)}</ha-button
>
<ha-button
appearance="plain"
@click=${this._revertBlueprint}
>${this.hass.localize(
"ui.common.no"
)}</ha-button
>
</div>
</ha-alert>`
: this._readOnly
? html`<ha-alert
alert-type="warning"
dismissable
>${this.hass.localize(
"ui.panel.config.automation.editor.read_only"
)}
<ha-button
appearance="filled"
size="small"
variant="warning"
slot="action"
@click=${this._duplicate}
>
${this.hass.localize(
"ui.panel.config.automation.editor.migrate"
)}
</ha-button>
</ha-alert>`
: nothing}
${stateObj?.state === "off"
? html`
<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.automation.editor.disabled"
)}
<ha-button
size="small"
slot="action"
@click=${this._toggle}
>
${this.hass.localize(
"ui.panel.config.automation.editor.enable"
)}
</ha-button>
</ha-alert>
`
: nothing}
</div>
</manual-automation-editor>
`}
</div>
`
@@ -542,7 +541,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
</ha-button>
</ha-alert>
`
: ""}
: nothing}
<ha-yaml-editor
copy-clipboard
.hass=${this.hass}
@@ -1113,6 +1112,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
protected supportedShortcuts(): SupportedShortcuts {
return {
s: () => this._handleSaveAutomation(),
c: () => this._copySelectedRow(),
x: () => this._cutSelectedRow(),
Delete: () => this._deleteSelectedRow(),
Backspace: () => this._deleteSelectedRow(),
};
}
@@ -1124,14 +1127,28 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
return this._confirmUnsavedChanged();
}
// @ts-ignore
private _collapseAll() {
this._manualEditor?.collapseAll();
}
// @ts-ignore
private _expandAll() {
this._manualEditor?.expandAll();
}
private _copySelectedRow() {
this._manualEditor?.copySelectedRow();
}
private _cutSelectedRow() {
this._manualEditor?.cutSelectedRow();
}
private _deleteSelectedRow() {
this._manualEditor?.deleteSelectedRow();
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -1156,22 +1173,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
display: block;
}
:not(.yaml-mode) > .error-wrapper {
position: absolute;
top: 4px;
z-index: 3;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
:not(.yaml-mode) > .error-wrapper ha-alert {
background-color: var(--card-background-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
border-radius: var(--ha-border-radius-sm);
}
manual-automation-editor {
max-width: 1540px;
padding: 0 12px;

View File

@@ -292,7 +292,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
extraTemplate: (automation) =>
automation.labels.length
? html`<ha-data-table-labels
@label-clicked=${this._labelClicked}
@label-clicked=${narrow ? undefined : this._labelClicked}
.labels=${automation.labels}
></ha-data-table-labels>`
: nothing,

View File

@@ -1,6 +1,5 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-bottom-sheet";
import type { HaBottomSheet } from "../../../components/ha-bottom-sheet";
import {
@@ -34,6 +33,8 @@ export default class HaAutomationSidebar extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
@state() private _yamlMode = false;
@query("ha-bottom-sheet") private _bottomSheetElement?: HaBottomSheet;
@@ -52,8 +53,9 @@ export default class HaAutomationSidebar extends LitElement {
.narrow=${this.narrow}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
.sidebarKey=${this.sidebarKey}
@toggle-yaml-mode=${this._toggleYamlMode}
@close-sidebar=${this._handleCloseSidebar}
@close-sidebar=${this.triggerCloseSidebar}
></ha-automation-sidebar-trigger>
`;
}
@@ -67,8 +69,9 @@ export default class HaAutomationSidebar extends LitElement {
.narrow=${this.narrow}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
.sidebarKey=${this.sidebarKey}
@toggle-yaml-mode=${this._toggleYamlMode}
@close-sidebar=${this._handleCloseSidebar}
@close-sidebar=${this.triggerCloseSidebar}
></ha-automation-sidebar-condition>
`;
}
@@ -82,8 +85,9 @@ export default class HaAutomationSidebar extends LitElement {
.narrow=${this.narrow}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
.sidebarKey=${this.sidebarKey}
@toggle-yaml-mode=${this._toggleYamlMode}
@close-sidebar=${this._handleCloseSidebar}
@close-sidebar=${this.triggerCloseSidebar}
></ha-automation-sidebar-action>
`;
}
@@ -96,7 +100,7 @@ export default class HaAutomationSidebar extends LitElement {
.isWide=${this.isWide}
.narrow=${this.narrow}
.disabled=${this.disabled}
@close-sidebar=${this._handleCloseSidebar}
@close-sidebar=${this.triggerCloseSidebar}
></ha-automation-sidebar-option>
`;
}
@@ -110,8 +114,9 @@ export default class HaAutomationSidebar extends LitElement {
.narrow=${this.narrow}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
.sidebarKey=${this.sidebarKey}
@toggle-yaml-mode=${this._toggleYamlMode}
@close-sidebar=${this._handleCloseSidebar}
@close-sidebar=${this.triggerCloseSidebar}
></ha-automation-sidebar-script-field-selector>
`;
}
@@ -125,8 +130,9 @@ export default class HaAutomationSidebar extends LitElement {
.narrow=${this.narrow}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
.sidebarKey=${this.sidebarKey}
@toggle-yaml-mode=${this._toggleYamlMode}
@close-sidebar=${this._handleCloseSidebar}
@close-sidebar=${this.triggerCloseSidebar}
></ha-automation-sidebar-script-field>
`;
}
@@ -182,8 +188,8 @@ export default class HaAutomationSidebar extends LitElement {
return undefined;
}
private _handleCloseSidebar(ev: CustomEvent) {
ev.stopPropagation();
public triggerCloseSidebar(ev?: CustomEvent) {
ev?.stopPropagation();
if (this.narrow) {
this._bottomSheetElement?.closeSheet();
return;
@@ -193,21 +199,16 @@ export default class HaAutomationSidebar extends LitElement {
}
private _closeSidebar() {
this.config?.close();
this.config?.close(true);
}
private _toggleYamlMode = () => {
this._yamlMode = this.config!.toggleYamlMode();
fireEvent(this, "value-changed", {
value: {
...this.config,
yamlMode: this._yamlMode,
},
});
(this.config as ActionSidebarConfig)?.toggleYamlMode();
};
static styles = css`
:host {
z-index: 6;
outline: none;
height: 100%;
--ha-card-border-radius: var(
@@ -235,5 +236,8 @@ declare global {
interface HASSDomEvents {
"toggle-yaml-mode": undefined;
"yaml-changed": {
value: unknown;
};
}
}

View File

@@ -3,7 +3,13 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { load } from "js-yaml";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import {
customElement,
property,
query,
queryAll,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import {
any,
@@ -23,12 +29,12 @@ import {
extractSearchParam,
removeSearchParam,
} from "../../../common/url/search-params";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/ha-button";
import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
import "../../../components/ha-markdown";
import type {
ActionSidebarConfig,
AutomationConfig,
Condition,
ManualAutomationConfig,
@@ -93,11 +99,15 @@ export class HaManualAutomationEditor extends LitElement {
@state() private _sidebarConfig?: SidebarConfig;
@query(".content")
private _contentElement?: HTMLDivElement;
@state() private _sidebarKey?: string;
@query("ha-automation-sidebar") private _sidebarElement?: HaAutomationSidebar;
@queryAll("ha-automation-action, ha-automation-condition")
private _collapsableElements?: NodeListOf<
HaAutomationAction | HaAutomationCondition
>;
private _previousConfig?: ManualAutomationConfig;
public connectedCallback() {
@@ -112,20 +122,6 @@ export class HaManualAutomationEditor extends LitElement {
private _renderContent() {
return html`
${this.stateObj?.state === "off"
? html`
<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.automation.editor.disabled"
)}
<ha-button size="small" slot="action" @click=${this._enable}>
${this.hass.localize(
"ui.panel.config.automation.editor.enable"
)}
</ha-button>
</ha-alert>
`
: nothing}
${this.config.description
? html`<ha-markdown
class="description"
@@ -170,7 +166,7 @@ export class HaManualAutomationEditor extends LitElement {
.disabled=${this.disabled || this.saving}
.narrow=${this.narrow}
@open-sidebar=${this._openSidebar}
@request-close-sidebar=${this._closeSidebar}
@request-close-sidebar=${this._triggerCloseSidebar}
@close-sidebar=${this._handleCloseSidebar}
root
sidebar
@@ -217,7 +213,7 @@ export class HaManualAutomationEditor extends LitElement {
.disabled=${this.disabled || this.saving}
.narrow=${this.narrow}
@open-sidebar=${this._openSidebar}
@request-close-sidebar=${this._closeSidebar}
@request-close-sidebar=${this._triggerCloseSidebar}
@close-sidebar=${this._handleCloseSidebar}
root
sidebar
@@ -259,7 +255,7 @@ export class HaManualAutomationEditor extends LitElement {
.highlightedActions=${this._pastedConfig?.actions || []}
@value-changed=${this._actionChanged}
@open-sidebar=${this._openSidebar}
@request-close-sidebar=${this._closeSidebar}
@request-close-sidebar=${this._triggerCloseSidebar}
@close-sidebar=${this._handleCloseSidebar}
.hass=${this.hass}
.narrow=${this.narrow}
@@ -274,37 +270,44 @@ export class HaManualAutomationEditor extends LitElement {
return html`
<div
class=${classMap({
"split-view": true,
"sidebar-hidden": !this._sidebarConfig,
"has-sidebar": this._sidebarConfig && !this.narrow,
})}
>
<div class="content-wrapper">
<div class="content">${this._renderContent()}</div>
<ha-fab
slot="fab"
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this.saving}
extended
@click=${this._saveAutomation}
<div
class="content ${this._sidebarConfig && this.narrow
? "has-bottom-sheet"
: ""}"
>
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab>
<slot name="alerts"></slot>
${this._renderContent()}
</div>
<div class="fab-positioner">
<ha-fab
slot="fab"
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this.saving}
extended
@click=${this._saveAutomation}
>
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab>
</div>
</div>
<div class="sidebar-positioner">
<ha-automation-sidebar
tabindex="-1"
class=${classMap({ hidden: !this._sidebarConfig })}
.isWide=${this.isWide}
.hass=${this.hass}
.narrow=${this.narrow}
.config=${this._sidebarConfig}
@value-changed=${this._sidebarConfigChanged}
.disabled=${this.disabled}
.sidebarKey=${this._sidebarKey}
></ha-automation-sidebar>
</div>
<ha-automation-sidebar
tabindex="-1"
class=${classMap({
sidebar: true,
overlay: !this.isWide && !this.narrow,
rtl: computeRTL(this.hass),
})}
.isWide=${this.isWide}
.hass=${this.hass}
.narrow=${this.narrow}
.config=${this._sidebarConfig}
@value-changed=${this._sidebarConfigChanged}
.disabled=${this.disabled}
></ha-automation-sidebar>
</div>
`;
}
@@ -330,6 +333,7 @@ export class HaManualAutomationEditor extends LitElement {
// deselect previous selected row
this._sidebarConfig?.close?.();
this._sidebarConfig = ev.detail;
this._sidebarKey = JSON.stringify(this._sidebarConfig);
await this._sidebarElement?.updateComplete;
this._sidebarElement?.focus();
@@ -347,16 +351,18 @@ export class HaManualAutomationEditor extends LitElement {
};
}
private _closeSidebar() {
private _triggerCloseSidebar() {
if (this._sidebarConfig) {
if (this._sidebarElement) {
this._sidebarElement.triggerCloseSidebar();
return;
}
this._sidebarConfig?.close();
}
}
private _handleCloseSidebar() {
this._sidebarConfig = undefined;
// fix content shift when bottom rows are scrolled into view
this._contentElement?.scrollIntoView();
}
private _triggerChanged(ev: CustomEvent): void {
@@ -386,17 +392,8 @@ export class HaManualAutomationEditor extends LitElement {
});
}
private async _enable(): Promise<void> {
if (!this.hass || !this.stateObj) {
return;
}
await this.hass.callService("automation", "turn_on", {
entity_id: this.stateObj.entity_id,
});
}
private _saveAutomation() {
this._closeSidebar();
this._triggerCloseSidebar();
fireEvent(this, "save-automation");
}
@@ -501,7 +498,12 @@ export class HaManualAutomationEditor extends LitElement {
if (normalized) {
ev.preventDefault();
if (this.dirty) {
if (
this.dirty ||
ensureArray(this.config.triggers)?.length ||
ensureArray(this.config.conditions)?.length ||
ensureArray(this.config.actions)?.length
) {
const result = await new Promise<boolean>((resolve) => {
showPasteReplaceDialog(this, {
domain: "automation",
@@ -610,24 +612,36 @@ export class HaManualAutomationEditor extends LitElement {
});
}
private _getCollapsableElements() {
return this.shadowRoot!.querySelectorAll<
HaAutomationAction | HaAutomationCondition
>("ha-automation-action, ha-automation-condition");
}
public expandAll() {
this._getCollapsableElements().forEach((element) => {
this._collapsableElements?.forEach((element) => {
element.expandAll();
});
}
public collapseAll() {
this._getCollapsableElements().forEach((element) => {
this._collapsableElements?.forEach((element) => {
element.collapseAll();
});
}
public copySelectedRow() {
if ((this._sidebarConfig as ActionSidebarConfig)?.copy) {
(this._sidebarConfig as ActionSidebarConfig).copy();
}
}
public cutSelectedRow() {
if ((this._sidebarConfig as ActionSidebarConfig)?.cut) {
(this._sidebarConfig as ActionSidebarConfig).cut();
}
}
public deleteSelectedRow() {
if ((this._sidebarConfig as ActionSidebarConfig)?.delete) {
(this._sidebarConfig as ActionSidebarConfig).delete();
}
}
static get styles(): CSSResultGroup {
return [
saveFabStyles,
@@ -656,9 +670,8 @@ export class HaManualAutomationEditor extends LitElement {
line-height: 0;
}
ha-alert {
display: block;
margin-bottom: 16px;
.description {
margin-top: 16px;
}
`,
];

View File

@@ -2,9 +2,9 @@ import { consume } from "@lit/context";
import {
mdiArrowDown,
mdiArrowUp,
mdiContentDuplicate,
mdiDelete,
mdiDotsVertical,
mdiPlusCircleMultipleOutline,
mdiRenameBox,
} from "@mdi/js";
import type { CSSResultGroup } from "lit";
@@ -17,6 +17,7 @@ import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_de
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import "../../../../components/ha-automation-row";
import type { HaAutomationRow } from "../../../../components/ha-automation-row";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
@@ -63,6 +64,9 @@ export default class HaAutomationOptionRow extends LitElement {
@property({ type: Boolean, attribute: "sidebar" })
public optionsInSidebar = false;
@property({ type: Boolean, attribute: "sort-selected" })
public sortSelected = false;
@state() private _expanded = false;
@state() private _selected = false;
@@ -79,6 +83,13 @@ export default class HaAutomationOptionRow extends LitElement {
@query("ha-automation-action")
private _actionElement?: HaAutomationAction;
@query("ha-automation-row")
private _automationRowElement?: HaAutomationRow;
get selected() {
return this._selected;
}
private _expandedChanged(ev) {
if (ev.currentTarget.id !== "option") {
return;
@@ -123,14 +134,17 @@ export default class HaAutomationOptionRow extends LitElement {
<slot name="icons" slot="icons"></slot>
${this.option
${this.option && !this.optionsInSidebar
? html`
<ha-md-button-menu
quick
slot="icons"
@click=${preventDefaultStopPropagation}
@closed=${stopPropagation}
@keydown=${stopPropagation}
positioning="fixed"
anchor-corner="end-end"
menu-corner="start-end"
>
<ha-icon-button
slot="trigger"
@@ -138,35 +152,28 @@ export default class HaAutomationOptionRow extends LitElement {
.path=${mdiDotsVertical}
></ha-icon-button>
${!this.optionsInSidebar
? html`
<ha-md-menu-item
@click=${this._renameOption}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiRenameBox}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
@click=${this._renameOption}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
@click=${this._duplicateOption}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-md-menu-item>
`
: nothing}
<ha-md-menu-item
@click=${this._duplicateOption}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="start"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
@click=${this._moveUp}
@@ -175,7 +182,7 @@ export default class HaAutomationOptionRow extends LitElement {
${this.hass.localize(
"ui.panel.config.automation.editor.move_up"
)}
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon>
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
@@ -185,25 +192,23 @@ export default class HaAutomationOptionRow extends LitElement {
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon>
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon>
</ha-md-menu-item>
${!this.optionsInSidebar
? html`<ha-md-menu-item
@click=${this._removeOption}
class="warning"
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
<ha-svg-icon
class="warning"
slot="graphic"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>`
: nothing}
<ha-md-menu-item
@click=${this._removeOption}
class="warning"
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
`
: nothing}
@@ -215,6 +220,7 @@ export default class HaAutomationOptionRow extends LitElement {
return html`<div
class=${classMap({
"card-content": true,
card: !this.optionsInSidebar,
indent: this.optionsInSidebar,
selected: this._selected,
hidden: this.optionsInSidebar && this._collapsed,
@@ -269,8 +275,10 @@ export default class HaAutomationOptionRow extends LitElement {
left-chevron
.collapsed=${this._collapsed}
.selected=${this._selected}
.sortSelected=${this.sortSelected}
@click=${this._toggleSidebar}
@toggle-collapsed=${this._toggleCollapse}
@delete-row=${this._removeOption}
>${this._renderRow()}</ha-automation-row
>`
: html`
@@ -373,8 +381,7 @@ export default class HaAutomationOptionRow extends LitElement {
ev?.stopPropagation();
if (this._selected) {
this._selected = false;
fireEvent(this, "close-sidebar");
fireEvent(this, "request-close-sidebar");
return;
}
this.openSidebar();
@@ -382,14 +389,16 @@ export default class HaAutomationOptionRow extends LitElement {
public openSidebar(): void {
fireEvent(this, "open-sidebar", {
close: () => {
close: (focus?: boolean) => {
this._selected = false;
fireEvent(this, "close-sidebar");
if (focus) {
this.focus();
}
},
rename: () => {
this._renameOption();
},
toggleYamlMode: () => false, // no yaml mode for options
delete: this._removeOption,
duplicate: this._duplicateOption,
defaultOption: !!this.defaultActions,
@@ -398,10 +407,12 @@ export default class HaAutomationOptionRow extends LitElement {
this._collapsed = false;
if (this.narrow) {
this.scrollIntoView({
block: "start",
behavior: "smooth",
});
window.setTimeout(() => {
this.scrollIntoView({
block: "start",
behavior: "smooth",
});
}, 180); // duration of transition of added padding for bottom sheet
}
}
@@ -438,6 +449,10 @@ export default class HaAutomationOptionRow extends LitElement {
this._collapsed = !this._collapsed;
}
public focus() {
this._automationRowElement?.focus();
}
static get styles(): CSSResultGroup {
return [
rowStyles,

View File

@@ -1,12 +1,11 @@
import { mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import type { PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { listenMediaQuery } from "../../../../common/dom/media_query";
import { nextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button";
import "../../../../components/ha-sortable";
@@ -34,7 +33,7 @@ export default class HaAutomationOption extends LitElement {
@property({ type: Boolean, attribute: "show-default" })
public showDefaultActions = false;
@state() private _showReorder = false;
@state() private _rowSortSelected?: number;
@state()
@storage({
@@ -50,36 +49,23 @@ export default class HaAutomationOption extends LitElement {
private _focusLastOptionOnChange = false;
private _focusOptionIndexOnChange?: number;
private _optionsKeys = new WeakMap<Option, string>();
private _unsubMql?: () => void;
public connectedCallback() {
super.connectedCallback();
this._unsubMql = listenMediaQuery("(min-width: 600px)", (matches) => {
this._showReorder = matches;
});
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubMql?.();
this._unsubMql = undefined;
}
protected render() {
return html`
<ha-sortable
handle-selector=".handle"
draggable-selector="ha-automation-option-row"
.disabled=${!this._showReorder || this.disabled}
.disabled=${this.disabled}
group="options"
invert-swap
@item-moved=${this._optionMoved}
@item-added=${this._optionAdded}
@item-removed=${this._optionRemoved}
>
<div class="rows">
<div class="rows ${!this.optionsInSidebar ? "no-sidebar" : ""}">
${repeat(
this.options,
(option) => this._getKey(option),
@@ -98,10 +84,20 @@ export default class HaAutomationOption extends LitElement {
@value-changed=${this._optionChanged}
.hass=${this.hass}
.optionsInSidebar=${this.optionsInSidebar}
.sortSelected=${this._rowSortSelected === idx}
@stop-sort-selection=${this._stopSortSelection}
>
${this._showReorder && !this.disabled
${!this.disabled
? html`
<div class="handle" slot="icons">
<div
tabindex="0"
class="handle ${this._rowSortSelected === idx
? "active"
: ""}"
slot="icons"
@keydown=${this._handleDragKeydown}
.index=${idx}
>
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
`
@@ -143,12 +139,20 @@ export default class HaAutomationOption extends LitElement {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("options") && this._focusLastOptionOnChange) {
this._focusLastOptionOnChange = false;
if (
changedProps.has("options") &&
(this._focusLastOptionOnChange ||
this._focusOptionIndexOnChange !== undefined)
) {
const mode = this._focusLastOptionOnChange ? "new" : "moved";
const row = this.shadowRoot!.querySelector<HaAutomationOptionRow>(
"ha-automation-option-row:last-of-type"
`ha-automation-option-row:${mode === "new" ? "last-of-type" : `nth-of-type(${this._focusOptionIndexOnChange! + 1})`}`
)!;
this._focusLastOptionOnChange = false;
this._focusOptionIndexOnChange = undefined;
row.updateComplete.then(() => {
if (this.narrow) {
row.scrollIntoView({
@@ -156,8 +160,16 @@ export default class HaAutomationOption extends LitElement {
behavior: "smooth",
});
}
row.expand();
row.focus();
if (mode === "new") {
row.expand();
}
if (this.optionsInSidebar) {
row.openSidebar();
} else {
row.focus();
}
});
}
}
@@ -187,15 +199,27 @@ export default class HaAutomationOption extends LitElement {
private _moveUp(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index - 1;
this._move(index, newIndex);
if (!(ev.target as HaAutomationOptionRow).first) {
const newIndex = index - 1;
this._move(index, newIndex);
if (this._rowSortSelected === index) {
this._rowSortSelected = newIndex;
}
ev.target.focus();
}
}
private _moveDown(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index + 1;
this._move(index, newIndex);
if (!(ev.target as HaAutomationOptionRow).last) {
const newIndex = index + 1;
this._move(index, newIndex);
if (this._rowSortSelected === index) {
this._rowSortSelected = newIndex;
}
ev.target.focus();
}
}
private _move(oldIndex: number, newIndex: number) {
@@ -215,6 +239,9 @@ export default class HaAutomationOption extends LitElement {
private async _optionAdded(ev: CustomEvent): Promise<void> {
ev.stopPropagation();
const { index, data } = ev.detail;
const item = ev.detail.item as HaAutomationOptionRow;
const selected = item.selected;
const options = [
...this.options.slice(0, index),
data,
@@ -222,6 +249,9 @@ export default class HaAutomationOption extends LitElement {
];
// Add option locally to avoid UI jump
this.options = options;
if (selected) {
this._focusOptionIndexOnChange = options.length === 1 ? 0 : index;
}
await nextRender();
fireEvent(this, "value-changed", { value: this.options });
}
@@ -269,7 +299,28 @@ export default class HaAutomationOption extends LitElement {
fireEvent(this, "show-default-actions");
};
static styles = automationRowsStyles;
private _handleDragKeydown(ev: KeyboardEvent) {
if (ev.key === "Enter" || ev.key === " ") {
ev.stopPropagation();
this._rowSortSelected =
this._rowSortSelected === undefined
? (ev.target as any).index
: undefined;
}
}
private _stopSortSelection() {
this._rowSortSelected = undefined;
}
static styles = [
automationRowsStyles,
css`
:host([root]) .rows {
padding-inline-end: 8px;
}
`,
];
}
declare global {

View File

@@ -1,11 +1,12 @@
import { customElement, property, state } from "lit/decorators";
import { css, type CSSResultGroup, html, LitElement, nothing } from "lit";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import type { HomeAssistant } from "../../../../types";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import "../../../../components/ha-button";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../trigger/ha-automation-trigger-row";
import "../../../../components/ha-yaml-editor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { PasteReplaceDialogParams } from "./show-dialog-paste-replace";
@customElement("ha-dialog-paste-replace")

View File

@@ -1,16 +1,18 @@
import {
mdiAppleKeyboardCommand,
mdiContentCopy,
mdiContentCut,
mdiContentDuplicate,
mdiDelete,
mdiPlay,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiPlusCircleMultipleOutline,
mdiRenameBox,
mdiStopCircleOutline,
} from "@mdi/js";
import { html, LitElement } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import type { LocalizeKeys } from "../../../../common/translations/localize";
@@ -20,6 +22,7 @@ import { ACTION_BUILDING_BLOCKS } from "../../../../data/action";
import type { ActionSidebarConfig } from "../../../../data/automation";
import type { RepeatAction } from "../../../../data/script";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
import { getAutomationActionType } from "../action/ha-automation-action-row";
import { getRepeatType } from "../action/types/ha-automation-action-repeat";
@@ -41,6 +44,8 @@ export default class HaAutomationSidebarAction extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
@state() private _warnings?: string[];
@query(".sidebar-editor")
@@ -100,18 +105,24 @@ export default class HaAutomationSidebarAction extends LitElement {
<span slot="subtitle">${subtitle}</span>
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.run}>
${this.hass.localize("ui.panel.config.automation.editor.actions.run")}
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize("ui.panel.config.automation.editor.actions.run")}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.rename}
.disabled=${!!disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-divider
slot="menu-items"
@@ -123,36 +134,85 @@ export default class HaAutomationSidebarAction extends LitElement {
.clickAction=${this.config.duplicate}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon slot="start" .path=${mdiContentDuplicate}></ha-svg-icon>
<ha-svg-icon
slot="start"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.copy}
.disabled=${this.disabled}
>
${this.hass.localize("ui.panel.config.automation.editor.triggers.copy")}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
${!this.narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span>C</span>
</span>`
: nothing}
</div>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.cut}
.disabled=${this.disabled}
>
${this.hass.localize("ui.panel.config.automation.editor.triggers.cut")}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
${!this.narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span>X</span>
</span>`
: nothing}
</div>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this._toggleYamlMode}
.disabled=${!this.config.uiSupported || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-divider
slot="menu-items"
@@ -160,13 +220,16 @@ export default class HaAutomationSidebarAction extends LitElement {
tabindex="-1"
></ha-md-divider>
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.disable}>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
)}
<ha-svg-icon
slot="start"
.path=${this.disabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
@@ -174,25 +237,51 @@ export default class HaAutomationSidebarAction extends LitElement {
.disabled=${this.disabled}
class="warning"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
${!this.narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span
>${this.hass.localize(
"ui.panel.config.automation.editor.del"
)}</span
>
</span>`
: nothing}
</div>
</ha-md-menu-item>
${description && !this.yamlMode
? html`<div class="description">${description}</div>`
: html`<ha-automation-action-editor
class="sidebar-editor"
.hass=${this.hass}
.action=${actionConfig}
.yamlMode=${this.yamlMode}
.uiSupported=${this.config.uiSupported}
@value-changed=${this._valueChangedSidebar}
sidebar
narrow
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-action-editor>`}
: keyed(
this.sidebarKey,
html`<ha-automation-action-editor
class="sidebar-editor"
.hass=${this.hass}
.action=${actionConfig}
.yamlMode=${this.yamlMode}
.uiSupported=${this.config.uiSupported}
@value-changed=${this._valueChangedSidebar}
@yaml-changed=${this._yamlChangedSidebar}
sidebar
narrow
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-action-editor>`
)}
</ha-automation-sidebar-card>`;
}
@@ -220,6 +309,12 @@ export default class HaAutomationSidebarAction extends LitElement {
}
}
private _yamlChangedSidebar(ev: CustomEvent) {
ev.stopPropagation();
this.config?.save?.(ev.detail.value);
}
private _toggleYamlMode = () => {
fireEvent(this, "toggle-yaml-mode");
};

View File

@@ -1,4 +1,6 @@
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { mdiClose, mdiDotsVertical } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import {
customElement,
@@ -43,7 +45,22 @@ export default class HaAutomationSidebarCard extends LitElement {
@state() private _contentScrolled = false;
@query(".card-content") private _contentElement?: HTMLDivElement;
@state() private _contentScrollable = false;
@query(".card-content") private _contentElement!: HTMLDivElement;
private _contentSize = new ResizeController(this, {
target: null,
callback: (entries) => {
if (entries[0]?.target) {
this._canScrollDown(entries[0].target);
}
},
});
protected firstUpdated(_changedProperties: PropertyValues): void {
this._contentSize.observe(this._contentElement);
}
protected render() {
return html`
@@ -67,10 +84,13 @@ export default class HaAutomationSidebarCard extends LitElement {
<slot slot="subtitle" name="subtitle"></slot>
<slot name="overflow-menu" slot="actionItems">
<ha-md-button-menu
quick
@click=${this._openOverflowMenu}
@keydown=${stopPropagation}
@closed=${stopPropagation}
.positioning=${this.narrow ? "absolute" : "fixed"}
anchor-corner="end-end"
menu-corner="start-end"
>
<ha-icon-button
slot="trigger"
@@ -91,14 +111,29 @@ export default class HaAutomationSidebarCard extends LitElement {
<div class="card-content" @scroll=${this._onScroll}>
<slot></slot>
</div>
${this.narrow
? html`
<div
class="fade ${this._contentScrollable ? "scrollable" : ""}"
></div>
`
: nothing}
</ha-card>
`;
}
@eventOptions({ passive: true })
private _onScroll() {
const top = this._contentElement?.scrollTop ?? 0;
private _onScroll(ev) {
const top = ev.target.scrollTop ?? 0;
this._contentScrolled = top > 0;
this._canScrollDown(ev.target);
}
private _canScrollDown(element: HTMLElement) {
this._contentScrollable =
(element.scrollHeight ?? 0) - (element.clientHeight ?? 0) >
(element.scrollTop ?? 0);
}
private _closeSidebar() {
@@ -122,6 +157,7 @@ export default class HaAutomationSidebarCard extends LitElement {
@media all and (max-width: 870px) {
ha-card.mobile {
border: none;
box-shadow: none;
}
ha-card.mobile {
border-bottom-right-radius: var(--ha-border-radius-square);
@@ -135,7 +171,6 @@ export default class HaAutomationSidebarCard extends LitElement {
transition: box-shadow 180ms ease-in-out;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
z-index: 6;
position: relative;
background-color: var(
--ha-dialog-surface-background,
@@ -144,12 +179,28 @@ export default class HaAutomationSidebarCard extends LitElement {
}
ha-dialog-header.scrolled {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
box-shadow: var(--bar-box-shadow);
}
.fade {
position: fixed;
bottom: -12px;
left: 0;
right: 0;
height: 12px;
pointer-events: none;
transition: box-shadow 180ms ease-in-out;
}
.fade.scrollable {
box-shadow: var(--bar-box-shadow);
transform: rotate(180deg);
}
.card-content {
max-height: calc(100% - 80px);
overflow: auto;
margin-top: 0;
}
@media (min-width: 450px) and (min-height: 500px) {

View File

@@ -1,21 +1,30 @@
import {
mdiAppleKeyboardCommand,
mdiContentCopy,
mdiContentCut,
mdiContentDuplicate,
mdiDelete,
mdiFlask,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiPlusCircleMultipleOutline,
mdiRenameBox,
mdiStopCircleOutline,
} from "@mdi/js";
import { html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { keyed } from "lit/directives/keyed";
import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import type { ConditionSidebarConfig } from "../../../../data/automation";
import {
testCondition,
type ConditionSidebarConfig,
} from "../../../../data/automation";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
import { validateConfig } from "../../../../data/config";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
import "../condition/ha-automation-condition-editor";
import type HaAutomationConditionEditor from "../condition/ha-automation-condition-editor";
import { sidebarEditorStyles } from "../styles";
@@ -35,8 +44,14 @@ export default class HaAutomationSidebarCondition extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
@state() private _warnings?: string[];
@state() private _testing = false;
@state() private _testingResult?: boolean;
@query(".sidebar-editor")
public editor?: HaAutomationConditionEditor;
@@ -50,6 +65,10 @@ export default class HaAutomationSidebarCondition extends LitElement {
}
}
}
// Reset testing state when condition changes
if (changedProperties.has("sidebarKey")) {
this._testing = false;
}
}
protected render() {
@@ -85,21 +104,27 @@ export default class HaAutomationSidebarCondition extends LitElement {
>
<span slot="title">${title}</span>
<span slot="subtitle">${subtitle}</span>
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.test}>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.test"
)}
<ha-md-menu-item slot="menu-items" .clickAction=${this._testCondition}>
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.test"
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.rename}
.disabled=${!!disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-divider
@@ -113,10 +138,16 @@ export default class HaAutomationSidebarCondition extends LitElement {
.clickAction=${this.config.duplicate}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon slot="start" .path=${mdiContentDuplicate}></ha-svg-icon>
<ha-svg-icon
slot="start"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-menu-item
@@ -124,8 +155,28 @@ export default class HaAutomationSidebarCondition extends LitElement {
.clickAction=${this.config.copy}
.disabled=${this.disabled}
>
${this.hass.localize("ui.panel.config.automation.editor.triggers.copy")}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
${!this.narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span>C</span>
</span>`
: nothing}
</div>
</ha-md-menu-item>
<ha-md-menu-item
@@ -133,18 +184,41 @@ export default class HaAutomationSidebarCondition extends LitElement {
.clickAction=${this.config.cut}
.disabled=${this.disabled}
>
${this.hass.localize("ui.panel.config.automation.editor.triggers.cut")}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
${!this.narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span>X</span>
</span>`
: nothing}
</div>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this._toggleYamlMode}
.disabled=${!this.config.uiSupported || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-divider
slot="menu-items"
@@ -152,13 +226,16 @@ export default class HaAutomationSidebarCondition extends LitElement {
tabindex="-1"
></ha-md-divider>
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.disable}>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
)}
<ha-svg-icon
slot="start"
.path=${this.disabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
@@ -166,26 +243,128 @@ export default class HaAutomationSidebarCondition extends LitElement {
.disabled=${this.disabled}
class="warning"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
${!this.narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span
>${this.hass.localize(
"ui.panel.config.automation.editor.del"
)}</span
>
</span>`
: nothing}
</div>
</ha-md-menu-item>
${description && !this.yamlMode
? html`<div class="description">${description}</div>`
: html`<ha-automation-condition-editor
class="sidebar-editor"
.hass=${this.hass}
.condition=${this.config.config}
.yamlMode=${this.yamlMode}
.uiSupported=${this.config.uiSupported}
@value-changed=${this._valueChangedSidebar}
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-condition-editor> `}
: keyed(
this.sidebarKey,
html`<ha-automation-condition-editor
class="sidebar-editor"
.hass=${this.hass}
.condition=${this.config.config}
.yamlMode=${this.yamlMode}
.uiSupported=${this.config.uiSupported}
@value-changed=${this._valueChangedSidebar}
@yaml-changed=${this._yamlChangedSidebar}
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
sidebar
></ha-automation-condition-editor>`
)}
<div class="testing-wrapper">
<div
class="testing ${classMap({
active: this._testing,
pass: this._testingResult === true,
error: this._testingResult === false,
narrow: this.narrow,
})}"
>
${this._testingResult
? this.hass.localize(
"ui.panel.config.automation.editor.conditions.testing_pass"
)
: this.hass.localize(
"ui.panel.config.automation.editor.conditions.testing_error"
)}
</div>
</div>
</ha-automation-sidebar-card>`;
}
private _testCondition = async () => {
if (this._testing) {
return;
}
this._testingResult = undefined;
this._testing = true;
const condition = this.config.config;
try {
const validateResult = await validateConfig(this.hass, {
conditions: condition,
});
// Abort if condition changed.
if (this.config.config !== condition) {
this._testing = false;
return;
}
if (!validateResult.conditions.valid) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.conditions.invalid_condition"
),
text: validateResult.conditions.error,
});
this._testing = false;
return;
}
let result: { result: boolean };
try {
result = await testCondition(this.hass, condition);
} catch (err: any) {
if (this.config.config !== condition) {
this._testing = false;
return;
}
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.conditions.test_failed"
),
text: err.message,
});
this._testing = false;
return;
}
this._testingResult = result.result;
} finally {
setTimeout(() => {
this._testing = false;
}, 2500);
}
};
private _handleUiModeNotAvailable(ev: CustomEvent) {
this._warnings = handleStructError(this.hass, ev.detail).warnings;
if (!this.yamlMode) {
@@ -208,11 +387,67 @@ export default class HaAutomationSidebarCondition extends LitElement {
}
}
private _yamlChangedSidebar(ev: CustomEvent) {
ev.stopPropagation();
this.config?.save?.(ev.detail.value);
}
private _toggleYamlMode = () => {
fireEvent(this, "toggle-yaml-mode");
};
static styles = sidebarEditorStyles;
static styles = [
sidebarEditorStyles,
css`
ha-automation-sidebar-card {
position: relative;
}
.testing-wrapper {
position: absolute;
top: 0px;
right: 0px;
left: 0px;
margin: -1px;
overflow: hidden;
border-top-right-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
border-top-left-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
pointer-events: none;
height: 100px;
}
.testing {
--testing-color: var(--divider-color, #e0e0e0);
text-transform: uppercase;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-bold);
background-color: var(--testing-color);
color: var(--text-primary-color);
max-height: 0px;
transition:
max-height 0.3s ease-in-out,
padding-top 0.3s ease-in-out;
text-align: center;
}
.testing.active.narrow {
padding-top: 16px;
}
.testing.active {
max-height: 100%;
}
.testing.error {
--testing-color: var(--accent-color);
}
.testing.pass {
--testing-color: var(--success-color);
}
`,
];
}
declare global {

View File

@@ -1,8 +1,14 @@
import { mdiContentDuplicate, mdiDelete, mdiRenameBox } from "@mdi/js";
import { html, LitElement } from "lit";
import {
mdiAppleKeyboardCommand,
mdiDelete,
mdiPlusCircleMultipleOutline,
mdiRenameBox,
} from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import type { OptionSidebarConfig } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
import { sidebarEditorStyles } from "../styles";
import "./ha-automation-sidebar-card";
@@ -52,10 +58,13 @@ export default class HaAutomationSidebarOption extends LitElement {
.clickAction=${this.config.rename}
.disabled=${!!disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-menu-item
@@ -63,13 +72,16 @@ export default class HaAutomationSidebarOption extends LitElement {
@click=${this.config.duplicate}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiContentDuplicate}
slot="start"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-divider
slot="menu-items"
@@ -82,10 +94,32 @@ export default class HaAutomationSidebarOption extends LitElement {
.disabled=${this.disabled}
class="warning"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
${!this.narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span
>${this.hass.localize(
"ui.panel.config.automation.editor.del"
)}</span
>
</span>`
: nothing}
</div>
</ha-md-menu-item>
`}

View File

@@ -1,10 +1,12 @@
import { mdiDelete, mdiPlaylistEdit } from "@mdi/js";
import { html, LitElement } from "lit";
import { mdiAppleKeyboardCommand, mdiDelete, mdiPlaylistEdit } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import type { ScriptFieldSidebarConfig } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import "../../script/ha-script-field-selector-editor";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
import { sidebarEditorStyles } from "../styles";
@@ -24,6 +26,8 @@ export default class HaAutomationSidebarScriptFieldSelector extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
@state() private _warnings?: string[];
@query(".sidebar-editor")
@@ -65,10 +69,13 @@ export default class HaAutomationSidebarScriptFieldSelector extends LitElement {
.clickAction=${this._toggleYamlMode}
.disabled=${!!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
@@ -76,19 +83,45 @@ export default class HaAutomationSidebarScriptFieldSelector extends LitElement {
.disabled=${this.disabled}
class="warning"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
${!this.narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span
>${this.hass.localize(
"ui.panel.config.automation.editor.del"
)}</span
>
</span>`
: nothing}
</div>
</ha-md-menu-item>
<ha-script-field-selector-editor
class="sidebar-editor"
.hass=${this.hass}
.field=${this.config.config.field}
.disabled=${this.disabled}
@value-changed=${this._valueChangedSidebar}
.yamlMode=${this.yamlMode}
></ha-script-field-selector-editor>
${keyed(
this.sidebarKey,
html`<ha-script-field-selector-editor
class="sidebar-editor"
.hass=${this.hass}
.field=${this.config.config.field}
.disabled=${this.disabled}
@value-changed=${this._valueChangedSidebar}
@yaml-changed=${this._yamlChangedSidebar}
.yamlMode=${this.yamlMode}
></ha-script-field-selector-editor>`
)}
</ha-automation-sidebar-card>`;
}
@@ -116,6 +149,12 @@ export default class HaAutomationSidebarScriptFieldSelector extends LitElement {
}
}
private _yamlChangedSidebar(ev: CustomEvent) {
ev.stopPropagation();
this.config?.save?.(ev.detail.value);
}
private _toggleYamlMode = () => {
fireEvent(this, "toggle-yaml-mode");
};

View File

@@ -1,9 +1,11 @@
import { mdiDelete, mdiPlaylistEdit } from "@mdi/js";
import { html, LitElement } from "lit";
import { mdiAppleKeyboardCommand, mdiDelete, mdiPlaylistEdit } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { ScriptFieldSidebarConfig } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import "../../script/ha-script-field-editor";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
import { sidebarEditorStyles } from "../styles";
@@ -23,6 +25,8 @@ export default class HaAutomationSidebarScriptField extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
@state() private _warnings?: string[];
@query(".sidebar-editor")
@@ -58,10 +62,13 @@ export default class HaAutomationSidebarScriptField extends LitElement {
.clickAction=${this._toggleYamlMode}
.disabled=${!!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
@@ -69,21 +76,47 @@ export default class HaAutomationSidebarScriptField extends LitElement {
.disabled=${this.disabled}
class="warning"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
${!this.narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span
>${this.hass.localize(
"ui.panel.config.automation.editor.del"
)}</span
>
</span>`
: nothing}
</div>
</ha-md-menu-item>
<ha-script-field-editor
class="sidebar-editor"
.hass=${this.hass}
.field=${this.config.config.field}
.key=${this.config.config.key}
.excludeKeys=${this.config.config.excludeKeys}
.disabled=${this.disabled}
.yamlMode=${this.yamlMode}
@value-changed=${this._valueChangedSidebar}
></ha-script-field-editor>
${keyed(
this.sidebarKey,
html`<ha-script-field-editor
class="sidebar-editor"
.hass=${this.hass}
.field=${this.config.config.field}
.key=${this.config.config.key}
.excludeKeys=${this.config.config.excludeKeys}
.disabled=${this.disabled}
.yamlMode=${this.yamlMode}
@value-changed=${this._valueChangedSidebar}
@yaml-changed=${this._yamlChangedSidebar}
></ha-script-field-editor>`
)}
</ha-automation-sidebar-card>`;
}
@@ -110,6 +143,12 @@ export default class HaAutomationSidebarScriptField extends LitElement {
}
}
private _yamlChangedSidebar(ev: CustomEvent) {
ev.stopPropagation();
this.config?.save?.(ev.detail.value);
}
private _toggleYamlMode = () => {
fireEvent(this, "toggle-yaml-mode");
};

View File

@@ -1,21 +1,24 @@
import {
mdiAppleKeyboardCommand,
mdiContentCopy,
mdiContentCut,
mdiContentDuplicate,
mdiDelete,
mdiIdentifier,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiPlusCircleMultipleOutline,
mdiRenameBox,
mdiStopCircleOutline,
} from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import type { TriggerSidebarConfig } from "../../../../data/automation";
import { isTriggerList } from "../../../../data/trigger";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import { sidebarEditorStyles } from "../styles";
import "../trigger/ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "../trigger/ha-automation-trigger-editor";
@@ -35,6 +38,8 @@ export default class HaAutomationSidebarTrigger extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
@state() private _requestShowId = false;
@state() private _warnings?: string[];
@@ -86,10 +91,13 @@ export default class HaAutomationSidebarTrigger extends LitElement {
.clickAction=${this.config.rename}
.disabled=${disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
${!this.yamlMode &&
!("id" in this.config.config) &&
@@ -99,10 +107,13 @@ export default class HaAutomationSidebarTrigger extends LitElement {
.clickAction=${this._showTriggerId}
.disabled=${disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>`
: nothing}
@@ -120,7 +131,10 @@ export default class HaAutomationSidebarTrigger extends LitElement {
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.duplicate"
)}
<ha-svg-icon slot="start" .path=${mdiContentDuplicate}></ha-svg-icon>
<ha-svg-icon
slot="start"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
@@ -128,10 +142,28 @@ export default class HaAutomationSidebarTrigger extends LitElement {
.clickAction=${this.config.copy}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
${!this.narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span>C</span>
</span>`
: nothing}
</div>
</ha-md-menu-item>
<ha-md-menu-item
@@ -139,20 +171,41 @@ export default class HaAutomationSidebarTrigger extends LitElement {
.clickAction=${this.config.cut}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
${!this.narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span>X</span>
</span>`
: nothing}
</div>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this._toggleYamlMode}
.disabled=${!this.config.uiSupported || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this.yamlMode ? "yaml" : "ui"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-divider
slot="menu-items"
@@ -164,13 +217,16 @@ export default class HaAutomationSidebarTrigger extends LitElement {
.clickAction=${this.config.disable}
.disabled=${type === "list"}
>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
)}
<ha-svg-icon
slot="start"
.path=${this.disabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
<ha-md-menu-item
slot="menu-items"
@@ -178,22 +234,49 @@ export default class HaAutomationSidebarTrigger extends LitElement {
.disabled=${this.disabled}
class="warning"
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
${!this.narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span
>${this.hass.localize(
"ui.panel.config.automation.editor.del"
)}</span
>
</span>`
: nothing}
</div>
</ha-md-menu-item>
<ha-automation-trigger-editor
class="sidebar-editor"
.hass=${this.hass}
.trigger=${this.config.config}
@value-changed=${this._valueChangedSidebar}
.uiSupported=${this.config.uiSupported}
.showId=${this._requestShowId}
.yamlMode=${this.yamlMode}
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-trigger-editor>
${keyed(
this.sidebarKey,
html`<ha-automation-trigger-editor
class="sidebar-editor"
.hass=${this.hass}
.trigger=${this.config.config}
@value-changed=${this._valueChangedSidebar}
@yaml-changed=${this._yamlChangedSidebar}
.uiSupported=${this.config.uiSupported}
.showId=${this._requestShowId}
.yamlMode=${this.yamlMode}
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
sidebar
></ha-automation-trigger-editor>`
)}
</ha-automation-sidebar-card>
`;
}
@@ -220,6 +303,12 @@ export default class HaAutomationSidebarTrigger extends LitElement {
}
}
private _yamlChangedSidebar(ev: CustomEvent) {
ev.stopPropagation();
this.config?.save?.(ev.detail.value);
}
private _toggleYamlMode = () => {
fireEvent(this, "toggle-yaml-mode");
};

View File

@@ -37,12 +37,6 @@ export const rowStyles = css`
ha-tooltip {
cursor: default;
}
:host([highlight]) ha-card {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
border-color: var(--state-inactive-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
.hidden {
display: none;
}
@@ -54,7 +48,7 @@ export const editorStyles = css`
pointer-events: none;
}
.card-content {
.card-content.card {
padding: 16px;
}
.card-content.yaml {
@@ -68,12 +62,15 @@ export const indentStyle = css`
.card-content.indent,
.selector-row,
:host([indent]) ha-form {
margin-left: 12px;
padding: 12px 24px 16px 16px;
border-left: 2px solid var(--ha-color-border-neutral-quiet);
margin-inline-start: 12px;
padding-top: 12px;
padding-bottom: 16px;
padding-inline-start: 16px;
padding-inline-end: 0px;
border-inline-start: 2px solid var(--ha-color-border-neutral-quiet);
border-bottom: 2px solid var(--ha-color-border-neutral-quiet);
border-radius: 0;
border-bottom-left-radius: var(--ha-border-radius-lg);
border-end-start-radius: var(--ha-border-radius-lg);
}
.card-content.indent.selected,
:host([selected]) .card-content.indent,
@@ -108,78 +105,64 @@ export const saveFabStyles = css`
export const manualEditorStyles = css`
:host {
display: block;
--sidebar-width: 0;
--sidebar-gap: 0;
}
.split-view {
.has-sidebar {
--sidebar-width: min(35vw, 500px);
--sidebar-gap: 16px;
}
.fab-positioner {
display: flex;
flex-direction: row;
height: 100%;
position: relative;
gap: 16px;
justify-content: flex-end;
}
.split-view.sidebar-hidden {
gap: 0;
.fab-positioner ha-fab {
position: fixed;
right: unset;
left: unset;
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
.fab-positioner ha-fab.dirty {
bottom: 16px;
}
.content-wrapper {
position: relative;
flex: 6;
padding-right: calc(var(--sidebar-width) + var(--sidebar-gap));
padding-inline-end: calc(var(--sidebar-width) + var(--sidebar-gap));
padding-inline-start: 0;
}
.content {
padding: 32px 16px 64px 0;
height: calc(100vh - 153px);
height: calc(100dvh - 153px);
overflow-y: auto;
overflow-x: hidden;
padding-top: 24px;
padding-bottom: 72px;
transition: padding-bottom 180ms ease-in-out;
}
.sidebar {
padding: 12px 0;
flex: 4;
height: calc(100vh - 81px);
height: calc(100dvh - 81px);
width: 40%;
}
.split-view.sidebar-hidden .sidebar {
border-color: transparent;
border-width: 0;
overflow: hidden;
flex: 0;
visibility: hidden;
.content.has-bottom-sheet {
padding-bottom: calc(90vh - 72px);
}
.sidebar.overlay {
ha-automation-sidebar {
position: fixed;
bottom: 8px;
right: 8px;
height: calc(100% - 70px);
padding: 0;
z-index: 5;
box-shadow: -8px 0 16px rgba(0, 0, 0, 0.2);
top: calc(var(--header-height) + 16px);
height: calc(-81px + 100dvh);
width: var(--sidebar-width);
display: block;
}
.sidebar.overlay.rtl {
right: unset;
left: 8px;
ha-automation-sidebar.hidden {
display: none;
}
@media all and (max-width: 870px) {
.split-view {
gap: 0;
margin-right: -8px;
}
.sidebar {
height: 0;
width: 0;
flex: 0;
}
.sidebar-positioner {
display: flex;
justify-content: flex-end;
}
.split-view.sidebar-hidden .sidebar.overlay {
width: 0;
}
.description {
margin: 0;
}
@@ -190,13 +173,13 @@ export const manualEditorStyles = css`
export const automationRowsStyles = css`
.rows {
padding: 16px 0 16px 16px;
margin: -16px;
margin-right: -20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.rows.no-sidebar {
margin-inline-end: 0;
}
.sortable-ghost {
background: none;
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
@@ -209,9 +192,18 @@ export const automationRowsStyles = css`
scroll-margin-top: 48px;
}
.handle {
padding: 12px;
padding: 4px;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
border-radius: var(--ha-border-radius-pill);
}
.handle:focus {
outline: var(--wa-focus-ring);
background: var(--ha-color-fill-neutral-quiet-resting);
}
.handle.active {
outline: var(--wa-focus-ring);
background: var(--ha-color-fill-neutral-normal-active);
}
.handle ha-svg-icon {
pointer-events: none;
@@ -228,9 +220,39 @@ export const automationRowsStyles = css`
export const sidebarEditorStyles = css`
.sidebar-editor {
display: block;
padding-top: 16px;
padding-top: 8px;
}
.description {
padding-top: 16px;
}
.overflow-label {
display: flex;
justify-content: space-between;
gap: 12px;
white-space: nowrap;
}
.overflow-label .shortcut {
--mdc-icon-size: 12px;
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
.overflow-label .shortcut span {
font-size: var(--ha-font-size-s);
font-family: var(--ha-font-family-code);
color: var(--ha-color-text-secondary);
}
.shortcut-placeholder {
display: inline-block;
width: 60px;
}
.shortcut-placeholder.mac {
width: 46px;
}
@media all and (max-width: 870px) {
.shortcut-placeholder {
display: none;
}
}
`;

View File

@@ -29,6 +29,8 @@ export default class HaAutomationTriggerEditor extends LitElement {
@property({ type: Boolean, attribute: "show-id" }) public showId = false;
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
protected render() {
@@ -47,6 +49,7 @@ export default class HaAutomationTriggerEditor extends LitElement {
this.trigger.enabled === false &&
!this.yamlMode),
yaml: yamlMode,
card: !this.inSidebar,
})}
>
${yamlMode
@@ -118,7 +121,7 @@ export default class HaAutomationTriggerEditor extends LitElement {
if (!ev.detail.isValid) {
return;
}
fireEvent(this, "value-changed", {
fireEvent(this, "yaml-changed", {
value: migrateAutomationTrigger(ev.detail.value),
});
}

View File

@@ -4,12 +4,12 @@ import {
mdiArrowUp,
mdiContentCopy,
mdiContentCut,
mdiContentDuplicate,
mdiDelete,
mdiDotsVertical,
mdiIdentifier,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiPlusCircleMultipleOutline,
mdiRenameBox,
mdiStopCircleOutline,
} from "@mdi/js";
@@ -28,6 +28,7 @@ import { handleStructError } from "../../../../common/structs/handle-errors";
import { debounce } from "../../../../common/util/debounce";
import "../../../../components/ha-alert";
import "../../../../components/ha-automation-row";
import type { HaAutomationRow } from "../../../../components/ha-automation-row";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
@@ -51,6 +52,7 @@ import {
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import "../ha-automation-editor-warning";
import { rowStyles } from "../styles";
import "./ha-automation-trigger-editor";
@@ -112,9 +114,14 @@ export default class HaAutomationTriggerRow extends LitElement {
@property({ type: Boolean }) public last?: boolean;
@property({ type: Boolean }) public highlight?: boolean;
@property({ type: Boolean, attribute: "sidebar" })
public optionsInSidebar = false;
@property({ type: Boolean, attribute: "sort-selected" })
public sortSelected = false;
@state() private _yamlMode = false;
@state() private _triggered?: Record<string, unknown>;
@@ -132,6 +139,9 @@ export default class HaAutomationTriggerRow extends LitElement {
@query("ha-automation-trigger-editor")
public triggerEditor?: HaAutomationTriggerEditor;
@query("ha-automation-row")
private _automationRowElement?: HaAutomationRow;
@storage({
key: "automationClipboard",
state: false,
@@ -144,6 +154,10 @@ export default class HaAutomationTriggerRow extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
get selected() {
return this._selected;
}
private _triggerUnsub?: Promise<UnsubscribeFunc>;
private _renderRow() {
@@ -165,145 +179,142 @@ export default class HaAutomationTriggerRow extends LitElement {
<slot name="icons" slot="icons"></slot>
<ha-md-button-menu
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
${!this.optionsInSidebar
? html`<ha-md-button-menu
quick
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
anchor-corner="end-end"
menu-corner="start-end"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item
.clickAction=${this._renameTrigger}
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
${!this.optionsInSidebar
? html`<ha-md-menu-item
.clickAction=${this._renameTrigger}
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._showTriggerId}
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._showTriggerId}
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.duplicate"
)}
<ha-svg-icon
slot="start"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._duplicateTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.duplicate"
)}
<ha-svg-icon
slot="start"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>`
: nothing}
<ha-md-menu-item
.clickAction=${this._moveUp}
.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._moveUp}
.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}
>
${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-md-menu-item
.clickAction=${this._moveDown}
.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-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!supported || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
${!this.optionsInSidebar
? html`
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!supported || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon
slot="start"
.path=${mdiPlaylistEdit}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled || type === "list"}
>
${"enabled" in this.trigger && this.trigger.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${"enabled" in this.trigger &&
this.trigger.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._onDelete}
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled || type === "list"}
>
${"enabled" in this.trigger && this.trigger.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${"enabled" in this.trigger &&
this.trigger.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._onDelete}
class="warning"
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
`
: nothing}
</ha-md-button-menu>
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>`
: nothing}
${!this.optionsInSidebar
? html`${this._warnings
? html`<ha-automation-editor-warning
@@ -343,8 +354,13 @@ export default class HaAutomationTriggerRow extends LitElement {
? html`<ha-automation-row
.disabled=${"enabled" in this.trigger &&
this.trigger.enabled === false}
@click=${this._toggleSidebar}
.selected=${this._selected}
.highlight=${this.highlight}
.sortSelected=${this.sortSelected}
@click=${this._toggleSidebar}
@copy-row=${this._copyTrigger}
@cut-row=${this._cutTrigger}
@delete-row=${this._onDelete}
>${this._selected
? "selected"
: nothing}${this._renderRow()}</ha-automation-row
@@ -465,8 +481,7 @@ export default class HaAutomationTriggerRow extends LitElement {
ev?.stopPropagation();
if (this._selected) {
this._selected = false;
fireEvent(this, "close-sidebar");
fireEvent(this, "request-close-sidebar");
return;
}
this.openSidebar();
@@ -477,16 +492,19 @@ export default class HaAutomationTriggerRow extends LitElement {
save: (value) => {
fireEvent(this, "value-changed", { value });
},
close: () => {
close: (focus?: boolean) => {
this._selected = false;
fireEvent(this, "close-sidebar");
if (focus) {
this.focus();
}
},
rename: () => {
this._renameTrigger();
},
toggleYamlMode: () => {
this._toggleYamlMode();
return this._yamlMode;
this.openSidebar();
},
disable: this._onDisable,
delete: this._onDelete,
@@ -500,10 +518,12 @@ export default class HaAutomationTriggerRow extends LitElement {
this._selected = true;
if (this.narrow) {
this.scrollIntoView({
block: "start",
behavior: "smooth",
});
window.setTimeout(() => {
this.scrollIntoView({
block: "start",
behavior: "smooth",
});
}, 180);
}
}
@@ -621,6 +641,12 @@ export default class HaAutomationTriggerRow extends LitElement {
private _copyTrigger = () => {
this._setClipboard();
showToast(this, {
message: this.hass.localize(
"ui.panel.config.automation.editor.triggers.copied_to_clipboard"
),
duration: 2000,
});
};
private _cutTrigger = () => {
@@ -629,6 +655,12 @@ export default class HaAutomationTriggerRow extends LitElement {
if (this._selected) {
fireEvent(this, "close-sidebar");
}
showToast(this, {
message: this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut_to_clipboard"
),
duration: 2000,
});
};
private _moveUp = () => {
@@ -666,6 +698,10 @@ export default class HaAutomationTriggerRow extends LitElement {
customElements.get(`ha-automation-trigger-${type}`) !== undefined
);
public focus() {
this._automationRowElement?.focus();
}
static get styles(): CSSResultGroup {
return [
rowStyles,

View File

@@ -1,12 +1,11 @@
import { mdiDrag, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { listenMediaQuery } from "../../../../common/dom/media_query";
import { nextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
@@ -44,7 +43,7 @@ export default class HaAutomationTrigger extends LitElement {
@property({ type: Boolean }) public root = false;
@state() private _showReorder = false;
@state() private _rowSortSelected?: number;
@state()
@storage({
@@ -57,36 +56,23 @@ export default class HaAutomationTrigger extends LitElement {
private _focusLastTriggerOnChange = false;
private _focusTriggerIndexOnChange?: number;
private _triggerKeys = new WeakMap<Trigger, string>();
private _unsubMql?: () => void;
public connectedCallback() {
super.connectedCallback();
this._unsubMql = listenMediaQuery("(min-width: 600px)", (matches) => {
this._showReorder = matches;
});
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubMql?.();
this._unsubMql = undefined;
}
protected render() {
return html`
<ha-sortable
handle-selector=".handle"
draggable-selector="ha-automation-trigger-row"
.disabled=${!this._showReorder || this.disabled}
.disabled=${this.disabled}
group="triggers"
invert-swap
@item-moved=${this._triggerMoved}
@item-added=${this._triggerAdded}
@item-removed=${this._triggerRemoved}
>
<div class="rows">
<div class="rows ${!this.optionsInSidebar ? "no-sidebar" : ""}">
${repeat(
this.triggers,
(trigger) => this._getKey(trigger),
@@ -104,12 +90,22 @@ export default class HaAutomationTrigger extends LitElement {
.hass=${this.hass}
.disabled=${this.disabled}
.narrow=${this.narrow}
?highlight=${this.highlightedTriggers?.includes(trg)}
.highlight=${this.highlightedTriggers?.includes(trg)}
.optionsInSidebar=${this.optionsInSidebar}
.sortSelected=${this._rowSortSelected === idx}
@stop-sort-selection=${this._stopSortSelection}
>
${this._showReorder && !this.disabled
${!this.disabled
? html`
<div class="handle" slot="icons">
<div
tabindex="0"
class="handle ${this._rowSortSelected === idx
? "active"
: ""}"
slot="icons"
@keydown=${this._handleDragKeydown}
.index=${idx}
>
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
</div>
`
@@ -172,12 +168,18 @@ export default class HaAutomationTrigger extends LitElement {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("triggers") && this._focusLastTriggerOnChange) {
this._focusLastTriggerOnChange = false;
if (
changedProps.has("triggers") &&
(this._focusLastTriggerOnChange ||
this._focusTriggerIndexOnChange !== undefined)
) {
const row = this.shadowRoot!.querySelector<HaAutomationTriggerRow>(
"ha-automation-trigger-row:last-of-type"
`ha-automation-trigger-row:${this._focusLastTriggerOnChange ? "last-of-type" : `nth-of-type(${this._focusTriggerIndexOnChange! + 1})`}`
)!;
this._focusLastTriggerOnChange = false;
this._focusTriggerIndexOnChange = undefined;
row.updateComplete.then(() => {
if (this.optionsInSidebar) {
row.openSidebar();
@@ -189,8 +191,8 @@ export default class HaAutomationTrigger extends LitElement {
}
} else {
row.expand();
row.focus();
}
row.focus();
});
}
}
@@ -216,15 +218,27 @@ export default class HaAutomationTrigger extends LitElement {
private _moveUp(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index - 1;
this._move(index, newIndex);
if (!(ev.target as HaAutomationTriggerRow).first) {
const newIndex = index - 1;
this._move(index, newIndex);
if (this._rowSortSelected === index) {
this._rowSortSelected = newIndex;
}
ev.target.focus();
}
}
private _moveDown(ev) {
ev.stopPropagation();
const index = (ev.target as any).index;
const newIndex = index + 1;
this._move(index, newIndex);
if (!(ev.target as HaAutomationTriggerRow).last) {
const newIndex = index + 1;
this._move(index, newIndex);
if (this._rowSortSelected === index) {
this._rowSortSelected = newIndex;
}
ev.target.focus();
}
}
private _move(oldIndex: number, newIndex: number) {
@@ -244,6 +258,9 @@ export default class HaAutomationTrigger extends LitElement {
private async _triggerAdded(ev: CustomEvent): Promise<void> {
ev.stopPropagation();
const { index, data } = ev.detail;
const item = ev.detail.item as HaAutomationTriggerRow;
const selected = item.selected;
let triggers = [
...this.triggers.slice(0, index),
data,
@@ -251,6 +268,9 @@ export default class HaAutomationTrigger extends LitElement {
];
// Add trigger locally to avoid UI jump
this.triggers = triggers;
if (selected) {
this._focusTriggerIndexOnChange = triggers.length === 1 ? 0 : index;
}
await nextRender();
if (this.triggers !== triggers) {
// Ensure trigger is added even after update
@@ -259,6 +279,9 @@ export default class HaAutomationTrigger extends LitElement {
data,
...this.triggers.slice(index),
];
if (selected) {
this._focusTriggerIndexOnChange = triggers.length === 1 ? 0 : index;
}
}
fireEvent(this, "value-changed", { value: triggers });
}
@@ -302,14 +325,21 @@ export default class HaAutomationTrigger extends LitElement {
});
}
static styles = [
automationRowsStyles,
css`
:host([root]) .rows {
padding-right: 8px;
}
`,
];
private _handleDragKeydown(ev: KeyboardEvent) {
if (ev.key === "Enter" || ev.key === " ") {
ev.stopPropagation();
this._rowSortSelected =
this._rowSortSelected === undefined
? (ev.target as any).index
: undefined;
}
}
private _stopSortSelection() {
this._rowSortSelected = undefined;
}
static styles = automationRowsStyles;
}
declare global {

View File

@@ -42,6 +42,8 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
.disabled=${this.disabled || this._tags.length === 0}
.value=${this.trigger.tag_id}
@selected=${this._tagChanged}
fixedMenuPosition
naturalMenuWidth
>
${this._tags.map(
(tag) => html`

View File

@@ -49,8 +49,8 @@ const STRATEGIES = [
{
type: "home",
images: {
light: "/static/images/dashboard-options/light/icon-dashboard-areas.svg",
dark: "/static/images/dashboard-options/dark/icon-dashboard-areas.svg",
light: "/static/images/dashboard-options/light/icon-dashboard-home.svg",
dark: "/static/images/dashboard-options/dark/icon-dashboard-home.svg",
},
name: "ui.panel.config.lovelace.dashboards.dialog_new.strategy.home.title",
description:

View File

@@ -239,12 +239,8 @@ class DialogMQTTDeviceDebugInfo extends LitElement {
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-dialog {
--mdc-dialog-min-width: calc(
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
);
--mdc-dialog-max-width: calc(
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
);
--mdc-dialog-min-width: 100vw;
--mdc-dialog-max-width: 100vw;
}
}
ha-switch {

View File

@@ -773,6 +773,11 @@ export class HaConfigDevicePage extends LitElement {
appearance="plain"
target=${ifDefined(firstDeviceAction!.target)}
class=${ifDefined(firstDeviceAction!.classes)}
.variant=${firstDeviceAction!.classes?.includes(
"warning"
)
? "danger"
: "brand"}
.action=${firstDeviceAction!.action}
@click=${this._deviceActionClicked}
>

View File

@@ -430,7 +430,13 @@ export class DialogHelperDetail extends LitElement {
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-list {
height: calc(100vh - 184px);
height: calc(
100vh -
184px - var(--safe-area-inset-top, 0px) - var(
--safe-area-inset-bottom,
0px
)
);
}
}
`,

View File

@@ -461,7 +461,9 @@ class AddIntegrationDialog extends LitElement {
class="ha-scrollbar"
style=${styleMap({
width: `${this._width}px`,
height: this._narrow ? "calc(100vh - 184px)" : "500px",
height: this._narrow
? "calc(100vh - 184px - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))"
: "500px",
})}
@click=${this._integrationPicked}
@keypress=${this._handleKeyPress}

View File

@@ -1,5 +1,5 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../../components/ha-card";
import "../../../../../components/ha-code-editor";
@@ -11,13 +11,22 @@ import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-d
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { subscribeBluetoothConnectionAllocations } from "../../../../../data/bluetooth";
import {
subscribeBluetoothConnectionAllocations,
subscribeBluetoothScannerState,
subscribeBluetoothScannersDetails,
} from "../../../../../data/bluetooth";
import type {
BluetoothAllocationsData,
BluetoothScannerState,
BluetoothScannersDetails,
HaScannerType,
} from "../../../../../data/bluetooth";
import {
getValueInPercentage,
roundWithOneDecimal,
} from "../../../../../util/calculate";
import "../../../../../components/ha-metric";
import type { BluetoothAllocationsData } from "../../../../../data/bluetooth";
@customElement("bluetooth-config-dashboard")
export class BluetoothConfigDashboard extends LitElement {
@@ -29,16 +38,26 @@ export class BluetoothConfigDashboard extends LitElement {
@state() private _connectionAllocationsError?: string;
@state() private _scannerState?: BluetoothScannerState;
@state() private _scannerDetails?: BluetoothScannersDetails;
private _configEntry = new URLSearchParams(window.location.search).get(
"config_entry"
);
private _unsubConnectionAllocations?: (() => Promise<void>) | undefined;
private _unsubScannerState?: (() => Promise<void>) | undefined;
private _unsubScannerDetails?: (() => void) | undefined;
public connectedCallback(): void {
super.connectedCallback();
if (this.hass) {
this._subscribeBluetoothConnectionAllocations();
this._subscribeBluetoothScannerState();
this._subscribeScannerDetails();
}
}
@@ -61,12 +80,45 @@ export class BluetoothConfigDashboard extends LitElement {
}
}
private async _subscribeBluetoothScannerState(): Promise<void> {
if (this._unsubScannerState || !this._configEntry) {
return;
}
this._unsubScannerState = await subscribeBluetoothScannerState(
this.hass.connection,
(scannerState) => {
this._scannerState = scannerState;
},
this._configEntry
);
}
private _subscribeScannerDetails(): void {
if (this._unsubScannerDetails) {
return;
}
this._unsubScannerDetails = subscribeBluetoothScannersDetails(
this.hass.connection,
(details) => {
this._scannerDetails = details;
}
);
}
public disconnectedCallback() {
super.disconnectedCallback();
if (this._unsubConnectionAllocations) {
this._unsubConnectionAllocations();
this._unsubConnectionAllocations = undefined;
}
if (this._unsubScannerState) {
this._unsubScannerState();
this._unsubScannerState = undefined;
}
if (this._unsubScannerDetails) {
this._unsubScannerDetails();
this._unsubScannerDetails = undefined;
}
}
protected render(): TemplateResult {
@@ -78,6 +130,7 @@ export class BluetoothConfigDashboard extends LitElement {
"ui.panel.config.bluetooth.settings_title"
)}
>
<div class="card-content">${this._renderScannerState()}</div>
<div class="card-actions">
<ha-button @click=${this._openOptionFlow}
>${this.hass.localize(
@@ -142,6 +195,118 @@ export class BluetoothConfigDashboard extends LitElement {
private _getUsedAllocations = (used: number, total: number) =>
roundWithOneDecimal(getValueInPercentage(used, 0, total));
private _renderScannerMismatchWarning(
scannerState: BluetoothScannerState,
scannerType: HaScannerType,
formatMode: (mode: string | null) => string
) {
const instructions: string[] = [];
if (scannerType === "remote" || scannerType === "unknown") {
instructions.push(
this.hass.localize(
"ui.panel.config.bluetooth.scanner_mode_mismatch_remote"
)
);
}
if (scannerType === "usb" || scannerType === "unknown") {
instructions.push(
this.hass.localize(
"ui.panel.config.bluetooth.scanner_mode_mismatch_usb"
)
);
}
if (scannerType === "uart" || scannerType === "unknown") {
instructions.push(
this.hass.localize(
"ui.panel.config.bluetooth.scanner_mode_mismatch_uart"
)
);
}
return html`<ha-alert alert-type="warning">
<div>
${this.hass.localize(
"ui.panel.config.bluetooth.scanner_mode_mismatch",
{
requested: formatMode(scannerState.requested_mode),
current: formatMode(scannerState.current_mode),
}
)}
</div>
<ul>
${instructions.map((instruction) => html`<li>${instruction}</li>`)}
</ul>
</ha-alert>`;
}
private _renderScannerState() {
if (!this._configEntry || !this._scannerState) {
return html`<div>
${this.hass.localize(
"ui.panel.config.bluetooth.no_scanner_state_available"
)}
</div>`;
}
const scannerState = this._scannerState;
// Find the scanner details for this source
const scannerDetails = this._scannerDetails?.[scannerState.source];
const scannerType: HaScannerType =
scannerDetails?.scanner_type ?? "unknown";
const formatMode = (mode: string | null) => {
switch (mode) {
case null:
return this.hass.localize(
"ui.panel.config.bluetooth.scanning_mode_none"
);
case "active":
return this.hass.localize(
"ui.panel.config.bluetooth.scanning_mode_active"
);
case "passive":
return this.hass.localize(
"ui.panel.config.bluetooth.scanning_mode_passive"
);
default:
return mode; // Fallback for unknown modes
}
};
return html`
<div class="scanner-state">
<div class="state-row">
<span
>${this.hass.localize(
"ui.panel.config.bluetooth.current_scanning_mode"
)}:</span
>
<span class="state-value"
>${formatMode(scannerState.current_mode)}</span
>
</div>
<div class="state-row">
<span
>${this.hass.localize(
"ui.panel.config.bluetooth.requested_scanning_mode"
)}:</span
>
<span class="state-value"
>${formatMode(scannerState.requested_mode)}</span
>
</div>
${scannerState.current_mode !== scannerState.requested_mode
? this._renderScannerMismatchWarning(
scannerState,
scannerType,
formatMode
)
: nothing}
</div>
`;
}
private _renderConnectionAllocations() {
if (this._connectionAllocationsError) {
return html`<ha-alert alert-type="error"
@@ -220,6 +385,18 @@ export class BluetoothConfigDashboard extends LitElement {
display: flex;
justify-content: flex-end;
}
.scanner-state {
margin-bottom: 16px;
}
.state-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
}
.state-value {
font-weight: 500;
}
`,
];
}

View File

@@ -34,6 +34,16 @@ const UPDATE_THROTTLE_TIME = 10000;
const CORE_SOURCE_ID = "ha";
const CORE_SOURCE_LABEL = "Home Assistant";
const RSSI_COLOR_THRESHOLDS: [number, string][] = [
[-70, "--green-color"], // Excellent: > -70 dBm
[-75, "--lime-color"], // Good: -70 to -75 dBm
[-80, "--yellow-color"], // Okay: -75 to -80 dBm
[-85, "--amber-color"], // Marginal: -80 to -85 dBm
[-90, "--orange-color"], // Weak: -85 to -90 dBm
[-95, "--deep-orange-color"], // Poor: -90 to -95 dBm
[-Infinity, "--red-color"], // Very poor: < -95 dBm
];
@customElement("bluetooth-network-visualization")
export class BluetoothNetworkVisualization extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -125,6 +135,16 @@ export class BluetoothNetworkVisualization extends LitElement {
`;
}
private _getRssiColorVar = memoizeOne((rssi: number): string => {
for (const [threshold, colorVar] of RSSI_COLOR_THRESHOLDS) {
if (rssi > threshold) {
return colorVar;
}
}
// Fallback (should never reach here)
return "--red-color";
});
private _formatNetworkData = memoizeOne(
(
data: BluetoothDeviceData[],
@@ -206,7 +226,7 @@ export class BluetoothNetworkVisualization extends LitElement {
symbol: "none",
lineStyle: {
width: this._getLineWidth(node.rssi),
color: style.getPropertyValue("--primary-color"),
color: style.getPropertyValue(this._getRssiColorVar(node.rssi)),
},
});
return;
@@ -227,7 +247,7 @@ export class BluetoothNetworkVisualization extends LitElement {
lineStyle: {
width: this._getLineWidth(node.rssi),
color: device
? style.getPropertyValue("--primary-color")
? style.getPropertyValue(this._getRssiColorVar(node.rssi))
: style.getPropertyValue("--disabled-color"),
},
});

View File

@@ -243,7 +243,7 @@ class ErrorLogCard extends LitElement {
${!streaming || this._error
? html`<ha-icon-button
.path=${mdiRefresh}
@click=${this._loadLogs}
@click=${this._handleRefresh}
.label=${localize("ui.common.refresh")}
></ha-icon-button>`
: nothing}
@@ -715,6 +715,10 @@ class ErrorLogCard extends LitElement {
this._wrapLines = !this._wrapLines;
}
private _handleRefresh() {
this._loadLogs();
}
private _handleOverflowAction(ev: CustomEvent<ActionDetail>) {
let index = ev.detail.index;
if (this.provider === "core") {

View File

@@ -199,8 +199,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
"ui.panel.config.lovelace.dashboards.picker.headers.require_admin"
),
sortable: true,
type: "icon",
hidden: narrow,
type: "icon",
minWidth: "120px",
maxWidth: "120px",
template: (dashboard) =>
dashboard.require_admin
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
@@ -210,8 +212,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
title: localize(
"ui.panel.config.lovelace.dashboards.picker.headers.sidebar"
),
type: "icon",
hidden: narrow,
type: "icon",
minWidth: "120px",
maxWidth: "120px",
template: (dashboard) =>
dashboard.show_in_sidebar
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`

View File

@@ -23,10 +23,7 @@ class ConfigNetwork extends LitElement {
@state() private _error?: { code: string; message: string };
protected render() {
if (
!this.hass.userData?.showAdvanced ||
!isComponentLoaded(this.hass, "network")
) {
if (!isComponentLoaded(this.hass, "network")) {
return nothing;
}

View File

@@ -289,6 +289,15 @@ export const showRepairsFlowDialog = (
);
},
renderMenuOptionDescription(hass, step, option) {
return hass.localize(
`component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.menu_option_descriptions.${option}`,
mergePlaceholders(issue, step)
);
},
renderLoadingDescription(hass, reason) {
return (
hass.localize(

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