Compare commits

..

118 Commits

Author SHA1 Message Date
Aidan Timson
74db7f5868 Reapply "Migrate dialog-device-registry-detail to ha-wa-dialog (#27668)" (#27716)
This reverts commit 5f75fc5bcb.
2025-10-30 14:20:54 +00:00
renovate[bot]
8ea7ad3026 Update vitest monorepo to v4.0.4 (#27720)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 15:59:03 +02:00
renovate[bot]
73747fbedc Update dependency @rsdoctor/rspack-plugin to v1.3.6 (#27719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 13:57:36 +00:00
Aidan Timson
aaa92bd354 Assist pipelines: allow user to stop TTS audio test (#27710)
* Allow user to stop TTS audio test

* Change button color on state

* Use getter over state
2025-10-30 15:35:19 +02:00
Aidan Timson
5f75fc5bcb Revert "Migrate dialog-device-registry-detail to ha-wa-dialog (#27668)" (#27716)
This reverts commit 2a8d935601.
2025-10-30 12:12:23 +00:00
karwosts
5fa44548c3 Add a state filter to logbook card (#27685)
* Add a state filter to logbook card

* types
2025-10-30 12:23:29 +02:00
Aidan Timson
1945c11621 Trend feature: make sure content is centered when loading (#27708)
* Make sure content is centered when loading

* Restore from test
2025-10-30 09:43:04 +01:00
renovate[bot]
930575d292 Update dependency @rsdoctor/rspack-plugin to v1.3.5 (#27706)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-30 06:02:18 +00:00
Paul Bottein
0147dbab00 Restore trigger id in overflow menu for trigger (#27702) 2025-10-30 04:26:25 +02:00
Paul Bottein
13ace24b83 Only display add button if at least one entity is selected in entities picker (#27699) 2025-10-29 17:05:34 +01:00
Paul Bottein
616333591a Only clear from and to trigger in state trigger (#27700) 2025-10-29 17:02:02 +01:00
Bram Kragten
8f5875c30f Merge branch 'rc' into dev 2025-10-29 16:20:58 +01:00
Bram Kragten
517cd49f35 Bumped version to 20251029.0 2025-10-29 16:19:23 +01:00
Petar Petrov
25d9fc94b2 Add legend to energy pie chart (#27697)
* Add legend to energy pie chart

* Update src/components/chart/ha-chart-base.ts

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

* resize fix

* some fixes

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2025-10-29 14:50:57 +00:00
Ezra Freedman
7b188759e3 Fix todo item date picker displaying previous day (#27698) 2025-10-29 14:21:34 +00:00
Paul Bottein
76772d1098 Default card name to friendly name (#27696)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-10-29 13:41:45 +00:00
Paul Bottein
6052745ca0 Add floor icon to every home dashboard views (#27695) 2025-10-29 13:36:28 +01:00
Paul Bottein
89b9780345 Revert "Default entity name to friendly name"
This reverts commit a607edca96.
2025-10-29 13:02:55 +01:00
Paul Bottein
a607edca96 Default entity name to friendly name 2025-10-29 13:02:14 +01:00
Tobias Bieniek
52eb3d8063 Add automations category to home dashboard area views (#27641) 2025-10-29 12:21:17 +01:00
renovate[bot]
1361fc36bf Update dependency @lezer/highlight to v1.2.3 (#27691)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-29 10:44:54 +00:00
Tobias Bieniek
505ef2bd11 home dashboard: Allow users to choose weather entity if they have more than one (#27643) 2025-10-29 11:36:05 +01:00
Petar Petrov
c0cc66c1ab Fix next flow config flow showing an empty dialog (#27682) 2025-10-29 09:53:14 +01:00
ildar170975
7cfbc521c7 Dev tools -> Templates: max-height fix for cm-editor (#27461) 2025-10-29 09:52:45 +01:00
Ezra Freedman
e064ce56cc Fix calendar all day date display (#27689) 2025-10-29 09:42:50 +01:00
renovate[bot]
8d688aa3a9 Update Node.js to v22.21.1 (#27686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 19:46:07 +00:00
Aidan Timson
d122483449 Fix entities card size and add grid contstraints (#27684)
* Add grid card options

* Allow overflow

* Use ha-scrollbar

* Use title/header for min rows calculation

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

* Format

* Remove entities length check

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-10-28 20:35:55 +01:00
Aidan Timson
f17bbc3f79 Fix activity card height and add constraints for grid layout (#27683)
* Fix logbook height

* Add grid option constraints

* Reverse
2025-10-28 17:02:50 +02:00
karwosts
c88f8fcce0 Shift stats in history by 1 hour (#27633) 2025-10-28 15:53:22 +02:00
Tobias Bieniek
8efabde916 Add floor icons to home dashboard headings (#27639)
* Add floor icons to home dashboard headings

This displays floor icons next to floor names in the home dashboard to provide visual consistency with the areas overview dashboard. The icons use either the custom floor icon if configured, or fall back to level-based default icons (e.g., `mdi:home-floor-0`, `mdi:home-floor-1`).

* Remove floor icon fallback from home dashboard headings

as requested in https://github.com/home-assistant/frontend/pull/27639#issuecomment-3452048655
2025-10-28 15:50:50 +02:00
Niklas Wagner
e821e1ec83 Allow selecting multiple states in trigger condition (#27455)
* Allow selecting multiple states in trigger condition

* Make from/to select exlusive to each other

* Simplify code

* fix: returning correct type

* Remove unnecessary any type
2025-10-28 15:43:34 +02:00
Wendelin
dc7516da94 Bottom-sheet swipe to close (#27537)
* WIP new add automation element

* WIP new add dialog

* revert merge

* Add tabs

* fix height

* Add max-height

* Add keybindings and blocks search separation

* Fix device translation

* add swipe to close for bottom sheet

* fix translations, scroll issues, RTL

* update target picker selector

* Fix bottom sheet padding

* Simplify scroll lock

* Simplify scroll lock

* Improve swipe gesture

* Fix methods

* Fix race condition

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2025-10-28 13:47:11 +02:00
Aidan Timson
a545a377a7 Fix typos and improve grammar on ha-dialogs design docs (#27681) 2025-10-28 12:38:47 +01:00
Aidan Timson
3634dbcbbf Add media player volume buttons card feature (#27624)
* Add media player volume buttons card feature

* Sort import

* Add uom

* Update src/panels/lovelace/card-features/hui-media-player-volume-buttons-card-feature.ts

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

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-10-28 13:29:56 +02:00
renovate[bot]
75af4f939e Update vitest monorepo to v4.0.3 (#27673)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 14:42:03 +00:00
Wendelin
453a2ac7f3 Use generic picker for language picker (#27631)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-10-27 14:02:13 +00:00
karwosts
8fbd0226fc Media selector for view backgrounds (#27544)
* Media selector for view backgrounds

* Bring back preview image
2025-10-27 15:26:20 +02:00
Aidan Timson
2a8d935601 Migrate dialog-device-registry-detail to ha-wa-dialog (#27668) 2025-10-27 14:30:22 +02:00
Aidan Timson
a6328fb6d7 Use space tokens in ha-more-info-dialog (#27666) 2025-10-27 13:44:49 +02:00
Aidan Timson
a78b61006f Use space tokens in ha-automation-row (#27665) 2025-10-27 13:40:37 +02:00
Aidan Timson
d506aa23b6 Use space tokens in ha-markdown (#27664) 2025-10-27 13:40:04 +02:00
Aidan Timson
48b4df43ab Use space tokens in ha-quick-bar (#27663) 2025-10-27 13:39:41 +02:00
Aidan Timson
8cdcd9cb55 Use space tokens in ha-card (#27662) 2025-10-27 13:38:36 +02:00
ildar170975
a1e2ac1d99 Label picker/selector: add a decription to a list (#27635)
add a decription to getLabels()
2025-10-27 11:11:53 +02:00
Simon Lamon
8ecddbc42c Adjust primary action buttons for create/update category/area dialogs (#27651)
Adjust primary action buttons
2025-10-27 10:52:02 +02:00
dependabot[bot]
6f70ef52a5 Bump actions/upload-artifact from 4.6.2 to 5.0.0 (#27660)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.2 to 5.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](ea165f8d65...330a01c490)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 10:49:12 +02:00
renovate[bot]
7dff02d7c8 Update dependency @types/sortablejs to v1.15.9 (#27659)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 06:35:59 +00:00
Simon Lamon
8bbd7a6a06 Add min and max values for hardware graphs (#27649) 2025-10-27 08:31:24 +02:00
karwosts
5c73a06f76 Regenerate service-picker valueRenderer on localize update (#27640)
* Regenerate service-picker valueRenderer on localize update

* memoize services
2025-10-27 08:30:17 +02:00
Simon Lamon
9943dae82c Adjust line height in weather card (#27653) 2025-10-27 08:29:17 +02:00
renovate[bot]
70bf049df0 Update babel monorepo to v7.28.5 (#27655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 08:28:29 +02:00
renovate[bot]
f9d9fbb7f0 Update vitest monorepo to v4.0.2 (#27656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 08:28:00 +02:00
dependabot[bot]
9cb84d3f37 Bump github/codeql-action from 4.30.9 to 4.31.0 (#27661)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.30.9 to 4.31.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](16140ae1a1...4e94bd11f7)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 08:27:30 +02:00
renovate[bot]
c1bcf27cf8 Update dependency @types/qrcode to v1.5.6 (#27658)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-27 08:26:41 +02:00
Paul Bottein
3e749ec085 20251001.4 (#27458) 2025-10-11 14:42:36 +02:00
Paul Bottein
ee2ec00069 Bumped version to 20251001.4 2025-10-11 14:41:59 +02:00
Paul Bottein
0aa2941868 Fix unresolved merge conflict in core style 2025-10-11 14:41:42 +02:00
Paul Bottein
46cd1d5156 20251001.3 (#27457) 2025-10-11 14:28:02 +02:00
Paul Bottein
07a5c41fd4 Bumped version to 20251001.3 2025-10-11 14:27:11 +02:00
Paul Bottein
4ad3c553d5 Adjust yarn updates for rc (#27456) 2025-10-11 14:26:10 +02:00
Simon Lamon
d40cc448a5 Adjust yarn updates for rc 2025-10-11 12:08:06 +00:00
Bram Kragten
e2f3f9d348 Merge branch 'rc' 2025-10-10 11:36:56 +02:00
Bram Kragten
98d44950f8 Bumped version to 20251001.2 2025-10-10 11:36:46 +02:00
Paul Bottein
8ae9edb1ef Fix ha dialog default size (#27415)
* Don't hardcode width height on mobile for all dialogs

* Don't set min width on desktop
2025-10-10 11:35:45 +02:00
Paul Bottein
84c4396c13 Add tooltip instead of title for 'add' button (#27399) 2025-10-10 11:35:44 +02:00
Bram Kragten
2b937a30e3 Merge branch 'rc' 2025-10-10 10:38:43 +02:00
Bram Kragten
b7815bfd86 Bumped version to 20251001.1 2025-10-10 10:38:26 +02:00
Wendelin
d94fa03411 Fix ha-button keyboard focus (#27437) 2025-10-10 10:38:08 +02:00
Petar Petrov
0a7007ef9e Escape device names in energy dashboard (#27425) 2025-10-10 10:31:09 +02:00
Paul Bottein
dd12136dee Use right variable for content color in tooltip (#27400) 2025-10-10 10:31:08 +02:00
Wendelin
6e2f89fe3d Fix android tap highlight border radius (#27382) 2025-10-10 10:31:07 +02:00
Jan-Philipp Benecke
092085b9af Fix media player more info title calculations (#27360) 2025-10-10 10:31:06 +02:00
Paulus Schoutsen
1c06eb8661 Add ESPHome to discovery sources (#27327) 2025-10-10 10:31:05 +02:00
Jan-Philipp Benecke
c7e87b06b5 Fix formatting of position slider tooltip in media player more info (#27326) 2025-10-10 10:31:04 +02:00
Jan-Philipp Benecke
38c738c199 Add "media_stop" action to media player controls in more info (#27325) 2025-10-10 10:31:03 +02:00
Wendelin
e899587307 Fix mobile ha-dialog height in Browser (#27298)
Enhance dialog responsiveness by adjusting min/max height to use svh units
2025-10-10 10:31:02 +02:00
Jan-Philipp Benecke
c9feb0b75f Add color tokens for slider thumb and indicator (#27295) 2025-10-10 10:31:00 +02:00
Jan-Philipp Benecke
10718c35d1 Make ha-slider not depend on font sizes (#27294) 2025-10-10 10:30:59 +02:00
Wendelin
4dc6a37bad Fix automation editor safe area (#27292) 2025-10-10 10:30:59 +02:00
Jan-Philipp Benecke
ac49fc7aba Support redo on Shift+CMD+Z (#27287)
* Support redo on Shift+CMD+Z

* Update redo shortcut for macOS to CMD+Shift+Z
2025-10-10 10:30:57 +02:00
Paul Bottein
e4f008800b Align bottom sheet border radius with resizable bottom sheet (#27280) 2025-10-10 10:30:56 +02:00
Bram Kragten
0b0ffd7bab Merge branch 'rc' 2025-10-01 08:58:47 +02:00
Bram Kragten
dfa77526a2 Bumped version to 20251001.0 2025-10-01 08:58:30 +02:00
Bram Kragten
9a3bd6c613 Fix intl polyfill loading (#27261) 2025-10-01 08:55:19 +02:00
Wendelin
1161de5746 Update WA to fix tab group scrolling (#27255) 2025-10-01 08:51:12 +02:00
Jan-Philipp Benecke
9df8e20391 Use local entity picture if available in media player more info (#27252) 2025-10-01 08:51:11 +02:00
Petar Petrov
11047a9c95 Make "loading next step" look like progress step in config flows (#27234) 2025-10-01 08:51:10 +02:00
Jan-Philipp Benecke
18fa66f61c Adjust media player cover image sizes in more info for smaller screens (#27232)
Adjust media player cover image sizes for smaller screens
2025-10-01 08:51:09 +02:00
Simon Lamon
758a048f34 Set explicit netlify version to fix workflows (#27229)
netlify set explicit version for fix
2025-10-01 08:51:08 +02:00
Jan-Philipp Benecke
ee0fc360b0 Add custom color token for control color (#27227) 2025-10-01 08:51:07 +02:00
Jan-Philipp Benecke
4012f95ec1 Add tooltips for undo/redo in automation & script editors (#27224) 2025-10-01 08:51:06 +02:00
Paul Bottein
0336ce4606 20250926.0 (#27213) 2025-09-26 15:39:29 +02:00
Paul Bottein
9ba36ab7e2 Bumped version to 20250926.0 2025-09-26 15:38:51 +02:00
Paul Bottein
fe7a08a1b0 Don't display negative durations in media player more info (#27212)
Don't display negative value in media player more info
2025-09-26 15:38:21 +02:00
Paul Bottein
87a8f9cedc Fix slider ticks support for number selector (#27211) 2025-09-26 15:38:21 +02:00
Paul Bottein
01df7e20ca Fix try tts dialog max width (#27208) 2025-09-26 15:38:20 +02:00
Jan-Philipp Benecke
d181219522 Refactor media player slider to use slot for position and duration display (#27205)
* Refactor media player slider to use slot for position and duration display

* Fix variable naming
2025-09-26 15:38:19 +02:00
karwosts
6ae24b8135 Add validation issues to energy diagnostic (#27203) 2025-09-26 15:38:18 +02:00
karwosts
8e009f24f9 Add dropdown mode to water heater operation feature (#27201) 2025-09-26 15:38:17 +02:00
Simon Lamon
53031f44ac Fix typos in media player more info (#27198) 2025-09-26 15:38:16 +02:00
Jan-Philipp Benecke
af5a988457 Round seconds in media player more info before formatting (#27196) 2025-09-26 15:38:15 +02:00
Paul Bottein
bab0391a19 20250925.1 (#27191) 2025-09-25 17:57:22 +02:00
Paul Bottein
444123c47e Bumped version to 20250925.1 2025-09-25 17:56:13 +02:00
Paul Bottein
f123d34046 Revert "Update dependency @types/chromecast-caf-receiver to v6.0.24" (#27188) 2025-09-25 17:53:59 +02:00
Paul Bottein
1b40f99f68 Fix storage bar not displayed (#27183) 2025-09-25 17:50:52 +02:00
Paul Bottein
b314b3ed2b Fix analytics switches (#27181) 2025-09-25 17:50:51 +02:00
Paul Bottein
59b8932969 Add icon option to common controls section strategy (#27180) 2025-09-25 17:50:50 +02:00
Wendelin
107af753ec Reduce default tab padding in tab-group (#27173) 2025-09-25 17:50:49 +02:00
Paul Bottein
1f0acb3046 Disabled config badge (#27172)
* Add disabled option for badge

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

* Update en.json
2025-09-25 10:46:34 +02:00
Paulus Schoutsen
404edf9483 Avoid invalid entities in common controls (#27158) 2025-09-25 10:46:33 +02:00
Paul Bottein
a166b4e9b6 Do not show error message when action has no response in dev tools (#27156) 2025-09-25 10:46:32 +02:00
Paul Bottein
7a285f11db 20250924.0 (#27155) 2025-09-24 17:15:37 +02:00
88 changed files with 1882 additions and 1007 deletions

View File

@@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: frontend-bundle-stats
path: build/stats/*.json
@@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: supervisor-bundle-stats
path: build/stats/*.json

View File

@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
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@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
# 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@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0

View File

@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: translations
path: translations.tar.gz

View File

@@ -19,11 +19,8 @@ jobs:
release:
name: Release
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: write # Required to upload release assets
id-token: write # For "Trusted Publisher" to PyPi
if: github.repository_owner == 'home-assistant'
steps:
- name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -49,18 +46,14 @@ jobs:
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build and release package
run: |
python3 -m pip install build
python3 -m pip install twine build
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
script/release
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
skip-existing: true
- name: Upload release assets
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
with:

2
.nvmrc
View File

@@ -1 +1 @@
22.21.0
22.21.1

View File

@@ -5,14 +5,14 @@ subtitle: Dialogs provide important prompts in a user flow.
# Material Design 3
Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guideliness. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guidelines. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
# Guidelines
## Design
- Dialogs have a max width of 560px. Alert and confirmation dialogs got a fixed width of 320px. If you need more width, consider a dedicated page instead.
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guideliness.
- Dialogs have a max width of 560px. Alert and confirmation dialogs have a fixed width of 320px. If you need more width, consider a dedicated page instead.
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guidelines.
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake.
- Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu.
- The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom.
@@ -26,7 +26,7 @@ Our dialogs are based on the latest version of Material Design. Please note that
- A best practice is to always use a title, even if it is optional by Material guidelines.
- People mainly read the title and a button. Put the most important information in those two.
- Try to avoid user generated content in the title, this could make the title unreadable long.
- Try to avoid user generated content in the title, this could make the title unreadably long.
- If users become unsure, they read the description. Make sure this explains what will happen.
- Strive for minimalism.

View File

@@ -53,7 +53,7 @@
"@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@home-assistant/webawesome": "3.0.0-beta.6.ha.6",
"@lezer/highlight": "1.2.2",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6",
"@lit-labs/virtualizer": "2.1.1",
@@ -148,16 +148,16 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.28.4",
"@babel/core": "7.28.5",
"@babel/helper-define-polyfill-provider": "0.6.5",
"@babel/plugin-transform-runtime": "7.28.3",
"@babel/preset-env": "7.28.3",
"@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.5",
"@bundle-stats/plugin-webpack-filter": "4.21.5",
"@lokalise/node-api": "15.3.1",
"@octokit/auth-oauth-device": "8.0.2",
"@octokit/plugin-retry": "8.0.2",
"@octokit/rest": "22.0.0",
"@rsdoctor/rspack-plugin": "1.3.4",
"@rsdoctor/rspack-plugin": "1.3.6",
"@rspack/core": "1.5.8",
"@rspack/dev-server": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
@@ -173,12 +173,12 @@
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.7.1",
"@types/mocha": "10.0.10",
"@types/qrcode": "1.5.5",
"@types/sortablejs": "1.15.8",
"@types/qrcode": "1.5.6",
"@types/sortablejs": "1.15.9",
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.1",
"@vitest/coverage-v8": "4.0.4",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
@@ -219,7 +219,7 @@
"typescript": "5.9.3",
"typescript-eslint": "8.46.2",
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.1",
"vitest": "4.0.4",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
@@ -235,5 +235,8 @@
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
},
"packageManager": "yarn@4.10.3"
"packageManager": "yarn@4.10.3",
"volta": {
"node": "22.21.1"
}
}

View File

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

View File

@@ -1,4 +1,5 @@
#!/bin/sh
# Pushes a new version to PyPi.
# Stop on errors
set -e
@@ -11,4 +12,5 @@ yarn install
script/build_frontend
rm -rf dist home_assistant_frontend.egg-info
python3 -m build -q
python3 -m build
python3 -m twine upload dist/*.whl --skip-existing

View File

@@ -0,0 +1,116 @@
export interface SwipeGestureResult {
velocity: number;
delta: number;
isSwipe: boolean;
isDownwardSwipe: boolean;
}
export interface SwipeGestureConfig {
velocitySwipeThreshold?: number;
movementTimeThreshold?: number;
}
const VELOCITY_SWIPE_THRESHOLD = 0.5; // px/ms
const MOVEMENT_TIME_THRESHOLD = 100; // ms
/**
* Recognizes swipe gestures and calculates velocity for touch interactions.
* Tracks touch movement and provides velocity-based and position-based gesture detection.
*/
export class SwipeGestureRecognizer {
private _startY = 0;
private _delta = 0;
private _startTime = 0;
private _lastY = 0;
private _lastTime = 0;
private _velocityThreshold: number;
private _movementTimeThreshold: number;
constructor(config: SwipeGestureConfig = {}) {
this._velocityThreshold =
config.velocitySwipeThreshold ?? VELOCITY_SWIPE_THRESHOLD; // px/ms
this._movementTimeThreshold =
config.movementTimeThreshold ?? MOVEMENT_TIME_THRESHOLD; // ms
}
/**
* Initialize gesture tracking with starting touch position
*/
public start(clientY: number): void {
const now = Date.now();
this._startY = clientY;
this._startTime = now;
this._lastY = clientY;
this._lastTime = now;
this._delta = 0;
}
/**
* Update gesture state during movement
* Returns the current delta (negative when dragging down)
*/
public move(clientY: number): number {
const now = Date.now();
this._delta = this._startY - clientY;
this._lastY = clientY;
this._lastTime = now;
return this._delta;
}
/**
* Calculate final gesture result when touch ends
*/
public end(): SwipeGestureResult {
const velocity = this.getVelocity();
const hasSignificantVelocity = Math.abs(velocity) > this._velocityThreshold;
return {
velocity,
delta: this._delta,
isSwipe: hasSignificantVelocity,
isDownwardSwipe: velocity > 0,
};
}
/**
* Get current drag delta (negative when dragging down)
*/
public getDelta(): number {
return this._delta;
}
/**
* Calculate velocity based on recent movement
* Returns 0 if no recent movement detected
* Positive velocity means downward swipe
*/
public getVelocity(): number {
const now = Date.now();
const timeSinceLastMove = now - this._lastTime;
// Only consider velocity if the last movement was recent
if (timeSinceLastMove >= this._movementTimeThreshold) {
return 0;
}
const timeDelta = this._lastTime - this._startTime;
return timeDelta > 0 ? (this._lastY - this._startY) / timeDelta : 0;
}
/**
* Reset all tracking state
*/
public reset(): void {
this._startY = 0;
this._delta = 0;
this._startTime = 0;
this._lastY = 0;
this._lastTime = 0;
}
}

View File

@@ -35,6 +35,7 @@ export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
const LEGEND_OVERFLOW_LIMIT = 10;
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
const DOUBLE_TAP_TIME = 300;
const RESIZE_ANIMATION_DURATION = 250;
export type CustomLegendOption = ECOption["legend"] & {
type: "custom";
@@ -205,6 +206,15 @@ export class HaChartBase extends LitElement {
}
if (changedProps.has("options")) {
chartOptions = { ...chartOptions, ...this._createOptions() };
if (
this._compareCustomLegendOptions(
changedProps.get("options"),
this.options
)
) {
// custom legend changes may require a resize to layout properly
this._shouldResizeChart = true;
}
} else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
chartOptions.dataZoom = this._getDataZoomConfig();
}
@@ -296,7 +306,7 @@ export class HaChartBase extends LitElement {
itemStyle = {
color: dataset?.color as string,
...(dataset?.itemStyle as { borderColor?: string }),
itemStyle,
...itemStyle,
};
const color = itemStyle?.color as string;
const borderColor = itemStyle?.borderColor as string;
@@ -508,6 +518,7 @@ export class HaChartBase extends LitElement {
);
}
});
this.requestUpdate("_hiddenDatasets");
}
private _getDataZoomConfig(): DataZoomComponentOption | undefined {
@@ -958,11 +969,31 @@ export class HaChartBase extends LitElement {
private _handleChartRenderFinished = () => {
if (this._shouldResizeChart) {
this.chart?.resize();
this.chart?.resize({
animation: this._reducedMotion
? undefined
: { duration: RESIZE_ANIMATION_DURATION },
});
this._shouldResizeChart = false;
}
};
private _compareCustomLegendOptions(
oldOptions: ECOption | undefined,
newOptions: ECOption | undefined
): boolean {
const oldLegends = ensureArray(
oldOptions?.legend || []
) as LegendComponentOption[];
const newLegends = ensureArray(
newOptions?.legend || []
) as LegendComponentOption[];
return (
oldLegends.some((l) => l.show && l.type === "custom") !==
newLegends.some((l) => l.show && l.type === "custom")
);
}
static styles = css`
:host {
display: block;

View File

@@ -0,0 +1 @@
export const ANY_STATE_VALUE = "__ANY_STATE_IGNORE_ATTRIBUTES__";

View File

@@ -147,7 +147,7 @@ class HaEntitiesPicker extends LitElement {
.createDomains=${this.createDomains}
.required=${this.required && !currentEntities.length}
@value-changed=${this._addEntity}
add-button
.addButton=${currentEntities.length > 0}
></ha-entity-picker>
</div>
`;

View File

@@ -312,7 +312,7 @@ export class HaEntityNamePicker extends LitElement {
private _toValue = memoizeOne(
(items: EntityNameItem[]): typeof this.value => {
if (items.length === 0) {
return "";
return undefined;
}
if (items.length === 1) {
const item = items[0];

View File

@@ -4,6 +4,7 @@ import { customElement, property } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../common/dom/fire_event";
import { ANY_STATE_VALUE } from "./const";
import { ensureArray } from "../../common/array/ensure-array";
import type { HomeAssistant } from "../../types";
import "./ha-entity-state-picker";
@@ -57,6 +58,7 @@ export class HaEntityStatesPicker extends LitElement {
const value = this.value || [];
const hide = [...(this.hideStates || []), ...value];
const hideValue = value.includes(ANY_STATE_VALUE);
return html`
${repeat(
@@ -84,7 +86,7 @@ export class HaEntityStatesPicker extends LitElement {
`
)}
<div>
${this.disabled && value.length
${(this.disabled && value.length) || hideValue
? nothing
: keyed(
value.length,

View File

@@ -118,7 +118,7 @@ export class HaAutomationRow extends LitElement {
}
.row {
display: flex;
padding: 0 8px;
padding: var(--ha-space-0) var(--ha-space-2);
min-height: 48px;
align-items: center;
cursor: pointer;
@@ -134,12 +134,12 @@ export class HaAutomationRow extends LitElement {
.expand-button {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
color: var(--ha-color-on-neutral-quiet);
margin-left: -8px;
margin-left: calc(var(--ha-space-2) * -1);
}
:host([building-block]) .leading-icon-wrapper {
background-color: var(--ha-color-fill-neutral-loud-resting);
border-radius: var(--ha-border-radius-md);
padding: 4px;
padding: var(--ha-space-1);
display: flex;
justify-content: center;
align-items: center;
@@ -149,7 +149,7 @@ export class HaAutomationRow extends LitElement {
color: var(--ha-color-on-neutral-quiet);
}
:host([building-block]) ::slotted([slot="leading-icon"]) {
--mdc-icon-size: 20px;
--mdc-icon-size: var(--ha-space-5);
color: var(--white-color);
transform: rotate(-45deg);
}
@@ -170,7 +170,7 @@ export class HaAutomationRow extends LitElement {
::slotted([slot="header"]) {
flex: 1;
overflow-wrap: anywhere;
margin: 0 12px;
margin: var(--ha-space-0) var(--ha-space-3);
}
:host([sort-selected]) .row {
outline: solid;

View File

@@ -1,6 +1,7 @@
import "@home-assistant/webawesome/dist/components/drawer/drawer";
import { css, html, LitElement, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
import { haStyleScrollbar } from "../resources/styles";
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
@@ -14,6 +15,12 @@ export class HaBottomSheet extends LitElement {
@state() private _drawerOpen = false;
@query("#drawer") private _drawer!: HTMLElement;
private _gestureRecognizer = new SwipeGestureRecognizer();
private _isDragging = false;
private _handleAfterHide() {
this.open = false;
const ev = new Event("closed", {
@@ -33,19 +40,132 @@ export class HaBottomSheet extends LitElement {
render() {
return html`
<wa-drawer
id="drawer"
placement="bottom"
.open=${this._drawerOpen}
@wa-after-hide=${this._handleAfterHide}
without-header
@touchstart=${this._handleTouchStart}
>
<slot name="header"></slot>
<div class="body ha-scrollbar">
<div id="body" class="body ha-scrollbar">
<slot></slot>
</div>
</wa-drawer>
`;
}
private _handleTouchStart = (ev: TouchEvent) => {
// Check if any element inside drawer in the composed path has scrollTop > 0
for (const path of ev.composedPath()) {
const el = path as HTMLElement;
if (el === this._drawer) {
break;
}
if (el.scrollTop > 0) {
return;
}
}
this._startResizing(ev.touches[0].clientY);
};
private _startResizing(clientY: number) {
// register event listeners for drag handling
document.addEventListener("touchmove", this._handleTouchMove, {
passive: false,
});
document.addEventListener("touchend", this._handleTouchEnd);
document.addEventListener("touchcancel", this._handleTouchEnd);
this._gestureRecognizer.start(clientY);
}
private _handleTouchMove = (ev: TouchEvent) => {
const currentY = ev.touches[0].clientY;
const delta = this._gestureRecognizer.move(currentY);
if (delta < 0) {
ev.preventDefault();
this._isDragging = true;
requestAnimationFrame(() => {
if (this._isDragging) {
this.style.setProperty(
"--dialog-transform",
`translateY(${delta * -1}px)`
);
}
});
}
};
private _animateSnapBack() {
// Add transition for smooth animation
this.style.setProperty(
"--dialog-transition",
`transform ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms ease-out`
);
// Reset transform to snap back
this.style.removeProperty("--dialog-transform");
// Remove transition after animation completes
setTimeout(() => {
this.style.removeProperty("--dialog-transition");
}, BOTTOM_SHEET_ANIMATION_DURATION_MS);
}
private _handleTouchEnd = () => {
this._unregisterResizeHandlers();
this._isDragging = false;
const result = this._gestureRecognizer.end();
// If velocity exceeds threshold, use velocity direction to determine action
if (result.isSwipe) {
if (result.isDownwardSwipe) {
// Downward swipe - close the bottom sheet
this._drawerOpen = false;
} else {
// Upward swipe - keep open and animate back
this._animateSnapBack();
}
return;
}
// If velocity is below threshold, use position-based logic
// Get the drawer height to calculate 50% threshold
const drawerBody = this._drawer.shadowRoot?.querySelector(
'[part="body"]'
) as HTMLElement;
const drawerHeight = drawerBody?.offsetHeight || 0;
// delta is negative when dragging down
// Close if dragged down past 50% of the drawer height
if (
drawerHeight > 0 &&
result.delta < 0 &&
Math.abs(result.delta) > drawerHeight * 0.5
) {
this._drawerOpen = false;
} else {
this._animateSnapBack();
}
};
private _unregisterResizeHandlers = () => {
document.removeEventListener("touchmove", this._handleTouchMove);
document.removeEventListener("touchend", this._handleTouchEnd);
document.removeEventListener("touchcancel", this._handleTouchEnd);
};
disconnectedCallback() {
super.disconnectedCallback();
this._unregisterResizeHandlers();
this._isDragging = false;
}
static styles = [
haStyleScrollbar,
css`
@@ -59,6 +179,8 @@ export class HaBottomSheet extends LitElement {
wa-drawer::part(dialog) {
max-height: var(--ha-bottom-sheet-max-height, 90vh);
align-items: center;
transform: var(--dialog-transform);
transition: var(--dialog-transition);
}
wa-drawer::part(body) {
max-width: var(--ha-bottom-sheet-max-width);
@@ -90,6 +212,11 @@ export class HaBottomSheet extends LitElement {
max-width: 100%;
display: flex;
flex-direction: column;
padding: var(
--ha-bottom-sheet-padding,
0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left)
);
}
`,
];

View File

@@ -44,26 +44,26 @@ export class HaCard extends LitElement {
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
letter-spacing: -0.012em;
line-height: var(--ha-line-height-expanded);
padding: 12px 16px 16px;
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4);
display: block;
margin-block-start: 0px;
margin-block-end: 0px;
margin-block-start: var(--ha-space-0);
margin-block-end: var(--ha-space-0);
font-weight: var(--ha-font-weight-normal);
}
:host ::slotted(.card-content:not(:first-child)),
slot:not(:first-child)::slotted(.card-content) {
padding-top: 0px;
margin-top: -8px;
padding-top: var(--ha-space-0);
margin-top: calc(var(--ha-space-2) * -1);
}
:host ::slotted(.card-content) {
padding: 16px;
padding: var(--ha-space-4);
}
:host ::slotted(.card-actions) {
border-top: 1px solid var(--divider-color, #e8e8e8);
padding: 8px;
padding: var(--ha-space-2);
}
`;

View File

@@ -148,7 +148,7 @@ export class HaForm extends LitElement implements HaFormElement {
.value=${getValue(this.data, item)}
.label=${this._computeLabel(item, this.data)}
.disabled=${item.disabled || this.disabled || false}
.placeholder=${item.required ? "" : item.default}
.placeholder=${item.required ? undefined : item.default}
.helper=${this._computeHelper(item)}
.localizeValue=${this.localizeValue}
.required=${item.required || false}

View File

@@ -24,7 +24,7 @@ import "./ha-svg-icon";
@customElement("ha-generic-picker")
export class HaGenericPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hass?: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@@ -68,6 +68,21 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: "not-found-label", type: String })
public notFoundLabel?: string;
@property({ attribute: "popover-placement" })
public popoverPlacement:
| "bottom"
| "top"
| "left"
| "right"
| "top-start"
| "top-end"
| "right-start"
| "right-end"
| "bottom-start"
| "bottom-end"
| "left-start"
| "left-end" = "bottom-start";
/** If set picker shows an add button instead of textbox when value isn't set */
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
@@ -135,7 +150,7 @@ export class HaGenericPicker extends LitElement {
style="--body-width: ${this._popoverWidth}px;"
without-arrow
distance="-4"
placement="bottom-start"
.placement=${this.popoverPlacement}
for="picker"
auto-size="vertical"
auto-size-padding="16"
@@ -144,9 +159,7 @@ export class HaGenericPicker extends LitElement {
trap-focus
role="dialog"
aria-modal="true"
aria-label=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
aria-label=${this.label || "Select option"}
>
${this._renderComboBox()}
</wa-popover>
@@ -159,9 +172,7 @@ export class HaGenericPicker extends LitElement {
@closed=${this._hidePicker}
role="dialog"
aria-modal="true"
aria-label=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
aria-label=${this.label || "Select option"}
>
${this._renderComboBox(true)}
</ha-bottom-sheet>`
@@ -179,7 +190,8 @@ export class HaGenericPicker extends LitElement {
<ha-picker-combo-box
.hass=${this.hass}
.allowCustomValue=${this.allowCustomValue}
.label=${this.searchLabel ?? this.hass.localize("ui.common.search")}
.label=${this.searchLabel ??
(this.hass?.localize("ui.common.search") || "Search")}
.value=${this.value}
@value-changed=${this._valueChanged}
.rowRenderer=${this.rowRenderer}

View File

@@ -1,56 +1,58 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { formatLanguageCode } from "../common/language/format_language";
import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { FrontendLocaleData } from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata";
import type { HomeAssistant } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-generic-picker";
import "./ha-list-item";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
import "./ha-select";
import type { HaSelect } from "./ha-select";
export const getLanguageOptions = (
languages: string[],
nativeName: boolean,
noSort: boolean,
locale?: FrontendLocaleData
) => {
let options: { label: string; value: string }[] = [];
): PickerComboBoxItem[] => {
let options: PickerComboBoxItem[] = [];
if (nativeName) {
const translations = translationMetadata.translations;
options = languages.map((lang) => {
let label = translations[lang]?.nativeName;
if (!label) {
let primary = translations[lang]?.nativeName;
if (!primary) {
try {
// this will not work if Intl.DisplayNames is polyfilled, it will return in the language of the user
label = new Intl.DisplayNames(lang, {
primary = new Intl.DisplayNames(lang, {
type: "language",
fallback: "code",
}).of(lang)!;
} catch (_err) {
label = lang;
primary = lang;
}
}
return {
value: lang,
label,
id: lang,
primary,
search_labels: [primary],
};
});
} else if (locale) {
options = languages.map((lang) => ({
value: lang,
label: formatLanguageCode(lang, locale),
id: lang,
primary: formatLanguageCode(lang, locale),
search_labels: [formatLanguageCode(lang, locale)],
}));
}
if (!noSort && locale) {
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, locale.language)
caseInsensitiveStringCompare(a.primary, b.primary, locale.language)
);
}
return options;
@@ -80,115 +82,69 @@ export class HaLanguagePicker extends LitElement {
@state() _defaultLanguages: string[] = [];
@query("ha-select") private _select!: HaSelect;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._computeDefaultLanguageOptions();
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
const localeChanged =
changedProperties.has("hass") &&
this.hass &&
changedProperties.get("hass") &&
changedProperties.get("hass").locale.language !==
this.hass.locale.language;
if (
changedProperties.has("languages") ||
changedProperties.has("value") ||
localeChanged
) {
this._select.layoutOptions();
if (!this.disabled && this._select.value !== this.value) {
fireEvent(this, "value-changed", { value: this._select.value });
}
if (!this.value) {
return;
}
const languageOptions = this._getLanguagesOptions(
this.languages ?? this._defaultLanguages,
this.nativeName,
this.noSort,
this.hass?.locale
);
const selectedItemIndex = languageOptions.findIndex(
(option) => option.value === this.value
);
if (selectedItemIndex === -1) {
this.value = undefined;
}
if (localeChanged) {
this._select.select(selectedItemIndex);
}
}
}
private _getLanguagesOptions = memoizeOne(getLanguageOptions);
private _computeDefaultLanguageOptions() {
this._defaultLanguages = Object.keys(translationMetadata.translations);
}
protected render() {
const languageOptions = this._getLanguagesOptions(
private _getItems = () =>
this._getLanguagesOptions(
this.languages ?? this._defaultLanguages,
this.nativeName,
this.noSort,
this.hass?.locale
);
private _valueRenderer = (value) => {
const language = this._getItems().find(
(lang) => lang.id === value
)?.primary;
return html`<span slot="headline">${language ?? value}</span> `;
};
protected render() {
const value =
this.value ??
(this.required && !this.disabled
? languageOptions[0]?.value
: this.value);
(this.required && !this.disabled ? this._getItems()[0].id : this.value);
return html`
<ha-select
.label=${this.label ??
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
popover-placement="bottom-end"
.notFoundLabel=${this.hass?.localize(
"ui.components.language-picker.no_match"
)}
.placeholder=${this.label ??
(this.hass?.localize("ui.components.language-picker.language") ||
"Language")}
.value=${value || ""}
.required=${this.required}
.value=${value}
.valueRenderer=${this._valueRenderer}
.disabled=${this.disabled}
@selected=${this._changed}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
.inlineArrow=${this.inlineArrow}
>
${languageOptions.length === 0
? html`<ha-list-item value=""
>${this.hass?.localize(
"ui.components.language-picker.no_languages"
) || "No languages"}</ha-list-item
>`
: languageOptions.map(
(option) => html`
<ha-list-item .value=${option.value}
>${option.label}</ha-list-item
>
`
)}
</ha-select>
.getItems=${this._getItems}
@value-changed=${this._changed}
hide-clear-icon
></ha-generic-picker>
`;
}
static styles = css`
ha-select {
ha-generic-picker {
width: 100%;
min-width: 200px;
display: block;
}
`;
private _changed(ev): void {
const target = ev.target as HaSelect;
if (this.disabled || target.value === "" || target.value === this.value) {
return;
}
this.value = target.value;
private _changed(ev: ValueChangedEvent<string>): void {
ev.stopPropagation();
this.value = ev.detail.value;
fireEvent(this, "value-changed", { value: this.value });
}
}

View File

@@ -50,7 +50,7 @@ export class HaMarkdown extends LitElement {
}
ha-alert {
display: block;
margin: 4px 0;
margin: var(--ha-space-1) 0;
}
a {
color: var(--primary-color);
@@ -75,7 +75,7 @@ export class HaMarkdown extends LitElement {
padding: 0;
}
pre {
padding: 16px;
padding: var(--ha-space-4);
overflow: auto;
line-height: var(--ha-line-height-condensed);
font-family: var(--ha-font-family-code);
@@ -95,7 +95,7 @@ export class HaMarkdown extends LitElement {
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: 16px 0;
margin: var(--ha-space-4) 0;
}
` as CSSResultGroup;
}

View File

@@ -69,7 +69,7 @@ export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
@customElement("ha-picker-combo-box")
export class HaPickerComboBox extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hass?: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@@ -140,7 +140,9 @@ export class HaPickerComboBox extends LitElement {
protected render() {
return html`<ha-textfield
.label=${this.label ?? this.hass.localize("ui.common.search")}
.label=${this.label ??
this.hass?.localize("ui.common.search") ??
"Search"}
@input=${this._filterChanged}
></ha-textfield>
<lit-virtualizer
@@ -159,12 +161,18 @@ export class HaPickerComboBox extends LitElement {
private _defaultNotFoundItem = memoizeOne(
(
label: this["notFoundLabel"],
localize: LocalizeFunc
localize?: LocalizeFunc
): PickerComboBoxItemWithLabel => ({
id: NO_MATCHING_ITEMS_FOUND_ID,
primary: label || localize("ui.components.combo-box.no_match"),
primary:
label ||
(localize && localize("ui.components.combo-box.no_match")) ||
"No matching items found",
icon_path: mdiMagnify,
a11y_label: label || localize("ui.components.combo-box.no_match"),
a11y_label:
label ||
(localize && localize("ui.components.combo-box.no_match")) ||
"No matching items found",
})
);
@@ -189,13 +197,13 @@ export class HaPickerComboBox extends LitElement {
caseInsensitiveStringCompare(
entityA.sorting_label!,
entityB.sorting_label!,
this.hass.locale.language
this.hass?.locale.language ?? navigator.language
)
);
if (!sortedItems.length) {
sortedItems.push(
this._defaultNotFoundItem(this.notFoundLabel, this.hass.localize)
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
);
}
@@ -249,8 +257,20 @@ export class HaPickerComboBox extends LitElement {
const textfield = ev.target as HaTextField;
const searchString = textfield.value.trim();
if (!searchString) {
this._items = this._allItems;
return;
}
const index = this._fuseIndex(this._allItems);
const fuse = new HaFuse(this._allItems, { shouldSort: false }, index);
const fuse = new HaFuse(
this._allItems,
{
shouldSort: false,
minMatchCharLength: Math.min(searchString.length, 2),
},
index
);
const results = fuse.multiTermsSearch(searchString);
let filteredItems = this._allItems as PickerComboBoxItem[];
@@ -258,7 +278,7 @@ export class HaPickerComboBox extends LitElement {
const items = results.map((result) => result.item);
if (items.length === 0) {
items.push(
this._defaultNotFoundItem(this.notFoundLabel, this.hass.localize)
this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
);
}
const additionalItems = this._getAdditionalItems(searchString);
@@ -431,6 +451,17 @@ export class HaPickerComboBox extends LitElement {
private _pickSelectedItem = (ev: KeyboardEvent) => {
ev.stopPropagation();
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
if (
this._virtualizerElement?.items.length === 1 &&
firstItem.id !== NO_MATCHING_ITEMS_FOUND_ID
) {
fireEvent(this, "value-changed", {
value: firstItem.id,
});
}
if (this._selectedItemIndex === -1) {
return;
}
@@ -438,7 +469,9 @@ export class HaPickerComboBox extends LitElement {
// if filter button is focused
ev.preventDefault();
const item: any = this._virtualizerElement?.items[this._selectedItemIndex];
const item = this._virtualizerElement?.items[
this._selectedItemIndex
] as PickerComboBoxItem;
if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) {
fireEvent(this, "value-changed", { value: item.id });
}

View File

@@ -1,122 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { BackgroundSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-picture-upload";
import "../ha-alert";
import type { HaPictureUpload } from "../ha-picture-upload";
import { URL_PREFIX } from "../../data/image_upload";
@customElement("ha-selector-background")
export class HaBackgroundSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value?: any;
@property({ attribute: false }) public selector!: BackgroundSelector;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@state() private yamlBackground = false;
protected updated(changedProps) {
super.updated(changedProps);
if (changedProps.has("value")) {
this.yamlBackground = !!this.value && !this.value.startsWith(URL_PREFIX);
}
}
protected render() {
return html`
<div>
${this.yamlBackground
? html`
<div class="value">
<img
src=${this.value}
alt=${this.hass.localize(
"ui.components.picture-upload.current_image_alt"
)}
/>
</div>
<ha-alert alert-type="info">
${this.hass.localize(
`ui.components.selectors.background.yaml_info`
)}
<ha-button slot="action" @click=${this._clearValue}>
${this.hass.localize(
`ui.components.picture-upload.clear_picture`
)}
</ha-button>
</ha-alert>
`
: html`
<ha-picture-upload
.hass=${this.hass}
.value=${this.value?.startsWith(URL_PREFIX) ? this.value : null}
.original=${!!this.selector.background?.original}
.cropOptions=${this.selector.background?.crop}
select-media
@change=${this._pictureChanged}
></ha-picture-upload>
`}
</div>
`;
}
private _pictureChanged(ev) {
const value = (ev.target as HaPictureUpload).value;
fireEvent(this, "value-changed", { value: value ?? undefined });
}
private _clearValue() {
fireEvent(this, "value-changed", { value: undefined });
}
static styles = css`
:host {
display: block;
position: relative;
}
ha-picture-upload {
background-color: var(--primary-background-color);
border-radius: var(--file-upload-image-border-radius);
}
div {
display: flex;
flex-direction: column;
}
ha-button {
white-space: nowrap;
--mdc-theme-primary: var(--primary-color);
}
.value {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
img {
max-width: 100%;
max-height: 200px;
margin-bottom: 4px;
border-radius: var(--file-upload-image-border-radius);
transition: opacity 0.3s;
opacity: var(--picture-opacity, 1);
}
img:hover {
opacity: 1;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-background": HaBackgroundSelector;
}
}

View File

@@ -34,7 +34,6 @@ const LOAD_ELEMENTS = {
file: () => import("./ha-selector-file"),
floor: () => import("./ha-selector-floor"),
label: () => import("./ha-selector-label"),
background: () => import("./ha-selector-background"),
language: () => import("./ha-selector-language"),
navigation: () => import("./ha-selector-navigation"),
number: () => import("./ha-selector-number"),

View File

@@ -76,34 +76,42 @@ class HaServicePicker extends LitElement {
</ha-combo-box-item>
`;
private _valueRenderer: PickerValueRenderer = (value) => {
const serviceId = value;
const [domain, service] = serviceId.split(".");
private _valueRenderer = memoizeOne(
(
localize: LocalizeFunc,
services: HomeAssistant["services"]
): PickerValueRenderer =>
(value) => {
const serviceId = value;
const [domain, service] = serviceId.split(".");
if (!this.hass.services[domain]?.[service]) {
return html`
<ha-svg-icon slot="start" .path=${mdiRoomService}></ha-svg-icon>
<span slot="headline">${value}</span>
`;
}
if (!services[domain]?.[service]) {
return html`
<ha-svg-icon slot="start" .path=${mdiRoomService}></ha-svg-icon>
<span slot="headline">${value}</span>
`;
}
const serviceName =
this.hass.localize(`component.${domain}.services.${service}.name`) ||
this.hass.services[domain][service].name ||
service;
const serviceName =
localize(`component.${domain}.services.${service}.name`) ||
services[domain][service].name ||
service;
return html`
<ha-service-icon
slot="start"
.hass=${this.hass}
.service=${serviceId}
></ha-service-icon>
<span slot="headline">${serviceName}</span>
${this.showServiceId
? html`<span slot="supporting-text" class="code">${serviceId}</span>`
: nothing}
`;
};
return html`
<ha-service-icon
slot="start"
.hass=${this.hass}
.service=${serviceId}
></ha-service-icon>
<span slot="headline">${serviceName}</span>
${this.showServiceId
? html`<span slot="supporting-text" class="code"
>${serviceId}</span
>`
: nothing}
`;
}
);
protected render(): TemplateResult {
const placeholder =
@@ -123,7 +131,10 @@ class HaServicePicker extends LitElement {
.value=${this.value}
.getItems=${this._getItems}
.rowRenderer=${this._rowRenderer}
.valueRenderer=${this._valueRenderer}
.valueRenderer=${this._valueRenderer(
this.hass.localize,
this.hass.services
)}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>

View File

@@ -435,9 +435,9 @@ export const convertStatisticsToHistory = (
Object.entries(orderedStatistics).forEach(([key, value]) => {
const entityHistoryStates: EntityHistoryState[] = value.map((e) => ({
s: e.mean != null ? e.mean.toString() : e.state!.toString(),
lc: e.start / 1000,
lc: e.end / 1000,
a: {},
lu: e.start / 1000,
lu: e.end / 1000,
}));
statsHistoryStates[key] = entityHistoryStates;
});

View File

@@ -264,6 +264,7 @@ export const getLabels = (
const items = outputLabels.map<PickerComboBoxItem>((label) => ({
id: label.label_id,
primary: label.name,
secondary: label.description ?? "",
icon: label.icon || undefined,
icon_path: label.icon ? undefined : mdiLabel,
sorting_label: label.name,

View File

@@ -1,3 +1,4 @@
import type { MediaSelectorValue } from "../../selector";
import type { LovelaceBadgeConfig } from "./badge";
import type { LovelaceCardConfig } from "./card";
import type { LovelaceSectionRawConfig } from "./section";
@@ -8,7 +9,7 @@ export interface ShowViewConfig {
}
export interface LovelaceViewBackgroundConfig {
image?: string;
image?: string | MediaSelectorValue;
opacity?: number;
size?: "auto" | "cover" | "contain";
alignment?:

View File

@@ -5,7 +5,6 @@ import type {
import { ensureArray } from "../common/array/ensure-array";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { supportsFeature } from "../common/entity/supports-feature";
import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { isHelperDomain } from "../panels/config/helpers/const";
import type { UiAction } from "../panels/lovelace/components/hui-action-editor";
import type { HomeAssistant } from "../types";
@@ -47,8 +46,6 @@ export type Selector =
| FileSelector
| IconSelector
| LabelSelector
| ImageSelector
| BackgroundSelector
| LanguageSelector
| LocationSelector
| MediaSelector
@@ -273,14 +270,6 @@ export interface IconSelector {
} | null;
}
export interface ImageSelector {
image: { original?: boolean; crop?: CropOptions } | null;
}
export interface BackgroundSelector {
background: { original?: boolean; crop?: CropOptions } | null;
}
export interface LabelSelector {
label: {
multiple?: boolean;

View File

@@ -484,7 +484,7 @@ class DataEntryFlowDialog extends LitElement {
this._unsubDataEntryFlowProgress = undefined;
}
if (_step.next_flow[0] === "config_flow") {
showConfigFlowDialog(this._params!.dialogParentElement!, {
showConfigFlowDialog(this, {
continueFlowId: _step.next_flow[1],
carryOverDevices: this._devices(
this._params!.flowConfig.showDevices,
@@ -496,32 +496,23 @@ class DataEntryFlowDialog extends LitElement {
});
} else if (_step.next_flow[0] === "options_flow") {
if (_step.type === "create_entry") {
showOptionsFlowDialog(
this._params!.dialogParentElement!,
_step.result!,
{
continueFlowId: _step.next_flow[1],
navigateToResult: this._params!.navigateToResult,
dialogClosedCallback: this._params!.dialogClosedCallback,
}
);
showOptionsFlowDialog(this, _step.result!, {
continueFlowId: _step.next_flow[1],
navigateToResult: this._params!.navigateToResult,
dialogClosedCallback: this._params!.dialogClosedCallback,
});
}
} else if (_step.next_flow[0] === "config_subentries_flow") {
if (_step.type === "create_entry") {
showSubConfigFlowDialog(
this._params!.dialogParentElement!,
_step.result!,
_step.next_flow[0],
{
continueFlowId: _step.next_flow[1],
navigateToResult: this._params!.navigateToResult,
dialogClosedCallback: this._params!.dialogClosedCallback,
}
);
showSubConfigFlowDialog(this, _step.result!, _step.next_flow[0], {
continueFlowId: _step.next_flow[1],
navigateToResult: this._params!.navigateToResult,
dialogClosedCallback: this._params!.dialogClosedCallback,
});
}
} else {
this.closeDialog();
showAlertDialog(this._params!.dialogParentElement!, {
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error",
{ error: `Unsupported next flow type: ${_step.next_flow[0]}` }

View File

@@ -678,8 +678,8 @@ export class MoreInfoDialog extends LitElement {
/* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */
--vertical-align-dialog: flex-start;
--dialog-surface-margin-top: max(
40px,
var(--safe-area-inset-top, 0px)
var(--ha-space-10),
var(--safe-area-inset-top, var(--ha-space-0))
);
--dialog-content-padding: 0;
}
@@ -698,14 +698,15 @@ export class MoreInfoDialog extends LitElement {
}
ha-more-info-history-and-logbook {
padding: 8px 24px 24px 24px;
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-6)
var(--ha-space-6);
display: block;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
/* When in fullscreen dialog should be attached to top */
ha-dialog {
--dialog-surface-margin-top: 0px;
--dialog-surface-margin-top: var(--ha-space-0);
}
}
@@ -730,7 +731,8 @@ export class MoreInfoDialog extends LitElement {
display: flex;
flex-direction: column;
align-items: flex-start;
margin: 0 0 -10px 0;
margin: var(--ha-space-0) var(--ha-space-0)
calc(var(--ha-space-2) * -1) var(--ha-space-0);
}
.title p {
@@ -752,9 +754,9 @@ export class MoreInfoDialog extends LitElement {
font-size: var(--ha-font-size-m);
line-height: 16px;
--mdc-icon-size: 16px;
padding: 4px;
margin: -4px;
margin-top: -10px;
padding: var(--ha-space-1);
margin: calc(var(--ha-space-1) * -1);
margin-top: calc(var(--ha-space-2) * -1);
background: none;
border: none;
outline: none;

View File

@@ -1011,8 +1011,8 @@ export class QuickBar extends LitElement {
--mdc-dialog-max-width: 800px;
--mdc-dialog-min-width: 500px;
--dialog-surface-position: fixed;
--dialog-surface-top: 40px;
--mdc-dialog-max-height: calc(100% - 72px);
--dialog-surface-top: var(--ha-space-10);
--mdc-dialog-max-height: calc(100% - var(--ha-space-18));
}
}
@@ -1055,8 +1055,8 @@ export class QuickBar extends LitElement {
}
span.command-text {
margin-left: 8px;
margin-inline-start: 8px;
margin-left: var(--ha-space-2);
margin-inline-start: var(--ha-space-2);
margin-inline-end: initial;
direction: var(--direction);
}
@@ -1069,8 +1069,8 @@ export class QuickBar extends LitElement {
ha-md-list-item.two-line {
--md-list-item-one-line-container-height: 64px;
--md-list-item-two-line-container-height: 64px;
--md-list-item-top-space: 8px;
--md-list-item-bottom-space: 8px;
--md-list-item-top-space: var(--ha-space-2);
--md-list-item-bottom-space: var(--ha-space-2);
}
ha-md-list-item.three-line {
@@ -1078,8 +1078,8 @@ export class QuickBar extends LitElement {
--md-list-item-one-line-container-height: 72px;
--md-list-item-two-line-container-height: 72px;
--md-list-item-three-line-container-height: 72px;
--md-list-item-top-space: 8px;
--md-list-item-bottom-space: 8px;
--md-list-item-top-space: var(--ha-space-2);
--md-list-item-bottom-space: var(--ha-space-2);
}
ha-md-list-item .code {
@@ -1104,11 +1104,11 @@ export class QuickBar extends LitElement {
}
ha-tip {
padding: 20px;
padding: var(--ha-space-5);
}
.nothing-found {
padding: 16px 0px;
padding: var(--ha-space-4) var(--ha-space-0);
text-align: center;
}

View File

@@ -193,12 +193,12 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
).map(
(lang) =>
html`<ha-md-menu-item
.value=${lang.value}
.value=${lang.id}
@click=${this._handlePickLanguage}
@keydown=${this._handlePickLanguage}
.selected=${this._language === lang.value}
.selected=${this._language === lang.id}
>
${lang.label}
${lang.primary}
</ha-md-menu-item>`
)}
</ha-md-button-menu>`

View File

@@ -143,9 +143,14 @@ class DialogCalendarEventDetail extends LitElement {
this.hass.locale.time_zone,
this.hass.config.time_zone
);
const start = new TZDate(this._data!.dtstart, timeZone);
const endValue = new TZDate(this._data!.dtend, timeZone);
// All day events should be displayed as a day earlier
// For all-day events (date-only strings), parse without timezone to avoid offset issues
const start = isDate(this._data!.dtstart)
? new Date(this._data!.dtstart + "T00:00:00")
: new TZDate(this._data!.dtstart, timeZone);
const endValue = isDate(this._data!.dtend)
? new Date(this._data!.dtend + "T00:00:00")
: new TZDate(this._data!.dtend, timeZone);
// All day event end dates are exclusive in iCalendar format, subtract one day for display
const end = isDate(this._data.dtend) ? addDays(endValue, -1) : endValue;
// The range can be shortened when the start and end are on the same day.
if (isSameDay(start, end)) {

View File

@@ -15,6 +15,7 @@ import {
getFloors,
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
export interface ClimateViewStrategyConfig {
type: "climate";
@@ -152,6 +153,7 @@ export class ClimateViewStrategy extends ReactiveElement {
floorCount > 1
? floor.name
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
icon: floor.icon || floorDefaultIcon(floor),
},
],
};

View File

@@ -265,19 +265,15 @@ class DialogAreaDetail extends LitElement {
${this.hass.localize("ui.common.delete")}
</ha-button>`
: nothing}
<div slot="primaryAction">
<ha-button appearance="plain" @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
@click=${this._updateEntry}
.disabled=${nameInvalid || !!this._submitting}
>
${entry
? this.hass.localize("ui.common.save")
: this.hass.localize("ui.common.create")}
</ha-button>
</div>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${nameInvalid || !!this._submitting}
>
${entry
? this.hass.localize("ui.common.save")
: this.hass.localize("ui.common.create")}
</ha-button>
</ha-dialog>
`;
}

View File

@@ -3,6 +3,7 @@ import {
mdiContentCopy,
mdiContentCut,
mdiDelete,
mdiIdentifier,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiPlusCircleMultipleOutline,
@@ -40,6 +41,8 @@ export default class HaAutomationSidebarTrigger extends LitElement {
@property({ type: Number, attribute: "sidebar-key" })
public sidebarKey?: number;
@state() private _requestShowId = false;
@state() private _warnings?: string[];
@query(".sidebar-editor")
@@ -47,6 +50,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
protected willUpdate(changedProperties) {
if (changedProperties.has("config")) {
this._requestShowId = false;
this._warnings = undefined;
if (this.config) {
this.yamlMode = this.config.yamlMode;
@@ -101,6 +105,24 @@ export default class HaAutomationSidebarTrigger extends LitElement {
</div>
</ha-md-menu-item>
${!this.yamlMode &&
!("id" in this.config.config) &&
!this._requestShowId
? html`<ha-md-menu-item
slot="menu-items"
.clickAction=${this._showTriggerId}
.disabled=${this.disabled || type === "list"}
>
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-md-menu-item>`
: nothing}
<ha-md-divider
slot="menu-items"
role="separator"
@@ -250,6 +272,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
@value-changed=${this._valueChangedSidebar}
@yaml-changed=${this._yamlChangedSidebar}
.uiSupported=${this.config.uiSupported}
.showId=${this._requestShowId}
.yamlMode=${this.yamlMode}
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
@@ -292,6 +315,10 @@ export default class HaAutomationSidebarTrigger extends LitElement {
fireEvent(this, "toggle-yaml-mode");
};
private _showTriggerId = () => {
this._requestShowId = true;
};
static styles = [sidebarEditorStyles, overflowStyles];
}

View File

@@ -29,6 +29,8 @@ export default class HaAutomationTriggerEditor extends LitElement {
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
@property({ type: Boolean, attribute: "show-id" }) public showId = false;
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
protected render() {
@@ -36,6 +38,8 @@ export default class HaAutomationTriggerEditor extends LitElement {
const yamlMode = this.yamlMode || !this.uiSupported;
const showId = "id" in this.trigger || this.showId;
return html`
<div
class=${classMap({
@@ -70,20 +74,15 @@ export default class HaAutomationTriggerEditor extends LitElement {
></ha-yaml-editor>
`
: html`
${!isTriggerList(this.trigger)
${showId && !isTriggerList(this.trigger)
? html`
<ha-textfield
.label=${`${this.hass.localize(
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id"
)} (${this.hass.localize(
"ui.panel.config.automation.editor.triggers.optional"
)})`}
)}
.value=${this.trigger.id || ""}
.disabled=${this.disabled}
@change=${this._idChanged}
.helper=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id_helper"
)}
></ha-textfield>
`
: nothing}

View File

@@ -19,6 +19,7 @@ import { ensureArray } from "../../../../../common/array/ensure-array";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
import type { StateTrigger } from "../../../../../data/automation";
import { ANY_STATE_VALUE } from "../../../../../components/entity/const";
import type { HomeAssistant } from "../../../../../types";
import { baseTriggerStruct, forDictStruct } from "../../structs";
import type { TriggerElement } from "../ha-automation-trigger-row";
@@ -36,14 +37,12 @@ const stateTriggerStruct = assign(
trigger: literal("state"),
entity_id: optional(union([string(), array(string())])),
attribute: optional(string()),
from: optional(nullable(string())),
to: optional(nullable(string())),
from: optional(union([nullable(string()), array(string())])),
to: optional(union([nullable(string()), array(string())])),
for: optional(union([number(), string(), forDictStruct])),
})
);
const ANY_STATE_VALUE = "__ANY_STATE_IGNORE_ATTRIBUTES__";
@customElement("ha-automation-trigger-state")
export class HaStateTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -57,7 +56,12 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
}
private _schema = memoizeOne(
(localize: LocalizeFunc, attribute) =>
(
localize: LocalizeFunc,
attribute: string | undefined,
hideInFrom: string[],
hideInTo: string[]
) =>
[
{
name: "entity_id",
@@ -131,6 +135,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
},
selector: {
state: {
multiple: true,
extra_options: (attribute
? []
: [
@@ -142,6 +147,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
},
]) as any,
attribute: attribute,
hide_states: hideInFrom,
},
},
},
@@ -152,6 +158,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
},
selector: {
state: {
multiple: true,
extra_options: (attribute
? []
: [
@@ -163,6 +170,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
},
]) as any,
attribute: attribute,
hide_states: hideInTo,
},
},
},
@@ -207,13 +215,15 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
entity_id: ensureArray(this.trigger.entity_id),
for: trgFor,
};
if (!data.attribute && data.to === null) {
data.to = ANY_STATE_VALUE;
}
if (!data.attribute && data.from === null) {
data.from = ANY_STATE_VALUE;
}
const schema = this._schema(this.hass.localize, this.trigger.attribute);
data.to = this._normalizeStates(this.trigger.to, data.attribute);
data.from = this._normalizeStates(this.trigger.from, data.attribute);
const schema = this._schema(
this.hass.localize,
this.trigger.attribute,
data.to,
data.from
);
return html`
<ha-form
@@ -231,22 +241,60 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
ev.stopPropagation();
const newTrigger = ev.detail.value;
if (newTrigger.to === ANY_STATE_VALUE) {
newTrigger.to = newTrigger.attribute ? undefined : null;
newTrigger.to = this._applyAnyStateExclusive(
newTrigger.to,
newTrigger.attribute
);
if (Array.isArray(newTrigger.to) && newTrigger.to.length === 0) {
delete newTrigger.to;
}
if (newTrigger.from === ANY_STATE_VALUE) {
newTrigger.from = newTrigger.attribute ? undefined : null;
newTrigger.from = this._applyAnyStateExclusive(
newTrigger.from,
newTrigger.attribute
);
if (Array.isArray(newTrigger.from) && newTrigger.from.length === 0) {
delete newTrigger.from;
}
Object.keys(newTrigger).forEach((key) =>
newTrigger[key] === undefined || newTrigger[key] === ""
? delete newTrigger[key]
: {}
);
Object.keys(newTrigger).forEach((key) => {
const val = newTrigger[key];
if (val === undefined || val === "") {
delete newTrigger[key];
}
});
fireEvent(this, "value-changed", { value: newTrigger });
}
private _applyAnyStateExclusive(
val: string | string[] | null | undefined,
attribute?: string
): string | string[] | null | undefined {
const anyStateSelected = Array.isArray(val)
? val.includes(ANY_STATE_VALUE)
: val === ANY_STATE_VALUE;
if (anyStateSelected) {
// Any state is exclusive: null if no attribute, undefined if attribute
return attribute ? undefined : null;
}
return val;
}
private _normalizeStates(
value: string | string[] | null | undefined,
attribute?: string
): string[] {
// If no attribute is selected and backend value is null,
// expose it as the special ANY state option in the UI.
if (!attribute && value === null) {
return [ANY_STATE_VALUE];
}
if (value === undefined || value === null) {
return [];
}
return ensureArray(value);
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>

View File

@@ -98,7 +98,7 @@ class DialogCategoryDetail extends LitElement {
</div>
<ha-button
appearance="plain"
slot="primaryAction"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}

View File

@@ -5,7 +5,8 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
import "../../../../components/ha-alert";
import "../../../../components/ha-area-picker";
import "../../../../components/ha-dialog";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-button";
import "../../../../components/ha-labels-picker";
import type { HaSwitch } from "../../../../components/ha-switch";
@@ -19,6 +20,8 @@ import type { DeviceRegistryDetailDialogParams } from "./show-dialog-device-regi
class DialogDeviceRegistryDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@state() private _nameByUser!: string;
@state() private _error?: string;
@@ -42,10 +45,15 @@ class DialogDeviceRegistryDetail extends LitElement {
this._areaId = this._params.device.area_id || "";
this._labels = this._params.device.labels || [];
this._disabledBy = this._params.device.disabled_by;
this._open = true;
await this.updateComplete;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._error = "";
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -57,10 +65,12 @@ class DialogDeviceRegistryDetail extends LitElement {
}
const device = this._params.device;
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${computeDeviceNameDisplay(device, this.hass)}
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${computeDeviceNameDisplay(device, this.hass)}
prevent-scrim-close
@closed=${this._dialogClosed}
>
<div>
${this._error
@@ -68,6 +78,7 @@ class DialogDeviceRegistryDetail extends LitElement {
: ""}
<div class="form">
<ha-textfield
autofocus
.value=${this._nameByUser}
@input=${this._nameChanged}
.label=${this.hass.localize(
@@ -75,7 +86,6 @@ class DialogDeviceRegistryDetail extends LitElement {
)}
.placeholder=${device.name || ""}
.disabled=${this._submitting}
dialogInitialFocus
></ha-textfield>
<ha-area-picker
.hass=${this.hass}
@@ -131,22 +141,25 @@ class DialogDeviceRegistryDetail extends LitElement {
</div>
</div>
</div>
<ha-button
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
appearance="plain"
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${this._submitting}
>
${this.hass.localize("ui.dialogs.device-registry-detail.update")}
</ha-button>
</ha-dialog>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
appearance="plain"
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${this._submitting}
>
${this.hass.localize("ui.dialogs.device-registry-detail.update")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}

View File

@@ -153,6 +153,8 @@ class HaConfigHardware extends SubscribeMixin(LitElement) {
},
yAxis: {
type: "value",
min: 0,
max: 100,
splitLine: {
show: true,
},

View File

@@ -139,6 +139,12 @@ export class AssistPipelineDebug extends LitElement {
@property({ attribute: false }) public pipelineRun!: PipelineRun;
private _audioElement?: HTMLAudioElement;
private get _isPlaying(): boolean {
return this._audioElement != null && !this._audioElement.paused;
}
protected render(): TemplateResult {
const lastRunStage: string = this.pipelineRun
? ["tts", "intent", "stt", "wake_word"].find(
@@ -349,8 +355,13 @@ export class AssistPipelineDebug extends LitElement {
${this.pipelineRun?.tts?.tts_output
? html`
<div class="card-actions">
<ha-button @click=${this._playTTS}>
Play Audio
<ha-button
.variant=${this._isPlaying ? "danger" : "brand"}
@click=${this._isPlaying
? this._stopTTS
: this._playTTS}
>
${this._isPlaying ? "Stop audio" : "Play audio"}
</ha-button>
</div>
`
@@ -373,14 +384,41 @@ export class AssistPipelineDebug extends LitElement {
}
private _playTTS(): void {
// Stop any existing audio first
this._stopTTS();
const url = this.pipelineRun!.tts!.tts_output!.url;
const audio = new Audio(url);
audio.addEventListener("error", () => {
this._audioElement = new Audio(url);
this._audioElement.addEventListener("error", () => {
showAlertDialog(this, { title: "Error", text: "Error playing audio" });
});
audio.addEventListener("canplaythrough", () => {
audio.play();
this._audioElement.addEventListener("play", () => {
this.requestUpdate();
});
this._audioElement.addEventListener("ended", () => {
this.requestUpdate();
});
this._audioElement.addEventListener("canplaythrough", () => {
this._audioElement!.play();
});
}
private _stopTTS(): void {
if (this._audioElement) {
this._audioElement.pause();
this._audioElement.currentTime = 0;
this._audioElement = undefined;
this.requestUpdate();
}
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this._stopTTS();
}
static styles = css`

View File

@@ -280,10 +280,11 @@ ${type === "object"
.content.horizontal {
--code-mirror-max-height: calc(
100vh - var(--header-height) - (var(--ha-line-height-normal) * 3) -
(1em * 2) - (max(16px, var(--safe-area-inset-top)) * 2) -
100vh - var(--header-height) -
(var(--ha-line-height-normal) * var(--ha-font-size-m) * 3) -
(max(16px, var(--safe-area-inset-top)) * 2) -
(max(16px, var(--safe-area-inset-bottom)) * 2) -
(var(--ha-card-border-width, 1px) * 2) - 179px
(var(--ha-card-border-width, 1px) * 3) - (1em * 2) - 192px
);
}

View File

@@ -313,9 +313,14 @@ class HaPanelHistory extends LitElement {
return;
}
const statsStartDate = new Date(this._startDate);
// History uses the end datapoint of the statistic, so if we want the
// graph to start at 7AM, need to fetch the statistic from 6AM.
statsStartDate.setHours(statsStartDate.getHours() - 1);
const statistics = await fetchStatistics(
this.hass!,
this._startDate,
statsStartDate,
this._endDate,
statisticIds,
"hour",

View File

@@ -5,6 +5,7 @@ import {
generateEntityFilter,
type EntityFilter,
} from "../../../common/entity/entity_filter";
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
@@ -98,6 +99,7 @@ export class LightViewStrategy extends ReactiveElement {
floorCount > 1
? floor.name
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
icon: floor.icon || floorDefaultIcon(floor),
},
],
};

View File

@@ -52,6 +52,8 @@ export class HaLogbook extends LitElement {
@property({ attribute: false }) public deviceIds?: string[];
@property({ attribute: false }) public stateFilter?: string[];
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, reflect: true }) public virtualize = false;
@@ -165,7 +167,7 @@ export class HaLogbook extends LitElement {
protected willUpdate(changedProps: PropertyValues): void {
let changed = changedProps.has("time");
for (const key of ["entityIds", "deviceIds"]) {
for (const key of ["entityIds", "deviceIds", "stateFilter"]) {
if (!changedProps.has(key)) {
continue;
}
@@ -352,9 +354,19 @@ export class HaLogbook extends LitElement {
"recent" in this.time
? findStartOfRecentTime(new Date(), this.time.recent)
: undefined;
let eventsFiltered: LogbookEntry[] | undefined;
if (this.stateFilter && this.stateFilter.length > 0) {
eventsFiltered = streamMessage.events.filter(
(e) => e.state && this.stateFilter?.includes(e.state)
);
} else {
eventsFiltered = [...streamMessage.events];
}
// Put newest ones on top. Reverse works in-place so
// make a copy first.
const newEntries = [...streamMessage.events].reverse();
const newEntries = eventsFiltered.reverse();
if (!this._logbookEntries || !this._logbookEntries.length) {
this._logbookEntries = newEntries;
return;

View File

@@ -0,0 +1,126 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-number-buttons";
import { isUnavailableState } from "../../../data/entity";
import {
MediaPlayerEntityFeature,
type MediaPlayerEntity,
} from "../../../data/media-player";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
LovelaceCardFeatureContext,
MediaPlayerVolumeButtonsCardFeatureConfig,
} from "./types";
import { clamp } from "../../../common/number/clamp";
export const supportsMediaPlayerVolumeButtonsCardFeature = (
hass: HomeAssistant,
context: LovelaceCardFeatureContext
) => {
const stateObj = context.entity_id
? hass.states[context.entity_id]
: undefined;
if (!stateObj) return false;
const domain = computeDomain(stateObj.entity_id);
return (
domain === "media_player" &&
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET)
);
};
@customElement("hui-media-player-volume-buttons-card-feature")
class HuiMediaPlayerVolumeButtonsCardFeature
extends LitElement
implements LovelaceCardFeature
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: MediaPlayerVolumeButtonsCardFeatureConfig;
private get _stateObj() {
if (!this.hass || !this.context || !this.context.entity_id) {
return undefined;
}
return this.hass.states[this.context.entity_id] as
| MediaPlayerEntity
| undefined;
}
static getStubConfig(): MediaPlayerVolumeButtonsCardFeatureConfig {
return {
type: "media-player-volume-buttons",
step: 5,
};
}
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import(
"../editor/config-elements/hui-media-player-volume-buttons-card-feature-editor"
);
return document.createElement(
"hui-media-player-volume-buttons-card-feature-editor"
);
}
public setConfig(config: MediaPlayerVolumeButtonsCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
}
this._config = config;
}
protected render() {
if (
!this._config ||
!this.hass ||
!this.context ||
!this._stateObj ||
!supportsMediaPlayerVolumeButtonsCardFeature(this.hass, this.context)
) {
return nothing;
}
const position =
this._stateObj.attributes.volume_level != null
? Math.round(this._stateObj.attributes.volume_level * 100)
: undefined;
return html`
<ha-control-number-buttons
.disabled=${!this._stateObj || isUnavailableState(this._stateObj.state)}
.locale=${this.hass.locale}
min="0"
max="100"
.step=${this._config.step ?? 5}
.value=${position}
unit="%"
@value-changed=${this._valueChanged}
></ha-control-number-buttons>
`;
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
this.hass!.callService("media_player", "volume_set", {
entity_id: this._stateObj!.entity_id,
volume_level: clamp(ev.detail.value, 0, 100) / 100,
});
}
static get styles() {
return cardFeatureStyles;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-media-player-volume-buttons-card-feature": HuiMediaPlayerVolumeButtonsCardFeature;
}
}

View File

@@ -94,7 +94,7 @@ class HuiHistoryChartCardFeature
}
if (!this._coordinates) {
return html`
<div class="container">
<div class="container loading">
<ha-spinner size="small"></ha-spinner>
</div>
`;
@@ -153,6 +153,14 @@ class HuiHistoryChartCardFeature
align-items: flex-end;
pointer-events: none !important;
}
.container.loading {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
hui-graph-base {
width: 100%;
--accent-color: var(--feature-color);

View File

@@ -50,6 +50,11 @@ export interface MediaPlayerVolumeSliderCardFeatureConfig {
type: "media-player-volume-slider";
}
export interface MediaPlayerVolumeButtonsCardFeatureConfig {
type: "media-player-volume-buttons";
step?: number;
}
export interface FanDirectionCardFeatureConfig {
type: "fan-direction";
}
@@ -252,6 +257,7 @@ export type LovelaceCardFeatureConfig =
| LockCommandsCardFeatureConfig
| LockOpenDoorCardFeatureConfig
| MediaPlayerPlaybackCardFeatureConfig
| MediaPlayerVolumeButtonsCardFeatureConfig
| MediaPlayerVolumeSliderCardFeatureConfig
| NumericInputCardFeatureConfig
| SelectOptionsCardFeatureConfig

View File

@@ -35,6 +35,8 @@ import { measureTextWidth } from "../../../../util/text";
import "../../../../components/ha-icon-button";
import { storage } from "../../../../common/decorators/storage";
import { listenMediaQuery } from "../../../../common/dom/media_query";
import { getEnergyColor } from "./common/color";
import type { CustomLegendOption } from "../../../../components/chart/ha-chart-base";
@customElement("hui-energy-devices-graph-card")
export class HuiEnergyDevicesGraphCard
@@ -49,6 +51,8 @@ export class HuiEnergyDevicesGraphCard
@state() private _data?: EnergyData;
@state() private _legendData: NonNullable<CustomLegendOption["data"]> = [];
@state()
@storage({
key: "energy-devices-graph-chart-type",
@@ -57,6 +61,14 @@ export class HuiEnergyDevicesGraphCard
})
private _chartType: "bar" | "pie" = "bar";
@state()
@storage({
key: "energy-devices-pie-hidden-stats",
state: true,
subscribe: false,
})
private _hiddenStats: string[] = [];
@state() private _isMobile = false;
private _compoundStats: string[] = [];
@@ -121,10 +133,16 @@ export class HuiEnergyDevicesGraphCard
<ha-chart-base
.hass=${this.hass}
.data=${this._chartData}
.options=${this._createOptions(this._chartData, this._chartType)}
.height=${`${Math.max(300, (this._chartData[0]?.data?.length || 0) * 28 + 50)}px`}
@chart-click=${this._handleChartClick}
.options=${this._createOptions(
this._chartData,
this._chartType,
this._legendData
)}
.height=${`${Math.max(300, (this._legendData?.length || 0) * 28 + 50)}px`}
.extraComponents=${[PieChart]}
@chart-click=${this._handleChartClick}
@dataset-hidden=${this._datasetHidden}
@dataset-unhidden=${this._datasetUnhidden}
></ha-chart-base>
</div>
</ha-card>
@@ -145,7 +163,8 @@ export class HuiEnergyDevicesGraphCard
private _createOptions = memoizeOne(
(
data: (BarSeriesOption | PieSeriesOption)[],
chartType: "bar" | "pie"
chartType: "bar" | "pie",
legendData: typeof this._legendData
): ECOption => {
const options: ECOption = {
grid: {
@@ -161,6 +180,7 @@ export class HuiEnergyDevicesGraphCard
},
xAxis: { show: false },
yAxis: { show: false },
legend: { type: "custom", show: false },
};
if (chartType === "bar") {
options.xAxis = {
@@ -191,6 +211,18 @@ export class HuiEnergyDevicesGraphCard
),
},
};
} else {
options.legend = {
type: "custom",
show: true,
data: legendData,
selected: legendData
.filter((d) => d.id && this._hiddenStats.includes(d.id))
.reduce((acc, d) => {
acc[d.id!] = false;
return acc;
}, {}),
};
}
return options;
}
@@ -354,23 +386,12 @@ export class HuiEnergyDevicesGraphCard
}
});
chartData.sort((a: any, b: any) => b.value[0] - a.value[0]);
if (compareData) {
datasets[1].data = chartData.map((d) =>
chartDataCompare.find((d2) => (d2 as any).id === d.id)
) as typeof chartDataCompare;
}
datasets.forEach((dataset) => {
dataset.data!.length = Math.min(
this._config?.max_devices || Infinity,
dataset.data!.length
);
});
if (this._chartType === "pie") {
const { summedData } = getSummedData(energyData);
const { consumption } = computeConsumptionData(summedData);
const { summedData, compareSummedData } = getSummedData(energyData);
const { consumption, compareConsumption } = computeConsumptionData(
summedData,
compareSummedData
);
const totalUsed = consumption.total.used_total;
const showUntracked =
"from_grid" in summedData ||
@@ -380,6 +401,47 @@ export class HuiEnergyDevicesGraphCard
? totalUsed -
chartData.reduce((acc: number, d: any) => acc + d.value[0], 0)
: 0;
if (untracked > 0) {
const color = getEnergyColor(
computedStyle,
this.hass.themes.darkMode,
false,
false,
"--history-unknown-color"
);
chartData.push({
id: "untracked",
value: [untracked, "untracked"] as any,
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.untracked_consumption"
),
itemStyle: {
color: color + "7F",
borderColor: color,
},
});
if (compareData) {
const compareUntracked =
compareConsumption!.total.used_total -
chartDataCompare.reduce(
(acc: number, d: any) => acc + d.value[0],
0
);
if (compareUntracked > 0) {
chartDataCompare.push({
id: "untracked",
value: [compareUntracked, "untracked"] as any,
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.untracked_consumption"
),
itemStyle: {
color: color + "32",
borderColor: color + "7F",
},
});
}
}
}
datasets.push({
type: "pie",
radius: ["0%", compareData ? "30%" : "40%"],
@@ -401,17 +463,36 @@ export class HuiEnergyDevicesGraphCard
color: "rgba(0, 0, 0, 0)",
},
tooltip: {
formatter: () =>
untracked > 0
? this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_devices_graph.includes_untracked",
{ num: formatNumber(untracked, this.hass.locale) }
)
: "",
show: false,
},
});
}
chartData.sort((a: any, b: any) => b.value[0] - a.value[0]);
if (
this._config?.max_devices &&
chartData.length > this._config.max_devices
) {
chartData.splice(this._config.max_devices);
}
this._legendData = chartData.map((d) => ({
...d,
name: this._getDeviceName(d.name),
}));
// filter out hidden stats in place
for (let i = chartData.length - 1; i >= 0; i--) {
if (this._hiddenStats.includes((chartData[i] as any).id)) {
chartData.splice(i, 1);
}
}
if (compareData) {
datasets[1].data = chartData.map((d) =>
chartDataCompare.find((d2) => (d2 as any).id === d.id)
) as typeof chartDataCompare;
}
this._chartData = datasets;
await this.updateComplete;
}
@@ -440,6 +521,18 @@ export class HuiEnergyDevicesGraphCard
this._getStatistics(this._data!);
}
private _datasetHidden(ev: CustomEvent<{ id: string }>) {
this._hiddenStats = [...this._hiddenStats, ev.detail.id];
this._getStatistics(this._data!);
}
private _datasetUnhidden(ev: CustomEvent<{ id: string }>) {
this._hiddenStats = this._hiddenStats.filter(
(stat) => stat !== ev.detail.id
);
this._getStatistics(this._data!);
}
static styles = css`
.card-header {
display: flex;

View File

@@ -1,6 +1,6 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { DOMAINS_TOGGLE } from "../../../common/const";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeDomain } from "../../../common/entity/compute_domain";
@@ -20,9 +20,11 @@ import type {
import type {
LovelaceCard,
LovelaceCardEditor,
LovelaceGridOptions,
LovelaceHeaderFooter,
} from "../types";
import type { EntitiesCardConfig } from "./types";
import { haStyleScrollbar } from "../../../resources/styles";
export const computeShowHeaderToggle = <
T extends EntityConfig | LovelaceRowConfig,
@@ -75,6 +77,8 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
private _hass?: HomeAssistant;
@property({ attribute: false }) public layout?: string;
private _configEntities?: LovelaceRowConfig[];
private _showHeaderToggle?: boolean;
@@ -139,6 +143,14 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
return size;
}
public getGridOptions(): LovelaceGridOptions {
return {
columns: 12,
min_columns: 6,
min_rows: this._config?.title || this._showHeaderToggle ? 3 : 2,
};
}
public setConfig(config: EntitiesCardConfig): void {
if (!config.entities || !Array.isArray(config.entities)) {
throw new Error("Entities must be specified");
@@ -233,7 +245,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
`}
</h1>
`}
<div id="states" class="card-content">
<div id="states" class="card-content ha-scrollbar">
${this._configEntities!.map((entityConf) =>
this._renderEntity(entityConf)
)}
@@ -246,69 +258,73 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
`;
}
static styles = css`
ha-card {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.card-header {
display: flex;
justify-content: space-between;
}
static styles = [
haStyleScrollbar,
css`
ha-card {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.card-header {
display: flex;
justify-content: space-between;
}
.card-header .name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-header .name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#states {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--entities-card-row-gap, var(--card-row-gap, 8px));
}
#states {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--entities-card-row-gap, var(--card-row-gap, 8px));
overflow-y: auto;
}
#states > div > * {
overflow: clip visible;
}
#states > div > * {
overflow: clip visible;
}
#states > div {
position: relative;
}
#states > div {
position: relative;
}
.icon {
padding: 0px 18px 0px 8px;
}
.icon {
padding: 0px 18px 0px 8px;
}
.header {
border-top-left-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
border-top-right-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
margin-bottom: 16px;
overflow: hidden;
}
.header {
border-top-left-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
border-top-right-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
margin-bottom: 16px;
overflow: hidden;
}
.footer {
border-bottom-left-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
border-bottom-right-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
margin-top: -16px;
overflow: hidden;
}
`;
.footer {
border-bottom-left-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
border-bottom-right-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
margin-top: -16px;
overflow: hidden;
}
`,
];
private _renderEntity(entityConf: LovelaceRowConfig): TemplateResult {
const element = createRowElement(

View File

@@ -162,7 +162,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
private async _fetchStatistics(sensorNumericDeviceClasses: string[]) {
const now = new Date();
const start = new Date();
start.setHours(start.getHours() - this._hoursToShow);
start.setHours(start.getHours() - this._hoursToShow - 1);
const statistics = await fetchStatistics(
this.hass!,

View File

@@ -14,7 +14,11 @@ import { findEntities } from "../common/find-entities";
import { processConfigEntities } from "../common/process-config-entities";
import "../components/hui-warning";
import type { EntityConfig } from "../entity-rows/types";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type {
LovelaceCard,
LovelaceCardEditor,
LovelaceGridOptions,
} from "../types";
import type { LogbookCardConfig } from "./types";
import { resolveEntityIDs } from "../../../data/selector";
import { ensureArray } from "../../../common/array/ensure-array";
@@ -60,10 +64,21 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
@state() private _targetPickerValue: HassServiceTarget = {};
@state() private _stateFilter?: string[];
public getCardSize(): number {
return 9 + (this._config?.title ? 1 : 0);
}
public getGridOptions(): LovelaceGridOptions {
return {
rows: 6,
columns: 12,
min_columns: 6,
min_rows: this._config?.title ? 4 : 3,
};
}
public validateTarget(
config: LogbookCardConfig
): HassServiceTarget | undefined {
@@ -116,6 +131,8 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
};
this._targetPickerValue = target;
this._stateFilter = ensureArray(config.state_filter);
}
private _getEntityIds(): string[] | undefined {
@@ -189,9 +206,14 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
>
<div class="content">
<ha-logbook
class=${classMap({
"is-grid": this.layout === "grid",
"is-panel": this.layout === "panel",
})}
.hass=${this.hass}
.time=${this._time}
.entityIds=${this._getEntityIds()}
.stateFilter=${this._stateFilter}
narrow
relative-time
virtualize
@@ -212,6 +234,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
}
.content {
height: 100%;
padding: 0 16px 16px;
}
@@ -224,6 +247,11 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
display: block;
}
ha-logbook.is-grid,
ha-logbook.is-panel {
height: 100%;
}
:host([ispanel]) .content,
:host([ispanel]) ha-logbook {
height: 100%;

View File

@@ -563,7 +563,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
.name,
.attribute {
font-size: var(--ha-font-size-m);
line-height: 1;
line-height: var(--ha-line-height-condensed);
}
.name-state {
@@ -729,7 +729,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
.short .state,
.short .temp-attribute .temp {
font-size: 24px;
line-height: 1.25;
line-height: var(--ha-line-height-condensed);
}
.short .content + .forecast {

View File

@@ -345,6 +345,7 @@ export interface LogbookCardConfig extends LovelaceCardConfig {
title?: string;
hours_to_show?: number;
theme?: string;
state_filter?: string[];
}
interface GeoLocationSourceConfig {

View File

@@ -1,28 +1,29 @@
import type { HassEntity } from "home-assistant-js-websocket";
import {
DEFAULT_ENTITY_NAME,
type EntityNameItem,
} from "../../../../common/entity/compute_entity_name_display";
import type { HomeAssistant } from "../../../../types";
import { ensureArray } from "../../../../common/array/ensure-array";
import type { EntityNameItem } from "../../../../common/entity/compute_entity_name_display";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import type { HomeAssistant } from "../../../../types";
/**
* Computes the display name for an entity in Lovelace (cards and badges).
*
* @param hass - The Home Assistant instance
* @param stateObj - The entity state object
* @param nameConfig - The name configuration (string for override, or EntityNameItem[] for structured naming)
* @param config - The name configuration (string for override, or EntityNameItem[] for structured naming)
* @returns The computed entity name
*/
export const computeLovelaceEntityName = (
hass: HomeAssistant,
stateObj: HassEntity | undefined,
nameConfig: string | EntityNameItem | EntityNameItem[] | undefined
config: string | EntityNameItem | EntityNameItem[] | undefined
): string => {
if (typeof nameConfig === "string") {
return nameConfig;
// If no config is provided, fall back to the default state name
if (!config) {
return stateObj ? computeStateName(stateObj) : "";
}
if (typeof config === "string") {
return config;
}
const config = nameConfig || DEFAULT_ENTITY_NAME;
if (stateObj) {
return hass.formatEntityName(stateObj, config);
}

View File

@@ -23,6 +23,7 @@ import "../card-features/hui-light-color-temp-card-feature";
import "../card-features/hui-lock-commands-card-feature";
import "../card-features/hui-lock-open-door-card-feature";
import "../card-features/hui-media-player-playback-card-feature";
import "../card-features/hui-media-player-volume-buttons-card-feature";
import "../card-features/hui-media-player-volume-slider-card-feature";
import "../card-features/hui-numeric-input-card-feature";
import "../card-features/hui-select-options-card-feature";
@@ -72,6 +73,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
"lock-commands",
"lock-open-door",
"media-player-playback",
"media-player-volume-buttons",
"media-player-volume-slider",
"numeric-input",
"select-options",

View File

@@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { array, assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
@@ -65,9 +64,7 @@ export class HuiAlarmPanelCardEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -5,7 +5,6 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
@@ -73,7 +72,7 @@ export class HuiButtonCardEditor
{
name: "name",
selector: {
entity_name: { default_name: DEFAULT_ENTITY_NAME },
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -48,6 +48,7 @@ import { supportsLightColorTempCardFeature } from "../../card-features/hui-light
import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature";
import { supportsMediaPlayerPlaybackCardFeature } from "../../card-features/hui-media-player-playback-card-feature";
import { supportsMediaPlayerVolumeButtonsCardFeature } from "../../card-features/hui-media-player-volume-buttons-card-feature";
import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature";
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
@@ -102,6 +103,7 @@ const UI_FEATURE_TYPES = [
"lock-commands",
"lock-open-door",
"media-player-playback",
"media-player-volume-buttons",
"media-player-volume-slider",
"numeric-input",
"select-options",
@@ -131,6 +133,7 @@ const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
"fan-preset-modes",
"humidifier-modes",
"lawn-mower-commands",
"media-player-volume-buttons",
"numeric-input",
"select-options",
"trend-graph",
@@ -171,6 +174,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
"lock-commands": supportsLockCommandsCardFeature,
"lock-open-door": supportsLockOpenDoorCardFeature,
"media-player-playback": supportsMediaPlayerPlaybackCardFeature,
"media-player-volume-buttons": supportsMediaPlayerVolumeButtonsCardFeature,
"media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature,
"numeric-input": supportsNumericInputCardFeature,
"select-options": supportsSelectOptionsCardFeature,

View File

@@ -13,7 +13,6 @@ import {
string,
union,
} from "superstruct";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
@@ -86,9 +85,7 @@ export class HuiEntityBadgeEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -1,5 +1,4 @@
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import type { HaFormSchema } from "../../../../components/ha-form/types";
import { headerFooterConfigStructs } from "../../header-footer/structs";
@@ -26,9 +25,7 @@ const SCHEMA = [
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -14,7 +14,6 @@ import {
string,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import { NON_NUMERIC_ATTRIBUTES } from "../../../../data/entity_attributes";
@@ -102,9 +101,7 @@ export class HuiGaugeCardEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -14,7 +14,6 @@ import {
} from "superstruct";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type {
@@ -61,9 +60,7 @@ const SCHEMA = [
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -1,10 +1,9 @@
import { mdiGestureTap } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { mdiGestureTap } from "@mdi/js";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
@@ -37,9 +36,7 @@ const SCHEMA = [
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -10,6 +10,7 @@ import {
string,
} from "superstruct";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entities-picker";
import "../../../../components/ha-target-picker";
@@ -24,6 +25,7 @@ import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-logbook-card";
import { targetStruct } from "../../../../data/script";
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
import type { HaEntityPickerEntityFilterFunc } from "../../../../data/entity";
import { resolveEntityIDs } from "../../../../data/selector";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -33,6 +35,7 @@ const cardConfigStruct = assign(
hours_to_show: optional(number()),
theme: optional(string()),
target: optional(targetStruct),
state_filter: optional(array(string())),
})
);
@@ -50,6 +53,13 @@ const SCHEMA = [
},
],
},
{
name: "state_filter",
context: {
filter_entity: "context_entities",
},
selector: { state: { multiple: true } },
},
] as const;
@customElement("hui-logbook-card-editor")
@@ -106,7 +116,13 @@ export class HuiLogbookCardEditor
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.data=${this._data(
this._config,
this._targetPicker,
this.hass.entities,
this.hass.devices,
this.hass.areas
)}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
@@ -122,6 +138,25 @@ export class HuiLogbookCardEditor
`;
}
private _data = memoizeOne(
(
config: LogbookCardConfig,
target: HassServiceTarget,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"]
) => ({
...config,
context_entities: resolveEntityIDs(
this.hass!,
target,
entities,
devices,
areas
),
})
);
private _filterFunc: HaEntityPickerEntityFilterFunc = (entity) =>
filterLogbookCompatibleEntities(entity, this._sensorNumericDeviceClasses);
@@ -131,7 +166,9 @@ export class HuiLogbookCardEditor
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
const newConfig = { ...ev.detail.value };
delete newConfig.context_entities;
fireEvent(this, "config-changed", { config: newConfig });
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
@@ -142,6 +179,10 @@ export class HuiLogbookCardEditor
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})`;
case "state_filter":
return this.hass!.localize(
"ui.panel.lovelace.editor.card.logbook.state_filter"
);
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`

View File

@@ -2,7 +2,6 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
@@ -33,9 +32,7 @@ const SCHEMA = [
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -0,0 +1,86 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type {
LovelaceCardFeatureContext,
MediaPlayerVolumeButtonsCardFeatureConfig,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
@customElement("hui-media-player-volume-buttons-card-feature-editor")
export class HuiMediaPlayerVolumeButtonsCardFeatureEditor
extends LitElement
implements LovelaceCardFeatureEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: MediaPlayerVolumeButtonsCardFeatureConfig;
public setConfig(config: MediaPlayerVolumeButtonsCardFeatureConfig): void {
this._config = config;
}
private _schema = memoizeOne(
() =>
[
{
name: "step",
selector: {
number: {
mode: "slider",
step: 1,
min: 1,
max: 100,
unit_of_measurement: "%",
},
},
},
] as const
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const data: MediaPlayerVolumeButtonsCardFeatureConfig = {
type: "media-player-volume-buttons",
step: this._config.step ?? 5,
};
const schema = this._schema();
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) =>
this.hass!.localize(
`ui.panel.lovelace.editor.features.types.media-player-volume-buttons.${schema.name}`
);
}
declare global {
interface HTMLElementTagNameMap {
"hui-media-player-volume-buttons-card-feature-editor": HuiMediaPlayerVolumeButtonsCardFeatureEditor;
}
}

View File

@@ -1,8 +1,8 @@
import memoizeOne from "memoize-one";
import { mdiGestureTap } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
assert,
assign,
@@ -15,7 +15,6 @@ import {
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type {
@@ -71,9 +70,7 @@ export class HuiPictureEntityCardEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -2,7 +2,6 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
@@ -25,9 +24,7 @@ const SCHEMA = [
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -1,7 +1,7 @@
import memoizeOne from "memoize-one";
import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
assert,
assign,
@@ -12,18 +12,17 @@ import {
string,
union,
} from "superstruct";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-sensor-card";
import type { SensorCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import { configElementStyle } from "./config-elements-style";
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-sensor-card";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -71,9 +70,7 @@ export class HuiSensorCardEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -14,7 +14,7 @@ import {
} from "superstruct";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import { computeDomain } from "../../../../common/entity/compute_domain";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type {
@@ -35,7 +35,6 @@ import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor";
import type { FeatureType } from "./hui-card-features-editor";
import { computeDomain } from "../../../../common/entity/compute_domain";
const COMPATIBLE_FEATURES_TYPES: Record<string, FeatureType[]> = {
climate: [
@@ -89,9 +88,7 @@ export class HuiThermostatCardEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -16,7 +16,6 @@ import {
} from "superstruct";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { orderProperties } from "../../../../common/util/order-properties";
import "../../../../components/ha-expansion-panel";
@@ -102,9 +101,7 @@ export class HuiTileCardEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -12,7 +12,6 @@ import {
string,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
@@ -153,9 +152,7 @@ export class HuiWeatherForecastCardEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -13,7 +13,6 @@ import {
union,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
@@ -94,9 +93,7 @@ export class HuiHeadingEntityEditor
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
entity_name: {},
},
context: { entity: "entity" },
},

View File

@@ -1,11 +1,18 @@
import memoizeOne from "memoize-one";
import { LitElement, css, html, nothing } from "lit";
import type { PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import {
isMediaSourceContentId,
resolveMediaSource,
} from "../../../../data/media_source";
@customElement("hui-view-background-editor")
export class HuiViewBackgroundEditor extends LitElement {
@@ -13,6 +20,8 @@ export class HuiViewBackgroundEditor extends LitElement {
@state() private _config!: LovelaceViewConfig;
@state({ attribute: false }) private _resolvedImage?: string;
set config(config: LovelaceViewConfig) {
this._config = config;
}
@@ -20,133 +29,195 @@ export class HuiViewBackgroundEditor extends LitElement {
private _localizeValueCallback = (key: string) =>
this.hass.localize(key as any);
private _schema = memoizeOne((showSettings: boolean) => [
{
name: "image",
selector: { background: { original: true } },
},
...(showSettings
? ([
{
name: "settings",
flatten: true,
expanded: true,
type: "expandable" as const,
schema: [
{
name: "opacity",
selector: {
number: { min: 0, max: 100, mode: "slider", step: 10 },
},
},
{
name: "attachment",
selector: {
button_toggle: {
translation_key:
"ui.panel.lovelace.editor.edit_view.background.attachment",
options: ["scroll", "fixed"],
},
},
},
{
name: "size",
required: true,
selector: {
select: {
translation_key:
"ui.panel.lovelace.editor.edit_view.background.size",
options: ["auto", "cover", "contain"],
mode: "dropdown",
},
},
},
{
name: "alignment",
required: true,
selector: {
select: {
translation_key:
"ui.panel.lovelace.editor.edit_view.background.alignment",
options: [
"top left",
"top center",
"top right",
"center left",
"center",
"center right",
"bottom left",
"bottom center",
"bottom right",
],
mode: "dropdown",
},
},
},
{
name: "repeat",
required: true,
selector: {
select: {
translation_key:
"ui.panel.lovelace.editor.edit_view.background.repeat",
options: ["repeat", "no-repeat"],
mode: "dropdown",
},
},
},
],
private _schema = memoizeOne(
(localize: LocalizeFunc, showSettings: boolean) =>
[
{
name: "image",
selector: {
media: {
accept: ["image/*"] as string[],
clearable: true,
image_upload: true,
hide_content_type: true,
content_id_helper: localize(
"ui.panel.lovelace.editor.card.picture.content_id_helper"
),
},
},
] as const)
: []),
]);
},
...(showSettings
? ([
{
name: "settings",
flatten: true,
expanded: true,
type: "expandable" as const,
schema: [
{
name: "opacity",
selector: {
number: { min: 0, max: 100, mode: "slider", step: 10 },
},
},
{
name: "attachment",
selector: {
button_toggle: {
translation_key:
"ui.panel.lovelace.editor.edit_view.background.attachment",
options: ["scroll", "fixed"],
},
},
},
{
name: "size",
required: true,
selector: {
select: {
translation_key:
"ui.panel.lovelace.editor.edit_view.background.size",
options: ["auto", "cover", "contain"],
mode: "dropdown",
},
},
},
{
name: "alignment",
required: true,
selector: {
select: {
translation_key:
"ui.panel.lovelace.editor.edit_view.background.alignment",
options: [
"top left",
"top center",
"top right",
"center left",
"center",
"center right",
"bottom left",
"bottom center",
"bottom right",
],
mode: "dropdown",
},
},
},
{
name: "repeat",
required: true,
selector: {
select: {
translation_key:
"ui.panel.lovelace.editor.edit_view.background.repeat",
options: ["repeat", "no-repeat"],
mode: "dropdown",
},
},
},
],
},
] as const)
: []),
] as const
);
protected updated(changedProps: PropertyValues) {
if (
this._config &&
this.hass &&
(changedProps.has("_config") ||
(changedProps.has("hass") && !changedProps.get("hass")))
) {
const background = this._backgroundData(this._config);
this.style.setProperty(
"--picture-opacity",
`${(background.opacity ?? 100) / 100}`
);
const backgroundImage =
typeof background.image === "object"
? background.image.media_content_id
: background.image;
if (backgroundImage && isMediaSourceContentId(backgroundImage)) {
resolveMediaSource(this.hass, backgroundImage).then((result) => {
this._resolvedImage = result.url;
});
} else {
this._resolvedImage = backgroundImage;
}
}
}
protected render() {
if (!this.hass) {
return nothing;
}
let background = this._config?.background;
if (typeof background === "string") {
const backgroundUrl = background.match(/url\(['"]?([^'"]+)['"]?\)/)?.[1];
background = {
image: backgroundUrl,
};
}
if (!background) {
background = {
opacity: 33,
alignment: "center",
size: "cover",
repeat: "repeat",
attachment: "fixed",
};
} else {
background = {
opacity: 100,
alignment: "center",
size: "cover",
repeat: "no-repeat",
attachment: "scroll",
...background,
};
}
const background = this._backgroundData(this._config);
return html`
${this._resolvedImage
? html`<div class="previewContainer">
<img
src=${this._resolvedImage}
alt=${this.hass.localize(
"ui.components.picture-upload.current_image_alt"
)}
/>
</div>`
: nothing}
<ha-form
.hass=${this.hass}
.data=${background}
.schema=${this._schema(true)}
.schema=${this._schema(this.hass.localize, true)}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
.localizeValue=${this._localizeValueCallback}
style=${`--picture-opacity: ${(background.opacity ?? 100) / 100};`}
></ha-form>
`;
}
private _backgroundData = memoizeOne(
(backgroundConfig?: LovelaceViewConfig) => {
let background = backgroundConfig?.background;
if (typeof background === "string") {
const backgroundUrl = background.match(
/url\(['"]?([^'"]+)['"]?\)/
)?.[1];
background = {
image: backgroundUrl,
};
}
if (!background) {
background = {
opacity: 33,
alignment: "center",
size: "cover",
repeat: "repeat",
attachment: "fixed",
};
} else {
background = {
opacity: 100,
alignment: "center",
size: "cover",
repeat: "no-repeat",
attachment: "scroll",
...background,
...(typeof background.image === "string"
? { image: { media_content_id: background.image } }
: {}),
};
}
return background;
}
);
private _valueChanged(ev: CustomEvent): void {
const config = {
...this._config,
@@ -195,6 +266,23 @@ export class HuiViewBackgroundEditor extends LitElement {
display: block;
--file-upload-image-border-radius: var(--ha-border-radius-sm);
}
.previewContainer {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
img {
max-width: 100%;
max-height: 200px;
margin-bottom: 4px;
border-radius: var(--file-upload-image-border-radius);
transition: opacity 0.3s;
opacity: var(--picture-opacity, 1);
}
img:hover {
opacity: 1;
}
`;
}

View File

@@ -168,9 +168,17 @@ export class HomeAreaViewStrategy extends ReactiveElement {
const summaryEntities = Object.values(entitiesBySummary).flat();
// Automations section
const automationFilter = generateEntityFilter(hass, {
domain: "automation",
entity_category: "none",
});
const automations = areaEntities.filter(automationFilter);
// Rest of entities grouped by device
const otherEntities = areaEntities.filter(
(entityId) => !summaryEntities.includes(entityId)
(entityId) =>
!summaryEntities.includes(entityId) && !automations.includes(entityId)
);
const entitiesByDevice: Record<string, string[]> = {};
@@ -295,6 +303,21 @@ export class HomeAreaViewStrategy extends ReactiveElement {
sections.push(...deviceSections);
}
// Show automations last, if they exist
if (automations.length > 0) {
sections.push({
type: "grid",
cards: [
computeHeadingCard(
hass.localize("ui.panel.lovelace.strategy.home.automations"),
"mdi:robot",
"/config/automation/dashboard"
),
...automations.map(computeTileCard),
],
});
}
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
const maxColumns = clamp(sections.length, 2, 3);

View File

@@ -21,6 +21,7 @@ import type {
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
import { getHomeStructure } from "./helpers/home-structure";
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
export interface HomeMainViewStrategyConfig {
type: "home-main";
@@ -92,6 +93,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
? floor.name
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
heading_style: "title",
icon: floor.icon || floorDefaultIcon(floor),
},
...cards,
],
@@ -215,7 +217,9 @@ export class HomeMainViewStrategy extends ReactiveElement {
column_span: maxColumns,
cards: [],
};
const weatherEntity = Object.keys(hass.states).find(weatherFilter);
const weatherEntity = Object.keys(hass.states)
.filter(weatherFilter)
.sort()[0];
if (weatherEntity) {
widgetSection.cards!.push(

View File

@@ -4,6 +4,7 @@ import {
findEntities,
generateEntityFilter,
} from "../../../../common/entity/entity_filter";
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
@@ -96,6 +97,7 @@ export class HomeMMediaPlayersViewStrategy extends ReactiveElement {
floorCount > 1
? floor.name
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
icon: floor.icon || floorDefaultIcon(floor),
},
],
};

View File

@@ -27,7 +27,9 @@ export class HUIViewBackground extends LitElement {
const backgroundImage =
typeof this.background === "string"
? this.background
: this.background?.image;
: typeof this.background?.image === "object"
? this.background.image.media_content_id
: this.background?.image;
if (backgroundImage && isMediaSourceContentId(backgroundImage)) {
resolveMediaSource(this.hass, backgroundImage).then((result) => {
@@ -73,13 +75,17 @@ export class HUIViewBackground extends LitElement {
background?: string | LovelaceViewBackgroundConfig
) {
if (typeof background === "object" && background.image) {
if (isMediaSourceContentId(background.image) && !this.resolvedImage) {
const image =
typeof background.image === "object"
? background.image.media_content_id || ""
: background.image;
if (isMediaSourceContentId(image) && !this.resolvedImage) {
return null;
}
const alignment = background.alignment ?? "center";
const size = background.size ?? "cover";
const repeat = background.repeat ?? "no-repeat";
return `${alignment} / ${size} ${repeat} url('${this.hass.hassUrl(this.resolvedImage || background.image)}')`;
return `${alignment} / ${size} ${repeat} url('${this.hass.hassUrl(this.resolvedImage || image)}')`;
}
if (typeof background === "string") {
if (isMediaSourceContentId(background) && !this.resolvedImage) {

View File

@@ -5,6 +5,7 @@ import {
generateEntityFilter,
type EntityFilter,
} from "../../../common/entity/entity_filter";
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
@@ -140,6 +141,7 @@ export class SafetyViewStrategy extends ReactiveElement {
floorCount > 1
? floor.name
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
icon: floor.icon || floorDefaultIcon(floor),
},
],
};

View File

@@ -255,6 +255,10 @@ class DialogTodoItemEditor extends LitElement {
// Parse a date in the browser timezone
private _parseDate(dateStr: string): Date {
// If it's a date-only string (no 'T'), parse as midnight in browser time to avoid offset issues
if (!dateStr.includes("T")) {
return new Date(dateStr + "T00:00:00");
}
const tzDate = new TZDate(dateStr, this._timeZone!);
return new Date(tzDate.getTime());
}

View File

@@ -758,6 +758,7 @@
},
"language-picker": {
"language": "Language",
"no_match": "No matching languages found",
"no_languages": "No languages available"
},
"tts-picker": {
@@ -3944,7 +3945,6 @@
"add": "Add trigger",
"empty_search": "No triggers found for {term}",
"id": "Trigger ID",
"id_helper": "Helps identify each run based on which trigger fired.",
"optional": "Optional",
"edit_id": "Edit ID",
"duplicate": "[%key:ui::common::duplicate%]",
@@ -6942,7 +6942,8 @@
"areas": "Areas",
"other_areas": "Other areas",
"unamed_device": "Unnamed device",
"others": "Others"
"others": "Others",
"automations": "Automations"
},
"common_controls": {
"not_loaded": "Usage Prediction integration is not loaded.",
@@ -7074,10 +7075,10 @@
"energy_devices_graph": {
"energy_usage": "Energy usage",
"previous_energy_usage": "Previous energy usage",
"total_energy_usage": "Total energy usage",
"total_energy_usage": "Total",
"change_chart_type": "Change chart type",
"untracked": "untracked",
"includes_untracked": "Includes {num} kWh of untracked energy"
"untracked_consumption": "Untracked consumption",
"untracked": "untracked"
},
"energy_devices_detail_graph": {
"untracked_consumption": "Untracked consumption",
@@ -7650,7 +7651,8 @@
},
"logbook": {
"name": "Activity",
"description": "The Activity card shows a list of events for entities."
"description": "The Activity card shows a list of events for entities.",
"state_filter": "State filter"
},
"history-graph": {
"name": "History graph",
@@ -8153,6 +8155,10 @@
"media-player-playback": {
"label": "Media player playback controls"
},
"media-player-volume-buttons": {
"label": "Media player volume buttons",
"step": "Step size"
},
"media-player-volume-slider": {
"label": "Media player volume slider"
},

View File

@@ -0,0 +1,71 @@
import { expect, test } from "vitest";
import { TZDate } from "@date-fns/tz";
import { isDate } from "../../../src/common/string/is_date";
/**
* These tests verify that all-day event dates are correctly identified
* and can be distinguished from datetime strings. This is critical for
* proper date display in the calendar event detail dialog.
*/
test("isDate correctly identifies date-only strings", () => {
// Valid date-only strings (all-day events)
expect(isDate("2025-10-10")).toBe(true);
expect(isDate("2007-06-28")).toBe(true);
expect(isDate("2025-12-31")).toBe(true);
// DateTime strings should not be identified as dates
expect(isDate("2025-10-10T00:00:00")).toBe(false);
expect(isDate("2025-10-10T14:30:00")).toBe(false);
expect(isDate("2025-10-10T14:30:00Z")).toBe(false);
expect(isDate("2025-10-10T14:30:00+00:00")).toBe(false);
expect(isDate("2025-10-10T14:30:00-08:00")).toBe(false);
});
test("Date parsing for all-day events", () => {
// Verify that date-only strings can be parsed as local dates
const dateStr = "2025-10-10";
const parsed = new Date(dateStr + "T00:00:00");
expect(parsed.getFullYear()).toBe(2025);
expect(parsed.getMonth()).toBe(9); // October (0-indexed)
expect(parsed.getDate()).toBe(10);
});
test("Timed events respect timezone conversion", () => {
// Verify that datetime strings with timezone info are properly converted with TZDate
const datetimeStr = "2025-10-10T14:30:00-07:00"; // 2:30 PM Pacific time
const timeZone = "America/Los_Angeles"; // UTC-7 (PDT) in October
// This should NOT be identified as a date-only string
expect(isDate(datetimeStr)).toBe(false);
// Timed events should use TZDate which respects timezone
const tzDate = new TZDate(datetimeStr, timeZone);
// The date should be October 10, 2:30 PM in LA timezone
expect(tzDate.getFullYear()).toBe(2025);
expect(tzDate.getMonth()).toBe(9); // October (0-indexed)
expect(tzDate.getDate()).toBe(10);
expect(tzDate.getHours()).toBe(14);
expect(tzDate.getMinutes()).toBe(30);
});
test("Timed events display different day due to timezone offset", () => {
// An event at 1 AM UTC on October 10 should display as October 9 in Pacific time
const utcDatetimeStr = "2025-10-10T01:00:00Z";
const timeZone = "America/Los_Angeles"; // UTC-7 (PDT) in October
// This should NOT be identified as a date-only string
expect(isDate(utcDatetimeStr)).toBe(false);
// Parse the UTC datetime in Pacific timezone
const tzDate = new TZDate(utcDatetimeStr, timeZone);
// Due to the -7 hour offset, 1 AM UTC becomes 6 PM on the previous day in Pacific
expect(tzDate.getFullYear()).toBe(2025);
expect(tzDate.getMonth()).toBe(9); // October (0-indexed)
expect(tzDate.getDate()).toBe(9); // Previous day
expect(tzDate.getHours()).toBe(18); // 6 PM
expect(tzDate.getMinutes()).toBe(0);
});

View File

@@ -1,5 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { DEFAULT_ENTITY_NAME } from "../../../../../src/common/entity/compute_entity_name_display";
import { computeLovelaceEntityName } from "../../../../../src/panels/lovelace/common/entity/compute-lovelace-entity-name";
import type { HomeAssistant } from "../../../../../src/types";
import { mockStateObj } from "../../../../common/entity/context/context-mock";
@@ -23,30 +22,32 @@ describe("computeLovelaceEntityName", () => {
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("returns empty string when nameConfig is empty string", () => {
it("return state name when nameConfig is empty string", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const stateObj = mockStateObj({
entity_id: "light.kitchen",
attributes: { friendly_name: "Kitchen Light" },
});
const result = computeLovelaceEntityName(hass, stateObj, "");
expect(result).toBe("");
expect(result).toBe("Kitchen Light");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("calls formatEntityName with DEFAULT_ENTITY_NAME when nameConfig is undefined", () => {
it("return state name when nameConfig is undefined", () => {
const mockFormatEntityName = vi.fn(() => "Formatted Name");
const hass = createMockHass(mockFormatEntityName);
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const stateObj = mockStateObj({
entity_id: "light.kitchen",
attributes: { friendly_name: "Kitchen Light" },
});
const result = computeLovelaceEntityName(hass, stateObj, undefined);
expect(result).toBe("Formatted Name");
expect(mockFormatEntityName).toHaveBeenCalledTimes(1);
expect(mockFormatEntityName).toHaveBeenCalledWith(
stateObj,
DEFAULT_ENTITY_NAME
);
expect(result).toBe("Kitchen Light");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("calls formatEntityName with EntityNameItem config", () => {

515
yarn.lock
View File

@@ -73,46 +73,46 @@ __metadata:
languageName: node
linkType: hard
"@babel/compat-data@npm:^7.27.2, @babel/compat-data@npm:^7.27.7, @babel/compat-data@npm:^7.28.0":
version: 7.28.4
resolution: "@babel/compat-data@npm:7.28.4"
checksum: 10/95b7864e6b210c84c069743966da448c0cb50015a4de5e18dd755776a0b5e53c4653e74f26700aed8de922eaa3b8844fc5fc5b29bc64830249d2abe914aec832
"@babel/compat-data@npm:^7.27.2, @babel/compat-data@npm:^7.27.7, @babel/compat-data@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/compat-data@npm:7.28.5"
checksum: 10/5a5ff00b187049e847f04bd02e21fbd8094544e5016195c2b45e56fa2e311eeb925b158f52a85624c9e6bacc1ce0323e26c303513723d918a8034e347e22610d
languageName: node
linkType: hard
"@babel/core@npm:7.28.4, @babel/core@npm:^7.24.4":
version: 7.28.4
resolution: "@babel/core@npm:7.28.4"
"@babel/core@npm:7.28.5, @babel/core@npm:^7.24.4":
version: 7.28.5
resolution: "@babel/core@npm:7.28.5"
dependencies:
"@babel/code-frame": "npm:^7.27.1"
"@babel/generator": "npm:^7.28.3"
"@babel/generator": "npm:^7.28.5"
"@babel/helper-compilation-targets": "npm:^7.27.2"
"@babel/helper-module-transforms": "npm:^7.28.3"
"@babel/helpers": "npm:^7.28.4"
"@babel/parser": "npm:^7.28.4"
"@babel/parser": "npm:^7.28.5"
"@babel/template": "npm:^7.27.2"
"@babel/traverse": "npm:^7.28.4"
"@babel/types": "npm:^7.28.4"
"@babel/traverse": "npm:^7.28.5"
"@babel/types": "npm:^7.28.5"
"@jridgewell/remapping": "npm:^2.3.5"
convert-source-map: "npm:^2.0.0"
debug: "npm:^4.1.0"
gensync: "npm:^1.0.0-beta.2"
json5: "npm:^2.2.3"
semver: "npm:^6.3.1"
checksum: 10/0593295241fac9be567145ef16f3858d34fc91390a9438c6d47476be9823af4cc0488c851c59702dd46b968e9fd46d17ddf0105ea30195ca85f5a66b4044c519
checksum: 10/2f1e224125179f423f4300d605a0c5a3ef315003281a63b1744405b2605ee2a2ffc5b1a8349aa4f262c72eca31c7e1802377ee04ad2b852a2c88f8ace6cac324
languageName: node
linkType: hard
"@babel/generator@npm:^7.28.3":
version: 7.28.3
resolution: "@babel/generator@npm:7.28.3"
"@babel/generator@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/generator@npm:7.28.5"
dependencies:
"@babel/parser": "npm:^7.28.3"
"@babel/types": "npm:^7.28.2"
"@babel/parser": "npm:^7.28.5"
"@babel/types": "npm:^7.28.5"
"@jridgewell/gen-mapping": "npm:^0.3.12"
"@jridgewell/trace-mapping": "npm:^0.3.28"
jsesc: "npm:^3.0.2"
checksum: 10/d00d1e6b51059e47594aab7920b88ec6fcef6489954a9172235ab57ad2e91b39c95376963a6e2e4cc7e8b88fa4f931018f71f9ab32bbc9c0bc0de35a0231f26c
checksum: 10/ae618f0a17a6d76c3983e1fd5d9c2f5fdc07703a119efdb813a7d9b8ad4be0a07d4c6f0d718440d2de01a68e321f64e2d63c77fc5d43ae47ae143746ef28ac1f
languageName: node
linkType: hard
@@ -282,10 +282,10 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-validator-identifier@npm:^7.25.9, @babel/helper-validator-identifier@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/helper-validator-identifier@npm:7.27.1"
checksum: 10/75041904d21bdc0cd3b07a8ac90b11d64cd3c881e89cb936fa80edd734bf23c35e6bd1312611e8574c4eab1f3af0f63e8a5894f4699e9cfdf70c06fcf4252320
"@babel/helper-validator-identifier@npm:^7.25.9, @babel/helper-validator-identifier@npm:^7.27.1, @babel/helper-validator-identifier@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/helper-validator-identifier@npm:7.28.5"
checksum: 10/8e5d9b0133702cfacc7f368bf792f0f8ac0483794877c6dca5fcb73810ee138e27527701826fb58a40a004f3a5ec0a2f3c3dd5e326d262530b119918f3132ba7
languageName: node
linkType: hard
@@ -317,26 +317,26 @@ __metadata:
languageName: node
linkType: hard
"@babel/parser@npm:^7.23.5, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4":
version: 7.28.4
resolution: "@babel/parser@npm:7.28.4"
"@babel/parser@npm:^7.23.5, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/parser@npm:7.28.5"
dependencies:
"@babel/types": "npm:^7.28.4"
"@babel/types": "npm:^7.28.5"
bin:
parser: ./bin/babel-parser.js
checksum: 10/f54c46213ef180b149f6a17ea765bf40acc1aebe2009f594e2a283aec69a190c6dda1fdf24c61a258dbeb903abb8ffb7a28f1a378f8ab5d333846ce7b7e23bf1
checksum: 10/8d9bfb437af6c97a7f6351840b9ac06b4529ba79d6d3def24d6c2996ab38ff7f1f9d301e868ca84a93a3050fadb3d09dbc5105b24634cd281671ac11eebe8df7
languageName: node
linkType: hard
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.27.1"
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.28.5"
dependencies:
"@babel/helper-plugin-utils": "npm:^7.27.1"
"@babel/traverse": "npm:^7.27.1"
"@babel/traverse": "npm:^7.28.5"
peerDependencies:
"@babel/core": ^7.0.0
checksum: 10/fe65257d5b82558bc6bc0f3a5a7a35b4166f71bed3747714dafb6360fadb15f036d568bc1fbeedae819165008c8feb646633ab91c0e3a95284963972f4fa9751
checksum: 10/750de98b34e6d09b545ded6e635b43cbab02fe319622964175259b98f41b16052e5931c4fbd45bad8cd0a37ebdd381233edecec9ee395b8ec51f47f47d1dbcd4
languageName: node
linkType: hard
@@ -478,14 +478,14 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-block-scoping@npm:^7.28.0":
version: 7.28.4
resolution: "@babel/plugin-transform-block-scoping@npm:7.28.4"
"@babel/plugin-transform-block-scoping@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/plugin-transform-block-scoping@npm:7.28.5"
dependencies:
"@babel/helper-plugin-utils": "npm:^7.27.1"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10/0848c681b0229ebb98da8a1fab53a29a94f79c4b80e536cb00dcedc08ca29341a48ebdf34d846f4d738376aa8e36830fa7f444bae3e85c8761cab96e9ad72a0f
checksum: 10/4b695360ede8472262111efb9d5c35b515767e1ead9e272c3e9799235e3f5feeb21d99a66bb23acbba9424465d13e7695a22a22a680c4aa558702ef8aad461d6
languageName: node
linkType: hard
@@ -513,7 +513,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-classes@npm:^7.28.3":
"@babel/plugin-transform-classes@npm:^7.28.4":
version: 7.28.4
resolution: "@babel/plugin-transform-classes@npm:7.28.4"
dependencies:
@@ -541,15 +541,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-destructuring@npm:^7.28.0":
version: 7.28.0
resolution: "@babel/plugin-transform-destructuring@npm:7.28.0"
"@babel/plugin-transform-destructuring@npm:^7.28.0, @babel/plugin-transform-destructuring@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/plugin-transform-destructuring@npm:7.28.5"
dependencies:
"@babel/helper-plugin-utils": "npm:^7.27.1"
"@babel/traverse": "npm:^7.28.0"
"@babel/traverse": "npm:^7.28.5"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10/cddab2520ff32d18005670fc6646396a253d3811d1ccc49f6f858469f05985ee896c346a0cb34d1cf25155c9be76d1068ff878cf8e8459bd3fa27513ec5a6802
checksum: 10/9cc67d3377bc5d8063599f2eb4588f5f9a8ab3abc9b64a40c24501fb3c1f91f4d5cf281ea9f208fd6b2ef8d9d8b018dacf1bed9493334577c966cd32370a7036
languageName: node
linkType: hard
@@ -611,14 +611,14 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-exponentiation-operator@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.27.1"
"@babel/plugin-transform-exponentiation-operator@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.28.5"
dependencies:
"@babel/helper-plugin-utils": "npm:^7.27.1"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10/dbbedd24724c2d590ef59d32cb1fef34e99daba41c5b621f9f4c4da23e15c2bb4b1e3d954c314645016391404cf00f1e4ddec8f1f7891438bcde9aaf16e16ee0
checksum: 10/da9bb5acd35c9fba92b802641f9462b82334158a149c78a739a04576a1e62be41057a201a41c022dda263bb73ac1a26521bbc997c7fc067f54d487af297995f4
languageName: node
linkType: hard
@@ -680,14 +680,14 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-logical-assignment-operators@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.27.1"
"@babel/plugin-transform-logical-assignment-operators@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.28.5"
dependencies:
"@babel/helper-plugin-utils": "npm:^7.27.1"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10/2757955d81d65cc4701c17b83720745f6858f7a1d1d58117e379c204f47adbeb066b778596b6168bdbf4a22c229aab595d79a9abc261d0c6bfd62d4419466e73
checksum: 10/c76778f4b186cc4f0b7e3658d91c690678bdb2b9d032f189213016d6177f2564709b79b386523b022b7d52e52331fd91f280f7c7bf85d835e0758b4b0d371447
languageName: node
linkType: hard
@@ -726,17 +726,17 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-modules-systemjs@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/plugin-transform-modules-systemjs@npm:7.27.1"
"@babel/plugin-transform-modules-systemjs@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/plugin-transform-modules-systemjs@npm:7.28.5"
dependencies:
"@babel/helper-module-transforms": "npm:^7.27.1"
"@babel/helper-module-transforms": "npm:^7.28.3"
"@babel/helper-plugin-utils": "npm:^7.27.1"
"@babel/helper-validator-identifier": "npm:^7.27.1"
"@babel/traverse": "npm:^7.27.1"
"@babel/helper-validator-identifier": "npm:^7.28.5"
"@babel/traverse": "npm:^7.28.5"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10/06d7bf76ac4688a36ae8e8d2dde1c3b8bab4594362132b74a00d5a32e6716944d68911b9bc53df60e59f4f9c7f1796525503ce3e3eed42f842d7775ccdfd836e
checksum: 10/1b91b4848845eaf6e21663d97a2a6c896553b127deaf3c2e9a2a4f041249277d13ebf71fd42d0ecbc4385e9f76093eff592fe0da0dcf1401b3f38c1615d8c539
languageName: node
linkType: hard
@@ -797,7 +797,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-object-rest-spread@npm:^7.28.0":
"@babel/plugin-transform-object-rest-spread@npm:^7.28.4":
version: 7.28.4
resolution: "@babel/plugin-transform-object-rest-spread@npm:7.28.4"
dependencies:
@@ -835,15 +835,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-optional-chaining@npm:^7.27.1":
version: 7.27.1
resolution: "@babel/plugin-transform-optional-chaining@npm:7.27.1"
"@babel/plugin-transform-optional-chaining@npm:^7.27.1, @babel/plugin-transform-optional-chaining@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/plugin-transform-optional-chaining@npm:7.28.5"
dependencies:
"@babel/helper-plugin-utils": "npm:^7.27.1"
"@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10/34b0f96400c259a2722740d17a001fe45f78d8ff052c40e29db2e79173be72c1cfe8d9681067e3f5da3989e4a557402df5c982c024c18257587a41e022f95640
checksum: 10/0bc900bff66d5acc13b057107eaeb6084b4cb0b124654d35b103f71f292d33dba5beac444ab4f92528583585b6e0cf34d64ce9cbb473b15d22375a4a6ed3cbac
languageName: node
linkType: hard
@@ -894,7 +894,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-regenerator@npm:^7.28.3":
"@babel/plugin-transform-regenerator@npm:^7.28.4":
version: 7.28.4
resolution: "@babel/plugin-transform-regenerator@npm:7.28.4"
dependencies:
@@ -928,9 +928,9 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-runtime@npm:7.28.3":
version: 7.28.3
resolution: "@babel/plugin-transform-runtime@npm:7.28.3"
"@babel/plugin-transform-runtime@npm:7.28.5":
version: 7.28.5
resolution: "@babel/plugin-transform-runtime@npm:7.28.5"
dependencies:
"@babel/helper-module-imports": "npm:^7.27.1"
"@babel/helper-plugin-utils": "npm:^7.27.1"
@@ -940,7 +940,7 @@ __metadata:
semver: "npm:^6.3.1"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10/5e8d45f3d243ff15cf4ebe59c2bf52e33bb5864092365dd332330256c96eaa7a16e3cecb1c9e826d5ced7e3cc0b0925c2ca29df674d97252c8eb90ecddab1d78
checksum: 10/0d16c90d40dd34f1a981e742ad656ceef619b92d3662ec9ac8d7c8ba79f22bb425c3f9e097333659a4938f03868a53077b1a3aadb7f37504157a0c7af64ec2be
languageName: node
linkType: hard
@@ -1047,15 +1047,15 @@ __metadata:
languageName: node
linkType: hard
"@babel/preset-env@npm:7.28.3, @babel/preset-env@npm:^7.11.0":
version: 7.28.3
resolution: "@babel/preset-env@npm:7.28.3"
"@babel/preset-env@npm:7.28.5, @babel/preset-env@npm:^7.11.0":
version: 7.28.5
resolution: "@babel/preset-env@npm:7.28.5"
dependencies:
"@babel/compat-data": "npm:^7.28.0"
"@babel/compat-data": "npm:^7.28.5"
"@babel/helper-compilation-targets": "npm:^7.27.2"
"@babel/helper-plugin-utils": "npm:^7.27.1"
"@babel/helper-validator-option": "npm:^7.27.1"
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": "npm:^7.27.1"
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": "npm:^7.28.5"
"@babel/plugin-bugfix-safari-class-field-initializer-scope": "npm:^7.27.1"
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.27.1"
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.27.1"
@@ -1068,42 +1068,42 @@ __metadata:
"@babel/plugin-transform-async-generator-functions": "npm:^7.28.0"
"@babel/plugin-transform-async-to-generator": "npm:^7.27.1"
"@babel/plugin-transform-block-scoped-functions": "npm:^7.27.1"
"@babel/plugin-transform-block-scoping": "npm:^7.28.0"
"@babel/plugin-transform-block-scoping": "npm:^7.28.5"
"@babel/plugin-transform-class-properties": "npm:^7.27.1"
"@babel/plugin-transform-class-static-block": "npm:^7.28.3"
"@babel/plugin-transform-classes": "npm:^7.28.3"
"@babel/plugin-transform-classes": "npm:^7.28.4"
"@babel/plugin-transform-computed-properties": "npm:^7.27.1"
"@babel/plugin-transform-destructuring": "npm:^7.28.0"
"@babel/plugin-transform-destructuring": "npm:^7.28.5"
"@babel/plugin-transform-dotall-regex": "npm:^7.27.1"
"@babel/plugin-transform-duplicate-keys": "npm:^7.27.1"
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": "npm:^7.27.1"
"@babel/plugin-transform-dynamic-import": "npm:^7.27.1"
"@babel/plugin-transform-explicit-resource-management": "npm:^7.28.0"
"@babel/plugin-transform-exponentiation-operator": "npm:^7.27.1"
"@babel/plugin-transform-exponentiation-operator": "npm:^7.28.5"
"@babel/plugin-transform-export-namespace-from": "npm:^7.27.1"
"@babel/plugin-transform-for-of": "npm:^7.27.1"
"@babel/plugin-transform-function-name": "npm:^7.27.1"
"@babel/plugin-transform-json-strings": "npm:^7.27.1"
"@babel/plugin-transform-literals": "npm:^7.27.1"
"@babel/plugin-transform-logical-assignment-operators": "npm:^7.27.1"
"@babel/plugin-transform-logical-assignment-operators": "npm:^7.28.5"
"@babel/plugin-transform-member-expression-literals": "npm:^7.27.1"
"@babel/plugin-transform-modules-amd": "npm:^7.27.1"
"@babel/plugin-transform-modules-commonjs": "npm:^7.27.1"
"@babel/plugin-transform-modules-systemjs": "npm:^7.27.1"
"@babel/plugin-transform-modules-systemjs": "npm:^7.28.5"
"@babel/plugin-transform-modules-umd": "npm:^7.27.1"
"@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.27.1"
"@babel/plugin-transform-new-target": "npm:^7.27.1"
"@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.27.1"
"@babel/plugin-transform-numeric-separator": "npm:^7.27.1"
"@babel/plugin-transform-object-rest-spread": "npm:^7.28.0"
"@babel/plugin-transform-object-rest-spread": "npm:^7.28.4"
"@babel/plugin-transform-object-super": "npm:^7.27.1"
"@babel/plugin-transform-optional-catch-binding": "npm:^7.27.1"
"@babel/plugin-transform-optional-chaining": "npm:^7.27.1"
"@babel/plugin-transform-optional-chaining": "npm:^7.28.5"
"@babel/plugin-transform-parameters": "npm:^7.27.7"
"@babel/plugin-transform-private-methods": "npm:^7.27.1"
"@babel/plugin-transform-private-property-in-object": "npm:^7.27.1"
"@babel/plugin-transform-property-literals": "npm:^7.27.1"
"@babel/plugin-transform-regenerator": "npm:^7.28.3"
"@babel/plugin-transform-regenerator": "npm:^7.28.4"
"@babel/plugin-transform-regexp-modifiers": "npm:^7.27.1"
"@babel/plugin-transform-reserved-words": "npm:^7.27.1"
"@babel/plugin-transform-shorthand-properties": "npm:^7.27.1"
@@ -1123,7 +1123,7 @@ __metadata:
semver: "npm:^6.3.1"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10/b09991276a5ea4f2f95077bb451420f683e19d59405bc1fbbb392bb3571592edc922daac4eaa50b2b407c0b24c4e1e9df0f76738c3c573dac4e6bcf028daa8c5
checksum: 10/e9a5136a7e34553cc70dd6594716144678a2e9ecc971caf6885c380c38fcbed8b387f3af418c9aa4b2d2765964bb4e8a2e14b709c2f165eec6ed13bda32587ea
languageName: node
linkType: hard
@@ -1158,28 +1158,28 @@ __metadata:
languageName: node
linkType: hard
"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4":
version: 7.28.4
resolution: "@babel/traverse@npm:7.28.4"
"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4, @babel/traverse@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/traverse@npm:7.28.5"
dependencies:
"@babel/code-frame": "npm:^7.27.1"
"@babel/generator": "npm:^7.28.3"
"@babel/generator": "npm:^7.28.5"
"@babel/helper-globals": "npm:^7.28.0"
"@babel/parser": "npm:^7.28.4"
"@babel/parser": "npm:^7.28.5"
"@babel/template": "npm:^7.27.2"
"@babel/types": "npm:^7.28.4"
"@babel/types": "npm:^7.28.5"
debug: "npm:^4.3.1"
checksum: 10/c3099364b7b1c36bcd111099195d4abeef16499e5defb1e56766b754e8b768c252e856ed9041665158aa1b31215fc6682632756803c8fa53405381ec08c4752b
checksum: 10/1fce426f5ea494913c40f33298ce219708e703f71cac7ac045ebde64b5a7b17b9275dfa4e05fb92c3f123136913dff62c8113172f4a5de66dab566123dbe7437
languageName: node
linkType: hard
"@babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.4.4":
version: 7.28.4
resolution: "@babel/types@npm:7.28.4"
"@babel/types@npm:^7.25.4, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.4.4":
version: 7.28.5
resolution: "@babel/types@npm:7.28.5"
dependencies:
"@babel/helper-string-parser": "npm:^7.27.1"
"@babel/helper-validator-identifier": "npm:^7.27.1"
checksum: 10/db50bf257aafa5d845ad16dae0587f57d596e4be4cbb233ea539976a4c461f9fbcc0bf3d37adae3f8ce5dcb4001462aa608f3558161258b585f6ce6ce21a2e45
"@babel/helper-validator-identifier": "npm:^7.28.5"
checksum: 10/4256bb9fb2298c4f9b320bde56e625b7091ea8d2433d98dcf524d4086150da0b6555aabd7d0725162670614a9ac5bf036d1134ca13dedc9707f988670f1362d7
languageName: node
linkType: hard
@@ -2277,12 +2277,12 @@ __metadata:
languageName: node
linkType: hard
"@lezer/highlight@npm:1.2.2, @lezer/highlight@npm:^1.0.0":
version: 1.2.2
resolution: "@lezer/highlight@npm:1.2.2"
"@lezer/highlight@npm:1.2.3, @lezer/highlight@npm:^1.0.0":
version: 1.2.3
resolution: "@lezer/highlight@npm:1.2.3"
dependencies:
"@lezer/common": "npm:^1.3.0"
checksum: 10/73cb339de042b354cbc0b9e83978a91d2448435edae865a192cfc50d536e0b7d2e3cd563aabeb59eb6c86b0c38b3edc6f2871da8482c5dd8dca4a0899e743f7f
checksum: 10/8f787d464f8a036f117a0b23e73ac034d224a57d72501c6559089098a28f127c9e495b90ac7d132acc86199e0b64d4c038f75f9293a37c7c61add52fa1acdb4e
languageName: node
linkType: hard
@@ -3884,12 +3884,12 @@ __metadata:
languageName: node
linkType: hard
"@rsbuild/plugin-check-syntax@npm:1.4.0":
version: 1.4.0
resolution: "@rsbuild/plugin-check-syntax@npm:1.4.0"
"@rsbuild/plugin-check-syntax@npm:1.5.0":
version: 1.5.0
resolution: "@rsbuild/plugin-check-syntax@npm:1.5.0"
dependencies:
acorn: "npm:^8.15.0"
browserslist-to-es-version: "npm:^1.1.0"
browserslist-to-es-version: "npm:^1.1.1"
htmlparser2: "npm:10.0.0"
picocolors: "npm:^1.1.1"
source-map: "npm:^0.7.6"
@@ -3898,85 +3898,86 @@ __metadata:
peerDependenciesMeta:
"@rsbuild/core":
optional: true
checksum: 10/1e6ee37cc072bbf459ecd493a78cab2798e08c2e046828e0d93f618581738a4b7a6b6f3bf8b890a58b67b5a879d0a778810b33b834f75dadd89817cf16c73a38
checksum: 10/69f537d24aa298b9913c1e21378eded13a6deff21e597257bdfeb1ddbe42c711d21d068543190cdd2f6537531fea24feb84cf1637e2492c9fc5d2fcc1f9a53d6
languageName: node
linkType: hard
"@rsdoctor/client@npm:1.3.4":
version: 1.3.4
resolution: "@rsdoctor/client@npm:1.3.4"
checksum: 10/0f3d35ce16264c20294f317efc063153cc62b0d7d5f5f11aa444ace2cbf9617f1ea26828e1d8e5f7fe4ce6f6e276ea6cd8312fd4e4f3214a8a3c8d3728f0fc1f
"@rsdoctor/client@npm:1.3.6":
version: 1.3.6
resolution: "@rsdoctor/client@npm:1.3.6"
checksum: 10/9c1207c7bbc0f7ba3ca100074703db07625997e9ed9649c4115ebbb7f3deb44605d0d0a508f20586a10a22ba006d022046cf55a82c19f2f9c599f5f8cd963379
languageName: node
linkType: hard
"@rsdoctor/core@npm:1.3.4":
version: 1.3.4
resolution: "@rsdoctor/core@npm:1.3.4"
"@rsdoctor/core@npm:1.3.6":
version: 1.3.6
resolution: "@rsdoctor/core@npm:1.3.6"
dependencies:
"@rsbuild/plugin-check-syntax": "npm:1.4.0"
"@rsdoctor/graph": "npm:1.3.4"
"@rsdoctor/sdk": "npm:1.3.4"
"@rsdoctor/types": "npm:1.3.4"
"@rsdoctor/utils": "npm:1.3.4"
"@rsbuild/plugin-check-syntax": "npm:1.5.0"
"@rsdoctor/graph": "npm:1.3.6"
"@rsdoctor/sdk": "npm:1.3.6"
"@rsdoctor/types": "npm:1.3.6"
"@rsdoctor/utils": "npm:1.3.6"
browserslist-load-config: "npm:^1.0.1"
enhanced-resolve: "npm:5.12.0"
es-toolkit: "npm:^1.41.0"
filesize: "npm:^10.1.6"
fs-extra: "npm:^11.1.1"
semver: "npm:^7.7.3"
source-map: "npm:^0.7.6"
checksum: 10/852f79ea59b6e2d6bcf0301136e3a9dd6f8acc2c011277128b791f8378a5d291e91c385ab691440bdba454f2130bde1097c3a3ba7eedf35e0a48cf1462d4d7c8
checksum: 10/e4501b8621b687a55e2cb8002286f3f84d950f2399721ecfc906be789d1554f946818d5c3ab6900a7d37725874f43632181cd6dd0ccd7d0e5f318ff30ad98055
languageName: node
linkType: hard
"@rsdoctor/graph@npm:1.3.4":
version: 1.3.4
resolution: "@rsdoctor/graph@npm:1.3.4"
"@rsdoctor/graph@npm:1.3.6":
version: 1.3.6
resolution: "@rsdoctor/graph@npm:1.3.6"
dependencies:
"@rsdoctor/types": "npm:1.3.4"
"@rsdoctor/utils": "npm:1.3.4"
es-toolkit: "npm:^1.40.0"
"@rsdoctor/types": "npm:1.3.6"
"@rsdoctor/utils": "npm:1.3.6"
es-toolkit: "npm:^1.41.0"
path-browserify: "npm:1.0.1"
source-map: "npm:^0.7.6"
checksum: 10/77b50fa73e97c1bd01ef14134d5c81ba63dacba6c4119f712fff139c39adc787a71f4cca793565a7d4faa4d2dbdd869fa72e955632671121ece92ff945adfa83
checksum: 10/64373a0cae33687a65db1ed093a7e0713aec8f749131367474668d32e3e84dd3b9e14e5cf7b8268cc7bff31ba8f6288515cc63ecb1804b25816e7b8483134df3
languageName: node
linkType: hard
"@rsdoctor/rspack-plugin@npm:1.3.4":
version: 1.3.4
resolution: "@rsdoctor/rspack-plugin@npm:1.3.4"
"@rsdoctor/rspack-plugin@npm:1.3.6":
version: 1.3.6
resolution: "@rsdoctor/rspack-plugin@npm:1.3.6"
dependencies:
"@rsdoctor/core": "npm:1.3.4"
"@rsdoctor/graph": "npm:1.3.4"
"@rsdoctor/sdk": "npm:1.3.4"
"@rsdoctor/types": "npm:1.3.4"
"@rsdoctor/utils": "npm:1.3.4"
"@rsdoctor/core": "npm:1.3.6"
"@rsdoctor/graph": "npm:1.3.6"
"@rsdoctor/sdk": "npm:1.3.6"
"@rsdoctor/types": "npm:1.3.6"
"@rsdoctor/utils": "npm:1.3.6"
peerDependencies:
"@rspack/core": "*"
peerDependenciesMeta:
"@rspack/core":
optional: true
checksum: 10/bc09ae1f19c624011961f65e1185889420d60d8aea8ca404766368f60c9d4381bbff146d17e166db66d88080246a7a3029714285bac8f2a2d797130822e787d5
checksum: 10/7498e070668e57f747c786730618431732e4e74c4ba033cbe8b11f6b65974f41d426a31c0ceb847eb7ea886385004fa7a6e9e63ea30138dd2ff4c182cbf900c3
languageName: node
linkType: hard
"@rsdoctor/sdk@npm:1.3.4":
version: 1.3.4
resolution: "@rsdoctor/sdk@npm:1.3.4"
"@rsdoctor/sdk@npm:1.3.6":
version: 1.3.6
resolution: "@rsdoctor/sdk@npm:1.3.6"
dependencies:
"@rsdoctor/client": "npm:1.3.4"
"@rsdoctor/graph": "npm:1.3.4"
"@rsdoctor/types": "npm:1.3.4"
"@rsdoctor/utils": "npm:1.3.4"
"@rsdoctor/client": "npm:1.3.6"
"@rsdoctor/graph": "npm:1.3.6"
"@rsdoctor/types": "npm:1.3.6"
"@rsdoctor/utils": "npm:1.3.6"
safer-buffer: "npm:2.1.2"
socket.io: "npm:4.8.1"
tapable: "npm:2.2.3"
checksum: 10/9e57d1c9f51c4404a6c262c2f4602c19c6d3a9e467ca63456a81d8f614690d85a2574d5e936f03ca5672813a61544f02ca3c4e5f10eb5833f0116f8d2214572b
checksum: 10/b1a78b36843fdb9596d42a67e12d25fcf2ba03e26793483d6bd50de18ed9a20df5891b5772b456cdf8d73c920b1f42d5e8c90c03def813ae8d8b0ca0e7ba1113
languageName: node
linkType: hard
"@rsdoctor/types@npm:1.3.4":
version: 1.3.4
resolution: "@rsdoctor/types@npm:1.3.4"
"@rsdoctor/types@npm:1.3.6":
version: 1.3.6
resolution: "@rsdoctor/types@npm:1.3.6"
dependencies:
"@types/connect": "npm:3.4.38"
"@types/estree": "npm:1.0.5"
@@ -3990,16 +3991,16 @@ __metadata:
optional: true
webpack:
optional: true
checksum: 10/56ce0af51c6d78eaac6fa6236dba1c5753b52dd021f8ffc4d3fe40d7f52f079ecec7e2c247ac3ca6e44d8a6f274bb20b9c7310baa9ce7bf7e60d8ee16ec8c0c4
checksum: 10/090c5872cb7c04c83ed134ec47fdf673777056e80041860f21e36c4f7c506f019136a834d59ed3b84db8d8df615300e331060234dc48b71af5acab287352adc3
languageName: node
linkType: hard
"@rsdoctor/utils@npm:1.3.4":
version: 1.3.4
resolution: "@rsdoctor/utils@npm:1.3.4"
"@rsdoctor/utils@npm:1.3.6":
version: 1.3.6
resolution: "@rsdoctor/utils@npm:1.3.6"
dependencies:
"@babel/code-frame": "npm:7.26.2"
"@rsdoctor/types": "npm:1.3.4"
"@rsdoctor/types": "npm:1.3.6"
"@types/estree": "npm:1.0.5"
acorn: "npm:^8.10.0"
acorn-import-attributes: "npm:^1.9.5"
@@ -4013,7 +4014,7 @@ __metadata:
picocolors: "npm:^1.1.1"
rslog: "npm:^1.2.11"
strip-ansi: "npm:^6.0.1"
checksum: 10/04b72e7ab1b63cad77ef83a468cb5c5b1468662dd29d03fbae7ef16aa81d3d91b54958d8630ee53655b794c32b12effb5ff3162cc309174c5ec784373d3f0560
checksum: 10/cb3b6e0c6ea64d6d7657a6682c1c295a4d0a787af7292fe4e7da46ef064d8ca843c7a65b9e0014a9d77053aeaf3481f16776acec3963ac6a8dcc4ba1c4bf5a17
languageName: node
linkType: hard
@@ -4821,12 +4822,12 @@ __metadata:
languageName: node
linkType: hard
"@types/qrcode@npm:1.5.5":
version: 1.5.5
resolution: "@types/qrcode@npm:1.5.5"
"@types/qrcode@npm:1.5.6":
version: 1.5.6
resolution: "@types/qrcode@npm:1.5.6"
dependencies:
"@types/node": "npm:*"
checksum: 10/a25686339bd2718e6a93943e7807ed68dd9c74a9da28aa77212086ee0ce9a173c0a232af9e3f6835acd09938dfc8a0f98c6bccf1a6c6a905fb003ab07f9e08f2
checksum: 10/61aa00ba56a874a71a82006a55715d2ff2dc65689b582314c6f955c72c830938c93d0ee1e3376d370a1dcd6bac61b05afe75ad8d1f4a47ebcdd6344d39f70847
languageName: node
linkType: hard
@@ -4897,10 +4898,10 @@ __metadata:
languageName: node
linkType: hard
"@types/sortablejs@npm:1.15.8":
version: 1.15.8
resolution: "@types/sortablejs@npm:1.15.8"
checksum: 10/aea58b08cf45f5e9633707a8df0df1212595c731bbdfd29805487138fdd0d8c51fa5c741999738a645c1e801d43a92ba0d3fb5b45625b52e247c56588aef6c55
"@types/sortablejs@npm:1.15.9":
version: 1.15.9
resolution: "@types/sortablejs@npm:1.15.9"
checksum: 10/9e0d6ef1cdcd30acab7af74cab5cbf56685e2de47c6f254357f7dde2626e9b49b8668c43f92761202cf4e10f2e3fd3d7baa6a83a39c3d342fc45ce66e87bd5aa
languageName: node
linkType: hard
@@ -5376,12 +5377,12 @@ __metadata:
languageName: node
linkType: hard
"@vitest/coverage-v8@npm:4.0.1":
version: 4.0.1
resolution: "@vitest/coverage-v8@npm:4.0.1"
"@vitest/coverage-v8@npm:4.0.4":
version: 4.0.4
resolution: "@vitest/coverage-v8@npm:4.0.4"
dependencies:
"@bcoe/v8-coverage": "npm:^1.0.2"
"@vitest/utils": "npm:4.0.1"
"@vitest/utils": "npm:4.0.4"
ast-v8-to-istanbul: "npm:^0.3.5"
debug: "npm:^4.4.3"
istanbul-lib-coverage: "npm:^3.2.2"
@@ -5392,34 +5393,34 @@ __metadata:
std-env: "npm:^3.9.0"
tinyrainbow: "npm:^3.0.3"
peerDependencies:
"@vitest/browser": 4.0.1
vitest: 4.0.1
"@vitest/browser": 4.0.4
vitest: 4.0.4
peerDependenciesMeta:
"@vitest/browser":
optional: true
checksum: 10/6052deb51ba5be8bb1ad471f8678e7763823abd143cb8178503b5fefb260dd29b537c1ca44f6dfcb387b46e1e8c9613b4011d6604b010e29d5f229431244833a
checksum: 10/9aaab295ae81da2be090973173922e3db2822725d0fbe27e8084908018e95cc6666259c8cc4b6bab14bd40bb9365072e41c7ae64c4d2a28672ad23f7743cec29
languageName: node
linkType: hard
"@vitest/expect@npm:4.0.1":
version: 4.0.1
resolution: "@vitest/expect@npm:4.0.1"
"@vitest/expect@npm:4.0.4":
version: 4.0.4
resolution: "@vitest/expect@npm:4.0.4"
dependencies:
"@standard-schema/spec": "npm:^1.0.0"
"@types/chai": "npm:^5.2.2"
"@vitest/spy": "npm:4.0.1"
"@vitest/utils": "npm:4.0.1"
"@vitest/spy": "npm:4.0.4"
"@vitest/utils": "npm:4.0.4"
chai: "npm:^6.0.1"
tinyrainbow: "npm:^3.0.3"
checksum: 10/32b11776b15c044399c6d14f75efe3773d09f9af2f5f4df1a8c32cef6d4929ccb663183f800b79404a2b36b282ae54066641daf7fa84effb51dc0e6e7d922818
checksum: 10/7d6f4e501c5b84c63fd99259a2b0000b81b075fd62c1a4ff479c50136d46d6cf90bcdd514f6304bf972fbba148949fda23ebd2926fa26f0fd428f1010f1cff63
languageName: node
linkType: hard
"@vitest/mocker@npm:4.0.1":
version: 4.0.1
resolution: "@vitest/mocker@npm:4.0.1"
"@vitest/mocker@npm:4.0.4":
version: 4.0.4
resolution: "@vitest/mocker@npm:4.0.4"
dependencies:
"@vitest/spy": "npm:4.0.1"
"@vitest/spy": "npm:4.0.4"
estree-walker: "npm:^3.0.3"
magic-string: "npm:^0.30.19"
peerDependencies:
@@ -5430,54 +5431,54 @@ __metadata:
optional: true
vite:
optional: true
checksum: 10/519e3f66f9823ba024926fa818308fae091b0974e51e4b20026fa7b853dd8f6db9b50f78846f59cdca8141913425946e1dbf3ffc78ede5e021365a809ea49093
checksum: 10/070ddbd7f9907482aa1812e01d13ff66cea540479435a857072fc7528f4b583d62f0600eed112b5780e9698812c944b679fc959e764670de23621eba9be9e177
languageName: node
linkType: hard
"@vitest/pretty-format@npm:4.0.1":
version: 4.0.1
resolution: "@vitest/pretty-format@npm:4.0.1"
"@vitest/pretty-format@npm:4.0.4":
version: 4.0.4
resolution: "@vitest/pretty-format@npm:4.0.4"
dependencies:
tinyrainbow: "npm:^3.0.3"
checksum: 10/f2813708b46a36053106b22801764e3f7173e677228531677eb20dfdf43ce340b282a9e0ed388b9ac5a1a3289b0f91b4b8a8a37723be39deddbffc74a467a374
checksum: 10/64b0430012f8c34748a39dd1ea061f51d17053086d961830e93a4083b7cda50d822d6db8154398d7f800e15e1efc45c6460d7c1c5f06e21f09a282663824b9c9
languageName: node
linkType: hard
"@vitest/runner@npm:4.0.1":
version: 4.0.1
resolution: "@vitest/runner@npm:4.0.1"
"@vitest/runner@npm:4.0.4":
version: 4.0.4
resolution: "@vitest/runner@npm:4.0.4"
dependencies:
"@vitest/utils": "npm:4.0.1"
"@vitest/utils": "npm:4.0.4"
pathe: "npm:^2.0.3"
checksum: 10/a3a91897a283a3a1dd39bfb81ef356eb3a6f6b5d712e6da9e49d31bee46092884aaf8832a135f7f13d6350dc4238a38ddfea18da1d628efda73132fc4d01d6cd
checksum: 10/3bd7e8944d8770ae7dd7457fb83751def5cb4834766cb86b7231cc0fd77d4e889b5ec6da70ae82e06902b19c216f114a5a670e4422ba2db8384dee09dc24b832
languageName: node
linkType: hard
"@vitest/snapshot@npm:4.0.1":
version: 4.0.1
resolution: "@vitest/snapshot@npm:4.0.1"
"@vitest/snapshot@npm:4.0.4":
version: 4.0.4
resolution: "@vitest/snapshot@npm:4.0.4"
dependencies:
"@vitest/pretty-format": "npm:4.0.1"
"@vitest/pretty-format": "npm:4.0.4"
magic-string: "npm:^0.30.19"
pathe: "npm:^2.0.3"
checksum: 10/f90db2bbd0b20d53cded64464809ea9abdc6bb7abe2e1d12bf81e13895b14b15b9a052d964f0ba4b7312cc8af25c8e943a70dcb3ad9b5e8b27d88adacdde15d2
checksum: 10/dba29abb40e8a4e8931a9522076f0884a579280c440d1e7591c32c4f8d75366b18e3f78920eed13337909384767d0403d15f384275f22d7c0bc5c36a0c4f5482
languageName: node
linkType: hard
"@vitest/spy@npm:4.0.1":
version: 4.0.1
resolution: "@vitest/spy@npm:4.0.1"
checksum: 10/9efda95edb29b2c06859af98ef1b1497cbf7ee43fd95649a835139e61e77a3f6bdad9dec48b53b954a311f824656978fb2eafb4dd8b416b354e15ad430a60772
"@vitest/spy@npm:4.0.4":
version: 4.0.4
resolution: "@vitest/spy@npm:4.0.4"
checksum: 10/f3bca9c53840c6f271d676a5b7a06535eb4a80e4b2e52e179ff56cb53f7f725ba1c683cb13d2ef1283057c09bf260634b9b9b318bd2a1755853c067bc9a0dd6a
languageName: node
linkType: hard
"@vitest/utils@npm:4.0.1":
version: 4.0.1
resolution: "@vitest/utils@npm:4.0.1"
"@vitest/utils@npm:4.0.4":
version: 4.0.4
resolution: "@vitest/utils@npm:4.0.4"
dependencies:
"@vitest/pretty-format": "npm:4.0.1"
"@vitest/pretty-format": "npm:4.0.4"
tinyrainbow: "npm:^3.0.3"
checksum: 10/21d7460135327abcc827de640cfa1f049321ad28f59b69538251cf9c50f7731734ded5e55fed36dd95db840c6a4275308b3e2f08c3cbd02289b0268abf4c441b
checksum: 10/032aa6fad679efe270015bd0ceec55915b3ff4597f5e0b4ee9ae2ac54d5e8a2f903ef64533cc73f05264d8959c01104a32283c2b0298f4e903a003c9bf427514
languageName: node
linkType: hard
@@ -6146,12 +6147,12 @@ __metadata:
languageName: node
linkType: hard
"baseline-browser-mapping@npm:^2.8.9":
version: 2.8.16
resolution: "baseline-browser-mapping@npm:2.8.16"
"baseline-browser-mapping@npm:^2.8.19":
version: 2.8.21
resolution: "baseline-browser-mapping@npm:2.8.21"
bin:
baseline-browser-mapping: dist/cli.js
checksum: 10/52a5807591daeffc810b783b1afa20c4017dd94e5bb74934bcde4dd408758e492610e330cfe6e609a0f0bde5ce210dd934271540fb931389d6838db17ec8cfef
checksum: 10/4154199589f9d5ca0cf80962494f34967e5bcbe2a3df234f4eb4b7a1766b263062ed47640686ab1d949f1156f5153bdc382ff7815368e365bc86916dc099d61b
languageName: node
linkType: hard
@@ -6298,12 +6299,12 @@ __metadata:
languageName: node
linkType: hard
"browserslist-to-es-version@npm:^1.1.0":
version: 1.1.1
resolution: "browserslist-to-es-version@npm:1.1.1"
"browserslist-to-es-version@npm:^1.1.1":
version: 1.2.0
resolution: "browserslist-to-es-version@npm:1.2.0"
dependencies:
browserslist: "npm:^4.25.1"
checksum: 10/efce2f27e67bda030ee3957f6df5aaa8594ee5cf017d3567f1226f6abcea7c3840c1ba73105b6bd661c5f57339e44c6819804609e1f064cfdfb8d53894a9f777
browserslist: "npm:^4.26.2"
checksum: 10/a9af676f4b736fc17cd42cbc98edc735e7f1e775ccb71443afc5ec7184203669f4c6c37ec2d73964c598d7f8fa42ae6ce2ec0e9a9c64fcb1bc297110dbfbd0c9
languageName: node
linkType: hard
@@ -6325,18 +6326,18 @@ __metadata:
languageName: node
linkType: hard
"browserslist@npm:^4.24.0, browserslist@npm:^4.25.1, browserslist@npm:^4.25.3":
version: 4.26.3
resolution: "browserslist@npm:4.26.3"
"browserslist@npm:^4.24.0, browserslist@npm:^4.25.3, browserslist@npm:^4.26.2":
version: 4.27.0
resolution: "browserslist@npm:4.27.0"
dependencies:
baseline-browser-mapping: "npm:^2.8.9"
caniuse-lite: "npm:^1.0.30001746"
electron-to-chromium: "npm:^1.5.227"
node-releases: "npm:^2.0.21"
update-browserslist-db: "npm:^1.1.3"
baseline-browser-mapping: "npm:^2.8.19"
caniuse-lite: "npm:^1.0.30001751"
electron-to-chromium: "npm:^1.5.238"
node-releases: "npm:^2.0.26"
update-browserslist-db: "npm:^1.1.4"
bin:
browserslist: cli.js
checksum: 10/49add06fd753a2514d84c75a7de8d9fb3d70be675e53b72981d87f0c0ff40d8a8cd0bd92f77400381704be0bf1c9c5c65aef95d03843d69475ff55188aa12124
checksum: 10/56db4cdb98b5c93797a47e5a60decb144f73a2ae41c60a16c41b75516fabcb0db0116b8cfcf3a26c960cc6c9ab1c4f4801d8d3a743ec72f27acfe5380153ba2f
languageName: node
linkType: hard
@@ -6480,10 +6481,10 @@ __metadata:
languageName: node
linkType: hard
"caniuse-lite@npm:^1.0.30001746":
version: 1.0.30001750
resolution: "caniuse-lite@npm:1.0.30001750"
checksum: 10/2b912758d817cd2c2c179246e282f8b598695ec733bc446183e1d381eada60889c4770a1dfd86075e046a43d55f9922e2eaed1501347fcb12a38716cc14be297
"caniuse-lite@npm:^1.0.30001751":
version: 1.0.30001751
resolution: "caniuse-lite@npm:1.0.30001751"
checksum: 10/608f7e1248b7023020382c7dbb0ef389693b3fc98193c3ccea2d44126306d6ac905a5061cf9e62bf640535a86e7a98e563b34c02f909296cfe228f41627a4dc7
languageName: node
linkType: hard
@@ -7455,10 +7456,10 @@ __metadata:
languageName: node
linkType: hard
"electron-to-chromium@npm:^1.5.227":
version: 1.5.235
resolution: "electron-to-chromium@npm:1.5.235"
checksum: 10/fbc227d58a07dbb1b01e4a0f624a2fae03881f160a7c2e4416a68f30c83c1ca29b8f0e04056cb2851a6f493ebaf0d3b24bc2c7721d9e779cccbc9faeffef1c0e
"electron-to-chromium@npm:^1.5.238":
version: 1.5.243
resolution: "electron-to-chromium@npm:1.5.243"
checksum: 10/cc1d566936aa05edcdef45c837bd3bf3c640b297f16d961d6b2b8536efb82bf1938a3dbc6d930e7561ffe4545c3d683dd6ffe57da37c1f6230defd32818785ab
languageName: node
linkType: hard
@@ -7744,7 +7745,7 @@ __metadata:
languageName: node
linkType: hard
"es-toolkit@npm:^1.40.0":
"es-toolkit@npm:^1.41.0":
version: 1.41.0
resolution: "es-toolkit@npm:1.41.0"
dependenciesMeta:
@@ -9203,10 +9204,10 @@ __metadata:
version: 0.0.0-use.local
resolution: "home-assistant-frontend@workspace:."
dependencies:
"@babel/core": "npm:7.28.4"
"@babel/core": "npm:7.28.5"
"@babel/helper-define-polyfill-provider": "npm:0.6.5"
"@babel/plugin-transform-runtime": "npm:7.28.3"
"@babel/preset-env": "npm:7.28.3"
"@babel/plugin-transform-runtime": "npm:7.28.5"
"@babel/preset-env": "npm:7.28.5"
"@babel/runtime": "npm:7.28.4"
"@braintree/sanitize-url": "npm:7.1.1"
"@bundle-stats/plugin-webpack-filter": "npm:4.21.5"
@@ -9235,7 +9236,7 @@ __metadata:
"@fullcalendar/luxon3": "npm:6.1.19"
"@fullcalendar/timegrid": "npm:6.1.19"
"@home-assistant/webawesome": "npm:3.0.0-beta.6.ha.6"
"@lezer/highlight": "npm:1.2.2"
"@lezer/highlight": "npm:1.2.3"
"@lit-labs/motion": "npm:1.0.9"
"@lit-labs/observers": "npm:2.0.6"
"@lit-labs/virtualizer": "npm:2.1.1"
@@ -9271,7 +9272,7 @@ __metadata:
"@octokit/plugin-retry": "npm:8.0.2"
"@octokit/rest": "npm:22.0.0"
"@replit/codemirror-indentation-markers": "npm:6.5.3"
"@rsdoctor/rspack-plugin": "npm:1.3.4"
"@rsdoctor/rspack-plugin": "npm:1.3.6"
"@rspack/core": "npm:1.5.8"
"@rspack/dev-server": "npm:1.1.4"
"@swc/helpers": "npm:0.5.17"
@@ -9291,15 +9292,15 @@ __metadata:
"@types/lodash.merge": "npm:4.6.9"
"@types/luxon": "npm:3.7.1"
"@types/mocha": "npm:10.0.10"
"@types/qrcode": "npm:1.5.5"
"@types/sortablejs": "npm:1.15.8"
"@types/qrcode": "npm:1.5.6"
"@types/sortablejs": "npm:1.15.9"
"@types/tar": "npm:6.1.13"
"@types/ua-parser-js": "npm:0.7.39"
"@types/webspeechapi": "npm:0.0.29"
"@vaadin/combo-box": "npm:24.9.2"
"@vaadin/vaadin-themable-mixin": "npm:24.9.2"
"@vibrant/color": "npm:4.0.0"
"@vitest/coverage-v8": "npm:4.0.1"
"@vitest/coverage-v8": "npm:4.0.4"
"@vue/web-component-wrapper": "npm:1.3.0"
"@webcomponents/scoped-custom-element-registry": "npm:0.0.10"
"@webcomponents/webcomponentsjs": "npm:2.8.0"
@@ -9384,7 +9385,7 @@ __metadata:
typescript-eslint: "npm:8.46.2"
ua-parser-js: "npm:2.0.6"
vite-tsconfig-paths: "npm:5.1.4"
vitest: "npm:4.0.1"
vitest: "npm:4.0.4"
vue: "npm:2.7.16"
vue2-daterange-picker: "npm:0.6.8"
webpack-stats-plugin: "npm:1.1.3"
@@ -11429,10 +11430,10 @@ __metadata:
languageName: node
linkType: hard
"node-releases@npm:^2.0.21":
version: 2.0.21
resolution: "node-releases@npm:2.0.21"
checksum: 10/5344d634b39d20f47c0d85a1c64567fdb9cf46f7b27ed3d141f752642faab47dae326835c2109636f823758afb16ffbed7b0c0fe6f800ef91cec9f2beb4f2b4a
"node-releases@npm:^2.0.26":
version: 2.0.27
resolution: "node-releases@npm:2.0.27"
checksum: 10/f6c78ddb392ae500719644afcbe68a9ea533242c02312eb6a34e8478506eb7482a3fb709c70235b01c32fe65625b68dfa9665113f816d87f163bc3819b62b106
languageName: node
linkType: hard
@@ -14537,9 +14538,9 @@ __metadata:
languageName: node
linkType: hard
"update-browserslist-db@npm:^1.1.3":
version: 1.1.3
resolution: "update-browserslist-db@npm:1.1.3"
"update-browserslist-db@npm:^1.1.4":
version: 1.1.4
resolution: "update-browserslist-db@npm:1.1.4"
dependencies:
escalade: "npm:^3.2.0"
picocolors: "npm:^1.1.1"
@@ -14547,7 +14548,7 @@ __metadata:
browserslist: ">= 4.21.0"
bin:
update-browserslist-db: cli.js
checksum: 10/87af2776054ffb9194cf95e0201547d041f72ee44ce54b144da110e65ea7ca01379367407ba21de5c9edd52c74d95395366790de67f3eb4cc4afa0fe4424e76f
checksum: 10/79b2c0a31e9b837b49dc55d5cb7b77f44a69502847c7be352a44b1d35ac2032bf0e1bb7543f992809ed427bf9d32aa3f7ad41cef96198fa959c1666870174c06
languageName: node
linkType: hard
@@ -14766,17 +14767,17 @@ __metadata:
languageName: node
linkType: hard
"vitest@npm:4.0.1":
version: 4.0.1
resolution: "vitest@npm:4.0.1"
"vitest@npm:4.0.4":
version: 4.0.4
resolution: "vitest@npm:4.0.4"
dependencies:
"@vitest/expect": "npm:4.0.1"
"@vitest/mocker": "npm:4.0.1"
"@vitest/pretty-format": "npm:4.0.1"
"@vitest/runner": "npm:4.0.1"
"@vitest/snapshot": "npm:4.0.1"
"@vitest/spy": "npm:4.0.1"
"@vitest/utils": "npm:4.0.1"
"@vitest/expect": "npm:4.0.4"
"@vitest/mocker": "npm:4.0.4"
"@vitest/pretty-format": "npm:4.0.4"
"@vitest/runner": "npm:4.0.4"
"@vitest/snapshot": "npm:4.0.4"
"@vitest/spy": "npm:4.0.4"
"@vitest/utils": "npm:4.0.4"
debug: "npm:^4.4.3"
es-module-lexer: "npm:^1.7.0"
expect-type: "npm:^1.2.2"
@@ -14794,10 +14795,10 @@ __metadata:
"@edge-runtime/vm": "*"
"@types/debug": ^4.1.12
"@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0
"@vitest/browser-playwright": 4.0.1
"@vitest/browser-preview": 4.0.1
"@vitest/browser-webdriverio": 4.0.1
"@vitest/ui": 4.0.1
"@vitest/browser-playwright": 4.0.4
"@vitest/browser-preview": 4.0.4
"@vitest/browser-webdriverio": 4.0.4
"@vitest/ui": 4.0.4
happy-dom: "*"
jsdom: "*"
peerDependenciesMeta:
@@ -14821,7 +14822,7 @@ __metadata:
optional: true
bin:
vitest: vitest.mjs
checksum: 10/5f9aecae9c3d8446b581d26a1dc228e7bbf8af0b4fa760ed32ad9ebe7220b815ba6f04cc4e3177e0ead5059a8fecac94455e23852cf601f75ae952fc444f7e1f
checksum: 10/5a7cd573e1a1daeaa7d874d9a676945aab67c1b5167119e903d50be33f3490f919ea575119efdc473a42d8c42b176bbbc31c12f3c7f1fdd2eb817cc8e162c10a
languageName: node
linkType: hard