Compare commits

...

147 Commits

Author SHA1 Message Date
Paul Bottein
bab0391a19 20250925.1 (#27191) 2025-09-25 17:57:22 +02:00
Paul Bottein
444123c47e Bumped version to 20250925.1 2025-09-25 17:56:13 +02:00
Paul Bottein
f123d34046 Revert "Update dependency @types/chromecast-caf-receiver to v6.0.24" (#27188) 2025-09-25 17:53:59 +02:00
Paul Bottein
1b40f99f68 Fix storage bar not displayed (#27183) 2025-09-25 17:50:52 +02:00
Paul Bottein
b314b3ed2b Fix analytics switches (#27181) 2025-09-25 17:50:51 +02:00
Paul Bottein
59b8932969 Add icon option to common controls section strategy (#27180) 2025-09-25 17:50:50 +02:00
Wendelin
107af753ec Reduce default tab padding in tab-group (#27173) 2025-09-25 17:50:49 +02:00
Paul Bottein
1f0acb3046 Disabled config badge (#27172)
* Add disabled option for badge

* Add disabled to struct
2025-09-25 17:50:48 +02:00
Paul Bottein
431e533929 20250925.0 (#27170) 2025-09-25 10:48:30 +02:00
Paul Bottein
02c845cbc6 Bumped version to 20250925.0 2025-09-25 10:47:41 +02:00
Paul Bottein
628111ed20 Bumped version to 20250924.1 2025-09-25 10:46:44 +02:00
Paul Bottein
e825a9c02f Smooth animation of the sidebar resizing handle (#27166) 2025-09-25 10:46:36 +02:00
Paul Bottein
7a35bddf36 Fix safe padding for bottom sheet and add scroll lock (#27165) 2025-09-25 10:46:35 +02:00
Norbert Rittel
ad69270af8 Use "Add (person)" instead of "New person" / "Create" (#27161)
* Update dialog-person-detail.ts

* Update en.json
2025-09-25 10:46:34 +02:00
Paulus Schoutsen
404edf9483 Avoid invalid entities in common controls (#27158) 2025-09-25 10:46:33 +02:00
Paul Bottein
a166b4e9b6 Do not show error message when action has no response in dev tools (#27156) 2025-09-25 10:46:32 +02:00
Paul Bottein
7a285f11db 20250924.0 (#27155) 2025-09-24 17:15:37 +02:00
Paul Bottein
e42c5a3254 Merge branch 'master' into dev 2025-09-24 17:09:01 +02:00
Paul Bottein
df7a6297b0 Bumped version to 20250924.0 2025-09-24 16:45:20 +02:00
Jeremy Cook
e4ca478d01 Add themes variables to tile card to change font appearance (#27092)
* Add themes variables to tile card to change font appearance

* Update documentation header including themes variables

---------

Co-authored-by: Jeremy Cook <jeco@norceresearch.no>
2025-09-24 14:34:43 +00:00
Jan-Philipp Benecke
7be2c59295 Redesign media player more-info dialog (#26904)
* Redesign media player more-info dialog

* Add missing imports

* Add some more media player controls to gallery

* Fix NaN

* Fix first example source

* Regroup

* Remove

* Add marquee text

* Buttons

* aria-label

* Increase speed

* Improve marquee text

* Improve marquee text

* Improve marquee text

* Add touch events to marquee text

* Use classMap

* Remove chip styling

* Make ha-marquee-text slotted and add to gallery

* Format

* Remove aria-label

* Make turn on and off buttons have labels

* Match more figma

* Add integration logo and move grouping/inputs to top

* Hm

* Fix badge

* Minor tweaks

* Disable position slider when seek is not supported

* Process code review

* remove disabled color for slider

* Process UX

* Run prettier

* Mark listener as passive

* Improve bottom controls and styling

* Remove unused function

* Some minor improvements

* Show remaining instead duration

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-09-24 16:13:37 +02:00
Douwe
99d9c67492 Button feature change variable handling script (#26786)
* Update types.ts

Added extra parameter button_action

* Update hui-button-card-feature-editor.ts

Added second field

* Update hui-button-card-feature.ts

* Update types.ts

* Update hui-button-card-feature-editor.ts

Fix issue with field naming

* Update hui-button-card-feature-editor.ts

* Update hui-button-card-feature-editor.ts

* Update hui-button-card-feature-editor.ts

* .

* Strategy update

* Update types.ts

* Update hui-button-card-feature-editor.ts

* Fix linting issues

* Add data field to editor

* localize error

* Update hui-button-card-feature.ts

Added suggestions

* Use UI to set script variables in button feature

* Update src/panels/lovelace/card-features/hui-button-card-feature.ts

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

---------

Co-authored-by: Wendelin <w@pe8.at>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-09-24 15:53:42 +03:00
Wendelin
8f781e53e3 Hassio core logs latest endpoint (#27131) 2025-09-24 14:48:26 +02:00
Aidan Timson
3c92826e71 Fix pixel gap on mobile for automation card (#27153) 2025-09-24 13:23:39 +02:00
karwosts
151a879e0a Add an energy diagnostics downloader (#27150)
* Add an energy diagnostics downloader

* add version
2025-09-24 13:47:09 +03:00
Wendelin
f3a8529ed7 Fix show disabled integrations button (#27151)
* Fix show disabled integrations button

* fix tsc
2025-09-24 13:42:37 +03:00
karwosts
d2cc7856d1 Visually differentiate statistics graph in more-info (#27055)
visually differentiate statistics graph in more-info

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-09-24 10:41:43 +00:00
Aidan Timson
d5cb815bbd Automation sidebar bottom fade and safe area fixes (#27135)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
Co-authored-by: Wendelin <w@pe8.at>
2025-09-24 10:52:16 +02:00
Tom Carpenter
7f88d863e9 Add toolbar to YAML/template editors (#26580) 2025-09-24 09:49:35 +02:00
Aidan Timson
88ac56ac0b Analog clock: CSS animation and seconds motion options (#26943)
* Increase analog clock interval and improve accuracy

* Restore

* Use CSS to render hands instead of JS interval with resync to adjust offsets

* Option

* Remove

* Option

* Fix
2025-09-24 08:36:39 +03:00
Bram Kragten
3d173ad03e Revert "Revert "Rename "Logbook" to "Activity" in user-facing strings"" (#26882)
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2025-09-23 22:02:44 +02:00
Lukas Waslowski
3889d71768 Improve addon configuration UI (#26997)
* Replace deeply nested ternary operators with if-return chain

* Extract _convertSchemaElement from _convertSchema

* Extract _convertSchemaElementToSelector from _convertSchemaElement

* Add force parameter to _convertSchemaElementToSelector

* Add UI editor for (lists of) nested dicts in addon configs

* Render top-level dicts to expandable sections

* Use correct translation keys for nested fields in addon configs

* Restructure translation keys for nested addon config fields
2025-09-23 19:08:06 +02:00
Jan-Philipp Benecke
8872adf2ed Remove unused fab styles from automation and script blueprint editors (#27147)
* Add missing styles to save fab in automation/script blueprint editor

* Remove unused FAB styles from automation and script editors
2025-09-23 17:28:47 +02:00
Aidan Timson
969e655fff Set hardware loading to use alert with spinner (#27149)
* Fix icon size

* Set hardware loading to use alert with spinner

* Match storage size
2025-09-23 17:28:06 +02:00
Wendelin
cdc913d878 Automation Editor: Fix onDisable to not open sidebar (#27144)
Fix onDisable to not open sidebar always
2025-09-23 16:49:16 +03:00
Jan-Philipp Benecke
4ac1215def Add undo/redo to script editor (#27145)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-09-23 12:16:40 +00:00
Jan-Philipp Benecke
b2376fba56 Simplify undo of pasting in automation editor (#27141)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-09-23 13:30:17 +02:00
renovate[bot]
f14d9198ac Update Yarn to v4.10.2 (#27142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-23 12:34:38 +02:00
karwosts
f4e583b302 Don't hide empty entity-filter card in edit mode (#27119) 2025-09-23 07:54:54 +03:00
Paul Bottein
2c602aecee Clear ha-ripple state on disconnected (#27139) 2025-09-23 07:53:07 +03:00
Jan Layola
cbf96898fe Fix history chart issues (#27133)
* Add z-index to reset button

* Add sync-charts property to StateHistoryCharts component
2025-09-23 07:51:57 +03:00
karwosts
6760f4a2ae Support water_heater in thermostat card (#27096) 2025-09-22 22:16:11 +02:00
renovate[bot]
3481f7e8be Update dependency eslint to v9.36.0 (#27137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 18:14:37 +02:00
Wendelin
95a0fe335f Revive automation row overflow menu (#27093) 2025-09-22 16:46:35 +02:00
Aidan Timson
1e2d144d26 Update safe areas for dialogs (#27008) 2025-09-22 15:38:39 +02:00
Jan Layola
6aa89cb532 Fix ha-target-picker remove/expad buttons after tooltip migration (#27134) 2025-09-22 15:22:42 +02:00
Jan-Philipp Benecke
1b0ed7017f Add undo/redo functionality to automation editor (#26796) 2025-09-22 14:41:02 +02:00
Wendelin
1cc7e387da Do not show delete confirm on default choose (#27132) 2025-09-22 11:38:28 +00:00
Wendelin
41bf935f6e Add disabled info to subtitle, fixed disabled overflow (#27128) 2025-09-22 13:33:14 +02:00
Norbert Rittel
b08ea36a1e Explain "picker" using different term (#27114)
* Explain "picker" using different term

* Replace "list" with "menu list"
2025-09-22 09:40:22 +02:00
Paulus Schoutsen
4f52a46725 Fix header positioning for media players domain dashboard (#27123)
* Fix header positioning for media players domain dashboard

* Update home-media-players-view-strategy.ts
2025-09-22 09:09:52 +02:00
renovate[bot]
f8a82563b0 Update Yarn to v4.10.0 (#27125)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 08:38:27 +02:00
renovate[bot]
a1672ccdfb Update dependency del to v8.0.1 (#27120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-22 08:38:00 +02:00
renovate[bot]
bde851e5a4 Update dependency rspack-manifest-plugin to v5.1.0 (#27121) 2025-09-22 08:19:33 +02:00
Bram Kragten
d64acca598 Merge branch 'rc' 2025-09-12 14:43:23 +02:00
Bram Kragten
59571d03a6 Bumped version to 20250903.5 2025-09-12 14:43:09 +02:00
Wendelin
28c515bbac Fix automation sidebar overflow icon size (#27016) 2025-09-12 14:42:36 +02:00
Aidan Timson
27db5b3b02 Move close to the secondary action to allow enter key to submit (#27005) 2025-09-12 14:42:35 +02:00
Wendelin
1922db0474 Fix disabled automation style (#26988)
* Update disabled state logic in action and condition editors to account for indent property

* Remove opacity
2025-09-12 14:42:34 +02:00
Simon Lamon
c8c74a9744 Don't show "condition did not pass" before testing (#26987)
Testing wrapper
2025-09-12 14:42:33 +02:00
Simon Lamon
2c676baa99 Fix Webhook Trigger doesn't display correctly (#26982)
Webhook
2025-09-12 14:42:33 +02:00
Petar Petrov
3e41474faa Fix battery to grid connection in Sankey card (#26973) 2025-09-12 14:42:32 +02:00
Petar Petrov
5f9c69ac21 Fix for batteries with long names in energy dashboard (#26972) 2025-09-12 14:42:31 +02:00
Paul Bottein
8b45ccaaba Only copy/cut/delete selected automation rows (#26966) 2025-09-12 14:42:30 +02:00
Paul Bottein
455925f637 Don't trigger automation shortcuts when a field is focused or text selected (#26965)
* Don't trigger automation shortcut when a field is focused

* Don't trigger automation shortcut when a text is selected
2025-09-12 14:42:29 +02:00
Wendelin
9fba7427f8 Fix yaml editor save in config-flow (#26963) 2025-09-12 14:42:28 +02:00
karwosts
21aae02652 Mark new automation as dirty if it has initData (#26953) 2025-09-12 14:42:28 +02:00
karwosts
24e3fbf622 Fix condition action in config flow dialog (#26929) 2025-09-12 14:42:27 +02:00
Bram Kragten
2cbcf1a689 Merge branch 'rc' 2025-09-04 16:30:48 +02:00
Bram Kragten
1c1c0d70c5 Bumped version to 20250903.3 2025-09-04 16:30:32 +02:00
Paul Bottein
a66f5fb573 Fix testing condition in iOS (#26879) 2025-09-04 16:29:32 +02:00
Petar Petrov
9affeab755 Fix highlighting issue in Energy Sankey card (#26878) 2025-09-04 16:29:31 +02:00
Paul Bottein
2bfaf77908 Add UI editor for trend graph feature (#26872) 2025-09-04 16:29:30 +02:00
Bram Kragten
bc4caae796 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 16:29:29 +02:00
Wendelin
8746acd329 Fix script with fields fields in tile card button feature (#26866)
Check script fields in tile card button feature
2025-09-04 16:29:28 +02:00
Wendelin
96ecf16da2 Translate del in shortcut dialog (#26865) 2025-09-04 16:29:27 +02:00
Paul Bottein
1e95a0f3ef 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 16:29:26 +02:00
Simon Lamon
a164d793b1 Home dashboard: Climate fix (#26856)
* Climate dashboard fix

* Update home-climate-view-strategy.ts
2025-09-04 16:29:25 +02:00
Bram Kragten
510fc71b40 Merge branch 'rc' 2025-09-03 16:20:50 +02:00
Bram Kragten
2a6a3edb77 Bumped version to 20250903.2 2025-09-03 16:20:26 +02:00
Bram Kragten
c7a8796a47 Dont align with clipboard on copy/cut automation item (#26855) 2025-09-03 16:19:59 +02:00
Bram Kragten
9d40fa5f2b Merge branch 'rc' 2025-09-03 12:59:34 +02:00
Bram Kragten
8f2a023775 Bumped version to 20250903.1 2025-09-03 12:59:17 +02:00
Paul Bottein
989b0b34fe 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 12:58:48 +02:00
Bram Kragten
cf94e71215 Merge branch 'rc' 2025-09-03 12:06:15 +02:00
Bram Kragten
49896f3fa6 Bumped version to 20250903.0 2025-09-03 12:05:53 +02:00
Paul Bottein
fc4b7674b1 Improve responsive support for history graph feature (#26851) 2025-09-03 12:01:50 +02:00
Wendelin
04c9f32539 Fix automation narrow bottom sheet close animation (#26850) 2025-09-03 12:01:49 +02:00
Bram Kragten
21e3fc9bb9 Update shortcuts dialog (#26849) 2025-09-03 12:01:48 +02:00
Wendelin
4b78eb7656 Update device action button variant based on warning class (#26848) 2025-09-03 12:01:47 +02:00
Paul Bottein
e6f91aef8e 20250902.1 (#26843) 2025-09-02 21:05:33 +02:00
Paul Bottein
8f99f86c8b Bumped version to 20250902.1 2025-09-02 21:04:49 +02:00
Paul Bottein
b7eff547c7 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 21:04:20 +02:00
Paul Bottein
ceb6b64152 20250902.0 (#26842) 2025-09-02 18:42:11 +02:00
Paul Bottein
d253041376 Bumped version to 20250902.0 2025-09-02 18:41:08 +02:00
Paul Bottein
cb0aa81f89 Change home summaries color (#26839) 2025-09-02 18:40:48 +02:00
Bram Kragten
42061b2f8c Remove expand all/collapse all options from overflow (#26837)
* remove expand all/collapse all options from overflow

* ignore
2025-09-02 18:40:47 +02:00
Bram Kragten
69bfb89a65 Add new shortcuts to shortcuts dialog (#26836) 2025-09-02 18:40:46 +02:00
Bram Kragten
e0307f9688 Align box shadows (#26835)
* Align box shadows

* update colors
2025-09-02 18:40:45 +02:00
Bram Kragten
1cf353461f Tweak automation row (#26834)
tweak automation row
2025-09-02 18:40:44 +02:00
Paul Bottein
1786235c86 Clean graph in security and climate view in home dashboard (#26833) 2025-09-02 18:40:43 +02:00
Simon Lamon
645ba3f9c1 Home dashboard: Ensure temperature sensor entity exists (#26831) 2025-09-02 18:40:42 +02:00
Bram Kragten
b65f6f46e1 Fix scrolling items in the bottom into view (#26830) 2025-09-02 18:40:41 +02:00
Aidan Timson
84ad521b3d Fix capitalization on data table filter button (#26829) 2025-09-02 18:40:40 +02:00
Bram Kragten
dfb9c662e7 add shadow when scrollable in automation bottom sheet, min height 50vh (#26828) 2025-09-02 18:40:39 +02:00
Bram Kragten
5ac42e17b0 prevent keyboard shortcuts with more modifier keys (#26826) 2025-09-02 18:40:38 +02:00
karwosts
be2f19637e Differentate service vs device in more-info link (#26823) 2025-09-02 18:40:37 +02:00
Bram Kragten
b7a6ee3792 Bumped version to 20250901.0 2025-09-01 21:42:55 +02:00
Wendelin
1fb2f0c989 Automation-keybindings (#26762)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-09-01 21:42:19 +02:00
Bram Kragten
b4ad411e6f Change drag selected styling (#26822) 2025-09-01 21:41:20 +02:00
Bram Kragten
5d76a92f73 Update hover and hightlight states for automation rows (#26820) 2025-09-01 21:41:19 +02:00
Paul Bottein
beee09491a Fix tag trigger when using it inside sidebar (#26819) 2025-09-01 21:41:18 +02:00
Paul Bottein
ee5aabdddf Remove box shadow for ha card in automation bottom sheet on mobile (#26817) 2025-09-01 21:41:17 +02:00
Paul Bottein
ec80f6a6f1 Add automation testing logic to sidebar (#26815) 2025-09-01 21:41:16 +02:00
Paul Bottein
9845f0b47c Don't use ha-automation-row-selected to know if the item was selected (#26812) 2025-09-01 21:41:15 +02:00
Paul Bottein
cd294ba619 Force energy distribution to display the data of today (#26811) 2025-09-01 21:41:14 +02:00
Paul Bottein
61e27cb1ea Fix automation sidebar z index (#26810) 2025-09-01 21:41:14 +02:00
Paul Bottein
8d6295e8e8 Update heading subtitle height to better fix grid (#26808) 2025-09-01 21:41:13 +02:00
Paul Bottein
b0e95699f7 Fix add dialog on dashboards on mobile (#26807) 2025-09-01 21:41:12 +02:00
Petar Petrov
c8e1e7b8a8 Handle negative values in History chart card feature (#26806) 2025-09-01 21:41:11 +02:00
Bram Kragten
d2cea159af Enable drag and drop on mobile for automations (#26805) 2025-09-01 21:41:10 +02:00
Petar Petrov
eb5d1c79c8 Use feature-color in History chart feature (#26802) 2025-09-01 21:41:09 +02:00
Paulus Schoutsen
65ab6848ab Do not add graphs to every sensor tile card (#26793) 2025-09-01 21:41:09 +02:00
Paul Bottein
7a1d934e8d Use summary card in home dashboard (#26775) 2025-09-01 21:41:08 +02:00
Wendelin
cbacde12fa Automation editor: fix yaml editor and editor switch (#26772) 2025-09-01 21:41:07 +02:00
Paul Bottein
4c33618e05 20250829.0 (#26776) 2025-08-29 17:49:27 +02:00
Paul Bottein
3837b3e630 Bumped version to 20250829.0 2025-08-29 17:48:01 +02:00
karwosts
7c15633f6d Don't use context for media selector with 'accept' (#26773) 2025-08-29 17:45:59 +02:00
Paul Bottein
f7ec8650eb Add translations for home dashboard (#26763) 2025-08-29 17:45:58 +02:00
Paul Bottein
7674eee0fb Fix alert z-index for automation and script (#26759) 2025-08-29 17:45:13 +02:00
Norbert Rittel
f494a6453a Improve OAuth setup explanation (#26758)
* Improve OAuth setup explanation

* Add "your"

* Include "application" in headline
2025-08-29 17:43:54 +02:00
Wendelin
37f3682ffa Automation editor: fix focus handling (#26755) 2025-08-29 17:42:34 +02:00
Paul Bottein
8055286a1f 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 17:39:43 +02:00
Paul Bottein
0bdd213761 20250828.0 (#26752) 2025-08-28 18:54:59 +02:00
Paul Bottein
810b43760e Bumped version to 20250828.0 2025-08-28 18:51:59 +02:00
Aidan Timson
424d71c55a 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:51:14 +02:00
Petar Petrov
176924241c Increase disk usage request timeout (#26748) 2025-08-28 18:51:13 +02:00
Norbert Rittel
da08aa7fb0 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 18:51:12 +02:00
Wendelin
6047227648 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 18:51:11 +02:00
Norbert Rittel
fc71fd6bc3 Improve section descriptions in Automation editor (#26741)
Replace "listed here" or "list of" with "added here"
2025-08-28 18:51:10 +02:00
Wendelin
90a1b135e1 Fix automation editor drag selected row in/out nested (#26740)
Fix nested sort
2025-08-28 18:51:09 +02:00
Paulus Schoutsen
e19413b6ca Show binary sensors with graphs on the security dashboard (#26738) 2025-08-28 18:51:08 +02:00
Paulus Schoutsen
0dfc10af5f Show configured area sensors on climate domain dashboard (#26737) 2025-08-28 18:51:07 +02:00
karwosts
bbbc419bea Hide 'options' from enum more info (#26736)
* Hide 'options' from enum more info

* restrict to specific domain and class
2025-08-28 18:51:06 +02:00
Aidan Timson
50ad5e376f Move automation and script ha-alerts to main flow (#26735)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-08-28 18:51:05 +02:00
J. Nick Koston
a9f2254bbc Improve network adapter configuration discoverability (#26734) 2025-08-28 18:51:04 +02:00
Paul Bottein
a8836404d4 Use entity picture for home favorite and update home dashboard icon (#26732)
* Add strategy icon

* Use entity picture for favorite
2025-08-28 18:51:03 +02:00
92 changed files with 3595 additions and 1666 deletions

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.4.cjs
yarnPath: .yarn/releases/yarn-4.10.2.cjs

View File

@@ -5,17 +5,17 @@ const castContext = framework.CastReceiverContext.getInstance();
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
"LOAD" as framework.messages.MessageType.LOAD,
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 = "LIVE" as framework.messages.StreamType.LIVE;
media.streamType = framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
media.hlsVideoSegmentFormat =
"fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4;
framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}

View File

@@ -40,8 +40,7 @@ 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 =
"NONE" as framework.messages.StreamType.NONE;
loadRequestData.media.streamType = framework.messages.StreamType.NONE;
const metadata = new framework.messages.GenericMediaMetadata();
metadata.title = viewTitle;
loadRequestData.media.metadata = metadata;
@@ -90,7 +89,7 @@ const showMediaPlayer = () => {
const options = new framework.CastReceiverOptions();
options.disableIdleTimeout = true;
options.customNamespaces = {
[CAST_NS]: "json" as framework.system.MessageType.JSON,
[CAST_NS]: framework.system.MessageType.JSON,
};
castContext.addCustomMessageListener(
@@ -98,7 +97,9 @@ 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() !== "IDLE") {
if (
playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE
) {
playerManager.stop();
} else {
showLovelaceController();
@@ -112,7 +113,7 @@ castContext.addCustomMessageListener(
const playerManager = castContext.getPlayerManager();
playerManager.setMessageInterceptor(
"LOAD" as framework.messages.MessageType.LOAD,
framework.messages.MessageType.LOAD,
(loadRequestData) => {
if (
loadRequestData.media.contentId ===
@@ -126,23 +127,24 @@ playerManager.setMessageInterceptor(
// Special handling if it came from Google Assistant
if (media.entity) {
media.contentId = media.entity;
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
media.streamType = framework.messages.StreamType.LIVE;
media.contentType = "application/vnd.apple.mpegurl";
// @ts-ignore
media.hlsVideoSegmentFormat =
"fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4;
framework.messages.HlsVideoSegmentFormat.FMP4;
}
return loadRequestData;
}
);
playerManager.addEventListener(
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS,
framework.events.EventType.MEDIA_STATUS,
(event) => {
if (
event.mediaStatus?.playerState === "IDLE" &&
event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
event.mediaStatus?.idleReason &&
event.mediaStatus?.idleReason !== "INTERRUPTED"
event.mediaStatus?.idleReason !==
framework.messages.IdleReason.INTERRUPTED
) {
// media finished or stopped, return to default Lovelace
showLovelaceController();

View File

@@ -17,6 +17,10 @@ export const createMediaPlayerEntities = () => [
new Date().getTime() - 23000
).toISOString(),
volume_level: 0.5,
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
source: "AirPlay",
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
sound_mode: "Music",
}),
getEntity("media_player", "music_playing", "playing", {
friendly_name: "Playing The Music",
@@ -24,8 +28,8 @@ export const createMediaPlayerEntities = () => [
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media
supported_features: 195135,
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media + Grouping
supported_features: 784959,
entity_picture: "/images/album_cover.jpg",
media_duration: 300,
media_position: 0,
@@ -34,6 +38,9 @@ export const createMediaPlayerEntities = () => [
new Date().getTime() - 23000
).toISOString(),
volume_level: 0.5,
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
sound_mode: "Music",
group_members: ["media_player.playing", "media_player.stream_playing"],
}),
getEntity("media_player", "stream_playing", "playing", {
friendly_name: "Playing the Stream",
@@ -149,15 +156,18 @@ export const createMediaPlayerEntities = () => [
}),
getEntity("media_player", "receiver_on", "on", {
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
volume_level: 0.63,
is_volume_muted: false,
source: "TV",
sound_mode: "Movie",
friendly_name: "Receiver (selectable sources)",
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
supported_features: 84364,
}),
getEntity("media_player", "receiver_off", "off", {
source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"],
sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"],
friendly_name: "Receiver (selectable sources)",
// Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode
supported_features: 84364,

View File

@@ -0,0 +1,37 @@
---
title: Marquee Text
---
# Marquee Text `<ha-marquee-text>`
Marquee text component scrolls text horizontally if it overflows its container. It supports pausing on hover and customizable speed and pause duration.
## Implementation
### Example Usage
<ha-marquee-text style="width: 200px;">
This is a long text that will scroll horizontally if it overflows the container.
</ha-marquee-text>
```html
<ha-marquee-text style="width: 200px;">
This is a long text that will scroll horizontally if it overflows the
container.
</ha-marquee-text>
```
### API
**Slots**
- default slot: The text content to be displayed and scrolled.
- no default
**Properties/Attributes**
| Name | Type | Default | Description |
| -------------- | ------- | ------- | ---------------------------------------------------------------------------- |
| speed | number | `15` | The speed of the scrolling animation. Higher values result in faster scroll. |
| pause-on-hover | boolean | `true` | Whether to pause the scrolling animation when |
| pause-duration | number | `1000` | The delay in milliseconds before the scrolling animation starts/restarts. |

View File

@@ -0,0 +1,25 @@
import { css, LitElement } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-marquee-text";
@customElement("demo-components-ha-marquee-text")
export class DemoHaMarqueeText extends LitElement {
static styles = css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.card-content {
display: flex;
flex-direction: column;
align-items: flex-start;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-marquee-text": DemoHaMarqueeText;
}
}

View File

@@ -11,7 +11,10 @@ import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import type {
HaFormSchema,
HaFormDataContainer,
} from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-list-item";
@@ -33,6 +36,7 @@ import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
import { hassioStyle } from "../../resources/hassio-style";
import type { ObjectSelector, Selector } from "../../../../src/data/selector";
const SUPPORTED_UI_TYPES = [
"string",
@@ -78,77 +82,124 @@ class HassioAddonConfig extends LitElement {
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
public computeLabel = (entry: HaFormSchema): string =>
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
?.name ||
this.addon.translations.en?.configuration?.[entry.name]?.name ||
private _getTranslationEntry(
language: string,
entry: HaFormSchema,
options?: { path?: string[] }
) {
let parent = this.addon.translations[language]?.configuration;
if (!parent) return undefined;
if (options?.path) {
for (const key of options.path) {
parent = parent[key]?.fields;
if (!parent) return undefined;
}
}
return parent[entry.name];
}
public computeLabel = (
entry: HaFormSchema,
_data: HaFormDataContainer,
options?: { path?: string[] }
): string =>
this._getTranslationEntry(this.hass.language, entry, options)?.name ||
this._getTranslationEntry("en", entry, options)?.name ||
entry.name;
public computeHelper = (entry: HaFormSchema): string =>
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
public computeHelper = (
entry: HaFormSchema,
options?: { path?: string[] }
): string =>
this._getTranslationEntry(this.hass.language, entry, options)
?.description ||
this.addon.translations.en?.configuration?.[entry.name]?.description ||
this._getTranslationEntry("en", entry, options)?.description ||
"";
private _convertSchema = memoizeOne(
// Convert supervisor schema to selectors
(schema: Record<string, any>): HaFormSchema[] =>
schema.map((entry) =>
entry.type === "select"
? {
name: entry.name,
required: entry.required,
selector: { select: { options: entry.options } },
}
: entry.type === "string"
? entry.multiple
? {
name: entry.name,
required: entry.required,
selector: {
select: { options: [], multiple: true, custom_value: true },
},
}
: {
name: entry.name,
required: entry.required,
selector: {
text: {
type: entry.format
? entry.format
: MASKED_FIELDS.includes(entry.name)
? "password"
: "text",
},
},
}
: entry.type === "boolean"
? {
name: entry.name,
required: entry.required,
selector: { boolean: {} },
}
: entry.type === "schema"
? {
name: entry.name,
required: entry.required,
selector: { object: {} },
}
: entry.type === "float" || entry.type === "integer"
? {
name: entry.name,
required: entry.required,
selector: {
number: {
mode: "box",
step: entry.type === "float" ? "any" : undefined,
},
},
}
: entry
)
(schema: readonly HaFormSchema[]): HaFormSchema[] =>
this._convertSchemaElements(schema)
);
private _convertSchemaElements(
schema: readonly HaFormSchema[]
): HaFormSchema[] {
return schema.map((entry) => this._convertSchemaElement(entry));
}
private _convertSchemaElement(entry: any): HaFormSchema {
if (entry.type === "schema" && !entry.multiple) {
return {
name: entry.name,
type: "expandable",
required: entry.required,
schema: this._convertSchemaElements(entry.schema),
};
}
const selector = this._convertSchemaElementToSelector(entry, false);
if (selector) {
return {
name: entry.name,
required: entry.required,
selector,
};
}
return entry;
}
private _convertSchemaElementToSelector(
entry: any,
force: boolean
): Selector | null {
if (entry.type === "select") {
return { select: { options: entry.options } };
}
if (entry.type === "string") {
return entry.multiple
? { select: { options: [], multiple: true, custom_value: true } }
: {
text: {
type: entry.format
? entry.format
: MASKED_FIELDS.includes(entry.name)
? "password"
: "text",
},
};
}
if (entry.type === "boolean") {
return { boolean: {} };
}
if (entry.type === "schema") {
const fields: NonNullable<ObjectSelector["object"]>["fields"] = {};
for (const child_entry of entry.schema) {
fields[child_entry.name] = {
required: child_entry.required,
selector: this._convertSchemaElementToSelector(child_entry, true)!,
};
}
return {
object: {
multiple: entry.multiple,
fields,
},
};
}
if (entry.type === "float" || entry.type === "integer") {
return {
number: {
mode: "box",
step: entry.type === "float" ? "any" : undefined,
},
};
}
if (force) {
return { object: {} };
}
return null;
}
private _filteredSchema = memoizeOne(
(options: Record<string, unknown>, schema: HaFormSchema[]) =>
schema.filter((entry) => entry.name in options || entry.required)

View File

@@ -161,7 +161,7 @@
"@rspack/core": "1.5.5",
"@rspack/dev-server": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.24",
"@types/chromecast-caf-receiver": "6.0.22",
"@types/chromecast-caf-sender": "1.0.11",
"@types/color-name": "2.0.0",
"@types/culori": "4.0.1",
@@ -182,8 +182,8 @@
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.0",
"eslint": "9.35.0",
"del": "8.0.1",
"eslint": "9.36.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.10",
@@ -210,7 +210,7 @@
"map-stream": "0.0.7",
"pinst": "3.0.0",
"prettier": "3.6.2",
"rspack-manifest-plugin": "5.0.3",
"rspack-manifest-plugin": "5.1.0",
"serve": "14.2.5",
"sinon": "21.0.0",
"tar": "7.4.3",
@@ -235,5 +235,5 @@
"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"
},
"packageManager": "yarn@4.9.4"
"packageManager": "yarn@4.10.2"
}

View File

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

View File

@@ -84,6 +84,9 @@ export class StateHistoryCharts extends LitElement {
@property({ attribute: "expand-legend", type: Boolean })
public expandLegend?: boolean;
@property({ attribute: "sync-charts", type: Boolean })
public syncCharts = false;
private _computedStartTime!: Date;
private _computedEndTime!: Date;
@@ -146,7 +149,7 @@ export class StateHistoryCharts extends LitElement {
: html`${combinedItems.map((item, index) =>
this._renderHistoryItem(item, index)
)}`}
${this._hasZoomedCharts
${this.syncCharts && this._hasZoomedCharts
? html`<ha-fab
slot="fab"
class="reset-button"
@@ -190,7 +193,7 @@ export class StateHistoryCharts extends LitElement {
@chart-zoom-with-index=${this._handleTimelineSync}
.height=${this.virtualize ? undefined : this.height}
.expandLegend=${this.expandLegend}
hide-reset-button
?hide-reset-button=${this.syncCharts}
></state-history-chart-line>
</div> `;
}
@@ -209,7 +212,7 @@ export class StateHistoryCharts extends LitElement {
.clickForMoreInfo=${this.clickForMoreInfo}
@y-width-changed=${this._yWidthChanged}
@chart-zoom-with-index=${this._handleTimelineSync}
hide-reset-button
?hide-reset-button=${this.syncCharts}
></state-history-chart-timeline>
</div> `;
};
@@ -302,7 +305,7 @@ export class StateHistoryCharts extends LitElement {
private _handleTimelineSync(
e: CustomEvent<HASSDomEvents["chart-zoom-with-index"]>
) {
if (this._isSyncing) {
if (!this.syncCharts || this._isSyncing) {
return;
}
@@ -444,6 +447,7 @@ export class StateHistoryCharts extends LitElement {
position: fixed;
bottom: calc(24px + var(--safe-area-inset-bottom));
right: calc(24px + var(--safe-area-inset-bottom));
z-index: 1;
}
`;
}

View File

@@ -86,6 +86,10 @@ class HaAlert extends LitElement {
padding: 8px;
display: flex;
}
.icon {
height: var(--ha-alert-icon-size, 24px);
width: var(--ha-alert-icon-size, 24px);
}
.issue-type::after {
position: absolute;
top: 0;

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 } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
@@ -73,14 +73,18 @@ export class HaAnalytics extends LitElement {
.checked=${this.analytics?.preferences[preference]}
.preference=${preference}
name=${preference}
?disabled=${baseEnabled}
>
</ha-switch>
<ha-tooltip .for="switch-${preference}" placement="right">
${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
)}
</ha-tooltip>
${baseEnabled
? nothing
: html`<ha-tooltip
.for="switch-${preference}"
placement="right"
>
${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
)}
</ha-tooltip>`}
</span>
</ha-settings-row>
`

View File

@@ -54,9 +54,9 @@ export class HaBottomSheet extends LitElement {
border-top-left-radius: var(--ha-border-radius-lg);
border-top-right-radius: var(--ha-border-radius-lg);
max-height: 90vh;
margin-bottom: var(--safe-area-inset-bottom);
margin-left: var(--safe-area-inset-left);
margin-right: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
}
`;
}

View File

@@ -5,9 +5,16 @@ import type {
CompletionResult,
CompletionSource,
} from "@codemirror/autocomplete";
import { undo, undoDepth, redo, redoDepth } from "@codemirror/commands";
import type { Extension, TransactionSpec } from "@codemirror/state";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import { mdiArrowExpand, mdiArrowCollapse } from "@mdi/js";
import {
mdiArrowExpand,
mdiArrowCollapse,
mdiContentCopy,
mdiUndo,
mdiRedo,
} from "@mdi/js";
import type { HassEntities } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, ReactiveElement, html, render } from "lit";
@@ -16,11 +23,14 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { getEntityContext } from "../common/entity/context/get_entity_context";
import { copyToClipboard } from "../common/util/copy-clipboard";
import type { HomeAssistant } from "../types";
import { showToast } from "../util/toast";
import "./ha-code-editor-completion-items";
import type { CompletionItem } from "./ha-code-editor-completion-items";
import "./ha-icon";
import "./ha-icon-button";
import "./ha-code-editor-completion-items";
import "./ha-icon-button-toolbar";
import type { HaIconButtonToolbar } from "./ha-icon-button-toolbar";
declare global {
interface HASSDomEvents {
@@ -68,13 +78,24 @@ export class HaCodeEditor extends ReactiveElement {
@property({ type: Boolean, attribute: "disable-fullscreen" })
public disableFullscreen = false;
@property({ type: Boolean, attribute: "has-toolbar" })
public hasToolbar = true;
@state() private _value = "";
@state() private _isFullscreen = false;
@state() private _canUndo = false;
@state() private _canRedo = false;
@state() private _canCopy = false;
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
private _loadedCodeMirror?: typeof import("../resources/codemirror");
private _editorToolbar?: HaIconButtonToolbar;
private _iconList?: Completion[];
public set value(value: string) {
@@ -119,9 +140,7 @@ export class HaCodeEditor extends ReactiveElement {
super.disconnectedCallback();
this.removeEventListener("keydown", stopPropagation);
this.removeEventListener("keydown", this._handleKeyDown);
if (this._isFullscreen) {
this._toggleFullscreen();
}
this._updateFullscreenState(false);
this.updateComplete.then(() => {
this.codemirror!.destroy();
delete this.codemirror;
@@ -157,6 +176,7 @@ export class HaCodeEditor extends ReactiveElement {
this._loadedCodeMirror!.EditorView!.editable.of(!this.readOnly)
),
});
this._updateToolbarButtons();
}
if (changedProps.has("linewrap")) {
transactions.push({
@@ -177,14 +197,25 @@ export class HaCodeEditor extends ReactiveElement {
if (transactions.length > 0) {
this.codemirror.dispatch(...transactions);
}
if (changedProps.has("hasToolbar")) {
this._updateToolbar();
}
if (changedProps.has("error")) {
this.classList.toggle("error-state", this.error);
}
if (changedProps.has("_isFullscreen")) {
this.classList.toggle("fullscreen", this._isFullscreen);
this._updateToolbarButtons();
}
if (
changedProps.has("_canCopy") ||
changedProps.has("_canUndo") ||
changedProps.has("_canRedo")
) {
this._updateToolbarButtons();
}
if (changedProps.has("disableFullscreen")) {
this._updateFullscreenButton();
this._updateFullscreenState();
}
}
@@ -253,6 +284,7 @@ export class HaCodeEditor extends ReactiveElement {
}
}
// Create the code editor
this.codemirror = new this._loadedCodeMirror.EditorView({
state: this._loadedCodeMirror.EditorState.create({
doc: this._value,
@@ -260,71 +292,160 @@ export class HaCodeEditor extends ReactiveElement {
}),
parent: this.renderRoot,
});
this._canCopy = this._value?.length > 0;
this._updateFullscreenButton();
// Update the toolbar. Creating it if required
this._updateToolbar();
}
private _updateFullscreenButton() {
const existingButton = this.renderRoot.querySelector(".fullscreen-button");
private _fullscreenLabel(): string {
if (this._isFullscreen)
return (
this.hass?.localize("ui.components.yaml-editor.exit_fullscreen") ||
"Exit fullscreen"
);
return (
this.hass?.localize("ui.components.yaml-editor.enter_fullscreen") ||
"Enter fullscreen"
);
}
if (this.disableFullscreen) {
// Remove button if it exists and fullscreen is disabled
if (existingButton) {
existingButton.remove();
}
// Exit fullscreen if currently in fullscreen mode
if (this._isFullscreen) {
this._isFullscreen = false;
}
private _fullscreenIcon(): string {
return this._isFullscreen ? mdiArrowCollapse : mdiArrowExpand;
}
private _createEditorToolbar(): HaIconButtonToolbar {
// Create the editor toolbar element
const editorToolbar = document.createElement("ha-icon-button-toolbar");
editorToolbar.classList.add("code-editor-toolbar");
editorToolbar.items = [];
return editorToolbar;
}
private _updateToolbar() {
// Show/Hide the toolbar if we have one.
this.classList.toggle("hasToolbar", this.hasToolbar);
// Update fullscreen state. Handles toolbar and fullscreen mode being disabled.
this._updateFullscreenState();
// If we don't have a toolbar, nothing to update
if (!this.hasToolbar) {
return;
}
// Create button if it doesn't exist
if (!existingButton) {
const button = document.createElement("ha-icon-button");
(button as any).path = this._isFullscreen
? mdiArrowCollapse
: mdiArrowExpand;
button.setAttribute(
"label",
this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen"
);
button.classList.add("fullscreen-button");
// Use bound method to ensure proper this context
button.addEventListener("click", this._handleFullscreenClick);
this.renderRoot.appendChild(button);
} else {
// Update existing button
(existingButton as any).path = this._isFullscreen
? mdiArrowCollapse
: mdiArrowExpand;
existingButton.setAttribute(
"label",
this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen"
);
// If we don't yet have the toolbar, create it.
if (!this._editorToolbar) {
this._editorToolbar = this._createEditorToolbar();
}
// Ensure all toolbar buttons are correctly configured.
this._updateToolbarButtons();
// Render the toolbar. This must be placed as a child of the code
// mirror element to ensure it doesn't affect the positioning and
// size of codemirror.
this.codemirror?.dom.appendChild(this._editorToolbar);
}
private _updateToolbarButtons() {
// Re-render all toolbar items.
if (!this._editorToolbar) {
return;
}
this._editorToolbar.items = [
{
id: "undo",
disabled: !this._canUndo,
label: this.hass?.localize("ui.common.undo") || "Undo",
path: mdiUndo,
action: (e: Event) => this._handleUndoClick(e),
},
{
id: "redo",
disabled: !this._canRedo,
label: this.hass?.localize("ui.common.redo") || "Redo",
path: mdiRedo,
action: (e: Event) => this._handleRedoClick(e),
},
{
id: "copy",
disabled: !this._canCopy,
label:
this.hass?.localize("ui.components.yaml-editor.copy_to_clipboard") ||
"Copy to Clipboard",
path: mdiContentCopy,
action: (e: Event) => this._handleClipboardClick(e),
},
{
id: "fullscreen",
disabled: this.disableFullscreen,
label: this._fullscreenLabel(),
path: this._fullscreenIcon(),
action: (e: Event) => this._handleFullscreenClick(e),
},
];
}
private _updateFullscreenState(
fullscreen: boolean = this._isFullscreen
): boolean {
// Update the current fullscreen state based on selected value. If fullscreen
// is disabled, or we have no toolbar, ensure we are not in fullscreen mode.
this._isFullscreen =
fullscreen && !this.disableFullscreen && this.hasToolbar;
// Return whether successfully in requested state
return this._isFullscreen === fullscreen;
}
private _handleClipboardClick = async (e: Event) => {
e.preventDefault();
e.stopPropagation();
if (this.value) {
await copyToClipboard(this.value);
showToast(this, {
message:
this.hass?.localize("ui.common.copied_clipboard") ||
"Copied to clipboard",
});
}
};
private _handleUndoClick = (e: Event) => {
e.preventDefault();
e.stopPropagation();
if (!this.codemirror) {
return;
}
undo(this.codemirror);
};
private _handleRedoClick = (e: Event) => {
e.preventDefault();
e.stopPropagation();
if (!this.codemirror) {
return;
}
redo(this.codemirror);
};
private _handleFullscreenClick = (e: Event) => {
e.preventDefault();
e.stopPropagation();
this._toggleFullscreen();
this._updateFullscreenState(!this._isFullscreen);
};
private _toggleFullscreen() {
this._isFullscreen = !this._isFullscreen;
this._updateFullscreenButton();
}
private _handleKeyDown = (e: KeyboardEvent) => {
if (this._isFullscreen && e.key === "Escape") {
if (
(e.key === "Escape" &&
this._isFullscreen &&
this._updateFullscreenState(false)) ||
(e.key === "F11" && this._updateFullscreenState(true))
) {
// If we successfully performed the action, stop it propagating further.
e.preventDefault();
e.stopPropagation();
this._toggleFullscreen();
} else if (e.key === "F11" && !this.disableFullscreen) {
e.preventDefault();
e.stopPropagation();
this._toggleFullscreen();
}
};
@@ -592,10 +713,13 @@ export class HaCodeEditor extends ReactiveElement {
}
private _onUpdate = (update: ViewUpdate): void => {
this._canUndo = !this.readOnly && undoDepth(update.state) > 0;
this._canRedo = !this.readOnly && redoDepth(update.state) > 0;
if (!update.docChanged) {
return;
}
this._value = update.state.doc.toString();
this._canCopy = this._value?.length > 0;
fireEvent(this, "value-changed", { value: this._value });
};
@@ -614,39 +738,33 @@ export class HaCodeEditor extends ReactiveElement {
:host {
position: relative;
display: block;
--code-editor-toolbar-height: 28px;
}
:host(.error-state) .cm-gutters {
border-color: var(--error-state-color, red);
border-color: var(--error-state-color, var(--error-color)) !important;
}
.fullscreen-button {
position: absolute;
top: 8px;
right: 8px;
z-index: 1;
color: var(--secondary-text-color);
background-color: var(--secondary-background-color);
border-radius: 50%;
opacity: 0.9;
transition: opacity 0.2s;
--mdc-icon-button-size: 32px;
--mdc-icon-size: 18px;
/* Ensure button is clickable on iOS */
cursor: pointer;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
:host(.hasToolbar) .cm-gutters {
padding-top: 0;
}
.fullscreen-button:hover,
.fullscreen-button:active {
opacity: 1;
:host(.hasToolbar) .cm-focused .cm-gutters {
padding-top: 1px;
}
@media (hover: none) {
.fullscreen-button {
opacity: 0.8;
}
:host(.error-state) .cm-content {
border-color: var(--error-state-color, var(--error-color)) !important;
}
:host(.hasToolbar) .cm-content {
border: none;
border-top: 1px solid var(--secondary-text-color);
}
:host(.hasToolbar) .cm-focused .cm-content {
border-top: 2px solid var(--primary-color);
padding-top: 15px;
}
:host(.fullscreen) {
@@ -655,7 +773,7 @@ export class HaCodeEditor extends ReactiveElement {
left: 8px !important;
right: 8px !important;
bottom: 8px !important;
z-index: 9999 !important;
z-index: 6;
border-radius: 12px !important;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3) !important;
overflow: hidden !important;
@@ -672,15 +790,28 @@ export class HaCodeEditor extends ReactiveElement {
display: block !important;
}
:host(.hasToolbar) .cm-editor {
padding-top: var(--code-editor-toolbar-height);
}
:host(.fullscreen) .cm-editor {
height: 100% !important;
max-height: 100% !important;
border-radius: 0 !important;
}
:host(.fullscreen) .fullscreen-button {
top: calc(var(--safe-area-inset-top, 0px) + 8px);
right: calc(var(--safe-area-inset-right, 0px) + 8px);
:host(:not(.hasToolbar)) .code-editor-toolbar {
display: none !important;
}
.code-editor-toolbar {
--icon-button-toolbar-height: var(--code-editor-toolbar-height);
--icon-button-toolbar-color: var(
--code-editor-gutter-color,
var(--secondary-background-color, whitesmoke)
);
border-top-left-radius: var(--ha-border-radius-sm);
border-top-right-radius: var(--ha-border-radius-sm);
}
.completion-info {

View File

@@ -100,8 +100,6 @@ 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;
@@ -123,11 +121,7 @@ export class HaDialog extends DialogBase {
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-width: var(--mdc-dialog-min-width, 100vw);
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);
@@ -144,18 +138,12 @@ export class HaDialog extends DialogBase {
@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
)
);
min-height: 100vh;
max-height: 100vh;
padding-top: var(--safe-area-inset-top);
padding-bottom: var(--safe-area-inset-bottom);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
}
}

View File

@@ -0,0 +1,126 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, queryAll } from "lit/decorators";
import "./ha-icon";
import "./ha-icon-button";
import type { HaIconButton } from "./ha-icon-button";
import "./ha-icon-button-group";
import "./ha-tooltip";
export interface HaIconButtonToolbarItem {
[key: string]: any;
path: string;
label: string;
id?: string;
disabled?: boolean;
tooltip?: string;
action?: (e: Event) => any;
}
@customElement("ha-icon-button-toolbar")
export class HaIconButtonToolbar extends LitElement {
@property({ type: Array, attribute: false })
public items: (HaIconButtonToolbarItem | string)[] = [];
@queryAll("ha-icon-button") private _buttons?: HaIconButton[];
// Returns all toolbar buttons, or undefined if there are none.
// Optionally returns only those with matching selector.
public findToolbarButtons(selector = ""): HaIconButton[] | undefined {
// Search for all toolbar buttons
const toolbarButtons = this._buttons?.filter((button) =>
button.classList.contains("icon-toolbar-button")
);
if (!toolbarButtons || !toolbarButtons.length) return undefined;
if (!selector.length) return toolbarButtons;
// Filter by user class if provided
const classButtons = toolbarButtons.filter((button) =>
button.querySelector(selector)
);
return classButtons.length ? classButtons : undefined;
}
// Returns a toolbar button based on the provided id.
// Will return undefined if not found.
public findToolbarButtonById(id): HaIconButton | undefined {
// Find the specified id
const element = this.shadowRoot?.getElementById(id);
if (!element || element.localName !== "ha-icon-button") return undefined;
return element as HaIconButton;
}
protected render(): TemplateResult {
return html`
<ha-icon-button-group class="icon-toolbar-buttongroup">
${this.items.map((item) =>
typeof item === "string"
? html`<div class="icon-toolbar-divider" role="separator"></div>`
: html`<ha-tooltip
.disabled=${!item.tooltip}
.for=${item.id ?? "icon-button-" + item.label}
>${item.tooltip ?? ""}</ha-tooltip
>
<ha-icon-button
class="icon-toolbar-button"
.id=${item.id ?? "icon-button-" + item.label}
@click=${item.action}
.label=${item.label}
.path=${item.path}
.disabled=${item.disabled ?? false}
></ha-icon-button>`
)}
</ha-icon-button-group>
`;
}
static styles = css`
:host {
position: absolute;
top: 0px;
width: 100%;
display: flex;
flex-direction: row-reverse;
background-color: var(
--icon-button-toolbar-color,
var(--secondary-background-color, whitesmoke)
);
--icon-button-toolbar-height: 32px;
--icon-button-toolbar-button: calc(
var(--icon-button-toolbar-height) - 4px
);
--icon-button-toolbar-icon: calc(
var(--icon-button-toolbar-height) - 10px
);
}
.icon-toolbar-divider {
height: var(--icon-button-toolbar-icon);
margin: 0px 4px;
border: 0.5px solid
var(--divider-color, var(--secondary-text-color, transparent));
}
.icon-toolbar-buttongroup {
background-color: transparent;
padding-right: 4px;
height: var(--icon-button-toolbar-height);
gap: 8px;
}
.icon-toolbar-button {
color: var(--secondary-text-color);
--mdc-icon-button-size: var(--icon-button-toolbar-button);
--mdc-icon-size: var(--icon-button-toolbar-icon);
/* Ensure button is clickable on iOS */
cursor: pointer;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-icon-button-toolbar": HaIconButtonToolbar;
}
}

View File

@@ -0,0 +1,178 @@
import {
type TemplateResult,
LitElement,
html,
css,
type PropertyValues,
} from "lit";
import { customElement, eventOptions, property, query } from "lit/decorators";
@customElement("ha-marquee-text")
export class HaMarqueeText extends LitElement {
@property({ type: Number }) speed = 15; // pixels per second
@property({ type: Number, attribute: "pause-duration" }) pauseDuration = 1000; // ms delay at ends
@property({ type: Boolean, attribute: "pause-on-hover" })
pauseOnHover = false;
private _direction: "left" | "right" = "left";
private _animationFrame?: number;
@query(".marquee-container")
private _container?: HTMLDivElement;
@query(".marquee-text")
private _textSpan?: HTMLSpanElement;
private _position = 0;
private _maxOffset = 0;
private _pauseTimeout?: number;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._setupAnimation();
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("text")) {
this._setupAnimation();
}
}
public disconnectedCallback() {
super.disconnectedCallback();
if (this._animationFrame) {
cancelAnimationFrame(this._animationFrame);
}
if (this._pauseTimeout) {
clearTimeout(this._pauseTimeout);
this._pauseTimeout = undefined;
}
}
protected render(): TemplateResult {
return html`
<div
class="marquee-container"
@mouseenter=${this._handleMouseEnter}
@mouseleave=${this._handleMouseLeave}
@touchstart=${this._handleMouseEnter}
@touchend=${this._handleMouseLeave}
>
<span class="marquee-text"><slot></slot></span>
</div>
`;
}
private _setupAnimation() {
if (!this._container || !this._textSpan) {
return;
}
this._position = 0;
this._direction = "left";
this._maxOffset = Math.max(
0,
this._textSpan.offsetWidth - this._container.offsetWidth
);
this._textSpan.style.transform = `translateX(0px)`;
if (this._animationFrame) {
cancelAnimationFrame(this._animationFrame);
}
if (this._pauseTimeout) {
clearTimeout(this._pauseTimeout);
this._pauseTimeout = undefined;
}
this._animate();
}
private _animate = () => {
if (!this._container || !this._textSpan) {
return;
}
const dt = 1 / 60; // ~16ms per frame
const pxPerFrame = this.speed * dt;
let reachedEnd = false;
if (this._direction === "left") {
this._position -= pxPerFrame;
if (this._position <= -this._maxOffset) {
this._position = -this._maxOffset;
this._direction = "right";
reachedEnd = true;
}
} else {
this._position += pxPerFrame;
if (this._position >= 0) {
this._position = 0;
this._direction = "left";
reachedEnd = true;
}
}
this._textSpan.style.transform = `translateX(${this._position}px)`;
if (reachedEnd) {
this._pauseTimeout = window.setTimeout(() => {
this._pauseTimeout = undefined;
this._animationFrame = requestAnimationFrame(this._animate);
}, this.pauseDuration);
} else {
this._animationFrame = requestAnimationFrame(this._animate);
}
};
@eventOptions({ passive: true })
private _handleMouseEnter() {
if (this.pauseOnHover && this._animationFrame) {
cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
if (this.pauseOnHover && this._pauseTimeout) {
clearTimeout(this._pauseTimeout);
this._pauseTimeout = undefined;
}
}
private _handleMouseLeave() {
if (this.pauseOnHover && !this._animationFrame && !this._pauseTimeout) {
this._animate();
}
}
static styles = css`
:host {
display: block;
overflow: hidden;
width: 100%;
}
.marquee-container {
width: 100%;
white-space: nowrap;
overflow: hidden;
user-select: none;
cursor: default;
}
.marquee-text {
display: inline-block;
vertical-align: middle;
will-change: transform;
font-size: 1em;
pointer-events: none;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-marquee-text": HaMarqueeText;
}
}

View File

@@ -168,16 +168,18 @@ export class HaMdDialog extends Dialog {
@media all and (max-width: 450px), all and (max-height: 500px) {
:host(:not([type="alert"])) {
min-width: calc(
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
);
max-width: calc(
100vw - var(--safe-area-inset-right) - var(--safe-area-inset-left)
);
min-width: var(--mdc-dialog-min-width, 100vw);
min-height: 100%;
max-height: 100%;
--md-dialog-container-shape: 0;
}
.container {
padding-top: var(--safe-area-inset-top);
padding-bottom: var(--safe-area-inset-bottom);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
}
}
::slotted(ha-dialog-header[slot="headline"]) {

View File

@@ -16,6 +16,14 @@ export class HaRipple extends Ripple {
this.attachableTouchController.attach(control);
}
disconnectedCallback(): void {
super.disconnectedCallback();
// @ts-ignore
this.hovered = false;
// @ts-ignore
this.pressed = false;
}
detach() {
super.detach();
this.attachableTouchController.detach();

View File

@@ -39,22 +39,24 @@ class HaSegmentedBar extends LitElement {
<slot name="extra"></slot>
</div>
<div class="bar">
${this.segments.map((segment) => {
const bar = html`<div
style=${styleMap({
width: `${(segment.value / totalValue) * 100}%`,
backgroundColor: segment.color,
})}
></div>`;
return this.hideTooltip && !segment.label
? bar
: html`
<ha-tooltip>
<span slot="content">${segment.label}</span>
${bar}
</ha-tooltip>
`;
})}
${this.segments.map(
(segment, index) => html`
${this.hideTooltip || !segment.label
? nothing
: html`
<ha-tooltip for="segment-${index}" placement="top">
${segment.label}
</ha-tooltip>
`}
<div
id="segment-${index}"
style=${styleMap({
width: `${(segment.value / totalValue) * 100}%`,
backgroundColor: segment.color,
})}
></div>
`
)}
</div>
${this.hideLegend
? nothing

View File

@@ -18,6 +18,8 @@ export class HaTabGroupTab extends Tab {
opacity: 0.8;
color: inherit;
--wa-space-l: 16px;
}
:host([active]:not([disabled])) {

View File

@@ -522,6 +522,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
private _handleExpand(ev) {
const target = ev.currentTarget as any;
const id = target.id.replace(/^expand-/, "");
const newAreas: string[] = [];
const newDevices: string[] = [];
const newEntities: string[] = [];
@@ -529,7 +530,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
if (target.type === "floor_id") {
Object.values(this.hass.areas).forEach((area) => {
if (
area.floor_id === target.id &&
area.floor_id === id &&
!this.value!.area_id?.includes(area.area_id) &&
this._areaMeetsFilter(area)
) {
@@ -539,7 +540,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
} else if (target.type === "area_id") {
Object.values(this.hass.devices).forEach((device) => {
if (
device.area_id === target.id &&
device.area_id === id &&
!this.value!.device_id?.includes(device.id) &&
this._deviceMeetsFilter(device)
) {
@@ -548,7 +549,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
});
Object.values(this.hass.entities).forEach((entity) => {
if (
entity.area_id === target.id &&
entity.area_id === id &&
!this.value!.entity_id?.includes(entity.entity_id) &&
this._entityRegMeetsFilter(entity)
) {
@@ -558,7 +559,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
} else if (target.type === "device_id") {
Object.values(this.hass.entities).forEach((entity) => {
if (
entity.device_id === target.id &&
entity.device_id === id &&
!this.value!.entity_id?.includes(entity.entity_id) &&
this._entityRegMeetsFilter(entity)
) {
@@ -568,7 +569,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
} else if (target.type === "label_id") {
Object.values(this.hass.areas).forEach((area) => {
if (
area.labels.includes(target.id) &&
area.labels.includes(id) &&
!this.value!.area_id?.includes(area.area_id) &&
this._areaMeetsFilter(area)
) {
@@ -577,7 +578,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
});
Object.values(this.hass.devices).forEach((device) => {
if (
device.labels.includes(target.id) &&
device.labels.includes(id) &&
!this.value!.device_id?.includes(device.id) &&
this._deviceMeetsFilter(device)
) {
@@ -586,7 +587,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
});
Object.values(this.hass.entities).forEach((entity) => {
if (
entity.labels.includes(target.id) &&
entity.labels.includes(id) &&
!this.value!.entity_id?.includes(entity.entity_id) &&
this._entityRegMeetsFilter(entity, true)
) {
@@ -606,14 +607,15 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
if (newAreas.length) {
value = this._addItems(value, "area_id", newAreas);
}
value = this._removeItem(value, target.type, target.id);
value = this._removeItem(value, target.type, id);
fireEvent(this, "value-changed", { value });
}
private _handleRemove(ev) {
const target = ev.currentTarget as any;
const id = target.id.replace(/^remove-/, "");
fireEvent(this, "value-changed", {
value: this._removeItem(this.value, target.type, target.id),
value: this._removeItem(this.value, target.type, id),
});
}

View File

@@ -1,6 +1,28 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
/**
* Home Assistant tile info component
*
* @element ha-tile-info
*
* @summary
* A tile info component, used in tile card in Home Assistant to display primary and secondary text.
*
* @slot primary - The primary text container.
* @slot secondary - The secondary text container.
*
* @cssprop --ha-tile-info-primary-font-size - The font size of the primary text. defaults to `var(--ha-font-size-m)`.
* @cssprop --ha-tile-info-primary-font-weight - The font weight of the primary text. defaults to `var(--ha-font-weight-medium)`.
* @cssprop --ha-tile-info-primary-line-height - The line height of the primary text. defaults to `var(--ha-line-height-normal)`.
* @cssprop --ha-tile-info-primary-letter-spacing - The letter spacing of the primary text. defaults to `0.1px`.
* @cssprop --ha-tile-info-primary-color - The color of the primary text. defaults to `var(--primary-text-color)`.
* @cssprop --ha-tile-info-secondary-font-size - The font size of the secondary text. defaults to `var(--ha-font-size-s)`.
* @cssprop --ha-tile-info-secondary-font-weight - The font weight of the secondary text. defaults to `var(--ha-font-weight-normal)`.
* @cssprop --ha-tile-info-secondary-line-height - The line height of the secondary text. defaults to `var(--ha-line-height-condensed)`.
* @cssprop --ha-tile-info-secondary-letter-spacing - The letter spacing of the secondary text. defaults to `0.4px`.
* @cssprop --ha-tile-info-secondary-color - The color of the secondary text. defaults to `var(--primary-text-color)`.
*/
@customElement("ha-tile-info")
export class HaTileInfo extends LitElement {
@property() public primary?: string;
@@ -21,6 +43,48 @@ export class HaTileInfo extends LitElement {
}
static styles = css`
:host {
--tile-info-primary-font-size: var(
--ha-tile-info-primary-font-size,
var(--ha-font-size-m)
);
--tile-info-primary-font-weight: var(
--ha-tile-info-primary-font-weight,
var(--ha-font-weight-medium)
);
--tile-info-primary-line-height: var(
--ha-tile-info-primary-line-height,
var(--ha-line-height-normal)
);
--tile-info-primary-letter-spacing: var(
--ha-tile-info-primary-letter-spacing,
0.1px
);
--tile-info-primary-color: var(
--ha-tile-info-primary-color,
var(--primary-text-color)
);
--tile-info-secondary-font-size: var(
--ha-tile-info-secondary-font-size,
var(--ha-font-size-s)
);
--tile-info-secondary-font-weight: var(
--ha-tile-info-secondary-font-weight,
var(--ha-font-weight-normal)
);
--tile-info-secondary-line-height: var(
--ha-tile-info-secondary-line-height,
var(--ha-line-height-condensed)
);
--tile-info-secondary-letter-spacing: var(
--ha-tile-info-secondary-letter-spacing,
0.4px
);
--tile-info-secondary-color: var(
--ha-tile-info-secondary-color,
var(--primary-text-color)
);
}
.info {
width: 100%;
display: flex;
@@ -36,18 +100,18 @@ export class HaTileInfo extends LitElement {
width: 100%;
}
.primary {
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
line-height: var(--ha-line-height-normal);
letter-spacing: 0.1px;
color: var(--primary-text-color);
font-size: var(--tile-info-primary-font-size);
font-weight: var(--tile-info-primary-font-weight);
line-height: var(--tile-info-primary-line-height);
letter-spacing: var(--tile-info-primary-letter-spacing);
color: var(--tile-info-primary-color);
}
.secondary {
font-size: var(--ha-font-size-s);
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-condensed);
letter-spacing: 0.4px;
color: var(--primary-text-color);
font-size: var(--tile-info-secondary-font-size);
font-weight: var(--tile-info-secondary-font-weight);
line-height: var(--tile-info-secondary-line-height);
letter-spacing: var(--tile-info-secondary-letter-spacing);
color: var(--tile-info-secondary-color);
}
`;
}

View File

@@ -29,9 +29,15 @@ export type AddonState =
| null;
export type AddonRepository = "core" | "local" | string;
interface AddonFieldTranslation {
name?: string;
description?: string;
fields?: Record<string, AddonFieldTranslation>;
}
interface AddonTranslations {
network?: Record<string, string>;
configuration?: Record<string, { name?: string; description?: string }>;
configuration?: Record<string, AddonFieldTranslation>;
}
export interface HassioAddonInfo {

View File

@@ -279,3 +279,5 @@ export const setSupervisorOption = async (
data
);
};
export const coreLatestLogsUrl = "/api/hassio/core/logs/latest";

View File

@@ -4,6 +4,7 @@ export interface LovelaceBadgeConfig {
type: string;
[key: string]: any;
visibility?: Condition[];
disabled?: boolean;
}
export const ensureBadgeConfig = (

View File

@@ -5,17 +5,20 @@ import type {
} from "home-assistant-js-websocket";
import type { Describe } from "superstruct";
import {
object,
optional,
string,
union,
array,
assign,
boolean,
object,
optional,
refine,
string,
union,
} from "superstruct";
import { arrayLiteralIncludes } from "../common/array/literal-includes";
import { computeObjectId } from "../common/entity/compute_object_id";
import { navigate } from "../common/navigate";
import { hasTemplate } from "../common/string/has-template";
import { createSearchParam } from "../common/url/search-params";
import type { HomeAssistant } from "../types";
import type {
Condition,
@@ -26,9 +29,6 @@ import type {
} from "./automation";
import { migrateAutomationTrigger } from "./automation";
import type { BlueprintInput } from "./blueprint";
import { computeObjectId } from "../common/entity/compute_object_id";
import { createSearchParam } from "../common/url/search-params";
import { hasTemplate } from "../common/string/has-template";
export const MODES = ["single", "restart", "queued", "parallel"] as const;
export const MODES_MAX = ["queued", "parallel"] as const;
@@ -395,6 +395,37 @@ export const hasScriptFields = (
return fields !== undefined && Object.keys(fields).length > 0;
};
export const hasRequiredScriptFields = (
hass: HomeAssistant,
entityId: string
): boolean => {
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
return (
fields !== undefined &&
Object.values(fields).some((field) => field.required)
);
};
export const requiredScriptFieldsFilled = (
hass: HomeAssistant,
entityId: string,
data?: Record<string, any>
): boolean => {
const fields = hass.services.script[computeObjectId(entityId)]?.fields;
if (fields === undefined || Object.keys(fields).length === 0) {
return true;
}
if (data === undefined) {
return false;
}
return Object.entries(fields).every(([key, field]) => {
if (field.required) {
return data[key] !== undefined;
}
return true;
});
};
export const migrateAutomationAction = (
action: Action | Action[]
): Action | Action[] => {

View File

@@ -4,7 +4,7 @@ import type { Action } from "./script";
export const callExecuteScript = (
hass: HomeAssistant,
sequence: Action | Action[]
): Promise<{ context: Context; response: Record<string, any> }> =>
): Promise<{ context: Context; response: Record<string, any> | null }> =>
hass.callWS({
type: "execute_script",
sequence,

View File

@@ -34,6 +34,7 @@ export const DOMAINS_WITH_NEW_MORE_INFO = [
"valve",
"water_heater",
"weather",
"media_player",
];
/** Domains with full height more info dialog */
export const DOMAINS_FULL_HEIGHT_MORE_INFO = ["update"];

View File

@@ -1,6 +1,7 @@
import {
mdiLoginVariant,
mdiMusicNote,
mdiMusicNoteEighth,
mdiPlayBoxMultiple,
mdiSpeakerMultiple,
mdiVolumeHigh,
@@ -8,11 +9,13 @@ import {
mdiVolumeOff,
mdiVolumePlus,
} from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { ifDefined } from "lit/directives/if-defined";
import { classMap } from "lit/directives/class-map";
import { stateActive } from "../../../common/entity/state_active";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { formatDurationDigital } from "../../../common/datetime/format_duration";
import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-select";
@@ -27,12 +30,17 @@ import type {
MediaPlayerEntity,
} from "../../../data/media-player";
import {
MediaPlayerEntityFeature,
computeMediaControls,
handleMediaControlClick,
MediaPlayerEntityFeature,
mediaPlayerPlayMedia,
} from "../../../data/media-player";
import type { HomeAssistant } from "../../../types";
import HassMediaPlayerEntity from "../../../util/hass-media-player-model";
import "../../../components/ha-md-button-menu";
import "../../../components/chips/ha-assist-chip";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-marquee-text";
@customElement("more-info-media_player")
class MoreInfoMediaPlayer extends LitElement {
@@ -40,259 +48,488 @@ class MoreInfoMediaPlayer extends LitElement {
@property({ attribute: false }) public stateObj?: MediaPlayerEntity;
private _formateDuration(duration: number) {
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
const seconds = duration % 60;
return formatDurationDigital(this.hass.locale, {
hours,
minutes,
seconds,
})!;
}
protected _renderVolumeControl() {
if (!this.stateObj) {
return nothing;
}
const supportsMute = supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_MUTE
);
const supportsSliding = supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_SET
);
return html`${(supportsFeature(
this.stateObj!,
MediaPlayerEntityFeature.VOLUME_SET
) ||
supportsFeature(this.stateObj!, MediaPlayerEntityFeature.VOLUME_STEP)) &&
stateActive(this.stateObj!)
? html`
<div class="volume">
${supportsMute
? html`
<ha-icon-button
.path=${this.stateObj.attributes.is_volume_muted
? mdiVolumeOff
: mdiVolumeHigh}
.label=${this.hass.localize(
`ui.card.media_player.${
this.stateObj.attributes.is_volume_muted
? "media_volume_unmute"
: "media_volume_mute"
}`
)}
@click=${this._toggleMute}
></ha-icon-button>
`
: ""}
${supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_STEP
) && !supportsSliding
? html`
<ha-icon-button
action="volume_down"
.path=${mdiVolumeMinus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_down"
)}
@click=${this._handleClick}
></ha-icon-button>
<ha-icon-button
action="volume_up"
.path=${mdiVolumePlus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_up"
)}
@click=${this._handleClick}
></ha-icon-button>
`
: nothing}
${supportsSliding
? html`
${!supportsMute
? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>`
: nothing}
<ha-slider
labeled
id="input"
.value=${Number(this.stateObj.attributes.volume_level) *
100}
@change=${this._selectedValueChanged}
></ha-slider>
`
: nothing}
</div>
`
: nothing}`;
}
protected _renderSourceControl() {
if (
!this.stateObj ||
!supportsFeature(this.stateObj, MediaPlayerEntityFeature.SELECT_SOURCE) ||
!this.stateObj.attributes.source_list?.length
) {
return nothing;
}
return html`<ha-md-button-menu positioning="popover">
<ha-button
slot="trigger"
appearance="plain"
variant="neutral"
size="small"
title=${this.hass.localize(`ui.card.media_player.source`)}
>
<ha-svg-icon .path=${mdiLoginVariant}></ha-svg-icon>
</ha-button>
${this.stateObj.attributes.source_list!.map(
(source) =>
html`<ha-md-menu-item
data-source=${source}
@click=${this._handleSourceClick}
@keydown=${this._handleSourceClick}
.selected=${source === this.stateObj?.attributes.source}
>
${source}
</ha-md-menu-item>`
)}
</ha-md-button-menu>`;
}
protected _renderSoundMode() {
if (
!this.stateObj ||
!supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.SELECT_SOUND_MODE
) ||
!this.stateObj.attributes.sound_mode_list?.length
) {
return nothing;
}
return html`<ha-md-button-menu positioning="popover">
<ha-button
slot="trigger"
appearance="plain"
variant="neutral"
size="small"
title=${this.hass.localize(`ui.card.media_player.sound_mode`)}
>
<ha-svg-icon .path=${mdiMusicNoteEighth}></ha-svg-icon>
</ha-button>
${this.stateObj.attributes.sound_mode_list!.map(
(soundMode) =>
html`<ha-md-menu-item
data-sound-mode=${soundMode}
@click=${this._handleSoundModeClick}
@keydown=${this._handleSoundModeClick}
.selected=${soundMode === this.stateObj?.attributes.sound_mode}
>
${soundMode}
</ha-md-menu-item>`
)}
</ha-md-button-menu>`;
}
protected _renderGrouping() {
if (
!this.stateObj ||
isUnavailableState(this.stateObj.state) ||
!supportsFeature(this.stateObj, MediaPlayerEntityFeature.GROUPING)
) {
return nothing;
}
const groupMembers = this.stateObj.attributes.group_members;
const hasMultipleMembers = groupMembers && groupMembers?.length > 1;
return html`<ha-button
class="grouping"
@click=${this._showGroupMediaPlayers}
appearance="plain"
variant="neutral"
size="small"
title=${this.hass.localize("ui.card.media_player.join")}
>
<div>
<ha-svg-icon .path=${mdiSpeakerMultiple}></ha-svg-icon>
${hasMultipleMembers
? html`<span class="badge"> ${groupMembers?.length || 4} </span>`
: nothing}
</div>
</ha-button>`;
}
protected _renderEmptyCover(title: string, icon?: string) {
return html`
<div class="cover-container">
<div class="cover-image empty-cover" role="img" aria-label=${title}>
${icon ? html`<ha-svg-icon .path=${icon}></ha-svg-icon>` : title}
</div>
</div>
`;
}
protected render() {
if (!this.stateObj) {
return nothing;
}
if (isUnavailableState(this.stateObj.state)) {
return this._renderEmptyCover(this.hass.formatEntityState(this.stateObj));
}
const stateObj = this.stateObj;
const controls = computeMediaControls(stateObj, true);
const groupMembers = stateObj.attributes.group_members?.length;
const coverUrl = stateObj.attributes.entity_picture || "";
const playerObj = new HassMediaPlayerEntity(this.hass, this.stateObj);
const position = Math.floor(playerObj.currentProgress) || 0;
const duration = stateObj.attributes.media_duration || 0;
const remaining = duration - position;
const durationFormated =
remaining > 0 ? this._formateDuration(remaining) : 0;
const postionFormated = this._formateDuration(position);
const primaryTitle = playerObj.primaryTitle;
const secondaryTitle = playerObj.secondaryTitle;
const turnOn = controls?.find((c) => c.action === "turn_on");
const turnOff = controls?.find((c) => c.action === "turn_off");
return html`
<div class="controls">
<div class="basic-controls">
${!controls
? ""
: controls.map(
(control) => html`
<ha-icon-button
action=${control.action}
@click=${this._handleClick}
.path=${control.icon}
.label=${this.hass.localize(
`ui.card.media_player.${control.action}`
)}
>
</ha-icon-button>
`
)}
</div>
${!isUnavailableState(stateObj.state) &&
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
? html`
<ha-button
@click=${this._showBrowseMedia}
appearance="plain"
size="small"
>
<ha-svg-icon
.path=${mdiPlayBoxMultiple}
slot="start"
></ha-svg-icon>
${this.hass.localize("ui.card.media_player.browse_media")}
</ha-button>
`
: ""}
${!isUnavailableState(stateObj.state) &&
supportsFeature(stateObj, MediaPlayerEntityFeature.GROUPING)
? html`
<ha-button
@click=${this._showGroupMediaPlayers}
appearance="plain"
size="small"
>
<ha-svg-icon
.path=${mdiSpeakerMultiple}
slot="start"
></ha-svg-icon>
${groupMembers && groupMembers > 1
? html`<span class="badge">
${stateObj.attributes.group_members?.length || 4}
</span>`
: nothing}
${this.hass.localize("ui.card.media_player.join")}
</ha-button>
`
: ""}
</div>
${(supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET) ||
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_STEP)) &&
stateActive(stateObj)
${coverUrl
? html`<div class="cover-container">
<img
class=${classMap({
"cover-image": true,
"cover-image--playing": stateObj.state === "playing",
})}
src=${coverUrl}
alt=${ifDefined(primaryTitle)}
/>
</div>`
: this._renderEmptyCover(
this.hass.formatEntityState(this.stateObj),
mdiMusicNote
)}
${primaryTitle || secondaryTitle
? html`<div class="media-info-row">
${primaryTitle
? html`<ha-marquee-text
class="media-title"
speed="30"
pause-on-hover
>
${primaryTitle}
</ha-marquee-text>`
: nothing}
${secondaryTitle
? html`<ha-marquee-text
class="media-artist"
speed="30"
pause-on-hover
>
${secondaryTitle}
</ha-marquee-text>`
: nothing}
</div>`
: nothing}
${duration && duration > 0
? html`
<div class="volume">
${supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_MUTE)
? html`
<ha-icon-button
.path=${stateObj.attributes.is_volume_muted
? mdiVolumeOff
: mdiVolumeHigh}
.label=${this.hass.localize(
`ui.card.media_player.${
stateObj.attributes.is_volume_muted
? "media_volume_unmute"
: "media_volume_mute"
}`
)}
@click=${this._toggleMute}
></ha-icon-button>
`
: ""}
${supportsFeature(
stateObj,
MediaPlayerEntityFeature.VOLUME_SET
) ||
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_STEP)
? html`
<ha-icon-button
action="volume_down"
.path=${mdiVolumeMinus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_down"
)}
@click=${this._handleClick}
></ha-icon-button>
<ha-icon-button
action="volume_up"
.path=${mdiVolumePlus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_up"
)}
@click=${this._handleClick}
></ha-icon-button>
`
: ""}
${supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET)
? html`
<ha-slider
labeled
id="input"
.value=${Number(stateObj.attributes.volume_level) * 100}
@change=${this._selectedValueChanged}
></ha-slider>
`
: ""}
</div>
`
: ""}
${stateActive(stateObj) &&
supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOURCE) &&
stateObj.attributes.source_list?.length
? html`
<div class="source-input">
<ha-select
.label=${this.hass.localize("ui.card.media_player.source")}
icon
.value=${stateObj.attributes.source!}
@selected=${this._handleSourceChanged}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
>
${stateObj.attributes.source_list!.map(
(source) => html`
<ha-list-item .value=${source}>
${this.hass.formatEntityAttributeValue(
stateObj,
"source",
source
)}
</ha-list-item>
`
<div class="position-bar">
<ha-slider
min="0"
max=${duration}
step="1"
.value=${position}
aria-label=${this.hass.localize(
"ui.card.media_player.track_position"
)}
<ha-svg-icon .path=${mdiLoginVariant} slot="icon"></ha-svg-icon>
</ha-select>
@change=${this._handleMediaSeekChanged}
?disabled=${!stateActive(stateObj) ||
!supportsFeature(stateObj, MediaPlayerEntityFeature.SEEK)}
></ha-slider>
<div class="position-info-row">
<span class="position-time">${postionFormated}</span>
<span class="duration-time">${durationFormated}</span>
</div>
</div>
`
: nothing}
${stateActive(stateObj) &&
supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOUND_MODE) &&
stateObj.attributes.sound_mode_list?.length
? html`
<div class="sound-input">
<ha-select
.label=${this.hass.localize("ui.card.media_player.sound_mode")}
.value=${stateObj.attributes.sound_mode!}
icon
fixedMenuPosition
naturalMenuWidth
@selected=${this._handleSoundModeChanged}
@closed=${stopPropagation}
>
${stateObj.attributes.sound_mode_list.map(
(mode) => html`
<ha-list-item .value=${mode}>
${this.hass.formatEntityAttributeValue(
stateObj,
"sound_mode",
mode
<div class="bottom-controls">
${controls && controls.length > 0
? html`<div class="main-controls">
${["repeat_set", "media_previous_track"].map((action) => {
const control = controls?.find((c) => c.action === action);
return control
? html`<ha-icon-button
action=${action}
@click=${this._handleClick}
.path=${control.icon}
.label=${this.hass.localize(
`ui.card.media_player.${control.action}`
)}
</ha-list-item>
`
>
</ha-icon-button>`
: html`<span class="spacer"></span>`;
})}
${["media_play_pause", "media_pause", "media_play"].map(
(action) => {
const control = controls?.find((c) => c.action === action);
return control
? html`<ha-button
variant="brand"
appearance="filled"
size="medium"
action=${action}
@click=${this._handleClick}
class="center-control"
>
<ha-svg-icon
.path=${control.icon}
aria-label=${this.hass.localize(
`ui.card.media_player.${control.action}`
)}
></ha-svg-icon>
</ha-button>`
: nothing;
}
)}
${["media_next_track", "shuffle_set"].map((action) => {
const control = controls?.find((c) => c.action === action);
return control
? html`<ha-icon-button
action=${action}
@click=${this._handleClick}
.path=${control.icon}
.label=${this.hass.localize(
`ui.card.media_player.${control.action}`
)}
>
</ha-icon-button>`
: html`<span class="spacer"></span>`;
})}
</div>`
: nothing}
${this._renderVolumeControl()}
<div class="controls-row">
${!isUnavailableState(stateObj.state) &&
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
? html`
<ha-button
@click=${this._showBrowseMedia}
appearance="plain"
variant="neutral"
size="small"
title=${this.hass.localize(
"ui.card.media_player.browse_media"
)}
>
<ha-svg-icon .path=${mdiPlayBoxMultiple}></ha-svg-icon>
</ha-button>
`
: nothing}
${this._renderGrouping()} ${this._renderSourceControl()}
${this._renderSoundMode()}
${turnOn
? html`<ha-button
action=${turnOn.action}
@click=${this._handleClick}
appearance="plain"
variant="neutral"
size="small"
title=${this.hass.localize(
`ui.card.media_player.${turnOn.action}`
)}
<ha-svg-icon .path=${mdiMusicNote} slot="icon"></ha-svg-icon>
</ha-select>
</div>
`
: ""}
>
<ha-svg-icon .path=${turnOn.icon}></ha-svg-icon>
</ha-button>`
: nothing}
${turnOff
? html`<ha-button
action=${turnOff.action}
@click=${this._handleClick}
appearance="plain"
variant="neutral"
size="small"
title=${this.hass.localize(
`ui.card.media_player.${turnOff.action}`
)}
>
<ha-svg-icon .path=${turnOff.icon}></ha-svg-icon>
</ha-button>`
: nothing}
</div>
</div>
`;
}
static styles = css`
ha-slider {
flex-grow: 1;
}
ha-icon-button[action="turn_off"],
ha-icon-button[action="turn_on"] {
margin-right: auto;
margin-left: inherit;
margin-inline-start: inherit;
margin-inline-end: auto;
}
.controls {
:host {
display: flex;
flex-wrap: wrap;
flex-direction: column;
gap: 24px;
margin-top: 0;
}
.cover-container {
display: flex;
justify-content: center;
align-items: center;
--mdc-theme-primary: currentColor;
direction: ltr;
height: 320px;
width: 100%;
}
.basic-controls {
display: inline-flex;
flex-grow: 1;
.cover-image {
width: 240px;
height: 240px;
max-width: 100%;
max-height: 100%;
object-fit: cover;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
position: relative;
display: flex;
align-items: center;
justify-content: center;
transition:
width 0.3s,
height 0.3s;
}
.volume {
direction: ltr;
.cover-image--playing {
width: 320px;
height: 320px;
}
.source-input,
.sound-input {
direction: var(--direction);
.empty-cover {
background-color: var(--secondary-background-color);
font-size: 1.5em;
color: var(--secondary-text-color);
}
.volume,
.source-input,
.sound-input {
.main-controls {
display: flex;
align-items: center;
justify-content: space-between;
}
.source-input ha-select,
.sound-input ha-select {
margin-left: 10px;
flex-grow: 1;
margin-inline-start: 10px;
margin-inline-end: initial;
direction: var(--direction);
.center-control {
--ha-button-height: 56px;
}
.tts {
margin-top: 16px;
font-style: italic;
.spacer {
width: 48px;
}
ha-button > ha-svg-icon {
vertical-align: text-bottom;
.volume,
.position-bar,
.main-controls {
direction: ltr;
}
.volume ha-slider,
.position-bar ha-slider {
width: 100%;
}
.volume {
display: flex;
align-items: center;
gap: 12px;
margin-left: 8px;
}
.volume ha-svg-icon {
padding: 4px;
height: 16px;
width: 16px;
}
.volume ha-icon-button {
--mdc-icon-button-size: 32px;
--mdc-icon-size: 16px;
}
.badge {
position: absolute;
top: -6px;
left: 24px;
top: -10px;
left: 16px;
display: flex;
justify-content: center;
align-items: center;
@@ -301,9 +538,68 @@ class MoreInfoMediaPlayer extends LitElement {
border-radius: 10px;
font-weight: var(--ha-font-weight-normal);
font-size: var(--ha-font-size-xs);
background-color: var(--accent-color);
background-color: var(--primary-color);
padding: 0 4px;
color: var(--text-accent-color, var(--text-primary-color));
color: var(--primary-text-color);
}
.position-bar {
display: flex;
flex-direction: column;
}
.position-info-row {
display: flex;
flex-direction: row;
justify-content: space-between;
color: var(--secondary-text-color);
padding: 0 8px;
font-size: var(--ha-font-size-s);
}
.media-info-row {
display: flex;
flex-direction: column;
justify-content: space-between;
margin: 8px 0 8px 8px;
}
.media-title {
font-size: var(--ha-font-size-xl);
font-weight: var(--ha-font-weight-bold);
margin-bottom: 4px;
}
.media-artist {
font-size: var(--ha-font-size-l);
font-weight: var(--ha-font-weight-normal);
color: var(--secondary-text-color);
}
.controls-row {
display: flex;
align-items: center;
justify-content: space-around;
}
.controls-row ha-button {
width: 32px;
}
.controls-row ha-svg-icon {
color: var(--ha-color-on-neutral-quiet);
}
.grouping::part(label) {
position: relative;
}
.bottom-controls {
display: flex;
flex-direction: column;
gap: 24px;
align-self: center;
width: 320px;
}
`;
@@ -329,29 +625,29 @@ class MoreInfoMediaPlayer extends LitElement {
});
}
private _handleSourceChanged(e) {
const newVal = e.target.value;
if (!newVal || this.stateObj!.attributes.source === newVal) {
private _handleSourceClick(e: Event) {
const source = (e.currentTarget as HTMLElement).getAttribute("data-source");
if (!source || this.stateObj!.attributes.source === source) {
return;
}
this.hass.callService("media_player", "select_source", {
entity_id: this.stateObj!.entity_id,
source: newVal,
source,
});
}
private _handleSoundModeChanged(e) {
const newVal = e.target.value;
if (!newVal || this.stateObj?.attributes.sound_mode === newVal) {
private _handleSoundModeClick(e: Event) {
const soundMode = (e.currentTarget as HTMLElement).getAttribute(
"data-sound-mode"
);
if (!soundMode || this.stateObj!.attributes.sound_mode === soundMode) {
return;
}
this.hass.callService("media_player", "select_sound_mode", {
entity_id: this.stateObj!.entity_id,
sound_mode: newVal,
sound_mode: soundMode,
});
}
@@ -374,6 +670,18 @@ class MoreInfoMediaPlayer extends LitElement {
entityId: this.stateObj!.entity_id,
});
}
private async _handleMediaSeekChanged(e: Event): Promise<void> {
if (!this.stateObj) {
return;
}
const newValue = (e.target as any).value;
this.hass.callService("media_player", "media_seek", {
entity_id: this.stateObj.entity_id,
seek_position: newValue,
});
}
}
declare global {

View File

@@ -3,20 +3,24 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-relative-time";
import "../../../components/ha-service-control";
import { listenMediaQuery } from "../../../common/dom/media_query";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import "../../../components/entity/state-info";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import "../../../components/entity/state-info";
import type { HomeAssistant } from "../../../types";
import type { ScriptEntity } from "../../../data/script";
import { canRun } from "../../../data/script";
import { isUnavailableState } from "../../../data/entity";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import { listenMediaQuery } from "../../../common/dom/media_query";
import "../components/ha-more-info-state-header";
import type { ExtEntityRegistryEntry } from "../../../data/entity_registry";
import "../../../components/ha-markdown";
import "../../../components/ha-relative-time";
import "../../../components/ha-service-control";
import { isUnavailableState } from "../../../data/entity";
import type { ExtEntityRegistryEntry } from "../../../data/entity_registry";
import type { ScriptEntity } from "../../../data/script";
import {
canRun,
hasRequiredScriptFields,
requiredScriptFieldsFilled,
} from "../../../data/script";
import type { HomeAssistant } from "../../../types";
import "../components/ha-more-info-state-header";
@customElement("more-info-script")
class MoreInfoScript extends LitElement {
@@ -26,6 +30,8 @@ class MoreInfoScript extends LitElement {
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry;
@property({ attribute: false }) public data?: Record<string, any>;
@state() private _scriptData: Record<string, any> = {};
@state() private narrow = false;
@@ -110,7 +116,10 @@ class MoreInfoScript extends LitElement {
hide-picker
hide-description
.hass=${this.hass}
.value=${this._scriptData}
.value=${{
...(this.data ? { data: this.data } : {}),
...this._scriptData,
}}
.showAdvanced=${this.hass.userData?.showAdvanced}
.narrow=${this.narrow}
@value-changed=${this._scriptDataChanged}
@@ -198,7 +207,13 @@ class MoreInfoScript extends LitElement {
private _canRun() {
if (
canRun(this.stateObj!) ||
!hasRequiredScriptFields(this.hass, this.stateObj!.entity_id) ||
(requiredScriptFieldsFilled(
this.hass,
this.stateObj!.entity_id,
this._scriptData.data
) &&
canRun(this.stateObj!)) ||
// Restart can also always runs. Just cancels other run.
this.stateObj!.attributes.mode === "restart"
) {

View File

@@ -64,6 +64,7 @@ export interface MoreInfoDialogParams {
view?: View;
/** @deprecated Use `view` instead */
tab?: View;
data?: Record<string, any>;
}
type View = "info" | "history" | "settings" | "related";
@@ -96,6 +97,8 @@ export class MoreInfoDialog extends LitElement {
@state() private _entityId?: string | null;
@state() private _data?: Record<string, any>;
@state() private _currView: View = DEFAULT_VIEW;
@state() private _initialView: View = DEFAULT_VIEW;
@@ -116,6 +119,8 @@ export class MoreInfoDialog extends LitElement {
this.closeDialog();
return;
}
this._data = params.data;
this._currView = params.view || DEFAULT_VIEW;
this._initialView = params.view || DEFAULT_VIEW;
this._childView = undefined;
@@ -570,6 +575,7 @@ export class MoreInfoDialog extends LitElement {
.entityId=${this._entityId}
.entry=${this._entry}
.editMode=${this._infoEditMode}
.data=${this._data}
></ha-more-info-info>
`
: this._currView === "history"

View File

@@ -59,9 +59,18 @@ export class MoreInfoHistory extends LitElement {
return html`${isComponentLoaded(this.hass, "history")
? html`<div class="header">
<h2>
${this.hass.localize("ui.dialogs.more_info_control.history")}
</h2>
<div>
<h2>
${this.hass.localize("ui.dialogs.more_info_control.history")}
</h2>
${this._statistics
? html`<div class="header-secondary">
${this.hass.localize(
"ui.dialogs.more_info_control.aggregate"
)}
</div>`
: nothing}
</div>
${__DEMO__
? nothing
: html`<a href=${this._showMoreHref}
@@ -246,6 +255,10 @@ export class MoreInfoHistory extends LitElement {
a:visited {
color: var(--primary-color);
}
.header-secondary {
font-size: var(--ha-font-size-s);
color: var(--secondary-text-color);
}
h2 {
margin: 0;
}

View File

@@ -27,6 +27,8 @@ export class MoreInfoInfo extends LitElement {
@property({ attribute: false }) public editMode?: boolean;
@property({ attribute: false }) public data?: Record<string, any>;
@state() private _sensorNumericDeviceClasses?: string[] = [];
private async _loadNumericDeviceClasses() {
@@ -105,6 +107,7 @@ export class MoreInfoInfo extends LitElement {
.hass=${this.hass}
.entry=${this.entry}
.editMode=${this.editMode}
.data=${this.data}
></more-info-content>
</div>
</div>

View File

@@ -6,14 +6,14 @@ import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import "../../components/ha-badge";
import type { ExtEntityRegistryEntry } from "../../data/entity_registry";
import { supportsCoverPositionCardFeature } from "../../panels/lovelace/card-features/hui-cover-position-card-feature";
import { supportsLightBrightnessCardFeature } from "../../panels/lovelace/card-features/hui-light-brightness-card-feature";
import type { LovelaceCardFeatureConfig } from "../../panels/lovelace/card-features/types";
import type { TileCardConfig } from "../../panels/lovelace/cards/types";
import { importMoreInfoControl } from "../../panels/lovelace/custom-card-helpers";
import "../../panels/lovelace/sections/hui-section";
import type { HomeAssistant } from "../../types";
import { stateMoreInfoType } from "./state_more_info_control";
import type { LovelaceCardFeatureConfig } from "../../panels/lovelace/card-features/types";
import { supportsLightBrightnessCardFeature } from "../../panels/lovelace/card-features/hui-light-brightness-card-feature";
import { supportsCoverPositionCardFeature } from "../../panels/lovelace/card-features/hui-cover-position-card-feature";
@customElement("more-info-content")
class MoreInfoContent extends LitElement {
@@ -25,6 +25,8 @@ class MoreInfoContent extends LitElement {
@property({ attribute: false }) public editMode?: boolean;
@property({ attribute: false }) public data?: Record<string, any>;
protected render() {
let moreInfoType: string | undefined;
@@ -48,6 +50,7 @@ class MoreInfoContent extends LitElement {
stateObj: this.stateObj,
entry: this.entry,
editMode: this.editMode,
data: this.data,
})}
${this._showEntityMembers(this.stateObj)
? html`

View File

@@ -104,6 +104,16 @@ const _SHORTCUTS: Section[] = [
descriptionTranslationKey:
"ui.dialogs.shortcuts.automation_script.save",
},
{
shortcut: [CTRL_CMD, "Z"],
descriptionTranslationKey:
"ui.dialogs.shortcuts.automation_script.undo",
},
{
shortcut: [CTRL_CMD, "Y"],
descriptionTranslationKey:
"ui.dialogs.shortcuts.automation_script.redo",
},
],
},
{

View File

@@ -22,6 +22,7 @@ import { demoPanels } from "./demo_panels";
import { demoServices } from "./demo_services";
import type { Entity } from "./entity";
import { getEntity } from "./entity";
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
const ensureArray = <T>(val: T | T[]): T[] =>
Array.isArray(val) ? val : [val];
@@ -147,6 +148,17 @@ export const provideHass = (
} else {
updateStates(states);
}
for (const ent of ensureArray(newEntities)) {
hass().entities[ent.entityId] = {
entity_id: ent.entityId,
name: ent.name,
icon: ent.icon,
platform: "demo",
labels: [],
} satisfies EntityRegistryDisplayEntry;
}
updateFormatFunctions();
}

View File

@@ -882,12 +882,8 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
}
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;
--mdc-dialog-min-height: 100%;
--mdc-dialog-max-height: 100%;
--vertical-align-dialog: flex-end;

View File

@@ -0,0 +1,60 @@
import type { LitElement } from "lit";
import type { Constructor } from "../types";
export const UndoRedoMixin = <T extends Constructor<LitElement>, ConfigType>(
superClass: T
) => {
class UndoRedoClass extends superClass {
private _undoStack: ConfigType[] = [];
private _redoStack: ConfigType[] = [];
protected _undoStackLimit = 75;
protected pushToUndo(config: ConfigType) {
if (this._undoStack.length >= this._undoStackLimit) {
this._undoStack.shift();
}
this._undoStack.push({ ...config });
this._redoStack = [];
}
public undo() {
const currentConfig = this.currentConfig;
if (this._undoStack.length === 0 || !currentConfig) {
return;
}
this._redoStack.push({ ...currentConfig });
const config = this._undoStack.pop()!;
this.applyUndoRedo(config);
}
public redo() {
const currentConfig = this.currentConfig;
if (this._redoStack.length === 0 || !currentConfig) {
return;
}
this._undoStack.push({ ...currentConfig });
const config = this._redoStack.pop()!;
this.applyUndoRedo(config);
}
public get canUndo(): boolean {
return this._undoStack.length > 0;
}
public get canRedo(): boolean {
return this._redoStack.length > 0;
}
protected get currentConfig(): ConfigType | undefined {
return undefined;
}
protected applyUndoRedo(_: ConfigType) {
throw new Error("applyUndoRedo not implemented");
}
}
return UndoRedoClass;
};

View File

@@ -1,6 +1,7 @@
import { consume } from "@lit/context";
import {
mdiAlertCircleCheck,
mdiAppleKeyboardCommand,
mdiArrowDown,
mdiArrowUp,
mdiContentCopy,
@@ -16,7 +17,7 @@ import {
} from "@mdi/js";
import deepClone from "deep-clone-simple";
import { dump } from "js-yaml";
import type { PropertyValues } from "lit";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -73,9 +74,10 @@ import {
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import { showToast } from "../../../../util/toast";
import "../ha-automation-editor-warning";
import { rowStyles } from "../styles";
import { overflowStyles, rowStyles } from "../styles";
import "./ha-automation-action-editor";
import type HaAutomationActionEditor from "./ha-automation-action-editor";
import "./types/ha-automation-action-choose";
@@ -225,6 +227,20 @@ export default class HaAutomationActionRow extends LitElement {
}
}
private _renderOverflowLabel(label: string, shortcut?: TemplateResult) {
return html`
<div class="overflow-label">
${label}
${this.optionsInSidebar && !this.narrow
? shortcut ||
html`<span
class="shortcut-placeholder ${isMac ? "mac" : ""}"
></span>`
: nothing}
</div>
`;
}
private _renderRow() {
const type = getAutomationActionType(this.action);
@@ -272,136 +288,200 @@ export default class HaAutomationActionRow extends LitElement {
)}
</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-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>
<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._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._runAction}>
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize("ui.panel.config.automation.editor.actions.run")
)}
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._renameAction}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)
)}
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateAction}
.disabled=${this.disabled}
>
<ha-svg-icon
slot="start"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
<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>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)
)}
</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._copyAction}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
),
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>`
)}
</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._cutAction}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
),
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>`
)}
</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._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>
`
: nothing}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!this._uiModeAvailable || !!this._warnings}
>
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)
)}
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
<ha-svg-icon
slot="start"
.path=${this.action.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.actions.${this.action.enabled === false ? "enable" : "disable"}`
)
)}
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
),
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>`
)}
</ha-md-menu-item>
</ha-md-button-menu>
<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"
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
@@ -516,7 +596,10 @@ export default class HaAutomationActionRow extends LitElement {
const enabled = !(this.action.enabled ?? true);
const value = { ...this.action, enabled };
fireEvent(this, "value-changed", { value });
this.openSidebar(value); // refresh sidebar
if (this._selected && this.optionsInSidebar) {
this.openSidebar(value); // refresh sidebar
}
if (this._yamlMode && !this.optionsInSidebar) {
this._actionEditor?.yamlEditor?.setValue(value);
@@ -680,7 +763,7 @@ export default class HaAutomationActionRow extends LitElement {
fireEvent(this, "move-down");
};
private _toggleYamlMode = () => {
private _toggleYamlMode = (item?: HTMLElement) => {
if (this._yamlMode) {
this._switchUiMode();
} else {
@@ -689,6 +772,8 @@ export default class HaAutomationActionRow extends LitElement {
if (!this.optionsInSidebar) {
this.expand();
} else if (item) {
this.openSidebar();
}
};
@@ -804,7 +889,7 @@ export default class HaAutomationActionRow extends LitElement {
this._automationRowElement?.focus();
}
static styles = rowStyles;
static styles = [rowStyles, overflowStyles];
}
declare global {

View File

@@ -111,8 +111,6 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
}
ha-fab {
position: fixed;
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
right: calc(16px + var(--safe-area-inset-right, 0px));
}
`,
];

View File

@@ -1,5 +1,6 @@
import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
mdiArrowDown,
mdiArrowUp,
mdiContentCopy,
@@ -15,7 +16,7 @@ import {
} from "@mdi/js";
import deepClone from "deep-clone-simple";
import { dump } from "js-yaml";
import type { CSSResultGroup, PropertyValues } from "lit";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -56,9 +57,10 @@ import {
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import { showToast } from "../../../../util/toast";
import "../ha-automation-editor-warning";
import { rowStyles } from "../styles";
import { overflowStyles, rowStyles } from "../styles";
import "./ha-automation-condition-editor";
import type HaAutomationConditionEditor from "./ha-automation-condition-editor";
import "./types/ha-automation-condition-and";
@@ -162,6 +164,20 @@ export default class HaAutomationConditionRow extends LitElement {
return this._selected;
}
private _renderOverflowLabel(label: string, shortcut?: TemplateResult) {
return html`
<div class="overflow-label">
${label}
${this.optionsInSidebar && !this.narrow
? shortcut ||
html`<span
class="shortcut-placeholder ${isMac ? "mac" : ""}"
></span>`
: nothing}
</div>
`;
}
private _renderRow() {
return html`
<ha-svg-icon
@@ -177,140 +193,198 @@ export default class HaAutomationConditionRow extends LitElement {
<slot name="icons" slot="icons"></slot>
${!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
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._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}>
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.conditions.test"
)
)}
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._renameCondition}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.conditions.rename"
)
)}
</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=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._duplicateCondition}
.disabled=${this.disabled}
>
<ha-svg-icon
slot="start"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)
)}
</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}
>
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon
>${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
),
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>`
)}
</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>
<ha-md-menu-item
.clickAction=${this._cutCondition}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon
>${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
),
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>`
)}
</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>
${!this.optionsInSidebar
? html`
<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>
`
: nothing}
<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}>
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)
)}
</ha-md-menu-item>
<ha-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}
>
<ha-svg-icon
slot="start"
.path=${this.condition.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
<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"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>`
: nothing}
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.actions.${this.condition.enabled === false ? "enable" : "disable"}`
)
)}
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
),
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>`
)}
</ha-md-menu-item>
</ha-md-button-menu>
${!this.optionsInSidebar
? html`${this._warnings
? html`<ha-automation-editor-warning
@@ -447,7 +521,10 @@ export default class HaAutomationConditionRow extends LitElement {
const enabled = !(this.condition.enabled ?? true);
const value = { ...this.condition, enabled };
fireEvent(this, "value-changed", { value });
this.openSidebar(value); // refresh sidebar
if (this._selected && this.optionsInSidebar) {
this.openSidebar(value); // refresh sidebar
}
if (this._yamlMode && !this.optionsInSidebar) {
this.conditionEditor?.yamlEditor?.setValue(value);
@@ -490,9 +567,9 @@ export default class HaAutomationConditionRow extends LitElement {
this._testing = true;
const condition = this.condition;
requestAnimationFrame(() => {
// @ts-ignore is supported in all browsers expect firefox
// @ts-ignore is supported in all browsers except firefox
if (this.scrollIntoViewIfNeeded) {
// @ts-ignore is supported in all browsers expect firefox
// @ts-ignore is supported in all browsers except firefox
this.scrollIntoViewIfNeeded();
return;
}
@@ -626,7 +703,7 @@ export default class HaAutomationConditionRow extends LitElement {
fireEvent(this, "move-down");
};
private _toggleYamlMode = () => {
private _toggleYamlMode = (item?: HTMLElement) => {
if (this._yamlMode) {
this._switchUiMode();
} else {
@@ -635,6 +712,8 @@ export default class HaAutomationConditionRow extends LitElement {
if (!this.optionsInSidebar) {
this.expand();
} else if (item) {
this.openSidebar();
}
};
@@ -742,6 +821,7 @@ export default class HaAutomationConditionRow extends LitElement {
static get styles(): CSSResultGroup {
return [
rowStyles,
overflowStyles,
css`
.testing {
position: absolute;

View File

@@ -11,11 +11,13 @@ import {
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiPlusCircleMultipleOutline,
mdiRedo,
mdiRenameBox,
mdiRobotConfused,
mdiStopCircleOutline,
mdiTag,
mdiTransitConnection,
mdiUndo,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
@@ -84,6 +86,7 @@ import {
import "./blueprint-automation-editor";
import "./manual-automation-editor";
import type { HaManualAutomationEditor } from "./manual-automation-editor";
import { UndoRedoMixin } from "../../../mixins/undo-redo-mixin";
declare global {
interface HTMLElementTagNameMap {
@@ -106,9 +109,12 @@ declare global {
}
}
export class HaAutomationEditor extends PreventUnsavedMixin(
KeyboardShortcutMixin(LitElement)
) {
const baseEditorMixins = PreventUnsavedMixin(KeyboardShortcutMixin(LitElement));
export class HaAutomationEditor extends UndoRedoMixin<
typeof baseEditorMixins,
AutomationConfig
>(baseEditorMixins) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public automationId: string | null = null;
@@ -218,6 +224,24 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.header=${this._config.alias ||
this.hass.localize("ui.panel.config.automation.editor.default_name")}
>
${this._mode === "gui" && !this.narrow
? html`<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.undo")}
.path=${mdiUndo}
@click=${this.undo}
.disabled=${!this.canUndo}
>
</ha-icon-button>
<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.redo")}
.path=${mdiRedo}
@click=${this.redo}
.disabled=${!this.canRedo}
>
</ha-icon-button>`
: nothing}
${this._config?.id && !this.narrow
? html`
<ha-button
@@ -239,6 +263,25 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.path=${mdiDotsVertical}
></ha-icon-button>
${this._mode === "gui" && this.narrow
? html`<ha-list-item
graphic="icon"
@click=${this.undo}
.disabled=${!this.canUndo}
>
${this.hass.localize("ui.common.undo")}
<ha-svg-icon slot="graphic" .path=${mdiUndo}></ha-svg-icon>
</ha-list-item>
<ha-list-item
graphic="icon"
@click=${this.redo}
.disabled=${!this.canRedo}
>
${this.hass.localize("ui.common.redo")}
<ha-svg-icon slot="graphic" .path=${mdiRedo}></ha-svg-icon>
</ha-list-item>`
: nothing}
<ha-list-item
graphic="icon"
.disabled=${!stateObj}
@@ -443,6 +486,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
@value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
@editor-save=${this._handleSaveAutomation}
@undo-paste=${this.undo}
>
<div class="alert-wrapper" slot="alerts">
${this._errors || stateObj?.state === UNAVAILABLE
@@ -549,7 +593,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
`
: nothing}
<ha-yaml-editor
copy-clipboard
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this._readOnly}
@@ -715,6 +758,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private _valueChanged(ev: CustomEvent<{ value: AutomationConfig }>) {
ev.stopPropagation();
if (this._config) {
this.pushToUndo(this._config);
}
this._config = ev.detail.value;
if (this._readOnly) {
return;
@@ -1123,6 +1170,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
x: () => this._cutSelectedRow(),
Delete: () => this._deleteSelectedRow(),
Backspace: () => this._deleteSelectedRow(),
z: () => this.undo(),
y: () => this.redo(),
};
}
@@ -1156,6 +1205,16 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this._manualEditor?.deleteSelectedRow();
}
protected get currentConfig() {
return this._config;
}
protected applyUndoRedo(config: AutomationConfig) {
this._manualEditor?.triggerCloseSidebar();
this._config = config;
this._dirty = true;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@@ -171,7 +171,7 @@ export default class HaAutomationSidebar extends LitElement {
@mousedown=${this._handleMouseDown}
@touchstart=${this._handleMouseDown}
>
${this._resizing ? html`<div class="indicator"></div>` : nothing}
<div class="indicator ${this._resizing ? "" : "hidden"}"></div>
</div>
${this._renderContent()}
`;
@@ -333,6 +333,15 @@ export default class HaAutomationSidebar extends LitElement {
height: 100%;
width: 4px;
border-radius: var(--ha-border-radius-pill);
transform: scale3d(1, 1, 1);
opacity: 1;
transition:
transform 180ms ease-in-out,
opacity 180ms ease-in-out;
}
.handle .indicator.hidden {
transform: scale3d(0, 1, 1);
opacity: 0;
}
`;
}

View File

@@ -117,8 +117,6 @@ export class HaManualAutomationEditor extends LitElement {
HaAutomationAction | HaAutomationCondition
>;
private _previousConfig?: ManualAutomationConfig;
private _prevSidebarWidthPx?: number;
public connectedCallback() {
@@ -177,7 +175,7 @@ export class HaManualAutomationEditor extends LitElement {
.disabled=${this.disabled || this.saving}
.narrow=${this.narrow}
@open-sidebar=${this._openSidebar}
@request-close-sidebar=${this._triggerCloseSidebar}
@request-close-sidebar=${this.triggerCloseSidebar}
@close-sidebar=${this._handleCloseSidebar}
root
sidebar
@@ -224,7 +222,7 @@ export class HaManualAutomationEditor extends LitElement {
.disabled=${this.disabled || this.saving}
.narrow=${this.narrow}
@open-sidebar=${this._openSidebar}
@request-close-sidebar=${this._triggerCloseSidebar}
@request-close-sidebar=${this.triggerCloseSidebar}
@close-sidebar=${this._handleCloseSidebar}
root
sidebar
@@ -266,7 +264,7 @@ export class HaManualAutomationEditor extends LitElement {
.highlightedActions=${this._pastedConfig?.actions}
@value-changed=${this._actionChanged}
@open-sidebar=${this._openSidebar}
@request-close-sidebar=${this._triggerCloseSidebar}
@request-close-sidebar=${this.triggerCloseSidebar}
@close-sidebar=${this._handleCloseSidebar}
.hass=${this.hass}
.narrow=${this.narrow}
@@ -370,7 +368,7 @@ export class HaManualAutomationEditor extends LitElement {
};
}
private _triggerCloseSidebar() {
public triggerCloseSidebar() {
if (this._sidebarConfig) {
if (this._sidebarElement) {
this._sidebarElement.triggerCloseSidebar();
@@ -412,7 +410,7 @@ export class HaManualAutomationEditor extends LitElement {
}
private _saveAutomation() {
this._triggerCloseSidebar();
this.triggerCloseSidebar();
fireEvent(this, "save-automation");
}
@@ -526,9 +524,7 @@ export class HaManualAutomationEditor extends LitElement {
["triggers", "conditions", "actions"].includes(keysPresent[0])
) {
// if only one type of element is pasted, insert under the currently active item
const previousConfig = { ...this.config };
if (this._tryInsertAfterSelected(normalized[keysPresent[0]])) {
this._previousConfig = previousConfig;
this._showPastedToastWithUndo();
return;
}
@@ -565,26 +561,27 @@ export class HaManualAutomationEditor extends LitElement {
};
private _appendToExistingConfig(config: ManualAutomationConfig) {
// make a copy otherwise we will reference the original config
this._previousConfig = { ...this.config } as ManualAutomationConfig;
this._pastedConfig = config;
// make a copy otherwise we will modify the original config
// which breaks the (referenced) config used for storing in undo stack
const workingCopy: ManualAutomationConfig = { ...this.config };
if (!this.config) {
if (!workingCopy) {
return;
}
if ("triggers" in config) {
this.config.triggers = ensureArray(this.config.triggers || []).concat(
workingCopy.triggers = ensureArray(workingCopy.triggers || []).concat(
ensureArray(config.triggers)
);
}
if ("conditions" in config) {
this.config.conditions = ensureArray(this.config.conditions || []).concat(
workingCopy.conditions = ensureArray(workingCopy.conditions || []).concat(
ensureArray(config.conditions)
);
}
if ("actions" in config) {
this.config.actions = ensureArray(this.config.actions || []).concat(
workingCopy.actions = ensureArray(workingCopy.actions || []).concat(
ensureArray(config.actions)
) as Action[];
}
@@ -593,22 +590,19 @@ export class HaManualAutomationEditor extends LitElement {
fireEvent(this, "value-changed", {
value: {
...this.config!,
...workingCopy!,
},
});
}
private _replaceExistingConfig(config: ManualAutomationConfig) {
// make a copy otherwise we will reference the original config
this._previousConfig = { ...this.config } as ManualAutomationConfig;
this._pastedConfig = config;
this.config = config;
this._showPastedToastWithUndo();
fireEvent(this, "value-changed", {
value: {
...this.config,
...config,
},
});
}
@@ -622,13 +616,8 @@ export class HaManualAutomationEditor extends LitElement {
action: {
text: this.hass.localize("ui.common.undo"),
action: () => {
fireEvent(this, "value-changed", {
value: {
...this._previousConfig!,
},
});
fireEvent(this, "undo-paste");
this._previousConfig = undefined;
this._pastedConfig = undefined;
},
},
@@ -636,12 +625,7 @@ export class HaManualAutomationEditor extends LitElement {
}
public resetPastedConfig() {
if (!this._previousConfig) {
return;
}
this._pastedConfig = undefined;
this._previousConfig = undefined;
showToast(this, {
message: "",
@@ -758,5 +742,6 @@ declare global {
"open-sidebar": SidebarConfig;
"request-close-sidebar": undefined;
"close-sidebar": undefined;
"undo-paste": undefined;
}
}

View File

@@ -1,5 +1,6 @@
import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
mdiArrowDown,
mdiArrowUp,
mdiDelete,
@@ -7,7 +8,7 @@ import {
mdiPlusCircleMultipleOutline,
mdiRenameBox,
} from "@mdi/js";
import type { CSSResultGroup } from "lit";
import type { CSSResultGroup, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -37,11 +38,17 @@ import {
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import "../action/ha-automation-action";
import type HaAutomationAction from "../action/ha-automation-action";
import "../condition/ha-automation-condition";
import type HaAutomationCondition from "../condition/ha-automation-condition";
import { editorStyles, indentStyle, rowStyles } from "../styles";
import {
editorStyles,
indentStyle,
overflowStyles,
rowStyles,
} from "../styles";
@customElement("ha-automation-option-row")
export default class HaAutomationOptionRow extends LitElement {
@@ -119,6 +126,20 @@ export default class HaAutomationOptionRow extends LitElement {
return str;
}
private _renderOverflowLabel(label: string, shortcut?: TemplateResult) {
return html`
<div class="overflow-label">
${label}
${this.optionsInSidebar && !this.narrow
? shortcut ||
html`<span
class="shortcut-placeholder ${isMac ? "mac" : ""}"
></span>`
: nothing}
</div>
`;
}
private _renderRow() {
return html`
<h3 slot="header">
@@ -134,7 +155,7 @@ export default class HaAutomationOptionRow extends LitElement {
<slot name="icons" slot="icons"></slot>
${this.option && !this.optionsInSidebar
${this.option
? html`
<ha-md-button-menu
quick
@@ -156,58 +177,92 @@ export default class HaAutomationOptionRow extends LitElement {
@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>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)
)}
</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="start"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)
)}
</ha-md-menu-item>
<ha-md-menu-item
@click=${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
@click=${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._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>
`
: 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>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
),
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>`
)}
</ha-md-menu-item>
</ha-md-button-menu>
`
@@ -309,25 +364,27 @@ export default class HaAutomationOptionRow extends LitElement {
}
private _removeOption = () => {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.delete_confirm_title"
),
text: this.hass.localize(
"ui.panel.config.automation.editor.actions.delete_confirm_text"
),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: () => {
fireEvent(this, "value-changed", {
value: null,
});
if (this._selected) {
fireEvent(this, "close-sidebar");
}
},
});
if (this.option) {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.delete_confirm_title"
),
text: this.hass.localize(
"ui.panel.config.automation.editor.actions.delete_confirm_text"
),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: () => {
fireEvent(this, "value-changed", {
value: null,
});
if (this._selected) {
fireEvent(this, "close-sidebar");
}
},
});
}
};
private _renameOption = async () => {
@@ -457,6 +514,7 @@ export default class HaAutomationOptionRow extends LitElement {
return [
rowStyles,
editorStyles,
overflowStyles,
indentStyle,
css`
li[role="separator"] {

View File

@@ -26,7 +26,7 @@ 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";
import { sidebarEditorStyles } from "../styles";
import { overflowStyles, sidebarEditorStyles } from "../styles";
import "../trigger/ha-automation-trigger-editor";
import "./ha-automation-sidebar-card";
@@ -66,9 +66,8 @@ export default class HaAutomationSidebarAction extends LitElement {
protected render() {
const actionConfig = this.config.config.action;
const disabled =
this.disabled ||
("enabled" in actionConfig && actionConfig.enabled === false);
const rowDisabled =
"enabled" in actionConfig && actionConfig.enabled === false;
const actionType = getAutomationActionType(actionConfig);
@@ -102,7 +101,13 @@ export default class HaAutomationSidebarAction extends LitElement {
.narrow=${this.narrow}
>
<span slot="title">${title}</span>
<span slot="subtitle">${subtitle}</span>
<span slot="subtitle"
>${subtitle}${rowDisabled
? html` (${this.hass.localize(
"ui.panel.config.automation.editor.actions.disabled"
)})`
: ""}</span
>
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.run}>
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
@@ -114,7 +119,7 @@ export default class HaAutomationSidebarAction extends LitElement {
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.rename}
.disabled=${!!disabled}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
<div class="overflow-label">
@@ -215,14 +220,18 @@ export default class HaAutomationSidebarAction extends LitElement {
role="separator"
tabindex="-1"
></ha-md-divider>
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.disable}>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.disable}
.disabled=${this.disabled}
>
<ha-svg-icon
slot="start"
.path=${this.disabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
.path=${rowDisabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
`ui.panel.config.automation.editor.actions.${rowDisabled ? "enable" : "disable"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
@@ -315,7 +324,7 @@ export default class HaAutomationSidebarAction extends LitElement {
fireEvent(this, "toggle-yaml-mode");
};
static styles = sidebarEditorStyles;
static styles = [sidebarEditorStyles, overflowStyles];
}
declare global {

View File

@@ -111,13 +111,9 @@ 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}
<div
class=${classMap({ fade: true, scrollable: this._contentScrollable })}
></div>
</ha-card>
`;
}
@@ -131,9 +127,13 @@ export default class HaAutomationSidebarCard extends LitElement {
}
private _canScrollDown(element: HTMLElement) {
const safeAreaInsetBottom =
parseFloat(
getComputedStyle(element).getPropertyValue("--safe-area-inset-bottom")
) || 0;
this._contentScrollable =
(element.scrollHeight ?? 0) - (element.clientHeight ?? 0) >
(element.scrollTop ?? 0);
(element.scrollTop ?? 0) + safeAreaInsetBottom + 16;
}
private _closeSidebar() {
@@ -147,11 +147,13 @@ export default class HaAutomationSidebarCard extends LitElement {
static styles = css`
ha-card {
position: relative;
height: 100%;
width: 100%;
border-color: var(--primary-color);
border-width: 2px;
display: block;
display: flex;
flex-direction: column;
}
@media all and (max-width: 870px) {
@@ -183,30 +185,43 @@ export default class HaAutomationSidebarCard extends LitElement {
}
.fade {
position: fixed;
bottom: -12px;
left: 0;
right: 0;
height: 12px;
position: absolute;
bottom: 1px;
left: 1px;
right: 1px;
height: 16px;
pointer-events: none;
transition: box-shadow 180ms ease-in-out;
background-color: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
transform: rotate(180deg);
border-radius: var(--ha-card-border-radius);
border-bottom-left-radius: var(--ha-border-radius-square);
border-bottom-right-radius: var(--ha-border-radius-square);
}
.fade.scrollable {
box-shadow: var(--bar-box-shadow);
transform: rotate(180deg);
}
.card-content {
max-height: calc(100% - 80px);
flex: 1 1 auto;
min-height: 0;
overflow: auto;
margin-top: 0;
padding-bottom: max(var(--safe-area-inset-bottom, 0px), 32px);
}
@media (min-width: 450px) and (min-height: 500px) {
@media all and (max-width: 870px) {
.fade {
bottom: 0;
border-radius: var(--ha-border-radius-square);
}
.card-content {
max-height: calc(100% - 104px);
overflow: auto;
padding-bottom: 42px;
}
}
`;

View File

@@ -27,7 +27,7 @@ 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";
import { overflowStyles, sidebarEditorStyles } from "../styles";
import "./ha-automation-sidebar-card";
@customElement("ha-automation-sidebar-condition")
@@ -72,9 +72,8 @@ export default class HaAutomationSidebarCondition extends LitElement {
}
protected render() {
const disabled =
this.disabled ||
("enabled" in this.config.config && this.config.config.enabled === false);
const rowDisabled =
"enabled" in this.config.config && this.config.config.enabled === false;
const type = this.config.config.condition;
@@ -103,7 +102,11 @@ export default class HaAutomationSidebarCondition extends LitElement {
.narrow=${this.narrow}
>
<span slot="title">${title}</span>
<span slot="subtitle">${subtitle}</span>
<span slot="subtitle"
>${subtitle}${rowDisabled
? ` (${this.hass.localize("ui.panel.config.automation.editor.actions.disabled")})`
: ""}</span
>
<ha-md-menu-item slot="menu-items" .clickAction=${this._testCondition}>
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
<div class="overflow-label">
@@ -116,7 +119,7 @@ export default class HaAutomationSidebarCondition extends LitElement {
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.rename}
.disabled=${!!disabled}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
<div class="overflow-label">
@@ -221,14 +224,18 @@ export default class HaAutomationSidebarCondition extends LitElement {
role="separator"
tabindex="-1"
></ha-md-divider>
<ha-md-menu-item slot="menu-items" .clickAction=${this.config.disable}>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.disable}
.disabled=${this.disabled}
>
<ha-svg-icon
slot="start"
.path=${this.disabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
.path=${rowDisabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
`ui.panel.config.automation.editor.actions.${rowDisabled ? "enable" : "disable"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
@@ -357,6 +364,7 @@ export default class HaAutomationSidebarCondition extends LitElement {
} finally {
setTimeout(() => {
this._testing = false;
this._testingResult = undefined;
}, 2500);
}
};
@@ -395,6 +403,7 @@ export default class HaAutomationSidebarCondition extends LitElement {
static styles = [
sidebarEditorStyles,
overflowStyles,
css`
ha-automation-sidebar-card {
position: relative;

View File

@@ -10,7 +10,7 @@ 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 { overflowStyles, sidebarEditorStyles } from "../styles";
import "./ha-automation-sidebar-card";
@customElement("ha-automation-sidebar-option")
@@ -127,7 +127,7 @@ export default class HaAutomationSidebarOption extends LitElement {
</ha-automation-sidebar-card>`;
}
static styles = sidebarEditorStyles;
static styles = [sidebarEditorStyles, overflowStyles];
}
declare global {

View File

@@ -8,7 +8,7 @@ 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";
import { overflowStyles, sidebarEditorStyles } from "../styles";
import "./ha-automation-sidebar-card";
@customElement("ha-automation-sidebar-script-field")
@@ -153,7 +153,7 @@ export default class HaAutomationSidebarScriptField extends LitElement {
fireEvent(this, "toggle-yaml-mode");
};
static styles = sidebarEditorStyles;
static styles = [sidebarEditorStyles, overflowStyles];
}
declare global {

View File

@@ -3,7 +3,6 @@ import {
mdiContentCopy,
mdiContentCut,
mdiDelete,
mdiIdentifier,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiPlusCircleMultipleOutline,
@@ -19,7 +18,7 @@ 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 { overflowStyles, sidebarEditorStyles } from "../styles";
import "../trigger/ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "../trigger/ha-automation-trigger-editor";
import "./ha-automation-sidebar-card";
@@ -40,8 +39,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
@property({ attribute: "sidebar-key" }) public sidebarKey?: string;
@state() private _requestShowId = false;
@state() private _warnings?: string[];
@query(".sidebar-editor")
@@ -49,7 +46,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
protected willUpdate(changedProperties) {
if (changedProperties.has("config")) {
this._requestShowId = false;
this._warnings = undefined;
if (this.config) {
this.yamlMode = this.config.yamlMode;
@@ -61,7 +57,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
}
protected render() {
const disabled =
const rowDisabled =
this.disabled ||
("enabled" in this.config.config && this.config.config.enabled === false);
const type = isTriggerList(this.config.config)
@@ -85,11 +81,15 @@ export default class HaAutomationSidebarTrigger extends LitElement {
.narrow=${this.narrow}
>
<span slot="title">${title}</span>
<span slot="subtitle">${subtitle}</span>
<span slot="subtitle"
>${subtitle}${rowDisabled
? ` (${this.hass.localize("ui.panel.config.automation.editor.actions.disabled")})`
: ""}</span
>
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.rename}
.disabled=${disabled || type === "list"}
.disabled=${this.disabled || type === "list"}
>
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
<div class="overflow-label">
@@ -99,23 +99,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>
${!this.yamlMode &&
!("id" in this.config.config) &&
!this._requestShowId
? html`<ha-md-menu-item
slot="menu-items"
.clickAction=${this._showTriggerId}
.disabled=${disabled || type === "list"}
>
<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}
<ha-md-divider
slot="menu-items"
@@ -211,15 +194,15 @@ export default class HaAutomationSidebarTrigger extends LitElement {
<ha-md-menu-item
slot="menu-items"
.clickAction=${this.config.disable}
.disabled=${type === "list"}
.disabled=${this.disabled || type === "list"}
>
<ha-svg-icon
slot="start"
.path=${this.disabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
.path=${rowDisabled ? mdiPlayCircleOutline : mdiStopCircleOutline}
></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.actions.${disabled ? "enable" : "disable"}`
`ui.panel.config.automation.editor.actions.${rowDisabled ? "enable" : "disable"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
@@ -266,7 +249,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
@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}
@@ -309,11 +291,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
fireEvent(this, "toggle-yaml-mode");
};
private _showTriggerId = () => {
this._requestShowId = true;
};
static styles = sidebarEditorStyles;
static styles = [sidebarEditorStyles, overflowStyles];
}
declare global {

View File

@@ -238,6 +238,9 @@ export const sidebarEditorStyles = css`
.description {
padding-top: 16px;
}
`;
export const overflowStyles = css`
.overflow-label {
display: flex;
justify-content: space-between;

View File

@@ -27,8 +27,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
false;
@property({ type: Boolean, attribute: "show-id" }) public showId = false;
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
@@ -37,7 +35,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
const type = isTriggerList(this.trigger) ? "list" : this.trigger.trigger;
const yamlMode = this.yamlMode || !this.uiSupported;
const showId = "id" in this.trigger || this.showId;
return html`
<div
@@ -73,19 +70,23 @@ export default class HaAutomationTriggerEditor extends LitElement {
></ha-yaml-editor>
`
: html`
${showId && !isTriggerList(this.trigger)
${!isTriggerList(this.trigger)
? html`
<ha-textfield
.label=${this.hass.localize(
.label=${`${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id"
)}
)} (${this.hass.localize(
"ui.panel.config.automation.editor.triggers.optional"
)})`}
.value=${this.trigger.id || ""}
.disabled=${this.disabled}
@change=${this._idChanged}
>
</ha-textfield>
.helper=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id_helper"
)}
></ha-textfield>
`
: ""}
: nothing}
<div @value-changed=${this._onUiChanged}>
${dynamicElement(`ha-automation-trigger-${type}`, {
hass: this.hass,
@@ -144,9 +145,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
pointer-events: none;
}
.card-content {
padding: 16px;
}
.card-content.yaml {
padding: 0 1px;
border-top: 1px solid var(--divider-color);

View File

@@ -1,12 +1,13 @@
import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
mdiArrowDown,
mdiArrowUp,
mdiContentCopy,
mdiContentCut,
mdiDelete,
mdiDotsVertical,
mdiIdentifier,
mdiInformation,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiPlusCircleMultipleOutline,
@@ -15,7 +16,7 @@ import {
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { dump } from "js-yaml";
import type { CSSResultGroup, PropertyValues } from "lit";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -38,6 +39,7 @@ import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-svg-icon";
import type {
AutomationClipboard,
Trigger,
@@ -55,9 +57,10 @@ import {
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import { showToast } from "../../../../util/toast";
import "../ha-automation-editor-warning";
import { rowStyles } from "../styles";
import { overflowStyles, rowStyles } from "../styles";
import "./ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "./ha-automation-trigger-editor";
import "./types/ha-automation-trigger-calendar";
@@ -133,8 +136,6 @@ export default class HaAutomationTriggerRow extends LitElement {
@state() private _selected = false;
@state() private _requestShowId = false;
@state() private _warnings?: string[];
@property({ type: Boolean }) public narrow = false;
@@ -163,6 +164,20 @@ export default class HaAutomationTriggerRow extends LitElement {
private _triggerUnsub?: Promise<UnsubscribeFunc>;
private _renderOverflowLabel(label: string, shortcut?: TemplateResult) {
return html`
<div class="overflow-label">
${label}
${this.optionsInSidebar && !this.narrow
? shortcut ||
html`<span
class="shortcut-placeholder ${isMac ? "mac" : ""}"
></span>`
: nothing}
</div>
`;
}
private _renderRow() {
const type = this._getType(this.trigger);
@@ -182,142 +197,192 @@ export default class HaAutomationTriggerRow extends LitElement {
<slot name="icons" slot="icons"></slot>
${!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>
<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"}
>
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)
)}
</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}
>
<ha-svg-icon
slot="start"
.path=${mdiPlusCircleMultipleOutline}
></ha-svg-icon>
<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>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)
)}
</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}
>
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
),
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>`
)}
</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}
>
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
),
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>`
)}
</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>
${!this.optionsInSidebar
? html`
<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>
`
: nothing}
<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}
>
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
)
)}
</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>
<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"}
>
<ha-svg-icon
slot="start"
.path=${"enabled" in this.trigger && this.trigger.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
<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"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>`
: nothing}
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.actions.${"enabled" in this.trigger && this.trigger.enabled === false ? "enable" : "disable"}`
)
)}
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._onDelete}
class="warning"
.disabled=${this.disabled}
>
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
),
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>`
)}
</ha-md-menu-item>
</ha-md-button-menu>
${!this.optionsInSidebar
? html`${this._warnings
? html`<ha-automation-editor-warning
@@ -331,7 +396,6 @@ export default class HaAutomationTriggerRow extends LitElement {
.trigger=${this.trigger}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
.showId=${this._requestShowId}
.uiSupported=${supported}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-trigger-editor>`
@@ -380,6 +444,7 @@ export default class HaAutomationTriggerRow extends LitElement {
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.triggered"
)}
<ha-svg-icon .path=${mdiInformation}></ha-svg-icon>
</div>
</ha-card>
`;
@@ -563,7 +628,10 @@ export default class HaAutomationTriggerRow extends LitElement {
const enabled = !(this.trigger.enabled ?? true);
const value = { ...this.trigger, enabled };
fireEvent(this, "value-changed", { value });
this.openSidebar(value); // refresh sidebar
if (this._selected && this.optionsInSidebar) {
this.openSidebar(value); // refresh sidebar
}
if (this._yamlMode && !this.optionsInSidebar) {
this.triggerEditor?.yamlEditor?.setValue(value);
@@ -630,14 +698,6 @@ export default class HaAutomationTriggerRow extends LitElement {
}
};
private _showTriggerId = () => {
this._requestShowId = true;
if (!this.optionsInSidebar) {
this.expand();
}
};
private _duplicateTrigger = () => {
fireEvent(this, "duplicate");
};
@@ -682,7 +742,7 @@ export default class HaAutomationTriggerRow extends LitElement {
fireEvent(this, "move-down");
};
private _toggleYamlMode = () => {
private _toggleYamlMode = (item?: HTMLElement) => {
if (this._yamlMode) {
this._switchUiMode();
} else {
@@ -691,6 +751,8 @@ export default class HaAutomationTriggerRow extends LitElement {
if (!this.optionsInSidebar) {
this.expand();
} else if (item) {
this.openSidebar();
}
};
@@ -716,6 +778,7 @@ export default class HaAutomationTriggerRow extends LitElement {
static get styles(): CSSResultGroup {
return [
rowStyles,
overflowStyles,
css`
.triggered {
cursor: pointer;
@@ -740,9 +803,20 @@ export default class HaAutomationTriggerRow extends LitElement {
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
line-height: 1;
padding: 0;
}
.triggered ha-svg-icon {
--mdc-icon-size: 16px;
}
.triggered.active {
max-height: 100px;
padding: 4px;
}
.triggered:hover {
opacity: 0.8;

View File

@@ -1,4 +1,5 @@
import "../../../layouts/hass-error-screen";
import { mdiDownload } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -26,6 +27,7 @@ import "./components/ha-energy-solar-settings";
import "./components/ha-energy-battery-settings";
import "./components/ha-energy-gas-settings";
import "./components/ha-energy-water-settings";
import { fileDownload } from "../../../util/file_download";
const INITIAL_CONFIG: EnergyPreferences = {
energy_sources: [],
@@ -85,6 +87,14 @@ class HaConfigEnergy extends LitElement {
: "/config/lovelace/dashboards"}
.header=${this.hass.localize("ui.panel.config.energy.caption")}
>
<ha-icon-button
slot="toolbar-icon"
.path=${mdiDownload}
.label=${this.hass.localize(
"ui.panel.config.devices.download_diagnostics"
)}
@click=${this._downloadDiagnostics}
></ha-icon-button>
<ha-alert>
${this.hass.localize("ui.panel.config.energy.new_device_info")}
</ha-alert>
@@ -185,6 +195,32 @@ class HaConfigEnergy extends LitElement {
this._statsMetadata = statsMetadata;
}
private async _downloadDiagnostics() {
const data = {
version: this.hass.config.version,
info: this._info,
preferences: this._preferences,
metadata: this._statsMetadata,
entities: Object.fromEntries(
Object.keys(this._statsMetadata || {}).map((key) => [
key,
this.hass.entities[key],
])
),
states: Object.fromEntries(
Object.keys(this._statsMetadata || {}).map((key) => [
key,
this.hass.states[key],
])
),
};
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
fileDownload(url, "energy_diagnostics.json");
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@@ -255,10 +255,10 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
@click=${this._showRestartDialog}
></ha-icon-button>
`
: ""}
: nothing}
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
: nothing}
<div class="content">
${boardName || isComponentLoaded(this.hass, "hassio")
? html`
@@ -271,7 +271,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>`
: ""}
: nothing}
<div class="board-info">
<p class="primary-text">
${boardName ||
@@ -281,7 +281,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
</p>
${boardId
? html`<p class="secondary-text">${boardId}</p>`
: ""}
: nothing}
</div>
</div>
${documentationURL
@@ -305,7 +305,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>
`
: ""}
: nothing}
${boardConfigEntries.length ||
isComponentLoaded(this.hass, "hassio")
? html`<div class="card-actions">
@@ -335,10 +335,10 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
`
: nothing}
</div>`
: ""}
: nothing}
</ha-card>
`
: ""}
: nothing}
${dongles?.length
? html`<ha-card outlined>
${dongles.map((dongle) => {
@@ -358,11 +358,11 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
"ui.panel.config.hardware.configure"
)}
</ha-button>`
: ""}
: nothing}
</div>`;
})}
</ha-card>`
: ""}
: nothing}
${this._systemStatusData
? html`<ha-card outlined>
<div class="header">
@@ -412,14 +412,15 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
: isComponentLoaded(this.hass, "hardware")
? html`<ha-card outlined>
<div class="card-content">
<div class="value">
<ha-alert alert-type="info">
<ha-spinner slot="icon"></ha-spinner>
${this.hass.localize(
"ui.panel.config.hardware.loading_system_data"
)}
</div>
</ha-alert>
</div>
</ha-card>`
: ""}
: nothing}
</div>
</hass-subpage>
`;
@@ -541,6 +542,14 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
display: flex;
justify-content: space-between;
}
ha-alert {
--ha-alert-icon-size: 24px;
}
ha-alert ha-spinner {
--ha-spinner-size: 24px;
}
`,
];
}

View File

@@ -17,11 +17,11 @@ import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { extractSearchParam } from "../../../common/url/search-params";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-check-list-item";
import "../../../components/ha-checkbox";
import "../../../components/ha-fab";
import "../../../components/ha-button";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "../../../components/search-input";
@@ -478,10 +478,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
</search-input-outlined>
<div class="filters">
${!this._showDisabled && disabledConfigEntries.length
? html`<div
class="active-filters"
@click=${this._preventDefault}
>
? html`<div class="active-filters">
${this.hass.localize(
"ui.panel.config.integrations.disable.disabled_integrations",
{ number: disabledConfigEntries.length }
@@ -496,7 +493,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
)}
</ha-button>
</div>`
: ""}
: nothing}
${filterMenu}
</div>
</div>
@@ -668,10 +665,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
`;
}
private _preventDefault(ev) {
ev.preventDefault();
}
private async _scanUSBDevices() {
if (!isComponentLoaded(this.hass, "usb")) {
return;
@@ -1036,13 +1029,10 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
}
.active-filters {
color: var(--primary-text-color);
position: relative;
display: flex;
align-items: center;
padding-top: 2px;
padding-bottom: 2px;
padding-right: 2px;
padding-left: 8px;
padding: 2px 2px 2px 8px;
line-height: 1;
padding-inline-start: 8px;
padding-inline-end: 2px;
font-size: var(--ha-font-size-m);
@@ -1050,6 +1040,8 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
cursor: initial;
direction: var(--direction);
height: 32px;
background-color: var(--ha-color-fill-primary-normal-resting);
border-radius: var(--ha-border-radius-sm);
}
.active-filters ha-button {
margin-left: 8px;
@@ -1057,17 +1049,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
margin-inline-end: initial;
direction: var(--direction);
}
.active-filters::before {
background-color: var(--primary-color);
opacity: 0.12;
border-radius: 4px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
content: "";
}
.badge {
min-width: 20px;
min-height: 20px;

View File

@@ -221,12 +221,8 @@ class DialogMatterAddDevice extends LitElement {
--horizontal-padding: 16px;
}
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;
}
}
.loading {

View File

@@ -1111,12 +1111,8 @@ class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
}
@media all and (max-width: 500px), 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;
--mdc-dialog-min-height: 100%;
--mdc-dialog-max-height: 100%;
--vertical-align-dialog: flex-end;

View File

@@ -3,6 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { atLeastVersion } from "../../../common/config/version";
import { fireEvent } from "../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/buttons/ha-call-service-button";
@@ -14,6 +15,7 @@ import "../../../components/ha-list-item";
import "../../../components/ha-spinner";
import { getSignedPath } from "../../../data/auth";
import { getErrorLogDownloadUrl } from "../../../data/error_log";
import { coreLatestLogsUrl } from "../../../data/hassio/supervisor";
import { domainToName } from "../../../data/integration";
import type { LoggedError } from "../../../data/system_log";
import {
@@ -23,7 +25,6 @@ import {
} from "../../../data/system_log";
import type { HomeAssistant } from "../../../types";
import { fileDownload } from "../../../util/file_download";
import { showDownloadLogsDialog } from "./show-dialog-download-logs";
import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail";
import { formatSystemLogTime } from "./util";
@@ -229,20 +230,12 @@ export class SystemLogCard extends LitElement {
}
private async _downloadLogs() {
// download logs via supervisor
if (isComponentLoaded(this.hass, "hassio")) {
showDownloadLogsDialog(this, {
header: this.header,
provider: "core",
defaultLineCount: 100,
boot: 0,
});
return;
}
// download logs from core
const timeString = new Date().toISOString().replace(/:/g, "-");
const downloadUrl = getErrorLogDownloadUrl;
const downloadUrl =
isComponentLoaded(this.hass, "hassio") &&
atLeastVersion(this.hass.config.version, 2025, 10)
? coreLatestLogsUrl
: getErrorLogDownloadUrl;
const logFileName = `home-assistant_${timeString}.log`;
const signedUrl = await getSignedPath(this.hass, downloadUrl);
fileDownload(signedUrl.path, logFileName);

View File

@@ -260,7 +260,7 @@ class DialogPersonDetail extends LitElement implements HassDialog {
>
${this._params.entry
? this.hass!.localize("ui.common.save")
: this.hass!.localize("ui.panel.config.person.detail.create")}
: this.hass!.localize("ui.common.add")}
</ha-button>
</ha-dialog>
`;

View File

@@ -66,8 +66,6 @@ export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {
}
ha-fab {
position: fixed;
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
right: calc(16px + var(--safe-area-inset-right, 0px));
}
`,
];

View File

@@ -11,10 +11,12 @@ import {
mdiPlay,
mdiPlaylistEdit,
mdiPlusCircleMultipleOutline,
mdiRedo,
mdiRenameBox,
mdiRobotConfused,
mdiTag,
mdiTransitConnection,
mdiUndo,
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
@@ -72,10 +74,16 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
import "./blueprint-script-editor";
import "./manual-script-editor";
import type { HaManualScriptEditor } from "./manual-script-editor";
import { UndoRedoMixin } from "../../../mixins/undo-redo-mixin";
export class HaScriptEditor extends SubscribeMixin(
const baseEditorMixins = SubscribeMixin(
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
) {
);
export class HaScriptEditor extends UndoRedoMixin<
typeof baseEditorMixins,
ScriptConfig
>(baseEditorMixins) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public scriptId: string | null = null;
@@ -169,6 +177,24 @@ export class HaScriptEditor extends SubscribeMixin(
.header=${this._config.alias ||
this.hass.localize("ui.panel.config.script.editor.default_name")}
>
${this._mode === "gui" && !this.narrow
? html`<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.undo")}
.path=${mdiUndo}
@click=${this.undo}
.disabled=${!this.canUndo}
>
</ha-icon-button>
<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.redo")}
.path=${mdiRedo}
@click=${this.redo}
.disabled=${!this.canRedo}
>
</ha-icon-button>`
: nothing}
${this.scriptId && !this.narrow
? html`
<ha-button
@@ -189,6 +215,25 @@ export class HaScriptEditor extends SubscribeMixin(
.path=${mdiDotsVertical}
></ha-icon-button>
${this._mode === "gui" && this.narrow
? html`<ha-list-item
graphic="icon"
@click=${this.undo}
.disabled=${!this.canUndo}
>
${this.hass.localize("ui.common.undo")}
<ha-svg-icon slot="graphic" .path=${mdiUndo}></ha-svg-icon>
</ha-list-item>
<ha-list-item
graphic="icon"
@click=${this.redo}
.disabled=${!this.canRedo}
>
${this.hass.localize("ui.common.redo")}
<ha-svg-icon slot="graphic" .path=${mdiRedo}></ha-svg-icon>
</ha-list-item>`
: nothing}
<ha-list-item
graphic="icon"
.disabled=${!this.scriptId}
@@ -387,6 +432,7 @@ export class HaScriptEditor extends SubscribeMixin(
@value-changed=${this._valueChanged}
@editor-save=${this._handleSaveScript}
@save-script=${this._handleSaveScript}
@undo-paste=${this.undo}
>
<div class="alert-wrapper" slot="alerts">
${this._errors || stateObj?.state === UNAVAILABLE
@@ -454,7 +500,6 @@ export class HaScriptEditor extends SubscribeMixin(
`
: this._mode === "yaml"
? html`<ha-yaml-editor
copy-clipboard
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this._readOnly}
@@ -602,6 +647,10 @@ export class HaScriptEditor extends SubscribeMixin(
}
private _valueChanged(ev) {
if (this._config) {
this.pushToUndo(this._config);
}
this._config = ev.detail.value;
this._errors = undefined;
this._dirty = true;
@@ -694,6 +743,11 @@ export class HaScriptEditor extends SubscribeMixin(
if ("fields" in this._config!) {
return;
}
if (this._config) {
this.pushToUndo(this._config);
}
this._manualEditor?.addFields();
this._dirty = true;
}
@@ -1025,6 +1079,8 @@ export class HaScriptEditor extends SubscribeMixin(
x: () => this._cutSelectedRow(),
Delete: () => this._deleteSelectedRow(),
Backspace: () => this._deleteSelectedRow(),
z: () => this.undo(),
y: () => this.redo(),
};
}
@@ -1058,6 +1114,16 @@ export class HaScriptEditor extends SubscribeMixin(
this._manualEditor?.deleteSelectedRow();
}
protected get currentConfig() {
return this._config;
}
protected applyUndoRedo(config: ScriptConfig) {
this._manualEditor?.triggerCloseSidebar();
this._config = config;
this._dirty = true;
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@@ -1,19 +1,30 @@
import {
mdiAppleKeyboardCommand,
mdiDelete,
mdiDotsVertical,
mdiPlaylistEdit,
} from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event";
import { preventDefaultStopPropagation } from "../../../common/dom/prevent_default_stop_propagation";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import type { LocalizeKeys } from "../../../common/translations/localize";
import "../../../components/ha-automation-row";
import type { HaAutomationRow } from "../../../components/ha-automation-row";
import "../../../components/ha-card";
import "../../../components/ha-md-button-menu";
import "../../../components/ha-md-menu-item";
import type { ScriptFieldSidebarConfig } from "../../../data/automation";
import type { Field } from "../../../data/script";
import { SELECTOR_SELECTOR_BUILDING_BLOCKS } from "../../../data/selector/selector_selector";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { indentStyle } from "../automation/styles";
import { isMac } from "../../../util/is_mac";
import { indentStyle, overflowStyles } from "../automation/styles";
import "./ha-script-field-selector-editor";
import type HaScriptFieldSelectorEditor from "./ha-script-field-selector-editor";
@@ -66,6 +77,64 @@ export default class HaScriptFieldRow extends LitElement {
.highlight=${this.highlight}
@delete-row=${this._onDelete}
>
<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._toggleYamlMode}>
<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
.clickAction=${this._onDelete}
.disabled=${this.disabled}
class="warning"
>
<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-md-button-menu>
<h3 slot="header">${this.key}</h3>
<slot name="icons" slot="icons"></slot>
@@ -97,6 +166,71 @@ export default class HaScriptFieldRow extends LitElement {
"ui.panel.config.script.editor.field.selector"
)}
</h3>
<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._toggleYamlMode}
selector-row
>
<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
.clickAction=${this._onDelete}
.disabled=${this.disabled}
class="warning"
>
<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-md-button-menu>
</ha-automation-row>
</ha-card>
${typeof this.field.selector === "object" &&
@@ -243,8 +377,12 @@ export default class HaScriptFieldRow extends LitElement {
}
}
private _toggleYamlMode = () => {
private _toggleYamlMode = (item?: HTMLElement) => {
this._yamlMode = !this._yamlMode;
if (item) {
this.openSidebar(item.hasAttribute("selector-row"));
}
};
private _onDelete = () => {
@@ -279,6 +417,7 @@ export default class HaScriptFieldRow extends LitElement {
return [
haStyle,
indentStyle,
overflowStyles,
css`
.disabled {
opacity: 0.5;

View File

@@ -35,7 +35,12 @@ import type {
ActionSidebarConfig,
SidebarConfig,
} from "../../../data/automation";
import type { Action, Fields, ScriptConfig } from "../../../data/script";
import type {
Action,
Fields,
ManualScriptConfig,
ScriptConfig,
} from "../../../data/script";
import {
getActionType,
MODES,
@@ -103,8 +108,6 @@ export class HaManualScriptEditor extends LitElement {
HaAutomationAction | HaScriptFields
>;
private _previousConfig?: ScriptConfig;
private _openFields = false;
private _prevSidebarWidthPx?: number;
@@ -181,7 +184,7 @@ export class HaManualScriptEditor extends LitElement {
.disabled=${this.disabled}
.narrow=${this.narrow}
@open-sidebar=${this._openSidebar}
@request-close-sidebar=${this._triggerCloseSidebar}
@request-close-sidebar=${this.triggerCloseSidebar}
@close-sidebar=${this._handleCloseSidebar}
></ha-script-fields>`
: nothing
@@ -212,7 +215,7 @@ export class HaManualScriptEditor extends LitElement {
.highlightedActions=${this._pastedConfig?.sequence}
@value-changed=${this._sequenceChanged}
@open-sidebar=${this._openSidebar}
@request-close-sidebar=${this._triggerCloseSidebar}
@request-close-sidebar=${this.triggerCloseSidebar}
@close-sidebar=${this._handleCloseSidebar}
.hass=${this.hass}
.narrow=${this.narrow}
@@ -402,9 +405,7 @@ export class HaManualScriptEditor extends LitElement {
if (keysPresent.length === 1 && ["sequence"].includes(keysPresent[0])) {
// if only one type of element is pasted, insert under the currently active item
const previousConfig = { ...this.config };
if (this._tryInsertAfterSelected(normalized[keysPresent[0]])) {
this._previousConfig = previousConfig;
this._showPastedToastWithUndo();
return;
}
@@ -439,22 +440,23 @@ export class HaManualScriptEditor extends LitElement {
};
private _appendToExistingConfig(config: ScriptConfig) {
// make a copy otherwise we will reference the original config
this._previousConfig = { ...this.config } as ScriptConfig;
this._pastedConfig = config;
// make a copy otherwise we will modify the original config
// which breaks the (referenced) config used for storing in undo stack
const workingCopy: ManualScriptConfig = { ...this.config };
if (!this.config) {
if (!workingCopy) {
return;
}
if ("fields" in config) {
this.config.fields = {
...this.config.fields,
workingCopy.fields = {
...workingCopy.fields,
...config.fields,
};
}
if ("sequence" in config) {
this.config.sequence = ensureArray(this.config.sequence || []).concat(
workingCopy.sequence = ensureArray(workingCopy.sequence || []).concat(
ensureArray(config.sequence)
) as Action[];
}
@@ -463,22 +465,19 @@ export class HaManualScriptEditor extends LitElement {
fireEvent(this, "value-changed", {
value: {
...this.config,
...workingCopy,
},
});
}
private _replaceExistingConfig(config: ScriptConfig) {
// make a copy otherwise we will reference the original config
this._previousConfig = { ...this.config } as ScriptConfig;
this._pastedConfig = config;
this.config = config;
this._showPastedToastWithUndo();
fireEvent(this, "value-changed", {
value: {
...this.config,
...config,
},
});
}
@@ -492,13 +491,8 @@ export class HaManualScriptEditor extends LitElement {
action: {
text: this.hass.localize("ui.common.undo"),
action: () => {
fireEvent(this, "value-changed", {
value: {
...this._previousConfig!,
},
});
fireEvent(this, "undo-paste");
this._previousConfig = undefined;
this._pastedConfig = undefined;
},
},
@@ -506,12 +500,7 @@ export class HaManualScriptEditor extends LitElement {
}
public resetPastedConfig() {
if (!this._previousConfig) {
return;
}
this._pastedConfig = undefined;
this._previousConfig = undefined;
showToast(this, {
message: "",
@@ -541,7 +530,7 @@ export class HaManualScriptEditor extends LitElement {
};
}
private _triggerCloseSidebar() {
public triggerCloseSidebar() {
if (this._sidebarConfig) {
if (this._sidebarElement) {
this._sidebarElement.triggerCloseSidebar();
@@ -556,7 +545,7 @@ export class HaManualScriptEditor extends LitElement {
}
private _saveScript() {
this._triggerCloseSidebar();
this.triggerCloseSidebar();
fireEvent(this, "save-script");
}

View File

@@ -520,6 +520,10 @@ class HaConfigSectionStorage extends LitElement {
width: 24px;
}
ha-alert {
--ha-alert-icon-size: 24px;
}
ha-alert ha-spinner {
--ha-spinner-size: 24px;
}

View File

@@ -51,7 +51,7 @@ class HaPanelDevAction extends LitElement {
@state() private _response?: {
domain: string;
service: string;
result: Record<string, any>;
result: Record<string, any> | null;
media?: Promise<TemplateResult | typeof nothing>;
};
@@ -205,7 +205,7 @@ class HaPanelDevAction extends LitElement {
</ha-progress-button>
</div>
</div>
${this._response
${this._response?.result
? html`<div class="content response">
<ha-card
.header=${this.hass.localize(
@@ -215,7 +215,6 @@ class HaPanelDevAction extends LitElement {
<div class="card-content">
<ha-yaml-editor
.hass=${this.hass}
copy-clipboard
read-only
auto-update
has-extra-actions
@@ -492,7 +491,7 @@ class HaPanelDevAction extends LitElement {
service,
result,
media:
"media_source_id" in result
result && "media_source_id" in result
? resolveMediaSource(this.hass, result.media_source_id).then(
(resolved) =>
resolved.mime_type.startsWith("image/")

View File

@@ -802,12 +802,8 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
}
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;
--mdc-dialog-min-height: 100%;
--mdc-dialog-max-height: 100%;
--vertical-align-dialog: flex-end;

View File

@@ -199,6 +199,7 @@ class HaPanelHistory extends LitElement {
.startTime=${this._startDate}
.endTime=${this._endDate}
.narrow=${this.narrow}
sync-charts
>
</state-history-charts>
`}

View File

@@ -161,7 +161,7 @@ export class HuiBadge extends ReactiveElement {
);
}
private _updateVisibility(forceVisible?: boolean) {
private _updateVisibility(ignoreConditions?: boolean) {
if (!this._element || !this.hass) {
return;
}
@@ -171,9 +171,18 @@ export class HuiBadge extends ReactiveElement {
return;
}
if (this.preview) {
this._setElementVisibility(true);
return;
}
if (this.config?.disabled) {
this._setElementVisibility(false);
return;
}
const visible =
forceVisible ||
this.preview ||
ignoreConditions ||
!this.config?.visibility ||
checkConditionsMet(this.config.visibility, this.hass);
this._setElementVisibility(visible);

View File

@@ -4,7 +4,10 @@ import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import { hasScriptFields } from "../../../data/script";
import {
hasRequiredScriptFields,
requiredScriptFieldsFilled,
} from "../../../data/script";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
@@ -50,15 +53,28 @@ class HuiButtonCardFeature extends LitElement implements LovelaceCardFeature {
if (domain === "script") {
const entityId = this._stateObj.entity_id;
if (hasScriptFields(this.hass!, entityId)) {
showMoreInfoDialog(this, { entityId: entityId });
if (
hasRequiredScriptFields(this.hass!, entityId) &&
!requiredScriptFieldsFilled(this.hass!, entityId, this._config?.data)
) {
showMoreInfoDialog(this, {
entityId: entityId,
data: this._config?.data,
});
return;
}
}
this.hass.callService(domain, service, {
const serviceData = {
entity_id: this._stateObj.entity_id,
});
...(this._config?.data
? {
variables: this._config.data,
}
: {}),
};
this.hass.callService(domain, service, serviceData);
}
static getStubConfig(): ButtonCardFeatureConfig {

View File

@@ -2,9 +2,12 @@ import type { AlarmMode } from "../../../data/alarm_control_panel";
import type { HvacMode } from "../../../data/climate";
import type { OperationMode } from "../../../data/water_heater";
export type ButtonCardData = Record<string, any>;
export interface ButtonCardFeatureConfig {
type: "button";
action_name?: string;
data?: ButtonCardData;
}
export interface CoverOpenCloseCardFeatureConfig {

View File

@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { ClockCardConfig } from "../types";
import type { HomeAssistant } from "../../../../types";
import { INTERVAL } from "../hui-clock-card";
import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
function romanize12HourClock(num: number) {
@@ -35,13 +34,11 @@ export class HuiClockCardAnalog extends LitElement {
@state() private _dateTimeFormat?: Intl.DateTimeFormat;
@state() private _hourDeg?: number;
@state() private _hourOffsetSec?: number;
@state() private _minuteDeg?: number;
@state() private _minuteOffsetSec?: number;
@state() private _secondDeg?: number;
private _tickInterval?: undefined | number;
@state() private _secondOffsetSec?: number;
private _initDate() {
if (!this.config || !this.hass) {
@@ -63,7 +60,7 @@ export class HuiClockCardAnalog extends LitElement {
resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
});
this._tick();
this._computeOffsets();
}
protected updated(changedProps: PropertyValues) {
@@ -77,27 +74,25 @@ export class HuiClockCardAnalog extends LitElement {
public connectedCallback() {
super.connectedCallback();
this._startTick();
document.addEventListener("visibilitychange", this._handleVisibilityChange);
this._computeOffsets();
}
public disconnectedCallback() {
super.disconnectedCallback();
this._stopTick();
document.removeEventListener(
"visibilitychange",
this._handleVisibilityChange
);
}
private _startTick() {
this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
this._tick();
}
private _stopTick() {
if (this._tickInterval) {
clearInterval(this._tickInterval);
this._tickInterval = undefined;
private _handleVisibilityChange = () => {
if (!document.hidden) {
this._computeOffsets();
}
}
};
private _tick() {
private _computeOffsets() {
if (!this._dateTimeFormat) return;
const parts = this._dateTimeFormat.formatToParts();
@@ -108,10 +103,14 @@ export class HuiClockCardAnalog extends LitElement {
const hour = hourStr ? parseInt(hourStr, 10) : 0;
const minute = minuteStr ? parseInt(minuteStr, 10) : 0;
const second = secondStr ? parseInt(secondStr, 10) : 0;
const ms = new Date().getMilliseconds();
const secondsWithMs = second + ms / 1000;
this._hourDeg = hour * 30 + minute * 0.5; // 30deg per hour + 0.5deg per minute
this._minuteDeg = minute * 6 + second * 0.1; // 6deg per minute + 0.1deg per second
this._secondDeg = this.config?.show_seconds ? second * 6 : undefined; // 6deg per second
const hour12 = hour % 12;
this._secondOffsetSec = secondsWithMs;
this._minuteOffsetSec = minute * 60 + secondsWithMs;
this._hourOffsetSec = hour12 * 3600 + minute * 60 + secondsWithMs;
}
render() {
@@ -212,16 +211,24 @@ export class HuiClockCardAnalog extends LitElement {
<div class="center-dot"></div>
<div
class="hand hour"
style=${`--hand-rotation: ${this._hourDeg ?? 0}deg;`}
style=${`animation-delay: -${this._hourOffsetSec ?? 0}s;`}
></div>
<div
class="hand minute"
style=${`--hand-rotation: ${this._minuteDeg ?? 0}deg;`}
style=${`animation-delay: -${this._minuteOffsetSec ?? 0}s;`}
></div>
${this.config.show_seconds
? html`<div
class="hand second"
style=${`--hand-rotation: ${this._secondDeg ?? 0}deg;`}
class=${classMap({
hand: true,
second: true,
step: this.config.seconds_motion === "tick",
})}
style=${`animation-delay: -${
(this.config.seconds_motion === "tick"
? Math.floor(this._secondOffsetSec ?? 0)
: (this._secondOffsetSec ?? 0)) as number
}s;`}
></div>`
: nothing}
</div>
@@ -350,10 +357,13 @@ export class HuiClockCardAnalog extends LitElement {
left: 50%;
bottom: 50%;
transform-origin: 50% 100%;
transform: translate(-50%, 0) rotate(var(--hand-rotation, 0deg));
transform: translate(-50%, 0) rotate(0deg);
background: var(--primary-text-color);
border-radius: 2px;
will-change: transform;
animation-name: ha-clock-rotate;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.hand.hour {
@@ -362,6 +372,7 @@ export class HuiClockCardAnalog extends LitElement {
background: var(--primary-text-color);
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.2);
z-index: 1;
animation-duration: 43200s; /* 12 hours */
}
.hand.minute {
@@ -371,6 +382,7 @@ export class HuiClockCardAnalog extends LitElement {
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.2);
opacity: 0.9;
z-index: 3;
animation-duration: 3600s; /* 60 minutes */
}
.hand.second {
@@ -380,6 +392,20 @@ export class HuiClockCardAnalog extends LitElement {
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2);
opacity: 0.8;
z-index: 2;
animation-duration: 60s; /* 60 seconds */
}
.hand.second.step {
animation-timing-function: steps(60, end);
}
@keyframes ha-clock-rotate {
from {
transform: translate(-50%, 0) rotate(0deg);
}
to {
transform: translate(-50%, 0) rotate(360deg);
}
}
`;
}

View File

@@ -3,10 +3,11 @@ import type { PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { ClockCardConfig } from "../types";
import type { HomeAssistant } from "../../../../types";
import { INTERVAL } from "../hui-clock-card";
import { useAmPm } from "../../../../common/datetime/use_am_pm";
import { resolveTimeZone } from "../../../../common/datetime/resolve-time-zone";
const INTERVAL = 1000;
@customElement("hui-clock-card-digital")
export class HuiClockCardDigital extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;

View File

@@ -11,8 +11,6 @@ import type {
} from "../types";
import type { ClockCardConfig } from "./types";
export const INTERVAL = 1000;
@customElement("hui-clock-card")
export class HuiClockCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {

View File

@@ -143,8 +143,7 @@ export class HuiEntityFilterCard
this._element.preview = this.preview;
this._element.layout = this.layout;
}
if (changedProps.has("_config")) {
if (changedProps.has("_config") || changedProps.has("preview")) {
return true;
}
if (changedProps.has("hass")) {
@@ -186,7 +185,11 @@ export class HuiEntityFilterCard
return true;
});
if (entitiesList.length === 0 && this._config.show_empty === false) {
if (
entitiesList.length === 0 &&
this._config.show_empty === false &&
!this.preview
) {
if (!this.hidden) {
this.style.display = "none";
this.toggleAttribute("hidden", true);

View File

@@ -10,8 +10,8 @@ import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import type { ClimateEntity } from "../../../data/climate";
import "../../../state-control/climate/ha-state-control-climate-temperature";
import "../../../state-control/water_heater/ha-state-control-water_heater-temperature";
import type { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features";
import type { LovelaceCardFeatureContext } from "../card-features/types";
@@ -23,6 +23,7 @@ import type {
LovelaceGridOptions,
} from "../types";
import type { ThermostatCardConfig } from "./types";
import { computeDomain } from "../../../common/entity/compute_domain";
@customElement("hui-thermostat-card")
export class HuiThermostatCard extends LitElement implements LovelaceCard {
@@ -69,8 +70,13 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
}
public setConfig(config: ThermostatCardConfig): void {
if (!config.entity || config.entity.split(".")[0] !== "climate") {
throw new Error("Specify an entity from within the climate domain");
if (
!config.entity ||
!["climate", "water_heater"].includes(config.entity.split(".")[0])
) {
throw new Error(
"Specify an entity from within the climate or water_heater domain"
);
}
this._config = config;
@@ -115,7 +121,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
if (!this.hass || !this._config) {
return nothing;
}
const stateObj = this.hass.states[this._config.entity] as ClimateEntity;
const stateObj = this.hass.states[this._config.entity];
if (!stateObj) {
return html`
@@ -124,6 +130,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
</hui-warning>
`;
}
const domain = computeDomain(stateObj.entity_id);
const name = this._config!.name || computeStateName(stateObj);
@@ -137,16 +144,26 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
<ha-card>
<p class="title">${name}</p>
<div class="container">
<ha-state-control-climate-temperature
style=${styleMap({
maxWidth: controlMaxWidth,
})}
prevent-interaction-on-scroll
.showCurrentAsPrimary=${this._config.show_current_as_primary}
show-secondary
.hass=${this.hass}
.stateObj=${stateObj}
></ha-state-control-climate-temperature>
${domain === "water_heater"
? html` <ha-state-control-water_heater-temperature
style=${styleMap({
maxWidth: controlMaxWidth,
})}
prevent-interaction-on-scroll
show-current
.hass=${this.hass}
.stateObj=${stateObj}
></ha-state-control-water_heater-temperature>`
: html` <ha-state-control-climate-temperature
style=${styleMap({
maxWidth: controlMaxWidth,
})}
prevent-interaction-on-scroll
.showCurrentAsPrimary=${this._config.show_current_as_primary}
show-secondary
.hass=${this.hass}
.stateObj=${stateObj}
></ha-state-control-climate-temperature>`}
</div>
<ha-icon-button
class="more-info"

View File

@@ -375,6 +375,7 @@ export interface ClockCardConfig extends LovelaceCardConfig {
clock_style?: "digital" | "analog";
clock_size?: "small" | "medium" | "large";
show_seconds?: boolean | undefined;
seconds_motion?: "continuous" | "tick";
time_format?: TimeFormat;
time_zone?: string;
no_background?: boolean;

View File

@@ -1,10 +1,21 @@
import { html, LitElement, nothing } from "lit";
import { mdiApplicationVariableOutline } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import "../../../../components/ha-service-control";
import "../../../../components/ha-svg-icon";
import { hasScriptFields } from "../../../../data/script";
import type { HomeAssistant } from "../../../../types";
import type { ButtonCardFeatureConfig } from "../../card-features/types";
import type {
ButtonCardFeatureConfig,
LovelaceCardFeatureContext,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
@customElement("hui-button-card-feature-editor")
@@ -14,6 +25,8 @@ export class HuiButtonCardFeatureEditor
{
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: ButtonCardFeatureConfig;
public setConfig(config: ButtonCardFeatureConfig): void {
@@ -35,6 +48,27 @@ export class HuiButtonCardFeatureEditor
return nothing;
}
let scriptData:
| {
action: string;
data?: Record<string, any>;
}
| undefined;
if (this.context?.entity_id) {
const domain = computeDomain(this.context.entity_id);
if (
domain === "script" &&
hasScriptFields(this.hass, this.context.entity_id)
) {
scriptData = {
action: this.context.entity_id,
data: this._config.data,
};
}
}
return html`
<ha-form
.hass=${this.hass}
@@ -43,19 +77,68 @@ export class HuiButtonCardFeatureEditor
.computeLabel=${this._computeLabel}
@value-changed=${this._valueChanged}
></ha-form>
${scriptData
? html`<ha-expansion-panel
outlined
expanded
.header=${this.hass.localize(
"ui.components.service-control.script_variables"
)}
.secondary=${this.hass.localize("ui.common.optional")}
no-collapse
>
<ha-svg-icon
slot="leading-icon"
.path=${mdiApplicationVariableOutline}
></ha-svg-icon>
<ha-service-control
hide-picker
hide-description
.hass=${this.hass}
.value=${scriptData}
.showAdvanced=${this.hass.userData?.showAdvanced}
.narrow=${false}
@value-changed=${this._scriptFieldVariablesChanged}
></ha-service-control
></ha-expansion-panel>`
: nothing}
`;
}
private _computeLabel = () => this.hass.localize("ui.common.name");
private _computeLabel = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "action_name":
return this.hass!.localize("ui.common.name");
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
}
};
private _scriptFieldVariablesChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", {
config: {
...(this._config || {}),
data: ev.detail.value.data,
},
});
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
this.dispatchEvent(
new CustomEvent("config-changed", {
detail: { config: ev.detail.value },
})
);
fireEvent(this, "config-changed", {
config: { ...(this._config || {}), ...ev.detail.value },
});
}
static styles = css`
ha-expansion-panel {
margin-top: 16px;
}
`;
}
declare global {

View File

@@ -52,6 +52,12 @@ const cardConfigStruct = assign(
literal("hour")
)
),
seconds_motion: optional(
defaulted(
union([literal("continuous"), literal("tick")]),
literal("continuous")
)
),
face_style: optional(
defaulted(
union([
@@ -78,7 +84,8 @@ export class HuiClockCardEditor
(
localize: LocalizeFunc,
clockStyle: ClockCardConfig["clock_style"],
ticks: ClockCardConfig["ticks"]
ticks: ClockCardConfig["ticks"],
showSeconds: boolean | undefined
) =>
[
{ name: "title", selector: { text: {} } },
@@ -170,6 +177,33 @@ export class HuiClockCardEditor
},
},
},
...(showSeconds
? ([
{
name: "seconds_motion",
description: {
suffix: localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.description`
),
},
default: "continuous",
selector: {
select: {
mode: "dropdown",
options: ["continuous", "tick"].map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.${value}.label`
),
description: localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.${value}.description`
),
})),
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
...(ticks !== "none"
? ([
{
@@ -257,7 +291,8 @@ export class HuiClockCardEditor
.schema=${this._schema(
this.hass.localize,
this._data(this._config).clock_style,
this._data(this._config).ticks
this._data(this._config).ticks,
this._data(this._config).show_seconds
)}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@@ -278,10 +313,17 @@ export class HuiClockCardEditor
ev.detail.value.border = ev.detail.value.border ?? false;
ev.detail.value.ticks = ev.detail.value.ticks ?? "hour";
ev.detail.value.face_style = ev.detail.value.face_style ?? "markers";
if (ev.detail.value.show_seconds) {
ev.detail.value.seconds_motion =
ev.detail.value.seconds_motion ?? "continuous";
} else {
delete ev.detail.value.seconds_motion;
}
} else {
delete ev.detail.value.border;
delete ev.detail.value.ticks;
delete ev.detail.value.face_style;
delete ev.detail.value.seconds_motion;
}
if (ev.detail.value.ticks !== "none") {
@@ -333,6 +375,10 @@ export class HuiClockCardEditor
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.ticks.label`
);
case "seconds_motion":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.label`
);
case "face_style":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.face_style.label`
@@ -354,6 +400,10 @@ export class HuiClockCardEditor
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.ticks.description`
);
case "seconds_motion":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.seconds_motion.description`
);
case "face_style":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.clock.face_style.description`

View File

@@ -33,14 +33,18 @@ import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor";
import type { FeatureType } from "./hui-card-features-editor";
import { computeDomain } from "../../../../common/entity/compute_domain";
const COMPATIBLE_FEATURES_TYPES: FeatureType[] = [
"climate-hvac-modes",
"climate-preset-modes",
"climate-fan-modes",
"climate-swing-modes",
"climate-swing-horizontal-modes",
];
const COMPATIBLE_FEATURES_TYPES: Record<string, FeatureType[]> = {
climate: [
"climate-hvac-modes",
"climate-preset-modes",
"climate-fan-modes",
"climate-swing-modes",
"climate-swing-horizontal-modes",
],
water_heater: ["water-heater-operation-modes"],
};
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -53,24 +57,6 @@ const cardConfigStruct = assign(
})
);
const SCHEMA = [
{ name: "entity", selector: { entity: { domain: "climate" } } },
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
],
},
{
name: "show_current_as_primary",
selector: {
boolean: {},
},
},
] as const satisfies readonly HaFormSchema[];
@customElement("hui-thermostat-card-editor")
export class HuiThermostatCardEditor
extends LitElement
@@ -91,19 +77,48 @@ export class HuiThermostatCardEditor
})
);
private _schema = memoizeOne(
(domain: string) =>
[
{
name: "entity",
selector: { entity: { domain: ["climate", "water_heater"] } },
},
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
],
},
...(domain === "climate"
? [
{
name: "show_current_as_primary",
selector: {
boolean: {},
},
},
]
: []),
] as const satisfies readonly HaFormSchema[]
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const entityId = this._config.entity;
const domain = computeDomain(entityId);
const featureContext = this._featureContext(entityId);
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${SCHEMA}
.schema=${this._schema(domain)}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
@@ -118,7 +133,7 @@ export class HuiThermostatCardEditor
<hui-card-features-editor
.hass=${this.hass}
.context=${featureContext}
.featuresTypes=${COMPATIBLE_FEATURES_TYPES}
.featuresTypes=${COMPATIBLE_FEATURES_TYPES[domain]}
.features=${this._config!.features ?? []}
@features-changed=${this._featuresChanged}
@edit-detail-element=${this._editDetailElement}
@@ -177,7 +192,9 @@ export class HuiThermostatCardEditor
});
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
if (schema.name === "show_current_as_primary") {
return this.hass!.localize(
"ui.panel.lovelace.editor.card.thermostat.show_current_as_primary"

View File

@@ -1,6 +1,7 @@
import { object, string, any } from "superstruct";
import { object, string, any, optional, boolean } from "superstruct";
export const baseLovelaceBadgeConfig = object({
type: string(),
visibility: any(),
disabled: optional(boolean()),
});

View File

@@ -1,4 +1,4 @@
import { object, string, any } from "superstruct";
import { object, string, any, optional, boolean } from "superstruct";
export const baseLovelaceCardConfig = object({
type: string(),
@@ -6,4 +6,5 @@ export const baseLovelaceCardConfig = object({
layout_options: any(),
grid_options: any(),
visibility: any(),
disabled: optional(boolean()),
});

View File

@@ -32,13 +32,13 @@ const processAreasForMediaPlayers = (
const areaCards: LovelaceCardConfig[] = [];
for (const entityId of areaEntities) {
cards.push({
areaCards.push({
type: "media-control",
entity: entityId,
} satisfies MediaControlCardConfig);
}
if (areaEntities.length > 0) {
if (areaCards.length > 0) {
cards.push({
heading_style: "subtitle",
type: "heading",

View File

@@ -4,13 +4,14 @@ import { isComponentLoaded } from "../../../../common/config/is_component_loaded
import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
import { getCommonControlUsagePrediction } from "../../../../data/usage_prediction";
import type { HomeAssistant } from "../../../../types";
import type { TileCardConfig } from "../../cards/types";
import type { HeadingCardConfig, TileCardConfig } from "../../cards/types";
const DEFAULT_LIMIT = 8;
export interface CommonControlSectionStrategyConfig {
type: "common-controls";
title?: string;
icon?: string;
limit?: number;
exclude_entities?: string[];
hide_empty?: boolean;
@@ -31,7 +32,8 @@ export class CommonControlsSectionStrategy extends ReactiveElement {
section.cards?.push({
type: "heading",
heading: config.title,
});
icon: config.icon,
} satisfies HeadingCardConfig);
}
if (!isComponentLoaded(hass, "usage_prediction")) {
@@ -46,7 +48,9 @@ export class CommonControlsSectionStrategy extends ReactiveElement {
}
const predictedCommonControl = await getCommonControlUsagePrediction(hass);
let predictedEntities = predictedCommonControl.entities;
let predictedEntities = predictedCommonControl.entities.filter(
(entity) => entity in hass.states
);
if (config.exclude_entities) {
predictedEntities = predictedEntities.filter(

View File

@@ -155,12 +155,8 @@ export const haStyleDialog = css`
/* make dialog fullscreen on small screens */
@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;
--mdc-dialog-min-height: 100%;
--mdc-dialog-max-height: 100%;
--vertical-align-dialog: flex-end;

View File

@@ -1,3 +1,4 @@
import scrollLockStyles from "@home-assistant/webawesome/dist/styles/utilities/scroll-lock.css.js";
import { css } from "lit";
import { extractDerivedVars } from "../../common/style/derived-css-vars";
@@ -18,6 +19,8 @@ export const waMainStyles = css`
--wa-border-width-l: var(--ha-border-radius-l);
--wa-space-xl: 32px;
}
${scrollLockStyles}
`;
export const waMainDerivedVariables = extractDerivedVars(waMainStyles);

View File

@@ -30,6 +30,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
{
entityId: ev.detail.entityId,
view: ev.detail.view || ev.detail.tab,
data: ev.detail.data,
},
() => import("../dialogs/more-info/ha-more-info-dialog")
);

View File

@@ -5,7 +5,7 @@
"config": "Settings",
"states": "Overview",
"map": "Map",
"logbook": "Logbook",
"logbook": "Activity",
"history": "History",
"todo": "To-do lists",
"developer_tools": "Developer tools",
@@ -233,7 +233,8 @@
"join": "Join",
"media_players": "Media players",
"select_all": "Select all",
"idle": "Idle"
"idle": "Idle",
"track_position": "Track position"
},
"persistent_notification": {
"dismiss": "Dismiss"
@@ -416,6 +417,7 @@
"next": "Next",
"back": "Back",
"undo": "Undo",
"redo": "Redo",
"move": "Move",
"save": "Save",
"apply": "Apply",
@@ -526,7 +528,7 @@
}
},
"logbook": {
"entries_not_found": "No logbook events found.",
"entries_not_found": "No activity found.",
"triggered_by": "triggered by",
"triggered_by_automation": "triggered by automation",
"triggered_by_script": "triggered by script",
@@ -539,7 +541,7 @@
"triggered_by_homeassistant_stopping": "triggered by Home Assistant stopping",
"triggered_by_homeassistant_starting": "triggered by Home Assistant starting",
"show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]",
"retrieval_error": "Could not load logbook",
"retrieval_error": "Could not load activity",
"not_loaded": "[%key:ui::dialogs::helper_settings::platform_not_loaded%]",
"messages": {
"was_away": "was detected away",
@@ -954,7 +956,8 @@
"target": "Targets",
"target_secondary": "What should this action use as targeted areas, devices or entities.",
"action_data": "Action data",
"integration_doc": "Integration documentation"
"integration_doc": "Integration documentation",
"script_variables": "Script variables"
},
"related-items": {
"no_related_found": "No related items found.",
@@ -1218,7 +1221,9 @@
"yaml-editor": {
"copy_to_clipboard": "[%key:ui::panel::config::automation::editor::copy_to_clipboard%]",
"error": "Error in parsing YAML: {reason}",
"error_location": "line: {line}, column: {column}"
"error_location": "line: {line}, column: {column}",
"enter_fullscreen": "Enter fullscreen",
"exit_fullscreen": "Exit fullscreen"
},
"state-content-picker": {
"state": "State",
@@ -1382,7 +1387,8 @@
"info": "Information",
"related": "Related",
"history": "History",
"logbook": "Logbook",
"aggregate": "5-minute aggregated",
"logbook": "Activity",
"device_or_service_info": "[%key:ui::panel::config::devices::device_info%]",
"device_type": {
"device": "[%key:ui::panel::config::devices::type::device_heading%]",
@@ -2066,7 +2072,9 @@
"cut": "to cut the selected item and place it on the clipboard",
"delete": "to delete the selected item",
"paste": "to paste automation/script YAML from clipboard to editor",
"save": "to save automation/script"
"save": "to save automation/script",
"undo": "to undo last change",
"redo": "to redo last undone change"
},
"charts": {
"title": "Charts",
@@ -3880,6 +3888,8 @@
"add": "Add trigger",
"search": "Search trigger",
"id": "Trigger ID",
"id_helper": "Helps identify each run based on which trigger fired.",
"optional": "Optional",
"edit_id": "Edit ID",
"duplicate": "[%key:ui::common::duplicate%]",
"re_order": "Re-order",
@@ -4284,7 +4294,7 @@
},
"trigger": {
"label": "Triggered by",
"no_triggers": "No triggers have an ID set. Use the three-dot menu on a trigger and select Edit ID to assign one.",
"no_triggers": "There are no triggers with ID's set in this automation. Edit a trigger and give it a Trigger ID name.",
"id": "Trigger",
"description": {
"picker": "If the automation has been triggered by a specific trigger.",
@@ -4598,7 +4608,7 @@
"tabs": {
"details": "Step details",
"timeline": "Trace timeline",
"logbook": "Related logbook entries",
"logbook": "Related activity",
"automation_config": "Automation config",
"step_config": "Step config",
"changed_variables": "Changed variables",
@@ -4615,7 +4625,7 @@
"error": "Error: {error}",
"result": "Result:",
"step_not_executed": "This step was not executed.",
"no_logbook_entries": "No logbook entries found for this step.",
"no_logbook_entries": "No activity found for this step.",
"no_variables_changed": "No variables changed",
"unable_to_find_config": "Unable to find config"
},
@@ -4641,8 +4651,8 @@
"disabled": "(disabled)",
"triggered_by": "{triggeredBy, select, \n alias {{alias} triggered}\n other {Triggered} \n} {triggeredPath, select, \n trigger {by the {trigger}}\n other {manually} \n} at {time}",
"path_error": "Unable to extract path {path}. Download trace and report as bug.",
"not_all_entries_are_related_automation_note": "Not all shown logbook entries might be related to this automation.",
"not_all_entries_are_related_script_note": "Not all shown logbook entries might be related to this script."
"not_all_entries_are_related_automation_note": "Not all shown activity might be related to this automation.",
"not_all_entries_are_related_script_note": "Not all shown activity might be related to this script."
}
}
},
@@ -5355,7 +5365,7 @@
"person_not_found_title": "Person not found",
"person_not_found": "We couldn't find the person you were trying to edit.",
"detail": {
"new_person": "New person",
"new_person": "Add person",
"name": "Name",
"name_error_msg": "Name is required",
"linked_user": "Linked user",
@@ -5366,7 +5376,6 @@
"device_tracker_picked": "Track device",
"device_tracker_pick": "Pick device to track",
"delete": "Delete",
"create": "Create",
"update": "Update",
"confirm_delete_user_title": "Delete user account",
"confirm_delete_user_text": "The user account for ''{name}'' will be permanently deleted. You can still track the user, but the person will no longer be able to log in.",
@@ -7582,8 +7591,8 @@
"square": "Render cards as squares"
},
"logbook": {
"name": "Logbook",
"description": "The Logbook card shows a list of events for entities."
"name": "Activity",
"description": "The Activity card shows a list of events for entities."
},
"history-graph": {
"name": "History graph",
@@ -7825,6 +7834,18 @@
"description": "60 ticks (Every minute)"
}
},
"seconds_motion": {
"label": "Seconds motion",
"description": "How the seconds hand moves on the clock",
"continuous": {
"label": "Continuous",
"description": "The seconds hand moves continuously"
},
"tick": {
"label": "Tick",
"description": "The seconds hand moves in 1 second steps"
}
},
"face_style": {
"label": "Clock face style",
"description": "Which kind of indices to use for the clock face",
@@ -7919,7 +7940,7 @@
},
"thermostat": {
"name": "Thermostat",
"description": "The Thermostat card gives control of a climate entity for heating or cooling, allowing you to set its mode and desired temperature.",
"description": "The Thermostat card gives control of a climate or water heater entity for heating or cooling, allowing you to set its mode and desired temperature.",
"show_current_as_primary": "Show current temperature as primary information"
},
"tile": {
@@ -8514,7 +8535,7 @@
},
"entity_id_picker": {
"title": "Display entity IDs in picker",
"description": "Shows the full entity IDs when selecting entities with a picker."
"description": "Shows the full entity IDs when selecting entities from a menu list."
},
"refresh_tokens": {
"header": "Refresh tokens",

View File

@@ -1646,10 +1646,10 @@ __metadata:
languageName: node
linkType: hard
"@eslint/js@npm:9.35.0":
version: 9.35.0
resolution: "@eslint/js@npm:9.35.0"
checksum: 10/a8764d0592ebe9a4804f8c0dafa7f49dbcdb38cabf30dd50587a3cfa51d898b90a3a0b93975d549f47debdd96b3e21da95081935f74213e45ec8c25f11f2ba1e
"@eslint/js@npm:9.36.0":
version: 9.36.0
resolution: "@eslint/js@npm:9.36.0"
checksum: 10/a0542f529f87b9ad69ef85c47b0c070b763591a61773b131a9d1d53934a587f0708c05a1a8f48a6805486004a4922c91d696c1e4835ff61f8750ffbded2f0c30
languageName: node
linkType: hard
@@ -4491,10 +4491,10 @@ __metadata:
languageName: node
linkType: hard
"@types/chromecast-caf-receiver@npm:6.0.24":
version: 6.0.24
resolution: "@types/chromecast-caf-receiver@npm:6.0.24"
checksum: 10/1f2b95e8a15dbb36d5328895229d4a5cb255b33e62d46335bd6ed75e16aa9ea6a7d765a64ae120d19b3134fb3e51e9547d2544c7277f7bffe0bf0b3999f026da
"@types/chromecast-caf-receiver@npm:6.0.22":
version: 6.0.22
resolution: "@types/chromecast-caf-receiver@npm:6.0.22"
checksum: 10/6c51cb52527776ddfa187a261b88184c98bdd61c129dd8719cba213894d565cf69073734d6473696ffd60a768f6fb5a3fe9932693f43174fbc5e7af201db8a90
languageName: node
linkType: hard
@@ -7352,17 +7352,18 @@ __metadata:
languageName: node
linkType: hard
"del@npm:8.0.0":
version: 8.0.0
resolution: "del@npm:8.0.0"
"del@npm:8.0.1":
version: 8.0.1
resolution: "del@npm:8.0.1"
dependencies:
globby: "npm:^14.0.2"
is-glob: "npm:^4.0.3"
is-path-cwd: "npm:^3.0.0"
is-path-inside: "npm:^4.0.0"
p-map: "npm:^7.0.2"
presentable-error: "npm:^0.0.1"
slash: "npm:^5.1.0"
checksum: 10/502dea7a846f989e1d921733f5d41ae4ae9b3eff168d335bfc050c9ce938ddc46198180be133814269268c4b0aed441a82fbace948c0ec5eed4ed086a4ad3b0e
checksum: 10/53ed4a379a68c90e7d6d3bcce09c49229e77de9a946d0a5fc25f45b16c950cb8665986b7d0d0423416c03bfd43e0f31e528c5a19c558fe47449be9d6fae7f846
languageName: node
linkType: hard
@@ -8193,9 +8194,9 @@ __metadata:
languageName: node
linkType: hard
"eslint@npm:9.35.0":
version: 9.35.0
resolution: "eslint@npm:9.35.0"
"eslint@npm:9.36.0":
version: 9.36.0
resolution: "eslint@npm:9.36.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.8.0"
"@eslint-community/regexpp": "npm:^4.12.1"
@@ -8203,7 +8204,7 @@ __metadata:
"@eslint/config-helpers": "npm:^0.3.1"
"@eslint/core": "npm:^0.15.2"
"@eslint/eslintrc": "npm:^3.3.1"
"@eslint/js": "npm:9.35.0"
"@eslint/js": "npm:9.36.0"
"@eslint/plugin-kit": "npm:^0.3.5"
"@humanfs/node": "npm:^0.16.6"
"@humanwhocodes/module-importer": "npm:^1.0.1"
@@ -8239,7 +8240,7 @@ __metadata:
optional: true
bin:
eslint: bin/eslint.js
checksum: 10/238155639343d53bac639319ba92895083cbd15826081ac51204b29d64fbb52cebf0d355f11f57f146d2b15c4f2e1d85e3df0b0ac7ec0e2ef5e759c99dcab75e
checksum: 10/6e512a82e680e6cdc554e97c7e232b83171f24a52fb46f89c2df74bcb80fe59b6e0a989485c9fe7e9ca81556b1953dd8604ace4ed37f651eded9a37816c06b7c
languageName: node
linkType: hard
@@ -9441,7 +9442,7 @@ __metadata:
"@tsparticles/engine": "npm:3.9.1"
"@tsparticles/preset-links": "npm:3.2.0"
"@types/babel__plugin-transform-runtime": "npm:7.9.5"
"@types/chromecast-caf-receiver": "npm:6.0.24"
"@types/chromecast-caf-receiver": "npm:6.0.22"
"@types/chromecast-caf-sender": "npm:1.0.11"
"@types/color-name": "npm:2.0.0"
"@types/culori": "npm:4.0.1"
@@ -9478,11 +9479,11 @@ __metadata:
date-fns: "npm:4.1.0"
deep-clone-simple: "npm:1.1.1"
deep-freeze: "npm:0.0.1"
del: "npm:8.0.0"
del: "npm:8.0.1"
dialog-polyfill: "npm:0.5.6"
echarts: "npm:6.0.0"
element-internals-polyfill: "npm:3.0.2"
eslint: "npm:9.35.0"
eslint: "npm:9.36.0"
eslint-config-airbnb-base: "npm:15.0.0"
eslint-config-prettier: "npm:10.1.8"
eslint-import-resolver-webpack: "npm:0.13.10"
@@ -9532,7 +9533,7 @@ __metadata:
qrcode: "npm:1.5.4"
roboto-fontface: "npm:0.10.0"
rrule: "npm:2.8.1"
rspack-manifest-plugin: "npm:5.0.3"
rspack-manifest-plugin: "npm:5.1.0"
serve: "npm:14.2.5"
sinon: "npm:21.0.0"
sortablejs: "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch"
@@ -12361,6 +12362,13 @@ __metadata:
languageName: node
linkType: hard
"presentable-error@npm:^0.0.1":
version: 0.0.1
resolution: "presentable-error@npm:0.0.1"
checksum: 10/013809ee7a47ced847a8d860e9b89a56cdd8c4f1ad04ad8da1e58fd60843f77f497d204146bb15aaa9793d3b94ad8626eed01256fc9eb5839a545af2000a5fa4
languageName: node
linkType: hard
"prettier@npm:3.6.2":
version: 3.6.2
resolution: "prettier@npm:3.6.2"
@@ -13022,9 +13030,9 @@ __metadata:
languageName: node
linkType: hard
"rspack-manifest-plugin@npm:5.0.3":
version: 5.0.3
resolution: "rspack-manifest-plugin@npm:5.0.3"
"rspack-manifest-plugin@npm:5.1.0":
version: 5.1.0
resolution: "rspack-manifest-plugin@npm:5.1.0"
dependencies:
"@rspack/lite-tapable": "npm:^1.0.1"
peerDependencies:
@@ -13032,7 +13040,7 @@ __metadata:
peerDependenciesMeta:
"@rspack/core":
optional: true
checksum: 10/59e951e25801e39e798ef0042a509f1ebf8f81350eab6016b30519ade6f1f949162fe3ecf3447b9c99afc495d4b83576edb6e886c98d66db7918c82de8936ab9
checksum: 10/672de1eb405b48c7a50f03d1813048b1a6711afd1e7f6829471c020bc16de96ecbf8d8a10c3f8cdb6bfcaa6e93461bd22cef92203f17d8494a53e285d21a16ad
languageName: node
linkType: hard