Compare commits

...

85 Commits

Author SHA1 Message Date
Aidan Timson 3bf5833262 Benchmark script 2026-02-11 02:14:56 +00:00
Aidan Timson 8c9da19935 SWC 2026-02-11 02:12:54 +00:00
Aidan Timson 4b1eeb0eb1 Use scrollbar styles on host 2026-02-11 02:12:54 +00:00
Aidan Timson f7ffdabe5d Move scrolling for dashboards inside view container 2026-02-11 02:12:54 +00: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](https://github.com/github/codeql-action/compare/b20883b0cd1f46c72ae0ba6d1090936928f9fa30...45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2)

---
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
234 changed files with 4613 additions and 3275 deletions
-1
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):**
+3 -3
View File
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
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@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
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@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
+1 -1
View File
@@ -1 +1 @@
24.13.0
24.13.1
+3
View File
@@ -31,4 +31,7 @@ module.exports = {
isDevContainer() {
return isTrue(process.env.DEV_CONTAINER);
},
jsMinifier() {
return (process.env.JS_MINIFIER || "swc").toLowerCase();
},
};
+7 -1
View File
@@ -80,7 +80,13 @@ const doneHandler = (done) => (err, stats) => {
console.log(stats.toString("minimal"));
}
log(`Build done @ ${new Date().toLocaleTimeString()}`);
const durationMs =
stats?.startTime && stats?.endTime ? stats.endTime - stats.startTime : 0;
const durationLabel = durationMs
? ` (${(durationMs / 1000).toFixed(1)}s, minifier: ${env.jsMinifier()})`
: ` (minifier: ${env.jsMinifier()})`;
log(`Build done @ ${new Date().toLocaleTimeString()}${durationLabel}`);
if (done) {
done();
+15 -5
View File
@@ -13,6 +13,7 @@ const { WebpackManifestPlugin } = require("rspack-manifest-plugin");
const log = require("fancy-log");
// eslint-disable-next-line @typescript-eslint/naming-convention
const WebpackBar = require("webpackbar/rspack");
const env = require("./env.cjs");
const paths = require("./paths.cjs");
const bundle = require("./bundle.cjs");
@@ -100,11 +101,20 @@ const createRspackConfig = ({
},
optimization: {
minimizer: [
new TerserPlugin({
parallel: true,
extractComments: true,
terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }),
}),
env.jsMinifier() === "terser"
? new TerserPlugin({
parallel: true,
extractComments: true,
terserOptions: bundle.terserOptions({ latestBuild, isTestBuild }),
})
: new rspack.SwcJsMinimizerRspackPlugin({
extractComments: true,
minimizerOptions: {
ecma: latestBuild ? 2015 : 5,
module: latestBuild,
format: { comments: false },
},
}),
],
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
+9 -13
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,14 +14,11 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
});
}
});
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
changeFunction = onChange;
onChange?.({
value: {
panelOrder: [],
hiddenPanels: [],
},
});
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 () => {};
});
@@ -48,4 +43,5 @@ export const mockFrontend = (hass: MockHomeAssistant) => {
return () => {};
});
hass.mockWS("repairs/list_issues", () => ({ issues: [] }));
hass.mockWS("frontend/get_themes", (_msg, currentHass) => currentHass.themes);
};
+1
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(() => {
+3 -1
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
+1
View File
@@ -0,0 +1 @@
import "../../../../src/components/ha-alert";
+1 -1
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!
+8 -8
View File
@@ -29,7 +29,7 @@
"@babel/runtime": "7.28.6",
"@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",
@@ -52,7 +52,7 @@
"@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",
@@ -132,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",
@@ -154,8 +154,8 @@
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.1",
"@rspack/core": "1.7.4",
"@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",
@@ -191,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",
@@ -235,6 +235,6 @@
},
"packageManager": "yarn@4.12.0",
"volta": {
"node": "24.13.0"
"node": "24.13.1"
}
}
+43
View File
@@ -0,0 +1,43 @@
#!/bin/sh
set -e
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
OUT_ROOT="$ROOT_DIR/hass_frontend"
OUT_LATEST="$OUT_ROOT/frontend_latest"
OUT_ES5="$OUT_ROOT/frontend_es5"
bytes_dir() {
if [ -d "$1" ]; then
du -sb "$1" | cut -f1
else
echo 0
fi
}
run_build() {
minifier="$1"
printf "\n==> Building with %s\n" "$minifier"
start_time=$(date +%s)
JS_MINIFIER="$minifier" "$ROOT_DIR/script/build_frontend"
end_time=$(date +%s)
duration=$((end_time - start_time))
latest_size=$(bytes_dir "$OUT_LATEST")
es5_size=$(bytes_dir "$OUT_ES5")
total_size=$(bytes_dir "$OUT_ROOT")
printf "%s|%s|%s|%s\n" "$minifier" "$duration" "$latest_size" "$es5_size" >> "$ROOT_DIR/temp/minifier_benchmark.tsv"
printf " duration: %ss\n" "$duration"
printf " frontend_latest: %s bytes\n" "$latest_size"
printf " frontend_es5: %s bytes\n" "$es5_size"
printf " hass_frontend: %s bytes\n" "$total_size"
}
mkdir -p "$ROOT_DIR/temp"
rm -f "$ROOT_DIR/temp/minifier_benchmark.tsv"
run_build swc
run_build terser
printf "\n==> Summary (minifier | seconds | latest bytes | es5 bytes)\n"
cat "$ROOT_DIR/temp/minifier_benchmark.tsv"
+3
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 = " · ";
+159 -44
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
);
};
+19 -10
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 }];
};
/**
@@ -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;
};
+16 -1
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,
+17 -11
View File
@@ -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,25 @@ 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
@@ -619,16 +635,6 @@ export class StatisticsChart extends LitElement {
isFinite(currentValue) &&
!this._hiddenStats.has(statistic_id)
) {
// First, 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]!])
);
});
}
// Then push the current state at now
statTypes.forEach((type, i) => {
const val: (number | null)[] = [];
+3 -1
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";
@@ -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 {
@@ -11,7 +11,7 @@ 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";
@@ -192,7 +192,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) {
+10 -20
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() {
+1 -1
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,
}
@@ -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;
+2 -2
View File
@@ -6,8 +6,8 @@ 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 type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-select";
import type { HaSelectOption } from "./ha-select";
const PREFERRED = "preferred";
const LAST_USED = "last_used";
@@ -94,7 +94,7 @@ export class HaAssistPipelinePicker extends LitElement {
}
`;
private _changed(ev: CustomEvent<{ value: string }>): void {
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
if (
!this.hass ||
+3 -2
View File
@@ -4,6 +4,7 @@ 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 type { HaSelectSelectEvent } from "./ha-select";
import "./ha-icon-button";
import "./ha-input-helper-text";
import "./ha-select";
@@ -276,11 +277,11 @@ export class HaBaseTimeInput extends LitElement {
fireEvent(this, "value-changed");
}
private _valueChanged(ev: InputEvent | CustomEvent<{ value: string }>): void {
private _valueChanged(ev: InputEvent | HaSelectSelectEvent): void {
const textField = ev.currentTarget as HaTextField;
this[textField.name] =
textField.name === "amPm"
? (ev as CustomEvent<{ value: string }>).detail.value
? (ev as HaSelectSelectEvent).detail.value
: Number(textField.value);
const value: TimeChangedEvent = {
hours: this.hours,
+2 -1
View File
@@ -6,6 +6,7 @@ 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 type { HaSelectSelectEvent } from "./ha-select";
import "./ha-select";
@customElement("ha-blueprint-picker")
@@ -76,7 +77,7 @@ class HaBluePrintPicker extends LitElement {
}
}
private _blueprintChanged(ev: CustomEvent<{ value: string }>) {
private _blueprintChanged(ev: HaSelectSelectEvent) {
const newValue = ev.detail.value;
if (newValue !== this.value) {
+2 -2
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 =
+1 -10
View File
@@ -89,7 +89,7 @@ export class HaControlSelectMenu extends LitElement {
private _renderOption = (option: SelectOption) =>
html`<ha-dropdown-item
.value=${option.value}
class=${this.value === option.value ? "selected" : ""}
.selected=${this.value === option.value}
>${option.iconPath
? html`<ha-svg-icon slot="icon" .path=${option.iconPath}></ha-svg-icon>`
: option.icon
@@ -263,15 +263,6 @@ export class HaControlSelectMenu extends LitElement {
cursor: not-allowed;
color: var(--disabled-color);
}
ha-dropdown-item.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);
}
ha-dropdown-item.selected:hover {
background-color: var(--ha-color-fill-primary-quiet-hover);
}
ha-dropdown::part(menu) {
min-width: var(--control-select-menu-width);
@@ -14,7 +14,7 @@ import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-option
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
import type { HomeAssistant } from "../types";
import "./ha-select";
import type { HaSelectOption } from "./ha-select";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
const NONE = "__NONE_OPTION__";
@@ -234,7 +234,7 @@ export class HaConversationAgentPicker extends LitElement {
}
`;
private _changed(ev: CustomEvent<{ value: string }>): void {
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
if (
!this.hass ||
+13
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) {
+61 -38
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%;
+10 -1
View File
@@ -2,7 +2,7 @@ import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-it
import "@home-assistant/webawesome/dist/components/icon/icon";
import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js";
import { css, type CSSResultGroup, html } from "lit";
import { customElement } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import "./ha-svg-icon";
/**
@@ -17,6 +17,8 @@ import "./ha-svg-icon";
*/
@customElement("ha-dropdown-item")
export class HaDropdownItem extends DropdownItem {
@property({ type: Boolean, reflect: true }) selected = false;
protected renderCheckboxIcon() {
return html`
<ha-svg-icon
@@ -47,6 +49,13 @@ 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);
}
`,
];
}
+34 -1
View File
@@ -1,9 +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";
export type HaDropdownSelectEvent = CustomEvent<{ item: HaDropdownItem }>;
/**
* 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
@@ -16,11 +23,37 @@ export type HaDropdownSelectEvent = CustomEvent<{ item: HaDropdownItem }>;
*
*/
@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,
+4 -1
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;
+5 -1
View File
@@ -315,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);
}
+3
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;
+3
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;
@@ -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;
+1 -1
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,
}
-1
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";
+1 -1
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,
}
-263
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;
}
}
-22
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;
}
}
-52
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;
}
}
-47
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;
}
}
+2 -2
View File
@@ -14,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 { HaSelectOption } from "./ha-select";
const _BACKUP_DATA_DISK_ = "/backup";
@@ -146,7 +146,7 @@ class HaMountPicker extends LitElement {
}
}
private _mountChanged(ev: CustomEvent<{ value: string }>) {
private _mountChanged(ev: HaSelectSelectEvent) {
const newValue = ev.detail.value;
if (newValue !== this.value) {
+1
View File
@@ -55,6 +55,7 @@ export interface PickerComboBoxItem {
sorting_label?: string;
icon_path?: string;
icon?: string;
isRelated?: boolean;
}
export interface PickerComboBoxIndexSelectedDetail {
+1 -6
View File
@@ -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>
@@ -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;
+14 -16
View File
@@ -17,6 +17,18 @@ export interface HaSelectOption {
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 LitElement {
@property({ type: Boolean }) public clearable = false;
@@ -82,10 +94,8 @@ export class HaSelect extends LitElement {
.disabled=${typeof option === "string"
? false
: (option.disabled ?? false)}
class=${this.value ===
(typeof option === "string" ? option : option.value)
? "selected"
: ""}
.selected=${this.value ===
(typeof option === "string" ? option : option.value)}
>
${option.iconPath
? html`<ha-svg-icon
@@ -170,10 +180,6 @@ export class HaSelect extends LitElement {
ha-picker-field.opened {
--mdc-text-field-idle-line-color: var(--primary-color);
}
ha-dropdown-item.selected:hover {
background-color: var(--ha-color-fill-primary-quiet-hover);
}
ha-dropdown-item .content {
display: flex;
gap: var(--ha-space-1);
@@ -188,14 +194,6 @@ export class HaSelect extends LitElement {
ha-dropdown::part(menu) {
min-width: var(--select-menu-width);
}
:host ::slotted(ha-dropdown-item.selected),
ha-dropdown-item.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);
}
`;
}
declare global {
+2 -2
View File
@@ -8,8 +8,8 @@ import { debounce } from "../common/util/debounce";
import type { STTEngine } from "../data/stt";
import { listSTTEngines } from "../data/stt";
import type { HomeAssistant } from "../types";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-select";
import type { HaSelectOption } from "./ha-select";
const NONE = "__NONE_OPTION__";
@@ -141,7 +141,7 @@ export class HaSTTPicker extends LitElement {
}
`;
private _changed(ev: CustomEvent<{ value: string }>): void {
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
if (
!this.hass ||
+2 -2
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";
@@ -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)) {
+2 -2
View File
@@ -3,8 +3,8 @@ import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant } from "../types";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-select";
import type { HaSelectOption } from "./ha-select";
const DEFAULT_THEME = "default";
@@ -63,7 +63,7 @@ export class HaThemePicker extends LitElement {
}
`;
private _changed(ev: CustomEvent<{ value: string }>): void {
private _changed(ev: HaSelectSelectEvent): void {
if (!this.hass || ev.detail.value === "") {
return;
}
+2 -1
View File
@@ -5,6 +5,7 @@ import { fireEvent } from "../common/dom/fire_event";
import type { FrontendLocaleData } from "../data/translation";
import "./ha-base-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
import type { ValueChangedEvent } from "../types";
@customElement("ha-time-input")
export class HaTimeInput extends LitElement {
@@ -69,7 +70,7 @@ export class HaTimeInput extends LitElement {
`;
}
private _timeChanged(ev: CustomEvent<{ value?: TimeChangedEvent }>) {
private _timeChanged(ev: ValueChangedEvent<TimeChangedEvent | undefined>) {
ev.stopPropagation();
const eventValue = ev.detail.value;
+2 -2
View File
@@ -8,8 +8,8 @@ import { debounce } from "../common/util/debounce";
import type { TTSEngine } from "../data/tts";
import { listTTSEngines } from "../data/tts";
import type { HomeAssistant } from "../types";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-select";
import type { HaSelectOption } from "./ha-select";
const NONE = "__NONE_OPTION__";
@@ -141,7 +141,7 @@ export class HaTTSPicker extends LitElement {
}
`;
private _changed(ev: CustomEvent<{ value: string }>): void {
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
if (
!this.hass ||
+2 -2
View File
@@ -6,8 +6,8 @@ import { debounce } from "../common/util/debounce";
import type { TTSVoice } from "../data/tts";
import { listTTSVoices } from "../data/tts";
import type { HomeAssistant } from "../types";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-select";
import type { HaSelectOption } from "./ha-select";
const NONE = "__NONE_OPTION__";
@@ -106,7 +106,7 @@ export class HaTTSVoicePicker extends LitElement {
}
`;
private _changed(ev: CustomEvent<{ value: string }>): void {
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
if (
!this.hass ||
+16 -16
View File
@@ -14,6 +14,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import { isIosApp } from "../util/is_ios";
import "./ha-dialog-header";
import "./ha-icon-button";
@@ -197,22 +198,21 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
await this.updateComplete;
requestAnimationFrame(() => {
// temporary disabled because of issues with focus in iOS app, can be reenabled in 2026.2.0
// if (isIosApp(this.hass)) {
// const element = this.querySelector("[autofocus]");
// if (element !== null) {
// if (!element.id) {
// element.id = "ha-wa-dialog-autofocus";
// }
// this.hass.auth.external!.fireMessage({
// type: "focus_element",
// payload: {
// element_id: element.id,
// },
// });
// }
// return;
// }
if (isIosApp(this.hass)) {
const element = this.querySelector("[autofocus]");
if (element !== null) {
if (!element.id) {
element.id = "ha-wa-dialog-autofocus";
}
this.hass.auth.external!.fireMessage({
type: "focus_element",
payload: {
element_id: element.id,
},
});
}
return;
}
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
});
};
@@ -1,11 +1,9 @@
import { mdiClose } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { supportsFeature } from "../../common/entity/supports-feature";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import { extractApiErrorMessage } from "../../data/hassio/common";
@@ -19,8 +17,9 @@ import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-alert";
import "../ha-button";
import "../ha-dialog";
import "../ha-dialog-footer";
import "../ha-dialog-header";
import "../ha-wa-dialog";
import "./ha-media-player-toggle";
import type { JoinMediaPlayersDialogParams } from "./show-join-media-players-dialog";
@@ -38,8 +37,11 @@ class DialogJoinMediaPlayers extends LitElement {
@state() private _error?: string;
@state() private _open = false;
public showDialog(params: JoinMediaPlayersDialogParams): void {
this._entityId = params.entityId;
this._open = true;
const stateObj = this.hass.states[params.entityId] as
| MediaPlayerEntity
@@ -54,6 +56,11 @@ class DialogJoinMediaPlayers extends LitElement {
}
public closeDialog() {
this._open = false;
}
private _dialogClosed(): void {
this._open = false;
this._entityId = undefined;
this._selectedEntities = [];
this._groupMembers = [];
@@ -68,23 +75,18 @@ class DialogJoinMediaPlayers extends LitElement {
}
const entityId = this._entityId;
const stateObj = this.hass.states[entityId] as HassEntity | undefined;
const name = (stateObj && computeStateName(stateObj)) || entityId;
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
flexContent
.heading=${name}
@closed=${this.closeDialog}
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
flexcontent
@closed=${this._dialogClosed}
>
<ha-dialog-header show-border slot="heading">
<ha-dialog-header show-border slot="header">
<ha-icon-button
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
dialogAction="close"
data-dialog="close"
slot="navigationIcon"
></ha-icon-button>
<span slot="title"
@@ -118,21 +120,23 @@ class DialogJoinMediaPlayers extends LitElement {
></ha-media-player-toggle>`
)}
</div>
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
.disabled=${!!this._submitting}
slot="primaryAction"
@click=${this._submit}
>
${this.hass.localize("ui.common.apply")}
</ha-button>
</ha-dialog>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
.disabled=${!!this._submitting}
slot="primaryAction"
@click=${this._submit}
>
${this.hass.localize("ui.common.apply")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
@@ -217,6 +221,7 @@ class DialogJoinMediaPlayers extends LitElement {
.content {
display: flex;
flex-direction: column;
padding-top: var(--ha-space-6);
row-gap: var(--ha-space-4);
}
@@ -24,6 +24,8 @@ import "../ha-icon-button";
import "./hat-logbook-note";
import type { NodeInfo } from "./hat-script-graph";
import { traceTabStyles } from "./trace-tab-styles";
import type { Trigger } from "../../data/automation";
import { migrateAutomationTrigger } from "../../data/automation";
const TRACE_PATH_TABS = [
"step_config",
@@ -166,7 +168,9 @@ export class HaTracePathDetails extends LitElement {
: selectedType === "trigger"
? html`<h2>
${describeTrigger(
currentDetail,
migrateAutomationTrigger({
...currentDetail,
}) as Trigger,
this.hass,
this._entityReg
)}
+4 -3
View File
@@ -32,12 +32,13 @@ export class VoiceAssistantBrandicon extends LitElement {
return [
haStyle,
css`
:host {
display: inline;
}
.logo {
position: relative;
vertical-align: middle;
height: 24px;
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
}
`,
];
+5 -20
View File
@@ -14,6 +14,7 @@ import {
import type { Collection, HassEntity } from "home-assistant-js-websocket";
import { getCollection } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { normalizeValueBySIPrefix } from "../common/number/normalize-by-si-prefix";
import {
calcDate,
calcDateProperty,
@@ -1431,26 +1432,10 @@ export const getPowerFromState = (stateObj: HassEntity): number | undefined => {
return undefined;
}
// Normalize to watts (W) based on unit of measurement (case-sensitive)
// Supported units: GW, kW, MW, mW, TW, W
const unit = stateObj.attributes.unit_of_measurement;
switch (unit) {
case "W":
return value;
case "kW":
return value * 1000;
case "mW":
return value / 1000;
case "MW":
return value * 1_000_000;
case "GW":
return value * 1_000_000_000;
case "TW":
return value * 1_000_000_000_000;
default:
// Assume value is in watts (W) if no unit or an unsupported unit is provided
return value;
}
return normalizeValueBySIPrefix(
value,
stateObj.attributes.unit_of_measurement
);
};
/**
+2 -1
View File
@@ -142,7 +142,7 @@ export const subscribeHistory = (
);
};
class HistoryStream {
export class HistoryStream {
hass: HomeAssistant;
hoursToShow?: number;
@@ -221,6 +221,7 @@ class HistoryStream {
// only expire the rest of the history as it ages.
const lastExpiredState = expiredStates[expiredStates.length - 1];
lastExpiredState.lu = purgeBeforePythonTime;
delete lastExpiredState.lc;
newHistory[entityId].unshift(lastExpiredState);
}
}
+6 -2
View File
@@ -41,12 +41,16 @@ export const enum TodoListEntityFeature {
SET_DESCRIPTION_ON_ITEM = 64,
}
export const getTodoLists = (hass: HomeAssistant): TodoList[] =>
export const getTodoLists = (
hass: HomeAssistant,
includeHidden = true
): TodoList[] =>
Object.keys(hass.states)
.filter(
(entityId) =>
computeDomain(entityId) === "todo" &&
!isUnavailableState(hass.states[entityId].state)
!isUnavailableState(hass.states[entityId].state) &&
(includeHidden || hass.entities[entityId]?.hidden !== true)
)
.map((entityId) => ({
...hass.states[entityId],
@@ -1,17 +1,12 @@
import { mdiClose } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button-toggle";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import {
getMobileCloseToBottomAnimation,
getMobileOpenFromBottomAnimation,
} from "../../../../components/ha-md-dialog";
import "../../../../components/ha-wa-dialog";
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type { LightColor, LightEntity } from "../../../../data/light";
import {
@@ -41,7 +36,7 @@ class DialogLightColorFavorite extends LitElement {
@state() private _modes: LightPickerMode[] = [];
@query("ha-md-dialog") private _dialog?: HaMdDialog;
@state() private _open = false;
public async showDialog(
dialogParams: LightColorFavoriteDialogParams
@@ -50,10 +45,11 @@ class DialogLightColorFavorite extends LitElement {
this._dialogParams = dialogParams;
this._color = dialogParams.initialColor ?? this._computeCurrentColor();
this._updateModes();
this._open = true;
}
public closeDialog(): void {
this._dialog?.close();
this._open = false;
}
private _updateModes() {
@@ -120,16 +116,8 @@ class DialogLightColorFavorite extends LitElement {
);
}
private async _cancel() {
this._dialogParams?.cancel?.();
}
private _cancelDialog() {
this._cancel();
this.closeDialog();
}
private _dialogClosed(): void {
this._open = false;
this._dialogParams = undefined;
this._entry = undefined;
this._color = undefined;
@@ -138,7 +126,7 @@ class DialogLightColorFavorite extends LitElement {
private async _save() {
if (!this._color) {
this._cancel();
this.closeDialog();
return;
}
this._dialogParams?.submit?.(this._color);
@@ -159,83 +147,76 @@ class DialogLightColorFavorite extends LitElement {
}
return html`
<ha-md-dialog
open
@cancel=${this._cancel}
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
.headerTitle=${this._dialogParams?.title}
@closed=${this._dialogClosed}
aria-labelledby="dialog-light-color-favorite-title"
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
.getCloseAnimation=${getMobileCloseToBottomAnimation}
>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title" id="dialog-light-color-favorite-title"
>${this._dialogParams?.title}</span
>
</ha-dialog-header>
<div slot="content">
<div class="header">
${this._modes.length > 1
? html`
<div class="modes">
${this._modes.map(
(value) => html`
<ha-icon-button-toggle
border-only
.selected=${value === this._mode}
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
.mode=${value}
@click=${this._modeChanged}
>
<span
class="wheel ${classMap({ [value]: true })}"
></span>
</ha-icon-button-toggle>
`
)}
</div>
`
: nothing}
</div>
<div class="content">
${this._mode === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-temp-picker>
`
: nothing}
${this._mode === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-rgb-picker>
`
: nothing}
</div>
<div class="header">
${this._modes.length > 1
? html`
<div class="modes">
${this._modes.map(
(value) => html`
<ha-icon-button-toggle
border-only
.selected=${value === this._mode}
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
.mode=${value}
@click=${this._modeChanged}
>
<span
class="wheel ${classMap({ [value]: true })}"
></span>
</ha-icon-button-toggle>
`
)}
</div>
`
: nothing}
</div>
<div slot="actions">
<ha-button appearance="plain" @click=${this._cancelDialog}>
<div class="content">
${this._mode === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-temp-picker>
`
: nothing}
${this._mode === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-rgb-picker>
`
: nothing}
</div>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._save} .disabled=${!this._color}
>${this.hass.localize("ui.common.save")}</ha-button
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${!this._color}
>
</div>
</ha-md-dialog>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
@@ -243,23 +224,18 @@ class DialogLightColorFavorite extends LitElement {
return [
haStyleDialog,
css`
ha-md-dialog {
min-width: 420px; /* prevent width jumps when switching modes */
max-height: min(
ha-wa-dialog {
--ha-dialog-width-md: 420px; /* prevent width jumps when switching modes */
--ha-dialog-max-height: min(
600px,
100% - 48px
); /* prevent scrolling on desktop */
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-md-dialog {
min-width: 100%;
min-height: auto;
max-height: calc(100% - 100px);
margin-bottom: 0;
--md-dialog-container-shape-start-start: 28px;
--md-dialog-container-shape-start-end: 28px;
ha-wa-dialog {
--ha-dialog-width-md: 100vw;
--ha-dialog-max-height: calc(100% - 100px);
}
}
@@ -7,7 +7,6 @@ export interface LightColorFavoriteDialogParams {
title: string;
initialColor?: LightColor;
submit?: (color?: LightColor) => void;
cancel?: () => void;
}
export const loadLightColorFavoriteDialog = () =>
@@ -18,7 +17,6 @@ export const showLightColorFavoriteDialog = (
dialogParams: LightColorFavoriteDialogParams
) =>
new Promise<LightColor | null>((resolve) => {
const origCancel = dialogParams.cancel;
const origSubmit = dialogParams.submit;
fireEvent(element, "show-dialog", {
@@ -26,12 +24,6 @@ export const showLightColorFavoriteDialog = (
dialogImport: loadLightColorFavoriteDialog,
dialogParams: {
...dialogParams,
cancel: () => {
resolve(null);
if (origCancel) {
origCancel();
}
},
submit: (color: LightColor) => {
resolve(color);
if (origSubmit) {
@@ -7,6 +7,7 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import "../../../../components/ha-button";
import "../../../../components/ha-control-button";
import type { HaSelectSelectEvent } from "../../../../components/ha-select";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item";
@@ -146,7 +147,7 @@ class MoreInfoSirenAdvancedControls extends LitElement {
`;
}
private _handleToneChange(ev: CustomEvent<{ value: string }>) {
private _handleToneChange(ev: HaSelectSelectEvent) {
this._tone = ev.detail.value;
}
@@ -5,7 +5,7 @@ import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { setDateValue } from "../../../data/date";
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@customElement("more-info-date")
class MoreInfoDate extends LitElement {
@@ -31,7 +31,7 @@ class MoreInfoDate extends LitElement {
`;
}
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
private _dateChanged(ev: ValueChangedEvent<string>): void {
if (ev.detail.value) {
setDateValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
}
@@ -6,7 +6,7 @@ import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { setDateTimeValue } from "../../../data/datetime";
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@customElement("more-info-datetime")
class MoreInfoDatetime extends LitElement {
@@ -45,7 +45,7 @@ class MoreInfoDatetime extends LitElement {
ev.stopPropagation();
}
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
private _timeChanged(ev: ValueChangedEvent<string>): void {
if (ev.detail.value) {
const dateObj = new Date(this.stateObj!.state);
const newTime = ev.detail.value.split(":").map(Number);
@@ -55,7 +55,7 @@ class MoreInfoDatetime extends LitElement {
}
}
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
private _dateChanged(ev: ValueChangedEvent<string>): void {
if (ev.detail.value) {
const dateObj = new Date(this.stateObj!.state);
const newDate = ev.detail.value.split("-").map(Number);
@@ -8,7 +8,7 @@ import {
setInputDateTimeValue,
stateToIsoDateString,
} from "../../../data/input_datetime";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@customElement("more-info-input_datetime")
class MoreInfoInputDatetime extends LitElement {
@@ -55,7 +55,7 @@ class MoreInfoInputDatetime extends LitElement {
ev.stopPropagation();
}
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
private _timeChanged(ev: ValueChangedEvent<string>): void {
setInputDateTimeValue(
this.hass!,
this.stateObj!.entity_id,
@@ -66,7 +66,7 @@ class MoreInfoInputDatetime extends LitElement {
);
}
private _dateChanged(ev: CustomEvent<{ value: string }>): void {
private _dateChanged(ev: ValueChangedEvent<string>): void {
setInputDateTimeValue(
this.hass!,
this.stateObj!.entity_id,
@@ -202,21 +202,18 @@ class MoreInfoMediaPlayer extends LitElement {
return nothing;
}
return html`<ha-dropdown>
return html`<ha-dropdown @wa-select=${this._handleSourceChange}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize(`ui.card.media_player.source`)}
.path=${mdiLoginVariant}
@wa-select=${this._handleSourceChange}
>
</ha-icon-button>
${this.stateObj.attributes.source_list!.map(
(source) =>
html`<ha-dropdown-item
.value=${source}
class=${source === this.stateObj?.attributes.source
? "selected"
: ""}
.selected=${source === this.stateObj?.attributes.source}
>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
@@ -251,9 +248,7 @@ class MoreInfoMediaPlayer extends LitElement {
(soundMode) =>
html`<ha-dropdown-item
.value=${soundMode}
class=${soundMode === this.stateObj?.attributes.sound_mode
? "selected"
: ""}
.selected=${soundMode === this.stateObj?.attributes.sound_mode}
>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
@@ -679,13 +674,6 @@ class MoreInfoMediaPlayer extends LitElement {
align-self: center;
width: 320px;
}
ha-dropdown-item.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);
}
`;
private _handleClick(e: MouseEvent): void {
@@ -1,6 +1,7 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { supportsFeature } from "../../../common/entity/supports-feature";
import type { HaSelectSelectEvent } from "../../../components/ha-select";
import "../../../components/ha-select";
import type { RemoteEntity } from "../../../data/remote";
import { REMOTE_SUPPORT_ACTIVITY } from "../../../data/remote";
@@ -43,7 +44,7 @@ class MoreInfoRemote extends LitElement {
`;
}
private _handleActivityChanged(ev: CustomEvent<{ value: string }>) {
private _handleActivityChanged(ev: HaSelectSelectEvent) {
const oldVal = this.stateObj!.attributes.current_activity;
const newVal = ev.detail.value;
@@ -5,7 +5,7 @@ import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
import { setTimeValue } from "../../../data/time";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@customElement("more-info-time")
class MoreInfoTime extends LitElement {
@@ -35,7 +35,7 @@ class MoreInfoTime extends LitElement {
ev.stopPropagation();
}
private _timeChanged(ev: CustomEvent<{ value: string }>): void {
private _timeChanged(ev: ValueChangedEvent<string>): void {
if (ev.detail.value) {
setTimeValue(this.hass!, this.stateObj!.entity_id, ev.detail.value);
}
@@ -14,6 +14,7 @@ import memoizeOne from "memoize-one";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/entity/ha-battery-icon";
import type { HaSelectSelectEvent } from "../../../components/ha-select";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-select";
@@ -285,7 +286,7 @@ class MoreInfoVacuum extends LitElement {
});
}
private _handleFanSpeedChanged(ev: CustomEvent<{ value: string }>) {
private _handleFanSpeedChanged(ev: HaSelectSelectEvent) {
const oldVal = this.stateObj!.attributes.fan_speed;
const newVal = ev.detail.value;
@@ -313,113 +313,119 @@ class MoreInfoWeather extends LitElement {
</div>
`
: nothing}
<div class="section">
${this.hass.localize("ui.card.weather.forecast")}:
</div>
${supportedForecasts?.length > 1
? html`<ha-tab-group @wa-tab-show=${this._handleForecastTypeChanged}>
${supportedForecasts.map(
(forecastType) =>
html`<ha-tab-group-tab
slot="nav"
.panel=${forecastType}
.active=${this._forecastType === forecastType}
${supportedForecasts?.length
? html`
<div class="section">
${this.hass.localize("ui.card.weather.forecast")}:
</div>
${supportedForecasts?.length > 1
? html`<ha-tab-group
@wa-tab-show=${this._handleForecastTypeChanged}
>
${this.hass!.localize(`ui.card.weather.${forecastType}`)}
</ha-tab-group-tab>`
)}
</ha-tab-group>`
: nothing}
<div class="forecast">
${forecast?.length
? this._groupForecastByDay(forecast).map((dayForecast) => {
const showDayHeader = hourly || dayNight;
return html`
<div class="forecast-day">
${showDayHeader
? html`<div class="forecast-day-header">
${formatDateWeekdayShort(
new Date(dayForecast[0].datetime),
this.hass!.locale,
this.hass!.config
${supportedForecasts.map(
(forecastType) =>
html`<ha-tab-group-tab
slot="nav"
.panel=${forecastType}
.active=${this._forecastType === forecastType}
>
${this.hass!.localize(
`ui.card.weather.${forecastType}`
)}
</div>`
: nothing}
<div class="forecast-day-content">
${dayForecast.map((item) =>
this._showValue(item.templow) ||
this._showValue(item.temperature)
? html`
<div class="forecast-item">
<div
class="forecast-item-label ${showDayHeader
? ""
: "no-header"}"
>
${hourly
? formatTime(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)
: dayNight
? html`<div class="daynight">
${item.is_daytime !== false
? this.hass!.localize(
"ui.card.weather.day"
)
: this.hass!.localize(
"ui.card.weather.night"
</ha-tab-group-tab>`
)}
</ha-tab-group>`
: nothing}
<div class="forecast">
${forecast?.length
? this._groupForecastByDay(forecast).map((dayForecast) => {
const showDayHeader = hourly || dayNight;
return html`
<div class="forecast-day">
${showDayHeader
? html`<div class="forecast-day-header">
${formatDateWeekdayShort(
new Date(dayForecast[0].datetime),
this.hass!.locale,
this.hass!.config
)}
</div>`
: nothing}
<div class="forecast-day-content">
${dayForecast.map((item) =>
this._showValue(item.templow) ||
this._showValue(item.temperature)
? html`
<div class="forecast-item">
<div
class="forecast-item-label ${showDayHeader
? ""
: "no-header"}"
>
${hourly
? formatTime(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)
: dayNight
? html`<div class="daynight">
${item.is_daytime !== false
? this.hass!.localize(
"ui.card.weather.day"
)
: this.hass!.localize(
"ui.card.weather.night"
)}
</div>`
: formatDateWeekdayShort(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
</div>`
: formatDateWeekdayShort(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
</div>
${this._showValue(item.condition)
? html`
<div class="forecast-image-icon">
${getWeatherStateIcon(
item.condition!,
this,
!(
item.is_daytime ||
item.is_daytime === undefined
)
)}
</div>
`
: nothing}
<div class="temp">
${this._showValue(item.temperature)
? html`${formatNumber(
item.temperature,
this.hass!.locale
)}°`
: "—"}
</div>
<div class="templow">
${this._showValue(item.templow)
? html`${formatNumber(
item.templow!,
this.hass!.locale
)}°`
: nothing}
</div>
</div>
`
: nothing
)}
</div>
</div>
`;
})
: html`<ha-spinner size="medium"></ha-spinner>`}
</div>
${this._showValue(item.condition)
? html`
<div class="forecast-image-icon">
${getWeatherStateIcon(
item.condition!,
this,
!(
item.is_daytime ||
item.is_daytime === undefined
)
)}
</div>
`
: nothing}
<div class="temp">
${this._showValue(item.temperature)
? html`${formatNumber(
item.temperature,
this.hass!.locale
)}°`
: "—"}
</div>
<div class="templow">
${this._showValue(item.templow)
? html`${formatNumber(
item.templow!,
this.hass!.locale
)}°`
: nothing}
</div>
</div>
`
: nothing
)}
</div>
</div>
`;
})
: html`<ha-spinner size="medium"></ha-spinner>`}
</div>
`
: nothing}
${this.stateObj.attributes.attribution
? html`
<div class="attribution">
+84 -20
View File
@@ -47,6 +47,7 @@ import {
type ActionCommandComboBoxItem,
type NavigationComboBoxItem,
} from "../../data/quick_bar";
import type { RelatedResult } from "../../data/search";
import {
multiTermSortedSearch,
type FuseWeightedKey,
@@ -75,6 +76,8 @@ export class QuickBar extends LitElement {
@state() private _opened = false;
@state() private _relatedResult?: RelatedResult;
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
private get _showEntityId() {
@@ -100,6 +103,9 @@ export class QuickBar extends LitElement {
this._initialize();
this._selectedSection = params.mode;
this._showHint = params.showHint ?? false;
this._relatedResult = params.contextItem ? params.related : undefined;
this._open = true;
}
@@ -417,6 +423,7 @@ export class QuickBar extends LitElement {
this._selectedSection = section as QuickBarSection | undefined;
return this._getItemsMemoized(
this._configEntryLookup,
this._relatedResult,
searchString,
this._selectedSection
);
@@ -425,10 +432,12 @@ export class QuickBar extends LitElement {
private _getItemsMemoized = memoizeOne(
(
configEntryLookup: Record<string, ConfigEntry>,
relatedResult: RelatedResult | undefined,
filter?: string,
section?: QuickBarSection
) => {
const items: (string | PickerComboBoxItem)[] = [];
const relatedIdSets = this._getRelatedIdSets(relatedResult);
if (!section || section === "navigate") {
let navigateItems = this._generateNavigationCommandsMemoized(
@@ -477,17 +486,29 @@ export class QuickBar extends LitElement {
}
if (!section || section === "entity") {
let entityItems = this._getEntitiesMemoized(this.hass).sort(
this._sortBySortingLabel
);
let entityItems = this._getEntitiesMemoized(this.hass);
// Mark related items
if (relatedIdSets.entities.size > 0) {
entityItems = entityItems.map((item) => ({
...item,
isRelated: relatedIdSets.entities.has(
(item as EntityComboBoxItem).stateObj?.entity_id || ""
),
}));
}
if (filter) {
entityItems = this._filterGroup(
"entity",
entityItems,
filter,
entityComboBoxKeys
) as EntityComboBoxItem[];
entityItems = this._sortRelatedFirst(
this._filterGroup(
"entity",
entityItems,
filter,
entityComboBoxKeys
) as EntityComboBoxItem[]
);
} else {
entityItems = this._sortRelatedByLabel(entityItems);
}
if (!section && entityItems.length) {
@@ -504,15 +525,25 @@ export class QuickBar extends LitElement {
let deviceItems = this._getDevicesMemoized(
this.hass,
configEntryLookup
).sort(this._sortBySortingLabel);
);
// Mark related items
if (relatedIdSets.devices.size > 0) {
deviceItems = deviceItems.map((item) => {
const deviceId = item.id.split(SEPARATOR)[1];
return {
...item,
isRelated: relatedIdSets.devices.has(deviceId || ""),
};
});
}
if (filter) {
deviceItems = this._filterGroup(
"device",
deviceItems,
filter,
deviceComboBoxKeys
deviceItems = this._sortRelatedFirst(
this._filterGroup("device", deviceItems, filter, deviceComboBoxKeys)
);
} else {
deviceItems = this._sortRelatedByLabel(deviceItems);
}
if (!section && deviceItems.length) {
@@ -528,13 +559,23 @@ export class QuickBar extends LitElement {
if (this.hass.user?.is_admin && (!section || section === "area")) {
let areaItems = this._getAreasMemoized(this.hass);
// Mark related items
if (relatedIdSets.areas.size > 0) {
areaItems = areaItems.map((item) => {
const areaId = item.id.split(SEPARATOR)[1];
return {
...item,
isRelated: relatedIdSets.areas.has(areaId || ""),
};
});
}
if (filter) {
areaItems = this._filterGroup(
"area",
areaItems,
filter,
areaComboBoxKeys
areaItems = this._sortRelatedFirst(
this._filterGroup("area", areaItems, filter, areaComboBoxKeys)
);
} else {
areaItems = this._sortRelatedByLabel(areaItems);
}
if (!section && areaItems.length) {
@@ -551,6 +592,12 @@ export class QuickBar extends LitElement {
}
);
private _getRelatedIdSets = memoizeOne((related?: RelatedResult) => ({
entities: new Set(related?.entity || []),
devices: new Set(related?.device || []),
areas: new Set(related?.area || []),
}));
private _getEntitiesMemoized = memoizeOne((hass: HomeAssistant) =>
getEntities(
hass,
@@ -654,6 +701,23 @@ export class QuickBar extends LitElement {
this.hass.locale.language
);
private _sortRelatedByLabel = (items: PickerComboBoxItem[]) =>
[...items].sort((a, b) => {
if (a.isRelated && !b.isRelated) return -1;
if (!a.isRelated && b.isRelated) return 1;
return this._sortBySortingLabel(a, b);
});
private _sortRelatedFirst = (items: PickerComboBoxItem[]) =>
[...items].sort((a, b) => {
const aRelated = Boolean(a.isRelated);
const bRelated = Boolean(b.isRelated);
if (aRelated === bRelated) {
return 0;
}
return aRelated ? -1 : 1;
});
// #endregion data
// #region interaction
@@ -1,4 +1,5 @@
import { fireEvent } from "../../common/dom/fire_event";
import type { ItemType, RelatedResult } from "../../data/search";
import { closeDialog } from "../make-dialog-manager";
export type QuickBarSection =
@@ -8,10 +9,17 @@ export type QuickBarSection =
| "navigate"
| "command";
export interface QuickBarContextItem {
itemType: ItemType;
itemId: string;
}
export interface QuickBarParams {
entityFilter?: string;
mode?: QuickBarSection;
showHint?: boolean;
contextItem?: QuickBarContextItem;
related?: RelatedResult;
}
export const loadQuickBar = () => import("./ha-quick-bar");
+2 -2
View File
@@ -30,7 +30,7 @@ import {
getPanelIconPath,
getPanelTitle,
} from "../../data/panel";
import type { HomeAssistant } from "../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import { showConfirmationDialog } from "../generic/show-dialog-box";
@customElement("dialog-edit-sidebar")
@@ -206,7 +206,7 @@ class DialogEditSidebar extends LitElement {
`;
}
private _changed(ev: CustomEvent<{ value: DisplayValue }>): void {
private _changed(ev: ValueChangedEvent<DisplayValue>): void {
const { order = [], hidden = [] } = ev.detail.value;
this._order = [...order];
this._hidden = [...hidden];
@@ -129,11 +129,12 @@ export class CloudStepIntro extends LitElement {
}
.feature .logos {
margin-bottom: 16px;
display: flex;
gap: var(--ha-space-4);
}
.feature .logos > * {
width: 40px;
height: 40px;
margin: 0 4px;
}
.round-icon {
border-radius: var(--ha-border-radius-circle);
@@ -196,7 +196,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
(lang) =>
html`<ha-dropdown-item
.value=${lang.id}
class=${this._language === lang.id ? "selected" : ""}
.selected=${this._language === lang.id}
>
${lang.primary}
</ha-dropdown-item>`
@@ -407,13 +407,6 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
margin-inline-end: 12px;
margin-inline-start: initial;
}
ha-dropdown-item.selected {
border: 1px solid var(--primary-color);
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);
}
`,
];
}
@@ -4,6 +4,7 @@ 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 type { HaSelectSelectEvent } from "../../components/ha-select";
import {
computeDeviceName,
computeDeviceNameDisplay,
@@ -231,7 +232,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
this._deviceName = ev.target.value;
}
private async _wakeWordPicked(ev: CustomEvent<{ value: string }>) {
private async _wakeWordPicked(ev: HaSelectSelectEvent) {
const option = ev.detail.value;
if (this.assistConfiguration) {
this.assistConfiguration.active_wake_words = [option];
@@ -239,7 +240,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
await setWakeWords(this.hass, this.assistEntityId!, [option]);
}
private _pipelinePicked(ev: CustomEvent<{ value: string }>) {
private _pipelinePicked(ev: HaSelectSelectEvent) {
const stateObj = this.hass!.states[
this.assistConfiguration!.pipeline_entity_id
] as InputSelectEntity;
+2 -2
View File
@@ -3,8 +3,8 @@ import type { Panels } from "../types";
export const demoPanels: Panels = {
lovelace: {
component_name: "lovelace",
icon: null,
title: null,
icon: "mdi:view-dashboard",
title: "demo",
config: { mode: "storage" },
url_path: "lovelace",
},
+15 -2
View File
@@ -52,6 +52,7 @@ export interface MockHomeAssistant extends HomeAssistant {
mockEvent(event);
mockTheme(theme: Record<string, string> | null);
formatEntityState(stateObj: HassEntity, state?: string): string;
formatEntityStateToParts(stateObj: HassEntity, state?: string): ValuePart[];
formatEntityAttributeValue(
stateObj: HassEntity,
attribute: string,
@@ -117,6 +118,7 @@ export const provideHass = (
async function updateFormatFunctions() {
const {
formatEntityState,
formatEntityStateToParts,
formatEntityAttributeName,
formatEntityAttributeValue,
formatEntityAttributeValueToParts,
@@ -133,6 +135,7 @@ export const provideHass = (
);
hass().updateHass({
formatEntityState,
formatEntityStateToParts,
formatEntityAttributeName,
formatEntityAttributeValue,
formatEntityAttributeValueToParts,
@@ -256,6 +259,10 @@ export const provideHass = (
darkMode: false,
theme: "default",
},
selectedTheme: {
theme: "default",
dark: false,
},
panels: demoPanels,
services: demoServices,
user: {
@@ -348,7 +355,7 @@ export const provideHass = (
mockTheme(theme) {
invalidateThemeCache();
hass().updateHass({
selectedTheme: { theme: theme ? "mock" : "default" },
selectedTheme: { theme: theme ? "mock" : "default", dark: false },
themes: {
...hass().themes,
themes: {
@@ -361,7 +368,7 @@ export const provideHass = (
document.documentElement,
themes,
selectedTheme!.theme,
undefined,
{ dark: false },
true
);
},
@@ -371,6 +378,12 @@ export const provideHass = (
floors: {},
formatEntityState: (stateObj, state) =>
(state !== null ? state : stateObj.state) ?? "",
formatEntityStateToParts: (stateObj, state) => [
{
type: "value",
value: (state !== null ? state : stateObj.state) ?? "",
},
],
formatEntityAttributeName: (_stateObj, attribute) => attribute,
formatEntityAttributeValue: (stateObj, attribute, value) =>
value !== null ? value : (stateObj.attributes[attribute] ?? ""),
+3 -1
View File
@@ -74,6 +74,8 @@ export class HAFullCalendar extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ attribute: "add-fab", type: Boolean }) public addFab = false;
@property({ attribute: false }) public events: CalendarEvent[] = [];
@property({ attribute: false }) public calendars: CalendarData[] = [];
@@ -208,7 +210,7 @@ export class HAFullCalendar extends LitElement {
: ""}
<div id="calendar"></div>
${this._hasMutableCalendars
${this.addFab && this._hasMutableCalendars
? html`<ha-fab
slot="fab"
.label=${this.hass.localize("ui.components.calendar.event.add")}
+5
View File
@@ -193,6 +193,7 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
</ha-list-item>`
: nothing}
<ha-full-calendar
add-fab
.events=${this._events}
.calendars=${this._calendars}
.narrow=${this.narrow}
@@ -330,6 +331,8 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
ha-dropdown-item {
padding-left: 32px;
padding-inline-start: 32px;
padding-inline-end: initial;
--icon-primary-color: var(--ha-color-fill-neutral-loud-resting);
}
@@ -339,6 +342,8 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
:host([mobile]) {
padding-left: unset;
padding-inline-start: unset;
padding-inline-end: initial;
}
.loading {
display: flex;
@@ -11,6 +11,7 @@ import "../../components/chips/ha-chip-set";
import "../../components/chips/ha-filter-chip";
import "../../components/ha-date-input";
import "../../components/ha-select";
import type { HaSelectSelectEvent } from "../../components/ha-select";
import "../../components/ha-textfield";
import type { HomeAssistant } from "../../types";
import type {
@@ -311,8 +312,8 @@ export class RecurrenceRuleEditor extends LitElement {
this._interval = (e.target! as any).value;
}
private _onRepeatSelected(e: CustomEvent<{ value: string }>) {
this._freq = e.detail.value as RepeatFrequency;
private _onRepeatSelected(e: HaSelectSelectEvent<RepeatFrequency>) {
this._freq = e.detail.value;
if (this._freq === "yearly") {
this._interval = 1;
@@ -323,7 +324,9 @@ export class RecurrenceRuleEditor extends LitElement {
}
}
private _onMonthlyDetailSelected(e: CustomEvent<{ value: string }>) {
private _onMonthlyDetailSelected(
e: HaSelectSelectEvent<MonthlyRepeatItem["value"]>
) {
const selectedItem = this._monthlyRepeatItems.find(
(item) => item.value === e.detail.value
);
@@ -346,8 +349,8 @@ export class RecurrenceRuleEditor extends LitElement {
this.requestUpdate("_weekday");
}
private _onEndSelected(e: CustomEvent<{ value: string }>) {
const end = e.detail.value as RepeatEnd;
private _onEndSelected(e: HaSelectSelectEvent<RepeatEnd>) {
const end = e.detail.value;
if (end === this._end) {
return;
}
@@ -5,6 +5,7 @@ import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-card";
import "../../../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
import type {
HassioAddonDetails,
HassioAddonSetOptionParams,
@@ -118,14 +119,14 @@ class SupervisorAppAudio extends LitElement {
}
}
private _setInputDevice(ev: CustomEvent<{ value: string }>): void {
private _setInputDevice(ev: HaSelectSelectEvent): void {
const device = ev.detail.value;
this._selectedInput = device;
this._selectedInput = device ?? null;
}
private _setOutputDevice(ev: CustomEvent<{ value: string }>): void {
private _setOutputDevice(ev: HaSelectSelectEvent): void {
const device = ev.detail.value;
this._selectedOutput = device;
this._selectedOutput = device ?? null;
}
private async _addonChanged(): Promise<void> {
@@ -63,6 +63,7 @@ class SupervisorAppDocumentationDashboard extends LitElement {
margin: auto;
padding: var(--ha-space-2);
max-width: 1024px;
direction: ltr;
}
ha-markdown {
padding: var(--ha-space-4);
@@ -7,6 +7,7 @@ import type { LocalizeFunc } from "../../../../../common/translations/localize";
import { CONDITION_ICONS } from "../../../../../components/ha-condition-icon";
import "../../../../../components/ha-dropdown-item";
import "../../../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
import {
DYNAMIC_PREFIX,
getValueFromDynamic,
@@ -101,7 +102,7 @@ export class HaConditionAction
}
return html`
<ha-dropdown-item .value=${opt} class=${selected ? "selected" : ""}>
<ha-dropdown-item .value=${opt} .selected=${selected}>
<ha-condition-icon
.hass=${this.hass}
slot="icon"
@@ -200,7 +201,7 @@ export class HaConditionAction
});
}
private _typeChanged(ev: CustomEvent<{ value: string }>) {
private _typeChanged(ev: HaSelectSelectEvent) {
const type = ev.detail.value;
if (!type) {
@@ -8,7 +8,7 @@ import "../../../../../components/ha-duration-input";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-textfield";
import type { WaitForTriggerAction } from "../../../../../data/script";
import type { HomeAssistant } from "../../../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
import "../../trigger/ha-automation-trigger";
import type { ActionElement } from "../ha-automation-action-row";
import { handleChangeEvent } from "../ha-automation-action-row";
@@ -78,7 +78,7 @@ export class HaWaitForTriggerAction
`;
}
private _timeoutChanged(ev: CustomEvent<{ value: TimeChangedEvent }>): void {
private _timeoutChanged(ev: ValueChangedEvent<TimeChangedEvent>): void {
ev.stopPropagation();
const value = ev.detail.value;
fireEvent(this, "value-changed", {
@@ -1,3 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
@@ -42,7 +43,6 @@ import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-button-prev";
import "../../../components/ha-icon-next";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
@@ -114,7 +114,7 @@ import {
} from "../../../data/trigger";
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import "./add-automation-element/ha-automation-add-from-target";
@@ -657,10 +657,7 @@ class DialogAddAutomationElement
.path=${mdiPlus}
></ha-svg-icon>
</ha-md-list-item>
<ha-md-divider
role="separator"
tabindex="-1"
></ha-md-divider>`
<wa-divider></wa-divider>`
: nothing}
${collections.map(
(collection, index) => html`
@@ -1752,7 +1749,7 @@ class DialogAddAutomationElement
this.closeDialog();
}
private _selected(ev: CustomEvent<{ value: string }>) {
private _selected(ev: ValueChangedEvent<string>) {
let target: HassServiceTarget | undefined;
if (
this._tab === "targets" &&
@@ -1766,7 +1763,7 @@ class DialogAddAutomationElement
}
private _handleTargetSelected = (
ev: CustomEvent<{ value: SingleHassServiceTarget }>
ev: ValueChangedEvent<SingleHassServiceTarget>
) => {
this._targetItems = undefined;
this._loadItemsError = false;
@@ -2177,8 +2174,8 @@ class DialogAddAutomationElement
width: var(--ha-space-6);
}
ha-md-list-item.paste {
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
wa-divider {
--spacing: 0;
}
ha-svg-icon.plus {
@@ -77,7 +77,12 @@ import "../../../layouts/hass-subpage";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
import { haStyle } from "../../../resources/styles";
import type { Entries, HomeAssistant, Route } from "../../../types";
import type {
Entries,
HomeAssistant,
Route,
ValueChangedEvent,
} from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
@@ -753,7 +758,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
}
private _valueChanged(ev: CustomEvent<{ value: AutomationConfig }>) {
private _valueChanged(ev: ValueChangedEvent<AutomationConfig>) {
ev.stopPropagation();
if (this._config) {
@@ -47,6 +47,10 @@ import type {
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-dropdown";
import type {
HaDropdown,
HaDropdownSelectEvent,
} from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-filter-blueprints";
@@ -57,10 +61,6 @@ import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-menu";
import type { HaMdMenu } from "../../../components/ha-md-menu";
import "../../../components/ha-md-menu-item";
import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
@@ -84,9 +84,9 @@ import { fullEntitiesContext } from "../../../data/context";
import type { DataTableFilters } from "../../../data/data_table_filters";
import {
deserializeFilters,
serializeFilters,
isFilterUsed,
isRelatedItemsFilterUsed,
serializeFilters,
} from "../../../data/data_table_filters";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type {
@@ -111,16 +111,16 @@ import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route, ServiceCallResponse } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity";
import {
getEntityIdHiddenTableColumn,
getAreaTableColumn,
getCategoryTableColumn,
getLabelsTableColumn,
getTriggeredAtTableColumn,
} from "../common/data-table-columns";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import {
getAreaTableColumn,
getCategoryTableColumn,
getEntityIdHiddenTableColumn,
getLabelsTableColumn,
getTriggeredAtTableColumn,
} from "../common/data-table-columns";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import {
@@ -129,7 +129,6 @@ import {
} from "../voice-assistants/expose/assistants-table-column";
import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants";
import { showNewAutomationDialog } from "./show-dialog-new-automation";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
type AutomationItem = AutomationEntity & {
name: string;
@@ -223,7 +222,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
})
private _activeHiddenColumns?: string[];
@query("#overflow-menu") private _overflowMenu!: HaMdMenu;
@query("#overflow-menu") private _overflowMenu!: HaDropdown;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
@@ -233,6 +232,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
return getAvailableAssistants(this.cloudStatus, this.hass);
}
private _openingOverflow = false;
private _automations = memoizeOne(
(
automations: AutomationEntity[],
@@ -371,16 +372,27 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
);
private _showOverflowMenu = (ev) => {
if (
this._overflowMenu.open &&
ev.target === this._overflowMenu.anchorElement
) {
this._overflowMenu.close();
if (this._overflowMenu.anchorElement === ev.target) {
this._overflowMenu.anchorElement = undefined;
return;
}
this._overflowAutomation = ev.target.automation;
this._openingOverflow = true;
this._overflowMenu.anchorElement = ev.target;
this._overflowMenu.show();
this._overflowAutomation = ev.target.automation;
this._overflowMenu.open = true;
};
private _overflowMenuOpened = () => {
this._openingOverflow = false;
};
private _overflowMenuClosed = () => {
// changing the anchorElement triggers a close event, ignore it
if (this._openingOverflow) {
return;
}
this._overflowMenu.anchorElement = undefined;
};
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
@@ -697,74 +709,58 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage-data-table>
<ha-md-menu id="overflow-menu" positioning="fixed">
<ha-md-menu-item .clickAction=${this._showInfo}>
<ha-svg-icon
.path=${mdiInformationOutline}
slot="start"
></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
</div>
</ha-md-menu-item>
<ha-dropdown
id="overflow-menu"
@wa-select=${this._handleOverflowAction}
@wa-after-show=${this._overflowMenuOpened}
@wa-after-hide=${this._overflowMenuClosed}
>
<ha-dropdown-item value="show_info">
<ha-svg-icon .path=${mdiInformationOutline} slot="icon"></ha-svg-icon>
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
</ha-dropdown-item>
<ha-md-menu-item .clickAction=${this._showSettings}>
<ha-svg-icon .path=${mdiCog} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.show_settings"
)}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._editCategory}>
<ha-svg-icon .path=${mdiTag} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}`
)}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._runActions}>
<ha-svg-icon .path=${mdiPlay} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.editor.run")}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._showTrace}>
<ha-svg-icon .path=${mdiTransitConnection} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.editor.show_trace"
)}
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._duplicate}>
<ha-svg-icon .path=${mdiContentDuplicate} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.picker.duplicate")}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._toggle}>
<ha-dropdown-item value="show_settings">
<ha-svg-icon .path=${mdiCog} slot="icon"></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.picker.show_settings"
)}
</ha-dropdown-item>
<ha-dropdown-item value="edit_category">
<ha-svg-icon .path=${mdiTag} slot="icon"></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}`
)}
</ha-dropdown-item>
<ha-dropdown-item value="run_actions">
<ha-svg-icon .path=${mdiPlay} slot="icon"></ha-svg-icon>
${this.hass.localize("ui.panel.config.automation.editor.run")}
</ha-dropdown-item>
<ha-dropdown-item value="show_trace">
<ha-svg-icon .path=${mdiTransitConnection} slot="icon"></ha-svg-icon>
${this.hass.localize("ui.panel.config.automation.editor.show_trace")}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="duplicate">
<ha-svg-icon .path=${mdiContentDuplicate} slot="icon"></ha-svg-icon>
${this.hass.localize("ui.panel.config.automation.picker.duplicate")}
</ha-dropdown-item>
<ha-dropdown-item value="toggle">
<ha-svg-icon
.path=${this._overflowAutomation?.state === "off"
? mdiToggleSwitch
: mdiToggleSwitchOffOutline}
slot="start"
slot="icon"
></ha-svg-icon>
<div slot="headline">
${this._overflowAutomation?.state === "off"
? this.hass.localize("ui.panel.config.automation.editor.enable")
: this.hass.localize("ui.panel.config.automation.editor.disable")}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._deleteConfirm} class="warning">
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.picker.delete")}
</div>
</ha-md-menu-item>
</ha-md-menu>
${this._overflowAutomation?.state === "off"
? this.hass.localize("ui.panel.config.automation.editor.enable")
: this.hass.localize("ui.panel.config.automation.editor.disable")}
</ha-dropdown-item>
<ha-dropdown-item value="delete" variant="danger">
<ha-svg-icon .path=${mdiDelete} slot="icon"></ha-svg-icon>
${this.hass.localize("ui.panel.config.automation.picker.delete")}
</ha-dropdown-item>
</ha-dropdown>
`;
}
@@ -901,33 +897,59 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this._applyFilters();
}
private _showInfo = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
fireEvent(this, "hass-more-info", { entityId: automation.entity_id });
private _handleOverflowAction = (ev: HaDropdownSelectEvent) => {
const action = ev.detail.item.value;
if (!action || !this._overflowAutomation) {
return;
}
switch (action) {
case "show_info":
this._showInfo(this._overflowAutomation);
break;
case "show_settings":
this._showSettings(this._overflowAutomation);
break;
case "edit_category":
this._editCategory(this._overflowAutomation);
break;
case "run_actions":
this._runActions(this._overflowAutomation);
break;
case "show_trace":
this._showTrace(this._overflowAutomation);
break;
case "toggle":
this._toggle(this._overflowAutomation);
break;
case "delete":
this._deleteConfirm(this._overflowAutomation);
break;
case "duplicate":
this._duplicate(this._overflowAutomation);
break;
}
};
private _showSettings = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _showInfo = (automation: AutomationItem) => {
fireEvent(this, "hass-more-info", {
entityId: automation.entity_id,
});
};
private _showSettings = (automation: AutomationItem) => {
fireEvent(this, "hass-more-info", {
entityId: automation.entity_id,
view: "settings",
});
};
private _runActions = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _runActions = (automation: AutomationItem) => {
triggerAutomationActions(this.hass, automation.entity_id);
};
private _editCategory = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _editCategory = (automation: AutomationItem) => {
const entityReg = this._entityReg.find(
(reg) => reg.entity_id === automation.entity_id
);
@@ -948,10 +970,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
});
};
private _showTrace = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _showTrace = (automation: AutomationItem) => {
if (!automation.attributes.id) {
showAlertDialog(this, {
text: this.hass.localize(
@@ -965,20 +984,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
);
};
private _toggle = async (item: HaMdMenuItem): Promise<void> => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _toggle = async (automation: AutomationItem): Promise<void> => {
const service = automation.state === "off" ? "turn_on" : "turn_off";
await this.hass.callService("automation", service, {
entity_id: automation.entity_id,
});
};
private _deleteConfirm = async (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _deleteConfirm = async (automation: AutomationItem) => {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.picker.delete_confirm_title"
@@ -994,9 +1007,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
});
};
private async _delete(automation) {
private async _delete(automation: AutomationItem) {
try {
await deleteAutomation(this.hass, automation.attributes.id);
await deleteAutomation(this.hass, automation.attributes.id!);
this._selected = this._selected.filter(
(entityId) => entityId !== automation.entity_id
);
@@ -1015,14 +1028,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
}
}
private _duplicate = async (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _duplicate = async (automation: AutomationItem) => {
try {
const config = await fetchAutomationFileConfig(
this.hass,
automation.attributes.id
automation.attributes.id!
);
duplicateAutomation(config);
} catch (err: any) {
@@ -54,7 +54,7 @@ import {
import { configEntriesContext } from "../../../data/context";
import { getActionType, type Action } from "../../../data/script";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
import "./action/ha-automation-action";
@@ -383,7 +383,7 @@ export class HaManualAutomationEditor extends SubscribeMixin(LitElement) {
this._sidebarElement?.focus();
}
private _sidebarConfigChanged(ev: CustomEvent<{ value: SidebarConfig }>) {
private _sidebarConfigChanged(ev: ValueChangedEvent<SidebarConfig>) {
ev.stopPropagation();
if (!this._sidebarConfig) {
return;
@@ -438,7 +438,6 @@ export class HaManualAutomationEditor extends SubscribeMixin(LitElement) {
}
private _saveAutomation() {
this.triggerCloseSidebar();
fireEvent(this, "save-automation");
}
-6
View File
@@ -40,9 +40,6 @@ export const rowStyles = css`
.warning ul {
margin: 4px 0;
}
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
}
ha-tooltip {
cursor: default;
}
@@ -272,7 +269,4 @@ export const overflowStyles = css`
display: none;
}
}
ha-md-menu-item {
--mdc-icon-size: 24px;
}
`;
@@ -4,6 +4,7 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../../common/string/compare";
import "../../../../../components/ha-select";
import type { HaSelectSelectEvent } from "../../../../../components/ha-select";
import type { TagTrigger } from "../../../../../data/automation";
import type { Tag } from "../../../../../data/tag";
import { fetchTags } from "../../../../../data/tag";
@@ -60,7 +61,7 @@ export class HaTagTrigger extends LitElement implements TriggerElement {
);
}
private _tagChanged(ev: CustomEvent<{ value: string }>) {
private _tagChanged(ev: HaSelectSelectEvent) {
if (
!ev.detail.value ||
!this._tags ||
@@ -28,7 +28,7 @@ import {
sortWeekdays,
} from "../../../../../data/backup";
import type { SupervisorUpdateConfig } from "../../../../../data/supervisor/update";
import type { HomeAssistant } from "../../../../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
import { documentationUrl } from "../../../../../util/documentation-url";
import "./ha-backup-config-retention";
@@ -405,7 +405,7 @@ class HaBackupConfigSchedule extends LitElement {
});
}
private _retentionChanged(ev: CustomEvent<{ value: Retention }>) {
private _retentionChanged(ev: ValueChangedEvent<Retention>) {
ev.stopPropagation();
const retention = ev.detail.value;
@@ -1,17 +1,16 @@
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-password-field";
@@ -86,14 +85,12 @@ const RECOMMENDED_CONFIG: BackupConfig = {
class DialogBackupOnboarding extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false;
@state() private _open = false;
@state() private _step?: Step;
@state() private _params?: BackupOnboardingDialogParams;
@query("ha-md-dialog") private _dialog!: HaMdDialog;
@state() private _config?: BackupConfig;
public showDialog(params: BackupOnboardingDialogParams): void {
@@ -115,21 +112,23 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
};
}
this._opened = true;
this._open = true;
}
public closeDialog() {
if (this._params!.cancel) {
this._params!.cancel();
this._open = false;
return true;
}
private _dialogClosed() {
if (this._params?.cancel) {
this._params.cancel();
}
if (this._opened) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._opened = false;
this._step = undefined;
this._config = undefined;
this._params = undefined;
return true;
this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private get _firstStep(): Step {
@@ -168,7 +167,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
try {
await this._save(true);
this._params?.submit!(true);
this._dialog.close();
this.closeDialog();
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
@@ -202,7 +201,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
}
protected render() {
if (!this._opened || !this._params || !this._step) {
if (!this._params || !this._step) {
return nothing;
}
@@ -210,33 +209,37 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
const isFirstStep = this._step === this._firstStep;
return html`
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
<ha-dialog-header slot="headline">
${isFirstStep
? html`
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
`
: html`
<ha-icon-button-prev
slot="navigationIcon"
@click=${this._previousStep}
></ha-icon-button-prev>
`}
<span slot="title">${this._stepTitle}</span>
</ha-dialog-header>
<div slot="content">${this._renderStepContent()}</div>
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this._stepTitle}
width="medium"
prevent-scrim-close
@closed=${this._dialogClosed}
>
${isFirstStep
? html`
<ha-icon-button
slot="headerNavigationIcon"
data-dialog="close"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
`
: html`
<ha-icon-button-prev
slot="headerNavigationIcon"
@click=${this._previousStep}
></ha-icon-button-prev>
`}
<div>${this._renderStepContent()}</div>
${!FULL_DIALOG_STEPS.has(this._step)
? html`
<div slot="actions">
<ha-dialog-footer slot="footer">
${isLastStep
? html`
<ha-button
slot="primaryAction"
@click=${this._done}
.disabled=${!this._isStepValid()}
>
@@ -247,16 +250,17 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
`
: html`
<ha-button
slot="primaryAction"
@click=${this._nextStep}
.disabled=${!this._isStepValid()}
>
${this.hass.localize("ui.common.next")}
</ha-button>
`}
</div>
</ha-dialog-footer>
`
: nothing}
</ha-md-dialog>
</ha-wa-dialog>
`;
}
@@ -540,11 +544,9 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
haStyle,
haStyleDialog,
css`
ha-md-dialog {
width: 90vw;
max-width: 560px;
--dialog-content-padding: 8px 24px;
max-height: min(605px, 100% - 48px);
ha-wa-dialog {
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
--ha-dialog-max-height: min(605px, 100% - 48px);
}
ha-md-list {
background: none;
@@ -557,14 +559,6 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
margin-left: -24px;
margin-right: -24px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-md-dialog {
max-width: none;
}
div[slot="content"] {
margin-top: 0;
}
}
p {
margin-top: 0;
}
@@ -4,7 +4,8 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
@@ -36,13 +37,20 @@ class LocalBackupLocationDialog extends LitElement {
@state() private _error?: string;
@state() private _open = false;
public async showDialog(
dialogParams: LocalBackupLocationDialogParams
): Promise<void> {
this._dialogParams = dialogParams;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._data = undefined;
this._error = undefined;
this._waiting = undefined;
@@ -55,17 +63,13 @@ class LocalBackupLocationDialog extends LitElement {
return nothing;
}
return html`
<ha-dialog
open
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
`ui.panel.config.backup.dialogs.local_backup_location.title`
)
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this.hass.localize(
`ui.panel.config.backup.dialogs.local_backup_location.title`
)}
@closed=${this.closeDialog}
@closed=${this._dialogClosed}
>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
@@ -77,34 +81,35 @@ class LocalBackupLocationDialog extends LitElement {
)}
</p>
<ha-form
autofocus
.hass=${this.hass}
.data=${this._data}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
dialogInitialFocus
></ha-form>
<ha-alert alert-type="info">
${this.hass.localize(
`ui.panel.config.backup.dialogs.local_backup_location.note`
)}
</ha-alert>
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
dialogInitialFocus
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
.disabled=${this._waiting || !this._data}
slot="primaryAction"
@click=${this._changeMount}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
.disabled=${this._waiting || !this._data}
slot="primaryAction"
@click=${this._changeMount}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
@@ -143,9 +148,6 @@ class LocalBackupLocationDialog extends LitElement {
haStyle,
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
}
ha-form {
display: block;
margin-bottom: 16px;
@@ -1,20 +1,17 @@
import { mdiClose } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-spinner";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-password-field";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import "../../../../components/ha-alert";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-wa-dialog";
import type { RestoreBackupParams } from "../../../../data/backup";
import {
fetchBackupConfig,
@@ -54,6 +51,8 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
@state() private _formData?: FormData;
@state() private _open = false;
@state() private _backupEncryptionKey?: string;
@state() private _userPassword?: string;
@@ -68,8 +67,6 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
@state() private _unsub?: Promise<UnsubscribeFunc>;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(params: RestoreBackupDialogParams) {
this._params = params;
@@ -94,10 +91,12 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
} else {
this._step = STEPS[0];
}
this._open = true;
}
public closeDialog() {
this._dialog?.close();
this._open = false;
return true;
}
@@ -112,6 +111,7 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
this._stage = undefined;
this._step = undefined;
this._unsubscribe();
this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -136,17 +136,14 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
);
return html`
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
<span slot="title" .title=${dialogTitle}>${dialogTitle}</span>
</ha-dialog-header>
<div slot="content" class="content">
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${dialogTitle}
width="medium"
@closed=${this._dialogClosed}
>
<div class="content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: this._step === "confirm"
@@ -155,18 +152,18 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
? this._renderEncryption()
: this._renderProgress()}
</div>
<div slot="actions">
${this._error
? html`
<ha-button @click=${this.closeDialog}>
${this._error
? html`
<ha-dialog-footer slot="footer">
<ha-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.close")}
</ha-button>
`
: this._step === "confirm" || this._step === "encryption"
? this._renderConfirmActions()
: nothing}
</div>
</ha-md-dialog>
</ha-dialog-footer>
`
: this._step === "confirm" || this._step === "encryption"
? this._renderConfirmActions()
: nothing}
</ha-wa-dialog>
`;
}
@@ -216,6 +213,7 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
${this._renderEncryptionIntro()}
<ha-password-field
autofocus
@input=${this._passwordChanged}
.label=${this.hass.localize(
"ui.panel.config.backup.dialogs.restore.encryption.input_label"
@@ -227,14 +225,24 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
private _renderConfirmActions() {
return html`
<ha-button appearance="plain" @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._restoreBackup} variant="danger">
${this.hass.localize(
"ui.panel.config.backup.dialogs.restore.actions.restore"
)}
</ha-button>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._restoreBackup}
variant="danger"
>
${this.hass.localize(
"ui.panel.config.backup.dialogs.restore.actions.restore"
)}
</ha-button>
</ha-dialog-footer>
`;
}
@@ -365,10 +373,6 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
haStyle,
haStyleDialog,
css`
ha-md-dialog {
max-width: 500px;
width: 100%;
}
.content p {
margin: 0 0 16px;
}
@@ -1,15 +1,13 @@
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
import { mdiContentCopy, mdiDownload } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-password-field";
@@ -31,40 +29,40 @@ type Step = (typeof STEPS)[number];
class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false;
@state() private _open = false;
@state() private _step?: Step;
@state() private _params?: SetBackupEncryptionKeyDialogParams;
@query("ha-md-dialog") private _dialog!: HaMdDialog;
@state() private _newEncryptionKey?: string;
public showDialog(params: SetBackupEncryptionKeyDialogParams): void {
this._params = params;
this._step = STEPS[0];
this._opened = true;
this._open = true;
this._newEncryptionKey = generateEncryptionKey();
}
public closeDialog() {
if (this._params!.cancel) {
this._params!.cancel();
this._open = false;
return true;
}
private _dialogClosed() {
if (this._params?.cancel) {
this._params.cancel();
}
if (this._opened) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._opened = false;
this._step = undefined;
this._params = undefined;
this._newEncryptionKey = undefined;
return true;
this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _done() {
this._params?.submit!(true);
this._dialog.close();
this.closeDialog();
}
private _nextStep() {
@@ -76,7 +74,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
}
protected render() {
if (!this._opened || !this._params) {
if (!this._params || !this._step) {
return nothing;
}
@@ -88,21 +86,20 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
: "";
return html`
<ha-md-dialog disable-cancel-action open @closed=${this.closeDialog}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
<span slot="title">${dialogTitle}</span>
</ha-dialog-header>
<div slot="content">${this._renderStepContent()}</div>
<div slot="actions">
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${dialogTitle}
width="medium"
prevent-scrim-close
@closed=${this._dialogClosed}
>
${this._renderStepContent()}
<ha-dialog-footer slot="footer">
${this._step === "key"
? html`
<ha-button
slot="primaryAction"
@click=${this._submit}
.disabled=${!this._newEncryptionKey}
>
@@ -112,14 +109,14 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
</ha-button>
`
: html`
<ha-button @click=${this._done}>
<ha-button slot="primaryAction" @click=${this._done}>
${this.hass.localize(
"ui.panel.config.backup.dialogs.set_encryption_key.actions.done"
)}
</ha-button>
`}
</div>
</ha-md-dialog>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
@@ -213,10 +210,8 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
haStyle,
haStyleDialog,
css`
ha-md-dialog {
width: 90vw;
max-width: 560px;
--dialog-content-padding: 8px 24px;
ha-wa-dialog {
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
}
ha-md-list {
background: none;
@@ -247,14 +242,6 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
flex: none;
margin: -16px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-md-dialog {
max-width: none;
}
div[slot="content"] {
margin-top: 0;
}
}
p {
margin-top: 0;
}

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