Compare commits

...
Author SHA1 Message Date
Aidan Timson ba22a12a20 Fix 2026-03-03 14:46:42 +00:00
Aidan Timson 098b54f749 Fix 2026-03-03 14:46:15 +00:00
Aidan Timson 4c6a7091a6 Filtering 2026-03-03 14:46:15 +00:00
Aidan Timson 322cb35526 More types 2026-03-03 14:46:15 +00:00
Aidan Timson c34f6bea2b Always show 2026-03-03 14:46:15 +00:00
Aidan Timson 41bf0652b0 Setup log classification 2026-03-03 14:46:15 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
23af40743b Update dependency lint-staged to v16.3.0 (#29954)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 14:36:48 +00:00
Paul BotteinandGitHub c4326b4f3a Use max width for dashboard footer (#29947) 2026-03-03 14:57:57 +01:00
Paul BotteinandGitHub d248f5614f Add label for toggle button in area strategy (#29949) 2026-03-03 13:34:05 +01:00
a4da7b26ea Fix copy to clipboard for wa dialogs (#29951)
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Wendelin <[email protected]>
2026-03-03 13:09:30 +01:00
WendelinandGitHub 3c49cdf3c0 ha-bottom-sheet reduce motion support (#29950) 2026-03-03 11:06:23 +00:00
Petar PetrovandGitHub 26af81d1a4 Use net battery power in power sankey card (#29940) 2026-03-03 11:53:54 +01:00
Aidan TimsonandGitHub 2a08f2d79b Add tooltip for config dashboard action button in toolbar (#29948) 2026-03-03 10:33:37 +01:00
Marcin BauerandGitHub a5be02b743 Add tooltip for Lovelace dropdown action button in top app bar (#29933) 2026-03-03 09:15:37 +00:00
sevorlandGitHub 4228871f00 Fix missing slot attribute on wa-divider in automation sidebar action (#29942) 2026-03-03 08:56:33 +00:00
WendelinandGitHub 9a7a8fd377 Add reportValidity in ha-form (#29884)
* Add validation for required fields in ha-auth-flow before submission

* Add reportValidity methods to form components for improved validation handling

* Remove async reportValidity funcs

* Review
2026-03-03 09:05:49 +02:00
WendelinandGitHub 8b82882e15 ha-authorize fix rtl check (#29937)
Add RTL direction handling in updated lifecycle method
2026-03-02 18:22:06 +01:00
2701015eda Fix data-table content bottom margin (#29805)
Co-authored-by: Copilot <[email protected]>
2026-03-02 17:02:31 +01:00
1991a9e493 Code editor fullscreen in dialogs (#29882)
Co-authored-by: Bram Kragten <[email protected]>
2026-03-02 17:01:13 +01:00
Petar PetrovandGitHub 2b72c54194 Migrate Energy date selector to new footer (#29867) 2026-03-02 17:00:34 +01:00
Paul BotteinandGitHub a7cb2fe7a7 Fix updates, discovered devices and repairs cards flickering (#29935) 2026-03-02 13:50:42 +00:00
Paul BotteinandGitHub 51ea0c8201 Fix sidebar not closing when reduced motion is enabled (#29934) 2026-03-02 13:19:26 +00:00
WendelinandGitHub ead7081bc6 Dialog: Add show event target check (#29927)
Add event phase check in _handleShow and _handleAfterShow methods
2026-03-02 11:41:52 +00:00
WendelinandGitHub ee982b1899 Add error translation for loading energy preferences (#29924) 2026-03-02 11:49:46 +02:00
Aidan TimsonandGitHub e8b100a39e Remove cache to fix re-add repo issue (#29926)
Remove cache to fix readd repo issue
2026-03-02 11:49:19 +02:00
50c361db62 Add mixin to remove code duplication in automation/script editors (#29842)
* Initial plan

* Changes before error encountered

Co-authored-by: wendevlin <[email protected]>

* Fix mixin: use function-body syntax for decorators, curried generics for type safety

Co-authored-by: wendevlin <[email protected]>

* Simplify automation/script editor mixin signature

* Add shared styles and loading animation to automation/script editor mixin

Co-authored-by: wendevlin <[email protected]>

* Remove underscore prefix from protected members per style guide

Co-authored-by: wendevlin <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: wendevlin <[email protected]>
Co-authored-by: Wendelin <[email protected]>
2026-03-02 11:44:04 +02:00
sevorlandGitHub e7a8d15a13 Use ha-duration-input for wait_template timeout (#29862) 2026-03-02 09:07:51 +01:00
karwostsandGitHub fbd0409837 Init ha-form expansion elements to undefined instead of null (#29900)
* Init ha-form expansion elements to undefined instead of null

* revert change to error/warning
2026-03-02 09:29:06 +02:00
karwostsandGitHub a0d100611f Fix distribution card stub error (#29915)
* Fix distribution card stub error

* unit check not required
2026-03-02 09:06:10 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a969bf1065 Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#29922)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/b7c566a772e6b6bfb58ed0dc250532a479d7789f...bbbca2ddaa5d8feaa63e36b76fdaad77386f024f)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 06:10:19 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
a153330610 Update dependency gulp-zopfli-green to v7 (#29919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 07:06:41 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
bd2f1ca3a8 Update dependency @html-eslint/eslint-plugin to v0.57.1 (#29905)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 20:08:37 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3263034416 Update dependency @codemirror/language to v6.12.2 (#29904)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 20:07:43 +01:00
Paul BotteinandGitHub 82b28b547a Fix control select menu color in ios (#29892) 2026-02-27 17:26:04 +01:00
Bram KragtenandGitHub 61c2c750b4 Fix overflow for icon buttons (#29891) 2026-02-27 15:44:21 +00:00
Petar PetrovandGitHub 117690ee70 Fix sensor card graph not updating when value is unchanged (#29889) 2026-02-27 15:41:54 +00:00
Petar PetrovandGitHub e753de85eb Make hui-sections-view always fill the screen so footer is at the bottom (#29890) 2026-02-27 15:39:21 +00:00
Paul BotteinandGitHub a240019968 Add render icon property to ha-control-select-menu (#29881) 2026-02-27 16:23:58 +01:00
Petar PetrovandGitHub 0bdf4b8777 Fix monetary device class state display with non-ISO 4217 currency symbols (#29887) 2026-02-27 14:59:14 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
6337828ed8 Update dependency barcode-detector to v3.1.0 (#29886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-27 15:46:29 +02:00
b8e5af652b Add audits and yaml mode to more info details (#29854)
* Add audits and yaml mode to more info details

* Reset yaml mode on back

* Use mapped array for state entries

* Typo

Co-authored-by: Bram Kragten <[email protected]>

* Memoize

* Rename

* Fix

* Format audits in normal mode

* Refactor, dont pass hass

---------

Co-authored-by: Bram Kragten <[email protected]>
2026-02-27 14:45:55 +01:00
Petar PetrovandGitHub e4ae29e8b5 Fix energy compare tooltip showing wrong year (#29885) 2026-02-27 14:37:52 +01:00
Aidan TimsonandGitHub 08231dbbb0 Use large width on system log dialogs (#29879) 2026-02-27 12:46:10 +01:00
Paul Bottein 0ca656933d Revert "Add render icon property to ha-control-select-menu"
This reverts commit b23cf8eba4.
2026-02-27 12:21:23 +01:00
Paul Bottein b23cf8eba4 Add render icon property to ha-control-select-menu 2026-02-27 12:20:52 +01:00
Robert ReschandGitHub 61b546415d Revert "Add vacuum mapping not configured issue" (#29876) 2026-02-27 11:18:49 +01:00
Brandon ChenandGitHub 4e1b709303 Fix YAML content invisible in dark mode for conversation debug result… (#29874) 2026-02-27 09:11:28 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
34e65b302d Update dependency typescript-eslint to v8.56.1 (#29868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 18:08:04 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
336d0e1b9d Update dependency @html-eslint/eslint-plugin to v0.57.0 (#29863)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 17:25:10 +01:00
Paul BotteinandGitHub 58d4cf8d84 Fix scrollbar in 2026.3 (#29865) 2026-02-26 16:44:12 +01:00
Aidan TimsonandGitHub d3453aff37 Add missing theming variable support to dialog and bottom sheet (#29857) 2026-02-26 16:43:20 +01:00
Aidan TimsonandGitHub 64ff2e414c Add thread configuration my link (#29861) 2026-02-26 15:06:46 +00:00
WendelinandGitHub 2ca25c980f Fix quick search icon size (#29858) 2026-02-26 15:59:27 +01:00
Aidan TimsonandGitHub 73d93bc601 Add matter configuration my link (#29859) 2026-02-26 14:41:43 +00:00
WendelinandGitHub 5ca6a8aced Fix ha-icon-button-toggle selected style (#29856) 2026-02-26 13:02:12 +00:00
Aidan TimsonandGitHub 7ff4993e0b Fix esc closing dialogs with prevent scrim close (#29851) 2026-02-26 13:20:05 +02:00
Norbert RittelandGitHub 4e6fbacccc Remove trailing periods from "Learn more" etc. links / tooltips (#29835) 2026-02-26 10:38:54 +00:00
Petar PetrovandGitHub 2958d49e36 Convert Energy Now tiles to badges (#29845) 2026-02-26 10:38:01 +00:00
Norbert RittelandGitHub 92289dc7ea Improve "Create a new … helper" option in entity picker (#29853) 2026-02-26 10:34:42 +00:00
Petar PetrovandGitHub f6c1a890e4 Dynamically calculate the date range picker's vertical opening direction (#29850) 2026-02-26 09:33:34 +00:00
WendelinandGitHub d06321ed43 Fix protocols dashboards fab padding (#29847) 2026-02-26 10:31:50 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3c3d8d9974 Bump rollup from 2.79.2 to 2.80.0 (#29841)
Bumps [rollup](https://github.com/rollup/rollup) from 2.79.2 to 2.80.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/v2.80.0/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.79.2...v2.80.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 2.80.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 08:18:15 +02:00
Paul BotteinandGitHub 4f39fa482d Only ask to refresh dashboard in edit mode or yaml mode (#29826) 2026-02-26 08:16:21 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5d0fe3236c Update dependency @swc/helpers to v0.5.19 (#29836)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 07:07:37 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
b86142ae50 Update Node.js to v24.14.0 (#29831)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 19:25:42 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5d2f3ee5e8 Update dependency tar to v7.5.9 (#29832)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 19:24:58 +00:00
AlCalzoneandGitHub e3f7c631a7 Rename "Z-Wave JS" to "Z-Wave" when not referring to the project/org (#29830) 2026-02-25 19:15:16 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
49f9d95853 Update dependency vite-tsconfig-paths to v6.1.1 (#29829)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 18:53:12 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
db3d7701b5 Update dependency typescript-eslint to v8.56.0 (#29828)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 17:35:36 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3e55acf531 Update dependency @home-assistant/webawesome to v3.2.1-ha.3 (#29810)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 18:26:47 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
f102618d9d Update dependency eslint-plugin-wc to v3.1.0 (#29824)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 18:25:06 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
a3c02b511d Update dependency jsdom to v28.1.0 (#29825)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 18:24:38 +01:00
Bram KragtenandGitHub 74111d248e Fix css minifying (#29827) 2026-02-25 17:53:50 +01:00
Bram Kragten f8161b3505 Merge branch 'rc' into dev 2026-02-25 17:13:44 +01:00
6070c1907a Adjust brands assets to proxy brand images through local API (#29799)
Co-authored-by: Bram Kragten <[email protected]>
2026-02-25 17:10:38 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
ce5991582c Update dependency @html-eslint/eslint-plugin to v0.56.0 (#29818)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 17:37:34 +02:00
Paul BotteinandGitHub d17217fc90 Use show in sidebar property instead of checking title (#29815) 2026-02-25 16:37:25 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
86b4bd0013 Update dependency eslint-plugin-lit to v2.2.1 (#29821)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 17:36:42 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
108ba3abd6 Update dependency eslint-plugin-unused-imports to v4.4.1 (#29822)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 17:36:12 +02:00
Matthias AlphartandGitHub d38a2894c4 Remove unused properties in ha-data-table and hass-tabs-subpage-data-table (#29808) 2026-02-25 16:31:39 +01:00
Aidan TimsonandGitHub 4c70376a62 Cleanup old comments (#29823) 2026-02-25 15:24:00 +00:00
WendelinandGitHub 8d69bd1401 Fix button active also for icon-buttons (#29820) 2026-02-25 16:21:31 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5dfecd3693 Update dependency @octokit/plugin-retry to v8.1.0 (#29819)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 15:09:19 +00:00
Aidan TimsonandGitHub efd51d2234 Rename more info "Attributes" to "Details", add raw state and all available attributes (#29811) 2026-02-25 15:57:27 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
668299c16a Update dependency marked to v17.0.3 (#29817)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 16:00:51 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5e155a4030 Update dependency glob to v13.0.6 (#29816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 16:00:25 +02:00
gpoitchandGitHub 809fa10135 Add day of week to energy chart tooltips (#29803)
* Add day of week to energy chart tooltips

* New localization helpers
2026-02-25 13:31:00 +00:00
Petar PetrovandGitHub 1cbc38f231 Water flow rate sankey chart in Now view (#29804) 2026-02-25 14:18:48 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
9ed39bb523 Update dependency @rspack/core to v1.7.6 (#29812)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 15:10:38 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4e3d66cf40 Update dependency eslint to v9.39.3 (#29813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 15:10:03 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2eaad79d1c Update dependency @codemirror/view to v6.39.15 (#29807)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 12:47:06 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
afef7a2c0f Update dependency @bundle-stats/plugin-webpack-filter to v4.21.10 (#29806)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 12:46:41 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
18d5224002 Update dependency @formatjs/intl-datetimeformat to v7.2.2 (#29809)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 12:46:08 +02:00
Johan HenkensandGitHub dbffdfeaca Add cover of device type window to the security dashboard (#29797) 2026-02-25 11:11:13 +01:00
Artur PragaczandGitHub 0a4b7917ab Update vacuum segment mapping description (#29802) 2026-02-25 08:38:37 +01:00
Simon LamonandGitHub e1524358d9 Remove duplicated buttons (#29798) 2026-02-25 08:22:40 +01:00
Artur PragaczandGitHub 8774f9c3fc Add vacuum mapping not configured issue (#29800) 2026-02-25 08:14:38 +01:00
WendelinandGitHub f9a9aeacab Fix app panel narrow header safe area top (#29792)
* Enhance narrow property to reflect changes and adjust header padding for safe area

* Remove safe-area-inset-top for narrow iframe

* handle kiosk mode
2026-02-24 20:26:06 +01:00
ildar170975andGitHub b798fee116 Data tables: keep "Actions" as the last column (#29364)
* Data tables: keep "Actions" as the last column

* last_fixed -> lastFixed

* last_fixed -> lastFixed

* last_fixed -> lastFixed

* last_fixed -> lastFixed

* last_fixed -> lastFixed

* last_fixed -> lastFixed

* last_fixed -> lastFixed

* last_fixed -> lastFixed

* last_fixed -> lastFixed

* last_fixed -> lastFixed

* simplify

* Update dialog-data-table-settings.ts

* narrow down a column

* blank line added

* narrow dow Assistants a bit more

* remove moveable/hideable for "actions"

* remove moveable/hideable for "actions"

* remove moveable/hideable for "actions"

* remove moveable/hideable for "actions"

* remove moveable/hideable for "actions"

* remove moveable/hideable for "actions"

* remove moveable/hideable for "actions"

* remove moveable/hideable for "actions"
2026-02-24 20:24:53 +01:00
Norbert RittelandGitHub b25f731f0f Simplify card descriptions using "This …" instead of repeating the name (#29795)
Simplify card description using "This …" instead of repeating the name
2026-02-24 20:17:05 +01:00
Paul BotteinandGitHub 26a7372c5e Don't show label for toggle all lights and align individual lights (#29794) 2026-02-24 17:28:53 +01:00
Paul BotteinandGitHub 70d3409d62 Don't use navigation history when using tabs (#29791) 2026-02-24 18:03:48 +02:00
WendelinandGitHub 0711ecddab Handle selector edge case for [] (#29790) 2026-02-24 15:30:29 +00:00
Petar PetrovandGitHub bcfaa67eba Add power, water and gas current flow rate tile cards (#29788) 2026-02-24 16:01:32 +01:00
1b60e6e04e Reorganize Zigbee settings page (#29671)
Co-authored-by: TheJulianJES <[email protected]>
Co-authored-by: Norbert Rittel <[email protected]>
Co-authored-by: Bram Kragten <[email protected]>
2026-02-24 15:11:36 +01:00
a1a634f6dc Add footer card support to sections view (#29620)
Co-authored-by: Wendelin <[email protected]>
Co-authored-by: Wendelin <[email protected]>
2026-02-24 11:00:50 +00:00
Petar PetrovandGitHub 55f48fbb56 Add tabs to energy config page (#29689) 2026-02-24 09:43:02 +00:00
Norbert RittelandGitHub ca4d66b94c Change second tab to "Electricity" in Energy dashboard (#29787) 2026-02-24 10:13:22 +01:00
51fd2eedd9 Update gallery with latest adaptive dialog changes (#29672)
* Update gallery with latest adaptive dialog changes

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

Co-authored-by: Bram Kragten <[email protected]>

* Format

---------

Co-authored-by: Bram Kragten <[email protected]>
2026-02-24 06:57:31 +01:00
ildar170975andGitHub 434a7c2e93 "Numeric state" trigger editor: add a "filter_entity" context to "attribute" selector (#29778)
* add a "filter_entity" context to "attribute" selector

* remove unused variable
2026-02-24 07:55:49 +02:00
b849fecf0b Add flow rate picker to gas, water, and water device energy dialogs (#29693)
* Add flow rate picker to gas, water, and water device energy dialogs

Add optional flow rate (stat_rate) picker to gas source, water source,
and water device configuration dialogs, matching the pattern used for
power tracking in grid/solar/battery sources and energy devices.

- Add stat_rate to GasSourceTypeEnergyPreference and WaterSourceTypeEnergyPreference
- Collect gas/water stat_rate in getReferencedStatisticIdsPower()
- Add flow rate ha-statistic-picker filtered to volume_flow_rate device class
- Move entity help text to picker helper props for cleaner layout

* Apply suggestions from code review

Co-authored-by: Norbert Rittel <[email protected]>

---------

Co-authored-by: Norbert Rittel <[email protected]>
2026-02-24 06:55:35 +01:00
ildar170975andGitHub 3a48e1996f hui-entities-card: fix "buttons-header-footer" margin-bottom (#29783)
* fix margin-bottom for hui-buttons-header-footer

* typo
2026-02-24 07:54:50 +02:00
ildar170975andGitHub 8299386737 ha-entity-attribute-picker: add valueRenderer (#29780)
add valueRenderer
2026-02-24 06:52:10 +01:00
Raphael HehlandGitHub 5e58ff476f Re-initialize camera stream when backend finishes starting (#29752) 2026-02-23 16:59:53 +01:00
758d955053 Add configuration to built-in panels (#29572)
Co-authored-by: Norbert Rittel <[email protected]>
Co-authored-by: Petar Petrov <[email protected]>
2026-02-23 16:15:54 +01:00
Kevin StillhammerandGitHub 1efd5d26f0 Show allow_negative in DurationSelector options (#29775) 2026-02-23 16:02:33 +01:00
Aidan TimsonandGitHub 36979f10cc Fix types for dialog hide events (#29777) 2026-02-23 15:54:36 +01:00
Aidan TimsonandGitHub 812c59fcb4 Add missing back path for protocol config dashboards (#29770) 2026-02-23 15:36:50 +01:00
karwostsandGitHub 0c34165bcf Disallow moving a section to non-sections view (#29756) 2026-02-23 10:54:11 +00:00
8c2bfbe9ce Reorganize Matter settings (#29708)
Co-authored-by: Norbert Rittel <[email protected]>
2026-02-23 10:31:38 +00:00
Aidan TimsonandGitHub 8f721d74e2 Fix swipe action bubbling up to stacked bottom drawer/sheet (#29768) 2026-02-23 10:26:10 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
63782e6ef3 Bump lodash from 4.17.21 to 4.17.23 (#29767)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 10:22:31 +01:00
eaad2295a9 Fix ZHA config dashboard button animation targeting (#29744)
Co-authored-by: Marie-Alice Blete <[email protected]>
2026-02-23 10:18:32 +01:00
Norbert RittelandGitHub e74eee3d34 Make all picker strings of Frontend conditions consistent (#29742) 2026-02-23 09:54:14 +01:00
karwostsandGitHub cc39010839 Fix group more-info names for not-in-registry entities (#29758) 2026-02-23 09:50:55 +01:00
Kevin StillhammerandGitHub 7f97425214 Allow to disable seconds in DurationSelector (#29760) 2026-02-23 09:30:04 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8fac6e63de Bump actions/stale from 10.1.1 to 10.2.0 (#29765)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 09:22:29 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2ac8fe2b21 Bump github/codeql-action from 4.32.3 to 4.32.4 (#29766)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 09:22:18 +01:00
Aidan TimsonandGitHub 45ca1b2cdc Fix esc closing all dialogs or sheets (close one after another) (#29732) 2026-02-23 09:13:28 +01:00
0667f1e789 Reorganize Bluetooth settings (#29723)
* Reorganize Bluetooth settings

* Additional changes

* Updates adapter page

* Update statuses

* Fix button icon

* Update en.json

Co-authored-by: Petar Petrov <[email protected]>

* Update bluetooth-adapter-info-page.ts

Co-authored-by: Petar Petrov <[email protected]>

* Update text

* Show GATT message and make row clickable

---------

Co-authored-by: Petar Petrov <[email protected]>
2026-02-23 06:59:09 +01:00
Norbert RittelandGitHub db49678ccb Make descriptions of Frontend actions consistent with Core (#29740) 2026-02-20 18:03:19 +01:00
Norbert RittelandGitHub 2ca7e9f71e Make building block descriptions consistent with new conditions (#29739)
Make building block descriptions consistent with conditions
2026-02-20 18:02:41 +01:00
karwostsandGitHub 8d883450a8 Add year period to stat graph card editor (#29741)
Add year period to stat graph card
2026-02-20 18:02:07 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Petar Petrov
2c136e00f5 Bump tar from 7.5.7 to 7.5.8 (#29735)
* Bump tar from 7.5.7 to 7.5.8

Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.7 to 7.5.8.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.7...v7.5.8)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.8
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <[email protected]>

* dedupe

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <[email protected]>
2026-02-20 07:05:32 +00:00
RoboMagusandGitHub 6f82478598 Fix header tab height (#29736)
Fix header tabs to header height
2026-02-20 08:48:43 +02:00
Aidan TimsonandGitHub 1093bd890f Add missing helper to ha-select, remove unused attr (#29729) 2026-02-19 18:54:29 +01:00
456c638750 Use ha-scrollbar in config dashboard (#29724)
* Use ha-scrollbar in config dashboard

* Remove padding

Co-authored-by: Copilot <[email protected]>

* Add padding to bottom

---------

Co-authored-by: Copilot <[email protected]>
2026-02-19 18:52:32 +01:00
60ca50deb4 Add a drag handle visual indicator to bottom sheet (#29707)
* Add drag handle to bottom sheet

* Remove locks

* Fix rounded corners

* Restore original functionality, keep visual indicator

* Add padding to combo box

* Apply suggestion from @wendevlin

* Fix prettier

* Shorter height

Co-authored-by: Marcin Bauer <[email protected]>

* Half width

Co-authored-by: Marcin Bauer <[email protected]>

* Restore after rebase

* Reduce space for picker

---------

Co-authored-by: Wendelin <[email protected]>
Co-authored-by: Wendelin <[email protected]>
Co-authored-by: Marcin Bauer <[email protected]>
2026-02-19 14:24:09 +01:00
Matthias de BaatandGitHub 2064ab4141 Reorganize Z-Wave settings page (#29697)
* Reorganize ZWave settings

* Next iteration

* Made more consistent with Zigbee settings page

* Update text

* Updates on the provisioned devices page

* Add identifier when you have multiple networks

* Update to force remove button

* Update button text

* Update rebuild text

* Update remove foreign device button text
2026-02-19 13:45:35 +02:00
karwostsandGitHub d34c42e587 Refine supported actions in button heading badge (#29718) 2026-02-19 12:49:30 +02:00
Joakim SørensenandGitHub 5da7bf6fba Add repository handling for missing addons in HaConfigAppDashboard (#29722)
* Add repository handling for missing addons in HaConfigAppDashboard

* Implement feedback

* More adjustments

* minor adjustment
2026-02-19 10:36:34 +00:00
Norbert RittelandGitHub f05ff58d27 Replace "consumption" with "usage" for battery and grid energy (#29719)
Replace "consumption" with "usage" for battery and grid power
2026-02-19 10:59:47 +02:00
Aidan TimsonandGitHub 7b0a381d93 Use ha-scrollbar with history panel, fix overflow position (#29715)
Use ha-scrollbar with history panel
2026-02-18 18:04:46 +01:00
Aidan TimsonandGitHub 8b38e6d170 Switch dialog device registry detail to adaptive dialog (#29713) 2026-02-18 18:04:05 +01:00
Aidan TimsonandGitHub 6daf0eb469 Use ha-scrollbar with media browser (#29714) 2026-02-18 18:03:26 +01:00
WendelinandGitHub 6f8f849af3 Prevent bottom-sheet from closing from child elements (#29716)
Fix handling of after hide event in ha-bottom-sheet component
2026-02-18 16:19:43 +00:00
Aidan TimsonandGitHub cafe0f62c6 Trigger add todo item dialog via search param (#29690)
* Fix scrim closure

* Trigger add todo item dialog via add_item=true search param

* Check supports before opening prompt

* Use in willUpdate

* Add subtitle as context using name of list
2026-02-18 16:28:20 +01:00
721cf46ce5 Migrate ha-icon-button to webawesome (#29622)
* Remove mwc-icon-button dependency and update ha-icon-button to use ha-button component

* --mdc-icon-button-size to --ha-icon-button-size

* Refactor ha-icon-button styles to improve encapsulation and remove redundant CSS rules

* add href functionality

* Migrate a wrapped ha-icon-button to ha-icon-button

* Update slot reference for ha-icon-button in hui-dialog-save-config

* fix overflow trigger

* Review

* fix sub icon buttons

* Fix attribute binding for href and target in ha-icon-button-next

* Fix binding for href and target properties in ha-icon-button

* Update src/layouts/hass-tabs-subpage.ts

Co-authored-by: Bram Kragten <[email protected]>

* Update src/panels/lovelace/editor/hui-dialog-save-config.ts

Co-authored-by: Bram Kragten <[email protected]>

* Update src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts

Co-authored-by: Bram Kragten <[email protected]>

* Update src/panels/config/labs/ha-config-labs.ts

Co-authored-by: Bram Kragten <[email protected]>

* Fix icon-button slot

* Update src/components/ha-icon-button.ts

Co-authored-by: Bram Kragten <[email protected]>

---------

Co-authored-by: Bram Kragten <[email protected]>
2026-02-18 16:18:28 +01:00
Marcin BauerandGitHub 4e087760ab Fix chip order in automation save dialog to match field order (#29710) 2026-02-18 15:18:07 +00:00
8fcfd4be84 Move scrolling for dashboards inside view container (#29444)
* Move scrolling for dashboards inside view container

* Use scrollbar styles on host

* Cleanup

* Inline

Co-authored-by: Wendelin <[email protected]>

---------

Co-authored-by: Wendelin <[email protected]>
2026-02-18 16:17:13 +01:00
WendelinandGitHub b03680a8ab ha-automation-action-condition use generic-picker (#29702)
Refactor ha-automation-action-condition to use ha-generic-picker and improve condition rendering
2026-02-18 15:14:25 +00:00
7ab0622bec cloud-tts-pref: fix for language picker (#29678)
* fix styles to prevent oveflow

* use a new variable to define min-width

* pass a "minWidth" property into ha-language-picker

* use a "minWidth" property for ha-generic-picker

* Update ha-language-picker.ts

* pass empty minWidth

* do not set min-width if empty

* add a style for ha-language-picker

* remove a style for ha-language-picker

* add a style for ha-language-picker

* remove min-width

* add a style for ha-language-picker

* Update src/panels/profile/ha-pick-language-row.ts

Co-authored-by: Petar Petrov <[email protected]>

* add a gap

---------

Co-authored-by: Petar Petrov <[email protected]>
2026-02-18 14:05:41 +00:00
Bram KragtenandGitHub c5aad44768 Add support for vacuum segment mapping to areas (#29343)
* Add support for vacuum segment mapping to areas

* simplify, use list item

* Update ha-more-info-view-vacuum-segment-mapping.ts

* review

* review

* Update dialog-vacuum-segment-mapping.ts
2026-02-18 14:11:17 +01:00
IcecoveryandGitHub 20ee7e5dc7 Split antimeridian-crossing paths in ha-map (#29694)
* Add option to split antimeridian-crossing path to ha-map

and map card with related editor options

* Remove split antimeridian-crossing option in ha-map

making it the default behavior, as suggested by @karwosts. And remove the option from the map card

* Fix longitudeDifference is zero edge case
2026-02-18 12:08:03 +00:00
32fdcc708e Fix history timeline showing same color for all zones (#29700)
* Fix history timeline showing same color for all zones

For person and device_tracker entities, zone states (e.g. "Kitchen",
"Office") all resolved to --state-person-active-color because their
zone-specific CSS variables don't exist and the fallback chain always
landed on the generic active color.

Now zone states only check for an explicitly defined CSS variable
(e.g. --state-person-kitchen-color) and otherwise fall through to the
generic color handler which assigns a unique palette color per zone.

Fixes #14705

* Update src/components/chart/timeline-color.ts

Co-authored-by: Wendelin <[email protected]>

---------

Co-authored-by: Wendelin <[email protected]>
2026-02-18 10:45:15 +01:00
7dd9b3308e Switch more info dialog to adaptive dialog (#29664)
* Switch more info dialog to adaptive dialog

* Remove old attr

* Fixed height

* Add dialog styles for ha-adaptive-dialog, fixes fixed top

* Lock swipe for moveable components

* Add more components

* Add locked classes

* Refactor

* Revert "Refactor"

This reverts commit 041161715e.

* Merge for loops

* Use events to track slider interaction and prevent bottom sheet closure

* Update src/components/ha-bottom-sheet.ts

* Update src/resources/styles.ts

---------

Co-authored-by: Wendelin <[email protected]>
2026-02-18 08:55:11 +01:00
Aidan TimsonandGitHub 71b870be15 Fix styles for manage zha device dialog (#29684)
* Fix styles for manage zha device dialog

* Prevent scrim close
2026-02-18 09:41:45 +02:00
Simon LamonandGitHub f08c5fa03a Assign no-stale to Tasks/Epic/Opportunity issue type (#29698)
* Enhance issue creation restrictions and labeling

Added functionality to restrict Task issue creation to organization members, authorized contributors, and integration code owners. Updated permissions and added a no-stale label for specific issue types.

* Refactor Task issue authorization checks

* Update github-script action and enhance Task issue handling
2026-02-18 09:37:31 +02:00
WendelinandGitHub fca408ae23 Upgrade webawesome to version 3.2.1-ha.2 (#29691)
Upgrade webawesome to version 3.2.1-ha.2 and adjust animation durations to 0ms
2026-02-18 08:35:23 +02:00
Aidan TimsonandGitHub f3a814e38a Cleanup dialog default width attrs (#29686) 2026-02-17 20:13:27 +01:00
7b0e4651c4 Fix iframe flash for dark theme using transition (#29685)
* Fix iframe flash for light mode using transition

* Use normal duration

Co-authored-by: Petar Petrov <[email protected]>

* Use normal duration

Co-authored-by: Petar Petrov <[email protected]>

---------

Co-authored-by: Petar Petrov <[email protected]>
2026-02-17 16:53:06 +02:00
Bram Kragten d98e373f64 Bumped version to 20260128.6 2026-02-04 15:41:09 +01:00
Paul BotteinandBram Kragten 649516c9fa Change default icon for blank area if not icon configured (#29394) 2026-02-04 15:40:36 +01:00
Paul BotteinandBram Kragten bbc4fb96b2 Load domain translation when integration page load (#29391) 2026-02-04 15:40:35 +01:00
Paul BotteinandBram Kragten 0ae639aeb0 Remove old lovelace overview from pickers (#29390) 2026-02-04 15:40:34 +01:00
karwostsandBram Kragten 0e7e41065e Don't shrink ha-dropdown checkboxes (#29387) 2026-02-04 15:40:33 +01:00
Paul BotteinandBram Kragten 685843f112 Add translations for new overview dialog (#29382) 2026-02-04 15:40:32 +01:00
Paul BotteinandBram Kragten 5e1a99d94a Use area icon for area empty state (#29371) 2026-02-04 15:40:31 +01:00
Bram Kragten d843349865 Bumped version to 20260128.5 2026-02-03 16:58:01 +01:00
Paul BotteinandBram Kragten ec23164aa9 Improve other devices page in home dashboard (#29370) 2026-02-03 16:57:45 +01:00
e74ef11101 Hide edit and delete actions for YAML dashboards in config (#29368)
YAML dashboards are defined in configuration files and cannot be
modified or deleted through the UI. This change ensures the edit
and delete actions are only shown for storage-mode dashboards.

Co-authored-by: Claude Opus 4.5 <[email protected]>
2026-02-03 16:57:43 +01:00
Paul BotteinandBram Kragten a222f6a736 Add missing danger variant in dropdown item (#29359) 2026-02-03 16:57:40 +01:00
Petar PetrovandBram Kragten ef3dd16d45 Move dialog scrim to pseudo-element (#29357) 2026-02-03 16:57:39 +01:00
karwostsandBram Kragten 5d4e1d205e Fix missing imports in devtools-statistics (#29355) 2026-02-03 16:57:38 +01:00
Darryn Capes-DavisandBram Kragten 1ee5ebbe75 Fix CSS minification issue for ha-card (#29354) 2026-02-03 16:57:37 +01:00
Bram Kragten 59d705aa3d Bumped version to 20260128.4 2026-02-02 17:17:24 +01:00
Paul BotteinandBram Kragten 332e108dae Fix "Reload resources" menu for YAML resource mode (#29346) 2026-02-02 17:17:17 +01:00
karwostsandBram Kragten 3c15b29d0a Entity diagnostic - handle entity not in the registry (#29344) 2026-02-02 17:17:16 +01:00
WendelinandBram Kragten 130c708e23 Fix dropdown width in datatables (#29340) 2026-02-02 17:17:15 +01:00
Paul BotteinandBram Kragten 588a14a8a7 Fix type error for missing hass.themes race condition in themes mixin (#29338) 2026-02-02 17:17:14 +01:00
Petar PetrovandBram Kragten a1ef6ad266 Remove redundant dialog backdrop color (#29337) 2026-02-02 17:17:13 +01:00
Aidan TimsonandBram Kragten a6c1f87730 Ensure template renderer overflows on overflow (#29335) 2026-02-02 17:17:12 +01:00
WendelinandBram Kragten 49252a3808 Fix missing ha-md-menu in config/labels (#29334) 2026-02-02 17:17:11 +01:00
Aidan TimsonandBram Kragten c7877fe38f Show hint only if keyboard shortcuts is enabled (#29332)
Enabled by default, must be explicity disabled
2026-02-02 17:17:10 +01:00
WendelinandBram Kragten e355a61d8f Revert "Fix automation sidebar ui supported check" (#29331) 2026-02-02 17:17:08 +01:00
Linus RathandBram Kragten f2e19e51ce Update untracked consumption threshold to 1W (#29310) 2026-02-02 17:17:07 +01:00
karwostsandBram Kragten fd9ab8f561 Use ha-form for condition template (#29301) 2026-02-02 17:17:06 +01:00
KristelandBram Kragten faa1b3c98f bugfix: add eventlistener for exposed-entities-changed to Entities page (#29299) 2026-02-02 17:17:06 +01:00
Aidan TimsonandBram Kragten acc4a84fc9 Fix scrolling for labs page (#29287) 2026-02-02 17:17:05 +01:00
karwostsandBram Kragten 4d723dac37 Fix areas cannot be deleted (#29285) 2026-02-02 17:17:03 +01:00
Aidan TimsonandBram Kragten f1d4d0ef98 Fix type error for missing hass.config race condition in themes mixin (#29280) 2026-02-02 17:17:02 +01:00
Paul BotteinandBram Kragten 88180a2708 Fix demo because of new default panel (#29279) 2026-02-02 17:17:01 +01:00
Aidan TimsonandBram Kragten 258d87e3d5 Add missing settings nav items for quick search (#29278)
* Add missing repairs quick search item

* Add voice assistants
2026-02-02 17:17:00 +01:00
WendelinandBram Kragten 55f22ba61a Implement fallback for dialog close event in Quick Search (#29260) 2026-02-02 17:16:59 +01:00
Aidan TimsonandBram Kragten 812f3ca8b9 Change default shortcut tip in Quick Search to mod+k, add tip to settings (#29253) 2026-02-02 17:16:58 +01:00
7f880d11a0 Keep focus on search field when clicking filter chips (#29249)
Co-authored-by: Claude Opus 4.5 <[email protected]>
2026-02-02 17:16:57 +01:00
Bram Kragten 6b2452c538 Update compress.js 2026-02-02 17:15:44 +01:00
Paul Bottein c2cbf8bd21 Bumped version to 20260128.3 2026-01-30 10:31:26 +01:00
WendelinandPaul Bottein 224bcece9c Fix multi select in quick search (#29272)
Add item selection state management to QuickBar component
2026-01-30 10:30:33 +01:00
WendelinandPaul Bottein dc84b7698f Fix --wa-color-text-normal (#29271)
Update normal text color variable in wa.globals.ts
2026-01-30 10:30:32 +01:00
WendelinandPaul Bottein bc22e6a9bd Fix device download diagnostic via overflow (#29269)
fix diagnostic download link handling to simplify URL signing
2026-01-30 10:30:31 +01:00
Simon LamonandPaul Bottein d44874783a Remove duplicated text (#29265) 2026-01-30 10:30:30 +01:00
Paul Bottein 8d1bb5c867 Fix default lovelace yaml loading (#29240) 2026-01-30 10:30:29 +01:00
Bram Kragten da1b528eee Bumped version to 20260128.2 2026-01-29 18:04:42 +01:00
Paul BotteinandBram Kragten 756138408a Remove default title for new dashboards (#29259) 2026-01-29 18:04:09 +01:00
Paul BotteinandBram Kragten 3c8f112565 Prevent action in tile container (#29257) 2026-01-29 18:04:08 +01:00
Paul BotteinandBram Kragten 2521f3dde4 Fix actions in dashboard overflow menu (#29256) 2026-01-29 18:04:07 +01:00
TheJulianJESandBram Kragten 56390aa01a Fix Matter dashboard using disabled and ignored config entries (#29254) 2026-01-29 17:52:32 +01:00
Paul BotteinandBram Kragten 9aac5b19da Stop click propagation when clicking item in icon overflow (#29252) 2026-01-29 17:52:31 +01:00
WendelinandBram Kragten 24afc3dc88 Prevent quick search to close from hot keys (#29251) 2026-01-29 17:52:30 +01:00
Paul BotteinandBram Kragten 873c7b2947 Remove unused theme option in distribution card (#29250) 2026-01-29 17:52:29 +01:00
Aidan TimsonandBram Kragten 648db4276b Add protocols to quick search (#29248)
Add protocols to quick search, extract logic and translations
2026-01-29 17:52:28 +01:00
Aidan TimsonandBram Kragten f86c3e7856 Remove unused "app" item from quick search (#29244) 2026-01-29 17:52:27 +01:00
Aidan TimsonandBram Kragten 1d0251cc28 Fixes for picker combo box scrolling and selection (#29242) 2026-01-29 17:52:26 +01:00
WendelinandBram Kragten 518cf87847 Fix quick search apps (#29238) 2026-01-29 17:52:25 +01:00
ildar170975andBram Kragten 81a9216c44 computeGroupEntitiesState(): fix condition (#29234)
* fix condition

* fix condition

* prettier
2026-01-29 17:52:24 +01:00
Paul BotteinandBram Kragten f0e10e0058 Fix default yaml lovelace panel loading (#29230) 2026-01-29 17:52:23 +01:00
Paul BotteinandBram Kragten 5df8ea4f07 Add welcome banner for new overview dashboard (#29223) 2026-01-29 17:52:22 +01:00
Aidan TimsonandBram Kragten 73f081f5cc Add meta+click/enter support to quick search (#29220)
* Allow meta+click event from combobox

* Handle new tab events for navigations

* Add mod+enter support for new tab

* Helper function
2026-01-29 17:52:21 +01:00
Petar PetrovandBram Kragten f0d1db1da6 Add non standard power sensor support (#28845)
* Add non standard power sensor support

* remove useless code

* GridPowerSourceInput type for grid power source saving
2026-01-29 17:52:20 +01:00
Bram Kragten c658eb414b Bumped version to 20260128.1 2026-01-28 17:52:10 +01:00
Bram Kragten bac493b72b dont include brotli compression 2026-01-28 17:50:19 +01:00
330 changed files with 12756 additions and 5191 deletions
+2 -2
View File
@@ -89,13 +89,13 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: frontend-bundle-stats
path: build/stats/*.json
if-no-files-found: error
- name: Upload frontend build
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: frontend-build
path: hass_frontend/
+3 -3
View File
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
+2 -2
View File
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: translations
path: translations.tar.gz
+30 -1
View File
@@ -5,9 +5,38 @@ on:
issues:
types: [opened]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}
jobs:
check-authorization:
add-no-stale:
name: Add no-stale label
runs-on: ubuntu-latest
permissions:
issues: write # To add labels to issues
if: >-
github.event.issue.type.name == 'Task'
|| github.event.issue.type.name == 'Epic'
|| github.event.issue.type.name == 'Opportunity'
steps:
- name: Add no-stale label
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['no-stale']
});
check-authorization:
name: Check authorization
runs-on: ubuntu-latest
permissions:
issues: write # To comment on, label, and close issues
# Only run if this is a Task issue type (from the issue form)
if: github.event.issue.type.name == 'Task'
steps:
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90
+1 -1
View File
@@ -1 +1 @@
24.13.1
24.14.0
@@ -21,8 +21,8 @@ type DialogType =
| "basic"
| "basic-subtitle-below"
| "basic-subtitle-above"
| "allow-mode-change"
| "form"
| "form-block-mode"
| "actions"
| "large"
| "small";
@@ -69,8 +69,8 @@ export class DemoHaAdaptiveDialog extends LitElement {
<ha-button @click=${this._handleOpenDialog("form")}
>Adaptive dialog with form</ha-button
>
<ha-button @click=${this._handleOpenDialog("form-block-mode")}
>Adaptive dialog with form (block mode change)</ha-button
<ha-button @click=${this._handleOpenDialog("allow-mode-change")}
>Adaptive dialog with allow mode change</ha-button
>
<ha-button @click=${this._handleOpenDialog("actions")}
>Adaptive dialog with actions</ha-button
@@ -164,27 +164,15 @@ export class DemoHaAdaptiveDialog extends LitElement {
<ha-adaptive-dialog
.hass=${this._hass}
.open=${this._openDialog === "form-block-mode"}
header-title="Adaptive dialog with form (block mode change)"
header-subtitle="This form will not reset when the viewport size changes"
block-mode-change
.allowModeChange=${this._openDialog === "allow-mode-change"}
header-title="Adaptive dialog with allow mode change"
header-subtitle="Resize the window while this dialog is open"
@closed=${this._handleClosed}
>
<ha-form autofocus .schema=${SCHEMA}></ha-form>
<ha-dialog-footer slot="footer">
<ha-button
@click=${this._handleClosed}
slot="secondaryAction"
variant="plain"
>Cancel</ha-button
>
<ha-button
@click=${this._handleClosed}
slot="primaryAction"
variant="accent"
>Submit</ha-button
>
</ha-dialog-footer>
<div>
This dialog can switch between dialog mode and bottom sheet mode
while open.
</div>
</ha-adaptive-dialog>
<ha-adaptive-dialog
@@ -225,10 +213,9 @@ export class DemoHaAdaptiveDialog extends LitElement {
</ul>
<p>
The mode is determined automatically and updates when the window is
resized. To prevent mode changes after the initial mount (useful for
preventing form resets), use the <code>block-mode-change</code>
attribute.
By default, the mode is determined at mount time and then stays fixed
while the dialog is open. To allow switching modes while the viewport
changes, use the <code>allow-mode-change</code> attribute.
</p>
<h3>Width</h3>
@@ -399,10 +386,10 @@ export class DemoHaAdaptiveDialog extends LitElement {
</p>
<p>
Use the <code>block-mode-change</code> attribute when you want to
prevent the dialog from switching modes after it's opened. This is
especially useful for forms, as it prevents form data from being lost
when users resize their browser window.
Use the <code>allow-mode-change</code> attribute when you want the
dialog to switch between modes as the viewport changes after opening.
For forms, you can keep the default behavior to avoid resetting fields
on resize.
</p>
<h3>Example usage</h3>
@@ -410,7 +397,6 @@ export class DemoHaAdaptiveDialog extends LitElement {
<pre><code>&lt;ha-adaptive-dialog
.hass=\${this.hass}
open
width="medium"
header-title="Dialog title"
header-subtitle="Dialog subtitle"
&gt;
@@ -427,23 +413,6 @@ export class DemoHaAdaptiveDialog extends LitElement {
&lt;/ha-dialog-footer&gt;
&lt;/ha-adaptive-dialog&gt;</code></pre>
<p>Example with <code>block-mode-change</code> for forms:</p>
<pre><code>&lt;ha-adaptive-dialog
.hass=\${this.hass}
open
header-title="Edit configuration"
block-mode-change
&gt;
&lt;ha-form .schema=\${schema} .data=\${data}&gt;&lt;/ha-form&gt;
&lt;ha-dialog-footer slot="footer"&gt;
&lt;ha-button slot="secondaryAction" variant="plain"
&gt;Cancel&lt;/ha-button
&gt;
&lt;ha-button slot="primaryAction" variant="accent"&gt;Save&lt;/ha-button&gt;
&lt;/ha-dialog-footer&gt;
&lt;/ha-adaptive-dialog&gt;</code></pre>
<h3>API</h3>
<p>
@@ -521,12 +490,10 @@ export class DemoHaAdaptiveDialog extends LitElement {
<td></td>
</tr>
<tr>
<td><code>block-mode-change</code></td>
<td><code>allow-mode-change</code></td>
<td>
When set, the mode is determined at mount time based on the
current screen size, but subsequent mode changes are blocked.
Useful for preventing forms from resetting when the viewport
size changes.
When set, the dialog can switch between modes as the viewport
size changes while it is open.
</td>
<td><code>false</code></td>
<td><code>false</code>, <code>true</code></td>
@@ -548,6 +515,14 @@ export class DemoHaAdaptiveDialog extends LitElement {
<td><code>--ha-dialog-surface-background</code></td>
<td>Dialog/sheet background color.</td>
</tr>
<tr>
<td><code>--ha-dialog-surface-backdrop-filter</code></td>
<td>Dialog/sheet surface backdrop filter.</td>
</tr>
<tr>
<td><code>--dialog-box-shadow</code></td>
<td>Dialog surface box shadow (dialog mode only).</td>
</tr>
<tr>
<td><code>--ha-dialog-border-radius</code></td>
<td>Border radius of the dialog surface (dialog mode only).</td>
@@ -560,6 +535,34 @@ export class DemoHaAdaptiveDialog extends LitElement {
<td><code>--ha-dialog-hide-duration</code></td>
<td>Hide animation duration (dialog mode only).</td>
</tr>
<tr>
<td><code>--ha-dialog-scrim-backdrop-filter</code></td>
<td>Dialog/sheet scrim backdrop filter.</td>
</tr>
<tr>
<td><code>--dialog-backdrop-filter</code></td>
<td>Dialog/sheet scrim backdrop filter (legacy fallback).</td>
</tr>
<tr>
<td><code>--mdc-dialog-scrim-color</code></td>
<td>Dialog/sheet scrim color (legacy compatibility).</td>
</tr>
<tr>
<td><code>--ha-bottom-sheet-surface-background</code></td>
<td>Bottom sheet background color (sheet mode only).</td>
</tr>
<tr>
<td><code>--ha-bottom-sheet-surface-backdrop-filter</code></td>
<td>Bottom sheet surface backdrop filter (sheet mode only).</td>
</tr>
<tr>
<td><code>--ha-bottom-sheet-scrim-backdrop-filter</code></td>
<td>Bottom sheet scrim backdrop filter (sheet mode only).</td>
</tr>
<tr>
<td><code>--ha-bottom-sheet-scrim-color</code></td>
<td>Bottom sheet scrim color (sheet mode only).</td>
</tr>
</tbody>
</table>
+18 -2
View File
@@ -380,13 +380,29 @@ export class DemoHaDialog extends LitElement {
<td><code>--ha-dialog-surface-background</code></td>
<td>Dialog background color.</td>
</tr>
<tr>
<td><code>--ha-dialog-surface-backdrop-filter</code></td>
<td>Backdrop filter applied to the dialog surface.</td>
</tr>
<tr>
<td><code>--dialog-box-shadow</code></td>
<td>Dialog surface box shadow.</td>
</tr>
<tr>
<td><code>--ha-dialog-border-radius</code></td>
<td>Border radius of the dialog surface.</td>
</tr>
<tr>
<td><code>--dialog-z-index</code></td>
<td>Z-index for the dialog.</td>
<td><code>--ha-dialog-scrim-backdrop-filter</code></td>
<td>Backdrop filter applied to the dialog scrim.</td>
</tr>
<tr>
<td><code>--dialog-backdrop-filter</code></td>
<td>Legacy fallback for the dialog scrim backdrop filter.</td>
</tr>
<tr>
<td><code>--mdc-dialog-scrim-color</code></td>
<td>Dialog scrim color (legacy compatibility).</td>
</tr>
<tr>
<td><code>--dialog-surface-margin-top</code></td>
+3
View File
@@ -222,6 +222,9 @@ class HaLandingPage extends LandingPageBaseElement {
flex-direction: column;
gap: var(--ha-space-4);
}
ha-language-picker {
min-width: 200px;
}
ha-alert p {
text-align: unset;
}
+24 -25
View File
@@ -30,14 +30,14 @@
"@braintree/sanitize-url": "7.1.2",
"@codemirror/autocomplete": "6.20.0",
"@codemirror/commands": "6.10.2",
"@codemirror/language": "6.12.1",
"@codemirror/language": "6.12.2",
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.6.0",
"@codemirror/state": "6.5.4",
"@codemirror/view": "6.39.12",
"@codemirror/view": "6.39.15",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.2.1",
"@formatjs/intl-datetimeformat": "7.2.2",
"@formatjs/intl-displaynames": "7.2.1",
"@formatjs/intl-durationformat": "0.10.1",
"@formatjs/intl-getcanonicallocales": "3.2.1",
@@ -52,7 +52,7 @@
"@fullcalendar/list": "6.1.20",
"@fullcalendar/luxon3": "6.1.20",
"@fullcalendar/timegrid": "6.1.20",
"@home-assistant/webawesome": "3.2.1-ha.0",
"@home-assistant/webawesome": "3.2.1-ha.3",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.1.0",
"@lit-labs/observers": "2.1.0",
@@ -68,7 +68,6 @@
"@material/mwc-fab": "0.27.0",
"@material/mwc-floating-label": "0.27.0",
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
"@material/mwc-icon-button": "0.27.0",
"@material/mwc-linear-progress": "0.27.0",
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"@material/mwc-radio": "0.27.0",
@@ -84,7 +83,7 @@
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.18",
"@swc/helpers": "0.5.19",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0",
@@ -93,7 +92,7 @@
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "3.0.8",
"barcode-detector": "3.1.0",
"color-name": "2.1.0",
"comlink": "4.4.2",
"core-js": "3.48.0",
@@ -107,7 +106,7 @@
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"gulp-zopfli-green": "7.0.0",
"hls.js": "1.6.15",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
@@ -119,7 +118,7 @@
"lit": "3.3.2",
"lit-html": "3.3.2",
"luxon": "3.7.2",
"marked": "17.0.1",
"marked": "17.0.3",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.4",
"object-hash": "3.0.0",
@@ -149,14 +148,14 @@
"@babel/helper-define-polyfill-provider": "0.6.6",
"@babel/plugin-transform-runtime": "7.29.0",
"@babel/preset-env": "7.29.0",
"@bundle-stats/plugin-webpack-filter": "4.21.9",
"@html-eslint/eslint-plugin": "0.55.0",
"@bundle-stats/plugin-webpack-filter": "4.21.10",
"@html-eslint/eslint-plugin": "0.57.1",
"@lokalise/node-api": "15.6.1",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.2",
"@rspack/core": "1.7.5",
"@rspack/core": "1.7.6",
"@rspack/dev-server": "1.2.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.25",
@@ -173,7 +172,7 @@
"@types/mocha": "10.0.10",
"@types/qrcode": "1.5.6",
"@types/sortablejs": "1.15.9",
"@types/tar": "6.1.13",
"@types/tar": "7.0.87",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.18",
@@ -181,27 +180,27 @@
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.1",
"eslint": "9.39.2",
"eslint": "9.39.3",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.10",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-lit": "2.1.1",
"eslint-plugin-lit": "2.2.1",
"eslint-plugin-lit-a11y": "5.1.1",
"eslint-plugin-unused-imports": "4.3.0",
"eslint-plugin-wc": "3.0.2",
"eslint-plugin-unused-imports": "4.4.1",
"eslint-plugin-wc": "3.1.0",
"fancy-log": "2.0.0",
"fs-extra": "11.3.3",
"glob": "13.0.1",
"glob": "13.0.6",
"gulp": "5.0.1",
"gulp-brotli": "3.0.0",
"gulp-json-transform": "0.5.0",
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "28.0.0",
"jsdom": "28.1.0",
"jszip": "3.10.1",
"lint-staged": "16.2.7",
"lint-staged": "16.3.0",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
@@ -211,12 +210,12 @@
"rspack-manifest-plugin": "5.2.1",
"serve": "14.2.5",
"sinon": "21.0.1",
"tar": "7.5.7",
"tar": "7.5.9",
"terser-webpack-plugin": "5.3.16",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.54.0",
"vite-tsconfig-paths": "6.0.5",
"typescript-eslint": "8.56.1",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.0.18",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
@@ -236,6 +235,6 @@
},
"packageManager": "[email protected]",
"volta": {
"node": "24.13.1"
"node": "24.14.0"
}
}
+10 -2
View File
@@ -2,7 +2,7 @@
import { genClientId } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert";
@@ -23,6 +23,7 @@ import type {
DataEntryFlowStepForm,
} from "../data/data_entry_flow";
import "./ha-auth-form";
import type { HaAuthForm } from "./ha-auth-form";
type State = "loading" | "error" | "step";
@@ -52,6 +53,8 @@ export class HaAuthFlow extends LitElement {
@state() private _submitting = false;
@query("ha-auth-form") private _form?: HaAuthForm;
createRenderRoot() {
return this;
}
@@ -179,7 +182,7 @@ export class HaAuthFlow extends LitElement {
<div class="action">
<ha-button
@click=${this._handleSubmit}
.disabled=${this._submitting}
.loading=${this._submitting}
>
${this.step.type === "form"
? this.localize("ui.panel.page-authorize.form.next")
@@ -370,6 +373,11 @@ export class HaAuthFlow extends LitElement {
this._providerChanged(this.authProvider);
return;
}
if (!this._form?.reportValidity()) {
return;
}
this._submitting = true;
const postData = { ...this._stepData, client_id: this.clientId };
+5 -1
View File
@@ -12,6 +12,10 @@ export class HaAuthFormString extends HaFormString {
return this;
}
public reportValidity(): boolean {
return this.querySelector("ha-auth-textfield")?.reportValidity() ?? true;
}
protected render(): TemplateResult {
return html`
<style>
@@ -31,7 +35,7 @@ export class HaAuthFormString extends HaFormString {
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--ha-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
+36
View File
@@ -210,3 +210,39 @@ const formatDateWeekdayShortMem = memoizeOne(
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
})
);
// Mon, Aug 10
export const formatDateWeekdayVeryShortDate = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) =>
formatDateWeekdayVeryShortDateMem(locale, config.time_zone).format(dateObj);
const formatDateWeekdayVeryShortDateMem = memoizeOne(
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
weekday: "short",
month: "short",
day: "numeric",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
})
);
// Mon, Aug 10, 2021
export const formatDateWeekdayShortDate = (
dateObj: Date,
locale: FrontendLocaleData,
config: HassConfig
) => formatDateWeekdayShortDateMem(locale, config.time_zone).format(dateObj);
const formatDateWeekdayShortDateMem = memoizeOne(
(locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, {
weekday: "short",
month: "short",
day: "numeric",
year: "numeric",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
})
);
+22 -21
View File
@@ -133,33 +133,34 @@ const computeStateToPartsFromEntityAttributes = (
),
});
} catch (_err) {
// fallback to default
// fallback to default numeric formatting below
}
const TYPE_MAP: Record<string, ValuePart["type"]> = {
integer: "value",
group: "value",
decimal: "value",
fraction: "value",
literal: "literal",
currency: "unit",
};
if (parts.length) {
const TYPE_MAP: Record<string, ValuePart["type"]> = {
integer: "value",
group: "value",
decimal: "value",
fraction: "value",
literal: "literal",
currency: "unit",
};
const valueParts: ValuePart[] = [];
const valueParts: ValuePart[] = [];
for (const part of parts) {
const type = TYPE_MAP[part.type];
if (!type) continue;
const last = valueParts[valueParts.length - 1];
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
if (type === "value" && last?.type === "value") {
last.value += part.value;
} else {
valueParts.push({ type, value: part.value });
for (const part of parts) {
const type = TYPE_MAP[part.type];
if (!type) continue;
const last = valueParts[valueParts.length - 1];
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
if (type === "value" && last?.type === "value") {
last.value += part.value;
} else {
valueParts.push({ type, value: part.value });
}
}
return valueParts;
}
return valueParts;
}
// default processing of numeric values
+27 -1
View File
@@ -1,3 +1,24 @@
import { deepActiveElement } from "../dom/deep-active-element";
const getClipboardFallbackRoot = (): HTMLElement => {
const activeElement = deepActiveElement();
if (activeElement instanceof HTMLElement) {
let root: Node = activeElement.getRootNode();
let host: HTMLElement | null = null;
while (root instanceof ShadowRoot && root.host instanceof HTMLElement) {
host = root.host;
root = root.host.getRootNode();
}
if (host) {
return host;
}
}
return document.body;
};
export const copyToClipboard = async (str, rootEl?: HTMLElement) => {
if (navigator.clipboard) {
try {
@@ -8,10 +29,15 @@ export const copyToClipboard = async (str, rootEl?: HTMLElement) => {
}
}
const root = rootEl ?? document.body;
const root = rootEl || getClipboardFallbackRoot();
const el = document.createElement("textarea");
el.value = str;
el.setAttribute("readonly", "");
el.style.position = "fixed";
el.style.top = "0";
el.style.left = "0";
el.style.opacity = "0";
root.appendChild(el);
el.select();
document.execCommand("copy");
+5 -5
View File
@@ -18,9 +18,11 @@ import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { ensureArray } from "../../common/array/ensure-array";
import { getAllGraphColors } from "../../common/color/colors";
import { fireEvent } from "../../common/dom/fire_event";
import type { HASSDomEvent } from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import { listenMediaQuery } from "../../common/dom/media_query";
import { afterNextRender } from "../../common/util/render-status";
import { filterXSS } from "../../common/util/xss";
import { themesContext } from "../../data/context";
import type { Themes } from "../../data/ws-themes";
import type { ECOption } from "../../resources/echarts/echarts";
@@ -28,8 +30,6 @@ import type { HomeAssistant } from "../../types";
import { isMac } from "../../util/is_mac";
import "../chips/ha-assist-chip";
import "../ha-icon-button";
import { afterNextRender } from "../../common/util/render-status";
import { filterXSS } from "../../common/util/xss";
import { formatTimeLabel } from "./axis-label";
import { downSampleLineData } from "./down-sample";
@@ -1115,13 +1115,13 @@ export class HaChartBase extends LitElement {
.chart-controls ::slotted(ha-icon-button) {
background: var(--card-background-color);
border-radius: var(--ha-border-radius-sm);
--mdc-icon-button-size: 32px;
--ha-icon-button-size: 32px;
color: var(--primary-color);
border: 1px solid var(--divider-color);
}
.chart-controls.small ha-icon-button,
.chart-controls.small ::slotted(ha-icon-button) {
--mdc-icon-button-size: 22px;
--ha-icon-button-size: 22px;
--mdc-icon-size: 16px;
}
.chart-controls ha-icon-button.inactive,
+17 -2
View File
@@ -6,6 +6,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
import { stateColorProperties } from "../../common/entity/state_color";
import { slugify } from "../../common/string/slugify";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import { computeCssValue } from "../../resources/css-variables";
@@ -32,6 +33,22 @@ function computeTimelineStateColor(
return computeCssValue("--history-unknown-color", computedStyles);
}
const domain = computeDomain(stateObj.entity_id);
// Zone states for person/device_tracker don't have specific CSS color variables,
// so they all fall back to the same --state-person-active-color.
// Only use a custom CSS variable if explicitly defined (e.g. --state-person-kitchen-color),
// otherwise return undefined to get unique colors from the generic color handler.
if (
(domain === "person" || domain === "device_tracker") &&
!((FIXED_DOMAIN_STATES[domain] || []) as readonly string[]).includes(state)
) {
return computeCssValue(
`--state-${domain}-${slugify(state, "_")}-color`,
computedStyles
);
}
const properties = stateColorProperties(stateObj, state);
if (!properties) {
@@ -41,8 +58,6 @@ function computeTimelineStateColor(
const rgb = computeCssValue(properties, computedStyles);
if (!rgb) return undefined;
const domain = computeDomain(stateObj.entity_id);
const shade = DOMAIN_STATE_SHADES[domain]?.[state] as number | number;
if (!shade) {
return rgb;
@@ -32,11 +32,13 @@ export class DialogDataTableSettings extends LitElement {
@state() private _hiddenColumns?: string[];
private _lastFixedKeys: string[] = [];
@state() private _open = false;
public showDialog(params: DataTableSettingsDialogParams) {
this._params = params;
this._columnOrder = params.columnOrder;
this._columnOrder = this._preserveLastFixed(params.columnOrder);
this._hiddenColumns = params.hiddenColumns;
this._open = true;
}
@@ -50,6 +52,29 @@ export class DialogDataTableSettings extends LitElement {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _lastFixedCount(): number {
const lastFixedKeys = Object.keys(this._params!.columns).filter(
(col) => this._params!.columns[col].lastFixed
);
if (lastFixedKeys.length) {
this._lastFixedKeys = lastFixedKeys;
}
return lastFixedKeys.length;
}
private _preserveLastFixed(columnOrder) {
let strippedColumnOrder;
const lastFixedCount = this._lastFixedCount();
if (lastFixedCount && columnOrder) {
strippedColumnOrder = [...columnOrder];
strippedColumnOrder.splice(
columnOrder.length - lastFixedCount,
lastFixedCount
);
}
return strippedColumnOrder;
}
private _sortedColumns = memoizeOne(
(
columns: DataTableColumnContainer,
@@ -57,7 +82,7 @@ export class DialogDataTableSettings extends LitElement {
hiddenColumns: string[] | undefined
) =>
Object.keys(columns)
.filter((col) => !columns[col].hidden)
.filter((col) => !columns[col].hidden && !columns[col].lastFixed)
.sort((a, b) => {
const orderA = columnOrder?.indexOf(a) ?? -1;
const orderB = columnOrder?.indexOf(b) ?? -1;
@@ -195,7 +220,8 @@ export class DialogDataTableSettings extends LitElement {
this._columnOrder = columnOrder;
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
const reportedOrder = columnOrder.concat(this._lastFixedKeys);
this._params!.onUpdate(reportedOrder, this._hiddenColumns);
}
private _toggle(ev) {
@@ -276,7 +302,8 @@ export class DialogDataTableSettings extends LitElement {
this._hiddenColumns = hidden;
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
const reportedOrder = this._columnOrder.concat(this._lastFixedKeys);
this._params!.onUpdate(reportedOrder, this._hiddenColumns);
}
private _reset() {
+13 -21
View File
@@ -86,6 +86,7 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
flex?: number;
forceLTR?: boolean;
hidden?: boolean;
lastFixed?: boolean;
}
export type ClonedDataTableColumnData = Omit<DataTableColumnData, "title"> & {
@@ -117,8 +118,6 @@ export class HaDataTable extends LitElement {
@property({ type: Boolean }) public clickable = false;
@property({ attribute: "has-fab", type: Boolean }) public hasFab = false;
/**
* Add an extra row at the bottom of the data table
* @type {TemplateResult}
@@ -135,9 +134,6 @@ export class HaDataTable extends LitElement {
@property({ attribute: false }) public searchLabel?: string;
@property({ type: Boolean, attribute: "no-label-float" })
public noLabelFloat? = false;
@property({ type: String }) public filter = "";
@property({ attribute: false }) public groupColumn?: string;
@@ -359,6 +355,11 @@ export class HaDataTable extends LitElement {
.sort((a, b) => {
const orderA = columnOrder!.indexOf(a);
const orderB = columnOrder!.indexOf(b);
const fixedA = Boolean(columns[a].lastFixed);
const fixedB = Boolean(columns[b].lastFixed);
if (fixedA !== fixedB) {
return fixedA ? 1 : -1;
}
if (orderA !== orderB) {
if (orderA === -1) {
return 1;
@@ -394,7 +395,6 @@ export class HaDataTable extends LitElement {
.hass=${this.hass}
@value-changed=${this._handleSearchChange}
.label=${this.searchLabel}
.noLabelFloat=${this.noLabelFloat}
></search-input>
</div>
`
@@ -428,9 +428,9 @@ export class HaDataTable extends LitElement {
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleHeaderRowCheckboxClick}
.indeterminate=${this._checkedRows.length &&
.indeterminate=${!!this._checkedRows.length &&
this._checkedRows.length !== this._checkableRowsCount}
.checked=${this._checkedRows.length &&
.checked=${!!this._checkedRows.length &&
this._checkedRows.length === this._checkableRowsCount}
>
</ha-checkbox>
@@ -517,7 +517,6 @@ export class HaDataTable extends LitElement {
this._filteredData,
localize,
this.appendRow,
this.hasFab,
this.groupColumn,
this.groupOrder,
this._collapsedGroups,
@@ -714,14 +713,13 @@ export class HaDataTable extends LitElement {
data: DataTableRowData[],
localize: LocalizeFunc,
appendRow,
hasFab: boolean,
groupColumn: string | undefined,
groupOrder: string[] | undefined,
collapsedGroups: string[],
sortColumn: string | undefined,
sortDirection: SortingDirection
) => {
if (appendRow || hasFab || groupColumn) {
if (appendRow || groupColumn) {
let items = [...data];
if (groupColumn) {
@@ -811,13 +809,11 @@ export class HaDataTable extends LitElement {
items.push({ append: true, selectable: false, content: appendRow });
}
if (hasFab) {
items.push({ empty: true });
}
items.push({ empty: true });
return items;
}
return data;
return [...data, { empty: true }];
}
);
@@ -869,7 +865,6 @@ export class HaDataTable extends LitElement {
this._filteredData,
this.localizeFunc || this.hass.localize,
this.appendRow,
this.hasFab,
this.groupColumn,
this.groupOrder,
this._collapsedGroups,
@@ -1087,11 +1082,8 @@ export class HaDataTable extends LitElement {
}
.mdc-data-table__row.empty-row {
height: max(
var(
--data-table-empty-row-height,
var(--data-table-row-height, 52px)
),
height: var(
--data-table-empty-row-height,
var(--safe-area-inset-bottom, 0px)
);
}
@@ -6,6 +6,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-generic-picker";
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
import type { PickerValueRenderer } from "../ha-picker-field";
@customElement("ha-entity-attribute-picker")
class HaEntityAttributePicker extends LitElement {
@@ -94,12 +95,19 @@ class HaEntityAttributePicker extends LitElement {
.helper=${this.helper}
.allowCustomValue=${this.allowCustomValue}
.getItems=${this._getItems}
.valueRenderer=${this._valueRenderer}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>
`;
}
private _valueRenderer: PickerValueRenderer = (value: string) => {
const items = this._getItems();
const item = items.find((option) => option.id === value);
return html`<span slot="headline">${item?.primary ?? value}</span>`;
};
private _valueChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value;
+1 -1
View File
@@ -164,7 +164,7 @@ export class HaEntityToggle extends LitElement {
min-width: 38px;
}
ha-icon-button {
--mdc-icon-button-size: 40px;
--ha-icon-button-size: 40px;
color: var(--ha-icon-button-inactive-color, var(--primary-text-color));
transition: color 0.5s;
}
+2
View File
@@ -15,6 +15,7 @@ import { iconColorCSS } from "../../common/style/icon_color_css";
import { cameraUrlWithWidthHeight } from "../../data/camera";
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
import type { HomeAssistant } from "../../types";
import { addBrandsAuth } from "../../util/brands-url";
import "../ha-state-icon";
@customElement("state-badge")
@@ -137,6 +138,7 @@ export class StateBadge extends LitElement {
let imageUrl =
stateObj.attributes.entity_picture_local ||
stateObj.attributes.entity_picture;
imageUrl = addBrandsAuth(imageUrl);
if (this.hass) {
imageUrl = this.hass.hassUrl(imageUrl);
}
+10
View File
@@ -31,9 +31,18 @@ type DialogSheetMode = "dialog" | "bottom-sheet";
* @slot footer - Dialog/sheet footer content.
*
* @cssprop --ha-dialog-surface-background - Dialog/sheet background color.
* @cssprop --ha-dialog-surface-backdrop-filter - Dialog/sheet backdrop filter.
* @cssprop --dialog-box-shadow - Dialog box shadow (dialog mode only).
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface (dialog mode only).
* @cssprop --ha-dialog-show-duration - Show animation duration (dialog mode only).
* @cssprop --ha-dialog-hide-duration - Hide animation duration (dialog mode only).
* @cssprop --ha-dialog-scrim-backdrop-filter - Dialog/sheet scrim backdrop filter.
* @cssprop --dialog-backdrop-filter - Dialog/sheet scrim backdrop filter (legacy).
* @cssprop --mdc-dialog-scrim-color - Dialog/sheet scrim color (legacy).
* @cssprop --ha-bottom-sheet-surface-background - Bottom sheet background color (sheet mode only).
* @cssprop --ha-bottom-sheet-surface-backdrop-filter - Bottom sheet backdrop filter (sheet mode only).
* @cssprop --ha-bottom-sheet-scrim-backdrop-filter - Bottom sheet scrim backdrop filter (sheet mode only).
* @cssprop --ha-bottom-sheet-scrim-color - Bottom sheet scrim color (sheet mode only).
*
* @attr {boolean} open - Controls the dialog/sheet open state.
* @attr {("alert"|"standard")} type - Dialog type (dialog mode only). Defaults to "standard".
@@ -220,6 +229,7 @@ export class HaAdaptiveDialog extends LitElement {
return [
css`
ha-bottom-sheet {
--ha-bottom-sheet-border-radius: var(--ha-border-radius-2xl);
--ha-bottom-sheet-surface-background: var(
--ha-dialog-surface-background,
var(--card-background-color, var(--ha-color-surface-default))
+1 -1
View File
@@ -135,7 +135,7 @@ class HaAlert extends LitElement {
}
.action ha-icon-button {
--mdc-theme-primary: var(--primary-text-color);
--mdc-icon-button-size: 36px;
--ha-icon-button-size: 36px;
}
.issue-type.info > .icon {
color: var(--info-color);
+5 -5
View File
@@ -672,11 +672,11 @@ export class HaAssistChat extends LitElement {
--markdown-code-background-color: var(--primary-background-color);
--markdown-code-text-color: var(--primary-text-color);
--markdown-list-indent: 1.15em;
&:not(:has(ha-markdown-element)) {
min-height: 1lh;
min-width: 1lh;
flex-shrink: 0;
}
}
ha-markdown:not(:has(ha-markdown-element)) {
min-height: 1lh;
min-width: 1lh;
flex-shrink: 0;
}
.bouncer {
width: 48px;
+14 -3
View File
@@ -1,13 +1,13 @@
import { mdiClose } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, queryAll } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import type { HaSelectSelectEvent } from "./ha-select";
import "./ha-icon-button";
import "./ha-input-helper-text";
import "./ha-select";
import type { HaSelectSelectEvent } from "./ha-select";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@@ -133,6 +133,17 @@ export class HaBaseTimeInput extends LitElement {
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
@queryAll("ha-textfield") private _inputs?: HaTextField[];
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
return this._inputs?.every((input) => input.reportValidity()) ?? true;
}
protected render(): TemplateResult {
return html`
${this.label
@@ -368,7 +379,7 @@ export class HaBaseTimeInput extends LitElement {
}
ha-icon-button {
position: relative;
--mdc-icon-button-size: 36px;
--ha-icon-button-size: 36px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
+220 -15
View File
@@ -2,6 +2,7 @@ import "@home-assistant/webawesome/dist/components/drawer/drawer";
import type WaDrawer from "@home-assistant/webawesome/dist/components/drawer/drawer";
import { css, html, LitElement, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type { HASSDomEvent } from "../common/dom/fire_event";
import { fireEvent } from "../common/dom/fire_event";
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
@@ -11,6 +12,40 @@ import { isIosApp } from "../util/is_ios";
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
const SWIPE_LOCKED_COMPONENTS = new Set([
"ha-control-slider",
"ha-slider",
"ha-control-switch",
"ha-control-circular-slider",
"ha-hs-color-picker",
"ha-map",
"ha-more-info-control-select-container",
"ha-filter-chip",
]);
const SWIPE_LOCKED_CLASSES = new Set(["volume-slider-container", "forecast"]);
/**
* Home Assistant bottom sheet component.
*
* @element ha-bottom-sheet
* @extends {LitElement}
*
* @cssprop --ha-bottom-sheet-height - Preferred height of the bottom sheet.
* @cssprop --ha-bottom-sheet-max-height - Maximum height of the bottom sheet.
* @cssprop --ha-bottom-sheet-max-width - Maximum width of the bottom sheet.
* @cssprop --ha-bottom-sheet-border-radius - Top border radius of the bottom sheet.
* @cssprop --ha-bottom-sheet-surface-background - Bottom sheet background color.
* @cssprop --ha-bottom-sheet-surface-backdrop-filter - Bottom sheet surface backdrop filter.
* @cssprop --ha-bottom-sheet-scrim-backdrop-filter - Bottom sheet scrim backdrop filter.
* @cssprop --ha-bottom-sheet-scrim-color - Bottom sheet scrim color.
*
* @cssprop --ha-dialog-surface-background - Bottom sheet background color fallback.
* @cssprop --ha-dialog-surface-backdrop-filter - Bottom sheet surface backdrop filter fallback.
* @cssprop --ha-dialog-scrim-backdrop-filter - Bottom sheet scrim backdrop filter fallback.
* @cssprop --dialog-backdrop-filter - Bottom sheet scrim backdrop filter legacy fallback.
* @cssprop --mdc-dialog-scrim-color - Bottom sheet scrim color legacy fallback.
*/
@customElement("ha-bottom-sheet")
export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
@property({ attribute: false }) public hass?: HomeAssistant;
@@ -31,6 +66,8 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
@state() private _drawerOpen = false;
@state() private _sliderInteractionActive = false;
@query("#drawer") private _drawer!: HTMLElement;
@query("#body") private _bodyElement!: HTMLDivElement;
@@ -78,26 +115,58 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
fireEvent(this, "after-show");
};
private _handleAfterHide = () => {
this.open = false;
this._drawerOpen = false;
fireEvent(this, "closed");
private _handleSliderInteractionStart = () => {
this._sliderInteractionActive = true;
};
private _handleSliderInteractionStop = () => {
this._sliderInteractionActive = false;
};
private _handleAfterHide = (ev: CustomEvent<{ source: Element }>) => {
if (this._sliderInteractionActive) {
this._drawerOpen = true;
this.open = true;
return;
}
if (ev.eventPhase === Event.AT_TARGET) {
this.open = false;
this._drawerOpen = false;
fireEvent(this, "closed");
}
};
private _handleHide = (ev: CustomEvent<{ source: Element }>) => {
if (
this.preventScrimClose &&
this._escapePressed &&
ev.detail.source === (ev.target as WaDrawer).drawer
) {
// Ignore bubbled wa-hide events from nested drawers (e.g., picker bottom sheet)
if (ev.eventPhase !== Event.AT_TARGET) {
return;
}
const sourceIsDrawer = ev.detail.source === (ev.target as WaDrawer).drawer;
if (this._sliderInteractionActive) {
ev.preventDefault();
this._drawerOpen = true;
this.open = true;
this._escapePressed = false;
return;
}
if (this.preventScrimClose && this._escapePressed && sourceIsDrawer) {
ev.preventDefault();
}
this._escapePressed = false;
};
private _handleKeyDown = (ev: KeyboardEvent) => {
if (ev.key === "Escape") {
this._escapePressed = true;
if (this.preventScrimClose) {
ev.preventDefault();
}
ev.stopPropagation();
(ev.currentTarget as WaDrawer).open = false;
}
};
@@ -116,6 +185,24 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
}
};
connectedCallback() {
super.connectedCallback();
this.addEventListener(
"slider-interaction-start",
this._handleSliderInteractionStart,
{
capture: true,
}
);
this.addEventListener(
"slider-interaction-stop",
this._handleSliderInteractionStop,
{
capture: true,
}
);
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("open")) {
@@ -141,6 +228,9 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
without-header
@touchstart=${this._handleTouchStart}
>
<div class="handle-wrapper" aria-hidden="true">
<div class="handle"></div>
</div>
<slot name="header"></slot>
<div class="content-wrapper">
<div id="body" class="body ha-scrollbar">
@@ -158,17 +248,33 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
return;
}
// Check if any element inside drawer in the composed path has scrollTop > 0
for (const path of ev.composedPath()) {
const el = path as HTMLElement;
if (el === this._drawer) {
const path = ev.composedPath();
for (const target of path) {
if (target === this._drawer) {
break;
}
if (el.scrollTop > 0) {
if (!(target instanceof HTMLElement)) {
continue;
}
if (
// Check if any element inside drawer in the composed path has scrollTop > 0 (list)
target.scrollTop > 0 ||
// Check if the element is a swipe locked component or has a swipe locked class
SWIPE_LOCKED_COMPONENTS.has(target.localName) ||
Array.from(target.classList).some((cls) =>
SWIPE_LOCKED_CLASSES.has(cls)
)
) {
return;
}
}
// Stop propagation so parent bottom sheets don't also start tracking
// this gesture (same pattern as _handleKeyDown for Escape)
ev.stopPropagation();
this._startResizing(ev.touches[0].clientY);
};
@@ -264,6 +370,20 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener(
"slider-interaction-start",
this._handleSliderInteractionStart,
{
capture: true,
}
);
this.removeEventListener(
"slider-interaction-stop",
this._handleSliderInteractionStop,
{
capture: true,
}
);
this._unregisterResizeHandlers();
this._isDragging = false;
}
@@ -286,9 +406,42 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
transform: var(--dialog-transform);
transition: var(--dialog-transition);
}
@media (prefers-reduced-motion: reduce) {
wa-drawer {
--wa-color-surface-raised: transparent;
--spacing: 0;
--size: var(--ha-bottom-sheet-height, auto);
--show-duration: 1ms;
--hide-duration: 1ms;
}
wa-drawer::part(dialog) {
transition: 1ms;
}
}
wa-drawer::part(dialog)::backdrop {
-webkit-backdrop-filter: var(
--ha-bottom-sheet-scrim-backdrop-filter,
var(
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter, none)
)
);
backdrop-filter: var(
--ha-bottom-sheet-scrim-backdrop-filter,
var(
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter, none)
)
);
background-color: var(
--ha-bottom-sheet-scrim-color,
var(--mdc-dialog-scrim-color, none)
);
}
wa-drawer::part(body) {
max-width: var(--ha-bottom-sheet-max-width);
width: 100%;
position: relative;
border-top-left-radius: var(
--ha-bottom-sheet-border-radius,
var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
@@ -299,7 +452,18 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
);
background-color: var(
--ha-bottom-sheet-surface-background,
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
var(
--ha-dialog-surface-background,
var(--card-background-color, var(--ha-color-surface-default))
)
);
-webkit-backdrop-filter: var(
--ha-bottom-sheet-surface-backdrop-filter,
var(--ha-dialog-surface-backdrop-filter, none)
);
backdrop-filter: var(
--ha-bottom-sheet-surface-backdrop-filter,
var(--ha-dialog-surface-backdrop-filter, none)
);
padding: var(
--ha-bottom-sheet-padding,
@@ -311,6 +475,35 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
display: flex;
flex-direction: column;
}
:host([prevent-scrim-close]) .handle-wrapper {
display: none;
}
.handle-wrapper {
position: absolute;
top: 0;
inset-inline-start: 0;
width: 100%;
padding-bottom: 2px;
display: flex;
justify-content: center;
align-items: center;
pointer-events: none;
z-index: 1;
}
.handle-wrapper .handle {
height: 16px;
width: 200px;
display: flex;
justify-content: center;
align-items: center;
}
.handle-wrapper .handle::after {
content: "";
border-radius: var(--ha-border-radius-md);
height: 4px;
background: var(--ha-bottom-sheet-handle-color, var(--divider-color));
width: 40px;
}
.content-wrapper {
position: relative;
flex: 1;
@@ -360,6 +553,18 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
}
declare global {
interface HASSDomEvents {
"slider-interaction-start": undefined;
"slider-interaction-stop": undefined;
}
interface HTMLElementEventMap {
"slider-interaction-start": HASSDomEvent<
HASSDomEvents["slider-interaction-start"]
>;
"slider-interaction-stop": HASSDomEvent<
HASSDomEvents["slider-interaction-stop"]
>;
}
interface HTMLElementTagNameMap {
"ha-bottom-sheet": HaBottomSheet;
}
+26 -7
View File
@@ -42,7 +42,7 @@ export class HaButton extends Button {
Button.styles,
css`
:host {
--wa-form-control-padding-inline: 16px;
--wa-form-control-padding-inline: var(--ha-space-4);
--wa-font-weight-action: var(--ha-font-weight-medium);
--wa-form-control-border-radius: var(
--ha-button-border-radius,
@@ -68,7 +68,7 @@ export class HaButton extends Button {
var(--button-height, 32px)
);
font-size: var(--wa-font-size-s, var(--ha-font-size-m));
--wa-form-control-padding-inline: 12px;
--wa-form-control-padding-inline: var(--ha-space-3);
}
:host([variant="brand"]) {
@@ -84,6 +84,9 @@ export class HaButton extends Button {
--button-color-fill-loud-hover: var(
--ha-color-fill-primary-loud-hover
);
--button-color-fill-quiet-active: var(
--ha-color-fill-primary-quiet-active
);
}
:host([variant="neutral"]) {
@@ -99,6 +102,9 @@ export class HaButton extends Button {
--button-color-fill-loud-hover: var(
--ha-color-fill-neutral-loud-hover
);
--button-color-fill-quiet-active: var(
--ha-color-fill-neutral-normal-active
);
}
:host([variant="success"]) {
@@ -114,6 +120,9 @@ export class HaButton extends Button {
--button-color-fill-loud-hover: var(
--ha-color-fill-success-loud-hover
);
--button-color-fill-quiet-active: var(
--ha-color-fill-success-quiet-active
);
}
:host([variant="warning"]) {
@@ -129,6 +138,9 @@ export class HaButton extends Button {
--button-color-fill-loud-hover: var(
--ha-color-fill-warning-loud-hover
);
--button-color-fill-quiet-active: var(
--ha-color-fill-warning-quiet-active
);
}
:host([variant="danger"]) {
@@ -144,6 +156,9 @@ export class HaButton extends Button {
--button-color-fill-loud-hover: var(
--ha-color-fill-danger-loud-hover
);
--button-color-fill-quiet-active: var(
--ha-color-fill-danger-quiet-active
);
}
:host([appearance~="plain"]) .button {
@@ -187,6 +202,10 @@ export class HaButton extends Button {
background-color: var(--ha-color-fill-disabled-normal-resting);
color: var(--ha-color-on-disabled-normal);
}
:host([appearance~="plain"])
.button:not(.disabled):not(.loading):active {
background-color: var(--button-color-fill-quiet-active);
}
:host([appearance~="accent"]) .button {
background-color: var(
@@ -212,21 +231,21 @@ export class HaButton extends Button {
}
slot[name="start"]::slotted(*) {
margin-inline-end: 4px;
margin-inline-end: var(--ha-space-1);
}
slot[name="end"]::slotted(*) {
margin-inline-start: 4px;
margin-inline-start: var(--ha-space-1);
}
.button.has-start {
padding-inline-start: 8px;
padding-inline-start: var(--ha-space-2);
}
.button.has-end {
padding-inline-end: 8px;
padding-inline-end: var(--ha-space-2);
}
.label {
overflow: hidden;
overflow: var(--ha-button-label-overflow, hidden);
text-overflow: ellipsis;
padding: var(--ha-space-1) 0;
}
+14 -3
View File
@@ -2,6 +2,7 @@ import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { styleMap } from "lit/directives/style-map";
import { STATE_RUNNING } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { computeStateName } from "../common/entity/compute_state_name";
import { supportsFeature } from "../common/entity/supports-feature";
@@ -58,12 +59,22 @@ export class HaCameraStream extends LitElement {
@state() private _webRtcStreams?: { hasAudio: boolean; hasVideo: boolean };
public willUpdate(changedProps: PropertyValues): void {
if (
const entityChanged =
changedProps.has("stateObj") &&
this.stateObj &&
(changedProps.get("stateObj") as CameraEntity | undefined)?.entity_id !==
this.stateObj.entity_id
) {
this.stateObj.entity_id;
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const backendStarted =
changedProps.has("hass") &&
this.hass &&
this.stateObj &&
oldHass &&
this.hass.config.state === STATE_RUNNING &&
oldHass.config?.state !== STATE_RUNNING;
if (entityChanged || backendStarted) {
this._getCapabilities();
this._getPosterUrl();
}
+32 -4
View File
@@ -84,6 +84,9 @@ export class HaCodeEditor extends ReactiveElement {
@property({ type: Boolean, attribute: "disable-fullscreen" })
public disableFullscreen = false;
@property({ type: Boolean, attribute: "in-dialog" })
public inDialog = false;
@property({ type: Boolean, attribute: "has-toolbar" })
public hasToolbar = true;
@@ -132,6 +135,7 @@ export class HaCodeEditor extends ReactiveElement {
public connectedCallback() {
super.connectedCallback();
this.classList.toggle("in-dialog", this.inDialog);
// Force update on reconnection so editor is recreated
if (this.hasUpdated) {
this.requestUpdate();
@@ -150,6 +154,7 @@ export class HaCodeEditor extends ReactiveElement {
}
public disconnectedCallback() {
fireEvent(this, "dialog-set-fullscreen", false);
super.disconnectedCallback();
this.removeEventListener("keydown", stopPropagation);
this.removeEventListener("keydown", this._handleKeyDown);
@@ -216,6 +221,9 @@ export class HaCodeEditor extends ReactiveElement {
if (changedProps.has("error")) {
this.classList.toggle("error-state", this.error);
}
if (changedProps.has("inDialog")) {
this.classList.toggle("in-dialog", this.inDialog);
}
if (changedProps.has("_isFullscreen")) {
this.classList.toggle("fullscreen", this._isFullscreen);
this._updateToolbarButtons();
@@ -434,10 +442,19 @@ export class HaCodeEditor extends ReactiveElement {
private _updateFullscreenState(
fullscreen: boolean = this._isFullscreen
): boolean {
const previousFullscreen = this._isFullscreen;
this.classList.toggle("in-dialog", this.inDialog);
// 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;
if (previousFullscreen !== this._isFullscreen) {
fireEvent(this, "dialog-set-fullscreen", this._isFullscreen);
}
// Return whether successfully in requested state
return this._isFullscreen === fullscreen;
}
@@ -846,10 +863,10 @@ export class HaCodeEditor extends ReactiveElement {
:host(.fullscreen) {
position: fixed !important;
top: calc(var(--header-height, 56px) + 8px) !important;
left: 8px !important;
right: 8px !important;
bottom: 8px !important;
top: calc(var(--header-height, 56px) + var(--ha-space-2)) !important;
left: var(--ha-space-2) !important;
right: var(--ha-space-2) !important;
bottom: var(--ha-space-2) !important;
z-index: 6;
border-radius: var(--ha-border-radius-lg) !important;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3) !important;
@@ -867,6 +884,17 @@ export class HaCodeEditor extends ReactiveElement {
display: block !important;
}
:host(.in-dialog.fullscreen) {
position: absolute !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
padding: 0 !important;
}
:host(.hasToolbar) .cm-editor {
padding-top: var(--code-editor-toolbar-height);
}
+2 -1
View File
@@ -33,6 +33,7 @@ export class HaControlButton extends LitElement {
--control-button-background-color: var(--disabled-color);
--control-button-background-opacity: 0.2;
--control-button-border-radius: var(--ha-border-radius-md);
--control-button-font-weight: var(--ha-font-weight-medium);
--control-button-padding: 8px;
--mdc-icon-size: 20px;
--ha-ripple-color: var(--secondary-text-color);
@@ -59,7 +60,7 @@ export class HaControlButton extends LitElement {
box-sizing: border-box;
line-height: inherit;
font-family: var(--ha-font-family-body);
font-weight: var(--ha-font-weight-medium);
font-weight: var(--control-button-font-weight);
outline: none;
overflow: hidden;
background: none;
+17 -33
View File
@@ -1,11 +1,9 @@
import { mdiMenuDown } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import type { HomeAssistant } from "../types";
import "./ha-attribute-icon";
import "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-icon";
@@ -16,17 +14,10 @@ export interface SelectOption {
value: string;
iconPath?: string;
icon?: string;
attributeIcon?: {
stateObj: HassEntity;
attribute: string;
attributeValue?: string;
};
}
@customElement("ha-control-select-menu")
export class HaControlSelectMenu extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, attribute: "show-arrow" })
public showArrow = false;
@@ -47,6 +38,9 @@ export class HaControlSelectMenu extends LitElement {
@property({ attribute: false }) public options: SelectOption[] = [];
@property({ attribute: false })
public renderIcon?: (value: string) => TemplateResult<1> | typeof nothing;
@query("button") private _triggerButton!: HTMLButtonElement;
public override render() {
@@ -94,14 +88,8 @@ export class HaControlSelectMenu extends LitElement {
? html`<ha-svg-icon slot="icon" .path=${option.iconPath}></ha-svg-icon>`
: option.icon
? html`<ha-icon slot="icon" .icon=${option.icon}></ha-icon>`
: option.attributeIcon
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${option.attributeIcon.stateObj}
.attribute=${option.attributeIcon.attribute}
.attributeValue=${option.attributeIcon.attributeValue}
></ha-attribute-icon>`
: this.renderIcon
? html`<span slot="icon">${this.renderIcon(option.value)}</span>`
: nothing}
${option.label}</ha-dropdown-item
>`;
@@ -119,24 +107,20 @@ export class HaControlSelectMenu extends LitElement {
}
private _renderIcon() {
const { iconPath, icon, attributeIcon } =
this.getValueObject(this.options, this.value) ?? {};
const value = this.getValueObject(this.options, this.value);
const defaultIcon = this.querySelector("[slot='icon']");
return html`
<div class="icon">
${iconPath
? html`<ha-svg-icon slot="icon" .path=${iconPath}></ha-svg-icon>`
: icon
? html`<ha-icon slot="icon" .icon=${icon}></ha-icon>`
: attributeIcon
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${attributeIcon.stateObj}
.attribute=${attributeIcon.attribute}
.attributeValue=${attributeIcon.attributeValue}
></ha-attribute-icon>`
${value?.iconPath
? html`<ha-svg-icon
slot="icon"
.path=${value.iconPath}
></ha-svg-icon>`
: value?.icon
? html`<ha-icon slot="icon" .icon=${value.icon}></ha-icon>`
: this.renderIcon && this.value
? this.renderIcon(this.value)
: defaultIcon
? html`<slot name="icon"></slot>`
: nothing}
@@ -172,12 +156,12 @@ export class HaControlSelectMenu extends LitElement {
font-size: var(--ha-font-size-m);
line-height: 1.4;
width: auto;
color: var(--primary-text-color);
-webkit-tap-highlight-color: transparent;
}
.select-anchor {
border: none;
text-align: left;
color: var(--primary-text-color);
height: var(--control-select-menu-height);
padding: var(--control-select-menu-padding);
overflow: hidden;
+1 -1
View File
@@ -95,7 +95,7 @@ export class HaCopyTextfield extends LitElement {
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--ha-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
+8 -1
View File
@@ -1,7 +1,7 @@
import { mdiCalendar } from "@mdi/js";
import type { HassConfig } from "home-assistant-js-websocket";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
import { formatDateNumeric } from "../common/datetime/format_date";
import { fireEvent } from "../common/dom/fire_event";
@@ -9,6 +9,7 @@ import { TimeZone } from "../data/translation";
import type { HomeAssistant } from "../types";
import "./ha-svg-icon";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
const loadDatePickerDialog = () => import("./ha-dialog-date-picker");
@@ -52,6 +53,12 @@ export class HaDateInput extends LitElement {
@property({ attribute: "can-clear", type: Boolean }) public canClear = false;
@query("ha-textfield", true) private _input?: HaTextField;
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
render() {
return html`<ha-textfield
.label=${this.label}
+22 -11
View File
@@ -93,6 +93,8 @@ export class HaDateRangePicker extends LitElement {
| "center"
| "inline";
@state() private _calcedVerticalOpeningDirection?: "up" | "down";
protected willUpdate(changedProps: PropertyValues) {
if (
(!this.hasUpdated && this.ranges === undefined) ||
@@ -134,7 +136,9 @@ export class HaDateRangePicker extends LitElement {
opening-direction=${ifDefined(
this.openingDirection || this._calcedOpeningDirection
)}
opens-vertical=${ifDefined(this.verticalOpeningDirection)}
opens-vertical=${ifDefined(
this.verticalOpeningDirection || this._calcedVerticalOpeningDirection
)}
first-day=${firstWeekdayIndex(this.hass.locale)}
language=${this.hass.locale.language}
@change=${this._handleChange}
@@ -328,17 +332,24 @@ export class HaDateRangePicker extends LitElement {
private _handleClick() {
// calculate opening direction if not set
if (!this._dateRangePicker.open && !this.openingDirection) {
const datePickerPosition = this.getBoundingClientRect().x;
let opens: "right" | "left" | "center" | "inline";
if (datePickerPosition > (2 * window.innerWidth) / 3) {
opens = "left";
} else if (datePickerPosition < window.innerWidth / 3) {
opens = "right";
} else {
opens = "center";
if (!this._dateRangePicker.open) {
if (!this.openingDirection) {
const datePickerPosition = this.getBoundingClientRect().x;
let opens: "right" | "left" | "center" | "inline";
if (datePickerPosition > (2 * window.innerWidth) / 3) {
opens = "left";
} else if (datePickerPosition < window.innerWidth / 3) {
opens = "right";
} else {
opens = "center";
}
this._calcedOpeningDirection = opens;
}
if (!this.verticalOpeningDirection) {
const rect = this.getBoundingClientRect();
this._calcedVerticalOpeningDirection =
rect.top > window.innerHeight / 2 ? "up" : "down";
}
this._calcedOpeningDirection = opens;
}
}
+117 -37
View File
@@ -10,7 +10,9 @@ import {
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import type { HASSDomEvent } from "../common/dom/fire_event";
import { fireEvent } from "../common/dom/fire_event";
import { withViewTransition } from "../common/util/view-transition";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
@@ -20,6 +22,8 @@ import "./ha-icon-button";
export type DialogWidth = "small" | "medium" | "large" | "full";
type DialogHideEvent = CustomEvent<{ source?: Element }>;
/**
* Home Assistant dialog component
*
@@ -50,7 +54,12 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
* @cssprop --ha-dialog-show-duration - Show animation duration.
* @cssprop --ha-dialog-hide-duration - Hide animation duration.
* @cssprop --ha-dialog-surface-background - Dialog background color.
* @cssprop --ha-dialog-surface-backdrop-filter - Dialog backdrop filter.
* @cssprop --dialog-box-shadow - Dialog box shadow.
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface.
* @cssprop --ha-dialog-scrim-backdrop-filter - Dialog scrim backdrop filter.
* @cssprop --dialog-backdrop-filter - Dialog scrim backdrop filter (legacy).
* @cssprop --mdc-dialog-scrim-color - Dialog scrim color (legacy).
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
*
* @attr {boolean} open - Controls the dialog open state.
@@ -120,6 +129,14 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
private _escapePressed = false;
public connectedCallback(): void {
super.connectedCallback();
this.addEventListener(
"dialog-set-fullscreen",
this._handleFullscreenChanged as EventListener
);
}
protected get scrollableElement(): HTMLElement | null {
return this.bodyContainer;
}
@@ -187,7 +204,10 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
`;
}
private _handleShow = async () => {
private _handleShow = async (ev: Event) => {
if (ev.eventPhase !== Event.AT_TARGET) {
return;
}
this._open = true;
fireEvent(this, "opened");
@@ -213,22 +233,46 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
});
};
private _handleAfterShow = () => {
private _handleAfterShow = (ev: Event) => {
if (ev.eventPhase !== Event.AT_TARGET) {
return;
}
fireEvent(this, "after-show");
};
private _handleAfterHide = (ev: CustomEvent<{ source: Element }>) => {
private _handleAfterHide = (ev: DialogHideEvent) => {
if (ev.eventPhase === Event.AT_TARGET) {
this._open = false;
this._setFullscreen(false);
fireEvent(this, "closed");
}
};
public disconnectedCallback(): void {
this.removeEventListener(
"dialog-set-fullscreen",
this._handleFullscreenChanged as EventListener
);
this._setFullscreen(false);
super.disconnectedCallback();
this._open = false;
}
private _handleFullscreenChanged(ev: HASSDomEvent<boolean>): void {
if (!this._open) {
this._setFullscreen(ev.detail);
return;
}
withViewTransition(() => {
this._setFullscreen(ev.detail);
});
}
private _setFullscreen(fullscreen: boolean): void {
this.toggleAttribute("fullscreen", fullscreen);
}
@eventOptions({ passive: true })
private _handleBodyScroll(ev: Event) {
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
@@ -237,17 +281,21 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
private _handleKeyDown(ev: KeyboardEvent) {
if (ev.key === "Escape") {
this._escapePressed = true;
if (this.preventScrimClose) {
ev.preventDefault();
}
ev.stopPropagation();
(ev.currentTarget as WaDialog).open = false;
}
}
private _handleHide(ev: CustomEvent<{ source: Element }>) {
if (
this.preventScrimClose &&
this._escapePressed &&
ev.detail.source === (ev.target as WaDialog).dialog
) {
private _handleHide(ev: DialogHideEvent) {
const sourceIsDialog = ev.detail?.source === (ev.target as WaDialog).dialog;
if (this.preventScrimClose && this._escapePressed && sourceIsDialog) {
ev.preventDefault();
}
this._escapePressed = false;
}
@@ -265,10 +313,6 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
--spacing: var(--dialog-content-padding, var(--ha-space-6));
--show-duration: var(--ha-dialog-show-duration, 200ms);
--hide-duration: var(--ha-dialog-hide-duration, 200ms);
--ha-dialog-surface-background: var(
--card-background-color,
var(--ha-color-surface-default)
);
--wa-color-surface-raised: var(
--ha-dialog-surface-background,
var(--card-background-color, var(--ha-color-surface-default))
@@ -281,8 +325,8 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
}
@media (prefers-reduced-motion: reduce) {
wa-dialog {
--show-duration: 1ms;
--hide-duration: 1ms;
--show-duration: 0ms;
--hide-duration: 0ms;
}
}
@@ -294,11 +338,34 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
}
:host([width="full"]) wa-dialog {
:host([width="full"]) wa-dialog,
:host([fullscreen]) wa-dialog {
--width: var(--full-width);
}
:host([fullscreen]) wa-dialog::part(dialog) {
min-height: var(--safe-height);
max-height: var(--safe-height);
margin-top: 0;
transform: none;
}
:host([fullscreen]) .content-wrapper {
overflow: hidden;
}
:host([fullscreen]) .body {
overflow: hidden;
padding: 0;
}
wa-dialog::part(dialog) {
-webkit-backdrop-filter: var(
--ha-dialog-surface-backdrop-filter,
none
);
backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
box-shadow: var(--dialog-box-shadow, var(--wa-shadow-l));
color: var(--primary-text-color);
min-width: var(--width, var(--full-width));
max-width: var(--width, var(--full-width));
@@ -328,32 +395,44 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
overflow: hidden;
}
wa-dialog::part(dialog)::backdrop {
-webkit-backdrop-filter: var(
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter, none)
);
backdrop-filter: var(
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter, none)
);
background-color: var(--mdc-dialog-scrim-color, none);
}
@media all and (max-width: 450px), all and (max-height: 500px) {
:host([type="standard"]) {
--ha-dialog-border-radius: 0;
}
wa-dialog {
/* Make the container fill the whole screen width and not the safe width */
--full-width: var(--ha-dialog-width-full, 100vw);
--width: var(--full-width);
}
:host([type="standard"]) wa-dialog {
/* Make the container fill the whole screen width and not the safe width */
--full-width: var(--ha-dialog-width-full, 100vw);
--width: var(--full-width);
}
wa-dialog::part(dialog) {
/* Make the dialog fill the whole screen height and not the safe height */
min-height: var(--ha-dialog-min-height, 100vh);
min-height: var(--ha-dialog-min-height, 100dvh);
max-height: var(--ha-dialog-max-height, 100vh);
max-height: var(--ha-dialog-max-height, 100dvh);
margin-top: 0;
margin-bottom: 0;
/* Use safe area as padding instead of the container size */
padding-top: var(--safe-area-inset-top);
padding-bottom: var(--safe-area-inset-bottom);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
/* Reset the transform to center the dialog */
transform: none;
}
:host([type="standard"]) wa-dialog::part(dialog) {
/* Make the dialog fill the whole screen height and not the safe height */
min-height: var(--ha-dialog-min-height, 100vh);
min-height: var(--ha-dialog-min-height, 100dvh);
max-height: var(--ha-dialog-max-height, 100vh);
max-height: var(--ha-dialog-max-height, 100dvh);
margin-top: 0;
margin-bottom: 0;
/* Use safe area as padding instead of the container size */
padding-top: var(--safe-area-inset-top);
padding-bottom: var(--safe-area-inset-bottom);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
/* Reset the transform to center the dialog */
transform: none;
}
}
@@ -440,6 +519,7 @@ declare global {
}
interface HASSDomEvents {
"dialog-set-fullscreen": boolean;
opened: undefined;
"after-show": undefined;
closed: undefined;
+3 -1
View File
@@ -186,9 +186,11 @@ export class HaDrawer extends DrawerBase {
padding-inline-start var(--ha-animation-duration-normal) ease;
}
@media (prefers-reduced-motion: reduce) {
/* Use 1ms instead of "none" so the transitionend event still fires.
The MDC drawer foundation relies on it to complete the close cycle. */
.mdc-drawer,
.mdc-drawer-app-content {
transition: none;
transition: 1ms;
}
}
`,
+36 -5
View File
@@ -1,8 +1,8 @@
import type WaButton from "@home-assistant/webawesome/dist/components/button/button";
import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdown";
import { css, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import type { HaDropdownItem } from "./ha-dropdown-item";
import type { HaIconButton } from "./ha-icon-button";
/**
* Event type for the ha-dropdown component when an item is selected.
@@ -29,16 +29,25 @@ export class HaDropdown extends Dropdown {
@property({ attribute: false }) dropdownItemTag = "ha-dropdown-item";
public get anchorElement(): HTMLButtonElement | WaButton | undefined {
public get anchorElement(): HTMLButtonElement | HaIconButton | undefined {
// @ts-ignore Allow to set an anchor element on popup
return this.popup?.anchor as HTMLButtonElement | WaButton | undefined;
return this.popup?.anchor as HTMLButtonElement | HaIconButton | undefined;
}
public set anchorElement(element: HTMLButtonElement | WaButton | undefined) {
public set anchorElement(
element: HTMLButtonElement | HaIconButton | undefined
) {
// @ts-ignore Allow to get the current anchor element from popup
if (!this.popup) {
return;
}
// @ts-ignore
if (this.popup.anchor && this.popup.anchor.localName === "ha-icon-button") {
// @ts-ignore
(this.popup.anchor as HaIconButton).selected = false;
}
// @ts-ignore Allow to get the current anchor element from popup
this.popup.anchor = element;
}
@@ -46,7 +55,7 @@ export class HaDropdown extends Dropdown {
/** Get the slotted trigger button, a <wa-button> or <button> element */
// @ts-ignore Override parent method to be able to use alternative anchor
// eslint-disable-next-line @typescript-eslint/naming-convention
private override getTrigger(): HTMLButtonElement | WaButton | null {
private override getTrigger(): HTMLButtonElement | HaIconButton | null {
if (this.anchorElement) {
return this.anchorElement;
}
@@ -54,6 +63,28 @@ export class HaDropdown extends Dropdown {
return super.getTrigger();
}
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/naming-convention
private override async showMenu() {
// @ts-ignore
await super.showMenu();
const triggerElement = this.getTrigger();
if (triggerElement && triggerElement.localName === "ha-icon-button") {
(triggerElement as HaIconButton).selected = true;
}
}
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/naming-convention
private override async hideMenu() {
const triggerElement = this.getTrigger();
if (triggerElement && triggerElement.localName === "ha-icon-button") {
(triggerElement as HaIconButton).selected = false;
}
// @ts-ignore
await super.hideMenu();
}
static get styles(): CSSResultGroup {
return [
Dropdown.styles,
+26 -9
View File
@@ -1,12 +1,12 @@
import { mdiMinusThick, mdiPlusThick } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-base-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
import "./ha-button-toggle-group";
import type { ValueChangedEvent } from "../types";
import "./ha-base-time-input";
import type { HaBaseTimeInput, TimeChangedEvent } from "./ha-base-time-input";
import "./ha-button-toggle-group";
export interface HaDurationData {
days?: number;
@@ -19,7 +19,7 @@ export interface HaDurationData {
const FIELDS = ["milliseconds", "seconds", "minutes", "hours", "days"];
@customElement("ha-duration-input")
class HaDurationInput extends LitElement {
export class HaDurationInput extends LitElement {
@property({ attribute: false }) public data?: HaDurationData;
@property() public label?: string;
@@ -37,10 +37,24 @@ class HaDurationInput extends LitElement {
@property({ attribute: "allow-negative", type: Boolean })
public allowNegative = false;
@property({ attribute: "enable-second", type: Boolean })
public enableSecond = true;
@property({ type: Boolean }) public disabled = false;
@query("ha-base-time-input", true) private _input?: HaBaseTimeInput;
private _toggleNegative = false;
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render(): TemplateResult {
return html`
<div class="row">
@@ -65,7 +79,7 @@ class HaDurationInput extends LitElement {
.autoValidate=${this.required}
.disabled=${this.disabled}
errorMessage="Required"
enable-second
.enableSecond=${this.enableSecond}
.enableMillisecond=${this.enableMillisecond}
.enableDay=${this.enableDay}
format="24"
@@ -162,9 +176,9 @@ class HaDurationInput extends LitElement {
if (value) {
value.hours ||= 0;
value.minutes ||= 0;
value.seconds ||= 0;
if ("days" in value) value.days ||= 0;
if ("seconds" in value) value.seconds ||= 0;
if ("milliseconds" in value) value.milliseconds ||= 0;
if (this.allowNegative) {
@@ -183,8 +197,11 @@ class HaDurationInput extends LitElement {
value.milliseconds %= 1000;
}
if (value.seconds > 59) {
value.minutes += Math.floor(value.seconds / 60);
if (!this.enableSecond && !value.seconds) {
// @ts-ignore
delete value.seconds;
} else if (this.enableSecond && value.seconds > 59) {
value.minutes = (value.minutes ?? 0) + Math.floor(value.seconds / 60);
value.seconds %= 60;
}
+5 -5
View File
@@ -4,14 +4,14 @@ import type { 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";
import { ensureArray } from "../common/array/ensure-array";
import { fireEvent } from "../common/dom/fire_event";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import { bytesToString } from "../util/bytes-to-string";
import "./ha-button";
import "./ha-icon-button";
import { blankBeforePercent } from "../common/translations/blank_before_percent";
import { ensureArray } from "../common/array/ensure-array";
import { bytesToString } from "../util/bytes-to-string";
import type { LocalizeFunc } from "../common/translations/localize";
declare global {
interface HASSDomEvents {
@@ -317,7 +317,7 @@ export class HaFileUpload extends LitElement {
}
ha-button {
--mdc-button-outline-color: var(--primary-color);
--mdc-icon-button-size: 24px;
--ha-icon-button-size: 24px;
}
mwc-linear-progress {
width: 100%;
+2 -2
View File
@@ -26,12 +26,12 @@ import { showCategoryRegistryDetailDialog } from "../panels/config/category/show
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-dropdown";
import type { HaDropdownSelectEvent } from "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-expansion-panel";
import "./ha-icon";
import "./ha-list";
import "./ha-list-item";
import type { HaDropdownSelectEvent } from "./ha-dropdown";
@customElement("ha-filter-categories")
export class HaFilterCategories extends SubscribeMixin(LitElement) {
@@ -317,7 +317,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
--mdc-list-item-meta-size: auto;
--mdc-list-side-padding-right: var(--ha-space-1);
--mdc-list-side-padding-left: var(--ha-space-4);
--mdc-icon-button-size: 36px;
--ha-icon-button-size: 36px;
}
ha-list-item {
--mdc-list-item-graphic-margin: var(--ha-space-4);
+9 -2
View File
@@ -1,8 +1,9 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import "./ha-form";
import "../ha-expansion-panel";
import "./ha-form";
import type { HaForm } from "./ha-form";
import type {
HaFormDataContainer,
HaFormElement,
@@ -35,6 +36,12 @@ export class HaFormExpandable extends LitElement implements HaFormElement {
key: string
) => string;
@query("ha-form", true) private _form?: HaForm;
public reportValidity(): boolean {
return this._form?.reportValidity() ?? true;
}
private _renderDescription() {
const description = this.computeHelper?.(this.schema);
return description ? html`<p>${description}</p>` : nothing;
+11 -8
View File
@@ -1,15 +1,15 @@
import type { TemplateResult, PropertyValues } from "lit";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { HaTextField } from "../ha-textfield";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../ha-textfield";
import type { HaTextField } from "../ha-textfield";
import type {
HaFormElement,
HaFormFloatData,
HaFormFloatSchema,
} from "./types";
import type { LocalizeFunc } from "../../common/translations/localize";
@customElement("ha-form-float")
export class HaFormFloat extends LitElement implements HaFormElement {
@@ -25,12 +25,15 @@ export class HaFormFloat extends LitElement implements HaFormElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-textfield") private _input?: HaTextField;
@query("ha-textfield", true) private _input?: HaTextField;
public focus() {
if (this._input) {
this._input.focus();
}
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render(): TemplateResult {
+21 -7
View File
@@ -1,14 +1,15 @@
import "./ha-form";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, queryAll } from "lit/decorators";
import type { HomeAssistant } from "../../types";
import "./ha-form";
import type { HaForm } from "./ha-form";
import type {
HaFormGridSchema,
HaFormDataContainer,
HaFormElement,
HaFormGridSchema,
HaFormSchema,
} from "./types";
import type { HomeAssistant } from "../../types";
@customElement("ha-form-grid")
export class HaFormGrid extends LitElement implements HaFormElement {
@@ -33,9 +34,22 @@ export class HaFormGrid extends LitElement implements HaFormElement {
key: string
) => string;
public async focus() {
await this.updateComplete;
this.renderRoot.querySelector("ha-form")?.focus();
@queryAll("ha-form", true) private _forms?: HaForm[];
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
const forms = this._forms ?? [];
let valid = true;
for (const form of forms) {
if (!form.reportValidity()) {
valid = false;
}
}
return valid;
}
protected updated(changedProps: PropertyValues): void {
+25 -10
View File
@@ -2,10 +2,11 @@ import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-slider";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-input-helper-text";
import "../ha-slider";
import "../ha-textfield";
import type { HaTextField } from "../ha-textfield";
import type {
@@ -13,7 +14,6 @@ import type {
HaFormIntegerData,
HaFormIntegerSchema,
} from "./types";
import type { LocalizeFunc } from "../../common/translations/localize";
@customElement("ha-form-integer")
export class HaFormInteger extends LitElement implements HaFormElement {
@@ -29,24 +29,39 @@ export class HaFormInteger extends LitElement implements HaFormElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-textfield ha-slider") private _input?:
@query("ha-textfield, ha-slider", true) private _input?:
| HaTextField
| HTMLInputElement;
private _lastValue?: HaFormIntegerData;
public focus() {
if (this._input) {
this._input.focus();
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
const showSlider = this._showSlider();
if (showSlider && this.schema.required && isNaN(Number(this.data))) {
return false;
}
if (!showSlider) {
return this._input?.reportValidity() ?? true;
}
return true;
}
protected render(): TemplateResult {
if (
private _showSlider(): boolean {
return (
this.schema.valueMin !== undefined &&
this.schema.valueMax !== undefined &&
this.schema.valueMax - this.schema.valueMin < 256
) {
);
}
protected render(): TemplateResult {
if (this._showSlider()) {
return html`
<div>
${this.label}
@@ -44,6 +44,13 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
this._dropdown?.focus();
}
public reportValidity(): boolean {
if (!this.schema.required || (this.data && this.data.length > 0)) {
return true;
}
return false;
}
protected render(): TemplateResult {
const options = Array.isArray(this.schema.options)
? this.schema.options
@@ -8,16 +8,17 @@ import type { LocalizeFunc } from "../../common/translations/localize";
import type { HomeAssistant } from "../../types";
import "../ha-button";
import "../ha-dropdown";
import type { HaDropdownSelectEvent } from "../ha-dropdown";
import "../ha-dropdown-item";
import "../ha-svg-icon";
import "./ha-form";
import type { HaForm } from "./ha-form";
import type {
HaFormDataContainer,
HaFormElement,
HaFormOptionalActionsSchema,
HaFormSchema,
} from "./types";
import type { HaDropdownSelectEvent } from "../ha-dropdown";
const NO_ACTIONS = [];
@@ -53,6 +54,11 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
this.renderRoot.querySelector("ha-form")?.focus();
}
public reportValidity(): boolean {
const form = this.renderRoot.querySelector<HaForm>("ha-form");
return form ? form.reportValidity() : true;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("data")) {
@@ -2,6 +2,7 @@ import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import "../ha-duration-input";
import type { HaDurationInput } from "../ha-duration-input";
import type { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./types";
@customElement("ha-form-positive_time_period_dict")
@@ -14,12 +15,15 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-time-input", true) private _input?: HTMLElement;
@query("ha-duration-input", true) private _input?: HaDurationInput;
public focus() {
if (this._input) {
this._input.focus();
}
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render(): TemplateResult {
+10 -3
View File
@@ -1,16 +1,16 @@
import memoizeOne from "memoize-one";
import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { SelectSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-selector/ha-selector-select";
import type {
HaFormElement,
HaFormSelectData,
HaFormSelectSchema,
} from "./types";
import type { SelectSelector } from "../../data/selector";
import "../ha-selector/ha-selector-select";
@customElement("ha-form-select")
export class HaFormSelect extends LitElement implements HaFormElement {
@@ -41,6 +41,13 @@ export class HaFormSelect extends LitElement implements HaFormElement {
})
);
public reportValidity(): boolean {
if (!this.schema.required || this.data) {
return true;
}
return false;
}
protected render(): TemplateResult {
return html`
<ha-selector-select
+13 -10
View File
@@ -3,6 +3,10 @@ import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type {
LocalizeFunc,
LocalizeKeys,
} from "../../common/translations/localize";
import "../ha-icon-button";
import "../ha-textfield";
import type { HaTextField } from "../ha-textfield";
@@ -11,10 +15,6 @@ import type {
HaFormStringData,
HaFormStringSchema,
} from "./types";
import type {
LocalizeFunc,
LocalizeKeys,
} from "../../common/translations/localize";
const MASKED_FIELDS = ["password", "secret", "token"];
@@ -37,12 +37,15 @@ export class HaFormString extends LitElement implements HaFormElement {
@state() protected unmaskedPassword = false;
@query("ha-textfield") private _input?: HaTextField;
@query("ha-textfield", true) private _input?: HaTextField;
public focus(): void {
if (this._input) {
this._input.focus();
}
static shadowRootOptions = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render(): TemplateResult {
@@ -148,7 +151,7 @@ export class HaFormString extends LitElement implements HaFormElement {
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--ha-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
+55 -18
View File
@@ -1,5 +1,5 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, ReactiveElement } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
@@ -24,7 +24,7 @@ const LOAD_ELEMENTS = {
};
const getValue = (obj, item) =>
obj ? (!item.name || item.flatten ? obj : obj[item.name]) : null;
obj ? (!item.name || item.flatten ? obj : obj[item.name]) : undefined;
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
@@ -76,22 +76,64 @@ export class HaForm extends LitElement implements HaFormElement {
return {};
}
public async focus() {
await this.updateComplete;
static shadowRootOptions: ShadowRootInit = {
mode: "open",
delegatesFocus: true,
};
public reportValidity(): boolean {
const root = this.renderRoot.querySelector(".root");
if (!root) {
return;
return true;
}
for (const child of root.children) {
if (child.tagName !== "HA-ALERT") {
if (child instanceof ReactiveElement) {
// eslint-disable-next-line no-await-in-loop
await child.updateComplete;
}
(child as HTMLElement).focus();
break;
const elements = [...root.children].filter(
(child) => child.localName !== "ha-alert"
) as (HTMLElement & { reportValidity?: () => boolean })[];
let isValid = true;
let firstInvalidElement: HTMLElement | undefined;
this.schema.forEach((item, index) => {
const element = elements[index];
if (!element) {
return;
}
let elementValid = true;
if (
"reportValidity" in element &&
typeof element.reportValidity === "function"
) {
elementValid = element.reportValidity();
} else if (
item.required &&
!(
"type" in item && ["boolean", "constant"].includes(item.type ?? "")
) &&
!(
"selector" in item &&
("boolean" in item.selector || "constant" in item.selector)
)
) {
const value = getValue(this.data, item);
elementValid = value !== undefined && value !== null && value !== "";
}
if (!elementValid) {
isValid = false;
if (!firstInvalidElement) {
firstInvalidElement = element;
}
}
});
if (firstInvalidElement) {
firstInvalidElement.focus?.();
}
return isValid;
}
protected willUpdate(changedProps: PropertyValues) {
@@ -105,11 +147,6 @@ export class HaForm extends LitElement implements HaFormElement {
}
}
static shadowRootOptions: ShadowRootInit = {
mode: "open",
delegatesFocus: true,
};
protected render(): TemplateResult {
return html`
<div class="root" part="root">
-1
View File
@@ -194,7 +194,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
.image=${this.image}
.label=${label}
.placeholder=${this.placeholder}
.helper=${this.helper}
.value=${this.value}
.valueRenderer=${this.valueRenderer}
.required=${this.required}
+4 -4
View File
@@ -1,14 +1,14 @@
import { mdiRestore } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
import "./ha-icon-button";
import { mdiRestore } from "@mdi/js";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import { conditionalClamp } from "../common/number/clamp";
import type { CardGridSize } from "../panels/lovelace/common/compute-card-grid-size";
import { DEFAULT_GRID_SIZE } from "../panels/lovelace/common/compute-card-grid-size";
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
@customElement("ha-grid-size-picker")
export class HaGridSizeEditor extends LitElement {
@@ -245,7 +245,7 @@ export class HaGridSizeEditor extends LitElement {
}
.reset {
grid-area: reset;
--mdc-icon-button-size: 36px;
--ha-icon-button-size: 36px;
}
.preview {
position: relative;
+1 -1
View File
@@ -38,7 +38,7 @@ export class HaBadge extends LitElement {
font-weight: var(--ha-heading-badge-font-weight, 400);
line-height: var(--ha-heading-badge-line-height, 20px);
letter-spacing: 0.1px;
--mdc-icon-size: 14px;
--mdc-icon-size: 16px;
}
::slotted([slot="icon"]) {
--ha-icon-display: block;
@@ -14,6 +14,14 @@ export class HaIconButtonArrowPrev extends LitElement {
@property() public label?: string;
@property() href?: string;
@property() target?: "_blank" | "_parent" | "_self" | "_top";
@property() rel?: string;
@property() download?: string;
@state() private _icon =
mainWindow.document.dir === "rtl" ? mdiArrowRight : mdiArrowLeft;
@@ -23,6 +31,10 @@ export class HaIconButtonArrowPrev extends LitElement {
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
.path=${this._icon}
.href=${this.href}
.target=${this.target}
.rel=${this.rel}
.download=${this.download}
></ha-icon-button>
`;
}
+12
View File
@@ -14,6 +14,14 @@ export class HaIconButtonNext extends LitElement {
@property() public label?: string;
@property() href?: string;
@property() target?: "_blank" | "_parent" | "_self" | "_top";
@property() rel?: string;
@property() download?: string;
@state() private _icon =
mainWindow.document.dir === "rtl" ? mdiChevronLeft : mdiChevronRight;
@@ -23,6 +31,10 @@ export class HaIconButtonNext extends LitElement {
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.next") || "Next"}
.path=${this._icon}
.href=${this.href}
.target=${this.target}
.rel=${this.rel}
.download=${this.download}
></ha-icon-button>
`;
}
+12
View File
@@ -14,6 +14,14 @@ export class HaIconButtonPrev extends LitElement {
@property() public label?: string;
@property() href?: string;
@property() target?: "_blank" | "_parent" | "_self" | "_top";
@property() rel?: string;
@property() download?: string;
@state() private _icon =
mainWindow.document.dir === "rtl" ? mdiChevronRight : mdiChevronLeft;
@@ -23,6 +31,10 @@ export class HaIconButtonPrev extends LitElement {
.disabled=${this.disabled}
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
.path=${this._icon}
.href=${this.href}
.target=${this.target}
.rel=${this.rel}
.download=${this.download}
></ha-icon-button>
`;
}
+46 -35
View File
@@ -1,3 +1,4 @@
import type { CSSResultGroup } from "lit";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
import { HaIconButton } from "./ha-icon-button";
@@ -6,41 +7,51 @@ import { HaIconButton } from "./ha-icon-button";
export class HaIconButtonToggle extends HaIconButton {
@property({ type: Boolean, reflect: true }) selected = false;
static styles = css`
:host {
position: relative;
}
mwc-icon-button {
position: relative;
transition: color 180ms ease-in-out;
}
mwc-icon-button::before {
opacity: 0;
transition: opacity 180ms ease-in-out;
background-color: var(--primary-text-color);
border-radius: var(--ha-border-radius-2xl);
height: 40px;
width: 40px;
content: "";
position: absolute;
top: -10px;
left: -10px;
bottom: -10px;
right: -10px;
margin: auto;
box-sizing: border-box;
}
:host([border-only]) mwc-icon-button::before {
background-color: transparent;
border: 2px solid var(--primary-text-color);
}
:host([selected]) mwc-icon-button {
color: var(--primary-background-color);
}
:host([selected]:not([disabled])) mwc-icon-button::before {
opacity: 1;
}
`;
static styles: CSSResultGroup = [
HaIconButton.styles,
css`
:host {
position: relative;
}
ha-button::part(base) {
position: relative;
transition: color 180ms ease-in-out;
}
ha-button::part(base)::before {
opacity: 0;
transition: opacity 180ms ease-in-out;
background-color: var(--primary-text-color);
border-radius: var(--ha-border-radius-2xl);
height: 40px;
width: 40px;
content: "";
position: absolute;
top: -10px;
left: -10px;
bottom: -10px;
right: -10px;
margin: auto;
box-sizing: border-box;
}
:host([border-only]) ha-button::part(base)::before {
background-color: transparent;
border: 2px solid var(--primary-text-color);
}
:host([selected]) ha-button::after {
opacity: 0;
}
:host([selected]) ha-button::part(base) {
color: var(--primary-background-color);
background-color: unset;
}
:host([selected]:not([disabled])) ha-button::part(base)::before {
opacity: 1;
}
::slotted(*) {
display: block;
}
`,
];
}
declare global {
+1 -1
View File
@@ -109,7 +109,7 @@ export class HaIconButtonToolbar extends LitElement {
.icon-toolbar-button {
color: var(--secondary-text-color);
--mdc-icon-button-size: var(--icon-button-toolbar-button);
--ha-icon-button-size: var(--icon-button-toolbar-button);
--mdc-icon-size: var(--icon-button-toolbar-icon);
/* Ensure button is clickable on iOS */
cursor: pointer;
+59 -17
View File
@@ -1,9 +1,8 @@
import "@material/mwc-icon-button";
import type { IconButton } from "@material/mwc-icon-button";
import type { TemplateResult } from "lit";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import "./ha-button";
import "./ha-svg-icon";
@customElement("ha-icon-button")
@@ -19,15 +18,19 @@ export class HaIconButton extends LitElement {
// These should always be set as properties, not attributes,
// so that only the <button> element gets the attribute
@property({ type: String, attribute: "aria-haspopup" })
override ariaHasPopup!: IconButton["ariaHasPopup"];
ariaHasPopup!: "false" | "true" | "menu" | "listbox" | "tree" | "grid";
@property({ attribute: "hide-title", type: Boolean }) hideTitle = false;
@query("mwc-icon-button", true) private _button?: IconButton;
@property({ type: Boolean, reflect: true }) selected = false;
public override focus() {
this._button?.focus();
}
@property() href?: string;
@property() target?: "_blank" | "_parent" | "_self" | "_top";
@property() rel?: string;
@property() download?: string;
static shadowRootOptions: ShadowRootInit = {
mode: "open",
@@ -36,30 +39,69 @@ export class HaIconButton extends LitElement {
protected render(): TemplateResult {
return html`
<mwc-icon-button
<ha-button
appearance="plain"
variant="neutral"
aria-label=${ifDefined(this.label)}
title=${ifDefined(this.hideTitle ? undefined : this.label)}
aria-haspopup=${ifDefined(this.ariaHasPopup)}
.disabled=${this.disabled}
.iconTag=${this.path ? "ha-svg-icon" : "span"}
.href=${this.href}
.target=${this.target}
.rel=${this.rel}
.download=${this.download}
>
${this.path
? html`<ha-svg-icon .path=${this.path}></ha-svg-icon>`
: html`<slot></slot>`}
</mwc-icon-button>
: html`<span><slot></slot></span>`}
</ha-button>
`;
}
static styles = css`
static styles: CSSResultGroup = css`
:host {
display: inline-block;
outline: none;
--ha-button-height: var(--ha-icon-button-size, 48px);
}
:host([disabled]) {
ha-button {
position: relative;
isolation: isolate;
--wa-form-control-padding-inline: var(
--ha-icon-button-padding-inline,
--ha-space-2
);
--wa-color-on-normal: currentColor;
--wa-color-fill-quiet: transparent;
--ha-button-label-overflow: visible;
}
ha-button::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: 50%;
background-color: currentColor;
opacity: 0;
pointer-events: none;
}
mwc-icon-button {
--mdc-theme-on-primary: currentColor;
--mdc-theme-text-disabled-on-light: var(--disabled-text-color);
ha-button::part(base) {
width: var(--wa-form-control-height);
aspect-ratio: 1;
outline-offset: -4px;
}
ha-button::part(label) {
display: flex;
}
:host([selected]) ha-button::after {
opacity: 0.1;
}
@media (hover: hover) {
:host(:hover:not([disabled])) ha-button::after {
opacity: 0.1;
}
}
`;
}
-1
View File
@@ -191,7 +191,6 @@ export class HaLanguagePicker extends LitElement {
static styles = css`
ha-generic-picker {
width: 100%;
min-width: 200px;
display: block;
}
`;
+9 -11
View File
@@ -84,13 +84,11 @@ export class HaMarkdown extends LitElement {
ha-markdown-element > :is(ol, ul) {
padding-inline-start: var(--markdown-list-indent, revert);
}
li {
&:has(input[type="checkbox"]) {
list-style: none;
& > input[type="checkbox"] {
margin-left: 0;
}
}
li:has(input[type="checkbox"]) {
list-style: none;
}
li:has(input[type="checkbox"]) > input[type="checkbox"] {
margin-left: 0;
}
svg {
background-color: var(--markdown-svg-background-color, none);
@@ -137,10 +135,10 @@ export class HaMarkdown extends LitElement {
--markdown-table-border-width: 0;
--markdown-table-padding-inline: 0;
--markdown-table-padding-block: 0;
th,
td {
vertical-align: middle;
}
}
table[role="presentation"] th,
table[role="presentation"] td {
vertical-align: middle;
}
table[role="presentation"] td[valign="top"],
table[role="presentation"] th[valign="top"] {
+1 -1
View File
@@ -196,7 +196,7 @@ export class HaPasswordField extends LitElement {
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--ha-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
+1 -1
View File
@@ -796,7 +796,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
:host {
display: flex;
flex-direction: column;
padding-top: var(--ha-space-3);
padding-top: var(--ha-space-4);
flex: 1;
}
+3 -2
View File
@@ -126,6 +126,7 @@ export class HaPickerField extends PickerMixin(LitElement) {
);
}
ha-combo-box-item {
position: relative;
background-color: var(--mdc-text-field-fill-color, whitesmoke);
border-radius: var(--ha-border-radius-sm);
border-end-end-radius: 0;
@@ -184,8 +185,8 @@ export class HaPickerField extends PickerMixin(LitElement) {
.clear {
margin: 0 -8px;
--mdc-icon-button-size: 32px;
--mdc-icon-size: 20px;
--ha-icon-button-size: 32px;
--ha-icon-button-padding-inline: var(--ha-space-1);
}
.arrow {
--mdc-icon-size: 20px;
+16 -2
View File
@@ -5,6 +5,7 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-input-helper-text";
import "./ha-picker-field";
import type { HaPickerField } from "./ha-picker-field";
import "./ha-svg-icon";
@@ -75,7 +76,7 @@ export class HaSelect extends LitElement {
protected override render() {
if (this.disabled) {
return this._renderField();
return html`${this._renderField()}${this._renderHelper()}`;
}
return html`
@@ -116,6 +117,7 @@ export class HaSelect extends LitElement {
)
: html`<slot></slot>`}
</ha-dropdown>
${this._renderHelper()}
`;
}
@@ -131,7 +133,6 @@ export class HaSelect extends LitElement {
aria-label=${ifDefined(this.label)}
@clear=${this._clearValue}
.label=${this.label}
.helper=${this.helper}
.value=${valueLabel}
.required=${this.required}
.disabled=${this.disabled}
@@ -144,6 +145,14 @@ export class HaSelect extends LitElement {
`;
}
private _renderHelper() {
return this.helper
? html`<ha-input-helper-text .disabled=${this.disabled}
>${this.helper}</ha-input-helper-text
>`
: nothing;
}
private _handleSelect(ev: CustomEvent<{ item: { value: string } }>) {
ev.stopPropagation();
const value = ev.detail.item.value;
@@ -194,6 +203,11 @@ export class HaSelect extends LitElement {
ha-dropdown::part(menu) {
min-width: var(--select-menu-width);
}
ha-input-helper-text {
display: block;
margin: var(--ha-space-2) 0 0;
}
`;
}
declare global {
@@ -1,8 +1,9 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import type { DateSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-date-input";
import type { HaDateInput } from "../ha-date-input";
@customElement("ha-selector-date")
export class HaDateSelector extends LitElement {
@@ -20,6 +21,12 @@ export class HaDateSelector extends LitElement {
@property({ type: Boolean }) public required = true;
@query("ha-date-input", true) private _input?: HaDateInput;
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render() {
return html`
<ha-date-input
@@ -29,6 +29,10 @@ export class HaDateTimeSelector extends LitElement {
@query("ha-time-input") private _timeInput!: HaTimeInput;
public reportValidity(): boolean {
return this._dateInput.reportValidity() && this._timeInput.reportValidity();
}
protected render() {
const values =
typeof this.value === "string" ? this.value.split(" ") : undefined;
@@ -1,10 +1,10 @@
import memoizeOne from "memoize-one";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { DurationSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import type { HaDurationData } from "../ha-duration-input";
import "../ha-duration-input";
import type { HaDurationData, HaDurationInput } from "../ha-duration-input";
@customElement("ha-selector-duration")
export class HaTimeDuration extends LitElement {
@@ -25,6 +25,12 @@ export class HaTimeDuration extends LitElement {
@property({ type: Boolean }) public required = true;
@query("ha-duration-input", true) private _input?: HaDurationInput;
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
private _data = memoizeOne(
(value?: HaDurationData | string | number): HaDurationData | undefined => {
if (typeof value === "number") {
@@ -66,6 +72,7 @@ export class HaTimeDuration extends LitElement {
.enableDay=${this.selector.duration?.enable_day}
.enableMillisecond=${this.selector.duration?.enable_millisecond}
.allowNegative=${this.selector.duration?.allow_negative}
.enableSecond=${this.selector.duration?.enable_second ?? true}
></ha-duration-input>
`;
}
@@ -64,7 +64,7 @@ export class HaEntitySelector extends LitElement {
if (!this.selector.entity?.multiple) {
return html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value}
.value=${typeof this.value === "string" ? this.value : ""}
.label=${this.label}
.placeholder=${this.placeholder}
.helper=${this.helper}
+12 -11
View File
@@ -13,7 +13,11 @@ import {
} from "../../data/media-player";
import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
import {
brandsUrl,
extractDomainFromBrandUrl,
isBrandUrl,
} from "../../util/brands-url";
import "../ha-alert";
import "../ha-form/ha-form";
import type { SchemaUnion } from "../ha-form/types";
@@ -72,16 +76,7 @@ export class HaMediaSelector extends LitElement {
if (thumbnail === oldThumbnail) {
return;
}
if (thumbnail && thumbnail.startsWith("/")) {
this._thumbnailUrl = undefined;
// Thumbnails served by local API require authentication
getSignedPath(this.hass, thumbnail).then((signedPath) => {
this._thumbnailUrl = signedPath.path;
});
} else if (
thumbnail &&
thumbnail.startsWith("https://brands.home-assistant.io")
) {
if (thumbnail && isBrandUrl(thumbnail)) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
this._thumbnailUrl = brandsUrl({
@@ -89,6 +84,12 @@ export class HaMediaSelector extends LitElement {
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
});
} else if (thumbnail && thumbnail.startsWith("/")) {
this._thumbnailUrl = undefined;
// Thumbnails served by local API require authentication
getSignedPath(this.hass, thumbnail).then((signedPath) => {
this._thumbnailUrl = signedPath.path;
});
} else {
this._thumbnailUrl = thumbnail;
}
@@ -1,6 +1,6 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import type { NumberSelector } from "../../data/selector";
@@ -8,6 +8,7 @@ import type { HomeAssistant } from "../../types";
import "../ha-input-helper-text";
import "../ha-slider";
import "../ha-textfield";
import type { HaTextField } from "../ha-textfield";
@customElement("ha-selector-number")
export class HaNumberSelector extends LitElement {
@@ -30,8 +31,14 @@ export class HaNumberSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-textfield", true) private _input?: HaTextField | HTMLInputElement;
private _valueStr = "";
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected willUpdate(changedProps: PropertyValues) {
if (changedProps.has("value")) {
if (this._valueStr === "" || this.value !== Number(this._valueStr)) {
@@ -221,7 +221,7 @@ export class HaSelectSelector extends LitElement {
.disabled=${this.disabled}
.required=${this.required}
.getItems=${this._getItems(options)}
.value=${this.value as string | undefined}
.value=${typeof this.value === "string" ? this.value : undefined}
@value-changed=${this._comboBoxValueChanged}
allow-custom-value
></ha-generic-picker>
@@ -231,7 +231,7 @@ export class HaSelectSelector extends LitElement {
return html`
<ha-select
.label=${this.label ?? ""}
.value=${(this.value as string) ?? ""}
.value=${typeof this.value === "string" ? this.value : ""}
.helper=${this.helper ?? ""}
.disabled=${this.disabled}
.required=${this.required}
@@ -64,6 +64,15 @@ const SELECTOR_SCHEMAS = {
name: "enable_millisecond",
selector: { boolean: {} },
},
{
name: "enable_second",
default: true,
selector: { boolean: {} },
},
{
name: "allow_negative",
selector: { boolean: {} },
},
] as const,
entity: [
{
+12 -5
View File
@@ -1,6 +1,6 @@
import { mdiEye, mdiEyeOff } from "@mdi/js";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import type { StringSelector } from "../../data/selector";
@@ -32,11 +32,18 @@ export class HaTextSelector extends LitElement {
@state() private _unmaskedPassword = false;
@query("ha-textfield, ha-textarea") private _input?: HTMLInputElement;
public async focus() {
await this.updateComplete;
(
this.renderRoot.querySelector("ha-textarea, ha-textfield") as HTMLElement
)?.focus();
this._input?.focus();
}
public reportValidity(): boolean {
if (this.selector.text?.multiple) {
return true;
}
return this._input?.reportValidity() ?? true;
}
protected render() {
@@ -141,7 +148,7 @@ export class HaTextSelector extends LitElement {
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--mdc-icon-button-size: 40px;
--ha-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
@@ -1,8 +1,9 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import type { TimeSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-time-input";
import type { HaTimeInput } from "../ha-time-input";
@customElement("ha-selector-time")
export class HaTimeSelector extends LitElement {
@@ -20,6 +21,12 @@ export class HaTimeSelector extends LitElement {
@property({ type: Boolean }) public required = false;
@query("ha-time-input") private _input?: HaTimeInput;
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render() {
return html`
<ha-time-input
+20 -2
View File
@@ -1,6 +1,6 @@
import type { PropertyValues } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import type { Selector } from "../../data/selector";
@@ -94,9 +94,27 @@ export class HaSelector extends LitElement {
@property({ attribute: false }) public context?: Record<string, any>;
@query("#selector", true) private _selectorElement?: HTMLElement;
public reportValidity(): boolean {
if (
this._selectorElement &&
"reportValidity" in this._selectorElement &&
typeof this._selectorElement.reportValidity === "function"
) {
return this._selectorElement?.reportValidity() ?? true;
}
if (this.required) {
return (
this.value !== undefined && this.value !== null && this.value !== ""
);
}
return true;
}
public async focus() {
await this.updateComplete;
(this.renderRoot.querySelector("#selector") as HTMLElement)?.focus();
this._selectorElement?.focus();
}
private get _type() {
+3 -2
View File
@@ -37,8 +37,8 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs";
import type { UpdateEntity } from "../data/update";
import { updateCanInstall } from "../data/update";
import { showEditSidebarDialog } from "../dialogs/sidebar/show-dialog-edit-sidebar";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types";
@@ -144,6 +144,7 @@ export const computePanels = memoizeOne(
if (
!isDefaultPanel &&
(!panel.title ||
panel.show_in_sidebar === false ||
hiddenPanels.includes(panel.url_path) ||
(panel.default_visible === false &&
!panelsOrder.includes(panel.url_path)))
@@ -980,7 +981,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
ha-md-list-item,
ha-md-list-item .item-text,
.title {
transition: none;
transition: 1ms;
}
}
`,
+9 -3
View File
@@ -1,11 +1,11 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { useAmPm } from "../common/datetime/use_am_pm";
import { fireEvent } from "../common/dom/fire_event";
import type { FrontendLocaleData } from "../data/translation";
import "./ha-base-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
import type { ValueChangedEvent } from "../types";
import "./ha-base-time-input";
import type { HaBaseTimeInput, TimeChangedEvent } from "./ha-base-time-input";
@customElement("ha-time-input")
export class HaTimeInput extends LitElement {
@@ -26,6 +26,12 @@ export class HaTimeInput extends LitElement {
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
@query("ha-base-time-input") private _input?: HaBaseTimeInput;
public reportValidity(): boolean {
return this._input?.reportValidity() ?? true;
}
protected render() {
const useAMPM = useAmPm(this.locale);
+1 -1
View File
@@ -46,7 +46,7 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
}
@media (prefers-reduced-motion: reduce) {
.mdc-top-app-bar {
transition: none;
transition: 1ms;
}
}
.mdc-top-app-bar__title {
@@ -298,7 +298,7 @@ export class TopAppBarBaseBase extends BaseElement {
}
@media (prefers-reduced-motion: reduce) {
.mdc-top-app-bar {
transition: none;
transition: 1ms;
}
}
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
@@ -0,0 +1,204 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { Segment } from "../data/vacuum";
import { getVacuumSegments } from "../data/vacuum";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-alert";
import "./ha-area-picker";
import "./ha-md-list";
import "./ha-md-list-item";
type AreaSegmentMapping = Record<string, string[]>; // area ID -> segment IDs
@customElement("ha-vacuum-segment-area-mapper")
export class HaVacuumSegmentAreaMapper extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "entity-id" }) public entityId!: string;
@property({ attribute: false }) public value?: AreaSegmentMapping;
@state() private _segments?: Segment[];
@state() private _loading = false;
@state() private _error?: string;
public get lastSeenSegments() {
return this._segments;
}
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("entityId") && this.entityId) {
this._loadSegments();
}
}
private async _loadSegments() {
this._loading = true;
this._error = undefined;
try {
const result = await getVacuumSegments(this.hass, this.entityId);
this._segments = result.segments;
} catch (err: any) {
this._error = err.message || "Failed to load segments";
this._segments = undefined;
} finally {
this._loading = false;
}
}
protected render() {
if (this._loading) {
return html`
<div class="loading">${this.hass.localize("ui.common.loading")}...</div>
`;
}
if (this._error) {
return html` <ha-alert alert-type="error">${this._error}</ha-alert> `;
}
if (!this._segments || this._segments.length === 0) {
return html`
<ha-alert alert-type="info">
${this.hass.localize("ui.dialogs.vacuum_segment_mapping.no_segments")}
</ha-alert>
`;
}
// Group segments by group (if available)
const groupedSegments = this._groupSegments(this._segments);
return html`
${Object.entries(groupedSegments).map(
([groupName, segments]) => html`
${groupName ? html`<h2>${groupName}</h2>` : nothing}
<ha-md-list>
${segments.map((segment) => this._renderSegment(segment))}
</ha-md-list>
`
)}
`;
}
private _groupSegments(segments: Segment[]): Record<string, Segment[]> {
const grouped: Record<string, Segment[]> = {};
for (const segment of segments) {
const group = segment.group || "";
if (!grouped[group]) {
grouped[group] = [];
}
grouped[group].push(segment);
}
return grouped;
}
private _renderSegment(segment: Segment) {
const mappedAreas = this._getSegmentAreas(segment.id);
return html`
<ha-md-list-item>
<span slot="headline">${segment.name}</span>
<ha-area-picker
slot="end"
.hass=${this.hass}
.value=${mappedAreas}
.label=${this.hass.localize(
"ui.dialogs.vacuum_segment_mapping.area_label"
)}
@value-changed=${this._handleAreaChanged}
data-segment-id=${segment.id}
></ha-area-picker>
</ha-md-list-item>
`;
}
private _handleAreaChanged = (ev: CustomEvent) => {
const target = ev.currentTarget as HTMLElement;
const segmentId = target.dataset.segmentId;
if (segmentId) {
this._areaChanged(segmentId, ev);
}
};
private _getSegmentAreas(segmentId: string): string | undefined {
if (!this.value) {
return undefined;
}
// Find which area(s) contain this segment
for (const [areaId, segmentIds] of Object.entries(this.value)) {
if (segmentIds.includes(segmentId)) {
return areaId;
}
}
return undefined;
}
private _areaChanged(segmentId: string, ev: CustomEvent) {
ev.stopPropagation();
const newAreaId = ev.detail.value as string | undefined;
// Create a copy of the current mapping
const newMapping: AreaSegmentMapping = { ...this.value };
// Remove segment from all areas
for (const areaId of Object.keys(newMapping)) {
newMapping[areaId] = newMapping[areaId].filter((id) => id !== segmentId);
// Remove empty area entries
if (newMapping[areaId].length === 0) {
delete newMapping[areaId];
}
}
// Add segment to new area if specified
if (newAreaId) {
if (!newMapping[newAreaId]) {
newMapping[newAreaId] = [];
}
newMapping[newAreaId].push(segmentId);
}
fireEvent(this, "value-changed", { value: newMapping });
}
static styles: CSSResultGroup = [
haStyle,
css`
:host {
display: block;
}
ha-area-picker {
flex: 1;
}
h2 {
margin: 0;
margin-inline-start: var(--ha-space-4);
}
.loading {
padding: var(--ha-space-4);
text-align: center;
color: var(--secondary-text-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-vacuum-segment-area-mapper": HaVacuumSegmentAreaMapper;
}
}
+11
View File
@@ -47,6 +47,9 @@ export class HaYamlEditor extends LitElement {
@property({ type: Boolean, attribute: "disable-fullscreen" })
public disableFullscreen = false;
@property({ type: Boolean, attribute: "in-dialog" })
public inDialog = false;
@property({ type: Boolean }) public required = false;
@property({ attribute: "copy-clipboard", type: Boolean })
@@ -101,6 +104,13 @@ export class HaYamlEditor extends LitElement {
}
}
public disableCodeEditorFullscreen(): void {
this.disableFullscreen = true;
if (this._codeEditor) {
this._codeEditor.disableFullscreen = true;
}
}
protected render() {
if (this._yaml === undefined) {
return nothing;
@@ -114,6 +124,7 @@ export class HaYamlEditor extends LitElement {
.value=${this._yaml}
.readOnly=${this.readOnly}
.disableFullscreen=${this.disableFullscreen}
.inDialog=${this.inDialog}
mode="yaml"
autocomplete-entities
autocomplete-icons
+58 -12
View File
@@ -423,31 +423,77 @@ export class HaMap extends ReactiveElement {
? baseOpacity! + pointIndex * opacityStep!
: undefined;
const thisPoint = path.points[pointIndex];
const nextPoint = path.points[pointIndex + 1];
// DRAW point
this._mapPaths.push(
Leaflet.circleMarker(path.points[pointIndex].point, {
Leaflet.circleMarker(thisPoint.point, {
radius: isTouch ? 8 : 3,
color: path.color || darkPrimaryColor,
opacity,
fillOpacity: opacity,
interactive: true,
}).bindTooltip(
this._computePathTooltip(path, path.points[pointIndex]),
{ direction: "top" }
)
}).bindTooltip(this._computePathTooltip(path, thisPoint), {
direction: "top",
})
);
// DRAW line between this and next point
this._mapPaths.push(
Leaflet.polyline(
[path.points[pointIndex].point, path.points[pointIndex + 1].point],
{
if (Math.abs(thisPoint.point[1] - nextPoint.point[1]) <= 180) {
// if the path does not cross the antimeridian, draw a simple line
// between the two points
this._mapPaths.push(
Leaflet.polyline([thisPoint.point, nextPoint.point], {
color: path.color || darkPrimaryColor,
opacity,
interactive: false,
}
)
);
})
);
} else {
// if the path crosses the antimeridian, split the line into two, to
// avoid it being drawn across the entire map
const longitudeDifference =
((nextPoint.point[1] - thisPoint.point[1] + 540) % 360) - 180;
let intersectionLatitude: number;
if (longitudeDifference === 0) {
// very, very unlikely edge case
intersectionLatitude =
(thisPoint.point[0] + nextPoint.point[0]) / 2;
} else {
intersectionLatitude =
thisPoint.point[0] +
((nextPoint.point[0] - thisPoint.point[0]) *
(thisPoint.point[1] > 0
? 180 - thisPoint.point[1]
: -180 - thisPoint.point[1])) /
longitudeDifference;
}
const intersectionPoint1: LatLngTuple = [
intersectionLatitude,
thisPoint.point[1] > 0 ? 180 : -180,
];
const intersectionPoint2: LatLngTuple = [
intersectionLatitude,
nextPoint.point[1] > 0 ? 180 : -180,
];
this._mapPaths.push(
Leaflet.polyline([thisPoint.point, intersectionPoint1], {
color: path.color || darkPrimaryColor,
opacity,
interactive: false,
})
);
this._mapPaths.push(
Leaflet.polyline([intersectionPoint2, nextPoint.point], {
color: path.color || darkPrimaryColor,
opacity,
interactive: false,
})
);
}
}
const pointIndex = path.points.length - 1;
if (pointIndex >= 0) {
@@ -242,6 +242,10 @@ class BrowseMediaTTS extends LitElement {
margin-top: 16px;
display: flex;
justify-content: space-between;
gap: var(--ha-space-2);
}
ha-language-picker {
width: 100%;
}
ha-textarea {
width: 100%;
@@ -260,7 +264,7 @@ class BrowseMediaTTS extends LitElement {
}
.footer {
--mdc-icon-size: 14px;
--mdc-icon-button-size: 24px;
--ha-icon-button-size: 24px;
display: flex;
justify-content: center;
align-items: center;
@@ -36,7 +36,7 @@ import {
} from "../../data/media_source";
import { isTTSMediaSource } from "../../data/tts";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../resources/styles";
import { haStyle, haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import {
@@ -584,7 +584,7 @@ export class HaMediaPlayerBrowse extends LitElement {
})}
.items=${children}
.renderItem=${this._renderGridItem}
class="children ${classMap({
class="children ha-scrollbar ${classMap({
portrait:
childrenMediaClass.thumbnail_ratio ===
"portrait",
@@ -612,6 +612,7 @@ export class HaMediaPlayerBrowse extends LitElement {
style=${styleMap({
height: `${children.length * 72 + 26}px`,
})}
class="ha-scrollbar"
.renderItem=${this._renderListItem}
></lit-virtualizer>
${currentItem.not_shown
@@ -764,6 +765,16 @@ export class HaMediaPlayerBrowse extends LitElement {
return "";
}
if (isBrandUrl(thumbnailUrl)) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
return brandsUrl({
domain: extractDomainFromBrandUrl(thumbnailUrl),
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
});
}
if (thumbnailUrl.startsWith("/")) {
// Thumbnails served by local API require authentication
return new Promise((resolve, reject) => {
@@ -786,16 +797,6 @@ export class HaMediaPlayerBrowse extends LitElement {
});
}
if (isBrandUrl(thumbnailUrl)) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnailUrl),
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
});
}
return thumbnailUrl;
}
@@ -979,6 +980,7 @@ export class HaMediaPlayerBrowse extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleScrollbar,
css`
:host {
display: flex;
@@ -1232,7 +1234,7 @@ export class HaMediaPlayerBrowse extends LitElement {
}
.child .play:not(.can_expand) {
--mdc-icon-button-size: 70px;
--ha-icon-button-size: 70px;
--mdc-icon-size: 48px;
background-color: var(--primary-color);
color: var(--text-primary-color);
@@ -1293,7 +1295,7 @@ export class HaMediaPlayerBrowse extends LitElement {
transition: all 0.5s;
background-color: rgba(var(--rgb-card-background-color), 0.5);
border-radius: var(--ha-border-radius-circle);
--mdc-icon-button-size: 40px;
--ha-icon-button-size: 40px;
}
ha-list-item:hover .graphic .play {
+2 -2
View File
@@ -93,8 +93,8 @@ class SearchInputOutlined extends LitElement {
}
ha-svg-icon,
ha-icon-button {
--mdc-icon-button-size: 24px;
height: var(--mdc-icon-button-size);
--ha-icon-button-size: 24px;
height: var(--ha-icon-button-size);
display: flex;
color: var(--primary-text-color);
}
@@ -641,7 +641,7 @@ export class HaTargetPickerItemRow extends LitElement {
z-index: 1;
}
ha-icon-button {
--mdc-icon-button-size: 32px;
--ha-icon-button-size: 32px;
}
.summary {
display: flex;
@@ -247,7 +247,7 @@ export class HaTargetPickerValueChip extends LitElement {
cursor: default;
}
.mdc-chip ha-icon-button {
--mdc-icon-button-size: 24px;
--ha-icon-button-size: 24px;
display: flex;
align-items: center;
outline: none;
+84
View File
@@ -159,6 +159,9 @@ export interface GasSourceTypeEnergyPreference {
// kWh/volume meter
stat_energy_from: string;
// Flow rate (m³/h, L/min, etc.)
stat_rate?: string;
// $ meter
stat_cost: string | null;
@@ -174,6 +177,9 @@ export interface WaterSourceTypeEnergyPreference {
// volume meter
stat_energy_from: string;
// Flow rate (L/min, gal/min, m³/h, etc.)
stat_rate?: string;
// $ meter
stat_cost: string | null;
@@ -368,6 +374,9 @@ export const getReferencedStatisticIdsPower = (
for (const source of prefs.energy_sources) {
if (source.type === "gas" || source.type === "water") {
if (source.stat_rate) {
statIDs.push(source.stat_rate);
}
continue;
}
@@ -389,6 +398,7 @@ export const getReferencedStatisticIdsPower = (
}
}
statIDs.push(...prefs.device_consumption.map((d) => d.stat_rate));
statIDs.push(...prefs.device_consumption_water.map((d) => d.stat_rate));
return statIDs.filter(Boolean) as string[];
};
@@ -1391,6 +1401,80 @@ export const calculateSolarConsumedGauge = (
return undefined;
};
/**
* Conversion factors from each flow rate unit to L/min.
* All HA-supported UnitOfVolumeFlowRate values are covered.
*
* m³/h 1000/60 = 16.6667 L/min
* m³/min 1000 L/min
* m³/s 60000 L/min
* ft³/min 28.3168 L/min
* L/h 1/60 L/min
* L/min 1 L/min
* L/s 60 L/min
* gal/h 3.78541/60 L/min
* gal/min 3.78541 L/min
* gal/d 3.78541/1440 L/min
* mL/s 0.06 L/min
*/
/** Exact number of liters in one US gallon */
const LITERS_PER_GALLON = 3.785411784;
const FLOW_RATE_TO_LMIN: Record<string, number> = {
"m³/h": 1000 / 60,
"m³/min": 1000,
"m³/s": 60000,
"ft³/min": 28.316846592,
"L/h": 1 / 60,
"L/min": 1,
"L/s": 60,
"gal/h": LITERS_PER_GALLON / 60,
"gal/min": LITERS_PER_GALLON,
"gal/d": LITERS_PER_GALLON / 1440,
"mL/s": 60 / 1000,
};
/**
* Get current flow rate from an entity state, converted to L/min.
* @returns Flow rate in L/min, or undefined if unavailable/invalid.
*/
export const getFlowRateFromState = (
stateObj?: HassEntity
): number | undefined => {
if (!stateObj) {
return undefined;
}
const value = parseFloat(stateObj.state);
if (isNaN(value)) {
return undefined;
}
const unit = stateObj.attributes.unit_of_measurement;
const factor = unit ? FLOW_RATE_TO_LMIN[unit] : undefined;
if (factor === undefined) {
// Unknown unit return raw value as-is (best effort)
return value;
}
return value * factor;
};
/**
* Format a flow rate value (in L/min) to a human-readable string using
* the preferred unit system: metric L/min, imperial gal/min.
*/
export const formatFlowRateShort = (
hassLocale: HomeAssistant["locale"],
lengthUnitSystem: string,
litersPerMin: number
): string => {
const isMetric = lengthUnitSystem === "km";
if (isMetric) {
return `${formatNumber(litersPerMin, hassLocale, { maximumFractionDigits: 1 })} L/min`;
}
const galPerMin = litersPerMin / LITERS_PER_GALLON;
return `${formatNumber(galPerMin, hassLocale, { maximumFractionDigits: 1 })} gal/min`;
};
/**
* Get current power value from entity state, normalized to watts (W)
* @param stateObj - The entity state object to get power value from
-3
View File
@@ -3,7 +3,6 @@ import { formatDurationDigital } from "../../common/datetime/format_duration";
import type { FrontendLocaleData } from "../translation";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
// These attributes are hidden from the more-info window for all entities.
export const STATE_ATTRIBUTES = [
"entity_id",
"assumed_state",
@@ -29,8 +28,6 @@ export const STATE_ATTRIBUTES = [
"available_tones",
];
// These attributes are hidden from the more-info window for entities of the
// matching domain and device_class.
export const STATE_ATTRIBUTES_DOMAIN_CLASS = {
sensor: {
enum: ["options"],
+9 -1
View File
@@ -9,6 +9,7 @@ import { debounce } from "../../common/util/debounce";
import type { HomeAssistant } from "../../types";
import type { LightColor } from "../light";
import type { RegistryEntry } from "../registry";
import type { Segment } from "../vacuum";
type EntityCategory = "config" | "diagnostic";
@@ -120,6 +121,11 @@ export interface SwitchAsXEntityOptions {
invert: boolean;
}
export interface VacuumEntityOptions {
area_mapping?: Record<string, string[]>;
last_seen_segments?: Segment[];
}
export interface EntityRegistryOptions {
number?: NumberEntityOptions;
sensor?: SensorEntityOptions;
@@ -128,6 +134,7 @@ export interface EntityRegistryOptions {
lock?: LockEntityOptions;
weather?: WeatherEntityOptions;
light?: LightEntityOptions;
vacuum?: VacuumEntityOptions;
switch_as_x?: SwitchAsXEntityOptions;
conversation?: Record<string, unknown>;
"cloud.alexa"?: Record<string, unknown>;
@@ -150,7 +157,8 @@ export interface EntityRegistryEntryUpdateParams {
| AlarmControlPanelEntityOptions
| CalendarEntityOptions
| WeatherEntityOptions
| LightEntityOptions;
| LightEntityOptions
| VacuumEntityOptions;
aliases?: string[];
labels?: string[];
categories?: Record<string, string | null>;
+8
View File
@@ -37,6 +37,13 @@ export interface LovelaceViewHeaderConfig {
badges_wrap?: "wrap" | "scroll";
}
export const DEFAULT_FOOTER_MAX_WIDTH_PX = 600;
export interface LovelaceViewFooterConfig {
card?: LovelaceCardConfig;
max_width?: number;
}
export interface LovelaceViewSidebarConfig {
sections?: LovelaceSectionConfig[];
content_label?: string;
@@ -68,6 +75,7 @@ export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
cards?: LovelaceCardConfig[];
sections?: LovelaceSectionRawConfig[];
header?: LovelaceViewHeaderConfig;
footer?: LovelaceViewFooterConfig;
// Only used for section view, it should move to a section view config type when the views will have dedicated editor.
sidebar?: LovelaceViewSidebarConfig;
}
+35 -8
View File
@@ -8,12 +8,17 @@ import {
mdiPlayBoxMultiple,
mdiTooltipAccount,
} from "@mdi/js";
import type { HomeAssistant, PanelInfo } from "../types";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { LocalizeKeys } from "../common/translations/localize";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { HomeAssistant, PanelInfo } from "../types";
export const HOME_PANEL = "home";
export const NOT_FOUND_PANEL = "notfound";
export const PROFILE_PANEL = "profile";
export const LOVELACE_PANEL = "lovelace";
/** Panel to show when no panel is picked. */
export const DEFAULT_PANEL = "home";
export const DEFAULT_PANEL = HOME_PANEL;
export const hasLegacyOverviewPanel = (hass: HomeAssistant): boolean =>
Boolean(hass.panels.lovelace?.config);
@@ -30,7 +35,7 @@ export const getDefaultPanelUrlPath = (hass: HomeAssistant): string => {
getLegacyDefaultPanelUrlPath() ||
DEFAULT_PANEL;
// If default panel is lovelace and no old overview exists, fall back to home
if (defaultPanel === "lovelace" && !hasLegacyOverviewPanel(hass)) {
if (defaultPanel === LOVELACE_PANEL && !hasLegacyOverviewPanel(hass)) {
return DEFAULT_PANEL;
}
return defaultPanel;
@@ -39,12 +44,16 @@ export const getDefaultPanelUrlPath = (hass: HomeAssistant): string => {
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo => {
const panel = getDefaultPanelUrlPath(hass);
return (panel ? hass.panels[panel] : undefined) ?? hass.panels[DEFAULT_PANEL];
return (
(panel ? hass.panels[panel] : undefined) ??
hass.panels[DEFAULT_PANEL] ??
hass.panels[NOT_FOUND_PANEL]
);
};
export const getPanelNameTranslationKey = (panel: PanelInfo) => {
if (panel.url_path === "profile") {
return "panel.profile" as const;
if ([PROFILE_PANEL, NOT_FOUND_PANEL].includes(panel.url_path)) {
return `panel.${panel.url_path}` as const;
}
return `panel.${panel.title}` as const;
@@ -137,4 +146,22 @@ export const PANEL_ICON_PATHS = {
export const getPanelIconPath = (panel: PanelInfo): string | undefined =>
PANEL_ICON_PATHS[panel.url_path];
export const FIXED_PANELS = ["profile", "config"];
export const FIXED_PANELS = [PROFILE_PANEL, "config", NOT_FOUND_PANEL];
export interface PanelMutableParams {
title?: string | null;
icon?: string | null;
require_admin?: boolean | null;
show_in_sidebar?: boolean | null;
}
export const updatePanel = (
hass: HomeAssistant,
urlPath: string,
updates: PanelMutableParams
) =>
hass.callWS({
type: "frontend/update_panel",
url_path: urlPath,
...updates,
});
+1
View File
@@ -231,6 +231,7 @@ export interface DurationSelector {
enable_day?: boolean;
enable_millisecond?: boolean;
allow_negative?: boolean;
enable_second?: boolean;
} | null;
}
+13
View File
@@ -7,10 +7,23 @@ export type SystemLogLevel =
| "info"
| "debug";
export type SystemLogErrorType =
| "auth"
| "connection"
| "invalid_response"
| "rate_limit"
| "server"
| "slow_setup"
| "timeout"
| "ssl"
| "statistics"
| "dns";
export interface LoggedError {
name: string;
message: [string];
level: SystemLogLevel;
error_type?: SystemLogErrorType;
source: [string, number];
exception: string;
count: number;
+17
View File
@@ -2,6 +2,7 @@ import type {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE } from "./entity/entity";
export type VacuumEntityState =
@@ -29,6 +30,7 @@ export const enum VacuumEntityFeature {
MAP = 2048,
STATE = 4096,
START = 8192,
CLEAN_AREA = 16384,
}
interface VacuumEntityAttributes extends HassEntityAttributeBase {
@@ -62,3 +64,18 @@ export function canReturnHome(stateObj: VacuumEntity): boolean {
}
return stateObj.state !== "returning";
}
export interface Segment {
id: string;
name: string;
group?: string;
}
export const getVacuumSegments = (
hass: HomeAssistant,
entity_id: string
): Promise<{ segments: Segment[] }> =>
hass.callWS({
type: "vacuum/get_segments",
entity_id,
});
@@ -468,13 +468,13 @@ class LightRgbColorPicker extends LitElement {
border: none;
outline: none;
display: block;
width: var(--mdc-icon-button-size, 48px);
height: var(--mdc-icon-button-size, 48px);
width: var(--ha-icon-button-size, 48px);
height: var(--ha-icon-button-size, 48px);
padding: calc(
(var(--mdc-icon-button-size, 48px) - var(--mdc-icon-size, 24px)) / 2
(var(--ha-icon-button-size, 48px) - var(--mdc-icon-size, 24px)) / 2
);
background-color: transparent;
border-radius: calc(var(--mdc-icon-button-size, 48px) / 2);
border-radius: calc(var(--ha-icon-button-size, 48px) / 2);
overflow: hidden;
cursor: pointer;
transition: background-color 180ms ease-in-out;
@@ -0,0 +1,153 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../components/ha-button";
import "../../../../components/ha-spinner";
import "../../../../components/ha-vacuum-segment-area-mapper";
import type { HaVacuumSegmentAreaMapper } from "../../../../components/ha-vacuum-segment-area-mapper";
import type {
ExtEntityRegistryEntry,
VacuumEntityOptions,
} from "../../../../data/entity/entity_registry";
import {
getExtendedEntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../../data/entity/entity_registry";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-more-info-view-vacuum-segment-mapping")
export class HaMoreInfoViewVacuumSegmentMapping extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public params!: { entityId: string };
@state() private _areaMapping?: Record<string, string[]>;
@state() private _submitting = false;
@state() private _dirty = false;
@state() private _error?: string;
private _entry?: ExtEntityRegistryEntry;
protected firstUpdated() {
this._loadCurrentMapping();
}
private async _loadCurrentMapping() {
if (!this.params.entityId) return;
this._entry = await getExtendedEntityRegistryEntry(
this.hass,
this.params.entityId
);
if (this._entry?.options?.vacuum) {
this._areaMapping = this._entry.options.vacuum.area_mapping || {};
} else {
this._areaMapping = {};
}
}
private _valueChanged(ev: CustomEvent) {
this._areaMapping = ev.detail.value;
this._dirty = true;
}
private async _save() {
if (!this.params.entityId || !this._areaMapping) return;
this._error = undefined;
this._submitting = true;
// Get current segments from the mapper component
const mapper = this.shadowRoot!.querySelector(
"ha-vacuum-segment-area-mapper"
) as HaVacuumSegmentAreaMapper;
const options: VacuumEntityOptions = {
...(this._entry?.options?.vacuum ?? {}),
area_mapping: this._areaMapping,
last_seen_segments: mapper.lastSeenSegments,
};
try {
await updateEntityRegistryEntry(this.hass, this.params.entityId, {
options_domain: "vacuum",
options: options,
});
this._dirty = false;
} catch (err: any) {
this._error = err.message;
} finally {
this._submitting = false;
}
}
protected render() {
if (!this._areaMapping) {
return html`<ha-spinner active></ha-spinner>`;
}
return html`
<div class="content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<ha-vacuum-segment-area-mapper
.hass=${this.hass}
.entityId=${this.params.entityId}
.value=${this._areaMapping}
@value-changed=${this._valueChanged}
></ha-vacuum-segment-area-mapper>
<div class="footer">
<ha-button
@click=${this._save}
.disabled=${!this._dirty || this._submitting}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</div>
</div>
`;
}
static styles: CSSResultGroup = css`
:host {
display: block;
height: 100%;
}
.content {
display: flex;
flex-direction: column;
height: 100%;
}
ha-spinner {
margin: var(--ha-space-8);
display: flex;
justify-self: center;
}
ha-vacuum-segment-area-mapper {
flex: 1;
padding-inline-start: var(--ha-space-2);
}
.footer {
display: flex;
justify-content: flex-end;
padding: var(--ha-space-4);
border-top: 1px solid var(--divider-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-view-vacuum-segment-mapping": HaMoreInfoViewVacuumSegmentMapping;
}
}
@@ -0,0 +1,18 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
export const loadVacuumSegmentMappingView = () =>
import("./ha-more-info-view-vacuum-segment-mapping");
export const showVacuumSegmentMappingView = (
element: HTMLElement,
localize: LocalizeFunc,
entityId: string
): void => {
fireEvent(element, "show-child-view", {
viewTag: "ha-more-info-view-vacuum-segment-mapping",
viewImport: loadVacuumSegmentMappingView,
viewTitle: localize("ui.dialogs.vacuum_segment_mapping.title"),
viewParams: { entityId },
});
};

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