Compare commits

...

203 Commits

Author SHA1 Message Date
Pavilion Sahota
7b83e0f44c Implemented top bar fixed for climate lights and security panels 2026-02-13 10:55:36 +00:00
Pavilion Sahota
410df52a9a Refactored duplicate header styling 2026-02-13 10:55:13 +00:00
Paul Bottein
a45ef6e019 Light dashboard toggle area (#29363)
* Add toggle lights button on light dashboard

* Use not

* Use group card on desktop
2026-02-13 09:16:13 +02:00
Tom Carpenter
b256fc820d Fix compare button bug in energy-period-selector (#29611)
Fix bug in energy-period-selector

When the dropdown item for compare is disabled, the click callback still fires, allowing compare to be selected. So remove the callback for disabled buttons.

Also fix a slight visual bug in the ha-ripple border radius.
2026-02-13 09:02:43 +02:00
Wendelin
67fb7f61c2 Remove ha-divider and replace with wa-divider (#29609)
* Replace ha-divider with wa-divider in onboarding and dashboard components

* Refactor styles in onboarding-welcome component for consistency and responsiveness
2026-02-12 19:03:28 +01:00
Marcin Bauer
1b3ea3d55d Move automation help icons from sections to dialogs (#29584)
Co-authored-by: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Wendelin <w@pe8.at>
2026-02-12 15:42:44 +00:00
Steven Travers
3f724aba4f Fix kiosk mode not opening sidebar on toggle menu event (#29604) 2026-02-12 09:54:46 -05:00
Aidan Timson
7e225ed8a7 Migrate onboarding dialogs to wa (#29601)
* Migrate onboarding dialog(s) to wa

* Remove hass

* Allow hass to be uninitialised in wa dialog

* Use standard width for overflowing text
2026-02-12 15:35:32 +01:00
Wendelin
bd78800d8a Fix ha-dropdown-item selected hover (#29602)
Add hover effect for selected dropdown item
2026-02-12 15:32:35 +01:00
Aidan Timson
13d40993ef Change state card button/input_button to control button (#29367)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-02-12 12:41:56 +00:00
Paul Bottein
1ad74d46d6 Improve PR template and add AI agent instructions (#29590)
* Improve PR template and add AI agent instructions

Move screenshots section closer to proposed change for better flow, and add instructions in copilot-instructions.md to ensure AI agents use the PR template when creating pull requests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Instruct AI agents to not check checklist items on behalf of the user

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remind user to add screenshots for UI changes after creating PR

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 14:33:54 +02:00
Aidan Timson
4a128c904e Migrate enter code dialog to wa (#29592)
* Move enter code dialog migration to dedicated branch

* Fix

* Make small

* Small
2026-02-12 14:33:12 +02:00
Paul Bottein
e414bab746 Improve default dashboard confirmation dialog (#29596) 2026-02-12 12:58:34 +01:00
ildar170975
a354026780 Migrate more "ha-settings-row" to "ha-md-list-item" (#29508)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
Co-authored-by: Wendelin <w@pe8.at>
2026-02-12 09:54:49 +01:00
Matthias Alphart
24014116dc Add missing import in hass-tabs-subpage-data-table (#29591) 2026-02-12 08:49:09 +00:00
Aidan Timson
b07b604a20 Migrate zone config dialogs to wa (#29581)
* Migrate config-zone dialog(s) to wa

* Move cancel to new item, only delete if editing
2026-02-12 08:33:48 +00:00
Marcin Bauer
81525a2b14 Hide actions in attributes view (#29580)
* Hide actions in attributes view

Simplify the isDefaultView condition to hide actions when child views are shown in the more-info dialog.

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>

* Remove keepHeader option from ChildView interface

Completely removes the keepHeader property which was used to keep the header visible in child views. This simplifies the logic and ensures actions are consistently hidden across all child views, including the attributes view.

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2026-02-12 09:23:45 +01:00
Paulus Schoutsen
3b6babe0be Reorganize profile localization settings into dedicated card (#29496)
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2026-02-12 09:06:10 +01:00
Aidan Timson
432e8fc0d7 Migrate zeroconf discovery info dialog to wa (#29579)
* Migrate config-zeroconf dialog(s) to wa

* Cleanup
2026-02-11 18:06:58 +01:00
Aidan Timson
2e350d24f5 Migrate ssdp config dialogs to wa (#29582)
* Migrate config-ssdp dialog(s) to wa

* Cleanup
2026-02-11 18:01:34 +01:00
Aidan Timson
024e953c89 Migrate thread config dialog to wa (#29578)
* Migrate config-thread dialog(s) to wa

* Cleanup
2026-02-11 17:48:03 +01:00
Aidan Timson
c29401a5a5 Migrate tags config dialog to wa (#29577)
* Migrate config-tags dialog(s) to wa

* Prevent scrim close
2026-02-11 17:28:26 +01:00
Aidan Timson
f913677dfe Migrate storage config dialogs to wa (#29576)
Migrate config-storage dialog(s) to wa
2026-02-11 17:21:31 +01:00
Aidan Timson
3b3f8f3343 Migrate integrations dialogs to wa (#29567)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-02-11 12:48:41 +00:00
Aidan Timson
03917a5e1c Migrate energy config dialogs to wa (#29565)
Migrate config-energy dialog(s) to wa
2026-02-11 13:51:57 +02:00
Paul Bottein
5e3002f739 Align pull request template with core (#29573) 2026-02-11 13:50:18 +02:00
ildar170975
da4395b8b3 Entity card: allow a reversed order for "value" & "unit" (#29407)
* allow a reversed order

* formatEntityStateToParts() does not return "order"

* resolving conflicts

* fixed styles
2026-02-11 12:37:41 +01:00
ildar170975
16609053ac ha-fab: fix styles for disabled (#29446)
* Update ha-fab.ts

* fix styles for "disabled"

* fix styles

* do not change opacity

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>

* revert & simplify

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2026-02-11 12:21:22 +01:00
Aidan Timson
7beadb8108 Migrate person and user dialogs to wa (#29569) 2026-02-11 11:07:55 +00:00
Aidan Timson
8ca169fc23 Migrate hardware avaliable dialog to wa (#29566) 2026-02-11 12:07:39 +01:00
Aidan Timson
6502c14fad Migrate logs detail dialog to wa (#29568) 2026-02-11 11:52:19 +01:00
Aidan Timson
3d4b10caab Migrate cloud dialog to wa (#29563) 2026-02-11 11:40:33 +01:00
Aidan Timson
20bfe8b633 Migrate statistics dialogs to wa (#29437)
* Migrate config-developer-tools dialog(s) to wa

* Add space

* Remove duplicate method, fix variant typo

* Restore closeDialog
2026-02-11 12:01:45 +02:00
Aidan Timson
c6ecb5f217 Migrate mqtt device info dialog to wa (#29564)
Migrate config-devices dialog(s) to wa
2026-02-11 11:38:17 +02:00
Aidan Timson
938cc6a1a1 Migrate app store dialogs to wa, standardise registry actions (#29542)
* Migrate config-apps dialog(s) to wa

* Apply suggestions from code review

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

* Use standard buttons

* Use back action

* Remove extra close action

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-11 11:34:53 +02:00
Wendelin
1d1e05dbdf Migrate from ha-md-select to ha-select (#29551)
* Migrate from ha-md-select to ha-select across multiple components

* Review

* Remove --md-filled-field-content-space
2026-02-11 11:30:10 +02:00
Aidan Timson
a1b4923673 Migrate bluetooth device info dialog to wa (#29546)
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2026-02-11 10:00:16 +01:00
Aidan Timson
823a42f567 Add additional config areas to quick search (#29535)
* Add network discovery browsers to quick search

* Add cloud to quick search

* Add application credentials and mqtt, remove cloud as is a redirect
2026-02-11 09:46:26 +02:00
Aidan Timson
81275a96ff Migrate main calendar dialogs to wa (#29505)
* Migrate calendar dialog(s) to wa

* Remove width (too wide)

* Prevent scrim close
2026-02-11 09:26:11 +02:00
Paul Bottein
2e372b2f8a Add repairs and updates cards to home dashboard overview (#29552)
* Add repairs and updates cards to home dashboard overview

Add two new cards to the "For You" section of the home dashboard that display
links to repairs and updates when there are active issues or available updates.
Both cards are only visible to admin users and hide when empty.

https://claude.ai/code/session_013NTgs1U9x59uaEJs1smy8i

* Fix navigation and visibility

* Reorder

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-11 09:15:37 +02:00
sebcaps
541cc7d10b Add Percentage in distribution card tooltip (#29555)
Add Percentage in tooltip
2026-02-11 09:06:56 +02:00
Aidan Timson
678ee7e82a Migrate todo dialog to wa (#29527)
Migrate todo dialog(s) to wa
2026-02-10 20:38:45 +01:00
uptimeZERO_
ee72f4818d Add guard for failed JSON parsing of localStorage entries (#29502)
* Added guard for failed json parsing of selectedTheme key

* deleting corrupted keys and logging
2026-02-10 20:30:28 +01:00
karwosts
ccbd9c1f24 Don't show hidden todolists on todo panel (#29510) 2026-02-10 19:21:53 +01:00
ildar170975
84135a9424 hui-entity-editor: fix padding-top for ha-md-list (#29537)
fix padding-top for ha-md-list
2026-02-10 19:17:10 +01:00
Petar Petrov
4ebc334298 Normalize SI unit prefixes in distribution card proportions (#29539)
* Normalize SI unit prefixes in distribution card proportions

* Extract SI prefix normalization to shared utility with tests

Moves normalizeValueBySIPrefix to src/common/number/ so it can be
reused. Replaces the inline method in the distribution card and the
switch statement in getPowerFromState (energy.ts).
2026-02-10 19:16:02 +01:00
Petar Petrov
3c4c3e39e5 Fix storage space calculations to account for reserved system space (#29540) 2026-02-10 19:13:58 +01:00
Aidan Timson
1267003b42 Migrate local backup location dialog to wa (#29543)
Migrate config-backup dialog(s) to wa
2026-02-10 19:11:44 +01:00
Aidan Timson
733359c869 Fix position of 2 extra actions for date picker dialog (#29541)
Fix position of 2 extra actions for date picker
2026-02-10 19:08:55 +01:00
Aidan Timson
50b6e07ae5 Migrate import blueprint dialog to wa (#29544)
* Migrate config-blueprint dialog(s) to wa

* Add vt
2026-02-10 19:00:58 +01:00
Wendelin
1b62a7cff8 Dropdown item add selected prop (#29553)
Refactor dropdown item selection handling to use property binding for selected state
2026-02-10 18:55:38 +01:00
Wendelin
c293cf56f6 Migrate from ha-md-menu to ha-dropdown (#29548) 2026-02-10 15:33:49 +00:00
Aidan Timson
2ec6f3615d Use ValueChangedEvent for more CustomEvents (#29547) 2026-02-10 16:26:19 +01:00
Wendelin
d3b92059e5 Re-enable autofocus for iOS in ha-wa-dialog (#29534) 2026-02-10 15:09:33 +01:00
ildar170975
72e69f6291 Entities card: add “area” for “secondary_info” (#29268)
* add "area/floor" labels for secondary-info for Entities card

* use STRINGS_SEPARATOR_DOT

* add "area/floor" for secondary-info

* add "area/floor" for secondary-info

* add "area/floor" for secondary-info

* use STRINGS_SEPARATOR_DOT

* use STRINGS_SEPARATOR_DOT

* use STRINGS_SEPARATOR_DOT

* add STRINGS_SEPARATOR_DOT

* changed an order & renamed an option

* fixed name for "area with floor"

* chaged an order & renamed an entry

* renamed "area with floor" entry

* add STRINGS_SEPARATOR_DOT

* Delete src/common/strings-separator.ts

* change import

* change import

* change import

* change import

* change import

* fix import

* fix import

* fix import

* remove "area-with-floor"

* remove "area-with-floor"

* remove "area-with-floor"

* remove unneeded comma

* remove "area-with-floor"

* clean up

* typo

* Apply suggestion from @MindFreeze

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

* move area definition into a separate method

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-10 16:06:00 +02:00
Aidan Timson
3bff97595f Migrate date picker dialog to wa (#29506) 2026-02-10 14:52:01 +01:00
renovate[bot]
19b1d03cd1 Update Node.js to v24.13.1 (#29538)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 13:34:20 +00:00
Petar Petrov
f04557688c Use unique SVG mask IDs in graph base to fix rendering on Samsung WebView (#29525)
* Use unique SVG mask IDs in graph base to fix rendering on Samsung WebView

* Remove unused IDs
2026-02-10 14:23:52 +01:00
Matthias de Baat
d6953ea1bc Make storage charts consistent with lifetime chart (#29526)
* Make storage charts consistent with lifetime chart

* Update src/panels/config/storage/storage-breakdown-chart.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-10 13:12:53 +00:00
ildar170975
4501db18c7 Entities card editor: fix margin-top for ha-entity-picker (#29536)
* fix margin-top for ha-entity-picker

* Apply suggestion from @MindFreeze

* use ha-space var

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-10 13:10:40 +00:00
ildar170975
c68bcc5b32 Entity card: fix unit for monetary (#29406)
* fix unit for monetary

* fix for span

* formatEntityStateToParts() does not return "order"

* get unit from formatEntityStateToParts()

* resolving conflicts

* resolving conflicts

* some styling
2026-02-10 15:09:43 +02:00
Aidan Timson
3753c7d313 Migrate helper dialogs to wa (#29507)
* Migrate config-helpers dialog(s) to wa

* Stop secondary dialogs from automatically closing

* Styles

* Fix initial focus
2026-02-10 14:54:52 +02:00
Aidan Timson
4a2ef18375 Migrate media browser dialog to wa (#29529) 2026-02-10 13:28:48 +01:00
Wendelin
051da41eec Update @home-assistant/webawesome to version 3.2.1-ha.0 (#29533)
update @home-assistant/webawesome to version 3.2.1-ha.0
2026-02-10 13:25:53 +01:00
Aidan Timson
9b1a679f21 Add missing AI tasks item to quick search (#29531) 2026-02-10 13:10:35 +01:00
Aidan Timson
905a0f957c Migrate system information and startup time dialogs to wa (#29532) 2026-02-10 13:08:08 +01:00
Wendelin
d9a687b79c Hardware panel imporve chart loading UI (#29528) 2026-02-10 10:42:57 +00:00
karwosts
3b56497134 Hide more info weather forecast when unsupported (#29517) 2026-02-10 09:28:52 +00:00
Paul Bottein
ed0ec871ce Don't close automation and script sidebar on save (#29434) 2026-02-10 10:07:31 +01:00
ildar170975
29f5362182 hui-graph-header-footer: add own action handler (#29522)
* add action handler

* set action handler for ":host"
2026-02-10 11:03:31 +02:00
Matthias de Baat
5096bab26c Move AI task to its own page and change General into Home information (#29458)
* Move AI task to its own page and change General into Home information

* Fixed unused state properties for form submission states, removed unused imports and obsolete CSS, replaced hardcoded pixel values with spacing tokens, and added error handling for the map component.

* Update src/panels/config/core/ha-config-section-general.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-10 06:45:26 +00:00
Yosi Levy
ce2892cab9 Various RTL fixes (#29520) 2026-02-10 06:59:18 +01:00
renovate[bot]
57748c15de Update dependency @codemirror/commands to v6.10.2 (#29518)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 06:58:46 +01:00
Paul Bottein
ecb6a33c86 Add color settings for entity in distribution card (#29436)
* Add color settings for entity in distribution card

* Fix name

* Fix name again...
2026-02-09 16:15:14 +01:00
ildar170975
d34921ff6d state-card-input_number: fix for narrow viewport (#29327)
state-info: fix for narrow viewport
2026-02-09 15:30:40 +01:00
ildar170975
e6c9e81082 Add formatEntityStateToParts() + use it for hui-entity-card & ha-state-label-badge (#29239)
* add formatEntityStateToParts

* add formatEntityStateToParts

* use formatEntityStateToParts

* add formatEntityStateToParts

* use formatEntityStateToParts

* add formatEntityStateToParts

* add formatEntityStateToParts

* add computeStateDisplayToParts

* update for monetary

* fix a test for monetary

* fixed test for monetary

* do not include "order" into result

* do not include "order" into result

* do not include "order" into result

* do not include "order" into result

* do not include "order" into result

* do not include "order" into result

* do not include "order" into result

* simplify

* ensure less conflicts in future merges

* Refactor monetary computing

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2026-02-09 15:11:05 +01:00
Petar Petrov
aa13c6fa53 Add tests for energy chart functions (#29504) 2026-02-09 15:07:11 +01:00
ildar170975
d47738aa24 developer-tools-debug: migrate "ha-settings-row" to "ha-md-list-item" (#29501)
* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* background: 0 -> background: none
2026-02-09 14:25:23 +01:00
ildar170975
d473ee1084 User profile: migrate toggle rows "ha-settings-row" to "ha-md-list-item" (#29497)
* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* ha-settings-row -> ha-md-list-item

* remove unneeded "narrow"

* add import

* background: 0 -> background: none
2026-02-09 13:52:19 +01:00
Aidan Timson
0372ed932f Migrate new dashboard dialog to wa (#29438)
* Migrate config-dashboard dialog(s) to wa

* Restructure for scrolling
2026-02-09 14:08:04 +02:00
Aidan Timson
2baa044db5 Migrate profile dialogs to wa, refactor LL access token dialog (#29440)
* Migrate profile dialog(s) to wa

* Make sure code is entered before submit is allowed

* Refactor dialog

* Remove unused params

* Pass existing names and validate name is not already used

* Reduce cleanup on show

* Make QR image larger

* max width

* Fix

* Remove extra event fire

* Make params required

* cleanup

* Cleanup

* Fix

* Fix
2026-02-09 12:54:02 +01:00
Wendelin
3d04046bcc Migrate automation picker row to ha-dropdown (#29428)
* Update @home-assistant/webawesome to version 3.2.1 and refactor ha-dropdown integration in automation picker

* review

* revert wa update

* Update @home-assistant/webawesome to version 3.0.0-ha.2 in yarn.lock
2026-02-09 12:35:49 +01:00
Tom Carpenter
8e860cb17d Improve energy dashboard monthly/this-quarter chart time axes (#29435)
* Add splitNumber option to monthly ECharts

When there are a small number of bars (<=3) for monthly data, set the splitNumber parameter to force the date x-axis to show whole months.

* Add axis tick fomratting for short months

This ensures that the month format is consistent between 2/3 month and longer ranges.

* Avoid calling getSuggestedMax twice

* Fix another case of power chart cutting off last hour of data

The previous fix only solved the problem for 5-minute data, not hourly or daily. This should solve the issue regardless, and allows the energy chart to have other line-based plots in the future.

* Update other uses of getSuggestedMax()

* Fix statistics-chart Last Period Rendering

1. When appending the "current state" value, if the current time intersects with the final period, we can end up with the chart folding back on itself. This is fixed by ensuring for the final period we push the earlier of the statistic end time and the display end time (which is in turn limited to now).

2. Always close off the last data point at the chart end time. Otherwise for line charts, the final period doesn't get rendered.

* Remove unused monthStyle formatter.

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

* Rename getSuggestedMax function parameter in energy chart

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

* Document magic numbers in montly energy chart

* Make padding a constant for clarity.
* Explain the purpose of splitNumber.

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-09 13:05:42 +02:00
Petar Petrov
c41d7ff923 Fix history-graph card rendering stale data point on left edge (#29499)
When HistoryStream.processMessage() prunes expired history and preserves
the last expired state as a boundary marker, it updates lu (last_updated)
but not lc (last_changed). Chart components use lc preferentially, so
when lc is present the boundary point gets plotted at the original stale
timestamp far to the left of the visible window. Delete lc from the
boundary state so the chart uses the corrected lu timestamp.
2026-02-09 10:13:06 +01:00
Matthias de Baat
c22fc1021a Counter gravity effect on the Matter icon (#29459) 2026-02-09 10:05:06 +01:00
dependabot[bot]
6344233934 Bump github/codeql-action from 4.32.0 to 4.32.2 (#29498)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.0 to 4.32.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](b20883b0cd...45cbd0c69e)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-09 10:22:39 +02:00
ildar170975
23441d593b Data tables: filters: fix a placement for icon & text (#29488)
* fix padding & margin

* fix margin for icon

* fix margin for icon

* fix margin for image

* use ha-space-4

* use --ha-space-4

* use ha-space-1

* use ha-space-4
2026-02-09 08:57:00 +01:00
ildar170975
8393ed5fd4 voice-assistant-brand-icon: fixes for margin & alignment (#29493)
* fix styles

* fix a gap between logos

* fix a gap between logos

* fix right margin for logo

* fix right margin for logo

* show icons in flex

* remove unneeded style

* add right margin for logo
2026-02-09 08:54:45 +01:00
TheJulianJES
09afe9bb51 Fix ZHA dashboard using disabled and ignored config entries (#29494) 2026-02-09 08:18:43 +01:00
Benedikt Johannes
fb8d6062c5 Sugestion -> Suggestion (#29490)
* Update en.json

* Update ha-area-picker.ts

* Update ha-label-picker.ts

* Update ha-floor-picker.ts

* Update ha-category-picker.ts
2026-02-09 08:14:15 +01:00
karwosts
f93ae58b83 Fix dupl. id error in water-sankey (#29489) 2026-02-08 19:06:37 +01:00
karwosts
7626b26b2d No FAB in calendar-card (#29487) 2026-02-08 19:06:31 +01:00
renovate[bot]
a1bf30e501 Update dependency @rsdoctor/rspack-plugin to v1.5.2 (#29481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 08:41:36 +00:00
ildar170975
37a45d1729 voice-assistants-expose-assistant-icon: fix tooltip (#29469)
* fix tooltip

* provide id for tooltip for assistant icon
2026-02-08 09:32:12 +01:00
karwosts
6962a915a3 Fix describe legacy triggers in traces (#29473)
* Fix describe legacy triggers in traces

* remove unnecessary type
2026-02-07 22:05:05 +01:00
renovate[bot]
de3e2bcafa Update dependency glob to v13.0.1 (#29462)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 21:44:45 +01:00
Matthias de Baat
e732280b70 Add safe space at the bottom (#29454)
* Add safe space at the bottom

* Move margin-bottom from ha-config-analytics component to its parent wrapper in ha-config-section-analytics
2026-02-06 17:47:40 +01:00
renovate[bot]
4fb3453f73 Update dependency ua-parser-js to v2.0.9 (#29456)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 16:45:18 +00:00
renovate[bot]
0f9cb9c13e Update dependency @rspack/core to v1.7.5 (#29447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-06 12:23:27 +01:00
Matthias de Baat
7aa235c6af Fix Discord link for designers (#29393)
* Fix Discord link for designers

Updated Discord link for designers to the correct channel.

* Update Discord link for designers in home.markdown

* Update gallery/src/pages/concepts/home.markdown

Co-authored-by: Aidan Timson <aidan@timmo.dev>

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Aidan Timson <aidan@timmo.dev>
2026-02-05 21:09:28 +01:00
Aidan Timson
e8ddae8189 Migrate join beta dialog to wa (#29439)
Migrate config-updates dialog(s) to wa
2026-02-05 21:06:38 +01:00
Paul Bottein
c26e59f19c Fix theme and sidebar on demo (#29433) 2026-02-05 15:49:27 +01:00
Petar Petrov
83aa06cb18 Fix storage apps translation keys (#29432) 2026-02-05 15:43:47 +01:00
Tom Carpenter
9d3d0dac48 Fix energy dashboard date/time tooltip date labelling (#29431)
* Use Suggested Period for Energy Tooltip

Ensure the tooltips for energy charts match energy data grouping by using getSuggestedPeriod rather than hardcoded differenceInDays.

* Make getSuggestedMax return Date()

Currently used in two places - for energy charge ECOption, and for a statistics-graph. In both places a Date is expected rather than a Number. No point converting to a Number with getTime() when they are immediately converted back to a Date.
2026-02-05 15:40:12 +01:00
Aidan Timson
8da1154924 Delete ha-md-dialog (#29421) 2026-02-05 15:35:34 +01:00
Bram Kragten
eb588075b8 Use ha-alert for copyright of logo (#29429)
Use ha-alert for copyright of logo
2026-02-05 15:25:54 +01:00
Aidan Timson
bdeaf10d74 Migrate remaining backup dialogs dialog to wa (#29419)
Migrate backup dialogs dialog to wa
2026-02-05 15:24:05 +02:00
Aidan Timson
bec0d19fc9 Migrate favorite color picker to wa (more info) (#29373)
* Migrate favorite color picker to wa (more info)

* Remove cancel
2026-02-05 13:22:01 +00:00
Aidan Timson
325a7974c2 Migrate pick config entry dialog to wa (#29417) 2026-02-05 13:14:19 +00:00
Darren Griffin
fab1fde6e3 Update license text (#29423) 2026-02-05 14:08:18 +01:00
Norbert Rittel
0e9564e676 Make description of Map card consistent (#29420) 2026-02-05 15:06:26 +02:00
Aidan Timson
244eb75049 Migrate cloud support package dialog to wa (#29418) 2026-02-05 15:05:46 +02:00
Aidan Timson
644bb016d6 Migrate download logs dialog to wa (#29416) 2026-02-05 14:53:38 +02:00
Aidan Timson
02dbcf0946 Add context to quick bar, prioritise related entries (#29107)
* Add support for context in quick bar

* Send context from device page

* Use interface

* Prefetch and pass related result to dialog instead of loading on show (load on event call)

* Apply suggestion from @MindFreeze

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

* Add error

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

* Fix

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-05 12:48:02 +00:00
Aidan Timson
f7df4d8a90 Migrate join media players to wa (more info) (#29374)
* Migrate join media players to wa (more info)

* Fix padding
2026-02-05 14:00:01 +02:00
Aidan Timson
e00ced23ee Use ValueChangedEvent instead of CustomEvent (#29399)
* Use ValueChangedEvent with generic type requested

* Add more

* Add

* Add more
2026-02-05 13:55:43 +02:00
Aidan Timson
f5cc2104ef Refactor ha-select and ha-dropdown event handlers to use generic event types (#29397)
* Allow HaDropdownSelectEvent to pass the value type

* Fix potential type conflict

* Add clarification of type

* Fix type

* Create new type for ha-select

* Refactor

* Add clearable to only handle undefined when needed

* Value changed event

* Use clearable type

* Remove

* Profile section refactor

* Protocols refactor

* More config refactor

* Entity rows 1

* Remove unrelated

* Remove ValueChangedEvent changes (moved to separate branch)

* Revert

* Add

* Revert unrelated or extra checks

* Restore

* Restore

* Restore

* Update src/components/ha-conversation-agent-picker.ts

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

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-05 13:47:28 +02:00
Tom Carpenter
161dd26b4d Display Selected Year on Energy Date Picker (#29321)
* Show Year on Energy Dashboard Date Picker

When the selected range (start and/or end) is in a different year from the current one, show the year for that date.

* Correct Energy Picker Year Detection

Ensure that when checking if a range is a full year, the endpoints must be in the same calendar year.

Otherwise selecting a 12-month range that spans into two years would be treated as all being a single year.

* Add natural wrapping spans for date range

Encourage the range to wrap nicely if the text is too large for the toolbar.

* Use en dash between date range

The en dash character is usually used for date ranges rather than the
standard hypen

* Fix Now Button Rendering on Resize

Host element needs to be `display:block` no the default `display:inline` for the ResizeObserver to work.

* Remove P tag from date picker range label

Removing the paragraph tag and adding text-align center seems to produce cleaner wrapping of the date range on narrower screens.

* Allow Overriding HA Dialog Header Font Size

For the title and subtitle, variables can not be set in the host to control the font size. This is useful in cases where the heading is used on a narrow card and the title needs to be smaller.

* Add property to manually trigger opening of ha-date-range-picker

For cases where there is a desire to click in regions other than the calendar icon to have the date picker open.

* Add no-padding option to ha-dialog-header

When used within cards already containing padding, the extra padding may be unnecessary.

* Use ha-dialog-header for Energy Period Selector
Place the day/month in the title and the year in the subtitle. This gives a cleaner more consistent look.

* Remove Unnecessary IDs

Came from copy-paste from another example.

* Apply Typing to Date Picker Methods

* Move selector buttons to overflow if too small

When the period selector gets too small to fit everything (very narrow screens, or card in grid), then move the next/previous buttons to the overflow menu.

The now button exists on the overflow menu now too when in narrow mode.

* Change Date Picker openPicker to open()

Makes far more sense as a method not a property.

* Revert Padding Change to ha-dialog-header

* Simplify Energy Selector Overflow Buttons

Improve button labelling to just use index, avoiding possible localisation issues of using the label.

Simplify the interface to remove unnecessary fields.

* Update Button Collapse Width for Picker
Increase to 300px now that padding of dialog header is present.

* Fix Imports in Energy Period Selector

* Fix whitespace

* Properly leverage slots in ha-dialog-header

Make proper use of the actionItems slot for the control buttons to keep them properly contained as the date selector is resized.

* Move clickable date to title/subtitle elements

In moving the control buttons into the actionItems slot, we can no longer use the whole ha-dialog-header element as a clickable region for opening the date selector. Frankly this is not a bad thing as it meant it was not possible to nicely hover/highlight the date.

Instead we now make the title/subtitle clickable elements. This allows adding a nice hover effect and cursor pointer effect.

* Add option to make ha-dialog-header content clickable

* Use clickable dialog header in period selector

This way the whole title area is the hit point rather than the title and subtitle text individually.

* Remove ha-dialog-header from period selector

It's not a dialog, so it makes no sense to use that element. Instead recreate just the necessary parts to make it stylistically simiar. The reality is this is not much extra code, and it should make maintaining easier.

* Revert changes to ha-dialog-header

* Style Date Range as Input Field

* Use ha-ripple Effect

* Remove Unnecessary Tooltip Option

Unused, so remove the extra complexity.

* Remove more unused imports

* Force energy panel picker open direction

Now that the button is on the left, we need it to open to the right to avoid colision with the sidebar. Add the option to force the direction.

* Rename property to openingDirection for consistency
2026-02-05 12:36:10 +02:00
Paul Bottein
29cee99f10 Use consistent name for common controls in home dashboard (#29410) 2026-02-05 08:48:58 +00:00
renovate[bot]
47341e93fc Update dependency jsdom to v28 (#29409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-05 10:46:54 +02:00
Wendelin
cbae7d6e2f Error log card migrate ha-md-menu to ha-dropdown (#29398)
* Migrate from ha-md-menu to ha-dropdown in error log card, scene dashboard, script picker, and refresh tokens card

* Fix setBoot
2026-02-05 10:37:52 +02:00
karwosts
ebff35d17f Fix more-info media source select (#29400) 2026-02-04 17:18:49 +01:00
Wendelin
aec4a06156 migrate ha-select to ha-dropdown (#29392)
* migrate ha-select to ha-dropdown

* remove ha-menu

* review

* Fix eslint error

---------

Co-authored-by: Aidan Timson <aidan@timmo.dev>
2026-02-04 13:47:15 +00:00
Dominik Bruhn
917f2b4434 Add tag-id column in tag table (#29383) 2026-02-04 14:35:56 +01:00
Paul Bottein
79ec6b972e Change default icon for blank area if not icon configured (#29394) 2026-02-04 14:33:27 +01:00
Paul Bottein
9e35befa99 Remove old lovelace overview from pickers (#29390) 2026-02-04 12:03:29 +01:00
Paul Bottein
75160d67d3 Load domain translation when integration page load (#29391) 2026-02-04 11:58:27 +01:00
Tom Carpenter
b145d09041 Fix Horizontal Scrolling on System Logs Page (#29375) 2026-02-04 08:32:10 +00:00
Aidan Timson
f3f7a1e46a Migrate siren advanced controls to wa-dialog (more info) (#29369)
* Migrate siren advanced controls to wa (more info)

* Fix footer

* Update src/dialogs/more-info/components/siren/ha-more-info-siren-advanced-controls.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-04 06:45:50 +00:00
Simon Lamon
091315d9a9 Fixup dev container (#29376)
* Fixup dev container

* Fix yarn installation command in bootstrap script

* Fast restart
2026-02-04 08:33:39 +02:00
renovate[bot]
75b830cdf9 Update dependency globals to v17.3.0 (#29385)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 08:22:40 +02:00
renovate[bot]
e4b8352832 Update formatjs monorepo (#29386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-04 06:14:54 +01:00
karwosts
4e193187f9 Don't shrink ha-dropdown checkboxes (#29387) 2026-02-04 06:14:39 +01:00
Paul Bottein
5394b3b8cf Add translations for new overview dialog (#29382) 2026-02-03 23:37:24 +01:00
ildar170975
2ab867986a Data tables: standardize columns (#29155)
* Create data-table-columns.ts

* Update data-table-columns.ts

* move a code for columns into separate functions

* move a code for columns into separate functions

* move a code for columns into separate functions

* move a code for columns into separate functions

* move a code for columns into separate functions

* move a code for columns into separate functions

* move a code for columns into separate functions

* move a code for columns into separate functions

* fix a translation key

* move commonly used translations to generic

* remove a translation for another PR

* restore "domain" translation for while

* resolving conflicts

* resolve conflicts

* resolving conflicts

* resolving conflicts

* resolving conflicts

* resolving conflicts

* fix conflicts

* fix conflicts

* fix import

* fix import
2026-02-03 21:37:53 +01:00
renovate[bot]
a1c3a6c662 Update babel monorepo to v7.29.0 (#29379)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-03 21:37:34 +01:00
Benedikt Johannes
11296adbd4 Coversation -> Conversation (#29378)
* Coversation -> Conversation

* Update en.json
2026-02-03 20:22:32 +01:00
Paul Bottein
4e04f4284e Use area icon for area empty state (#29371) 2026-02-03 17:58:37 +01:00
Paul Bottein
a0cc0d9cca Improve other devices page in home dashboard (#29370) 2026-02-03 15:50:49 +00:00
uptimeZERO_
c925053bb8 Animate app side bar (#29026) 2026-02-03 14:55:10 +00:00
Aidan Timson
22a7aa8f8e Add default view transition to edit badge and card (#29360) 2026-02-03 14:33:20 +00:00
Petar Petrov
3a5f719a3e Fix chart theme colors in Lovelace edit mode (#29361)
When edit mode is toggled, existing cards are moved into edit mode
wrappers. This triggers connectedCallback which was calling _setupChart
synchronously before the browser recalculated CSS inheritance. The
chart would read stale CSS custom properties, resulting in low-contrast
axis labels in dark theme.

Defer _setupChart using afterNextRender to allow the browser to complete
layout and CSS recalculation first. Guard conditions prevent issues with
rapid connect/disconnect cycles.
2026-02-03 16:27:17 +02:00
Aidan Timson
7b7182c147 Migrate state card select/input_select to select menu (#29362)
* Migrate state card input select to select menu

* Sort

* Migrate state card select to select menu
2026-02-03 16:26:41 +02:00
Paul Bottein
0eb7229819 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:25:40 +02:00
Petar Petrov
fcc6f1b5e9 Move dialog scrim to pseudo-element (#29357) 2026-02-03 14:28:51 +01:00
Paul Bottein
19dc2a5865 Add missing danger variant in dropdown item (#29359) 2026-02-03 11:37:42 +00:00
Wendelin
ae63530123 Migrate ha-control-select-menu to use ha-dropdown (#29350) 2026-02-03 10:30:51 +00:00
uptimeZERO_
97e1f47af9 Add transition for more-info-dialog when toggling expand state (#29341) 2026-02-03 09:21:29 +00:00
Paul Bottein
346d916944 Add an disable view transition as it crashes chrome browser when using chrome dev tools (#29339) 2026-02-03 09:10:16 +01:00
Kristel
ab326b3277 Refactor _applyFilters for Scenes and Scripts pages (#29116)
* proper functionnames

* refactor _applyFilters on scenes and script page

* Apply suggestions from code review

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-02-03 07:50:02 +00:00
Tom
55e251c04f Expose target_humidity_step value to climate (#28834)
Add target_humidity_step to climate
2026-02-03 09:34:53 +02:00
Darryn Capes-Davis
b902f3e6f5 Fix CSS minification issue for ha-card (#29354) 2026-02-03 09:19:06 +02:00
ildar170975
9daac6c49f Remove "type" where "attribute: false" (#29356)
remove "type" when "attribute: false"
2026-02-03 09:17:33 +02:00
Benedikt Johannes
65ea0c9121 Fix "unamed" -> "unnamed" (potential crash / non registered translation information bug) (#29351)
* Fix typo

* Update home-area-view-strategy.ts

* Update home-other-devices-view-strategy.ts
2026-02-03 05:55:11 +00:00
Denis Shulyaka
505cc698f6 Fallback data flow section label translation (#29352) 2026-02-03 05:54:18 +00:00
karwosts
b0f9b31dae Fix missing imports in devtools-statistics (#29355) 2026-02-03 06:45:39 +01:00
Norbert Rittel
2b2966a214 Make explanation of "search command(s)" shortcut consistent (#29345)
Change "search command" to "search commands"
2026-02-02 17:49:45 +01:00
renovate[bot]
558b251e32 Update dependency @codemirror/view to v6.39.12 (#29349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 17:41:06 +01:00
renovate[bot]
df28eaa99e Update dependency tar to v7.5.7 [SECURITY] (#29348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 17:40:45 +01:00
Petar Petrov
40342c9cfd Remove redundant dialog backdrop color (#29337) 2026-02-02 17:12:27 +01:00
Paul Bottein
c6573b9c1b Fix "Reload resources" menu for YAML resource mode (#29346) 2026-02-02 16:07:10 +00:00
karwosts
54c46ad362 Entity diagnostic - handle entity not in the registry (#29344) 2026-02-02 17:00:26 +01:00
Wendelin
3376036392 Fix dropdown width in datatables (#29340) 2026-02-02 13:49:54 +00:00
Paul Bottein
ff5ecc047a Fix type error for missing hass.themes race condition in themes mixin (#29338) 2026-02-02 13:44:24 +00:00
Jeremy Cook
a8393cddd4 Improved HTML presentation table support to ha-markdown, compatible w… (#29108)
Improved HTML presentation table support to ha-markdown, compatible with companion app on android and iOS

- Support valign attribute for vertical alignment (top, middle, bottom, baseline)
- Support border attribute for cell borders (0, 1, 2, 3px)
- Set default styling for presentation tables (no borders, no padding)
- Use CSS variables for customization
- Include future-proof @supports rule for attr() function
- Maintain backward compatibility with existing markdown tables
2026-02-02 15:17:12 +02:00
Tom Carpenter
edfc33039b Merge Long Term Statistics for Power Sensors in Energy Dashboard (#29319)
* Merge Long Term Statistics for Power Sensors in Energy Dashboard

When using 5minute data for the power sources chart, data would be missing if the selected range was beyond the short term statistics limit. This change takes data from long term statistics and merges it in to the power sources data if the short term statistics doesn't extend far enough back for the selection.

* Skip for zero-length power statistics

Prevent out of bounds array access if power statistics has no entries.
2026-02-02 15:11:52 +02:00
ildar170975
14f8d982a9 hui-gauge-card-editor: use imported code for actions (#29326)
use imported code for actions
2026-02-02 15:02:22 +02:00
Marcin Bauer
847a040fa7 Keep focus on search field when clicking filter chips (#29249)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 13:45:54 +01:00
Aidan Timson
8611359481 Ensure template renderer overflows on overflow (#29335) 2026-02-02 12:07:49 +00:00
Kristel
f473ebf18c bugfix: add eventlistener for exposed-entities-changed to Entities page (#29299) 2026-02-02 11:32:32 +00:00
Wendelin
9010898742 Fix missing ha-md-menu in config/labels (#29334) 2026-02-02 11:15:33 +00:00
Petar Petrov
1e30394bf3 Append current entity state to history and statistics charts (#29273) 2026-02-02 09:34:50 +00:00
Paulus Schoutsen
bca2cb0c1e Conditionally show HTML5 push notifications (#29227) 2026-02-02 09:24:42 +00:00
Aidan Timson
29317eb842 Show hint only if keyboard shortcuts is enabled (#29332)
Enabled by default, must be explicity disabled
2026-02-02 10:59:14 +02:00
Wendelin
a8327ef59a Revert "Fix automation sidebar ui supported check" (#29331) 2026-02-02 08:57:19 +00:00
Petar Petrov
01c8832024 Add area dashboard link to navigation picker (#29264) 2026-02-02 09:51:03 +01:00
Linus Rath
a8c633e627 Update untracked consumption threshold to 1W (#29310) 2026-02-02 08:16:34 +00:00
Simon Lamon
b659671814 Bump codespace to Python 3.14 (#29316) 2026-02-02 09:52:07 +02:00
dependabot[bot]
3060cdf355 Bump github/codeql-action from 4.31.11 to 4.32.0 (#29329)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.11 to 4.32.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](19b2f06db2...b20883b0cd)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 08:44:06 +02:00
dependabot[bot]
215241df56 Bump actions/cache from 5.0.2 to 5.0.3 (#29328)
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.2 to 5.0.3.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](8b402f58fb...cdf6c1fa76)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 08:43:49 +02:00
renovate[bot]
f6852894b0 Update dependency @lokalise/node-api to v15.6.1 (#29322)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 06:33:24 +01:00
renovate[bot]
86d7205a3a Update dependency @braintree/sanitize-url to v7.1.2 (#29325)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-02 06:33:03 +01:00
Norbert Rittel
9c16ceda71 Also replace "Sensor type" with "Type of power measurement" (#29317)
Replace "Sensor type" with "Type of power measurement"
2026-02-01 10:44:47 +01:00
karwosts
288789a604 Use ha-form for condition template (#29301) 2026-02-01 10:18:27 +01:00
Norbert Rittel
09139d5bec Replace "Power sensor type" with "Type of power measurement" (#29305) 2026-02-01 09:54:56 +01:00
ildar170975
2d90be9af3 ha-config-device-page: fix placement for tooltip (#29302)
fix placement for tooltip
2026-02-01 09:39:08 +01:00
renovate[bot]
1c6464663e Update dependency @rsdoctor/rspack-plugin to v1.5.1 (#29297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-01 09:37:46 +01:00
renovate[bot]
189f0b9472 Update dependency node-vibrant to v4.0.4 (#29293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-31 09:04:52 +02:00
renovate[bot]
771f5eaff4 Update dependency @vibrant/color to v4.0.4 (#29292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-31 08:49:36 +02:00
renovate[bot]
a4cb3b5b01 Update dependency globals to v17.2.0 (#29288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 16:51:11 +00:00
Norbert Rittel
eb1ae99a1f Replace "home page" with "Overview page" for preferences panel (#29284)
Replace "home page" with "Overview page"
2026-01-30 17:46:34 +01:00
Aidan Timson
655643eb3f Cleanup padding on matter dashboard expansion panel (#29286)
Cleanup padding on matter config expansion panel
2026-01-30 17:43:32 +01:00
Aidan Timson
02eb1e6832 Fix scrolling for labs page (#29287) 2026-01-30 17:41:40 +01:00
karwosts
62f7a2eea1 Fix areas cannot be deleted (#29285) 2026-01-30 14:01:31 +00:00
Aidan Timson
a7f9b93018 Fix type error for missing hass.config race condition in themes mixin (#29280) 2026-01-30 12:54:08 +01:00
Paul Bottein
3e7011e2c8 Fix demo because of new default panel (#29279) 2026-01-30 11:48:47 +01:00
Aidan Timson
97f89bd983 Add missing settings nav items for quick search (#29278)
* Add missing repairs quick search item

* Add voice assistants
2026-01-30 12:11:09 +02:00
renovate[bot]
9aedaeabbf Update dependency @rspack/core to v1.7.4 (#29275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-30 11:58:31 +02:00
Aidan Timson
7e839dc895 Change default shortcut tip in Quick Search to mod+k, add tip to settings (#29253) 2026-01-30 09:48:03 +00:00
Wendelin
1f6c916d11 Fix multi select in quick search (#29272)
Add item selection state management to QuickBar component
2026-01-30 10:14:32 +01:00
Wendelin
71bd12bb90 Fix --wa-color-text-normal (#29271)
Update normal text color variable in wa.globals.ts
2026-01-30 09:48:52 +02:00
Simon Lamon
e741c14482 Remove duplicated text (#29265) 2026-01-30 08:46:18 +01:00
Wendelin
a496448ed9 Fix device download diagnostic via overflow (#29269)
fix diagnostic download link handling to simplify URL signing
2026-01-30 09:42:40 +02:00
463 changed files with 11920 additions and 10345 deletions

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/devcontainers/python:1-3.13
FROM mcr.microsoft.com/devcontainers/python:3.14
ENV \
DEBIAN_FRONTEND=noninteractive \

View File

@@ -21,6 +21,14 @@
-->
## Screenshots
<!--
If your PR includes visual changes, please add screenshots or a short video
showing the before and after. This helps reviewers understand the impact of
your changes.
Note: Remove this section if this PR has no visual changes.
-->
## Type of change
<!--
What type of change does your PR introduce to the Home Assistant frontend?
@@ -35,16 +43,6 @@
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests
## Example configuration
<!--
Supplying a configuration snippet, makes it easier for a maintainer to test
your PR.
-->
```yaml
```
## Additional information
<!--
Details are important, and help maintainers processing your PR.
@@ -54,6 +52,8 @@
- This PR fixes or closes issue: fixes #
- This PR is related to issue or discussion:
- Link to documentation pull request:
- Link to developer documentation pull request:
- Link to backend pull request:
## Checklist
<!--
@@ -61,18 +61,50 @@
creating the PR. If you're unsure about any of them, don't hesitate to ask.
We're here to help! This is simply a reminder of what we are going to look
for before merging your code.
AI tools are welcome, but contributors are responsible for *fully*
understanding the code before submitting a PR.
-->
- [ ] I understand the code I am submitting and can explain how it works.
- [ ] The code change is tested and works locally.
- [ ] There is no commented out code in this PR.
- [ ] Tests have been added to verify that the new code works.
- [ ] I have followed the [development checklist][dev-checklist]
- [ ] I have followed the [perfect PR recommendations][perfect-pr]
- [ ] Any generated code has been carefully reviewed for correctness and compliance with project standards.
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]
<!--
Thank you for contributing <3
This project is very active and we have a high turnover of pull requests.
Unfortunately, the number of incoming pull requests is higher than what our
reviewers can review and merge so there is a long backlog of pull requests
waiting for review. You can help here!
By reviewing another pull request, you will help raise the code quality of
that pull request and the final review will be faster. This way the general
pace of pull request reviews will go up and your wait time will go down.
When picking a pull request to review, try to choose one that hasn't yet
been reviewed.
Thanks for helping out!
-->
To help with the load of incoming pull requests:
- [ ] I have reviewed two other [open pull requests][prs] in this repository.
[prs]: https://github.com/home-assistant/frontend/pulls?q=is%3Aopen+is%3Apr+-author%3A%40me+-draft%3Atrue+sort%3Acreated-desc+review%3Anone+-status%3Afailure
<!--
Thank you for contributing <3
Below, some useful links you could explore:
-->
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
[docs-repository]: https://github.com/home-assistant/home-assistant.io
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr

View File

@@ -251,7 +251,6 @@ For browser support, API details, and current specifications, refer to these aut
**Available Dialog Types:**
- `ha-wa-dialog` - Preferred for new dialogs (Web Awesome based)
- `ha-md-dialog` - Material Design 3 dialog component
- `ha-dialog` - Legacy component (still widely used)
**Opening Dialogs (Fire Event Pattern - Recommended):**
@@ -559,6 +558,10 @@ this.hass.localize("ui.panel.config.updates.update_available", {
- Use HTTPS - All external resources must use HTTPS
- CSP compliance - Ensure code works with Content Security Policy
### Pull Requests
When creating a pull request, you **must** use the PR template located at `.github/PULL_REQUEST_TEMPLATE.md`. Read the template file and use its full content as the PR body, filling in each section appropriately. Do not omit, reorder, or rewrite the template sections. Do not check the checklist items on behalf of the user — those are the user's responsibility to review and check. If the PR includes UI changes, remind the user to add screenshots or a short video to the PR after creating it.
### Text and Copy Guidelines
#### Terminology Standards

View File

@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
node_modules/.cache/prettier

View File

@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
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@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
# 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@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2

View File

@@ -6,7 +6,7 @@ on:
- cron: "0 1 * * *"
env:
PYTHON_VERSION: "3.13"
PYTHON_VERSION: "3.14"
NODE_OPTIONS: --max_old_space_size=6144
permissions:

View File

@@ -6,7 +6,7 @@ on:
- published
env:
PYTHON_VERSION: "3.13"
PYTHON_VERSION: "3.14"
NODE_OPTIONS: --max_old_space_size=6144
# Set default workflow permissions
@@ -84,7 +84,7 @@ jobs:
- name: Build wheels
uses: home-assistant/wheels@2025.12.0
with:
abi: cp313
abi: cp314
tag: musllinux_1_2
arch: amd64
wheels-key: ${{ secrets.WHEELS_KEY }}

2
.nvmrc
View File

@@ -1 +1 @@
24.13.0
24.13.1

View File

@@ -9,11 +9,14 @@ import { selectedDemoConfig } from "./configs/demo-configs";
import { mockAreaRegistry } from "./stubs/area_registry";
import { mockAuth } from "./stubs/auth";
import { mockConfigEntries } from "./stubs/config_entries";
import { mockDeviceRegistry } from "./stubs/device_registry";
import { mockEnergy } from "./stubs/energy";
import { energyEntities } from "./stubs/entities";
import { mockEntityRegistry } from "./stubs/entity_registry";
import { mockEvents } from "./stubs/events";
import { mockFloorRegistry } from "./stubs/floor_registry";
import { mockFrontend } from "./stubs/frontend";
import { mockLabelRegistry } from "./stubs/label_registry";
import { mockIcons } from "./stubs/icons";
import { mockHistory } from "./stubs/history";
import { mockLovelace } from "./stubs/lovelace";
@@ -60,6 +63,9 @@ export class HaDemo extends HomeAssistantAppEl {
mockPersistentNotification(hass);
mockConfigEntries(hass);
mockAreaRegistry(hass);
mockDeviceRegistry(hass);
mockFloorRegistry(hass);
mockLabelRegistry(hass);
mockEntityRegistry(hass, [
{
config_entry_id: "co2signal",

View File

@@ -1,14 +1,12 @@
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
let changeFunction;
let sidebarChangeCallback;
export const mockFrontend = (hass: MockHomeAssistant) => {
hass.mockWS("frontend/get_user_data", () => ({
value: null,
}));
hass.mockWS("frontend/get_user_data", () => ({ value: null }));
hass.mockWS("frontend/set_user_data", ({ key, value }) => {
if (key === "sidebar") {
changeFunction?.({
sidebarChangeCallback?.({
value: {
panelOrder: value.panelOrder || [],
hiddenPanels: value.hiddenPanels || [],
@@ -16,15 +14,34 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
});
}
});
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
changeFunction = onChange;
hass.mockWS("frontend/subscribe_user_data", (msg, _hass, onChange) => {
if (msg.key === "sidebar") {
sidebarChangeCallback = onChange;
}
onChange?.({ value: null });
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
});
hass.mockWS(
"frontend/subscribe_system_data",
(_msg, currentHass, onChange) => {
onChange?.({
value: currentHass.systemData,
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
}
);
hass.mockWS("labs/subscribe", (_msg, _currentHass, onChange) => {
onChange?.({
value: {
panelOrder: [],
hiddenPanels: [],
},
preview_feature: _msg.preview_feature,
domain: _msg.domain,
enabled: false,
is_built_in: true,
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
});
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
hass.mockWS("frontend/get_themes", (_msg, currentHass) => currentHass.themes);
};

View File

@@ -29,6 +29,7 @@ export const mockLovelace = (
hass.mockWS("lovelace/config/save", () => Promise.resolve());
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
hass.mockWS("lovelace/dashboards/list", () => Promise.resolve([]));
};
customElements.whenDefined("hui-root").then(() => {

View File

@@ -7,8 +7,18 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
})
);
hass.mockWS("render_template", (msg, _hass, onChange) => {
let result = msg.template;
// Simple variable substitution for demo purposes
if (msg.variables) {
for (const [key, value] of Object.entries(msg.variables)) {
result = result.replace(
new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, "g"),
String(value)
);
}
}
onChange!({
result: msg.template,
result,
listeners: { all: false, domains: [], entities: [], time: false },
});
// eslint-disable-next-line @typescript-eslint/no-empty-function

View File

@@ -10,7 +10,9 @@ As a community, we are proud of our logo. Follow these guidelines to ensure it a
![Logo](/images/brand/logo.png)
Please note that this logo is not released under the CC license. All rights reserved.
<ha-alert alert-type="info">
This logo is trademarked and the property of the Open Home Foundation. This means it is not available for commercial use without express written permission from the foundation. We regard commercial use as anything designed to market or promote a product, software or service that is for sale. Please contact <a href="mailto:partner@openhomefoundation.org">partner@openhomefoundation.org</a> for further information
</ha-alert>
# Design

View File

@@ -0,0 +1 @@
import "../../../../src/components/ha-alert";

View File

@@ -18,7 +18,7 @@ The Home Assistant interface is based on Material Design. It's a design system c
We want to make it as easy for designers to contribute as it is for developers. Theres a lot a designer can contribute to:
- Meet us at <a href="https://www.home-assistant.io/join-chat" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
- Meet us at <a href="https://www.home-assistant.io/join-chat-design" rel="noopener noreferrer" target="_blank">Discord #designers channel</a>. If you can't see the channel, make sure you set the correct role in Channels & Roles.
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!

View File

@@ -100,7 +100,6 @@ class HaLandingPage extends LandingPageBaseElement {
button-style
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<ha-button
appearance="plain"

View File

@@ -27,32 +27,32 @@
"type": "module",
"dependencies": {
"@babel/runtime": "7.28.6",
"@braintree/sanitize-url": "7.1.1",
"@braintree/sanitize-url": "7.1.2",
"@codemirror/autocomplete": "6.20.0",
"@codemirror/commands": "6.10.1",
"@codemirror/commands": "6.10.2",
"@codemirror/language": "6.12.1",
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.6.0",
"@codemirror/state": "6.5.4",
"@codemirror/view": "6.39.11",
"@codemirror/view": "6.39.12",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.2.0",
"@formatjs/intl-displaynames": "7.2.0",
"@formatjs/intl-durationformat": "0.10.0",
"@formatjs/intl-getcanonicallocales": "3.2.0",
"@formatjs/intl-listformat": "8.2.0",
"@formatjs/intl-locale": "5.2.0",
"@formatjs/intl-numberformat": "9.2.1",
"@formatjs/intl-pluralrules": "6.2.1",
"@formatjs/intl-relativetimeformat": "12.2.1",
"@formatjs/intl-datetimeformat": "7.2.1",
"@formatjs/intl-displaynames": "7.2.1",
"@formatjs/intl-durationformat": "0.10.1",
"@formatjs/intl-getcanonicallocales": "3.2.1",
"@formatjs/intl-listformat": "8.2.1",
"@formatjs/intl-locale": "5.2.1",
"@formatjs/intl-numberformat": "9.2.2",
"@formatjs/intl-pluralrules": "6.2.2",
"@formatjs/intl-relativetimeformat": "12.2.2",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
"@fullcalendar/interaction": "6.1.20",
"@fullcalendar/list": "6.1.20",
"@fullcalendar/luxon3": "6.1.20",
"@fullcalendar/timegrid": "6.1.20",
"@home-assistant/webawesome": "3.0.0-ha.2",
"@home-assistant/webawesome": "3.2.1-ha.0",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.1.0",
"@lit-labs/observers": "2.1.0",
@@ -71,7 +71,6 @@
"@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-menu": "0.27.0",
"@material/mwc-radio": "0.27.0",
"@material/mwc-select": "0.27.0",
"@material/mwc-snackbar": "0.27.0",
@@ -89,7 +88,7 @@
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0",
"@vibrant/color": "4.0.0",
"@vibrant/color": "4.0.4",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
@@ -112,7 +111,7 @@
"hls.js": "1.6.15",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "11.1.1",
"intl-messageformat": "11.1.2",
"js-yaml": "4.1.1",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -122,7 +121,7 @@
"luxon": "3.7.2",
"marked": "17.0.1",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"node-vibrant": "4.0.4",
"object-hash": "3.0.0",
"punycode": "2.3.1",
"qr-scanner": "1.4.2",
@@ -133,7 +132,7 @@
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
"ua-parser-js": "2.0.8",
"ua-parser-js": "2.0.9",
"vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
@@ -146,17 +145,17 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.28.6",
"@babel/core": "7.29.0",
"@babel/helper-define-polyfill-provider": "0.6.6",
"@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.6",
"@babel/plugin-transform-runtime": "7.29.0",
"@babel/preset-env": "7.29.0",
"@bundle-stats/plugin-webpack-filter": "4.21.9",
"@lokalise/node-api": "15.6.0",
"@lokalise/node-api": "15.6.1",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.0",
"@rspack/core": "1.7.3",
"@rsdoctor/rspack-plugin": "1.5.2",
"@rspack/core": "1.7.5",
"@rspack/dev-server": "1.2.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.25",
@@ -192,14 +191,14 @@
"eslint-plugin-wc": "3.0.2",
"fancy-log": "2.0.0",
"fs-extra": "11.3.3",
"glob": "13.0.0",
"glob": "13.0.1",
"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": "27.4.0",
"jsdom": "28.0.0",
"jszip": "3.10.1",
"lint-staged": "16.2.7",
"lit-analyzer": "2.0.3",
@@ -211,7 +210,7 @@
"rspack-manifest-plugin": "5.2.1",
"serve": "14.2.5",
"sinon": "21.0.1",
"tar": "7.5.6",
"tar": "7.5.7",
"terser-webpack-plugin": "5.3.16",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
@@ -229,13 +228,13 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.2",
"@fullcalendar/daygrid": "6.1.20",
"globals": "17.1.0",
"globals": "17.3.0",
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"glob@^10.2.2": "^10.5.0"
},
"packageManager": "yarn@4.12.0",
"volta": {
"node": "24.13.0"
"node": "24.13.1"
}
}

View File

@@ -12,7 +12,7 @@ readme = "README.md"
authors = [
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
]
requires-python = ">=3.13.0"
requires-python = ">=3.14.0"
[project.urls]
"Homepage" = "https://github.com/home-assistant/frontend"

View File

@@ -16,6 +16,12 @@ if [[ -n "$DEVCONTAINER" ]]; then
nvm install --reinstall-packages-from="$nodeCurrent" --default
nvm uninstall "$nodeCurrent"
fi
# install yarn if not already available
if ! command -v yarn &> /dev/null; then
npm install -g corepack
yes | yarn
fi
fi
if ! command -v yarn &> /dev/null; then

View File

@@ -194,7 +194,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
button-style
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<ha-button
appearance="plain"

View File

@@ -116,3 +116,6 @@ export const UNIT_F = "°F";
/** Entity ID of the default view. */
export const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
/** String to visually separate labels on UI */
export const STRINGS_SEPARATOR_DOT = " · ";

View File

@@ -3,13 +3,14 @@ import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import type { FrontendLocaleData } from "../../data/translation";
import { TimeZone } from "../../data/translation";
import type { HomeAssistant } from "../../types";
import type { HomeAssistant, ValuePart } from "../../types";
import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time";
import { DURATION_UNITS, formatDuration } from "../datetime/format_duration";
import { formatTime } from "../datetime/format_time";
import {
formatNumber,
formatNumberToParts,
getNumberFormatOptions,
isNumericFromAttributes,
} from "../number/format_number";
@@ -51,8 +52,36 @@ export const computeStateDisplayFromEntityAttributes = (
attributes: any,
state: string
): string => {
const parts = computeStateToPartsFromEntityAttributes(
localize,
locale,
sensorNumericDeviceClasses,
config,
entity,
entityId,
attributes,
state
);
return parts.map((part) => part.value).join("");
};
const computeStateToPartsFromEntityAttributes = (
localize: LocalizeFunc,
locale: FrontendLocaleData,
sensorNumericDeviceClasses: string[],
config: HassConfig,
entity: EntityRegistryDisplayEntry | undefined,
entityId: string,
attributes: any,
state: string
): ValuePart[] => {
if (state === UNKNOWN || state === UNAVAILABLE) {
return localize(`state.default.${state}`);
return [
{
type: "value",
value: localize(`state.default.${state}`),
},
];
}
const domain = computeDomain(entityId);
@@ -73,19 +102,27 @@ export const computeStateDisplayFromEntityAttributes = (
DURATION_UNITS.includes(attributes.unit_of_measurement)
) {
try {
return formatDuration(
locale,
state,
attributes.unit_of_measurement,
entity?.display_precision
);
return [
{
type: "value",
value: formatDuration(
locale,
state,
attributes.unit_of_measurement,
entity?.display_precision
),
},
];
} catch (_err) {
// fallback to default
}
}
// state is monetary
if (attributes.device_class === "monetary") {
let parts: Record<string, string>[] = [];
try {
return formatNumber(state, locale, {
parts = formatNumberToParts(state, locale, {
style: "currency",
currency: attributes.unit_of_measurement,
minimumFractionDigits: 2,
@@ -98,8 +135,34 @@ export const computeStateDisplayFromEntityAttributes = (
} catch (_err) {
// fallback to default
}
const TYPE_MAP: Record<string, ValuePart["type"]> = {
integer: "value",
group: "value",
decimal: "value",
fraction: "value",
literal: "literal",
currency: "unit",
};
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 });
}
}
return valueParts;
}
// default processing of numeric values
const value = formatNumber(
state,
locale,
@@ -114,10 +177,14 @@ export const computeStateDisplayFromEntityAttributes = (
attributes.unit_of_measurement;
if (unit) {
return `${value}${blankBeforeUnit(unit, locale)}${unit}`;
return [
{ type: "value", value: value },
{ type: "literal", value: blankBeforeUnit(unit, locale) },
{ type: "unit", value: unit },
];
}
return value;
return [{ type: "value", value: value }];
}
if (["date", "input_datetime", "time"].includes(domain)) {
@@ -129,36 +196,51 @@ export const computeStateDisplayFromEntityAttributes = (
const components = state.split(" ");
if (components.length === 2) {
// Date and time.
return formatDateTime(
new Date(components.join("T")),
{ ...locale, time_zone: TimeZone.local },
config
);
return [
{
type: "value",
value: formatDateTime(
new Date(components.join("T")),
{ ...locale, time_zone: TimeZone.local },
config
),
},
];
}
if (components.length === 1) {
if (state.includes("-")) {
// Date only.
return formatDate(
new Date(`${state}T00:00`),
{ ...locale, time_zone: TimeZone.local },
config
);
return [
{
type: "value",
value: formatDate(
new Date(`${state}T00:00`),
{ ...locale, time_zone: TimeZone.local },
config
),
},
];
}
if (state.includes(":")) {
// Time only.
const now = new Date();
return formatTime(
new Date(`${now.toISOString().split("T")[0]}T${state}`),
{ ...locale, time_zone: TimeZone.local },
config
);
return [
{
type: "value",
value: formatTime(
new Date(`${now.toISOString().split("T")[0]}T${state}`),
{ ...locale, time_zone: TimeZone.local },
config
),
},
];
}
}
return state;
return [{ type: "value", value: state }];
} catch (_e) {
// Formatting methods may throw error if date parsing doesn't go well,
// just return the state string in that case.
return state;
return [{ type: "value", value: state }];
}
}
@@ -182,25 +264,58 @@ export const computeStateDisplayFromEntityAttributes = (
(domain === "sensor" && attributes.device_class === "timestamp")
) {
try {
return formatDateTime(new Date(state), locale, config);
return [
{
type: "value",
value: formatDateTime(new Date(state), locale, config),
},
];
} catch (_err) {
return state;
return [{ type: "value", value: state }];
}
}
return (
(entity?.translation_key &&
localize(
`component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}`
)) ||
// Return device class translation
(attributes.device_class &&
localize(
`component.${domain}.entity_component.${attributes.device_class}.state.${state}`
)) ||
// Return default translation
localize(`component.${domain}.entity_component._.state.${state}`) ||
// We don't know! Return the raw state.
state
return [
{
type: "value",
value:
(entity?.translation_key &&
localize(
`component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}`
)) ||
// Return device class translation
(attributes.device_class &&
localize(
`component.${domain}.entity_component.${attributes.device_class}.state.${state}`
)) ||
// Return default translation
localize(`component.${domain}.entity_component._.state.${state}`) ||
// We don't know! Return the raw state.
state,
},
];
};
export const computeStateToParts = (
localize: LocalizeFunc,
stateObj: HassEntity,
locale: FrontendLocaleData,
sensorNumericDeviceClasses: string[],
config: HassConfig,
entities: HomeAssistant["entities"],
state?: string
): ValuePart[] => {
const entity = entities?.[stateObj.entity_id] as
| EntityRegistryDisplayEntry
| undefined;
return computeStateToPartsFromEntityAttributes(
localize,
locale,
sensorNumericDeviceClasses,
config,
entity,
stateObj.entity_id,
stateObj.attributes,
state !== undefined ? state : stateObj.state
);
};

View File

@@ -5,7 +5,6 @@ import type {
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import type { FrontendLocaleData } from "../../data/translation";
import { NumberFormat } from "../../data/translation";
import { round } from "./round";
/**
* Returns true if the entity is considered numeric based on the attributes it has
@@ -52,7 +51,22 @@ export const formatNumber = (
num: string | number,
localeOptions?: FrontendLocaleData,
options?: Intl.NumberFormatOptions
): string => {
): string =>
formatNumberToParts(num, localeOptions, options)
.map((part) => part.value)
.join("");
/**
* Returns an array of objects containing the formatted number in parts
* Similar to Intl.NumberFormat.prototype.formatToParts()
*
* Input params - same as for formatNumber()
*/
export const formatNumberToParts = (
num: string | number,
localeOptions?: FrontendLocaleData,
options?: Intl.NumberFormatOptions
): any[] => {
const locale = localeOptions
? numberFormatToLocale(localeOptions)
: undefined;
@@ -71,7 +85,7 @@ export const formatNumber = (
return new Intl.NumberFormat(
locale,
getDefaultFormatOptions(num, options)
).format(Number(num));
).formatToParts(Number(num));
}
if (
@@ -86,15 +100,10 @@ export const formatNumber = (
...options,
useGrouping: false,
})
).format(Number(num));
).formatToParts(Number(num));
}
if (typeof num === "string") {
return num;
}
return `${round(num, options?.maximumFractionDigits).toString()}${
options?.style === "currency" ? ` ${options.currency}` : ""
}`;
return [{ type: "literal", value: num }];
};
/**

View File

@@ -0,0 +1,28 @@
const SI_PREFIX_MULTIPLIERS: Record<string, number> = {
T: 1e12,
G: 1e9,
M: 1e6,
k: 1e3,
m: 1e-3,
"\u00B5": 1e-6, // µ (micro sign)
"\u03BC": 1e-6, // μ (greek small letter mu)
};
/**
* Normalize a numeric value by detecting SI unit prefixes (T, G, M, k, m, µ).
* Only applies when the unit is longer than 1 character and starts with a
* recognized prefix, avoiding false positives on standalone units like "m" (meters).
*/
export const normalizeValueBySIPrefix = (
value: number,
unit: string | undefined
): number => {
if (!unit || unit.length <= 1) {
return value;
}
const prefix = unit[0];
if (prefix in SI_PREFIX_MULTIPLIERS) {
return value * SI_PREFIX_MULTIPLIERS[prefix];
}
return value;
};

View File

@@ -12,6 +12,10 @@ export type FormatEntityStateFunc = (
stateObj: HassEntity,
state?: string
) => string;
export type FormatEntityStateToPartsFunc = (
stateObj: HassEntity,
state?: string
) => ValuePart[];
export type FormatEntityAttributeValueFunc = (
stateObj: HassEntity,
attribute: string,
@@ -46,12 +50,13 @@ export const computeFormatFunctions = async (
sensorNumericDeviceClasses: string[]
): Promise<{
formatEntityState: FormatEntityStateFunc;
formatEntityStateToParts: FormatEntityStateToPartsFunc;
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
formatEntityAttributeValueToParts: FormatEntityAttributeValueToPartsFunc;
formatEntityAttributeName: FormatEntityAttributeNameFunc;
formatEntityName: FormatEntityNameFunc;
}> => {
const { computeStateDisplay } =
const { computeStateDisplay, computeStateToParts } =
await import("../entity/compute_state_display");
const {
computeAttributeValueDisplay,
@@ -70,6 +75,16 @@ export const computeFormatFunctions = async (
entities,
state
),
formatEntityStateToParts: (stateObj, state) =>
computeStateToParts(
localize,
stateObj,
locale,
sensorNumericDeviceClasses,
config,
entities,
state
),
formatEntityAttributeValue: (stateObj, attribute, value) =>
computeAttributeValueDisplay(
localize,

View File

@@ -1,3 +1,15 @@
let isViewTransitionDisabled = false;
try {
isViewTransitionDisabled =
window.localStorage.getItem("disableViewTransition") === "true";
} catch {
// ignore
}
export const setViewTransitionDisabled = (disabled: boolean): void => {
isViewTransitionDisabled = disabled;
};
/**
* Executes a synchronous callback within a View Transition if supported, otherwise runs it directly.
*
@@ -14,7 +26,7 @@
export const withViewTransition = (
callback: (viewTransitionAvailable: boolean) => void
): Promise<void> => {
if (!document.startViewTransition) {
if (!document.startViewTransition || isViewTransitionDisabled) {
callback(false);
return Promise.resolve();
}

View File

@@ -19,6 +19,7 @@ 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 { listenMediaQuery } from "../../common/dom/media_query";
import { themesContext } from "../../data/context";
import type { Themes } from "../../data/ws-themes";
@@ -27,6 +28,7 @@ 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";
@@ -92,10 +94,18 @@ export class HaChartBase extends LitElement {
private _resizeAnimationDuration?: number;
private _suspendResize = false;
private _layoutTransitionActive = false;
// @ts-ignore
private _resizeController = new ResizeController(this, {
callback: () => {
if (this.chart) {
if (this._suspendResize) {
this._shouldResizeChart = true;
return;
}
if (!this.chart.getZr().animation.isFinished()) {
this._shouldResizeChart = true;
} else {
@@ -113,8 +123,11 @@ export class HaChartBase extends LitElement {
private _originalZrFlush?: () => void;
private _pendingSetup = false;
public disconnectedCallback() {
super.disconnectedCallback();
this._pendingSetup = false;
while (this._listeners.length) {
this._listeners.pop()!();
}
@@ -126,7 +139,13 @@ export class HaChartBase extends LitElement {
public connectedCallback() {
super.connectedCallback();
if (this.hasUpdated) {
this._setupChart();
this._pendingSetup = true;
afterNextRender(() => {
if (this.isConnected && this._pendingSetup) {
this._pendingSetup = false;
this._setupChart();
}
});
}
this._listeners.push(
@@ -181,6 +200,26 @@ export class HaChartBase extends LitElement {
() => window.removeEventListener("keyup", handleKeyUp)
);
}
const handleLayoutTransition: EventListener = (ev) => {
const event = ev as HASSDomEvent<HASSDomEvents["hass-layout-transition"]>;
this._layoutTransitionActive = Boolean(event.detail?.active);
this.toggleAttribute(
"layout-transition-active",
this._layoutTransitionActive
);
this._suspendResize = this._layoutTransitionActive;
if (!this._suspendResize) {
this._resizeChartIfNeeded();
}
};
window.addEventListener("hass-layout-transition", handleLayoutTransition);
this._listeners.push(() =>
window.removeEventListener(
"hass-layout-transition",
handleLayoutTransition
)
);
}
protected firstUpdated() {
@@ -988,19 +1027,29 @@ export class HaChartBase extends LitElement {
}
private _handleChartRenderFinished = () => {
if (this._shouldResizeChart) {
this.chart?.resize({
animation:
this._reducedMotion ||
typeof this._resizeAnimationDuration !== "number"
? undefined
: { duration: this._resizeAnimationDuration },
});
this._shouldResizeChart = false;
this._resizeAnimationDuration = undefined;
}
this._resizeChartIfNeeded();
};
private _resizeChartIfNeeded() {
if (!this.chart || !this._shouldResizeChart) {
return;
}
if (this._suspendResize) {
return;
}
if (!this.chart.getZr().animation.isFinished()) {
return;
}
this.chart.resize({
animation:
this._reducedMotion || typeof this._resizeAnimationDuration !== "number"
? undefined
: { duration: this._resizeAnimationDuration },
});
this._shouldResizeChart = false;
this._resizeAnimationDuration = undefined;
}
private _compareCustomLegendOptions(
oldOptions: ECOption | undefined,
newOptions: ECOption | undefined
@@ -1022,11 +1071,18 @@ export class HaChartBase extends LitElement {
display: block;
position: relative;
letter-spacing: normal;
overflow: visible;
}
:host([layout-transition-active]),
:host([layout-transition-active]) .container,
:host([layout-transition-active]) .chart-container {
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
position: relative;
overflow: visible;
}
.container.has-height {
max-height: var(--chart-max-height, 350px);
@@ -1034,6 +1090,7 @@ export class HaChartBase extends LitElement {
.chart-container {
width: 100%;
max-height: var(--chart-max-height, 350px);
overflow: visible;
}
.has-height .chart-container {
flex: 1;

View File

@@ -58,7 +58,7 @@ export class HaSankeyChart extends LitElement {
@property({ type: Boolean }) public vertical = false;
@property({ type: String, attribute: false }) public valueFormatter?: (
@property({ attribute: false }) public valueFormatter?: (
value: number
) => string;

View File

@@ -29,7 +29,7 @@ export class HaSunburstChart extends LitElement {
@property({ attribute: false }) public data?: SunburstNode;
@property({ type: String, attribute: false }) public valueFormatter?: (
@property({ attribute: false }) public valueFormatter?: (
value: number
) => string;

View File

@@ -50,16 +50,16 @@ export class StateHistoryChartLine extends LitElement {
@property({ attribute: false }) public endTime!: Date;
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
@property({ attribute: false }) public paddingYAxis = 0;
@property({ attribute: false, type: Number }) public chartIndex?;
@property({ attribute: false }) public chartIndex?;
@property({ attribute: "logarithmic-scale", type: Boolean })
public logarithmicScale = false;
@property({ attribute: false, type: Number }) public minYAxis?: number;
@property({ attribute: false }) public minYAxis?: number;
@property({ attribute: false, type: Number }) public maxYAxis?: number;
@property({ attribute: false }) public maxYAxis?: number;
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
@@ -716,6 +716,18 @@ export class StateHistoryChartLine extends LitElement {
// Add an entry for final values
pushData(endTime, prevValues);
// For sensors, append current state if viewing recent data
const now = new Date();
// allow 1s of leeway for "now"
const isUpToNow = now.getTime() - endTime.getTime() <= 1000;
if (domain === "sensor" && isUpToNow && data.length === 1) {
const stateObj = this.hass.states[states.entity_id];
const currentValue = stateObj ? safeParseFloat(stateObj.state) : null;
if (currentValue !== null) {
data[0].data!.push([now, currentValue]);
}
}
// Concat two arrays
Array.prototype.push.apply(datasets, data);
});

View File

@@ -47,9 +47,9 @@ export class StateHistoryChartTimeline extends LitElement {
@property({ attribute: false }) public endTime!: Date;
@property({ attribute: false, type: Number }) public paddingYAxis = 0;
@property({ attribute: false }) public paddingYAxis = 0;
@property({ attribute: false, type: Number }) public chartIndex?;
@property({ attribute: false }) public chartIndex?;
@property({ attribute: "hide-reset-button", type: Boolean })
public hideResetButton?: boolean;

View File

@@ -60,7 +60,7 @@ export class StateHistoryCharts extends LitElement {
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
@property({ attribute: false, type: Number }) public hoursToShow?: number;
@property({ attribute: false }) public hoursToShow?: number;
@property({ attribute: "show-names", type: Boolean }) public showNames = true;
@@ -73,9 +73,9 @@ export class StateHistoryCharts extends LitElement {
@property({ attribute: "logarithmic-scale", type: Boolean })
public logarithmicScale = false;
@property({ attribute: false, type: Number }) public minYAxis?: number;
@property({ attribute: false }) public minYAxis?: number;
@property({ attribute: false, type: Number }) public maxYAxis?: number;
@property({ attribute: false }) public maxYAxis?: number;
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;

View File

@@ -62,14 +62,14 @@ export class StatisticsChart extends LitElement {
@property({ attribute: false }) public endTime?: Date;
@property({ attribute: false, type: Array })
@property({ attribute: false })
public statTypes: StatisticType[] = ["sum", "min", "mean", "max"];
@property({ attribute: false }) public chartType: "line" | "bar" = "line";
@property({ attribute: false, type: Number }) public minYAxis?: number;
@property({ attribute: false }) public minYAxis?: number;
@property({ attribute: false, type: Number }) public maxYAxis?: number;
@property({ attribute: false }) public maxYAxis?: number;
@property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false;
@@ -572,6 +572,7 @@ export class StatisticsChart extends LitElement {
let firstSum: number | null | undefined = null;
stats.forEach((stat) => {
const startDate = new Date(stat.start);
const endDate = new Date(stat.end);
if (prevDate === startDate) {
return;
}
@@ -601,10 +602,66 @@ export class StatisticsChart extends LitElement {
dataValues.push(val);
});
if (!this._hiddenStats.has(statistic_id)) {
pushData(startDate, new Date(stat.end), dataValues);
pushData(
startDate,
endDate.getTime() < endTime.getTime() ? endDate : endTime,
dataValues
);
}
});
// Close out the last stat segment at prevEndTime
const lastEndTime = prevEndTime;
const lastValues = prevValues;
if (lastEndTime && lastValues) {
statDataSets.forEach((d, i) => {
d.data!.push(
this._transformDataValue([lastEndTime, ...lastValues[i]!])
);
});
}
// Append current state if viewing recent data
const now = new Date();
// allow 10m of leeway for "now", because stats are 5 minute aggregated
const isUpToNow = now.getTime() - endTime.getTime() <= 600000;
if (isUpToNow) {
// Skip external statistics (they have ":" in the ID)
if (!statistic_id.includes(":")) {
const stateObj = this.hass.states[statistic_id];
if (stateObj) {
const currentValue = parseFloat(stateObj.state);
if (
isFinite(currentValue) &&
!this._hiddenStats.has(statistic_id)
) {
// Then push the current state at now
statTypes.forEach((type, i) => {
const val: (number | null)[] = [];
if (type === "sum" || type === "change") {
// Skip cumulative types - need special calculation
val.push(null);
} else if (
type === bandTop &&
this.chartType === "line" &&
drawBands &&
!this._hiddenStats.has(`${statistic_id}-${bandBottom}`)
) {
// For band chart, current value is both min and max, so diff is 0
val.push(0);
val.push(currentValue);
} else {
val.push(currentValue);
}
statDataSets[i].data!.push(
this._transformDataValue([now, ...val])
);
});
}
}
}
}
// Concat two arrays
Array.prototype.push.apply(totalDataSets, statDataSets);
Array.prototype.push.apply(legendData, statLegendData);

View File

@@ -10,9 +10,11 @@ import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-button";
import { createCloseHeading } from "../ha-dialog";
import "../ha-icon-button";
import "../ha-list";
import "../ha-list-item";
import "../ha-sortable";
import "../ha-svg-icon";
import type {
DataTableColumnContainer,
DataTableColumnData,

View File

@@ -20,6 +20,7 @@ import type { LocalizeFunc } from "../../common/translations/localize";
import { debounce } from "../../common/util/debounce";
import { groupBy } from "../../common/util/group-by";
import { nextRender } from "../../common/util/render-status";
import { STRINGS_SEPARATOR_DOT } from "../../common/const";
import { haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
@@ -130,9 +131,9 @@ export class HaDataTable extends LitElement {
// eslint-disable-next-line lit/no-native-attributes
@property({ type: String }) public id = "id";
@property({ attribute: false, type: String }) public noDataText?: string;
@property({ attribute: false }) public noDataText?: string;
@property({ attribute: false, type: String }) public searchLabel?: string;
@property({ attribute: false }) public searchLabel?: string;
@property({ type: Boolean, attribute: "no-label-float" })
public noLabelFloat? = false;
@@ -636,7 +637,7 @@ export class HaDataTable extends LitElement {
.map(
([key2, column2], i) =>
html`${i !== 0
? " · "
? STRINGS_SEPARATOR_DOT
: nothing}${column2.template
? column2.template(row)
: row[key2]}`
@@ -1192,6 +1193,7 @@ export class HaDataTable extends LitElement {
.mdc-data-table__cell--numeric {
text-align: var(--float-end);
direction: ltr;
}
.mdc-data-table__cell--icon {

View File

@@ -1,5 +1,5 @@
import { consume } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
@@ -11,10 +11,8 @@ import {
sortDeviceAutomations,
} from "../../data/device/device_automation";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { HomeAssistant } from "../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-generic-picker";
import "../ha-md-select";
import "../ha-md-select-option";
import type { PickerValueRenderer } from "../ha-picker-field";
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
@@ -192,7 +190,7 @@ export abstract class HaDeviceAutomationPicker<
this._renderEmpty = false;
}
private _automationChanged(ev: CustomEvent<{ value: string }>) {
private _automationChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
const value = ev.detail.value;
if (!value || NO_AUTOMATION_KEY === value) {
@@ -217,10 +215,4 @@ export abstract class HaDeviceAutomationPicker<
delete value.metadata;
fireEvent(this, "value-changed", { value });
}
static styles = css`
ha-select {
display: block;
}
`;
}

View File

@@ -48,7 +48,7 @@ export class HaDevicePicker extends LitElement {
@property({ type: String, attribute: "search-label" })
public searchLabel?: string;
@property({ attribute: false, type: Array }) public createDomains?: string[];
@property({ attribute: false }) public createDomains?: string[];
/**
* Show only devices with entities from specific domains.

View File

@@ -76,7 +76,7 @@ class HaEntitiesPicker extends LitElement {
@property({ attribute: false })
public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ attribute: false, type: Array }) public createDomains?: string[];
@property({ attribute: false }) public createDomains?: string[];
@property({ type: Boolean })
public reorder = false;

View File

@@ -58,7 +58,7 @@ export class HaEntityPicker extends LitElement {
@property({ type: String, attribute: "search-label" })
public searchLabel?: string;
@property({ attribute: false, type: Array }) public createDomains?: string[];
@property({ attribute: false }) public createDomains?: string[];
/**
* Show entities from specific domains.

View File

@@ -9,16 +9,7 @@ import secondsToDuration from "../../common/datetime/seconds_to_duration";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
import {
formatNumber,
getNumberFormatOptions,
isNumericState,
} from "../../common/number/format_number";
import {
isUnavailableState,
UNAVAILABLE,
UNKNOWN,
} from "../../data/entity/entity";
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import { timerTimeRemaining } from "../../data/timer";
import type { HomeAssistant } from "../../types";
@@ -180,16 +171,11 @@ export class HaStateLabelBadge extends LitElement {
}
// eslint-disable-next-line: disable=no-fallthrough
default:
return entityState.state === UNKNOWN ||
entityState.state === UNAVAILABLE
return isUnavailableState(entityState.state)
? "—"
: isNumericState(entityState)
? formatNumber(
entityState.state,
this.hass!.locale,
getNumberFormatOptions(entityState, entry)
)
: this.hass!.formatEntityState(entityState);
: this.hass!.formatEntityStateToParts(entityState).find(
(part) => part.type === "value"
)?.value;
}
}
@@ -238,7 +224,11 @@ export class HaStateLabelBadge extends LitElement {
if (domain === "timer") {
return secondsToDuration(_timerTimeRemaining);
}
return entityState.attributes.unit_of_measurement || null;
return (
this.hass!.formatEntityStateToParts(entityState).find(
(part) => part.type === "unit"
)?.value || null
);
}
private _clearInterval() {

View File

@@ -78,7 +78,7 @@ export class HaStatisticPicker extends LitElement {
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@property({ attribute: false, type: Array })
@property({ attribute: false })
public statisticIds?: StatisticsMetaData[];
@property({ attribute: false }) public helpMissingEntityUrl =

View File

@@ -11,7 +11,7 @@ class HaStatisticsPicker extends LitElement {
@property({ type: Array }) public value?: string[];
@property({ attribute: false, type: Array }) public statisticIds?: string[];
@property({ attribute: false }) public statisticIds?: string[];
@property({ attribute: "statistic-types" })
public statisticTypes?: "mean" | "sum";

View File

@@ -5,7 +5,7 @@ import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import type { Analytics, AnalyticsPreferences } from "../data/analytics";
import { haStyle } from "../resources/styles";
import "./ha-settings-row";
import "./ha-md-list-item";
import "./ha-switch";
import "./ha-tooltip";
import type { HaSwitch } from "./ha-switch";
@@ -33,105 +33,80 @@ export class HaAnalytics extends LitElement {
const baseEnabled = !loading && this.analytics!.preferences.base;
return html`
<ha-settings-row>
<span slot="heading" data-for="base">
${this.localize(
<ha-md-list-item>
<span slot="headline"
>${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.title`
)}
</span>
<span slot="description" data-for="base">
${this.localize(
)}</span
>
<span slot="supporting-text"
>${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.description`
)}
</span>
)}</span
>
<ha-switch
slot="end"
@change=${this._handleRowClick}
.checked=${!!baseEnabled}
.preference=${"base"}
.disabled=${loading}
name="base"
>
</ha-switch>
</ha-settings-row>
></ha-switch>
</ha-md-list-item>
${ADDITIONAL_PREFERENCES.map(
(preference) => html`
<ha-settings-row>
<span slot="heading" data-for=${preference}>
${this.localize(
<ha-md-list-item>
<span slot="headline"
>${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.title`
)}
</span>
<span slot="description" data-for=${preference}>
${this.localize(
)}</span
>
<span slot="supporting-text"
>${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.description`
)}
</span>
<span>
<ha-switch
.id="switch-${preference}"
@change=${this._handleRowClick}
.checked=${!!this.analytics?.preferences[preference]}
.preference=${preference}
name=${preference}
>
</ha-switch>
${baseEnabled
? nothing
: html`<ha-tooltip
.for="switch-${preference}"
placement="right"
>
${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
)}
</ha-tooltip>`}
</span>
</ha-settings-row>
)}</span
>
<ha-switch
slot="end"
.id="switch-${preference}"
@change=${this._handleRowClick}
.checked=${!!this.analytics?.preferences[preference]}
.preference=${preference}
name=${preference}
></ha-switch>
${baseEnabled
? nothing
: html`<ha-tooltip .for="switch-${preference}" placement="right">
${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
)}
</ha-tooltip>`}
</ha-md-list-item>
`
)}
<ha-settings-row>
<span slot="heading" data-for="diagnostics">
${this.localize(
<ha-md-list-item>
<span slot="headline"
>${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.title`
)}
</span>
<span slot="description" data-for="diagnostics">
${this.localize(
)}</span
>
<span slot="supporting-text"
>${this.localize(
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.description`
)}
</span>
)}</span
>
<ha-switch
slot="end"
@change=${this._handleRowClick}
.checked=${!!this.analytics?.preferences.diagnostics}
.preference=${"diagnostics"}
.disabled=${loading}
name="diagnostics"
>
</ha-switch>
</ha-settings-row>
></ha-switch>
</ha-md-list-item>
`;
}
protected updated(changedProps) {
super.updated(changedProps);
this.shadowRoot!.querySelectorAll("*[data-for]").forEach((el) => {
const forEl = (el as HTMLElement).dataset.for;
delete (el as HTMLElement).dataset.for;
el.addEventListener("click", () => {
const toFocus = this.shadowRoot!.querySelector(
`*[name=${forEl}]`
) as HTMLElement | null;
if (toFocus) {
toFocus.focus();
toFocus.click();
}
});
});
}
private _handleRowClick(ev: Event) {
const target = ev.currentTarget as HaSwitch;
const preference = (target as any).preference;
@@ -164,13 +139,10 @@ export class HaAnalytics extends LitElement {
color: var(--error-color);
}
ha-settings-row {
padding: 0;
}
span[slot="heading"],
span[slot="description"] {
cursor: pointer;
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
}
`,
];

View File

@@ -48,7 +48,6 @@ export class HaAnsiToHtml extends LitElement {
static styles = css`
pre {
overflow-x: auto;
margin: 0;
}
pre.wrap {

View File

@@ -163,7 +163,7 @@ export class HaAreaPicker extends LitElement {
{
id: ADD_NEW_ID + searchString,
primary: this.hass.localize(
"ui.components.area-picker.add_new_sugestion",
"ui.components.area-picker.add_new_suggestion",
{
name: searchString,
}

View File

@@ -9,7 +9,7 @@ import { computeFloorName } from "../common/entity/compute_floor_name";
import { getAreaContext } from "../common/entity/context/get_area_context";
import type { FloorRegistryEntry } from "../data/floor_registry";
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
import type { HomeAssistant } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-expansion-panel";
import "./ha-floor-icon";
import "./ha-items-display-editor";
@@ -200,7 +200,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
fireEvent(this, "value-changed", { value: newValue });
}
private async _areaDisplayChanged(ev: CustomEvent<{ value: DisplayValue }>) {
private async _areaDisplayChanged(ev: ValueChangedEvent<DisplayValue>) {
ev.stopPropagation();
const value = ev.detail.value;
const currentFloorId = (ev.currentTarget as any).floorId;

View File

@@ -36,7 +36,7 @@ export class HaAssistChat extends LitElement {
@property({ type: Boolean, attribute: "disable-speech" })
public disableSpeech = false;
@property({ type: Boolean, attribute: false })
@property({ attribute: false })
public startListening?: boolean;
@query("#message-input") private _messageInput!: HaTextField;

View File

@@ -2,14 +2,12 @@ import type { PropertyValueMap } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { formatLanguageCode } from "../common/language/format_language";
import type { AssistPipeline } from "../data/assist_pipeline";
import { listAssistPipelines } from "../data/assist_pipeline";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-select";
import type { HaSelect } from "./ha-select";
const PREFERRED = "preferred";
const LAST_USED = "last_used";
@@ -41,6 +39,31 @@ export class HaAssistPipelinePicker extends LitElement {
return nothing;
}
const value = this.value ?? this._default;
const options: HaSelectOption[] = [
{
value: PREFERRED,
label: this.hass.localize("ui.components.pipeline-picker.preferred", {
preferred: this._pipelines.find(
(pipeline) => pipeline.id === this._preferredPipeline
)?.name,
}),
},
];
if (this.includeLastUsed) {
options.unshift({
value: LAST_USED,
label: this.hass.localize("ui.components.pipeline-picker.last_used"),
});
}
options.push(
...this._pipelines.map((pipeline) => ({
value: pipeline.id,
label: `${pipeline.name} (${formatLanguageCode(pipeline.language, this.hass.locale)})`,
}))
);
return html`
<ha-select
.label=${this.label ||
@@ -49,33 +72,8 @@ export class HaAssistPipelinePicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.options=${options}
>
${this.includeLastUsed
? html`
<ha-list-item .value=${LAST_USED}>
${this.hass!.localize(
"ui.components.pipeline-picker.last_used"
)}
</ha-list-item>
`
: null}
<ha-list-item .value=${PREFERRED}>
${this.hass!.localize("ui.components.pipeline-picker.preferred", {
preferred: this._pipelines.find(
(pipeline) => pipeline.id === this._preferredPipeline
)?.name,
})}
</ha-list-item>
${this._pipelines.map(
(pipeline) =>
html`<ha-list-item .value=${pipeline.id}>
${pipeline.name}
(${formatLanguageCode(pipeline.language, this.hass.locale)})
</ha-list-item>`
)}
</ha-select>
`;
}
@@ -96,17 +94,17 @@ export class HaAssistPipelinePicker extends LitElement {
}
`;
private _changed(ev): void {
const target = ev.target as HaSelect;
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === this._default)
value === "" ||
value === this.value ||
(this.value === undefined && value === this._default)
) {
return;
}
this.value = target.value === this._default ? undefined : target.value;
this.value = value === this._default ? undefined : value;
fireEvent(this, "value-changed", { value: this.value });
}
}

View File

@@ -4,10 +4,9 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import type { HaSelectSelectEvent } from "./ha-select";
import "./ha-icon-button";
import "./ha-input-helper-text";
import "./ha-list-item";
import "./ha-select";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@@ -260,14 +259,10 @@ export class HaBaseTimeInput extends LitElement {
.required=${this.required}
.value=${this.amPm}
.disabled=${this.disabled}
name="amPm"
naturalMenuWidth
fixedMenuPosition
.name=${"amPm"}
@selected=${this._valueChanged}
@closed=${stopPropagation}
.options=${["AM", "PM"]}
>
<ha-list-item value="AM">AM</ha-list-item>
<ha-list-item value="PM">PM</ha-list-item>
</ha-select>`}
</div>
${this.helper
@@ -282,10 +277,12 @@ export class HaBaseTimeInput extends LitElement {
fireEvent(this, "value-changed");
}
private _valueChanged(ev: InputEvent) {
private _valueChanged(ev: InputEvent | HaSelectSelectEvent): void {
const textField = ev.currentTarget as HaTextField;
this[textField.name] =
textField.name === "amPm" ? textField.value : Number(textField.value);
textField.name === "amPm"
? (ev as HaSelectSelectEvent).detail.value
: Number(textField.value);
const value: TimeChangedEvent = {
hours: this.hours,
minutes: this.minutes,
@@ -366,10 +363,6 @@ export class HaBaseTimeInput extends LitElement {
ha-textfield:last-child {
--text-field-border-top-right-radius: var(--mdc-shape-medium);
}
ha-select {
--mdc-shape-small: 0;
width: 85px;
}
:host([clearable]) .mdc-select__anchor {
padding-inline-end: var(--select-selected-text-padding-end, 12px);
}

View File

@@ -2,12 +2,11 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { stringCompare } from "../common/string/compare";
import type { Blueprint, BlueprintDomain, Blueprints } from "../data/blueprint";
import { fetchBlueprints } from "../data/blueprint";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import type { HaSelectSelectEvent } from "./ha-select";
import "./ha-select";
@customElement("ha-blueprint-picker")
@@ -55,20 +54,16 @@ class HaBluePrintPicker extends LitElement {
<ha-select
.label=${this.label ||
this.hass.localize("ui.components.blueprint-picker.select_blueprint")}
fixedMenuPosition
naturalMenuWidth
.value=${this.value}
.disabled=${this.disabled}
@selected=${this._blueprintChanged}
@closed=${stopPropagation}
>
${this._processedBlueprints(this.blueprints).map(
(blueprint) => html`
<ha-list-item .value=${blueprint.path}>
${blueprint.name}
</ha-list-item>
`
.options=${this._processedBlueprints(this.blueprints).map(
(blueprint) => ({
value: blueprint.path,
label: blueprint.name,
})
)}
>
</ha-select>
`;
}
@@ -82,8 +77,8 @@ class HaBluePrintPicker extends LitElement {
}
}
private _blueprintChanged(ev) {
const newValue = ev.target.value;
private _blueprintChanged(ev: HaSelectSelectEvent) {
const newValue = ev.detail.value;
if (newValue !== this.value) {
this.value = newValue;

View File

@@ -51,6 +51,7 @@ export class HaCard extends LitElement {
font-weight: var(--ha-font-weight-normal);
}
/* clean-css ignore:start */
:host
::slotted(
.card-content:not(:nth-child(1 of .card-content, .card-header))
@@ -59,6 +60,7 @@ export class HaCard extends LitElement {
padding-top: 0;
margin-top: calc(var(--ha-space-2) * -1);
}
/* clean-css ignore:end */
:host ::slotted(.card-content) {
padding: var(--ha-space-4);

View File

@@ -6,7 +6,7 @@ import memoizeOne from "memoize-one";
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeKeys } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
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";
@@ -224,7 +224,7 @@ export class HaColorPicker extends LitElement {
`;
}
private _valueChanged(ev: CustomEvent<{ value?: string }>) {
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
ev.stopPropagation();
const selected = ev.detail.value;
const normalized =

View File

@@ -1,24 +1,31 @@
import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { mdiMenuDown } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, nothing } from "lit";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status";
import memoizeOne from "memoize-one";
import type { HomeAssistant } from "../types";
import "./ha-attribute-icon";
import "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-icon";
import type { HaIcon } from "./ha-icon";
import "./ha-ripple";
import "./ha-svg-icon";
import type { HaSvgIcon } from "./ha-svg-icon";
import "./ha-menu";
export interface SelectOption {
label: string;
value: string;
iconPath?: string;
icon?: string;
attributeIcon?: {
stateObj: HassEntity;
attribute: string;
attributeValue?: string;
};
}
@customElement("ha-control-select-menu")
export class HaControlSelectMenu extends SelectBase {
@query(".select") protected mdcRoot!: HTMLElement;
@query(".select-anchor") protected anchorElement!: HTMLDivElement | null;
export class HaControlSelectMenu extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, attribute: "show-arrow" })
public showArrow = false;
@@ -26,95 +33,83 @@ export class HaControlSelectMenu extends SelectBase {
@property({ type: Boolean, attribute: "hide-label" })
public hideLabel = false;
@property() public options;
@property({ type: Boolean })
public disabled = false;
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.get("options")) {
this.layoutOptions();
this.selectByValue(this.value);
}
}
@property({ type: Boolean })
public required = false;
@property()
public label?: string;
@property()
public value?: string;
@property({ attribute: false }) public options: SelectOption[] = [];
@query("button") private _triggerButton!: HTMLButtonElement;
public override render() {
const classes = {
"select-disabled": this.disabled,
"select-required": this.required,
"select-invalid": !this.isUiValid,
"select-no-value": !this.selectedText,
};
const labelledby = this.label && !this.hideLabel ? "label" : undefined;
const labelAttribute =
this.label && this.hideLabel ? this.label : undefined;
if (this.disabled) {
return this._renderTrigger();
}
return html`
<div class="select ${classMap(classes)}">
<input
class="formElement"
.name=${this.name}
.value=${this.value}
hidden
?disabled=${this.disabled}
?required=${this.required}
/>
<!-- @ts-ignore -->
<div
class="select-anchor"
aria-autocomplete="none"
role="combobox"
aria-expanded=${this.menuOpen}
aria-invalid=${!this.isUiValid}
aria-haspopup="listbox"
aria-labelledby=${ifDefined(labelledby)}
aria-label=${ifDefined(labelAttribute)}
aria-required=${this.required}
aria-controls="listbox"
@focus=${this.onFocus}
@blur=${this.onBlur}
@click=${this.onClick}
@keydown=${this.onKeydown}
>
${this._renderIcon()}
<div class="content">
${this.hideLabel
? nothing
: html`<p id="label" class="label">${this.label}</p>`}
${this.selectedText
? html`<p class="value">${this.selectedText}</p>`
: nothing}
</div>
${this._renderArrow()}
<ha-ripple .disabled=${this.disabled}></ha-ripple>
</div>
${this.renderMenu()}
</div>
<ha-dropdown placement="bottom" @wa-show=${this._showDropdown}>
${this._renderTrigger()} ${this.options.map(this._renderOption)}
</ha-dropdown>
`;
}
protected override renderMenu() {
const classes = this.getMenuClasses();
return html`<ha-menu
innerRole="listbox"
wrapFocus
class=${classMap(classes)}
activatable
.fullwidth=${this.fixedMenuPosition ? false : !this.naturalMenuWidth}
.open=${this.menuOpen}
.anchor=${this.anchorElement}
.fixed=${this.fixedMenuPosition}
@selected=${this.onSelected}
@opened=${this.onOpened}
@closed=${this.onClosed}
@items-updated=${this.onItemsUpdated}
@keydown=${this.handleTypeahead}
private _renderTrigger() {
const selectedText = this.getValueObject(this.options, this.value)?.label;
const classes = {
"select-disabled": this.disabled,
"select-required": this.required,
"select-no-value": !selectedText,
};
return html`<button
?disabled=${this.disabled}
slot="trigger"
class="select-anchor ${classMap(classes)}"
>
${this.renderMenuContent()}
</ha-menu>`;
${this._renderIcon()}
<div class="content">
${this.hideLabel
? nothing
: html`<p id="label" class="label">${this.label}</p>`}
${selectedText ? html`<p class="value">${selectedText}</p>` : nothing}
</div>
${this._renderArrow()}
</button>`;
}
private _renderOption = (option: SelectOption) =>
html`<ha-dropdown-item
.value=${option.value}
.selected=${this.value === option.value}
>${option.iconPath
? 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>`
: nothing}
${option.label}</ha-dropdown-item
>`;
private _renderArrow() {
if (!this.showArrow) return nothing;
if (!this.showArrow) {
return nothing;
}
return html`
<div class="icon">
@@ -124,47 +119,42 @@ export class HaControlSelectMenu extends SelectBase {
}
private _renderIcon() {
const index = this.mdcFoundation?.getSelectedIndex();
const items = this.menuElement?.items ?? [];
const item = index != null ? items[index] : undefined;
const { iconPath, icon, attributeIcon } =
this.getValueObject(this.options, this.value) ?? {};
const defaultIcon = this.querySelector("[slot='icon']");
const icon = (item?.querySelector("[slot='graphic']") ?? null) as
| HaSvgIcon
| HaIcon
| null;
if (!defaultIcon && !icon) {
return null;
}
return html`
<div class="icon">
${icon && icon.localName === "ha-svg-icon" && "path" in icon
? html`<ha-svg-icon .path=${icon.path}></ha-svg-icon>`
: icon && icon.localName === "ha-icon" && "icon" in icon
? html`<ha-icon .path=${icon.icon}></ha-icon>`
: html`<slot name="icon"></slot>`}
${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>`
: defaultIcon
? html`<slot name="icon"></slot>`
: nothing}
</div>
`;
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("translations-updated", this._translationsUpdated);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener(
"translations-updated",
this._translationsUpdated
private _showDropdown() {
this.style.setProperty(
"--control-select-menu-width",
`${this._triggerButton.offsetWidth}px`
);
}
private _translationsUpdated = debounce(async () => {
await nextRender();
this.layoutOptions();
}, 500);
private getValueObject = memoizeOne(
(options: SelectOption[], value?: string) =>
options.find((option) => option.value === value)
);
static override styles = [
css`
@@ -186,6 +176,8 @@ export class HaControlSelectMenu extends SelectBase {
-webkit-tap-highlight-color: transparent;
}
.select-anchor {
border: none;
text-align: left;
height: var(--control-select-menu-height);
padding: var(--control-select-menu-padding);
overflow: hidden;
@@ -211,6 +203,12 @@ export class HaControlSelectMenu extends SelectBase {
font-weight: var(--ha-font-weight-normal);
letter-spacing: 0.25px;
}
.select-anchor:hover {
--control-select-menu-background-color: var(
--ha-color-on-neutral-quiet
);
}
.content {
display: flex;
flex-direction: column;
@@ -230,16 +228,19 @@ export class HaControlSelectMenu extends SelectBase {
}
.label {
font-size: 0.85em;
font-size: var(--ha-font-size-s);
letter-spacing: 0.4px;
}
.select-no-value .label {
font-size: inherit;
line-height: inherit;
letter-spacing: inherit;
}
.content .value {
font-size: var(--ha-font-size-m);
}
.select-anchor:focus-visible {
box-shadow: 0 0 0 2px var(--control-select-menu-focus-color);
}
@@ -258,10 +259,14 @@ export class HaControlSelectMenu extends SelectBase {
opacity: var(--control-select-menu-background-opacity);
}
.select-disabled .select-anchor {
.select-disabled.select-anchor {
cursor: not-allowed;
color: var(--disabled-color);
}
ha-dropdown::part(menu) {
min-width: var(--control-select-menu-width);
}
`,
];
}

View File

@@ -24,10 +24,10 @@ export class HaControlSwitch extends LitElement {
@property({ type: Boolean }) public checked = false;
// SVG icon path (if you need a non SVG icon instead, use the provided on icon slot to pass an <ha-icon slot="icon-on"> in)
@property({ attribute: false, type: String }) pathOn?: string;
@property({ attribute: false }) pathOn?: string;
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
@property({ attribute: false, type: String }) pathOff?: string;
@property({ attribute: false }) pathOff?: string;
@property({ type: String })
public label?: string;

View File

@@ -3,7 +3,6 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { debounce } from "../common/util/debounce";
import type { ConfigEntry, SubEntry } from "../data/config_entries";
import { getConfigEntry, getSubEntries } from "../data/config_entries";
@@ -14,9 +13,8 @@ import { fetchIntegrationManifest } from "../data/integration";
import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow";
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
const NONE = "__NONE_OPTION__";
@@ -73,37 +71,35 @@ export class HaConversationAgentPicker extends LitElement {
value = NONE;
}
const options: HaSelectOption[] = this._agents.map((agent) => ({
value: agent.id,
label: agent.name,
disabled:
agent.supported_languages !== "*" &&
agent.supported_languages.length === 0,
}));
if (!this.required) {
options.unshift({
value: NONE,
label: this.hass.localize(
"ui.components.conversation-agent-picker.none"
),
});
}
return html`
<ha-select
.label=${this.label ||
this.hass!.localize(
"ui.components.coversation-agent-picker.conversation_agent"
"ui.components.conversation-agent-picker.conversation_agent"
)}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize(
"ui.components.coversation-agent-picker.none"
)}
</ha-list-item>`
: nothing}
${this._agents.map(
(agent) =>
html`<ha-list-item
.value=${agent.id}
.disabled=${agent.supported_languages !== "*" &&
agent.supported_languages.length === 0}
>
${agent.name}
</ha-list-item>`
)}</ha-select
.options=${options}
></ha-select
>${(this._subConfigEntry &&
this._configEntry?.supported_subentry_types[
this._subConfigEntry.subentry_type
@@ -238,17 +234,17 @@ export class HaConversationAgentPicker extends LitElement {
}
`;
private _changed(ev): void {
const target = ev.target as HaSelect;
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
) {
return;
}
this.value = target.value === NONE ? undefined : target.value;
this.value = value === NONE ? undefined : value;
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "supported-languages-changed", {
value: this._agents!.find((agent) => agent.id === this.value)

View File

@@ -65,6 +65,10 @@ export class HaDateRangePicker extends LitElement {
@property({ attribute: "time-picker", type: Boolean })
public timePicker = false;
public open(): void {
this._openPicker();
}
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public minimal = false;
@@ -306,6 +310,15 @@ export class HaDateRangePicker extends LitElement {
return dateRangePicker.vueComponent.$children[0];
}
private _openPicker() {
if (!this._dateRangePicker.open) {
const datePicker = this.shadowRoot!.querySelector(
"date-range-picker div.date-range-inputs"
) as any;
datePicker?.click();
}
}
private _handleInputClick() {
// close the date picker, so it will open again on the click event
if (this._dateRangePicker.open) {

View File

@@ -7,8 +7,9 @@ import { nextRender } from "../common/util/render-status";
import { haStyleDialog } from "../resources/styles";
import type { HomeAssistant } from "../types";
import type { DatePickerDialogParams } from "./ha-date-input";
import "./ha-dialog";
import "./ha-button";
import "./ha-dialog-footer";
import "./ha-wa-dialog";
@customElement("ha-dialog-date-picker")
export class HaDialogDatePicker extends LitElement {
@@ -22,6 +23,8 @@ export class HaDialogDatePicker extends LitElement {
@state() private _params?: DatePickerDialogParams;
@state() private _open = false;
@state() private _value?: string;
public async showDialog(params: DatePickerDialogParams): Promise<void> {
@@ -30,9 +33,14 @@ export class HaDialogDatePicker extends LitElement {
await nextRender();
this._params = params;
this._value = params.value;
this._open = true;
}
public closeDialog() {
this._open = false;
}
private _dialogClosed() {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -41,7 +49,13 @@ export class HaDialogDatePicker extends LitElement {
if (!this._params) {
return nothing;
}
return html`<ha-dialog open @closed=${this.closeDialog}>
return html`<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
width="small"
without-header
@closed=${this._dialogClosed}
>
<app-datepicker
.value=${this._value}
.min=${this._params.min}
@@ -50,35 +64,40 @@ export class HaDialogDatePicker extends LitElement {
@datepicker-value-updated=${this._valueChanged}
.firstDayOfWeek=${this._params.firstWeekday}
></app-datepicker>
${this._params.canClear
? html`<ha-button
slot="secondaryAction"
@click=${this._clear}
variant="danger"
appearance="plain"
>
${this.hass.localize("ui.dialogs.date-picker.clear")}
</ha-button>`
: nothing}
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this._setToday}
>
${this.hass.localize("ui.dialogs.date-picker.today")}
</ha-button>
<ha-button
appearance="plain"
slot="primaryAction"
dialogaction="cancel"
class="cancel-btn"
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._setValue}>
${this.hass.localize("ui.common.ok")}
</ha-button>
</ha-dialog>`;
<div class="bottom-actions">
${this._params.canClear
? html`<ha-button
slot="secondaryAction"
@click=${this._clear}
variant="danger"
appearance="plain"
>
${this.hass.localize("ui.dialogs.date-picker.clear")}
</ha-button>`
: nothing}
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this._setToday}
>
${this.hass.localize("ui.dialogs.date-picker.today")}
</ha-button>
</div>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._setValue}>
${this.hass.localize("ui.common.ok")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>`;
}
private _valueChanged(ev: CustomEvent) {
@@ -108,11 +127,20 @@ export class HaDialogDatePicker extends LitElement {
static styles = [
haStyleDialog,
css`
ha-dialog {
ha-wa-dialog {
--dialog-content-padding: 0;
--justify-action-buttons: space-between;
}
.bottom-actions {
display: flex;
gap: var(--ha-space-4);
justify-content: center;
align-items: center;
width: 100%;
margin-bottom: var(--ha-space-1);
}
app-datepicker {
display: block;
margin-inline: auto;
--app-datepicker-accent-color: var(--primary-color);
--app-datepicker-bg-color: transparent;
--app-datepicker-color: var(--primary-text-color);
@@ -129,11 +157,6 @@ export class HaDialogDatePicker extends LitElement {
app-datepicker::part(body) {
direction: ltr;
}
@media all and (min-width: 450px) {
ha-dialog {
--mdc-dialog-min-width: 300px;
}
}
@media all and (max-width: 450px), all and (max-height: 500px) {
app-datepicker {
width: 100%;

View File

@@ -76,6 +76,18 @@ export class HaDialog extends DialogBase {
var(--divider-color)
);
z-index: var(--dialog-z-index, 8);
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
--mdc-typography-headline6-font-size: 1.574rem;
}
.mdc-dialog::before {
content: "";
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
-webkit-backdrop-filter: var(
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter, none)
@@ -84,9 +96,9 @@ export class HaDialog extends DialogBase {
--ha-dialog-scrim-backdrop-filter,
var(--dialog-backdrop-filter, none)
);
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
--mdc-typography-headline6-font-size: 1.574rem;
}
.mdc-dialog .mdc-dialog__scrim {
background-color: var(--mdc-dialog-scrim-color, none);
}
.mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end);

View File

@@ -1,50 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined";
import { customElement, property } from "lit/decorators";
@customElement("ha-divider")
export class HaMdDivider extends LitElement {
@property() public label?: string;
public render() {
return html`
<div
role=${ifDefined(this.label ? "separator" : undefined)}
aria-label=${ifDefined(this.label)}
>
<span class="line"></span>
${this.label
? html`
<span class="label">${this.label}</span>
<span class="line"></span>
`
: nothing}
</div>
`;
}
static styles = css`
:host {
width: var(--ha-divider-width, 100%);
}
div {
display: flex;
align-items: center;
justify-content: center;
}
.label {
padding: var(--ha-divider-label-padding, 0 16px);
}
.line {
flex: 1;
background-color: var(--divider-color);
height: var(--ha-divider-line-height, 1px);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-divider": HaMdDivider;
}
}

View File

@@ -4,6 +4,18 @@ import type { PropertyValues } from "lit";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { HASSDomEvent } from "../common/dom/fire_event";
declare global {
interface HASSDomEvents {
"hass-layout-transition": { active: boolean; reason?: string };
}
interface HTMLElementEventMap {
"hass-layout-transition": HASSDomEvent<
HASSDomEvents["hass-layout-transition"]
>;
}
}
const blockingElements = (document as any).$blockingElements;
@@ -15,6 +27,30 @@ export class HaDrawer extends DrawerBase {
private _rtlStyle?: HTMLElement;
private _sidebarTransitionActive = false;
private _handleDrawerTransitionStart = (ev: TransitionEvent) => {
if (ev.propertyName !== "width" || this._sidebarTransitionActive) {
return;
}
this._sidebarTransitionActive = true;
fireEvent(window, "hass-layout-transition", {
active: true,
reason: "sidebar",
});
};
private _handleDrawerTransitionEnd = (ev: TransitionEvent) => {
if (ev.propertyName !== "width" || !this._sidebarTransitionActive) {
return;
}
this._sidebarTransitionActive = false;
fireEvent(window, "hass-layout-transition", {
active: false,
reason: "sidebar",
});
};
protected createAdapter() {
return {
...super.createAdapter(),
@@ -63,6 +99,38 @@ export class HaDrawer extends DrawerBase {
}
}
protected firstUpdated() {
super.firstUpdated();
this.mdcRoot?.addEventListener(
"transitionstart",
this._handleDrawerTransitionStart
);
this.mdcRoot?.addEventListener(
"transitionend",
this._handleDrawerTransitionEnd
);
this.mdcRoot?.addEventListener(
"transitioncancel",
this._handleDrawerTransitionEnd
);
}
public disconnectedCallback() {
super.disconnectedCallback();
this.mdcRoot?.removeEventListener(
"transitionstart",
this._handleDrawerTransitionStart
);
this.mdcRoot?.removeEventListener(
"transitionend",
this._handleDrawerTransitionEnd
);
this.mdcRoot?.removeEventListener(
"transitioncancel",
this._handleDrawerTransitionEnd
);
}
private async _setupSwipe() {
const hammer = await import("../resources/hammer");
this._mc = new hammer.Manager(document, {
@@ -90,6 +158,16 @@ export class HaDrawer extends DrawerBase {
border-color: var(--divider-color, rgba(0, 0, 0, 0.12));
inset-inline-start: 0 !important;
inset-inline-end: initial !important;
transition-property: transform, width;
transition-duration:
var(--mdc-drawer-transition-duration, 0.2s),
var(--ha-animation-duration-normal);
transition-timing-function:
var(
--mdc-drawer-transition-timing-function,
cubic-bezier(0.4, 0, 0.2, 1)
),
ease;
}
.mdc-drawer.mdc-drawer--modal.mdc-drawer--open {
z-index: 200;
@@ -103,6 +181,15 @@ export class HaDrawer extends DrawerBase {
direction: var(--direction);
width: 100%;
box-sizing: border-box;
transition:
padding-left var(--ha-animation-duration-normal) ease,
padding-inline-start var(--ha-animation-duration-normal) ease;
}
@media (prefers-reduced-motion: reduce) {
.mdc-drawer,
.mdc-drawer-app-content {
transition: none;
}
}
`,
];

View File

@@ -1,9 +1,9 @@
import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-item/dropdown-item";
import "@home-assistant/webawesome/dist/components/icon/icon";
import { css, type CSSResultGroup, html } from "lit";
import { customElement } from "lit/decorators";
import "./ha-svg-icon";
import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
import { css, type CSSResultGroup, html } from "lit";
import { customElement, property } from "lit/decorators";
import "./ha-svg-icon";
/**
* Home Assistant dropdown item component
@@ -17,6 +17,8 @@ import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
*/
@customElement("ha-dropdown-item")
export class HaDropdownItem extends DropdownItem {
@property({ type: Boolean, reflect: true }) selected = false;
protected renderCheckboxIcon() {
return html`
<ha-svg-icon
@@ -37,6 +39,7 @@ export class HaDropdownItem extends DropdownItem {
#check {
visibility: visible;
flex-shrink: 0;
}
#icon ::slotted(*) {
@@ -46,6 +49,16 @@ export class HaDropdownItem extends DropdownItem {
:host([variant="danger"]) #icon ::slotted(*) {
color: var(--ha-color-on-danger-quiet);
}
:host([selected]) {
font-weight: var(--ha-font-weight-medium);
color: var(--primary-color);
background-color: var(--ha-color-fill-primary-quiet-resting);
--icon-primary-color: var(--primary-color);
}
:host([selected]:hover) {
background-color: var(--ha-color-fill-primary-quiet-hover);
}
`,
];
}

View File

@@ -1,6 +1,16 @@
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";
/**
* Event type for the ha-dropdown component when an item is selected.
* @param T - The type of the value of the selected item.
*/
export type HaDropdownSelectEvent<T = string> = CustomEvent<{
item: Omit<HaDropdownItem, "value"> & { value: T };
}>;
/**
* Home Assistant dropdown component
@@ -13,11 +23,37 @@ import { customElement, property } from "lit/decorators";
*
*/
@customElement("ha-dropdown")
// @ts-ignore Allow to set an alternative anchor element
export class HaDropdown extends Dropdown {
@property({ attribute: false }) dropdownTag = "ha-dropdown";
@property({ attribute: false }) dropdownItemTag = "ha-dropdown-item";
public get anchorElement(): HTMLButtonElement | WaButton | undefined {
// @ts-ignore Allow to set an anchor element on popup
return this.popup?.anchor as HTMLButtonElement | WaButton | undefined;
}
public set anchorElement(element: HTMLButtonElement | WaButton | undefined) {
// @ts-ignore Allow to get the current anchor element from popup
if (!this.popup) {
return;
}
// @ts-ignore Allow to get the current anchor element from popup
this.popup.anchor = element;
}
/** 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 {
if (this.anchorElement) {
return this.anchorElement;
}
// @ts-ignore fallback to default trigger slot if no anchorElement is set
return super.getTrigger();
}
static get styles(): CSSResultGroup {
return [
Dropdown.styles,

View File

@@ -6,6 +6,7 @@ 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";
export interface HaDurationData {
days?: number;
@@ -152,7 +153,9 @@ class HaDurationInput extends LitElement {
: NaN;
}
private _durationChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
private _durationChanged(
ev: ValueChangedEvent<TimeChangedEvent | undefined>
) {
ev.stopPropagation();
const value = ev.detail.value ? { ...ev.detail.value } : undefined;

View File

@@ -39,7 +39,7 @@ export class HaFab extends FabBase {
}
:disabled {
--mdc-theme-secondary: var(--disabled-text-color);
pointer-events: none;
cursor: not-allowed !important;
}
`,
// safari workaround - must be explicit

View File

@@ -31,6 +31,7 @@ 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) {
@@ -174,7 +175,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
}
}
private _handleAction(ev: CustomEvent<{ item: { value: string } }>) {
private _handleAction(ev: HaDropdownSelectEvent) {
const categoryId = (ev.currentTarget as any).categoryId;
const action = ev.detail.item.value;
switch (action) {
@@ -314,9 +315,13 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
}
ha-list {
--mdc-list-item-meta-size: auto;
--mdc-list-side-padding-right: 4px;
--mdc-list-side-padding-right: var(--ha-space-1);
--mdc-list-side-padding-left: var(--ha-space-4);
--mdc-icon-button-size: 36px;
}
ha-list-item {
--mdc-list-item-graphic-margin: var(--ha-space-4);
}
ha-dropdown-item {
font-size: var(--ha-font-size-m);
}

View File

@@ -179,6 +179,9 @@ export class HaFilterDomains extends LitElement {
margin-inline-start: initial;
margin-inline-end: 8px;
}
ha-check-list-item {
--mdc-list-item-graphic-margin: var(--ha-space-4);
}
.badge {
display: inline-block;
margin-left: 8px;

View File

@@ -199,6 +199,9 @@ export class HaFilterIntegrations extends LitElement {
margin-inline-start: auto;
margin-inline-end: 8px;
}
ha-check-list-item {
--mdc-list-item-graphic-margin: var(--ha-space-4);
}
.badge {
display: inline-block;
margin-left: 8px;

View File

@@ -164,6 +164,9 @@ export class HaFilterVoiceAssistants extends LitElement {
margin-inline-start: auto;
margin-inline-end: 8px;
}
ha-check-list-item {
--mdc-list-item-graphic-margin: var(--ha-space-4);
}
.badge {
display: inline-block;
margin-left: 8px;

View File

@@ -359,7 +359,7 @@ export class HaFloorPicker extends LitElement {
{
id: ADD_NEW_ID + searchString,
primary: this.hass.localize(
"ui.components.floor-picker.add_new_sugestion",
"ui.components.floor-picker.add_new_suggestion",
{
name: searchString,
}

View File

@@ -11,8 +11,7 @@ import "../ha-formfield";
import "../ha-icon-button";
import "../ha-picker-field";
import type { HaDropdown } from "../ha-dropdown";
import type { HaDropdownItem } from "../ha-dropdown-item";
import type { HaDropdown, HaDropdownSelectEvent } from "../ha-dropdown";
import type {
HaFormElement,
HaFormMultiSelectData,
@@ -108,7 +107,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
`;
}
protected _toggleItem(ev: CustomEvent<{ item: HaDropdownItem }>) {
protected _toggleItem(ev: HaDropdownSelectEvent) {
ev.preventDefault(); // keep the dropdown open
const value = ev.detail.item.value;
const action = (ev.detail.item as any).action;

View File

@@ -17,6 +17,7 @@ import type {
HaFormOptionalActionsSchema,
HaFormSchema,
} from "./types";
import type { HaDropdownSelectEvent } from "../ha-dropdown";
const NO_ACTIONS = [];
@@ -142,7 +143,7 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
`;
}
private _handleAddAction(ev: CustomEvent<{ item: { value: string } }>) {
private _handleAddAction(ev: HaDropdownSelectEvent) {
const action = ev.detail.item.value;
this._displayActions = [...(this._displayActions ?? []), action];
}

View File

@@ -30,7 +30,7 @@ export class HaGauge extends LitElement {
@property({ attribute: false })
public formatOptions?: Intl.NumberFormatOptions;
@property({ attribute: false, type: String }) public valueText?: string;
@property({ attribute: false }) public valueText?: string;
@property({ attribute: false }) public locale!: FrontendLocaleData;

View File

@@ -48,7 +48,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
section?: string
) => (PickerComboBoxItem | string)[] | undefined;
@property({ attribute: false, type: Array })
@property({ attribute: false })
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
@property({ attribute: false })

View File

@@ -113,13 +113,13 @@ class HaHsColorPicker extends LitElement {
@property({ type: Boolean, reflect: true })
public disabled = false;
@property({ type: Number, attribute: false })
@property({ attribute: false })
public renderSize?: number;
@property({ type: Array })
public value?: [number, number];
@property({ attribute: false, type: Number })
@property({ attribute: false })
public colorBrightness?: number;
@property({ type: Number })
@@ -131,10 +131,10 @@ class HaHsColorPicker extends LitElement {
@property({ type: Number })
public ww?: number;
@property({ attribute: false, type: Number })
@property({ attribute: false })
public minKelvin?: number;
@property({ attribute: false, type: Number })
@property({ attribute: false })
public maxKelvin?: number;
@query("#canvas") private _canvas!: HTMLCanvasElement;

View File

@@ -19,7 +19,7 @@ export interface HaIconButtonToolbarItem {
@customElement("ha-icon-button-toolbar")
export class HaIconButtonToolbar extends LitElement {
@property({ type: Array, attribute: false })
@property({ attribute: false })
public items: (HaIconButtonToolbarItem | string)[] = [];
@queryAll("ha-icon-button") private _buttons?: HaIconButton[];

View File

@@ -9,7 +9,6 @@ import type { HomeAssistant } from "../types";
import "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-icon-button";
import "./ha-md-divider";
import "./ha-svg-icon";
import "./ha-tooltip";

View File

@@ -182,7 +182,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
{
id: ADD_NEW_ID + searchString,
primary: this.hass.localize(
"ui.components.label-picker.add_new_sugestion",
"ui.components.label-picker.add_new_suggestion",
{
name: searchString,
}

View File

@@ -107,9 +107,6 @@ export class HaLanguagePicker extends LitElement {
@property({ attribute: "no-sort", type: Boolean }) public noSort = false;
@property({ attribute: "inline-arrow", type: Boolean })
public inlineArrow = false;
@state() _defaultLanguages: string[] = [];
@query("ha-generic-picker", true) public genericPicker!: HaGenericPicker;

View File

@@ -134,16 +134,51 @@ export class HaMarkdown extends LitElement {
}
table[role="presentation"] {
--markdown-table-border-collapse: separate;
--markdown-table-border-width: attr(border, 0);
--markdown-table-border-width: 0;
--markdown-table-padding-inline: 0;
--markdown-table-padding-block: 0;
th {
vertical-align: attr(valign, middle);
}
th,
td {
vertical-align: middle;
}
}
table[role="presentation"] td[valign="top"],
table[role="presentation"] th[valign="top"] {
vertical-align: top;
}
table[role="presentation"] td[valign="middle"],
table[role="presentation"] th[valign="middle"] {
vertical-align: middle;
}
table[role="presentation"] td[valign="bottom"],
table[role="presentation"] th[valign="bottom"] {
vertical-align: bottom;
}
table[role="presentation"] td[valign="baseline"],
table[role="presentation"] th[valign="baseline"] {
vertical-align: baseline;
}
@supports (border-width: attr(border px, 0)) {
table[role="presentation"] {
--markdown-table-border-width: attr(border px, 0);
}
table[role="presentation"] th,
table[role="presentation"] td {
vertical-align: attr(valign, middle);
}
}
table[role="presentation"][border="0"] {
--markdown-table-border-width: 0;
}
table[role="presentation"][border="1"] {
--markdown-table-border-width: 1px;
}
table[role="presentation"][border="2"] {
--markdown-table-border-width: 2px;
}
table[role="presentation"][border="3"] {
--markdown-table-border-width: 3px;
}
table {
border-collapse: var(--markdown-table-border-collapse, collapse);
}

View File

@@ -1,263 +0,0 @@
import { Dialog } from "@material/web/dialog/internal/dialog";
import { styles } from "@material/web/dialog/internal/dialog-styles";
import {
type DialogAnimation,
DIALOG_DEFAULT_CLOSE_ANIMATION,
DIALOG_DEFAULT_OPEN_ANIMATION,
} from "@material/web/dialog/internal/animations";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
// workaround to be able to overlay a dialog with another dialog
Dialog.addInitializer(async (instance) => {
await instance.updateComplete;
const dialogInstance = instance as HaMdDialog;
// @ts-expect-error dialog is private
dialogInstance.dialog.prepend(dialogInstance.scrim);
// @ts-expect-error scrim is private
dialogInstance.scrim.style.inset = 0;
// @ts-expect-error scrim is private
dialogInstance.scrim.style.zIndex = 0;
const { getOpenAnimation, getCloseAnimation } = dialogInstance;
dialogInstance.getOpenAnimation = () => {
const animations = getOpenAnimation.call(this);
animations.container = [
...(animations.container ?? []),
...(animations.dialog ?? []),
];
animations.dialog = [];
return animations;
};
dialogInstance.getCloseAnimation = () => {
const animations = getCloseAnimation.call(this);
animations.container = [
...(animations.container ?? []),
...(animations.dialog ?? []),
];
animations.dialog = [];
return animations;
};
});
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>;
/**
* Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs
*
*/
@customElement("ha-md-dialog")
export class HaMdDialog extends Dialog {
/**
* When true the dialog will not close when the user presses the esc key or press out of the dialog.
*/
@property({ attribute: "disable-cancel-action", type: Boolean })
public disableCancelAction = false;
private _polyfillDialogRegistered = false;
constructor() {
super();
this.addEventListener("cancel", this._handleCancel);
if (typeof HTMLDialogElement !== "function") {
this.addEventListener("open", this._handleOpen);
if (!DIALOG_POLYFILL) {
DIALOG_POLYFILL = import("dialog-polyfill");
}
}
// if browser doesn't support animate API disable open/close animations
if (this.animate === undefined) {
this.quick = true;
}
// if browser doesn't support animate API disable open/close animations
if (this.animate === undefined) {
this.quick = true;
}
}
// prevent open in older browsers and wait for polyfill to load
private async _handleOpen(openEvent: Event) {
openEvent.preventDefault();
if (this._polyfillDialogRegistered) {
return;
}
this._polyfillDialogRegistered = true;
this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css");
const dialog = this.shadowRoot?.querySelector(
"dialog"
) as HTMLDialogElement;
const dialogPolyfill = await DIALOG_POLYFILL;
dialogPolyfill.default.registerDialog(dialog);
this.removeEventListener("open", this._handleOpen);
this.show();
}
private async _loadPolyfillStylesheet(href) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = href;
return new Promise<void>((resolve, reject) => {
link.onload = () => resolve();
link.onerror = () =>
reject(new Error(`Stylesheet failed to load: ${href}`));
this.shadowRoot?.appendChild(link);
});
}
private _handleCancel(closeEvent: Event) {
if (this.disableCancelAction) {
closeEvent.preventDefault();
const dialogElement = this.shadowRoot?.querySelector("dialog .container");
if (this.animate !== undefined) {
dialogElement?.animate(
[
{
transform: "rotate(-1deg)",
"animation-timing-function": "ease-in",
},
{
transform: "rotate(1.5deg)",
"animation-timing-function": "ease-out",
},
{
transform: "rotate(0deg)",
"animation-timing-function": "ease-in",
},
],
{
duration: 200,
iterations: 2,
}
);
}
}
}
static override styles = [
styles,
css`
:host {
--md-dialog-container-color: var(--card-background-color);
--md-dialog-headline-color: var(--primary-text-color);
--md-dialog-supporting-text-color: var(--primary-text-color);
--md-sys-color-scrim: #000000;
--md-dialog-headline-weight: var(--ha-font-weight-normal);
--md-dialog-headline-size: var(--ha-font-size-xl);
--md-dialog-supporting-text-size: var(--ha-font-size-m);
--md-dialog-supporting-text-line-height: var(--ha-line-height-normal);
--md-divider-color: var(--divider-color);
}
:host([type="alert"]) {
min-width: 320px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
:host(:not([type="alert"])) {
min-width: var(--mdc-dialog-min-width, 100vw);
min-height: 100%;
max-height: 100%;
--md-dialog-container-shape: 0;
}
.container {
margin-top: var(--safe-area-inset-top, 0);
margin-bottom: var(--safe-area-inset-bottom, 0);
margin-left: var(--safe-area-inset-left, 0);
margin-right: var(--safe-area-inset-right, 0);
}
}
::slotted(ha-dialog-header[slot="headline"]) {
display: contents;
}
slot[name="actions"]::slotted(*) {
padding: var(--ha-space-4);
}
.scroller {
overflow: var(--dialog-content-overflow, auto);
}
slot[name="content"]::slotted(*) {
padding: var(--dialog-content-padding, var(--ha-space-6));
}
.scrim {
z-index: 10; /* overlay navigation */
}
`,
];
}
// by default the dialog open/close animation will be from/to the top
// but if we have a special mobile dialog which is at the bottom of the screen, a from bottom animation can be used:
const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = {
...DIALOG_DEFAULT_OPEN_ANIMATION,
dialog: [
[
// Dialog slide up
[{ transform: "translateY(50px)" }, { transform: "translateY(0)" }],
{ duration: 500, easing: "cubic-bezier(.3,0,0,1)" },
],
],
container: [
[
// Container fade in
[{ opacity: 0 }, { opacity: 1 }],
{ duration: 50, easing: "linear", pseudoElement: "::before" },
],
],
};
const CLOSE_TO_BOTTOM_ANIMATION: DialogAnimation = {
...DIALOG_DEFAULT_CLOSE_ANIMATION,
dialog: [
[
// Dialog slide down
[{ transform: "translateY(0)" }, { transform: "translateY(50px)" }],
{ duration: 150, easing: "cubic-bezier(.3,0,0,1)" },
],
],
container: [
[
// Container fade out
[{ opacity: "1" }, { opacity: "0" }],
{ delay: 100, duration: 50, easing: "linear", pseudoElement: "::before" },
],
],
};
export const getMobileOpenFromBottomAnimation = () => {
const matches = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
return matches ? OPEN_FROM_BOTTOM_ANIMATION : DIALOG_DEFAULT_OPEN_ANIMATION;
};
export const getMobileCloseToBottomAnimation = () => {
const matches = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
return matches ? CLOSE_TO_BOTTOM_ANIMATION : DIALOG_DEFAULT_CLOSE_ANIMATION;
};
declare global {
interface HTMLElementTagNameMap {
"ha-md-dialog": HaMdDialog;
}
}

View File

@@ -1,22 +0,0 @@
import { Divider } from "@material/web/divider/internal/divider";
import { styles } from "@material/web/divider/internal/divider-styles";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-md-divider")
export class HaMdDivider extends Divider {
static override styles = [
styles,
css`
:host {
--md-divider-color: var(--divider-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-md-divider": HaMdDivider;
}
}

View File

@@ -1,52 +0,0 @@
import { MenuItemEl } from "@material/web/menu/internal/menuitem/menu-item";
import { styles } from "@material/web/menu/internal/menuitem/menu-item-styles";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-md-menu-item")
export class HaMdMenuItem extends MenuItemEl {
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
static override styles = [
styles,
css`
:host {
--ha-icon-display: block;
--md-sys-color-primary: var(--primary-text-color);
--md-sys-color-on-primary: var(--primary-text-color);
--md-sys-color-secondary: var(--secondary-text-color);
--md-sys-color-surface: var(--card-background-color);
--md-sys-color-on-surface: var(--primary-text-color);
--md-sys-color-on-surface-variant: var(--secondary-text-color);
--md-sys-color-secondary-container: rgba(
var(--rgb-primary-color),
0.15
);
--md-sys-color-on-secondary-container: var(--text-primary-color);
--mdc-icon-size: 16px;
--md-sys-color-on-primary-container: var(--primary-text-color);
--md-sys-color-on-secondary-container: var(--primary-text-color);
--md-menu-item-label-text-font: Roboto, sans-serif;
}
:host(.warning) {
--md-menu-item-label-text-color: var(--error-color);
--md-menu-item-leading-icon-color: var(--error-color);
}
::slotted([slot="headline"]) {
text-wrap: nowrap;
}
:host([disabled]) {
opacity: 1;
--md-menu-item-label-text-color: var(--disabled-text-color);
--md-menu-item-leading-icon-color: var(--disabled-text-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-md-menu-item": HaMdMenuItem;
}
}

View File

@@ -1,47 +0,0 @@
import { Menu } from "@material/web/menu/internal/menu";
import { styles } from "@material/web/menu/internal/menu-styles";
import type { CloseMenuEvent } from "@material/web/menu/menu";
import {
CloseReason,
KeydownCloseKey,
} from "@material/web/menu/internal/controllers/shared";
import { css } from "lit";
import { customElement } from "lit/decorators";
import type { HaMdMenuItem } from "./ha-md-menu-item";
@customElement("ha-md-menu")
export class HaMdMenu extends Menu {
connectedCallback(): void {
super.connectedCallback();
this.addEventListener("close-menu", this._handleCloseMenu);
}
private _handleCloseMenu(ev: CloseMenuEvent) {
if (
ev.detail.reason.kind === CloseReason.KEYDOWN &&
ev.detail.reason.key === KeydownCloseKey.ESCAPE
) {
return;
}
(ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator);
}
static override styles = [
styles,
css`
:host {
--md-sys-color-surface-container: var(--card-background-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-md-menu": HaMdMenu;
}
interface HTMLElementEventMap {
"close-menu": CloseMenuEvent;
}
}

View File

@@ -1,27 +0,0 @@
import { SelectOptionEl } from "@material/web/select/internal/selectoption/select-option";
import { styles } from "@material/web/menu/internal/menuitem/menu-item-styles";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-md-select-option")
export class HaMdSelectOption extends SelectOptionEl {
static override styles = [
styles,
css`
:host {
--ha-icon-display: block;
--md-sys-color-primary: var(--primary-text-color);
--md-sys-color-secondary: var(--secondary-text-color);
--md-sys-color-surface: var(--card-background-color);
--md-sys-color-on-surface: var(--primary-text-color);
--md-sys-color-on-surface-variant: var(--secondary-text-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-md-select-option": HaMdSelectOption;
}
}

View File

@@ -1,36 +0,0 @@
import { FilledSelect } from "@material/web/select/internal/filled-select";
import { styles as sharedStyles } from "@material/web/select/internal/shared-styles";
import { styles } from "@material/web/select/internal/filled-select-styles";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-md-select")
export class HaMdSelect extends FilledSelect {
static override styles = [
sharedStyles,
styles,
css`
:host {
--ha-icon-display: block;
--md-sys-color-primary: var(--primary-text-color);
--md-sys-color-secondary: var(--secondary-text-color);
--md-sys-color-surface: var(--card-background-color);
--md-sys-color-on-surface-variant: var(--secondary-text-color);
--md-sys-color-surface-container-highest: var(--input-fill-color);
--md-sys-color-on-surface: var(--input-ink-color);
--md-sys-color-surface-container: var(--input-fill-color);
--md-sys-color-on-secondary-container: var(--primary-text-color);
--md-sys-color-secondary-container: var(--input-fill-color);
--md-menu-container-color: var(--card-background-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-md-select": HaMdSelect;
}
}

View File

@@ -1,45 +0,0 @@
import { MenuBase } from "@material/mwc-menu/mwc-menu-base";
import { styles } from "@material/mwc-menu/mwc-menu.css";
import { html } from "lit";
import { customElement } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "./ha-list";
@customElement("ha-menu")
export class HaMenu extends MenuBase {
protected get listElement() {
if (!this.listElement_) {
this.listElement_ = this.renderRoot.querySelector("ha-list");
return this.listElement_;
}
return this.listElement_;
}
protected renderList() {
const itemRoles = this.innerRole === "menu" ? "menuitem" : "option";
const classes = this.renderListClasses();
return html`<ha-list
rootTabbable
.innerAriaLabel=${this.innerAriaLabel}
.innerRole=${this.innerRole}
.multi=${this.multi}
class=${classMap(classes)}
.itemRoles=${itemRoles}
.wrapFocus=${this.wrapFocus}
.activatable=${this.activatable}
@action=${this.onAction}
>
<slot></slot>
</ha-list>`;
}
static styles = styles;
}
declare global {
interface HTMLElementTagNameMap {
"ha-menu": HaMenu;
}
}

View File

@@ -5,7 +5,6 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { SupervisorMounts } from "../data/supervisor/mounts";
import {
@@ -15,9 +14,9 @@ import {
} from "../data/supervisor/mounts";
import type { HomeAssistant } from "../types";
import "./ha-alert";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
const _BACKUP_DATA_DISK_ = "/backup";
@@ -52,60 +51,54 @@ class HaMountPicker extends LitElement {
if (!this._mounts) {
return nothing;
}
const dataDiskOption = html`<ha-list-item
graphic="icon"
.value=${_BACKUP_DATA_DISK_}
>
<span>
${this.hass.localize("ui.components.mount-picker.use_datadisk") ||
"Use data disk for backup"}
</span>
<ha-svg-icon slot="graphic" .path=${mdiHarddisk}></ha-svg-icon>
</ha-list-item>`;
const options: HaSelectOption[] = this._filterMounts(
this._mounts,
this.usage
).map((mount) => ({
value: mount.name,
label: mount.name,
secondary: `${mount.server}${mount.port ? `:${mount.port}` : ""}${
mount.type === SupervisorMountType.NFS ? mount.path : `:${mount.share}`
}`,
iconPath:
mount.usage === SupervisorMountUsage.MEDIA
? mdiPlayBox
: mount.usage === SupervisorMountUsage.SHARE
? mdiFolder
: mdiBackupRestore,
}));
if (this.usage === SupervisorMountUsage.BACKUP) {
const dataDiskOption = {
value: _BACKUP_DATA_DISK_,
iconPath: mdiHarddisk,
label:
this.hass.localize("ui.components.mount-picker.use_datadisk") ||
"Use data disk for backup",
};
if (
!this._mounts.default_backup_mount ||
this._mounts.default_backup_mount === _BACKUP_DATA_DISK_
) {
options.unshift(dataDiskOption);
} else {
options.push(dataDiskOption);
}
}
return html`
<ha-select
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.mount-picker.mount")
: this.label}
.value=${this._value}
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}
.helper=${this.helper}
@selected=${this._mountChanged}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.options=${options}
>
${this.usage === SupervisorMountUsage.BACKUP &&
(!this._mounts.default_backup_mount ||
this._mounts.default_backup_mount === _BACKUP_DATA_DISK_)
? dataDiskOption
: nothing}
${this._filterMounts(this._mounts, this.usage).map(
(mount) =>
html`<ha-list-item twoline graphic="icon" .value=${mount.name}>
<span>${mount.name}</span>
<span slot="secondary"
>${mount.server}${mount.port
? `:${mount.port}`
: nothing}${mount.type === SupervisorMountType.NFS
? mount.path
: `:${mount.share}`}</span
>
<ha-svg-icon
slot="graphic"
.path=${mount.usage === SupervisorMountUsage.MEDIA
? mdiPlayBox
: mount.usage === SupervisorMountUsage.SHARE
? mdiFolder
: mdiBackupRestore}
></ha-svg-icon>
</ha-list-item>`
)}
${this.usage === SupervisorMountUsage.BACKUP &&
this._mounts.default_backup_mount
? dataDiskOption
: nothing}
</ha-select>
`;
}
@@ -153,16 +146,10 @@ class HaMountPicker extends LitElement {
}
}
private get _value() {
return this.value || "";
}
private _mountChanged(ev: HaSelectSelectEvent) {
const newValue = ev.detail.value;
private _mountChanged(ev: Event) {
ev.stopPropagation();
const target = ev.target as HaSelect;
const newValue = target.value;
if (newValue !== this._value) {
if (newValue !== this.value) {
this._setValue(newValue);
}
}

View File

@@ -11,6 +11,7 @@ import { fetchConfig } from "../data/lovelace/config/types";
import { getPanelIcon, getPanelTitle } from "../data/panel";
import { findRelated, type RelatedResult } from "../data/search";
import { PANEL_DASHBOARDS } from "../panels/config/lovelace/dashboards/ha-config-lovelace-dashboards";
import { computeAreaPath } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
import { multiTermSortedSearch } from "../resources/fuseMultiTerm";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { ActionRelatedContext } from "../panels/lovelace/components/hui-action-editor";
@@ -25,8 +26,9 @@ import {
type NavigationGroup = "related" | "dashboards" | "views" | "other_routes";
const RELATED_SORT_PREFIX = {
area: "0_area",
device: "1_device",
area_view: "0_area_view",
area: "1_area_settings",
device: "2_device",
} as const;
interface NavigationItem extends PickerComboBoxItem {
@@ -260,12 +262,7 @@ export class HaNavigationPicker extends LitElement {
const viewConfigs = await Promise.all(
lovelacePanels.map((panel) =>
fetchConfig(
this.hass!.connection,
// path should be null to fetch default lovelace panel
panel.url_path === "lovelace" ? null : panel.url_path,
true
)
fetchConfig(this.hass!.connection, panel.url_path, true)
.then((config) => [panel.id, config] as [string, typeof config])
.catch((_) => [panel.id, undefined] as [string, undefined])
)
@@ -437,10 +434,31 @@ export class HaNavigationPicker extends LitElement {
for (const areaId of relatedAreaIds) {
const area = this.hass.areas[areaId];
const primary = area?.name ?? areaId;
// Area dashboard view
const viewPath = `/home/${computeAreaPath(areaId)}`;
relatedItems.push({
id: viewPath,
primary,
secondary: viewPath,
icon: area?.icon ?? undefined,
icon_path: area?.icon ? undefined : mdiTextureBox,
sorting_label: createSortingLabel(
RELATED_SORT_PREFIX.area_view,
primary,
viewPath
),
group: "related",
});
// Area settings
const path = `/config/areas/area/${areaId}`;
relatedItems.push({
id: path,
primary,
primary: this.hass.localize(
"ui.components.navigation-picker.area_settings",
{ area: primary }
),
secondary: path,
icon: area?.icon ?? undefined,
icon_path: area?.icon ? undefined : mdiTextureBox,

View File

@@ -89,7 +89,7 @@ export class HaPasswordField extends LitElement {
@property({ type: Boolean }) readOnly = false;
// eslint-disable-next-line lit/no-native-attributes
@property({ attribute: false, type: String }) autocapitalize = "";
@property({ attribute: false }) autocapitalize = "";
@state() private _unmaskedPassword = false;

View File

@@ -22,6 +22,7 @@ import {
import { haStyleScrollbar } from "../resources/styles";
import { loadVirtualizer } from "../resources/virtualizer";
import type { HomeAssistant } from "../types";
import { isTouch } from "../util/is_touch";
import "./chips/ha-chip-set";
import "./chips/ha-filter-chip";
import "./ha-combo-box-item";
@@ -54,6 +55,7 @@ export interface PickerComboBoxItem {
sorting_label?: string;
icon_path?: string;
icon?: string;
isRelated?: boolean;
}
export interface PickerComboBoxIndexSelectedDetail {
@@ -118,7 +120,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
section?: string
) => PickerComboBoxItem[] | undefined;
@property({ attribute: false, type: Array })
@property({ attribute: false })
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
@property({ attribute: false })
@@ -284,6 +286,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
section === "separator"
? html`<div class="separator"></div>`
: html`<ha-filter-chip
@mousedown=${isTouch ? undefined : this._preventBlur}
@click=${this._toggleSection}
.section-id=${section.id}
.selected=${this._selectedSection === section.id}
@@ -506,6 +509,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
this._valuePinned = true;
};
private _preventBlur(ev: Event) {
ev.preventDefault();
}
private _toggleSection(ev: Event) {
ev.stopPropagation();
this._resetSelectedItem();

View File

@@ -22,7 +22,7 @@ export class HaQrCode extends LitElement {
@property({ type: Number })
public margin = 4;
@property({ attribute: false, type: Number })
@property({ attribute: false })
public maskPattern?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
@property({ attribute: "center-image" }) public centerImage?: string;

View File

@@ -14,8 +14,8 @@ import type { HomeAssistant } from "../types";
import "./ha-alert";
import "./ha-button";
import "./ha-dropdown";
import type { HaDropdownSelectEvent } from "./ha-dropdown";
import "./ha-dropdown-item";
import type { HaDropdownItem } from "./ha-dropdown-item";
import "./ha-spinner";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@@ -135,9 +135,7 @@ class HaQrScanner extends LitElement {
(camera) => html`
<ha-dropdown-item
.value=${camera.id}
class=${this._selectedCamera === camera.id
? "selected"
: ""}
.selected=${this._selectedCamera === camera.id}
>
${camera.label}
</ha-dropdown-item>
@@ -259,7 +257,7 @@ class HaQrScanner extends LitElement {
this._qrCodeScanned(this._manualInput!.value);
}
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
const cameraId = ev.detail?.item?.value;
if (cameraId) {
this._selectedCamera = cameraId;
@@ -380,9 +378,6 @@ class HaQrScanner extends LitElement {
color: white;
border-radius: var(--ha-border-radius-circle);
}
ha-dropdown-item.selected {
font-weight: var(--ha-font-weight-bold);
}
.row {
display: flex;
align-items: center;

View File

@@ -61,6 +61,7 @@ class HaSegmentedBar extends LitElement {
: html`
<ha-tooltip for="segment-${index}" placement="top">
${segment.label}
(${((segment.value / totalValue) * 100).toFixed(1)}%)
</ha-tooltip>
`}
<div

View File

@@ -1,187 +1,207 @@
import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { styles } from "@material/mwc-select/mwc-select.css";
import { mdiClose } from "@mdi/js";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status";
import "./ha-icon-button";
import "./ha-menu";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-picker-field";
import type { HaPickerField } from "./ha-picker-field";
import "./ha-svg-icon";
export interface HaSelectOption {
value: string;
label?: string;
secondary?: string;
iconPath?: string;
disabled?: boolean;
}
/**
* Event type for the ha-select component when an item is selected.
* @param T - The type of the value of the selected item.
* @param Clearable - Whether the select is clearable (allows undefined values).
*/
export type HaSelectSelectEvent<
T = string,
Clearable extends boolean = false,
> = CustomEvent<{
value: Clearable extends true ? T | undefined : T;
}>;
@customElement("ha-select")
export class HaSelect extends SelectBase {
// @ts-ignore
@property({ type: Boolean }) public icon = false;
export class HaSelect extends LitElement {
@property({ type: Boolean }) public clearable = false;
@property({ type: Boolean, reflect: true }) public clearable = false;
@property({ attribute: false }) public options?: HaSelectOption[] | string[];
@property({ attribute: "inline-arrow", type: Boolean })
public inlineArrow = false;
@property() public label?: string;
@property() public options;
@property() public helper?: string;
@property() public value?: string;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean }) public disabled = false;
@state() private _opened = false;
@query("ha-picker-field") private _triggerField!: HaPickerField;
private _getValueLabel = memoizeOne(
(
options: HaSelectOption[] | string[] | undefined,
value: string | undefined
) => {
if (!options || !value) {
return value;
}
for (const option of options) {
if (
(typeof option === "string" && option === value) ||
(typeof option !== "string" && option.value === value)
) {
return typeof option === "string"
? option
: option.label || option.value;
}
}
return value;
}
);
protected override render() {
if (this.disabled) {
return this._renderField();
}
return html`
${super.render()}
${this.clearable && !this.required && !this.disabled && this.value
? html`<ha-icon-button
label="clear"
@click=${this._clearValue}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
<ha-dropdown
placement="bottom"
@wa-select=${this._handleSelect}
@wa-show=${this._handleShow}
@wa-hide=${this._handleHide}
>
${this._renderField()}
${this.options
? this.options.map(
(option) => html`
<ha-dropdown-item
.value=${typeof option === "string" ? option : option.value}
.disabled=${typeof option === "string"
? false
: (option.disabled ?? false)}
.selected=${this.value ===
(typeof option === "string" ? option : option.value)}
>
${option.iconPath
? html`<ha-svg-icon
slot="icon"
.path=${option.iconPath}
></ha-svg-icon>`
: nothing}
<div class="content">
${typeof option === "string"
? option
: option.label || option.value}
${option.secondary
? html`<div class="secondary">${option.secondary}</div>`
: nothing}
</div>
</ha-dropdown-item>
`
)
: html`<slot></slot>`}
</ha-dropdown>
`;
}
protected override renderMenu() {
const classes = this.getMenuClasses();
return html`<ha-menu
innerRole="listbox"
wrapFocus
class=${classMap(classes)}
activatable
.fullwidth=${this.fixedMenuPosition ? false : !this.naturalMenuWidth}
.open=${this.menuOpen}
.anchor=${this.anchorElement}
.fixed=${this.fixedMenuPosition}
@selected=${this.onSelected}
@opened=${this.onOpened}
@closed=${this.onClosed}
@items-updated=${this.onItemsUpdated}
@keydown=${this.handleTypeahead}
>
${this.renderMenuContent()}
</ha-menu>`;
private _renderField() {
const valueLabel = this._getValueLabel(this.options, this.value);
return html`
<ha-picker-field
slot="trigger"
type="button"
class=${this._opened ? "opened" : ""}
compact
aria-label=${ifDefined(this.label)}
@clear=${this._clearValue}
.label=${this.label}
.helper=${this.helper}
.value=${valueLabel}
.required=${this.required}
.disabled=${this.disabled}
.hideClearIcon=${!this.clearable ||
this.required ||
this.disabled ||
!this.value}
>
</ha-picker-field>
`;
}
protected override renderLeadingIcon() {
if (!this.icon) {
return nothing;
private _handleSelect(ev: CustomEvent<{ item: { value: string } }>) {
ev.stopPropagation();
const value = ev.detail.item.value;
if (value === this.value) {
return;
}
return html`<span class="mdc-select__icon"
><slot name="icon"></slot
></span>`;
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("translations-updated", this._translationsUpdated);
}
protected async firstUpdated() {
super.firstUpdated();
if (this.inlineArrow) {
this.shadowRoot
?.querySelector(".mdc-select__selected-text-container")
?.classList.add("inline-arrow");
}
}
protected updated(changedProperties) {
super.updated(changedProperties);
if (changedProperties.has("inlineArrow")) {
const textContainerElement = this.shadowRoot?.querySelector(
".mdc-select__selected-text-container"
);
if (this.inlineArrow) {
textContainerElement?.classList.add("inline-arrow");
} else {
textContainerElement?.classList.remove("inline-arrow");
}
}
if (changedProperties.get("options")) {
this.layoutOptions();
this.selectByValue(this.value);
}
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener(
"translations-updated",
this._translationsUpdated
);
fireEvent(this, "selected", { value });
}
private _clearValue(): void {
if (this.disabled || !this.value) {
return;
}
this.valueSetDirectly = true;
this.select(-1);
this.mdcFoundation.handleChange();
fireEvent(this, "selected", { value: undefined });
}
private _translationsUpdated = debounce(async () => {
await nextRender();
this.layoutOptions();
}, 500);
private _handleShow() {
this.style.setProperty(
"--select-menu-width",
`${this._triggerField.offsetWidth}px`
);
this._opened = true;
}
static override styles = [
styles,
css`
:host([clearable]) {
position: relative;
}
.mdc-select:not(.mdc-select--disabled) .mdc-select__icon {
color: var(--secondary-text-color);
}
.mdc-select__anchor {
width: var(--ha-select-min-width, 200px);
}
.mdc-select--filled .mdc-select__anchor {
height: var(--ha-select-height, 56px);
}
.mdc-select--filled .mdc-floating-label {
inset-inline-start: var(--ha-space-4);
inset-inline-end: initial;
direction: var(--direction);
}
.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label {
inset-inline-start: 48px;
inset-inline-end: initial;
direction: var(--direction);
}
.mdc-select .mdc-select__anchor {
padding-inline-start: var(--ha-space-4);
padding-inline-end: 0px;
direction: var(--direction);
}
.mdc-select__anchor .mdc-floating-label--float-above {
transform-origin: var(--float-start);
}
.mdc-select__selected-text-container {
padding-inline-end: var(--select-selected-text-padding-end, 0px);
}
:host([clearable]) .mdc-select__selected-text-container {
padding-inline-end: var(
--select-selected-text-padding-end,
var(--ha-space-4)
);
}
ha-icon-button {
position: absolute;
top: 10px;
right: 28px;
--mdc-icon-button-size: 36px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
inset-inline-start: initial;
inset-inline-end: 28px;
direction: var(--direction);
}
.inline-arrow {
flex-grow: 0;
}
`,
];
private _handleHide() {
this._opened = false;
}
static styles = css`
:host {
position: relative;
}
ha-picker-field.opened {
--mdc-text-field-idle-line-color: var(--primary-color);
}
ha-dropdown-item .content {
display: flex;
gap: var(--ha-space-1);
flex-direction: column;
}
ha-dropdown-item .secondary {
font-size: var(--ha-font-size-s);
color: var(--ha-color-text-secondary);
}
ha-dropdown::part(menu) {
min-width: var(--select-menu-width);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-select": HaSelect;
}
interface HASSDomEvents {
selected: { value: string | undefined };
}
}

View File

@@ -5,17 +5,16 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { SelectOption, SelectSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-checkbox";
import "../ha-dropdown-item";
import "../ha-formfield";
import "../ha-generic-picker";
import "../ha-input-helper-text";
import "../ha-list-item";
import "../ha-radio";
import "../ha-select";
import "../ha-select-box";
@@ -231,24 +230,15 @@ export class HaSelectSelector extends LitElement {
return html`
<ha-select
fixedMenuPosition
naturalMenuWidth
.label=${this.label ?? ""}
.value=${this.value ?? ""}
.value=${(this.value as string) ?? ""}
.helper=${this.helper ?? ""}
.disabled=${this.disabled}
.required=${this.required}
clearable
@closed=${stopPropagation}
@selected=${this._valueChanged}
.options=${options}
>
${options.map(
(item: SelectOption) => html`
<ha-list-item .value=${item.value} .disabled=${!!item.disabled}
>${item.label}</ha-list-item
>
`
)}
</ha-select>
`;
}
@@ -295,7 +285,7 @@ export class HaSelectSelector extends LitElement {
private _valueChanged(ev) {
ev.stopPropagation();
if (ev.detail?.index === -1 && this.value !== undefined) {
if (ev.detail?.value === undefined && this.value !== undefined) {
fireEvent(this, "value-changed", {
value: undefined,
});
@@ -385,7 +375,7 @@ export class HaSelectSelector extends LitElement {
ha-formfield {
display: block;
}
ha-list-item[disabled] {
ha-dropdown-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
ha-chip-set {

View File

@@ -224,6 +224,7 @@ ${typeof this._templateResult.result === "object"
margin-bottom: 0;
direction: ltr;
border-radius: var(--ha-border-radius-sm);
overflow-wrap: break-word;
}
`;
}

View File

@@ -108,6 +108,7 @@ export class HaSettingsRow extends LitElement {
white-space: normal;
}
.prefix-wrap {
flex: 1;
display: var(--settings-row-prefix-display);
}
:host([narrow]) .prefix-wrap {

View File

@@ -492,19 +492,22 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
@mouseleave=${this._itemMouseLeave}
>
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
${!this.alwaysExpand &&
(this._updatesCount > 0 || this._issuesCount > 0)
? html`<span class="badge" slot="start"
>${this._updatesCount + this._issuesCount}</span
>`
${this._updatesCount > 0 || this._issuesCount > 0
? html`
<span class="badge" slot="start">
${this._updatesCount + this._issuesCount}
</span>
`
: nothing}
<span class="item-text" slot="headline"
>${this.hass.localize("panel.config")}</span
>
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
? html`<span class="badge" slot="end"
>${this._updatesCount + this._issuesCount}</span
>`
${this._updatesCount > 0 || this._issuesCount > 0
? html`
<span class="badge" slot="end"
>${this._updatesCount + this._issuesCount}</span
>
`
: nothing}
</ha-md-list-item>
`;
@@ -524,13 +527,15 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
type="button"
>
<ha-svg-icon slot="start" .path=${mdiBell}></ha-svg-icon>
${!this.alwaysExpand && notificationCount > 0
? html`<span class="badge" slot="start">${notificationCount}</span>`
${notificationCount > 0
? html`
<span class="badge" slot="start"> ${notificationCount} </span>
`
: nothing}
<span class="item-text" slot="headline"
>${this.hass.localize("ui.notification_drawer.title")}</span
>
${this.alwaysExpand && notificationCount > 0
${notificationCount > 0
? html`<span class="badge" slot="end">${notificationCount}</span>`
: nothing}
</ha-md-list-item>
@@ -739,6 +744,8 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
);
font-size: var(--ha-font-size-xl);
align-items: center;
overflow: hidden;
width: calc(56px + var(--safe-area-inset-left, 0px));
padding-left: calc(
var(--ha-space-1) + var(--safe-area-inset-left, 0px)
);
@@ -747,6 +754,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
);
padding-inline-end: initial;
padding-top: var(--safe-area-inset-top, 0px);
transition: width var(--ha-animation-duration-normal) ease;
}
:host([expanded]) .menu {
width: calc(256px + var(--safe-area-inset-left, 0px));
@@ -761,15 +769,22 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
margin-left: 3px;
margin-inline-start: 3px;
margin-inline-end: initial;
width: 100%;
display: none;
flex: 1;
min-width: 0;
max-width: 0;
opacity: 0;
transition:
max-width var(--ha-animation-duration-normal) ease,
opacity var(--ha-animation-duration-normal) ease;
}
:host([narrow]) .title {
margin: 0;
padding: 0 var(--ha-space-4);
}
:host([expanded]) .title {
display: initial;
max-width: 100%;
opacity: 1;
transition-delay: 0ms, 80ms;
}
.panels-list {
@@ -827,6 +842,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--ha-space-3);
--md-list-item-leading-icon-size: var(--ha-space-6);
transition: width var(--ha-animation-duration-normal) ease;
}
:host([expanded]) ha-md-list-item {
width: 248px;
@@ -867,11 +883,22 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
}
ha-md-list-item .item-text {
display: none;
display: block;
max-width: 0;
opacity: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
transition:
max-width var(--ha-animation-duration-normal) ease,
opacity var(--ha-animation-duration-normal) ease;
}
:host([expanded]) ha-md-list-item .item-text {
max-width: 100%;
opacity: 1;
transition-delay: 0ms, 80ms;
display: block;
overflow: hidden;
text-overflow: ellipsis;
@@ -889,6 +916,9 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
background-color: var(--accent-color);
padding: 2px 6px;
color: var(--text-accent-color, var(--text-primary-color));
transition:
opacity var(--ha-animation-duration-normal) ease,
transform var(--ha-animation-duration-normal) ease;
}
ha-svg-icon + .badge {
@@ -900,6 +930,12 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
line-height: var(--ha-line-height-expanded);
padding: 0 var(--ha-space-1);
}
:host([expanded]) .badge[slot="start"],
:host(:not([expanded])) .badge[slot="end"] {
opacity: 0;
transform: scale(0.8);
pointer-events: none;
}
ha-md-list-item.user {
--md-list-item-leading-icon-size: var(--ha-space-10);
@@ -938,6 +974,15 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
-webkit-transform: scaleX(var(--scale-direction));
transform: scaleX(var(--scale-direction));
}
@media (prefers-reduced-motion: reduce) {
.menu,
ha-md-list-item,
ha-md-list-item .item-text,
.title {
transition: none;
}
}
`,
];
}

View File

@@ -2,16 +2,14 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { debounce } from "../common/util/debounce";
import type { STTEngine } from "../data/stt";
import { listSTTEngines } from "../data/stt";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import { computeDomain } from "../common/entity/compute_domain";
const NONE = "__NONE_OPTION__";
@@ -61,6 +59,30 @@ export class HaSTTPicker extends LitElement {
value = NONE;
}
const options: HaSelectOption[] = this._engines
.filter((engine) => !engine.deprecated || engine.engine_id !== value)
.map((engine) => {
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return {
value: engine.engine_id,
label,
disabled: engine.supported_languages?.length === 0,
};
});
if (this.required || value === NONE) {
options.unshift({
value: NONE,
label: this.hass.localize("ui.components.stt-picker.none") || "None",
});
}
return html`
<ha-select
.label=${this.label ||
@@ -69,33 +91,8 @@ export class HaSTTPicker extends LitElement {
.required=${this.required}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.options=${options}
>
${!this.required
? html`<ha-list-item .value=${NONE}>
${this.hass!.localize("ui.components.stt-picker.none")}
</ha-list-item>`
: nothing}
${this._engines.map((engine) => {
if (engine.deprecated && engine.engine_id !== value) {
return nothing;
}
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass!.states[engine.engine_id];
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
return html`<ha-list-item
.value=${engine.engine_id}
.disabled=${engine.supported_languages?.length === 0}
>
${label}
</ha-list-item>`;
})}
</ha-select>
`;
}
@@ -144,17 +141,17 @@ export class HaSTTPicker extends LitElement {
}
`;
private _changed(ev): void {
const target = ev.target as HaSelect;
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
if (
!this.hass ||
target.value === "" ||
target.value === this.value ||
(this.value === undefined && target.value === NONE)
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
) {
return;
}
this.value = target.value === NONE ? undefined : target.value;
this.value = value === NONE ? undefined : value;
fireEvent(this, "value-changed", { value: this.value });
fireEvent(this, "supported-languages-changed", {
value: this._engines!.find((engine) => engine.engine_id === this.value)

View File

@@ -53,7 +53,7 @@ import {
multiTermSortedSearch,
type FuseWeightedKey,
} from "../resources/fuseMultiTerm";
import type { HomeAssistant } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import { brandsUrl } from "../util/brands-url";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-generic-picker";
@@ -76,7 +76,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean, reflect: true }) public compact = false;
@property({ attribute: false, type: Array }) public createDomains?: string[];
@property({ attribute: false }) public createDomains?: string[];
/**
* Show only targets with entities from specific domains.
@@ -403,7 +403,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
`;
}
private _targetPicked(ev: CustomEvent<{ value: string }>) {
private _targetPicked(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
const value = ev.detail.value;
if (value.startsWith(CREATE_ID)) {

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