Compare commits

..

222 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] 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 Bottein c4326b4f3a Use max width for dashboard footer (#29947) 2026-03-03 14:57:57 +01:00
Paul Bottein d248f5614f Add label for toggle button in area strategy (#29949) 2026-03-03 13:34:05 +01:00
Aidan Timson a4da7b26ea Fix copy to clipboard for wa dialogs (#29951)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-03-03 13:09:30 +01:00
Wendelin 3c49cdf3c0 ha-bottom-sheet reduce motion support (#29950) 2026-03-03 11:06:23 +00:00
Petar Petrov 26af81d1a4 Use net battery power in power sankey card (#29940) 2026-03-03 11:53:54 +01:00
Aidan Timson 2a08f2d79b Add tooltip for config dashboard action button in toolbar (#29948) 2026-03-03 10:33:37 +01:00
Marcin Bauer a5be02b743 Add tooltip for Lovelace dropdown action button in top app bar (#29933) 2026-03-03 09:15:37 +00:00
sevorl 4228871f00 Fix missing slot attribute on wa-divider in automation sidebar action (#29942) 2026-03-03 08:56:33 +00:00
Wendelin 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
Wendelin 8b82882e15 ha-authorize fix rtl check (#29937)
Add RTL direction handling in updated lifecycle method
2026-03-02 18:22:06 +01:00
Matthias Alphart 2701015eda Fix data-table content bottom margin (#29805)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 17:02:31 +01:00
Aidan Timson 1991a9e493 Code editor fullscreen in dialogs (#29882)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2026-03-02 17:01:13 +01:00
Petar Petrov 2b72c54194 Migrate Energy date selector to new footer (#29867) 2026-03-02 17:00:34 +01:00
Paul Bottein a7cb2fe7a7 Fix updates, discovered devices and repairs cards flickering (#29935) 2026-03-02 13:50:42 +00:00
Paul Bottein 51ea0c8201 Fix sidebar not closing when reduced motion is enabled (#29934) 2026-03-02 13:19:26 +00:00
Wendelin 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
Wendelin ee982b1899 Add error translation for loading energy preferences (#29924) 2026-03-02 11:49:46 +02:00
Aidan Timson 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
Copilot 50c361db62 Add mixin to remove code duplication in automation/script editors (#29842)
* Initial plan

* Changes before error encountered

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

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

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

* Simplify automation/script editor mixin signature

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

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

* Remove underscore prefix from protected members per style guide

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
Co-authored-by: Wendelin <w@pe8.at>
2026-03-02 11:44:04 +02:00
sevorl e7a8d15a13 Use ha-duration-input for wait_template timeout (#29862) 2026-03-02 09:07:51 +01:00
karwosts 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
karwosts 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] 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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 06:10:19 +00:00
renovate[bot] 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] 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] 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 Bottein 82b28b547a Fix control select menu color in ios (#29892) 2026-02-27 17:26:04 +01:00
Bram Kragten 61c2c750b4 Fix overflow for icon buttons (#29891) 2026-02-27 15:44:21 +00:00
Petar Petrov 117690ee70 Fix sensor card graph not updating when value is unchanged (#29889) 2026-02-27 15:41:54 +00:00
Petar Petrov 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 Bottein a240019968 Add render icon property to ha-control-select-menu (#29881) 2026-02-27 16:23:58 +01:00
Petar Petrov 0bdf4b8777 Fix monetary device class state display with non-ISO 4217 currency symbols (#29887) 2026-02-27 14:59:14 +01:00
renovate[bot] 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
Aidan Timson 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 <mail@bramkragten.nl>

* Memoize

* Rename

* Fix

* Format audits in normal mode

* Refactor, dont pass hass

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2026-02-27 14:45:55 +01:00
Petar Petrov e4ae29e8b5 Fix energy compare tooltip showing wrong year (#29885) 2026-02-27 14:37:52 +01:00
Aidan Timson 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 Resch 61b546415d Revert "Add vacuum mapping not configured issue" (#29876) 2026-02-27 11:18:49 +01:00
Brandon Chen 4e1b709303 Fix YAML content invisible in dark mode for conversation debug result… (#29874) 2026-02-27 09:11:28 +01:00
renovate[bot] 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] 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 Bottein 58d4cf8d84 Fix scrollbar in 2026.3 (#29865) 2026-02-26 16:44:12 +01:00
Aidan Timson d3453aff37 Add missing theming variable support to dialog and bottom sheet (#29857) 2026-02-26 16:43:20 +01:00
Aidan Timson 64ff2e414c Add thread configuration my link (#29861) 2026-02-26 15:06:46 +00:00
Wendelin 2ca25c980f Fix quick search icon size (#29858) 2026-02-26 15:59:27 +01:00
Aidan Timson 73d93bc601 Add matter configuration my link (#29859) 2026-02-26 14:41:43 +00:00
Wendelin 5ca6a8aced Fix ha-icon-button-toggle selected style (#29856) 2026-02-26 13:02:12 +00:00
Aidan Timson 7ff4993e0b Fix esc closing dialogs with prevent scrim close (#29851) 2026-02-26 13:20:05 +02:00
Norbert Rittel 4e6fbacccc Remove trailing periods from "Learn more" etc. links / tooltips (#29835) 2026-02-26 10:38:54 +00:00
Petar Petrov 2958d49e36 Convert Energy Now tiles to badges (#29845) 2026-02-26 10:38:01 +00:00
Norbert Rittel 92289dc7ea Improve "Create a new … helper" option in entity picker (#29853) 2026-02-26 10:34:42 +00:00
Petar Petrov f6c1a890e4 Dynamically calculate the date range picker's vertical opening direction (#29850) 2026-02-26 09:33:34 +00:00
Wendelin d06321ed43 Fix protocols dashboards fab padding (#29847) 2026-02-26 10:31:50 +02:00
dependabot[bot] 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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 08:18:15 +02:00
Paul Bottein 4f39fa482d Only ask to refresh dashboard in edit mode or yaml mode (#29826) 2026-02-26 08:16:21 +02:00
renovate[bot] 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] 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] 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
AlCalzone 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] 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] 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] 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] 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] 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 Kragten 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
Franck Nijhof 6070c1907a Adjust brands assets to proxy brand images through local API (#29799)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2026-02-25 17:10:38 +01:00
renovate[bot] 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 Bottein d17217fc90 Use show in sidebar property instead of checking title (#29815) 2026-02-25 16:37:25 +01:00
renovate[bot] 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] 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 Alphart d38a2894c4 Remove unused properties in ha-data-table and hass-tabs-subpage-data-table (#29808) 2026-02-25 16:31:39 +01:00
Aidan Timson 4c70376a62 Cleanup old comments (#29823) 2026-02-25 15:24:00 +00:00
Wendelin 8d69bd1401 Fix button active also for icon-buttons (#29820) 2026-02-25 16:21:31 +01:00
renovate[bot] 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 Timson 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] 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] 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
gpoitch 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 Petrov 1cbc38f231 Water flow rate sankey chart in Now view (#29804) 2026-02-25 14:18:48 +01:00
renovate[bot] 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] 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] 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] 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] 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 Henkens dbffdfeaca Add cover of device type window to the security dashboard (#29797) 2026-02-25 11:11:13 +01:00
Artur Pragacz 0a4b7917ab Update vacuum segment mapping description (#29802) 2026-02-25 08:38:37 +01:00
Simon Lamon e1524358d9 Remove duplicated buttons (#29798) 2026-02-25 08:22:40 +01:00
Artur Pragacz 8774f9c3fc Add vacuum mapping not configured issue (#29800) 2026-02-25 08:14:38 +01:00
Wendelin 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
ildar170975 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 Rittel 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 Bottein 26a7372c5e Don't show label for toggle all lights and align individual lights (#29794) 2026-02-24 17:28:53 +01:00
Paul Bottein 70d3409d62 Don't use navigation history when using tabs (#29791) 2026-02-24 18:03:48 +02:00
Wendelin 0711ecddab Handle selector edge case for [] (#29790) 2026-02-24 15:30:29 +00:00
Petar Petrov bcfaa67eba Add power, water and gas current flow rate tile cards (#29788) 2026-02-24 16:01:32 +01:00
Matthias de Baat 1b60e6e04e Reorganize Zigbee settings page (#29671)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2026-02-24 15:11:36 +01:00
Petar Petrov a1a634f6dc Add footer card support to sections view (#29620)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
Co-authored-by: Wendelin <w@pe8.at>
2026-02-24 11:00:50 +00:00
Petar Petrov 55f48fbb56 Add tabs to energy config page (#29689) 2026-02-24 09:43:02 +00:00
Norbert Rittel ca4d66b94c Change second tab to "Electricity" in Energy dashboard (#29787) 2026-02-24 10:13:22 +01:00
Aidan Timson 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 <mail@bramkragten.nl>

* Format

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2026-02-24 06:57:31 +01:00
ildar170975 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
Petar Petrov 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 <norbert@rittel.de>

---------

Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-02-24 06:55:35 +01:00
ildar170975 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
ildar170975 8299386737 ha-entity-attribute-picker: add valueRenderer (#29780)
add valueRenderer
2026-02-24 06:52:10 +01:00
Raphael Hehl 5e58ff476f Re-initialize camera stream when backend finishes starting (#29752) 2026-02-23 16:59:53 +01:00
Paul Bottein 758d955053 Add configuration to built-in panels (#29572)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-23 16:15:54 +01:00
Kevin Stillhammer 1efd5d26f0 Show allow_negative in DurationSelector options (#29775) 2026-02-23 16:02:33 +01:00
Aidan Timson 36979f10cc Fix types for dialog hide events (#29777) 2026-02-23 15:54:36 +01:00
Aidan Timson 812c59fcb4 Add missing back path for protocol config dashboards (#29770) 2026-02-23 15:36:50 +01:00
karwosts 0c34165bcf Disallow moving a section to non-sections view (#29756) 2026-02-23 10:54:11 +00:00
Matthias de Baat 8c2bfbe9ce Reorganize Matter settings (#29708)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-02-23 10:31:38 +00:00
Aidan Timson 8f721d74e2 Fix swipe action bubbling up to stacked bottom drawer/sheet (#29768) 2026-02-23 10:26:10 +01:00
dependabot[bot] 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
Marie-Alice Blete eaad2295a9 Fix ZHA config dashboard button animation targeting (#29744)
Co-authored-by: Marie-Alice Blete <malywut@users.noreply.github.com>
2026-02-23 10:18:32 +01:00
Norbert Rittel e74eee3d34 Make all picker strings of Frontend conditions consistent (#29742) 2026-02-23 09:54:14 +01:00
karwosts cc39010839 Fix group more-info names for not-in-registry entities (#29758) 2026-02-23 09:50:55 +01:00
Kevin Stillhammer 7f97425214 Allow to disable seconds in DurationSelector (#29760) 2026-02-23 09:30:04 +01:00
dependabot[bot] 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] 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 Timson 45ca1b2cdc Fix esc closing all dialogs or sheets (close one after another) (#29732) 2026-02-23 09:13:28 +01:00
Matthias de Baat 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 <MindFreeze@users.noreply.github.com>

* Update bluetooth-adapter-info-page.ts

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

* Update text

* Show GATT message and make row clickable

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-23 06:59:09 +01:00
Norbert Rittel db49678ccb Make descriptions of Frontend actions consistent with Core (#29740) 2026-02-20 18:03:19 +01:00
Norbert Rittel 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
karwosts 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] 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] <support@github.com>

* dedupe

---------

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

* Remove padding

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

* Add padding to bottom

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-19 18:52:32 +01:00
Aidan Timson 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 <marcinbauer85@gmail.com>

* Half width

Co-authored-by: Marcin Bauer <marcinbauer85@gmail.com>

* Restore after rebase

* Reduce space for picker

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
Co-authored-by: Wendelin <w@pe8.at>
Co-authored-by: Marcin Bauer <marcinbauer85@gmail.com>
2026-02-19 14:24:09 +01:00
Matthias de Baat 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
karwosts d34c42e587 Refine supported actions in button heading badge (#29718) 2026-02-19 12:49:30 +02:00
Joakim Sørensen 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 Rittel 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 Timson 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 Timson 8b38e6d170 Switch dialog device registry detail to adaptive dialog (#29713) 2026-02-18 18:04:05 +01:00
Aidan Timson 6daf0eb469 Use ha-scrollbar with media browser (#29714) 2026-02-18 18:03:26 +01:00
Wendelin 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 Timson 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
Wendelin 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 <mail@bramkragten.nl>

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

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

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

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

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

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Fix icon-button slot

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

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2026-02-18 16:18:28 +01:00
Marcin Bauer 4e087760ab Fix chip order in automation save dialog to match field order (#29710) 2026-02-18 15:18:07 +00:00
Aidan Timson 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 <12148533+wendevlin@users.noreply.github.com>

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-02-18 16:17:13 +01:00
Wendelin 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
ildar170975 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 <MindFreeze@users.noreply.github.com>

* add a gap

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-18 14:05:41 +00:00
Bram Kragten 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
Icecovery 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
Petar Petrov 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 <12148533+wendevlin@users.noreply.github.com>

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-02-18 10:45:15 +01:00
Aidan Timson 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 <12148533+wendevlin@users.noreply.github.com>
2026-02-18 08:55:11 +01:00
Aidan Timson 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 Lamon 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
Wendelin 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 Timson f3a814e38a Cleanup dialog default width attrs (#29686) 2026-02-17 20:13:27 +01:00
Aidan Timson 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 <MindFreeze@users.noreply.github.com>

* Use normal duration

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

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
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 Bottein 649516c9fa Change default icon for blank area if not icon configured (#29394) 2026-02-04 15:40:36 +01:00
Paul Bottein bbc4fb96b2 Load domain translation when integration page load (#29391) 2026-02-04 15:40:35 +01:00
Paul Bottein 0ae639aeb0 Remove old lovelace overview from pickers (#29390) 2026-02-04 15:40:34 +01:00
karwosts 0e7e41065e Don't shrink ha-dropdown checkboxes (#29387) 2026-02-04 15:40:33 +01:00
Paul Bottein 685843f112 Add translations for new overview dialog (#29382) 2026-02-04 15:40:32 +01:00
Paul Bottein 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 Bottein ec23164aa9 Improve other devices page in home dashboard (#29370) 2026-02-03 16:57:45 +01:00
Paul Bottein 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 <noreply@anthropic.com>
2026-02-03 16:57:43 +01:00
Paul Bottein a222f6a736 Add missing danger variant in dropdown item (#29359) 2026-02-03 16:57:40 +01:00
Petar Petrov ef3dd16d45 Move dialog scrim to pseudo-element (#29357) 2026-02-03 16:57:39 +01:00
karwosts 5d4e1d205e Fix missing imports in devtools-statistics (#29355) 2026-02-03 16:57:38 +01:00
Darryn Capes-Davis 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 Bottein 332e108dae Fix "Reload resources" menu for YAML resource mode (#29346) 2026-02-02 17:17:17 +01:00
karwosts 3c15b29d0a Entity diagnostic - handle entity not in the registry (#29344) 2026-02-02 17:17:16 +01:00
Wendelin 130c708e23 Fix dropdown width in datatables (#29340) 2026-02-02 17:17:15 +01:00
Paul Bottein 588a14a8a7 Fix type error for missing hass.themes race condition in themes mixin (#29338) 2026-02-02 17:17:14 +01:00
Petar Petrov a1ef6ad266 Remove redundant dialog backdrop color (#29337) 2026-02-02 17:17:13 +01:00
Aidan Timson a6c1f87730 Ensure template renderer overflows on overflow (#29335) 2026-02-02 17:17:12 +01:00
Wendelin 49252a3808 Fix missing ha-md-menu in config/labels (#29334) 2026-02-02 17:17:11 +01:00
Aidan Timson 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
Wendelin e355a61d8f Revert "Fix automation sidebar ui supported check" (#29331) 2026-02-02 17:17:08 +01:00
Linus Rath f2e19e51ce Update untracked consumption threshold to 1W (#29310) 2026-02-02 17:17:07 +01:00
karwosts fd9ab8f561 Use ha-form for condition template (#29301) 2026-02-02 17:17:06 +01:00
Kristel faa1b3c98f bugfix: add eventlistener for exposed-entities-changed to Entities page (#29299) 2026-02-02 17:17:06 +01:00
Aidan Timson acc4a84fc9 Fix scrolling for labs page (#29287) 2026-02-02 17:17:05 +01:00
karwosts 4d723dac37 Fix areas cannot be deleted (#29285) 2026-02-02 17:17:03 +01:00
Aidan Timson f1d4d0ef98 Fix type error for missing hass.config race condition in themes mixin (#29280) 2026-02-02 17:17:02 +01:00
Paul Bottein 88180a2708 Fix demo because of new default panel (#29279) 2026-02-02 17:17:01 +01:00
Aidan Timson 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
Wendelin 55f22ba61a Implement fallback for dialog close event in Quick Search (#29260) 2026-02-02 17:16:59 +01:00
Aidan Timson 812f3ca8b9 Change default shortcut tip in Quick Search to mod+k, add tip to settings (#29253) 2026-02-02 17:16:58 +01:00
Marcin Bauer 7f880d11a0 Keep focus on search field when clicking filter chips (#29249)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
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
Wendelin 224bcece9c Fix multi select in quick search (#29272)
Add item selection state management to QuickBar component
2026-01-30 10:30:33 +01:00
Wendelin dc84b7698f Fix --wa-color-text-normal (#29271)
Update normal text color variable in wa.globals.ts
2026-01-30 10:30:32 +01:00
Wendelin 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 Lamon 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 Bottein 756138408a Remove default title for new dashboards (#29259) 2026-01-29 18:04:09 +01:00
Paul Bottein 3c8f112565 Prevent action in tile container (#29257) 2026-01-29 18:04:08 +01:00
Paul Bottein 2521f3dde4 Fix actions in dashboard overflow menu (#29256) 2026-01-29 18:04:07 +01:00
TheJulianJES 56390aa01a Fix Matter dashboard using disabled and ignored config entries (#29254) 2026-01-29 17:52:32 +01:00
Paul Bottein 9aac5b19da Stop click propagation when clicking item in icon overflow (#29252) 2026-01-29 17:52:31 +01:00
Wendelin 24afc3dc88 Prevent quick search to close from hot keys (#29251) 2026-01-29 17:52:30 +01:00
Paul Bottein 873c7b2947 Remove unused theme option in distribution card (#29250) 2026-01-29 17:52:29 +01:00
Aidan Timson 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 Timson f86c3e7856 Remove unused "app" item from quick search (#29244) 2026-01-29 17:52:27 +01:00
Aidan Timson 1d0251cc28 Fixes for picker combo box scrolling and selection (#29242) 2026-01-29 17:52:26 +01:00
Wendelin 518cf87847 Fix quick search apps (#29238) 2026-01-29 17:52:25 +01:00
ildar170975 81a9216c44 computeGroupEntitiesState(): fix condition (#29234)
* fix condition

* fix condition

* prettier
2026-01-29 17:52:24 +01:00
Paul Bottein f0e10e0058 Fix default yaml lovelace panel loading (#29230) 2026-01-29 17:52:23 +01:00
Paul Bottein 5df8ea4f07 Add welcome banner for new overview dashboard (#29223) 2026-01-29 17:52:22 +01:00
Aidan Timson 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 Petrov 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
336 changed files with 12799 additions and 6244 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": "yarn@4.12.0",
"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
-507
View File
@@ -1,507 +0,0 @@
import type { TemplateResult } from "lit";
import { css, html, unsafeCSS } from "lit";
import { normalizeSearchText, splitSearchTerms } from "./search-query";
export interface HighlightRange {
start: number;
end: number;
}
interface NormalizedIndexMap {
normalizedText: string;
normalizedIndexMap: HighlightRange[];
}
export type HighlightedText =
| string
| TemplateResult
| (string | TemplateResult)[]
| null
| undefined;
const HIGHLIGHT_NAME_PREFIX = "ha-search";
// Shared selector so range extraction and mutation checks stay in sync.
const HIGHLIGHT_MARK_SELECTOR = "mark.ha-highlight";
const tokenizeSearchQuery = (query: string): string[] => [
...new Set(splitSearchTerms(query)),
];
/**
* Build normalized text and an index map back to original indexes.
* Needed because normalization can change character length/index positions.
*/
const buildNormalizedIndexMap = (
text: string,
language?: string
): NormalizedIndexMap => {
let normalizedText = "";
const normalizedIndexMap: HighlightRange[] = [];
let originalIndex = 0;
for (const char of text) {
const start = originalIndex;
const end = start + char.length;
const normalizedChar = normalizeSearchText(char, language);
normalizedText += normalizedChar;
// One original character can normalize into multiple UTF-16 code units.
// Keep a mapping entry for each normalized code unit because String#indexOf
// and String#length operate on UTF-16 indexes.
for (const _codeUnit of normalizedChar.split("")) {
normalizedIndexMap.push({ start, end });
}
originalIndex = end;
}
return { normalizedText, normalizedIndexMap };
};
const mergeHighlightRanges = (ranges: HighlightRange[]): HighlightRange[] => {
if (!ranges.length) {
return [];
}
const sortedRanges = [...ranges].sort((a, b) => a.start - b.start);
const mergedRanges: HighlightRange[] = [{ ...sortedRanges[0] }];
// Merge overlapping/adjacent ranges so the rendered marks stay minimal.
for (let i = 1; i < sortedRanges.length; i++) {
const previousRange = mergedRanges[mergedRanges.length - 1];
const currentRange = sortedRanges[i];
if (currentRange.start <= previousRange.end) {
previousRange.end = Math.max(previousRange.end, currentRange.end);
continue;
}
mergedRanges.push({ ...currentRange });
}
return mergedRanges;
};
/**
* Convert rendered `<mark>` nodes into DOM Ranges for `CSS.highlights`.
* We walk text nodes because Lit templates can place comment markers before
* text inside `<mark>`, so `firstChild` is not reliably the text node.
*/
const getHighlightRangesFromMarks = (root: ShadowRoot): Range[] => {
const ranges: Range[] = [];
root.querySelectorAll(HIGHLIGHT_MARK_SELECTOR).forEach((mark) => {
const textWalker = document.createTreeWalker(mark, NodeFilter.SHOW_TEXT);
let textNode = textWalker.nextNode();
while (textNode) {
const text = textNode.textContent;
if (text) {
const range = new Range();
range.setStart(textNode, 0);
range.setEnd(textNode, text.length);
ranges.push(range);
}
textNode = textWalker.nextNode();
}
});
return ranges;
};
const createHighlightStyle = (highlightName: string): string => css`
.ha-highlight {
/* Visual highlight comes from ::highlight(...), not the <mark> itself. */
background-color: transparent;
color: inherit;
border-radius: 0;
padding: 0;
box-shadow: none;
}
::highlight(${unsafeCSS(highlightName)}) {
background-color: var(
--ha-highlight-bg,
var(--ha-color-fill-primary-normal-hover)
);
color: var(--ha-highlight-color, var(--primary-text-color));
}
`.cssText;
const renderHighlightedParts = (
text: string,
ranges: HighlightRange[]
): (string | TemplateResult)[] => {
const parts: (string | TemplateResult)[] = [];
let previousIndex = 0;
for (const range of ranges) {
if (range.start > previousIndex) {
parts.push(text.slice(previousIndex, range.start));
}
parts.push(
html`<mark class="ha-highlight"
>${text.slice(range.start, range.end)}</mark
>`
);
previousIndex = range.end;
}
if (previousIndex < text.length) {
parts.push(text.slice(previousIndex));
}
return parts;
};
/**
* Search highlighting helper with two integration paths:
* 1) call `renderHighlightedText` + `applyFromMarks` when updates are driven
* by known state changes (like filter changes),
* 2) call `startAutoSyncFromMarks` when highlighted DOM can change
* independently of filter changes (like virtualized rows).
*/
export class SearchHighlight {
// `CSS.highlights` is document-global, not per shadow root.
// Each instance needs a unique key so that components do not overwrite each
// other's highlight ranges.
private static _nextHighlightId = 0;
// Fingerprints include Node identity, so map nodes to stable numeric IDs.
private static _nodeIds = new WeakMap<Node, number>();
private static _nextNodeId = 0;
// Cache the last apply inputs to avoid re-registering identical highlights.
private _lastCacheKey?: string;
private _lastFingerprint?: string;
private readonly _highlightName?: string;
private _autoSyncObserver?: MutationObserver;
private _autoSyncQueued = false;
private _autoSyncCacheKeyProvider?: () => string | null | undefined;
private _autoSyncObservedTarget?: Node;
public constructor(private readonly _root?: ShadowRoot) {
if (this._root) {
this._highlightName = `${HIGHLIGHT_NAME_PREFIX}-${SearchHighlight._nextHighlightId++}`;
this._addHighlightStyle();
}
}
/**
* Return text ranges that should be highlighted for the given query.
* Useful when the caller needs ranges without rendering `<mark>` output.
*/
public getHighlightRanges(
text: string,
query: string,
language?: string
): HighlightRange[] {
if (!text) {
return [];
}
const terms = tokenizeSearchQuery(query);
if (!terms.length) {
return [];
}
const { normalizedText, normalizedIndexMap } = buildNormalizedIndexMap(
text,
language
);
// Text can normalize to empty (for example, combining marks only).
if (!normalizedText) {
return [];
}
const ranges: HighlightRange[] = [];
for (const term of terms) {
const normalizedTerm = normalizeSearchText(term, language);
// Some tokens normalize to empty (like combining marks); skip them.
if (!normalizedTerm) {
continue;
}
let matchIndex = normalizedText.indexOf(normalizedTerm);
while (matchIndex !== -1) {
// Convert normalized-text match indexes back to original-text indexes.
// `indexOf` guarantees the full normalized term is within bounds, and
// we append one mapping item per normalized UTF-16 code unit.
const start = normalizedIndexMap[matchIndex]!.start;
const end =
normalizedIndexMap[matchIndex + normalizedTerm.length - 1]!.end;
ranges.push({ start, end });
matchIndex = normalizedText.indexOf(
normalizedTerm,
matchIndex + normalizedTerm.length
);
}
}
return mergeHighlightRanges(ranges);
}
/**
* Render plain text with matching segments wrapped in `<mark>`.
* `<mark>` nodes are used as stable anchors for range extraction.
*/
public renderHighlightedText(
text: string | null | undefined,
query: string | null | undefined,
language?: string
): HighlightedText {
if (!text) {
return text;
}
const ranges = this.getHighlightRanges(text, query ?? "", language);
if (!ranges.length) {
return text;
}
return renderHighlightedParts(text, ranges);
}
/**
* Read rendered `<mark>` nodes from the root and apply matching
* `CSS.highlights` ranges.
* `cacheKey` should represent the current query/filter used to build marks.
*/
public applyFromMarks(cacheKey?: string): void {
if (!this._root) {
return;
}
this.applyFromRanges(getHighlightRangesFromMarks(this._root), cacheKey);
}
/**
* Apply precomputed ranges directly to `CSS.highlights`.
* Use this when ranges are built outside this class.
* `cacheKey` should represent the inputs used to build `ranges`.
*/
public applyFromRanges(ranges: Range[], cacheKey?: string): void {
if (!this._root || !this._highlightName) {
return;
}
const highlightRegistry = globalThis.CSS?.highlights;
if (!highlightRegistry || typeof Highlight === "undefined") {
return;
}
if (!ranges.length) {
this.clear();
return;
}
const fingerprint = this._getRangesFingerprint(ranges);
// Skip writes only when both the caller key and concrete range positions
// are unchanged.
if (
cacheKey === this._lastCacheKey &&
fingerprint === this._lastFingerprint
) {
return;
}
this._lastCacheKey = cacheKey;
this._lastFingerprint = fingerprint;
highlightRegistry.set(this._highlightName, new Highlight(...ranges));
}
/**
* Auto-sync `CSS.highlights` from `<mark>` nodes whenever marked DOM changes.
* Use this for components where highlighted DOM can change without filter
* changes (for example, virtualized lists).
* `cacheKeyProvider` should return the current query/filter string.
* `observedTarget` allows callers to scope observation to a subtree.
*/
public startAutoSyncFromMarks(
cacheKeyProvider: () => string | null | undefined,
observedTarget?: Node
): void {
if (!this._root || !this._highlightName) {
return;
}
this._autoSyncCacheKeyProvider = cacheKeyProvider;
this._autoSyncObservedTarget = observedTarget ?? this._root;
if (!this._autoSyncObserver) {
this._autoSyncObserver = new MutationObserver((records) => {
if (
!records.some((record) => this._mutationAffectsHighlights(record))
) {
return;
}
this._queueAutoSyncFromMarks();
});
}
this._autoSyncObserver.disconnect();
this._autoSyncObserver.observe(this._autoSyncObservedTarget, {
childList: true,
subtree: true,
characterData: true,
});
this._queueAutoSyncFromMarks();
}
/**
* Stop auto-sync started via `startAutoSyncFromMarks`.
*/
public stopAutoSyncFromMarks(): void {
this._autoSyncObserver?.disconnect();
this._autoSyncQueued = false;
this._autoSyncCacheKeyProvider = undefined;
this._autoSyncObservedTarget = undefined;
}
public clear(): void {
if (!this._root) {
return;
}
globalThis.CSS?.highlights?.delete(this._highlightName!);
this._lastCacheKey = undefined;
this._lastFingerprint = undefined;
}
private _getNodeId(node: Node): number {
let nodeId = SearchHighlight._nodeIds.get(node);
if (nodeId !== undefined) {
return nodeId;
}
nodeId = SearchHighlight._nextNodeId++;
SearchHighlight._nodeIds.set(node, nodeId);
return nodeId;
}
/**
* Build a stable signature for a set of ranges so we can detect real range
* changes even when the count stays the same.
*/
private _getRangesFingerprint(ranges: Range[]): string {
return ranges
.map((range) => {
const startNodeId = this._getNodeId(range.startContainer);
const endNodeId = this._getNodeId(range.endContainer);
return `${startNodeId}:${range.startOffset}-${endNodeId}:${range.endOffset}`;
})
.join("|");
}
private _queueAutoSyncFromMarks(): void {
if (this._autoSyncQueued) {
return;
}
this._autoSyncQueued = true;
// Coalesce bursts of mutations into a single highlight recomputation.
queueMicrotask(() => {
this._autoSyncQueued = false;
if (!this._root || !(this._root.host as HTMLElement).isConnected) {
return;
}
const cacheKey = this._autoSyncCacheKeyProvider?.()?.trim();
if (!cacheKey) {
this.clear();
return;
}
this.applyFromMarks(cacheKey);
});
}
private _mutationAffectsHighlights(mutation: MutationRecord): boolean {
if (mutation.type === "characterData") {
return this._nodeContainsHighlightMark(mutation.target);
}
if (mutation.type !== "childList") {
return false;
}
if (this._nodeContainsHighlightMark(mutation.target)) {
return true;
}
for (const node of mutation.addedNodes) {
if (this._nodeContainsHighlightMark(node)) {
return true;
}
}
for (const node of mutation.removedNodes) {
if (this._nodeContainsHighlightMark(node)) {
return true;
}
}
return false;
}
/**
* Returns true when a node is a highlight mark, contains one, or is a text/comment
* node inside one. The text/comment case covers Lit marker nodes.
*/
private _nodeContainsHighlightMark(node: Node): boolean {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element;
return (
element.matches(HIGHLIGHT_MARK_SELECTOR) ||
Boolean(element.querySelector(HIGHLIGHT_MARK_SELECTOR))
);
}
if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
return Boolean(
(node as DocumentFragment).querySelector?.(HIGHLIGHT_MARK_SELECTOR)
);
}
if (
node.nodeType === Node.TEXT_NODE ||
node.nodeType === Node.COMMENT_NODE
) {
const parentElement = (node as ChildNode).parentElement;
return Boolean(parentElement?.closest(HIGHLIGHT_MARK_SELECTOR));
}
return false;
}
/**
* Inject marker styles and `::highlight()` theme colors.
*/
private _addHighlightStyle(): void {
if (!this._root || !this._highlightName) {
return;
}
const style = document.createElement("style");
style.textContent = createHighlightStyle(this._highlightName);
this._root.appendChild(style);
}
}
-13
View File
@@ -1,13 +0,0 @@
import { stripDiacritics } from "./strip-diacritics";
/**
* Normalize text for search comparisons (case-insensitive + diacritics-insensitive).
*/
export const normalizeSearchText = (text: string, language?: string): string =>
stripDiacritics(text).toLocaleLowerCase(language);
/**
* Split a user query into whitespace-delimited search terms.
*/
export const splitSearchTerms = (query: string): string[] =>
query.trim().split(/\s+/).filter(Boolean);
+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() {
+26 -126
View File
@@ -17,7 +17,6 @@ import { STRINGS_SEPARATOR_DOT } from "../../common/const";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare";
import { SearchHighlight } from "../../common/string/search-highlight";
import type { LocalizeFunc } from "../../common/translations/localize";
import { debounce } from "../../common/util/debounce";
import { groupBy } from "../../common/util/group-by";
@@ -87,6 +86,7 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
flex?: number;
forceLTR?: boolean;
hidden?: boolean;
lastFixed?: boolean;
}
export type ClonedDataTableColumnData = Omit<DataTableColumnData, "title"> & {
@@ -118,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}
@@ -136,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;
@@ -179,8 +174,6 @@ export class HaDataTable extends LitElement {
private _lastUpdate = 0;
private _searchHighlight?: SearchHighlight;
// @ts-ignore
@restoreScroll(".scroller") private _savedScrollPos?: number;
@@ -237,36 +230,21 @@ export class HaDataTable extends LitElement {
// Force update of location of rows
this._filteredData = [...this._filteredData];
}
// Re-attach observer when the element reconnects.
if (this.hasUpdated) {
this._updateSearchHighlightSync();
}
}
public disconnectedCallback() {
super.disconnectedCallback();
this._searchHighlight?.stopAutoSyncFromMarks();
this._searchHighlight?.clear();
}
protected firstUpdated() {
this.updateComplete.then(() => this._calcTableHeight());
this._updateSearchHighlightSync();
}
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("_filter")) {
this._updateSearchHighlightSync();
}
protected updated() {
const header = this.renderRoot.querySelector(".mdc-data-table__header-row");
if (header) {
if (header.scrollWidth > header.clientWidth) {
this.style.setProperty("--table-row-width", `${header.scrollWidth}px`);
} else {
this.style.removeProperty("--table-row-width");
}
if (!header) {
return;
}
if (header.scrollWidth > header.clientWidth) {
this.style.setProperty("--table-row-width", `${header.scrollWidth}px`);
} else {
this.style.removeProperty("--table-row-width");
}
}
@@ -377,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;
@@ -412,7 +395,6 @@ export class HaDataTable extends LitElement {
.hass=${this.hass}
@value-changed=${this._handleSearchChange}
.label=${this.searchLabel}
.noLabelFloat=${this.noLabelFloat}
></search-input>
</div>
`
@@ -446,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>
@@ -535,7 +517,6 @@ export class HaDataTable extends LitElement {
this._filteredData,
localize,
this.appendRow,
this.hasFab,
this.groupColumn,
this.groupOrder,
this._collapsedGroups,
@@ -638,12 +619,7 @@ export class HaDataTable extends LitElement {
${column.template
? column.template(row)
: narrow && column.main
? html`<div class="primary">
${this._renderValueWithHighlight(
row[key],
column.filterable
)}
</div>
? html`<div class="primary">${row[key]}</div>
<div class="secondary">
${Object.entries(columns)
.filter(
@@ -661,21 +637,15 @@ export class HaDataTable extends LitElement {
([key2, column2], i) =>
html`${i !== 0
? STRINGS_SEPARATOR_DOT
: nothing}${this._renderCellValue(
column2,
key2,
row
)}`
: nothing}${column2.template
? column2.template(row)
: row[key2]}`
)}
</div>
${column.extraTemplate
? column.extraTemplate(row)
: nothing}`
: html`${this._renderCellValue(
column,
key,
row
)}${column.extraTemplate
: html`${row[key]}${column.extraTemplate
? column.extraTemplate(row)
: nothing}`}
</div>
@@ -685,69 +655,6 @@ export class HaDataTable extends LitElement {
`;
};
private _renderCellValue(
column: DataTableColumnData,
key: string,
row: DataTableRowData
) {
if (column.template) {
return column.template(row);
}
return this._renderValueWithHighlight(row[key], column.filterable);
}
private _renderValueWithHighlight(
value: unknown,
filterable?: boolean
): unknown {
if (!filterable) {
return value;
}
const filter = this._filter.trim();
if (!filter) {
return value;
}
if (typeof value !== "string" && typeof value !== "number") {
return value;
}
const text = String(value);
return this._getSearchHighlight().renderHighlightedText(
text,
filter,
this.hass.locale.language
);
}
private _getSearchHighlight(): SearchHighlight {
if (!this._searchHighlight) {
this._searchHighlight = new SearchHighlight(
this.renderRoot as ShadowRoot
);
}
return this._searchHighlight;
}
private _updateSearchHighlightSync(): void {
if (!this._filter.trim()) {
this._searchHighlight?.stopAutoSyncFromMarks();
this._searchHighlight?.clear();
return;
}
const observedTarget =
this.renderRoot.querySelector("lit-virtualizer") ||
(this.renderRoot as ShadowRoot);
this._getSearchHighlight().startAutoSyncFromMarks(
() => this._filter,
observedTarget
);
}
private async _sortFilterData() {
const startTime = new Date().getTime();
const timeBetweenUpdate = startTime - this._lastUpdate;
@@ -806,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) {
@@ -903,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 }];
}
);
@@ -961,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,
@@ -1179,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)
);
}
@@ -3,10 +3,7 @@ import type { FuseOptionKey, IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import memoizeOne from "memoize-one";
import { ipCompare, stringCompare } from "../../common/string/compare";
import {
normalizeSearchText,
splitSearchTerms,
} from "../../common/string/search-query";
import { stripDiacritics } from "../../common/string/strip-diacritics";
import type {
ClonedDataTableColumnData,
DataTableRowData,
@@ -50,10 +47,10 @@ const getSearchableValue = (
const stringValues = value
.filter((item) => item != null && typeof item !== "object")
.map(String);
return normalizeSearchText(stringValues.join(" "));
return stripDiacritics(stringValues.join(" ").toLowerCase());
}
return normalizeSearchText(String(value));
return stripDiacritics(String(value).toLowerCase());
};
/** Filters data using exact substring matching (all terms must match). */
@@ -144,7 +141,7 @@ const filterData = (
columns: SortableColumnContainer,
filter: string
): DataTableRowData[] => {
const normalizedFilter = normalizeSearchText(filter).trim();
const normalizedFilter = stripDiacritics(filter.toLowerCase().trim());
if (!normalizedFilter) {
return data;
@@ -156,7 +153,7 @@ const filterData = (
return data;
}
const terms = splitSearchTerms(normalizedFilter);
const terms = normalizedFilter.split(/\s+/);
// First, try exact substring matching
const exactMatches = filterDataExact(data, filterKeys, terms);
@@ -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);
+13 -25
View File
@@ -12,7 +12,6 @@ import {
state as litState,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { SearchHighlight } from "../common/string/search-highlight";
interface State {
bold: boolean;
@@ -34,8 +33,6 @@ export class HaAnsiToHtml extends LitElement {
@litState() private _filter = "";
private _searchHighlight?: SearchHighlight;
protected render(): TemplateResult {
return html`<pre class=${classMap({ wrap: !this.wrapDisabled })}></pre>`;
}
@@ -49,11 +46,6 @@ export class HaAnsiToHtml extends LitElement {
}
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this._searchHighlight?.clear();
}
static styles = css`
pre {
margin: 0;
@@ -122,6 +114,11 @@ export class HaAnsiToHtml extends LitElement {
.bg-white {
background-color: rgb(204, 204, 204);
}
::highlight(search-results) {
background-color: var(--primary-color);
color: var(--text-primary-color);
}
`;
/**
@@ -326,29 +323,30 @@ export class HaAnsiToHtml extends LitElement {
this._filter = filter;
const lines = this.shadowRoot?.querySelectorAll("div") || [];
let numberOfFoundLines = 0;
const filterLower = filter.toLowerCase();
if (!filter) {
lines.forEach((line) => {
line.style.display = "";
});
numberOfFoundLines = lines.length;
this._searchHighlight?.clear();
if (CSS.highlights) {
CSS.highlights.delete("search-results");
}
} else {
const highlightRanges: Range[] = [];
lines.forEach((line) => {
if (!line.textContent?.toLowerCase().includes(filterLower)) {
if (!line.textContent?.toLowerCase().includes(filter.toLowerCase())) {
line.style.display = "none";
} else {
line.style.display = "";
numberOfFoundLines++;
if (line.firstChild !== null && line.textContent) {
if (CSS.highlights && line.firstChild !== null && line.textContent) {
const spansOfLine = line.querySelectorAll("span");
spansOfLine.forEach((span) => {
const text = span.textContent.toLowerCase();
const indices: number[] = [];
let startPos = 0;
while (startPos < text.length) {
const index = text.indexOf(filterLower, startPos);
const index = text.indexOf(filter.toLowerCase(), startPos);
if (index === -1) break;
indices.push(index);
startPos = index + filter.length;
@@ -364,11 +362,8 @@ export class HaAnsiToHtml extends LitElement {
}
}
});
if (this.shadowRoot) {
this._getSearchHighlight(this.shadowRoot).applyFromRanges(
highlightRanges,
filter
);
if (CSS.highlights) {
CSS.highlights.set("search-results", new Highlight(...highlightRanges));
}
}
@@ -380,13 +375,6 @@ export class HaAnsiToHtml extends LitElement {
this._pre.innerHTML = "";
}
}
private _getSearchHighlight(root: ShadowRoot): SearchHighlight {
if (!this._searchHighlight) {
this._searchHighlight = new SearchHighlight(root);
}
return this._searchHighlight;
}
}
declare global {
+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;

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