Compare commits

...

168 Commits

Author SHA1 Message Date
renovate[bot]
fa05cd0c90
Update dependency eslint-plugin-lit to v2.1.1 (#25110)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-19 17:33:34 +02:00
renovate[bot]
0b7fc330b3
Update dependency element-internals-polyfill to v3.0.2 (#25109)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-19 17:32:33 +02:00
Simon Lamon
6aa78794a7
Area strategy: Translate no entities in this area (#25101)
No entities in this area
2025-04-19 12:32:02 +02:00
renovate[bot]
3f17548582
Lock file maintenance (#25108)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-19 09:23:46 +02:00
renovate[bot]
0cee3c2882
Update rspack monorepo to v1.3.5 (#25104)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-18 14:56:02 +02:00
renovate[bot]
5753b3e166
Update dependency typescript-eslint to v8.30.1 (#25103)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-18 07:55:50 +02:00
Paul Bottein
7b78d821f9
Fix spinner in quick bar (#25097) 2025-04-17 20:23:59 +02:00
renovate[bot]
9a4469588c
Update dependency typescript-eslint to v8.30.0 (#25099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-17 17:57:40 +00:00
renovate[bot]
f9eadf08fd
Update vaadinWebComponents monorepo to v24.7.3 (#25087)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-17 19:47:50 +02:00
dependabot[bot]
c630176fcf
Bump http-proxy-middleware from 2.0.7 to 2.0.9 (#25092)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.7 to 2.0.9.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.9/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.7...v2.0.9)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-version: 2.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 19:46:20 +02:00
Paul Bottein
0389fbba52
Use ha-combox-box-list-item in all combo box components (#25096) 2025-04-17 17:13:19 +02:00
Paul Bottein
d56c7c41e2
Update entity naming in entity picker (#24971) 2025-04-17 16:43:47 +02:00
Paul Bottein
e74cac697e
Add fit mode support to picture glance card and picture entity card (#25005)
Co-authored-by: karwosts <karwosts@gmail.com>
2025-04-17 13:36:34 +00:00
renovate[bot]
77216e8e76
Update Yarn to v4.9.1 (#25089)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-17 15:28:28 +02:00
Bram Kragten
02a8924f63
Fix max height of video in more info (#25091) 2025-04-17 15:27:57 +02:00
Petar Petrov
9fc28e5abb
ZwaveJS controller migration flow (#25003)
* ZwaveJS migration flow

* Show exact progress in options flow

* progress fix

* Apply suggestions from code review

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

* remove unused string

* import fix

* fix selectedDomain

* entryId -> handler

* Update src/translations/en.json

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-04-17 13:32:33 +02:00
Wendelin
933fb1327a
Implement new Z-Wave add device flow (#24667)
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-04-17 13:12:04 +02:00
Petar Petrov
c73a9fccb8
Add support for exact % progress reports in options flow (#25082)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-04-17 09:42:52 +02:00
Wendelin
38c11e738e
Improve ha cloud info buttons (#25079) 2025-04-16 17:42:51 +02:00
renovate[bot]
93c5632ee0
Update dependency jsdom to v26.1.0 (#25081)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-16 13:50:29 +03:00
Wendelin
5459eaff30
Fix and improve remove z-wave node (#25078)
* Fix and improve remove zwave node

* Improve error

* Fix lint
2025-04-16 13:48:26 +03:00
Wendelin
b02f1037fb
Translate "Unnamed view" (#25080)
Add unnamed view translation
2025-04-16 11:01:13 +03:00
Paul Bottein
3d130b790c
Create covers section in area strategy dashboard (#25073)
* Move cover domain and garage, door, window binary sensor to opening section in area strategy

* Rename to cover and add input boolean and select
2025-04-15 23:10:40 +02:00
renovate[bot]
e23d2392d8
Update dependency @lokalise/node-api to v14.3.0 (#25077)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 21:51:24 +02:00
Bram Kragten
d5a6e16bf8
Fix safe area inset in sidebar (#25074) 2025-04-15 15:32:57 +02:00
ildar170975
91a5497c60
fix ha-textfield (max-width, text-overflow, padding) (#25043)
* fix max-width, text-overflow, padding

* simmetrical padding
2025-04-15 15:41:08 +03:00
Paul Bottein
65dae09a49
Avoid generic entity row with control to open more info (#25068) 2025-04-15 13:12:49 +02:00
Wendelin
7e0f293d1f
Add cloud info to backup locations (#25065)
* Add cloud info to backup agents

* Add ha cloud translation
2025-04-15 12:01:04 +02:00
0xEF
2682011ae6
Add padding back to weather forecast card for non-masonry layout (#25035) 2025-04-15 09:31:41 +00:00
Paul Bottein
1bba103a3d
Fix theme variables for ha-tabs (#25066) 2025-04-15 11:08:41 +02:00
renovate[bot]
e425375d55
Update dependency lint-staged to v15.5.1 (#25057)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 21:05:13 +02:00
Bram Kragten
a2689eee63
Add suffix to addon copies setting (#25055) 2025-04-14 13:52:35 +00:00
Bram Kragten
74741c5d69
Don't use advanced mode in hardware dialog (#25051) 2025-04-14 15:14:44 +02:00
Bram Kragten
53426d647a
Remove advanced mode from scenes editor (#25054) 2025-04-14 12:41:24 +00:00
Bram Kragten
f6e4f4c0d6
Remove advanced mode from dashboard creation (#25053) 2025-04-14 12:35:45 +00:00
Bram Kragten
2f086f4d00
Use expandable instead of advanced mode toggle (#25052) 2025-04-14 14:35:30 +02:00
ildar170975
cd91e8c07c
Use codemirror in dialog-import-blueprint (#25034)
* use codemirror

* hass not needed for codemirror

* fix uom for css var
2025-04-14 10:28:28 +03:00
dependabot[bot]
b3a5ea2893
Bump actions/setup-node from 4.3.0 to 4.4.0 (#25047)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.3.0 to 4.4.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.3.0...v4.4.0)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 4.4.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-04-14 09:23:24 +03:00
ildar170975
98ae0295b4
Do not show tips for shortcuts on a client with a touch screen (#25020)
* check isTouch

* check isTouch

* check isTouch

* restored lost line

* isTouch -> isMobileClient

* isTouch -> isMobileClient

* isTouch -> isMobileClient

* Create is_mobile.ts
2025-04-14 09:22:33 +03:00
ildar170975
43bb9d3401
codemirror: set cursor color to "--primary-color", set indent marker color to "--divider-color" (#25045)
* set cursor color to "--primary-color"

* indent markers' color
2025-04-14 09:15:29 +03:00
J. Nick Koston
8ad4385d67
Link the device info BLE address to the Advertisement Monitor (#25044)
* Link the device info BLE address to the Advertisement Monitor

* Link the device info BLE address to the Advertisement Monitor

* preen
2025-04-14 09:13:27 +03:00
renovate[bot]
8fb7c1594a
Update dependency eslint-config-prettier to v10.1.2 (#25036)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 09:10:19 +03:00
ildar170975
4fca09f9ae
If entity has "entity_picture" - allow using a CSS theme var for border-radius (#24248)
* added CSS theme var for border-radius

* prettier

* moved "50%" out of "styles"

* small refactoring

* lint

* lint

* lint

* revert to this.style.borderRadius

* prettier

* adding classes

* fixed styles + setting a class

* clean-up

* remove old classes in render()

* "!important" not needed

* using map
2025-04-14 09:09:52 +03:00
ildar170975
6793753755
more-info-camera: disable "download_snapshot" if idle (#25027)
* disable download_snapshot if idle

* prettier
2025-04-13 09:02:36 +02:00
ildar170975
f4e3fdb98e
ha-map-card: fit_zones tiny fix (#25031)
fit_zones fix
2025-04-13 08:57:53 +02:00
karwosts
63f4cc456c
No particles when prefers-reduced-motion (#25029) 2025-04-13 08:56:02 +02:00
renovate[bot]
33735abfb0
Update octokit monorepo (#25032)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-13 08:42:27 +02:00
renovate[bot]
22b59b247e
Update Yarn to v4.9.0 (#25025)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-12 18:13:27 +02:00
karwosts
6d7a40368c
Support more templates in action visual editor (#25015)
* Support more templates in action visual editor

* Make selector sticky

* typing
2025-04-12 15:03:44 +03:00
renovate[bot]
fbeb457c25
Update rspack monorepo to v1.3.4 (#25021)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-12 08:52:32 +02:00
dependabot[bot]
4a6834f0d9
Bump vite from 6.2.5 to 6.2.6 (#25014)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.5 to 6.2.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.2.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-11 18:19:46 +03:00
Bram Kragten
add417a166
Wait for backup integration to setup before subscribing (#25012) 2025-04-11 16:50:23 +02:00
Pierre
ae4f43496e
Add reconnection information when an instance is already connected (#25013) 2025-04-11 16:08:02 +02:00
renovate[bot]
4ce792e5bf
Update rspack monorepo to v1.3.3 (#25007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 16:20:55 +03:00
Bram Kragten
b9433b96dc
Wait for person before creating user in onboarding (#25011) 2025-04-11 16:19:22 +03:00
Wendelin
1dfd937c94
Add automatic backup toggle to OS update (#24995) 2025-04-11 15:10:43 +02:00
Paul Bottein
14e0666c3a
Only ask to refresh dashboard if necessary (#24993) 2025-04-11 14:41:12 +02:00
Bram Kragten
929a0b9cd4
Wait for cloud and backup in onboarding (#24997) 2025-04-11 13:43:45 +02:00
Bram Kragten
0541270695
Also show hardware integration if it has options (#25006)
also show hardware integration if it has options
2025-04-11 11:39:54 +02:00
Paul Bottein
20d357fb13
Add tests for get duplicates function (#24994) 2025-04-11 08:47:42 +02:00
renovate[bot]
6658c10b94
Update dependency typescript-eslint to v8.29.1 (#25000)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 06:41:13 +02:00
karwosts
c2ce02652b
Fix automation action row describing template targets (#25002) 2025-04-11 06:40:20 +02:00
Bram Kragten
634db1944f
Fix dragging in tab bar (#24998) 2025-04-10 19:12:29 +02:00
Bram Kragten
21b3177f95
Replace paper item in sidebar (#24883)
* replace paper item in sidebar

* make items same height as before

* remove polymer refs

* fix user badge

* replace removed styles (and remove unused)
2025-04-10 18:32:38 +02:00
renovate[bot]
7383e3247b
Update dependency marked to v15.0.8 (#24996)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-10 16:12:09 +00:00
karwosts
b33e4bf305
Render low carbon gauge when only solar consumption (#24992)
* Render low carbon gauge when only solar consumption

* no null check
2025-04-10 18:01:49 +02:00
Bram Kragten
9d9522cade
Use subscription for config flows in progress (#24985) 2025-04-10 16:59:20 +02:00
Bram Kragten
430e47c0fc
Replace paper tabs by shoelace tabs (#24909) 2025-04-10 14:20:24 +00:00
Paul Bottein
a6c9702ab2
Update entity naming in entities config page (#24966)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-04-10 12:43:08 +00:00
Wendelin
e3122e8e4d
Supervisor backup update config (#24990) 2025-04-10 11:55:20 +02:00
Paul Bottein
c8e46bd239
Fix refresh strategy config on HA start-up (#24984) 2025-04-10 11:04:43 +03:00
Bram Kragten
4fd87a1d7c
Hide hardware integrations without entities (#24986) 2025-04-10 08:12:21 +03:00
karwosts
80151ff759
Fix now-7d history to include today (#24989) 2025-04-10 08:10:04 +03:00
Paul Bottein
5f187c1bb3
Fix data-table group by unknown column (#24987) 2025-04-09 15:51:02 +00:00
Stefan Agner
ddc04dd48a
Allow to copy IP address of Matter devices to clipboard (#24983)
Often when debugging it is actually helpful to copy the IP address
for further investigation. This PR changes the list to allow
interaction and copies the IP address when clicked on a list item.
2025-04-09 17:44:13 +02:00
Jan-Philipp Benecke
228acf1fae
Add shortcuts item to command quick bar (#24952)
* Add shortcuts item to command quick bar

* Remove
2025-04-09 16:46:08 +02:00
Bram Kragten
74acd7ec38
fix dropdown behind datatable (#24981) 2025-04-09 16:16:47 +02:00
Paul Bottein
9bc867d0dc
Restore no grouping from local storage from datatable (#24979)
* Restore no grouping from localstorage

* Fix collapse/expand button
2025-04-09 14:15:27 +00:00
Bram Kragten
590df8dd1a
Restore media browser to browser when entity is not in state machine (#24982)
restore media browser to browser when entity is not in state machine
2025-04-09 15:49:16 +02:00
Wendelin
ccee57f4a5
Improve background settings and fix save button (#24978) 2025-04-09 13:04:07 +03:00
Jan-Philipp Benecke
828bf977b2
Migrate icon overflow menu to ha-md-button-menu (#24973)
Migrate icon overflow menu to `ha-md-button-menut`
2025-04-09 09:24:58 +03:00
Jan-Philipp Benecke
a2b3ea2ac6
Align automation trace tab order with script tab order (#24974) 2025-04-09 09:05:34 +03:00
Paul Bottein
9c3f77532c
Make the full generic entity row clickable (#24968)
* Make the full generic entity row clickable

* Apply suggestions from code review

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

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-04-08 15:17:05 +02:00
Paul Bottein
4a1cf250c4
Update datatable in devices config page (#24967) 2025-04-08 14:21:10 +02:00
Paul Bottein
9df5141aac
Fix data-table sort by unknown column (#24965)
Fix database sort by unknown column
2025-04-08 13:05:22 +02:00
Wendelin
13aeb02b53
Fix submit spinner in config-flow-form (#24969) 2025-04-08 12:39:42 +02:00
Jan-Philipp Benecke
f0f60bae78
Make some parts of shortcuts in dialog translatable (#24955)
* Make some parts of shortcuts in dialog translatable

* Adjust translations
2025-04-08 08:43:54 +03:00
renovate[bot]
d1465a79ae
Update dependency typescript to v5.8.3 (#24964)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-08 08:42:09 +03:00
renovate[bot]
6fe8af7c75
Update dependency eslint to v9.24.0 (#24962)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-08 08:41:47 +03:00
Bram Kragten
21180d066e
Allow to turn of debugConnection in dev (#24956) 2025-04-07 15:52:06 +02:00
Paul Bottein
dec968af54
Restore default hold action for some cards (#24947) 2025-04-07 13:18:05 +02:00
Bram Kragten
2ccc5355c4
fix voice wizard bugs (#24950) 2025-04-07 10:41:29 +00:00
Jan-Philipp Benecke
316c3f4e1f
Use --outline-color in shortcuts dialog (#24949) 2025-04-07 12:37:19 +02:00
renovate[bot]
f88d0ca613
Update dependency @types/luxon to v3.6.2 (#24946)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-07 12:35:37 +02:00
renovate[bot]
edd4a3c31f
Update vaadinWebComponents monorepo to v24.7.2 (#24941)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-06 20:57:57 +02:00
renovate[bot]
a7ee98e7de
Update dependency @types/luxon to v3.6.1 (#24944)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-06 20:57:43 +02:00
renovate[bot]
1b6ed8cdc3
Update rspack monorepo to v1.3.2 (#24942)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-06 17:17:33 +03:00
Jan-Philipp Benecke
671049beb2
Add dialog to show keyboard shortcuts (#24918)
* Add dialog to show keyboard shortcuts we have

* Add missing translation

* No need for function anymore

* Run updated prettier

* Replace translation keys

* Replace translation keys

* Remove automations for now

* Check whether shortcuts are enabled

* Use plain css for shortcuts
2025-04-06 09:02:52 +02:00
renovate[bot]
daf4158fa0
Update dependency @lokalise/node-api to v14.2.1 (#24933)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-05 20:03:38 +02:00
renovate[bot]
848713858f
Update fullcalendar monorepo to v6.1.17 (#24934)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-05 20:03:24 +02:00
Jan-Philipp Benecke
f0ef7e0c53
Import missing ha-tip in quick bar dialog (#24929) 2025-04-05 16:39:34 +02:00
renovate[bot]
e10b0fad95
Update rspack monorepo to v1.3.1 (#24925)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-04 18:18:14 +02:00
dependabot[bot]
8d50bb1d2b
Bump vite from 6.2.4 to 6.2.5 (#24928)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.4 to 6.2.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.5/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.2.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-04 18:16:24 +02:00
Simon Lamon
a15f0c7814
Show the correct area icon in entity breadcrumb (#24913) 2025-04-04 15:09:36 +02:00
renovate[bot]
e37f7219c2
Update dependency typescript-eslint to v8.29.0 (#24916)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-04 08:58:50 +02:00
renovate[bot]
570076c539
Update dependency luxon to v3.6.1 (#24915)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-04 08:46:19 +02:00
renovate[bot]
cfeb0336d1
Update vitest monorepo to v3.1.1 (#24907)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-03 18:30:18 +02:00
renovate[bot]
b18cc4dcfb
Update dependency @codemirror/commands to v6.8.1 (#24906)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-03 18:30:12 +02:00
Paul Bottein
e271989cee
Add missing translations for areas strategy (#24905) 2025-04-03 14:02:27 +02:00
Paul Bottein
ca223f9d73
Refresh dashboard strategy when registries changed (#24902)
* Refresh dashboard strategy when registries changed

* Display toast before refreshing dashboard

* Apply suggestions
2025-04-03 10:10:11 +00:00
renovate[bot]
8fb1cf35ad
Update Yarn to v4.8.1 (#24894)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 15:14:30 +03:00
karwosts
9f59be492e
Render autogenerated step descriptions in trace path viewer (#24886) 2025-04-02 14:04:34 +03:00
Alex Gustafsson
8429d114a8
Improve toggle button for disabled combo boxes (#24843) 2025-04-01 19:07:34 +02:00
Bram Kragten
4fbc155f8b
Use md list in config navigation (#24885) 2025-04-01 18:11:29 +02:00
Bram Kragten
cd39e2d0f2
Developer tools action fixes (#24876) 2025-04-01 13:18:04 +03:00
Paul Bottein
a23f57256c
Add ellipsis for more info breadcrumb (#24882) 2025-04-01 13:11:13 +03:00
renovate[bot]
c279efaa99
Update dependency @codemirror/view to v6.36.5 (#24881)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-01 13:08:04 +03:00
karwosts
c4389ec119
Fix condition rendering in trace choose node (#24878) 2025-04-01 10:07:25 +02:00
Clemens Brauers
50d632f8d4
Add area and category as columns in automation, scenes and scripts (#24874)
Add area and category as optional columns in automation, scenes and scripts

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-04-01 06:56:02 +00:00
Petar Petrov
dba2fba828
Revert "Update rspack monorepo to v1.3.0" (#24879)
Revert "Update rspack monorepo to v1.3.0 (#24862)"

This reverts commit 8a2ab2eab424706da77369bbf8cbdf444edd0317.
2025-04-01 06:29:14 +00:00
dependabot[bot]
3890afddb9
Bump vite from 6.2.3 to 6.2.4 (#24873)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.3 to 6.2.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.4/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-31 20:51:13 +02:00
Norbert Rittel
76f187ee2c
Properly sentence-case "assistant" / "pipeline" (#24872) 2025-03-31 19:31:00 +02:00
Bram Kragten
488b54cf19
Fix add zwave device my link (#24871) 2025-03-31 17:01:15 +03:00
Bram Kragten
29d2c29af3
fix spinner in tts try dialog (#24867) 2025-03-31 15:06:15 +02:00
renovate[bot]
a2f9101a9f
Update Yarn to v4.8.0 (#24863)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 16:04:51 +03:00
Paul Bottein
7893eba7a7
Handle date range shift during daylight saving time days (#24868) 2025-03-31 14:58:26 +02:00
Paul Bottein
94ced8af32
Add interactions for weather card editor (#24864) 2025-03-31 14:19:00 +02:00
Paul Bottein
c4b5882b2d
Force clock card to display time LTR (#24865) 2025-03-31 14:18:39 +02:00
Bram Kragten
6e8bac2e58
Take lang into account when search existing pipeline (#24866)
* Take lang into account when search existing pipeline

* Simplify logic
2025-03-31 14:18:20 +02:00
renovate[bot]
8a2ab2eab4
Update rspack monorepo to v1.3.0 (#24862)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 13:14:34 +02:00
karwosts
c7e5be185d
Bar charts start from 0 (#24854) 2025-03-31 12:13:03 +02:00
dependabot[bot]
e98721aa76
Bump home-assistant/wheels from 2025.02.0 to 2025.03.0 (#24860)
Bumps [home-assistant/wheels](https://github.com/home-assistant/wheels) from 2025.02.0 to 2025.03.0.
- [Release notes](https://github.com/home-assistant/wheels/releases)
- [Commits](https://github.com/home-assistant/wheels/compare/2025.02.0...2025.03.0)

---
updated-dependencies:
- dependency-name: home-assistant/wheels
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-31 09:10:57 +02:00
renovate[bot]
4c8d661c63
Update dependency @rsdoctor/rspack-plugin to v1.0.1 (#24853)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-30 15:57:18 +02:00
renovate[bot]
b7c60ffc74
Update dependency @material/web to v2.3.0 (#24850)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-30 09:08:38 +02:00
Jan-Philipp Benecke
db6c728cd6
Fix inner border radius for disabled bar in automation/script rows (#24840) 2025-03-29 09:54:09 +01:00
renovate[bot]
34f8335a9d
Update dependency @formatjs/intl-datetimeformat to v6.18.0 (#24841)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-29 08:23:21 +01:00
renovate[bot]
ecf5068bd0
Update dependency luxon to v3.6.0 (#24837)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-28 23:48:40 +01:00
Bram Kragten
0a2a2b8a70
Name local pipeline based on local or full choice (#24835) 2025-03-28 18:29:47 +01:00
Paul Bottein
52f4fe6bc0
Only use button for breadcrumb for admin users (#24836) 2025-03-28 13:03:56 -04:00
Bram Kragten
a781bca94b
Update lang support text in voice wizard (#24834) 2025-03-28 16:04:48 +00:00
Paul Bottein
63b44c25f8
Remove add-on word in satellite wizard translations for state (#24832) 2025-03-28 15:01:09 +00:00
Eloy Rodriguez
b96319703a
Add title and time zone to clock card (#24818)
* Add title and time zone to clock card

* Small changes to the spacing and text sizing of the clock card

* Update translations to use dropdown labels from profile configuration

* Use similar approach as #24819 for setting automatic time zone

* Update hui-clock-card.ts

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-03-28 14:32:50 +00:00
Paul Bottein
9e686190f6
More info breadcrumb clickable (#24830)
* Make more info breadcrum clickable

* css adjustements
2025-03-28 15:26:09 +01:00
Darren Griffin
5ca7b1d508
Fix default time_format option. Fixes #24798 (#24819)
* Fix default time_format option. Fixes #24798

* Update en.json

* Update src/translations/en.json

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-03-28 14:13:50 +00:00
Bram Kragten
7c1d74c6c3 Update voice-assistant-setup-step-local.ts 2025-03-28 15:01:08 +01:00
Bram Kragten
d257f667c1
Update text voice wizard install addons step (#24829) 2025-03-28 13:06:54 +00:00
Paulus Schoutsen
842a064682
Hide backup from default dashboard (#24828) 2025-03-28 12:55:39 +00:00
Bram Kragten
3d8e146582
Fix voice flow (#24825)
* Fix voice flow

* Apply suggestions from code review

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-03-28 12:53:08 +00:00
Paulus Schoutsen
78e8bd4305
Do not play pre-announce sound when testing voice on satellite (#24827) 2025-03-28 08:40:57 -04:00
Paul Bottein
0152a79bd5
Add hold and double tap action in the UI for every card that supports it. (#24824)
* Add double tap action to button card UI editor

* Add double tap action to light card UI editor

* Add hold action and double tap action to gauge card UI editor

* Add hold action and double tap action to picture glance card UI editor

* Add hold action and double tap action to picture card UI editor

* Add hold action and double tap action to entity card UI editor

* Add hold action and double tap action to elements
2025-03-28 13:12:07 +01:00
Paul Bottein
f5bb72f067
Add scroll restoration when using back navigation in dashboard (#24822)
Add scroll restoration when using back navigation with subviews
2025-03-28 12:07:42 +01:00
Alex Gustafsson
9ca6a886f5
Fix hide clear icon of entity picker (#24821) 2025-03-28 08:04:40 +00:00
renovate[bot]
f39011f8f4
Update dependency sinon to v20 (#24810)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-28 09:49:00 +02:00
puddly
8b190867e3
Show hardware integrations in the integration list (#24820)
Show hardware integrations in the frontend
2025-03-28 08:25:07 +02:00
renovate[bot]
321b15a270
Update dependency typescript-eslint to v8.28.0 (#24806)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-27 19:44:12 +01:00
renovate[bot]
6ba235d540
Update babel monorepo to v7.27.0 (#24807)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-27 19:43:54 +01:00
renovate[bot]
e34fd8161c
Update dependency sinon to v19.0.5 (#24809)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-27 19:43:38 +01:00
Paul Bottein
084cda8218
Fix dashboard strategy (#24808) 2025-03-27 18:09:46 +00:00
Paul Bottein
f06a0fa34c
Fallback to state name when the entry doesn't have name (#24805) 2025-03-27 18:51:08 +01:00
Paul Bottein
750c59399b
Set the max number of columns to 3 for area dashboard (#24802)
* Set the max number of columns to 4 for area dashboard

* Set it to 3
2025-03-27 16:39:47 +01:00
Paul Bottein
a6a17cd70c
Add loading state to area strategy (#24803) 2025-03-27 15:37:32 +00:00
karwosts
de1c6a5178
Energy device settings fixes (#24801) 2025-03-27 16:30:17 +01:00
Bram Kragten
04c3cd7d68
Align behavior of template selector with text selector (#24796) 2025-03-27 13:53:35 +01:00
Paul Bottein
17ef74d680
Fix take control of the dashboard (#24800) 2025-03-27 13:33:52 +01:00
Paul Bottein
098c6a2567
Remove fixed height in ha tile info (#24787)
Remove unused height in ha tile info
2025-03-27 11:10:45 +01:00
Paul Bottein
899288ae43
Revert "Restore scroll position when using back navigation in dashboard" (#24795)
Revert "Restore scroll position when using back navigation in dashboard (#24777)"

This reverts commit 9cfcd21a93dd50d61fb64039ce7bec973c721806.
2025-03-27 11:10:23 +01:00
Paul Bottein
a9823f30e3
Fix more info for disabled entities (#24789) 2025-03-27 08:49:25 +02:00
Bram Kragten
97966805fa
Fix typo in Arithmetic (#24786)
Fix type in Arithmetic
2025-03-26 16:13:11 +01:00
235 changed files with 9544 additions and 6173 deletions

View File

@ -26,7 +26,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@ -62,7 +62,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -26,7 +26,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@ -60,7 +60,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@ -78,7 +78,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@ -102,7 +102,7 @@ jobs:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -27,7 +27,7 @@ jobs:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@ -63,7 +63,7 @@ jobs:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -34,7 +34,7 @@ jobs:
uses: home-assistant/actions/helpers/verify-version@master
- name: Setup Node
uses: actions/setup-node@v4.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@ -74,7 +74,7 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels
uses: home-assistant/wheels@2025.02.0
uses: home-assistant/wheels@2025.03.0
with:
abi: cp313
tag: musllinux_1_2
@ -92,7 +92,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn
@ -121,7 +121,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
- name: Setup Node
uses: actions/setup-node@v4.3.0
uses: actions/setup-node@v4.4.0
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@ -1,34 +0,0 @@
diff --git a/lib/legacy/class.js b/lib/legacy/class.js
index aee2511be1cd9bf900ee552bc98190c1631c57c0..f2f499d68bf52034cac9c28307c99e8ce6b8417d 100644
--- a/lib/legacy/class.js
+++ b/lib/legacy/class.js
@@ -304,17 +304,23 @@ function GenerateClassFromInfo(info, Base, behaviors) {
// only proceed if the generated class' prototype has not been registered.
const generatedProto = PolymerGenerated.prototype;
if (!generatedProto.hasOwnProperty(JSCompiler_renameProperty('__hasRegisterFinished', generatedProto))) {
- generatedProto.__hasRegisterFinished = true;
+ // make sure legacy lifecycle is called on the *element*'s prototype
+ // and not the generated class prototype; if the element has been
+ // extended, these are *not* the same.
+ const proto = Object.getPrototypeOf(this);
+ // Only set flag when generated prototype itself is registered,
+ // as this element may be extended from, and needs to run `registered`
+ // on all behaviors on the subclass as well.
+ if (proto === generatedProto) {
+ generatedProto.__hasRegisterFinished = true;
+ }
// ensure superclass is registered first.
super._registered();
// copy properties onto the generated class lazily if we're optimizing,
- if (legacyOptimizations) {
+ if (legacyOptimizations && !Object.hasOwnProperty(generatedProto, '__hasCopiedProperties')) {
+ generatedProto.__hasCopiedProperties = true;
copyPropertiesToProto(generatedProto);
}
- // make sure legacy lifecycle is called on the *element*'s prototype
- // and not the generated class prototype; if the element has been
- // extended, these are *not* the same.
- const proto = Object.getPrototypeOf(this);
let list = lifecycle.beforeRegister;
if (list) {
for (let i=0; i < list.length; i++) {

File diff suppressed because one or more lines are too long

948
.yarn/releases/yarn-4.9.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.7.0.cjs
yarnPath: .yarn/releases/yarn-4.9.1.cjs

View File

@ -2,7 +2,7 @@ import defineProvider from "@babel/helper-define-polyfill-provider";
import { join } from "node:path";
import paths from "../paths.cjs";
const POLYFILL_DIR = join(paths.polymer_dir, "src/resources/polyfills");
const POLYFILL_DIR = join(paths.root_dir, "src/resources/polyfills");
// List of polyfill keys with supported browser targets for the functionality
const polyfillSupport = {

View File

@ -20,22 +20,16 @@ module.exports.ignorePackages = () => [];
// Files from NPM packages that we should replace with empty file
module.exports.emptyPackages = ({ isHassioBuild }) =>
[
// Contains all color definitions for all material color sets.
// We don't use it
require.resolve("@polymer/paper-styles/color.js"),
require.resolve("@polymer/paper-styles/default-theme.js"),
// Loads stuff from a CDN
require.resolve("@polymer/font-roboto/roboto.js"),
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
// Icons in supervisor conflict with icons in HA so we don't load.
isHassioBuild &&
require.resolve(
path.resolve(paths.polymer_dir, "src/components/ha-icon.ts")
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
),
isHassioBuild &&
require.resolve(
path.resolve(paths.polymer_dir, "src/components/ha-icon-picker.ts")
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
),
].filter(Boolean);
@ -50,7 +44,8 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__HASS_URL__: `\`${
"HASS_URL" in process.env
? process.env.HASS_URL
: "${location.protocol}//${location.host}"
: // eslint-disable-next-line no-template-curly-in-string
"${location.protocol}//${location.host}"
}\``,
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development"
@ -164,7 +159,7 @@ module.exports.babelOptions = ({
],
],
exclude: [
path.join(paths.polymer_dir, "src/resources/polyfills"),
path.join(paths.root_dir, "src/resources/polyfills"),
...[
"@formatjs/(?:ecma402-abstract|intl-\\w+)",
"@lit-labs/virtualizer/polyfills",
@ -182,6 +177,7 @@ module.exports.babelOptions = ({
include: /\/node_modules\//,
exclude: [
"element-internals-polyfill",
"@shoelace-style",
"@?lit(?:-labs|-element|-html)?",
].map((p) => new RegExp(`/node_modules/${p}/`)),
},

View File

@ -21,7 +21,7 @@ module.exports = {
},
version() {
const version = fs
.readFileSync(path.resolve(paths.polymer_dir, "pyproject.toml"), "utf8")
.readFileSync(path.resolve(paths.root_dir, "pyproject.toml"), "utf8")
.match(/version\W+=\W"(\d{8}\.\d(?:\.dev)?)"/);
if (!version) {
throw Error("Version not found");

View File

@ -169,14 +169,14 @@ const APP_PAGE_ENTRIES = {
gulp.task(
"gen-pages-app-dev",
genPagesDevTask(APP_PAGE_ENTRIES, paths.polymer_dir, paths.app_output_root)
genPagesDevTask(APP_PAGE_ENTRIES, paths.root_dir, paths.app_output_root)
);
gulp.task(
"gen-pages-app-prod",
genPagesProdTask(
APP_PAGE_ENTRIES,
paths.polymer_dir,
paths.root_dir,
paths.app_output_root,
paths.app_output_latest,
paths.app_output_es5

View File

@ -6,8 +6,8 @@ import path from "path";
import paths from "../paths.cjs";
const npmPath = (...parts) =>
path.resolve(paths.polymer_dir, "node_modules", ...parts);
const polyPath = (...parts) => path.resolve(paths.polymer_dir, ...parts);
path.resolve(paths.root_dir, "node_modules", ...parts);
const polyPath = (...parts) => path.resolve(paths.root_dir, ...parts);
const copyFileDir = (fromFile, toDir) =>
fs.copySync(fromFile, path.join(toDir, path.basename(fromFile)));

View File

@ -4,7 +4,7 @@ import gulp from "gulp";
import { join, resolve } from "node:path";
import paths from "../paths.cjs";
const formatjsDir = join(paths.polymer_dir, "node_modules", "@formatjs");
const formatjsDir = join(paths.root_dir, "node_modules", "@formatjs");
const outDir = join(paths.build_dir, "locale-data");
const INTL_POLYFILLS = {

View File

@ -1,7 +1,7 @@
const path = require("path");
module.exports = {
polymer_dir: path.resolve(__dirname, ".."),
root_dir: path.resolve(__dirname, ".."),
build_dir: path.resolve(__dirname, "../build"),
app_output_root: path.resolve(__dirname, "../hass_frontend"),

View File

@ -161,7 +161,7 @@ const createRspackConfig = ({
}),
new rspack.NormalModuleReplacementPlugin(
new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")),
path.resolve(paths.polymer_dir, "src/util/empty.js")
path.resolve(paths.root_dir, "src/util/empty.js")
),
!isProdBuild && new LogStartCompilePlugin(),
isProdBuild &&

View File

@ -309,7 +309,7 @@ export class HcMain extends HassElement {
"../../../../src/panels/lovelace/strategies/get-strategy"
);
const config = await generateLovelaceDashboardStrategy(
rawConfig.strategy,
rawConfig,
this.hass!
);
this._handleNewLovelaceConfig(config);
@ -351,10 +351,7 @@ export class HcMain extends HassElement {
"../../../../src/panels/lovelace/strategies/get-strategy"
);
this._handleNewLovelaceConfig(
await generateLovelaceDashboardStrategy(
DEFAULT_CONFIG.strategy,
this.hass!
)
await generateLovelaceDashboardStrategy(DEFAULT_CONFIG, this.hass!)
);
}

View File

@ -3,7 +3,6 @@ export const demoThemeJimpower = () => ({
"paper-item-icon-color": "var(--primary-text-color)",
"primary-color": "#5294E2",
"label-badge-red": "var(--accent-color)",
"paper-tabs-selection-bar-color": "green",
"light-primary-color": "var(--accent-color)",
"primary-background-color": "#383C45",
"primary-text-color": "#FFFFFF",

View File

@ -4,7 +4,6 @@ export const demoThemeKernehed = () => ({
"paper-item-icon-color": "var(--primary-text-color)",
"primary-color": "#2980b9",
"label-badge-red": "var(--accent-color)",
"paper-tabs-selection-bar-color": "green",
"primary-text-color": "#FFFFFF",
"light-primary-color": "var(--accent-color)",
"primary-background-color": "#222222",

View File

@ -42,7 +42,6 @@ export default tseslint.config(
__VERSION__: false,
__STATIC_PATH__: false,
__SUPERVISOR__: false,
Polymer: true,
},
parser: tseslint.parser,

View File

@ -16,23 +16,14 @@ import type { HomeAssistant } from "../../../../src/types";
import type { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware";
const _filterDevices = memoizeOne(
(
showAdvanced: boolean,
hardware: HassioHardwareInfo,
filter: string,
language: string
) =>
(hardware: HassioHardwareInfo, filter: string, language: string) =>
hardware.devices
.filter(
(device) =>
(showAdvanced ||
["tty", "gpio", "input"].includes(device.subsystem)) &&
(device.by_id?.toLowerCase().includes(filter) ||
device.name.toLowerCase().includes(filter) ||
device.dev_path.toLocaleLowerCase().includes(filter) ||
JSON.stringify(device.attributes)
.toLocaleLowerCase()
.includes(filter))
device.by_id?.toLowerCase().includes(filter) ||
device.name.toLowerCase().includes(filter) ||
device.dev_path.toLocaleLowerCase().includes(filter) ||
JSON.stringify(device.attributes).toLocaleLowerCase().includes(filter)
)
.sort((a, b) => stringCompare(a.name, b.name, language))
);
@ -60,7 +51,6 @@ class HassioHardwareDialog extends LitElement {
}
const devices = _filterDevices(
this.hass.userData?.showAdvanced || false,
this._dialogParams.hardware,
(this._filter || "").toLowerCase(),
this.hass.locale.language

View File

@ -1,9 +1,6 @@
import "./hassio-main";
import("../../src/resources/ha-style");
import("@polymer/polymer/lib/utils/settings").then(
({ setCancelSyntheticClickEvents }) => setCancelSyntheticClickEvents(false)
);
const styleEl = document.createElement("style");
styleEl.textContent = `

View File

@ -26,17 +26,17 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.26.10",
"@babel/runtime": "7.27.0",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/commands": "6.8.0",
"@codemirror/commands": "6.8.1",
"@codemirror/language": "6.11.0",
"@codemirror/legacy-modes": "6.5.0",
"@codemirror/search": "6.5.10",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.36.4",
"@codemirror/view": "6.36.5",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.17.4",
"@formatjs/intl-datetimeformat": "6.18.0",
"@formatjs/intl-displaynames": "6.8.11",
"@formatjs/intl-durationformat": "0.7.4",
"@formatjs/intl-getcanonicallocales": "2.5.5",
@ -45,12 +45,12 @@
"@formatjs/intl-numberformat": "8.15.4",
"@formatjs/intl-pluralrules": "5.4.4",
"@formatjs/intl-relativetimeformat": "11.4.11",
"@fullcalendar/core": "6.1.15",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/interaction": "6.1.15",
"@fullcalendar/list": "6.1.15",
"@fullcalendar/luxon3": "6.1.15",
"@fullcalendar/timegrid": "6.1.15",
"@fullcalendar/core": "6.1.17",
"@fullcalendar/daygrid": "6.1.17",
"@fullcalendar/interaction": "6.1.17",
"@fullcalendar/list": "6.1.17",
"@fullcalendar/luxon3": "6.1.17",
"@fullcalendar/timegrid": "6.1.17",
"@lezer/highlight": "1.2.1",
"@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.8",
@ -81,20 +81,16 @@
"@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "2.2.0",
"@material/web": "2.3.0",
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@polymer/paper-item": "3.0.1",
"@polymer/paper-listbox": "3.0.1",
"@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.2",
"@replit/codemirror-indentation-markers": "6.5.3",
"@shoelace-style/shoelace": "2.20.1",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.8.1",
"@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.7.1",
"@vaadin/vaadin-themable-mixin": "24.7.1",
"@vaadin/combo-box": "24.7.3",
"@vaadin/vaadin-themable-mixin": "24.7.3",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
@ -111,12 +107,12 @@
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
"echarts": "5.6.0",
"element-internals-polyfill": "3.0.1",
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.4.0",
"home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.7.16",
"js-yaml": "4.1.0",
@ -125,8 +121,8 @@
"leaflet.markercluster": "1.5.3",
"lit": "2.8.0",
"lit-html": "2.8.0",
"luxon": "3.5.0",
"marked": "15.0.7",
"luxon": "3.6.1",
"marked": "15.0.8",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
@ -159,15 +155,15 @@
"@babel/plugin-proposal-decorators": "7.25.9",
"@babel/plugin-transform-runtime": "7.26.10",
"@babel/preset-env": "7.26.9",
"@babel/preset-typescript": "7.26.0",
"@babel/preset-typescript": "7.27.0",
"@bundle-stats/plugin-webpack-filter": "4.19.1",
"@lokalise/node-api": "14.2.0",
"@octokit/auth-oauth-device": "7.1.4",
"@octokit/plugin-retry": "7.2.0",
"@lokalise/node-api": "14.3.0",
"@octokit/auth-oauth-device": "7.1.5",
"@octokit/plugin-retry": "7.2.1",
"@octokit/rest": "21.1.1",
"@rsdoctor/rspack-plugin": "1.0.0",
"@rspack/cli": "1.2.8",
"@rspack/core": "1.2.8",
"@rsdoctor/rspack-plugin": "1.0.1",
"@rspack/cli": "1.3.5",
"@rspack/core": "1.3.5",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.21",
"@types/chromecast-caf-sender": "1.0.11",
@ -179,24 +175,24 @@
"@types/leaflet-draw": "1.0.11",
"@types/leaflet.markercluster": "1.5.5",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2",
"@types/luxon": "3.6.2",
"@types/mocha": "10.0.10",
"@types/qrcode": "1.5.5",
"@types/sortablejs": "1.15.8",
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "3.0.9",
"@vitest/coverage-v8": "3.1.1",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.0",
"eslint": "9.23.0",
"eslint": "9.24.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.1",
"eslint-config-prettier": "10.1.2",
"eslint-import-resolver-webpack": "0.13.10",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-lit": "2.0.0",
"eslint-plugin-lit": "2.1.1",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "3.0.0",
@ -209,9 +205,9 @@
"gulp-rename": "2.0.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "26.0.0",
"jsdom": "26.1.0",
"jszip": "3.10.1",
"lint-staged": "15.5.0",
"lint-staged": "15.5.1",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
@ -220,29 +216,27 @@
"prettier": "3.5.3",
"rspack-manifest-plugin": "5.0.3",
"serve": "14.2.4",
"sinon": "19.0.4",
"sinon": "20.0.0",
"tar": "7.4.3",
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.8.2",
"typescript-eslint": "8.27.0",
"typescript": "5.8.3",
"typescript-eslint": "8.30.1",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.0.9",
"vitest": "3.1.1",
"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"
},
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.5.2#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@material/mwc-button@^0.25.3": "^0.27.0",
"lit": "2.8.0",
"lit-html": "2.8.0",
"clean-css": "5.3.3",
"@lit/reactive-element": "1.6.3",
"@fullcalendar/daygrid": "6.1.15",
"@fullcalendar/daygrid": "6.1.17",
"globals": "16.0.0",
"tslib": "2.8.1"
},
"packageManager": "yarn@4.7.0"
"packageManager": "yarn@4.9.1"
}

View File

@ -0,0 +1,15 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8283 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 39.4999L76.9105 39.4999V36.4999L37.5 36.4999L37.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M30.8239 22.3365L38.8239 38.8365L30.3239 50.3365" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
<mask id="mask0_1110_23734" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1110_23734)">
<rect x="30" y="27" width="18" height="18" fill="#212121"/>
</g>
<path d="M82 37.9999C82 36.343 83.3431 34.9999 85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
<rect x="23" y="11" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
<rect x="22" y="52" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/>
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/>
<circle cx="39" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,15 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.824 22.3365L38.824 38.8365L30.324 50.3365" stroke="white" stroke-opacity="0.24" stroke-width="3" stroke-linecap="round"/>
<mask id="mask0_1180_4955" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18">
<path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1180_4955)">
<rect x="30" y="27" width="18" height="18" fill="#00AFFF"/>
</g>
<path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8283 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 39.4999L76.9105 39.4999V36.4999L37.5 36.4999L37.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M82 37.9999C82 36.343 83.3431 34.9999 85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
<rect x="23" y="11" width="8" height="8" rx="4" fill="white" fill-opacity="0.48"/>
<rect x="22" y="52" width="8" height="8" rx="4" fill="white" fill-opacity="0.48"/>
<path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="white" stroke-opacity="0.24" stroke-width="3" stroke-linecap="round"/>
<circle cx="39" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,19 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="47" cy="36" r="34" fill="white"/>
<circle cx="47" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/>
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_1110_23775" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1110_23775)">
<rect x="38" y="27" width="18" height="18" fill="#212121"/>
</g>
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,19 @@
<svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<circle cx="47" cy="36" r="34" fill="#1C1C1C"/>
<circle cx="47" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/>
<path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<mask id="mask0_1180_4965" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18">
<path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/>
</mask>
<g mask="url(#mask0_1180_4965)">
<rect x="38" y="27" width="18" height="18" fill="#00AFFF"/>
</g>
<path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/>
<rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/>
<path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/>
<path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -265,7 +265,10 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
);
}
if (window.innerWidth > 450) {
if (
window.innerWidth > 450 &&
!matchMedia("(prefers-reduced-motion)").matches
) {
import("../resources/particles");
}

View File

@ -6,6 +6,10 @@ import {
differenceInMilliseconds,
differenceInMonths,
endOfMonth,
startOfDay,
endOfDay,
differenceInDays,
addDays,
} from "date-fns";
import { toZonedTime, fromZonedTime } from "date-fns-tz";
import type { HassConfig } from "home-assistant-js-websocket";
@ -100,6 +104,32 @@ export const shiftDateRange = (
locale,
config
);
} else if (
calcDateProperty(
startDate,
(date) => startOfDay(date).getMilliseconds() === date.getMilliseconds(),
locale,
config
) &&
calcDateProperty(
endDate,
(date) => endOfDay(date).getMilliseconds() === date.getMilliseconds(),
locale,
config
)
) {
const difference =
((calcDateDifferenceProperty(
endDate,
startDate,
differenceInDays,
locale,
config
) as number) +
1) *
(forward ? 1 : -1);
start = calcDate(startDate, addDays, locale, config, difference);
end = calcDate(endDate, addDays, locale, config, difference);
} else {
const difference =
((calcDateDifferenceProperty(

View File

@ -84,12 +84,12 @@ export const calcDateRange = (
case "now-7d":
return [
calcDate(today, subDays, hass.locale, hass.config, 7),
calcDate(today, subDays, hass.locale, hass.config, 1),
calcDate(today, subDays, hass.locale, hass.config, 0),
];
case "now-30d":
return [
calcDate(today, subDays, hass.locale, hass.config, 30),
calcDate(today, subDays, hass.locale, hass.config, 1),
calcDate(today, subDays, hass.locale, hass.config, 0),
];
case "now-12m":
return [

View File

@ -134,10 +134,7 @@ export const applyThemesOnElement = (
element.__themes = { cacheKey, keys: newTheme?.keys };
// Set and/or reset styles
if (element.updateStyles) {
// Use updateStyles() method of Polymer elements
element.updateStyles(styles);
} else if (window.ShadyCSS) {
if (window.ShadyCSS) {
// Use ShadyCSS if available
window.ShadyCSS.styleSubtree(/** @type {!HTMLElement} */ element, styles);
} else {

View File

@ -1,3 +1,4 @@
import memoizeOne from "memoize-one";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type {
EntityRegistryDisplayEntry,
@ -5,6 +6,7 @@ import type {
} from "../../data/entity_registry";
import type { HomeAssistant } from "../../types";
import { computeStateName } from "./compute_state_name";
import { getDuplicates } from "../string/get_duplicates";
export const computeDeviceName = (
device: DeviceRegistryEntry
@ -36,3 +38,13 @@ export const fallbackDeviceName = (
}
return undefined;
};
export const getDuplicatedDeviceNames = memoizeOne(
(devices: HomeAssistant["devices"]): Set<string> => {
const names = Object.values(devices)
.map((device) => computeDeviceName(device))
.filter((name): name is string => name !== undefined);
return getDuplicates(names);
}
);

View File

@ -33,7 +33,14 @@ export const computeEntityEntryName = (
const device = entry.device_id ? hass.devices[entry.device_id] : undefined;
if (!device) {
return name;
if (name) {
return name;
}
const stateObj = hass.states[entry.entity_id] as HassEntity | undefined;
if (stateObj) {
return computeStateName(stateObj);
}
return undefined;
}
const deviceName = computeDeviceName(device);

View File

@ -1,7 +1,11 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { AreaRegistryEntry } from "../../data/area_registry";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
ExtEntityRegistryEntry,
} from "../../data/entity_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import type { HomeAssistant } from "../../types";
@ -19,6 +23,23 @@ export const getEntityContext = (
| EntityRegistryDisplayEntry
| undefined;
if (!entry) {
return {
device: null,
area: null,
floor: null,
};
}
return getEntityEntryContext(entry, hass);
};
export const getEntityEntryContext = (
entry:
| EntityRegistryDisplayEntry
| EntityRegistryEntry
| ExtEntityRegistryEntry,
hass: HomeAssistant
): EntityContext => {
const deviceId = entry?.device_id;
const device = deviceId ? hass.devices[deviceId] : null;
const areaId = entry?.area_id || device?.area_id;

View File

@ -5,7 +5,7 @@ import { getIntegrationDescriptions } from "../../data/integrations";
import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { showMatterAddDeviceDialog } from "../../panels/config/integrations/integration-panels/matter/show-dialog-add-matter-device";
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/add-node/show-dialog-zwave_js-add-node";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import { isComponentLoaded } from "../config/is_component_loaded";

View File

@ -0,0 +1,14 @@
export function getDuplicates(array: string[]): Set<string> {
const duplicates = new Set<string>();
const seen = new Set<string>();
for (const item of array) {
if (seen.has(item)) {
duplicates.add(item);
} else {
seen.add(item);
}
}
return duplicates;
}

View File

@ -1,13 +1,15 @@
import "@material/mwc-button";
import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../ha-button";
import "../ha-spinner";
import "../ha-svg-icon";
@customElement("ha-progress-button")
export class HaProgressButton extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public progress = false;
@ -21,14 +23,16 @@ export class HaProgressButton extends LitElement {
public render(): TemplateResult {
const overlay = this._result || this.progress;
return html`
<mwc-button
?raised=${this.raised}
<ha-button
.raised=${this.raised}
.label=${this.label}
.unelevated=${this.unelevated}
.disabled=${this.disabled || this.progress}
class=${this._result || ""}
>
<slot name="icon" slot="icon"></slot>
<slot></slot>
</mwc-button>
</ha-button>
${!overlay
? nothing
: html`
@ -68,12 +72,12 @@ export class HaProgressButton extends LitElement {
pointer-events: none;
}
mwc-button {
ha-button {
transition: all 1s;
pointer-events: initial;
}
mwc-button.success {
ha-button.success {
--mdc-theme-primary: white;
background-color: var(--success-color);
transition: none;
@ -81,13 +85,13 @@ export class HaProgressButton extends LitElement {
pointer-events: none;
}
mwc-button[unelevated].success,
mwc-button[raised].success {
ha-button[unelevated].success,
ha-button[raised].success {
--mdc-theme-primary: var(--success-color);
--mdc-theme-on-primary: white;
}
mwc-button.error {
ha-button.error {
--mdc-theme-primary: white;
background-color: var(--error-color);
transition: none;
@ -95,8 +99,8 @@ export class HaProgressButton extends LitElement {
pointer-events: none;
}
mwc-button[unelevated].error,
mwc-button[raised].error {
ha-button[unelevated].error,
ha-button[raised].error {
--mdc-theme-primary: var(--error-color);
--mdc-theme-on-primary: white;
}
@ -113,8 +117,8 @@ export class HaProgressButton extends LitElement {
color: white;
}
mwc-button.success slot,
mwc-button.error slot {
ha-button.success slot,
ha-button.error slot {
visibility: hidden;
}
:host([destructive]) {

View File

@ -296,7 +296,11 @@ export class StatisticsChart extends LitElement {
align: "left",
},
position: computeRTL(this.hass) ? "right" : "left",
scale: true,
scale:
this.chartType !== "bar" ||
this.logarithmicScale ||
minYAxis !== undefined ||
maxYAxis !== undefined,
min: this._clampYAxis(minYAxis),
max: this._clampYAxis(maxYAxis),
splitLine: {

View File

@ -645,15 +645,16 @@ export class HaDataTable extends LitElement {
return;
}
const prom = this.sortColumn
? sortData(
filteredData,
this._sortColumns[this.sortColumn],
this.sortDirection,
this.sortColumn,
this.hass.locale.language
)
: filteredData;
const prom =
this.sortColumn && this._sortColumns[this.sortColumn]
? sortData(
filteredData,
this._sortColumns[this.sortColumn],
this.sortDirection,
this.sortColumn,
this.hass.locale.language
)
: filteredData;
const [data] = await Promise.all([prom, nextRender]);

View File

@ -1,7 +1,7 @@
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
@ -19,7 +19,7 @@ import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-list-item";
import "../ha-combo-box-item";
interface Device {
name: string;
@ -35,11 +35,14 @@ export type HaDevicePickerDeviceFilterFunc = (
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
const rowRenderer: ComboBoxLitRenderer<Device> = (item) =>
html`<ha-list-item .twoline=${!!item.area}>
<span>${item.name}</span>
<span slot="secondary">${item.area}</span>
</ha-list-item>`;
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`
<ha-combo-box-item type="button">
<span slot="headline">${item.name}</span>
${item.area
? html`<span slot="supporting-text">${item.area}</span>`
: nothing}
</ha-combo-box-item>
`;
@customElement("ha-device-picker")
export class HaDevicePicker extends LitElement {

View File

@ -1,35 +1,78 @@
import "../ha-list-item";
import { mdiMagnify, mdiPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { computeStateName } from "../../common/entity/compute_state_name";
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
import type { ValueChangedEvent, HomeAssistant } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-icon-button";
import "../ha-svg-icon";
import "./state-badge";
import { getEntityContext } from "../../common/entity/get_entity_context";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
import { computeRTL } from "../../common/util/compute_rtl";
import { domainToName } from "../../data/integration";
import type { HelperDomain } from "../../panels/config/helpers/const";
import { isHelperDomain } from "../../panels/config/helpers/const";
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box-item";
import "../ha-icon-button";
import "../ha-list-item";
import "../ha-svg-icon";
import "./state-badge";
interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
friendly_name: string;
const FAKE_ENTITY: HassEntity = {
entity_id: "",
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
attributes: {},
};
interface EntityPickerItem extends HassEntity {
label: string;
primary: string;
secondary?: string;
translated_domain?: string;
show_entity_id?: boolean;
entity_name?: string;
area_name?: string;
device_name?: string;
friendly_name?: string;
sorting_label?: string;
icon_path?: string;
}
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
const CREATE_ID = "___create-new-entity___";
const DOMAIN_STYLE = styleMap({
fontSize: "12px",
fontWeight: "400",
lineHeight: "18px",
alignSelf: "flex-end",
maxWidth: "30%",
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
});
const ENTITY_ID_STYLE = styleMap({
fontFamily: "var(--code-font-family, monospace)",
fontSize: "11px",
});
@customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -106,8 +149,7 @@ export class HaEntityPicker extends LitElement {
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ attribute: "item-label-path" }) public itemLabelPath =
"friendly_name";
@property({ attribute: "item-label-path" }) public itemLabelPath = "label";
@state() private _opened = false;
@ -123,30 +165,48 @@ export class HaEntityPicker extends LitElement {
await this.comboBox?.focus();
}
private _initedStates = false;
private _initialItems = false;
private _states: HassEntityWithCachedName[] = [];
private _items: EntityPickerItem[] = [];
private _rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (
item
) =>
html`<ha-list-item graphic="avatar" .twoline=${!!item.entity_id}>
${item.state
? html`<state-badge
slot="graphic"
.stateObj=${item}
.hass=${this.hass}
></state-badge>`
: ""}
<span>${item.friendly_name}</span>
<span slot="secondary"
>${item.entity_id.startsWith(CREATE_ID)
? this.hass.localize("ui.components.entity.entity-picker.new_entity")
: item.entity_id}</span
>
</ha-list-item>`;
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.hass.loadBackendTranslation("title");
}
private _getStates = memoizeOne(
private _rowRenderer: ComboBoxLitRenderer<EntityPickerItem> = (
item,
{ index }
) => html`
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
${item.icon_path
? html`<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>`
: html`
<state-badge
slot="start"
.stateObj=${item}
.hass=${this.hass}
></state-badge>
`}
<span slot="headline">${item.primary} </span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.entity_id && item.show_entity_id
? html`<span slot="supporting-text" style=${ENTITY_ID_STYLE}
>${item.entity_id}</span
>`
: nothing}
${item.translated_domain && !item.show_entity_id
? html`<div slot="trailing-supporting-text" style=${DOMAIN_STYLE}>
${item.translated_domain}
</div>`
: nothing}
</ha-combo-box-item>
`;
private _getItems = memoizeOne(
(
_opened: boolean,
hass: this["hass"],
@ -158,8 +218,8 @@ export class HaEntityPicker extends LitElement {
includeEntities: this["includeEntities"],
excludeEntities: this["excludeEntities"],
createDomains: this["createDomains"]
): HassEntityWithCachedName[] => {
let states: HassEntityWithCachedName[] = [];
): EntityPickerItem[] => {
let states: EntityPickerItem[] = [];
if (!hass) {
return [];
@ -168,7 +228,7 @@ export class HaEntityPicker extends LitElement {
const createItems = createDomains?.length
? createDomains.map((domain) => {
const newFriendlyName = hass.localize(
const primary = hass.localize(
"ui.components.entity.entity-picker.create_helper",
{
domain: isHelperDomain(domain)
@ -180,16 +240,14 @@ export class HaEntityPicker extends LitElement {
);
return {
...FAKE_ENTITY,
entity_id: CREATE_ID + domain,
state: "on",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
friendly_name: newFriendlyName,
attributes: {
icon: "mdi:plus",
},
strings: [domain, newFriendlyName],
primary: primary,
label: primary,
secondary: this.hass.localize(
"ui.components.entity.entity-picker.new_entity"
),
icon_path: mdiPlus,
};
})
: [];
@ -197,21 +255,14 @@ export class HaEntityPicker extends LitElement {
if (!entityIds.length) {
return [
{
entity_id: "",
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
friendly_name: this.hass!.localize(
...FAKE_ENTITY,
primary: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
attributes: {
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
icon: "mdi:magnify",
},
strings: [],
label: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
icon_path: mdiMagnify,
},
...createItems,
];
@ -241,19 +292,49 @@ export class HaEntityPicker extends LitElement {
);
}
const isRTL = computeRTL(this.hass);
states = entityIds
.map((key) => {
const friendly_name = computeStateName(hass!.states[key]) || key;
.map<EntityPickerItem>((entityId) => {
const stateObj = hass!.states[entityId];
const { area, device } = getEntityContext(stateObj, hass);
const friendlyName = computeStateName(stateObj); // Keep this for search
const entityName = computeEntityName(stateObj, hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const translatedDomain = domainToName(
this.hass.localize,
computeDomain(entityId)
);
return {
...hass!.states[key],
friendly_name,
strings: [key, friendly_name],
...hass!.states[entityId],
primary: primary,
secondary:
secondary ||
this.hass.localize("ui.components.device-picker.no_area"),
label: friendlyName,
translated_domain: translatedDomain,
sorting_label: [deviceName, entityName].filter(Boolean).join("-"),
entity_name: entityName || deviceName,
area_name: areaName,
device_name: deviceName,
friendly_name: friendlyName,
show_entity_id: hass.userData?.showEntityIdPicker,
};
})
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name,
entityA.sorting_label!,
entityB.sorting_label!,
this.hass.locale.language
)
);
@ -291,21 +372,14 @@ export class HaEntityPicker extends LitElement {
if (!states.length) {
return [
{
entity_id: "",
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
friendly_name: this.hass!.localize(
...FAKE_ENTITY,
primary: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
attributes: {
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
icon: "mdi:magnify",
},
strings: [],
label: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
icon_path: mdiMagnify,
},
...createItems,
];
@ -331,8 +405,8 @@ export class HaEntityPicker extends LitElement {
}
public willUpdate(changedProps: PropertyValues) {
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
this._states = this._getStates(
if (!this._initialItems || (changedProps.has("_opened") && this._opened)) {
this._items = this._getItems(
this._opened,
this.hass,
this.includeDomains,
@ -344,10 +418,10 @@ export class HaEntityPicker extends LitElement {
this.excludeEntities,
this.createDomains
);
if (this._initedStates) {
this.comboBox.filteredItems = this._states;
if (this._initialItems) {
this.comboBox.filteredItems = this._items;
}
this._initedStates = true;
this._initialItems = true;
}
if (changedProps.has("createDomains") && this.createDomains?.length) {
@ -367,10 +441,11 @@ export class HaEntityPicker extends LitElement {
: this.label}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._states}
.filteredItems=${this._items}
.renderer=${this._rowRenderer}
.required=${this.required}
.disabled=${this.disabled}
.hideClearIcon=${this.hideClearIcon}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
@ -407,12 +482,49 @@ export class HaEntityPicker extends LitElement {
}
}
private _fuseKeys = [
"entity_name",
"device_name",
"area_name",
"translated_domain",
"friendly_name", // for backwards compatibility
"entity_id", // for technical search
];
private _fuseIndex = memoizeOne((states: EntityPickerItem[]) =>
Fuse.createIndex(this._fuseKeys, states)
);
private _filterChanged(ev: CustomEvent): void {
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.trim().toLowerCase();
target.filteredItems = filterString.length
? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states)
: this._states;
const filterString = ev.detail.value.trim().toLowerCase() as string;
const minLength = 2;
const searchTerms = (filterString.split(" ") ?? []).filter(
(term) => term.length >= minLength
);
if (searchTerms.length > 0) {
const index = this._fuseIndex(this._items);
const options: IFuseOptions<EntityPickerItem> = {
isCaseSensitive: false,
threshold: 0.3,
ignoreDiacritics: true,
minMatchCharLength: minLength,
};
const fuse = new Fuse(this._items, options, index);
const results = fuse.search({
$and: searchTerms.map((term) => ({
$or: this._fuseKeys.map((key) => ({ [key]: term })),
})),
});
target.filteredItems = results.map((result) => result.item);
} else {
target.filteredItems = this._items;
}
}
private _setValue(value: string | undefined) {

View File

@ -1,23 +1,23 @@
import "@material/mwc-list/mwc-list-item";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare";
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
import type { StatisticsMetaData } from "../../data/recorder";
import { getStatisticIds, getStatisticLabel } from "../../data/recorder";
import type { ValueChangedEvent, HomeAssistant } from "../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box-item";
import "../ha-svg-icon";
import "./state-badge";
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
interface StatisticItem extends ScorableTextItem {
id: string;
@ -99,16 +99,18 @@ export class HaStatisticPicker extends LitElement {
@state() private _filteredItems?: StatisticItem[] = undefined;
private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = (item) =>
html`<mwc-list-item graphic="avatar" twoline>
html`<ha-combo-box-item type="button">
${item.state
? html`<state-badge
slot="graphic"
.stateObj=${item.state}
.hass=${this.hass}
></state-badge>`
: ""}
<span>${item.name}</span>
<span slot="secondary"
? html`
<state-badge
slot="start"
.stateObj=${item.state}
.hass=${this.hass}
></state-badge>
`
: html`<span slot="start" style="width: 32px"></span>`}
<span slot="headline">${item.name}</span>
<span slot="supporting-text"
>${item.id === "" || item.id === "__missing"
? html`<a
target="_blank"
@ -120,7 +122,7 @@ export class HaStatisticPicker extends LitElement {
>`
: item.id}</span
>
</mwc-list-item>`;
</ha-combo-box-item>`;
private _getStatistics = memoizeOne(
(

View File

@ -79,6 +79,17 @@ export class StateBadge extends LitElement {
</div>`;
}
const cls = this.getClass();
if (cls) {
cls.forEach((toSet, className) => {
if (!toSet) {
this.classList.remove(className);
} else {
this.classList.add(className);
}
});
}
if (!this.icon) {
return nothing;
}
@ -175,35 +186,57 @@ export class StateBadge extends LitElement {
backgroundImage = `url(${imageUrl})`;
this.icon = false;
}
if (domain === "update") {
this.style.borderRadius = "0";
} else if (domain === "media_player" || domain === "camera") {
this.style.borderRadius = "8%";
}
}
this._iconStyle = iconStyle;
this.style.backgroundImage = backgroundImage;
}
protected getClass() {
const cls = new Map(
["has-no-radius", "has-media-image", "has-image"].map((_cls) => [
_cls,
false,
])
);
if (this.stateObj) {
const domain = computeDomain(this.stateObj.entity_id);
if (domain === "update") {
cls.set("has-no-radius", true);
} else if (domain === "media_player" || domain === "camera") {
cls.set("has-media-image", true);
} else if (this.style.backgroundImage !== "") {
cls.set("has-image", true);
}
}
return cls;
}
static get styles(): CSSResultGroup {
return [
iconColorCSS,
css`
:host {
position: relative;
display: inline-block;
display: inline-flex;
width: 40px;
color: var(--paper-item-icon-color, #44739e);
border-radius: 50%;
border-radius: var(--state-badge-border-radius, 50%);
height: 40px;
text-align: center;
background-size: cover;
line-height: 40px;
vertical-align: middle;
box-sizing: border-box;
--state-inactive-color: initial;
align-items: center;
justify-content: center;
}
:host(.has-image) {
border-radius: var(--state-badge-with-image-border-radius, 50%);
}
:host(.has-media-image) {
border-radius: var(--state-badge-with-media-image-border-radius, 8%);
}
:host(.has-no-radius) {
border-radius: 0;
}
:host(:focus) {
outline: none;

View File

@ -10,20 +10,23 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-alert";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-list-item";
import "./ha-combo-box-item";
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) =>
html`<ha-list-item twoline graphic="icon">
<span>${item.name}</span>
<span slot="secondary">${item.slug}</span>
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`
<ha-combo-box-item type="button">
<span slot="headline">${item.name}</span>
<span slot="supporting-text">${item.slug}</span>
${item.icon
? html`<img
alt=""
slot="graphic"
.src="/api/hassio/addons/${item.slug}/icon"
/>`
: ""}
</ha-list-item>`;
? html`
<img
alt=""
slot="start"
.src="/api/hassio/addons/${item.slug}/icon"
/>
`
: nothing}
</ha-combo-box-item>
`;
@customElement("ha-addon-picker")
class HaAddonPicker extends LitElement {

View File

@ -25,6 +25,7 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-floor-icon";
import "./ha-icon-button";
import "./ha-list-item";
@ -125,38 +126,38 @@ export class HaAreaFloorPicker extends LitElement {
private _rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) => {
const rtl = computeRTL(this.hass);
return html`
<ha-list-item
graphic="icon"
<ha-combo-box-item
type="button"
style=${item.type === "area" && item.hasFloor
? rtl
? "--mdc-list-side-padding-right: 48px;"
: "--mdc-list-side-padding-left: 48px;"
? "--md-list-item-leading-space: 48px;"
: ""}
>
${item.type === "area" && item.hasFloor
? html`<ha-tree-indicator
style=${styleMap({
width: "48px",
position: "absolute",
top: "0px",
left: rtl ? undefined : "8px",
right: rtl ? "8px" : undefined,
transform: rtl ? "scaleX(-1)" : "",
})}
.end=${item.lastArea}
slot="graphic"
></ha-tree-indicator>`
? html`
<ha-tree-indicator
style=${styleMap({
width: "48px",
position: "absolute",
top: "0px",
left: rtl ? undefined : "4px",
right: rtl ? "4px" : undefined,
transform: rtl ? "scaleX(-1)" : "",
})}
.end=${item.lastArea}
slot="start"
></ha-tree-indicator>
`
: nothing}
${item.type === "floor"
? html`<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>`
? html`<ha-floor-icon slot="start" .floor=${item}></ha-floor-icon>`
: item.icon
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon
slot="graphic"
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${item.name}
</ha-list-item>
</ha-combo-box-item>
`;
};

View File

@ -4,7 +4,6 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
@ -24,22 +23,21 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-icon-button";
import "./ha-list-item";
import "./ha-svg-icon";
type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry;
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) =>
html`<ha-list-item
graphic="icon"
class=${classMap({ "add-new": item.area_id === ADD_NEW_ID })}
>
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) => html`
<ha-combo-box-item type="button">
${item.icon
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon slot="graphic" .path=${mdiTextureBox}></ha-svg-icon>`}
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiTextureBox}></ha-svg-icon>`}
${item.name}
</ha-list-item>`;
</ha-combo-box-item>
`;
const ADD_NEW_ID = "___ADD_NEW___";
const NO_ITEMS_ID = "___NO_ITEMS___";

View File

@ -1,6 +1,7 @@
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeStateName } from "../common/entity/compute_state_name";
import { supportsFeature } from "../common/entity/supports-feature";
@ -32,6 +33,10 @@ export class HaCameraStream extends LitElement {
@property({ attribute: false }) public stateObj?: CameraEntity;
@property({ attribute: false }) public aspectRatio?: number;
@property({ attribute: false }) public fitMode?: "cover" | "contain" | "fill";
@property({ type: Boolean, attribute: "controls" })
public controls = false;
@ -101,6 +106,10 @@ export class HaCameraStream extends LitElement {
: this._connected
? computeMJPEGStreamUrl(this.stateObj)
: this._posterUrl || ""}
style=${styleMap({
aspectRatio: this.aspectRatio,
objectFit: this.fitMode,
})}
alt=${`Preview of the ${computeStateName(this.stateObj)} camera.`}
/>`;
}
@ -117,6 +126,8 @@ export class HaCameraStream extends LitElement {
.posterUrl=${this._posterUrl}
@streams=${this._handleHlsStreams}
class=${stream.visible ? "" : "hidden"}
.aspectRatio=${this.aspectRatio}
.fitMode=${this.fitMode}
></ha-hls-player>`;
}
@ -131,6 +142,8 @@ export class HaCameraStream extends LitElement {
.posterUrl=${this._posterUrl}
@streams=${this._handleWebRtcStreams}
class=${stream.visible ? "" : "hidden"}
.aspectRatio=${this.aspectRatio}
.fitMode=${this.fitMode}
></ha-web-rtc-player>`;
}
@ -259,6 +272,16 @@ export class HaCameraStream extends LitElement {
width: 100%;
}
ha-web-rtc-player {
width: 100%;
height: 100%;
}
ha-hls-player {
width: 100%;
height: 100%;
}
.hidden {
display: none;
}

View File

@ -0,0 +1,46 @@
import { css } from "lit";
import { customElement, property } from "lit/decorators";
import { HaMdListItem } from "./ha-md-list-item";
@customElement("ha-combo-box-item")
export class HaComboBoxItem extends HaMdListItem {
@property({ type: Boolean, reflect: true, attribute: "border-top" })
public borderTop = false;
static override styles = [
...super.styles,
css`
:host {
--md-list-item-one-line-container-height: 48px;
--md-list-item-two-line-container-height: 64px;
}
:host([border-top]) md-item {
border-top: 1px solid var(--divider-color);
}
[slot="start"] {
--paper-item-icon-color: var(--secondary-text-color);
}
[slot="headline"] {
line-height: 22px;
font-size: 14px;
white-space: nowrap;
}
[slot="supporting-text"] {
line-height: 18px;
font-size: 12px;
white-space: nowrap;
}
::slotted(state-badge),
::slotted(img) {
width: 32px;
height: 32px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-combo-box-item": HaComboBoxItem;
}
}

View File

@ -16,8 +16,8 @@ import { customElement, property, query } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import type { HomeAssistant } from "../types";
import "./ha-combo-box-item";
import "./ha-icon-button";
import "./ha-list-item";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@ -105,6 +105,9 @@ export class HaComboBox extends LitElement {
@property({ type: Boolean, reflect: true }) public opened = false;
@property({ type: Boolean, attribute: "hide-clear-icon" })
public hideClearIcon = false;
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
@query("ha-textfield", true) private _inputElement!: HaTextField;
@ -187,7 +190,7 @@ export class HaComboBox extends LitElement {
>
<slot name="icon" slot="leadingIcon"></slot>
</ha-textfield>
${this.value
${this.value && !this.hideClearIcon
? html`<ha-svg-icon
role="button"
tabindex="-1"
@ -204,6 +207,7 @@ export class HaComboBox extends LitElement {
aria-expanded=${this.opened ? "true" : "false"}
class="toggle-button"
.path=${this.opened ? mdiMenuUp : mdiMenuDown}
?disabled=${this.disabled}
@click=${this._toggleOpen}
></ha-svg-icon>
</vaadin-combo-box-light>
@ -212,10 +216,11 @@ export class HaComboBox extends LitElement {
private _defaultRowRenderer: ComboBoxLitRenderer<
string | Record<string, any>
> = (item) =>
html`<ha-list-item>
> = (item) => html`
<ha-combo-box-item type="button">
${this.itemLabelPath ? item[this.itemLabelPath] : item}
</ha-list-item>`;
</ha-combo-box-item>
`;
private _clearValue(ev: Event) {
ev.stopPropagation();
@ -356,6 +361,10 @@ export class HaComboBox extends LitElement {
:host([opened]) .toggle-button {
color: var(--primary-color);
}
.toggle-button[disabled] {
color: var(--disabled-text-color);
pointer-events: none;
}
.clear-button {
--mdc-icon-size: 20px;
top: -7px;

View File

@ -1,4 +1,3 @@
import "@material/mwc-list/mwc-list-item";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
@ -11,6 +10,7 @@ import type { ValueChangedEvent, HomeAssistant } from "../types";
import { brandsUrl } from "../util/brands-url";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
export interface ConfigEntryExtended extends ConfigEntry {
localized_domain_name?: string;
@ -48,18 +48,20 @@ class HaConfigEntryPicker extends LitElement {
this._getConfigEntries();
}
private _rowRenderer: ComboBoxLitRenderer<ConfigEntryExtended> = (item) =>
html`<mwc-list-item twoline graphic="icon">
<span
>${item.title ||
private _rowRenderer: ComboBoxLitRenderer<ConfigEntryExtended> = (
item
) => html`
<ha-combo-box-item type="button">
<span slot="headline">
${item.title ||
this.hass.localize(
"ui.panel.config.integrations.config_entry.unnamed_entry"
)}</span
>
<span slot="secondary">${item.localized_domain_name}</span>
)}
</span>
<span slot="supporting-text">${item.localized_domain_name}</span>
<img
alt=""
slot="graphic"
slot="start"
src=${brandsUrl({
domain: item.domain,
type: "icon",
@ -70,7 +72,8 @@ class HaConfigEntryPicker extends LitElement {
@error=${this._onImageError}
@load=${this._onImageLoad}
/>
</mwc-list-item>`;
</ha-combo-box-item>
`;
protected render() {
if (!this._configEntries) {

View File

@ -3,7 +3,6 @@ import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
@ -28,9 +27,9 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-floor-icon";
import "./ha-icon-button";
import "./ha-list-item";
type ScorableFloorRegistryEntry = ScorableTextItem & FloorRegistryEntry;
@ -38,14 +37,12 @@ const ADD_NEW_ID = "___ADD_NEW___";
const NO_FLOORS_ID = "___NO_FLOORS___";
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) =>
html`<ha-list-item
graphic="icon"
class=${classMap({ "add-new": item.floor_id === ADD_NEW_ID })}
>
<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>
const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) => html`
<ha-combo-box-item type="button">
<ha-floor-icon slot="start" .floor=${item}></ha-floor-icon>
${item.name}
</ha-list-item>`;
</ha-combo-box-item>
`;
@customElement("ha-floor-picker")
export class HaFloorPicker extends LitElement {

View File

@ -2,12 +2,13 @@ import type HlsType from "hls.js";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { nextRender } from "../common/util/render-status";
import { fetchStreamUrl } from "../data/camera";
import type { HomeAssistant } from "../types";
import "./ha-alert";
import { fetchStreamUrl } from "../data/camera";
import { isComponentLoaded } from "../common/config/is_component_loaded";
type HlsLite = Omit<
HlsType,
@ -24,6 +25,10 @@ class HaHLSPlayer extends LitElement {
@property({ attribute: "poster-url" }) public posterUrl?: string;
@property({ attribute: false }) public aspectRatio?: number;
@property({ attribute: false }) public fitMode?: "cover" | "contain" | "fill";
@property({ type: Boolean, attribute: "controls" })
public controls = false;
@ -87,6 +92,11 @@ class HaHLSPlayer extends LitElement {
?playsinline=${this.playsInline}
?controls=${this.controls}
@loadeddata=${this._loadedData}
style=${styleMap({
height: this.aspectRatio == null ? "100%" : "auto",
aspectRatio: this.aspectRatio,
objectFit: this.fitMode,
})}
></video>`
: ""}
`;

View File

@ -5,11 +5,13 @@ import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-button-menu";
import "./ha-md-button-menu";
import "./ha-icon-button";
import "./ha-list-item";
import "./ha-svg-icon";
import "./ha-tooltip";
import "./ha-md-menu-item";
import "./ha-md-divider";
export interface IconOverflowMenuItem {
[key: string]: any;
@ -35,11 +37,9 @@ export class HaIconOverflowMenu extends LitElement {
return html`
${this.narrow
? html` <!-- Collapsed representation for small screens -->
<ha-button-menu
<ha-md-button-menu
@click=${this._handleIconOverflowMenuOpened}
@closed=${this._handleIconOverflowMenuClosed}
class="ha-icon-overflow-menu-overflow"
absolute
positioning="popover"
>
<ha-icon-button
.label=${this.hass.localize("ui.common.overflow_menu")}
@ -49,23 +49,24 @@ export class HaIconOverflowMenu extends LitElement {
${this.items.map((item) =>
item.divider
? html`<li divider role="separator"></li>`
: html`<ha-list-item
graphic="icon"
? html`<ha-md-divider
role="separator"
tabindex="-1"
></ha-md-divider>`
: html`<ha-md-menu-item
?disabled=${item.disabled}
@click=${item.action}
.clickAction=${item.action}
class=${classMap({ warning: Boolean(item.warning) })}
>
<div slot="graphic">
<ha-svg-icon
class=${classMap({ warning: Boolean(item.warning) })}
.path=${item.path}
></ha-svg-icon>
</div>
<ha-svg-icon
slot="start"
class=${classMap({ warning: Boolean(item.warning) })}
.path=${item.path}
></ha-svg-icon>
${item.label}
</ha-list-item> `
</ha-md-menu-item> `
)}
</ha-button-menu>`
</ha-md-button-menu>`
: html`
<!-- Icon representation for big screens -->
${this.items.map((item) =>
@ -91,20 +92,6 @@ export class HaIconOverflowMenu extends LitElement {
protected _handleIconOverflowMenuOpened(e) {
e.stopPropagation();
// If this component is used inside a data table, the z-index of the row
// needs to be increased. Otherwise the ha-button-menu would be displayed
// underneath the next row in the table.
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
if (row) {
row.style.zIndex = "1";
}
}
protected _handleIconOverflowMenuClosed() {
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
if (row) {
row.style.zIndex = "";
}
}
static get styles() {
@ -115,16 +102,10 @@ export class HaIconOverflowMenu extends LitElement {
display: flex;
justify-content: flex-end;
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
div[role="separator"] {
border-right: 1px solid var(--divider-color);
width: 1px;
}
ha-list-item[disabled] ha-svg-icon {
color: var(--disabled-text-color);
}
`,
];
}

View File

@ -11,8 +11,8 @@ import { fireEvent } from "../common/dom/fire_event";
import { customIcons } from "../data/custom_icons";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-combo-box";
import "./ha-list-item";
import "./ha-icon";
import "./ha-combo-box-item";
interface IconItem {
icon: string;
@ -67,11 +67,12 @@ const loadCustomIconItems = async (iconsetPrefix: string) => {
}
};
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) =>
html`<ha-list-item graphic="avatar">
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) => html`
<ha-combo-box-item type="button">
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
${item.icon}
</ha-list-item>`;
</ha-combo-box-item>
`;
@customElement("ha-icon-picker")
export class HaIconPicker extends LitElement {

View File

@ -3,7 +3,6 @@ import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
@ -26,8 +25,8 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-icon-button";
import "./ha-list-item";
import "./ha-svg-icon";
type ScorableLabelItem = ScorableTextItem & LabelRegistryEntry;
@ -36,16 +35,14 @@ const ADD_NEW_ID = "___ADD_NEW___";
const NO_LABELS_ID = "___NO_LABELS___";
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
const rowRenderer: ComboBoxLitRenderer<LabelRegistryEntry> = (item) =>
html`<ha-list-item
graphic="icon"
class=${classMap({ "add-new": item.label_id === ADD_NEW_ID })}
>
const rowRenderer: ComboBoxLitRenderer<LabelRegistryEntry> = (item) => html`
<ha-combo-box-item type="button">
${item.icon
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: nothing}
${item.name}
</ha-list-item>`;
</ha-combo-box-item>
`;
@customElement("ha-label-picker")
export class HaLabelPicker extends SubscribeMixin(LitElement) {

View File

@ -1,15 +1,14 @@
import "@material/mwc-list/mwc-list";
import type { ActionDetail } from "@material/mwc-list/mwc-list";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { navigate } from "../common/navigate";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { HomeAssistant } from "../types";
import "./ha-icon-next";
import "./ha-list-item";
import "./ha-svg-icon";
import "./ha-md-list";
import "./ha-md-list-item";
@customElement("ha-navigation-list")
class HaNavigationList extends LitElement {
@ -26,21 +25,21 @@ class HaNavigationList extends LitElement {
public render(): TemplateResult {
return html`
<mwc-list
<ha-md-list
innerRole="menu"
itemRoles="menuitem"
innerAriaLabel=${ifDefined(this.label)}
@action=${this._handleListAction}
>
${this.pages.map(
(page) => html`
<ha-list-item
graphic="avatar"
.twoline=${this.hasSecondary}
.hasMeta=${!this.narrow}
${this.pages.map((page) => {
const externalApp = page.path.endsWith("#external-app-configuration");
return html`
<ha-md-list-item
.type=${externalApp ? "button" : "link"}
.href=${externalApp ? undefined : page.path}
@click=${externalApp ? this._handleExternalApp : undefined}
>
<div
slot="graphic"
slot="start"
class=${page.iconColor ? "icon-background" : ""}
.style="background-color: ${page.iconColor || "undefined"}"
>
@ -48,31 +47,23 @@ class HaNavigationList extends LitElement {
</div>
<span>${page.name}</span>
${this.hasSecondary
? html`<span slot="secondary">${page.description}</span>`
? html`<span slot="supporting-text">${page.description}</span>`
: ""}
${!this.narrow
? html`<ha-icon-next slot="meta"></ha-icon-next>`
? html`<ha-icon-next slot="end"></ha-icon-next>`
: ""}
</ha-list-item>
`
)}
</mwc-list>
</ha-md-list-item>
`;
})}
</ha-md-list>
`;
}
private _handleListAction(ev: CustomEvent<ActionDetail>) {
const path = this.pages[ev.detail.index].path;
if (path.endsWith("#external-app-configuration")) {
this.hass.auth.external!.fireMessage({ type: "config_screen/show" });
} else {
navigate(path);
}
private _handleExternalApp() {
this.hass.auth.external!.fireMessage({ type: "config_screen/show" });
}
static styles: CSSResultGroup = css`
:host {
--mdc-list-vertical-padding: 0;
}
ha-svg-icon,
ha-icon-next {
color: var(--secondary-text-color);
@ -89,8 +80,7 @@ class HaNavigationList extends LitElement {
.icon-background ha-svg-icon {
color: #fff;
}
ha-list-item {
cursor: pointer;
ha-md-list-item {
font-size: var(--navigation-list-item-title-font-size);
}
`;

View File

@ -1,7 +1,6 @@
import "@material/mwc-list/mwc-list-item";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { titleCase } from "../common/string/title-case";
@ -10,6 +9,7 @@ import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-icon";
interface NavigationItem {
@ -21,11 +21,13 @@ interface NavigationItem {
const DEFAULT_ITEMS: NavigationItem[] = [];
const rowRenderer: ComboBoxLitRenderer<NavigationItem> = (item) => html`
<mwc-list-item graphic="icon" .twoline=${!!item.title}>
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
<span>${item.title || item.path}</span>
<span slot="secondary">${item.path}</span>
</mwc-list-item>
<ha-combo-box-item type="button">
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
<span slot="headline">${item.title || item.path}</span>
${item.title
? html`<span slot="supporting-text">${item.path}</span>`
: nothing}
</ha-combo-box-item>
`;
const createViewNavigationItem = (

View File

@ -1,7 +1,5 @@
import "@material/mwc-button/mwc-button";
import { mdiCamera } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
// The BarcodeDetector Web API is not yet supported in all browsers,
@ -12,12 +10,13 @@ import { prepareZXingModule } from "barcode-detector";
import type QrScanner from "qr-scanner";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import type { LocalizeFunc } from "../common/translations/localize";
import { addExternalBarCodeListener } from "../external_app/external_app_entrypoint";
import type { HomeAssistant } from "../types";
import "./ha-alert";
import "./ha-button";
import "./ha-button-menu";
import "./ha-list-item";
import "./ha-spinner";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@ -36,18 +35,22 @@ prepareZXingModule({
class HaQrScanner extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public localize!: LocalizeFunc;
@property() public description?: string;
@property({ attribute: "alternative_option_label" })
public alternativeOptionLabel?: string;
@property() public error?: string;
@property({ attribute: false }) public validate?: (
value: string
) => string | undefined;
@state() private _cameras?: QrScanner.Camera[];
@state() private _manual = false;
@state() private _loading = true;
@state() private _error?: string;
@state() private _warning?: string;
private _qrScanner?: QrScanner;
@ -88,29 +91,40 @@ class HaQrScanner extends LitElement {
this._loadQrScanner();
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("error") && this.error) {
alert(`error: ${this.error}`);
this._notifyExternalScanner(this.error);
}
}
protected render() {
if (this._nativeBarcodeScanner && !this._manual) {
if (this._nativeBarcodeScanner) {
return nothing;
}
return html`${this.error
? html`<ha-alert alert-type="error">${this.error}</ha-alert>`
: ""}
${navigator.mediaDevices && !this._manual
return html`${this._error || this._warning
? html`<ha-alert
.alertType=${this._error ? "error" : "warning"}
class=${this._error ? "" : "warning"}
>
${this._error || this._warning}
${this._error
? html` <ha-button @click=${this._retry} slot="action">
${this.hass.localize("ui.components.qr-scanner.retry")}
</ha-button>`
: nothing}
</ha-alert>`
: nothing}
${navigator.mediaDevices
? html`<video></video>
<div id="canvas-container">
${this._cameras && this._cameras.length > 1
${this._loading
? html`<div class="loading">
<ha-spinner active></ha-spinner>
</div>`
: nothing}
${!this._loading &&
!this._error &&
this._cameras &&
this._cameras.length > 1
? html`<ha-button-menu fixed @closed=${stopPropagation}>
<ha-icon-button
slot="trigger"
.label=${this.localize(
.label=${this.hass.localize(
"ui.components.qr-scanner.select_camera"
)}
.path=${mdiCamera}
@ -128,25 +142,25 @@ class HaQrScanner extends LitElement {
</ha-button-menu>`
: nothing}
</div>`
: html`${this._manual
? nothing
: html`<ha-alert alert-type="warning">
${!window.isSecureContext
? this.localize(
"ui.components.qr-scanner.only_https_supported"
)
: this.localize("ui.components.qr-scanner.not_supported")}
</ha-alert>`}
<p>${this.localize("ui.components.qr-scanner.manual_input")}</p>
: html`<ha-alert alert-type="warning">
${!window.isSecureContext
? this.hass.localize(
"ui.components.qr-scanner.only_https_supported"
)
: this.hass.localize("ui.components.qr-scanner.not_supported")}
</ha-alert>
<p>${this.hass.localize("ui.components.qr-scanner.manual_input")}</p>
<div class="row">
<ha-textfield
.label=${this.localize("ui.components.qr-scanner.enter_qr_code")}
.label=${this.hass.localize(
"ui.components.qr-scanner.enter_qr_code"
)}
@keyup=${this._manualKeyup}
@paste=${this._manualPaste}
></ha-textfield>
<mwc-button @click=${this._manualSubmit}>
${this.localize("ui.common.submit")}
</mwc-button>
<ha-button @click=${this._manualSubmit}>
${this.hass.localize("ui.common.submit")}
</ha-button>
</div>`}`;
}
@ -165,7 +179,9 @@ class HaQrScanner extends LitElement {
// eslint-disable-next-line @typescript-eslint/naming-convention
const QrScanner = (await import("qr-scanner")).default;
if (!(await QrScanner.hasCamera())) {
this._reportError("No camera found");
this._reportError(
this.hass.localize("ui.components.qr-scanner.no_camera_found")
);
return;
}
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
@ -181,6 +197,7 @@ class HaQrScanner extends LitElement {
canvas.style.display = "block";
try {
await this._qrScanner.start();
this._loading = false;
} catch (err: any) {
this._reportError(err);
}
@ -193,8 +210,8 @@ class HaQrScanner extends LitElement {
private _qrCodeError = (err: any) => {
if (err.endsWith("No QR code found")) {
this._qrNotFoundCount++;
if (this._qrNotFoundCount === 250) {
this._reportError(err);
if (this._qrNotFoundCount >= 250) {
this._reportWarning(err);
}
return;
}
@ -204,7 +221,17 @@ class HaQrScanner extends LitElement {
};
private _qrCodeScanned = (qrCodeString: string): void => {
this._warning = undefined;
this._qrNotFoundCount = 0;
if (this.validate) {
const validationMessage = this.validate(qrCodeString);
if (validationMessage) {
this._reportWarning(validationMessage);
return;
}
}
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
};
@ -234,7 +261,10 @@ class HaQrScanner extends LitElement {
if (msg.command === "bar_code/scan_result") {
if (msg.payload.format !== "qr_code") {
this._notifyExternalScanner(
`Wrong barcode scanned! ${msg.payload.format}: ${msg.payload.rawValue}, we need a QR code.`
this.hass.localize("ui.components.qr-scanner.wrong_code", {
format: msg.payload.format,
rawValue: msg.payload.rawValue,
})
);
} else {
this._qrCodeScanned(msg.payload.rawValue);
@ -244,7 +274,7 @@ class HaQrScanner extends LitElement {
if (msg.payload.reason === "canceled") {
fireEvent(this, "qr-code-closed");
} else {
this._manual = true;
fireEvent(this, "qr-code-more-options");
}
}
return true;
@ -252,10 +282,17 @@ class HaQrScanner extends LitElement {
this.hass.auth.external!.fireMessage({
type: "bar_code/scan",
payload: {
title: this.title || "Scan QR code",
description: this.description || "Scan a barcode.",
title:
this.title ||
this.hass.localize("ui.components.qr-scanner.app.title"),
description:
this.description ||
this.hass.localize("ui.components.qr-scanner.app.description"),
alternative_option_label:
this.alternativeOptionLabel || "Click to manually enter the barcode",
this.alternativeOptionLabel ||
this.hass.localize(
"ui.components.qr-scanner.app.alternativeOptionLabel"
),
},
});
}
@ -269,25 +306,55 @@ class HaQrScanner extends LitElement {
}
private _notifyExternalScanner(message: string) {
if (!this.hass.auth.external) {
if (!this._nativeBarcodeScanner) {
return;
}
this.hass.auth.external.fireMessage({
this.hass.auth.external!.fireMessage({
type: "bar_code/notify",
payload: {
message,
},
});
this.error = undefined;
this._warning = undefined;
this._error = undefined;
}
private _reportError(message: string) {
fireEvent(this, "qr-code-error", { message });
const canvas = this._qrScanner?.$canvas;
if (canvas) {
canvas.style.display = "none";
}
this._error = message;
}
private _reportWarning(message: string) {
if (this._nativeBarcodeScanner) {
this._notifyExternalScanner(message);
} else {
this._warning = message;
}
}
private async _retry() {
if (this._qrScanner) {
this._loading = true;
this._error = undefined;
this._warning = undefined;
const canvas = this._qrScanner.$canvas;
canvas.style.display = "block";
this._qrNotFoundCount = 0;
await this._qrScanner.start();
this._loading = false;
}
}
static styles = css`
:root {
position: relative;
}
canvas {
width: 100%;
border-radius: 16px;
}
#canvas-container {
position: relative;
@ -312,6 +379,24 @@ class HaQrScanner extends LitElement {
margin-inline-end: 8px;
margin-inline-start: initial;
}
.loading {
display: flex;
position: absolute;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
ha-alert {
display: block;
}
ha-alert.warning {
position: absolute;
z-index: 1;
background-color: var(--primary-background-color);
top: 0;
width: calc(100% - 48px);
}
`;
}
@ -319,8 +404,8 @@ declare global {
// for fire event
interface HASSDomEvents {
"qr-code-scanned": { value: string };
"qr-code-error": { message: string };
"qr-code-closed": undefined;
"qr-code-more-options": undefined;
}
interface HTMLElementTagNameMap {

View File

@ -3,7 +3,7 @@ import {
mdiAlertCircleOutline,
mdiDevices,
mdiPaletteSwatch,
mdiSofa,
mdiTextureBox,
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
@ -211,36 +211,12 @@ export class HaRelatedItems extends LitElement {
)}
</mwc-list>`
: nothing}
${this._related.device
? html`<h3>
${this.hass.localize("ui.components.related-items.device")}
</h3>
${this._related.device.map((relatedDeviceId) => {
const device = this.hass.devices[relatedDeviceId];
if (!device) {
return nothing;
}
return html`
<a href="/config/devices/device/${relatedDeviceId}">
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${mdiDevices}
slot="graphic"
></ha-svg-icon>
${device.name_by_user || device.name}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
`;
})} </mwc-list>
`
: nothing}
${this._related.area
? html`<h3>
${this.hass.localize("ui.components.related-items.area")}
</h3>
<mwc-list
>${this._related.area.map((relatedAreaId) => {
<mwc-list>
${this._related.area.map((relatedAreaId) => {
const area = this.hass.areas[relatedAreaId];
if (!area) {
return nothing;
@ -259,17 +235,47 @@ export class HaRelatedItems extends LitElement {
})}
slot="graphic"
></div>`
: html`<ha-svg-icon
.path=${mdiSofa}
slot="graphic"
></ha-svg-icon>`}
: area.icon
? html`<ha-icon
slot="graphic"
.icon=${area.icon}
></ha-icon>`
: html`<ha-svg-icon
slot="graphic"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${area.name}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
`;
})}</mwc-list
>`
})}
</mwc-list>`
: nothing}
${this._related.device
? html`<h3>
${this.hass.localize("ui.components.related-items.device")}
</h3>
<mwc-list>
${this._related.device.map((relatedDeviceId) => {
const device = this.hass.devices[relatedDeviceId];
if (!device) {
return nothing;
}
return html`
<a href="/config/devices/device/${relatedDeviceId}">
<ha-list-item hasMeta graphic="icon">
<ha-svg-icon
.path=${mdiDevices}
slot="graphic"
></ha-svg-icon>
${device.name_by_user || device.name}
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</a>
`;
})}
</mwc-list>`
: nothing}
${this._related.entity
? html`

View File

@ -83,6 +83,10 @@ export class HaBackgroundSelector extends LitElement {
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;

View File

@ -69,11 +69,14 @@ export class HaTemplateSelector extends LitElement {
}
private _handleChange(ev) {
const value = ev.target.value;
let value = ev.target.value;
if (this.value === value) {
return;
}
this.warn = WARNING_STRINGS.find((str) => value.includes(str));
if (value === "" && !this.required) {
value = undefined;
}
fireEvent(this, "value-changed", { value });
}

View File

@ -38,6 +38,7 @@ import "./ha-settings-row";
import "./ha-yaml-editor";
import type { HaYamlEditor } from "./ha-yaml-editor";
import "./ha-service-section-icon";
import { hasTemplate } from "../common/string/has-template";
const attributeFilter = (values: any[], attribute: any) => {
if (typeof attribute === "object") {
@ -101,6 +102,8 @@ export class HaServiceControl extends LitElement {
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
private _stickySelector: Record<string, Selector> = {};
protected willUpdate(changedProperties: PropertyValues<this>) {
if (!this.hasUpdated) {
this.hass.loadBackendTranslation("services");
@ -590,7 +593,23 @@ export class HaServiceControl extends LitElement {
return nothing;
}
const selector = dataField?.selector ?? { text: undefined };
const fieldDataHasTemplate =
this._value?.data && hasTemplate(this._value.data[dataField.key]);
const selector =
fieldDataHasTemplate &&
typeof this._value!.data![dataField.key] === "string"
? { template: null }
: fieldDataHasTemplate &&
typeof this._value!.data![dataField.key] === "object"
? { object: null }
: (this._stickySelector[dataField.key] ??
dataField?.selector ?? { text: null });
if (fieldDataHasTemplate) {
// Hold this selector type until the field is cleared
this._stickySelector[dataField.key] = selector;
}
const showOptional = showOptionalToggle(dataField);
@ -693,6 +712,7 @@ export class HaServiceControl extends LitElement {
this._checkedKeys.delete(key);
data = { ...this._value?.data };
delete data[key];
delete this._stickySelector[key];
}
if (data) {
fireEvent(this, "value-changed", {
@ -816,6 +836,10 @@ export class HaServiceControl extends LitElement {
private _serviceDataChanged(ev: CustomEvent) {
ev.stopPropagation();
if (ev.detail.isValid === false) {
// Don't clear an object selector that returns invalid YAML
return;
}
const key = (ev.currentTarget as any).key;
const value = ev.detail.value;
if (
@ -828,8 +852,13 @@ export class HaServiceControl extends LitElement {
const data = { ...this._value?.data, [key]: value };
if (value === "" || value === undefined) {
if (
value === "" ||
value === undefined ||
(typeof value === "object" && !Object.keys(value).length)
) {
delete data[key];
delete this._stickySelector[key];
}
fireEvent(this, "value-changed", {

View File

@ -7,7 +7,7 @@ import type { LocalizeFunc } from "../common/translations/localize";
import { domainToName } from "../data/integration";
import type { HomeAssistant } from "../types";
import "./ha-combo-box";
import "./ha-list-item";
import "./ha-combo-box-item";
import "./ha-service-icon";
import { getServiceIcons } from "../data/icons";
@ -29,18 +29,19 @@ class HaServicePicker extends LitElement {
}
private _rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> =
(item) =>
html`<ha-list-item twoline graphic="icon">
(item) => html`
<ha-combo-box-item type="button">
<ha-service-icon
slot="graphic"
slot="start"
.hass=${this.hass}
.service=${item.service}
></ha-service-icon>
<span>${item.name}</span>
<span slot="secondary"
<span slot="headline">${item.name}</span>
<span slot="supporting-text"
>${item.name === item.service ? "" : item.service}</span
>
</ha-list-item>`;
</ha-combo-box-item>
`;
protected render() {
return html`

View File

@ -17,15 +17,16 @@ import {
mdiTooltipAccount,
mdiViewDashboard,
} from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
import type { PaperIconItemElement } from "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResult, CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, eventOptions, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import {
customElement,
eventOptions,
property,
state,
query,
} from "lit/decorators";
import memoizeOne from "memoize-one";
import { storage } from "../common/decorators/storage";
import { fireEvent } from "../common/dom/fire_event";
@ -48,7 +49,9 @@ import "./ha-menu-button";
import "./ha-sortable";
import "./ha-svg-icon";
import "./user/ha-user-badge";
import { preventDefault } from "../common/dom/prevent_default";
import "./ha-md-list";
import "./ha-md-list-item";
import type { HaMdListItem } from "./ha-md-list-item";
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
@ -221,6 +224,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
})
private _hiddenPanels: string[] = [];
@query(".tooltip") private _tooltip!: HTMLDivElement;
public hassSubscribe(): UnsubscribeFunc[] {
return this.hass.user?.is_admin
? [
@ -238,13 +243,20 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return nothing;
}
// Show the supervisor as being part of configuration
const selectedPanel = this.route.path?.startsWith("/hassio/")
? "config"
: this.hass.panelUrl;
// prettier-ignore
return html`
${this._renderHeader()}
${this._renderAllPanels()}
${this._renderAllPanels(selectedPanel)}
${this._renderDivider()}
${this._renderNotifications()}
${this._renderUserItem()}
<ha-md-list>
${this._renderNotifications()}
${this._renderUserItem(selectedPanel)}
</ha-md-list>
<div disabled class="bottom-spacer"></div>
<div class="tooltip"></div>
`;
@ -314,9 +326,11 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
this.hass &&
changedProps.get("hass")?.connected === false &&
oldHass?.connected === false &&
this.hass.connected === true
) {
this._subscribePersistentNotifications();
@ -327,9 +341,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
if (!SUPPORT_SCROLL_IF_NEEDED) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (!oldHass || oldHass.panelUrl !== this.hass.panelUrl) {
const selectedEl = this.shadowRoot!.querySelector(".iron-selected");
if (oldHass?.panelUrl !== this.hass.panelUrl) {
const selectedEl = this.shadowRoot!.querySelector(".selected");
if (selectedEl) {
// @ts-ignore
selectedEl.scrollIntoViewIfNeeded();
@ -381,7 +394,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
</div>`;
}
private _renderAllPanels() {
private _renderAllPanels(selectedPanel: string) {
const [beforeSpacer, afterSpacer] = computePanels(
this.hass.panels,
this.hass.defaultPanel,
@ -390,34 +403,26 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this.hass.locale
);
// Show the supervisor as being part of configuration
const selectedPanel = this.route.path?.startsWith("/hassio/")
? "config"
: this.hass.panelUrl;
// prettier-ignore
return html`
<paper-listbox
attr-for-selected="data-panel"
<ha-md-list
class="ha-scrollbar"
.selected=${selectedPanel}
@focusin=${this._listboxFocusIn}
@focusout=${this._listboxFocusOut}
@scroll=${this._listboxScroll}
@keydown=${this._listboxKeydown}
@iron-activate=${preventDefault}
>
${this.editMode
? this._renderPanelsEdit(beforeSpacer)
: this._renderPanels(beforeSpacer)}
? this._renderPanelsEdit(beforeSpacer, selectedPanel)
: this._renderPanels(beforeSpacer, selectedPanel)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer)}
${this._renderPanels(afterSpacer, selectedPanel)}
${this._renderExternalConfiguration()}
</paper-listbox>
</ha-md-list>
`;
}
private _renderPanels(panels: PanelInfo[]) {
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
return panels.map((panel) =>
this._renderPanel(
panel.url_path,
@ -429,7 +434,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
? PANEL_ICONS.lovelace
: panel.url_path in PANEL_ICONS
? PANEL_ICONS[panel.url_path]
: undefined
: undefined,
selectedPanel
)
);
}
@ -437,30 +443,24 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _renderPanel(
urlPath: string,
title: string | null,
icon?: string | null,
iconPath?: string | null
icon: string | null | undefined,
iconPath: string | null | undefined,
selectedPanel: string
) {
return urlPath === "config"
? this._renderConfiguration(title)
? this._renderConfiguration(title, selectedPanel)
: html`
<a
role="option"
aria-selected=${urlPath === this.hass.panelUrl}
href=${`/${urlPath}`}
data-panel=${urlPath}
tabindex="-1"
<ha-md-list-item
.href=${this.editMode ? undefined : `/${urlPath}`}
type="link"
class=${selectedPanel === urlPath ? "selected" : ""}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
${iconPath
? html`<ha-svg-icon
slot="item-icon"
.path=${iconPath}
></ha-svg-icon>`
: html`<ha-icon slot="item-icon" .icon=${icon}></ha-icon>`}
<span class="item-text">${title}</span>
</paper-icon-item>
${iconPath
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
<span class="item-text" slot="headline">${title}</span>
${this.editMode
? html`<ha-icon-button
.label=${this.hass.localize("ui.sidebar.hide_panel")}
@ -468,9 +468,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
class="hide-panel"
.panel=${urlPath}
@click=${this._hidePanel}
slot="end"
></ha-icon-button>`
: ""}
</a>
: nothing}
</ha-md-list-item>
`;
}
@ -493,14 +494,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._panelOrder = panelOrder;
}
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
private _renderPanelsEdit(beforeSpacer: PanelInfo[], selectedPanel: string) {
return html`
<ha-sortable
handle-selector="paper-icon-item"
.disabled=${!this.editMode}
@item-moved=${this._panelMoved}
>
<div class="reorder-list">${this._renderPanels(beforeSpacer)}</div>
<ha-sortable .disabled=${!this.editMode} @item-moved=${this._panelMoved}
><div>${this._renderPanels(beforeSpacer, selectedPanel)}</div>
</ha-sortable>
${this._renderSpacer()}${this._renderHiddenPanels()}
`;
@ -513,26 +510,24 @@ class HaSidebar extends SubscribeMixin(LitElement) {
if (!panel) {
return "";
}
return html`<paper-icon-item
return html`<ha-md-list-item
@click=${this._unhidePanel}
class="hidden-panel"
.panel=${url}
type="button"
>
${panel.url_path === this.hass.defaultPanel && !panel.icon
? html`<ha-svg-icon
slot="item-icon"
slot="start"
.path=${PANEL_ICONS.lovelace}
></ha-svg-icon>`
: panel.url_path in PANEL_ICONS
? html`<ha-svg-icon
slot="item-icon"
slot="start"
.path=${PANEL_ICONS[panel.url_path]}
></ha-svg-icon>`
: html`<ha-icon
slot="item-icon"
.icon=${panel.icon}
></ha-icon>`}
<span class="item-text"
: html`<ha-icon slot="start" .icon=${panel.icon}></ha-icon>`}
<span class="item-text" slot="headline"
>${panel.url_path === this.hass.defaultPanel
? this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) ||
@ -542,8 +537,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
.label=${this.hass.localize("ui.sidebar.show_panel")}
.path=${mdiPlus}
class="show-panel"
slot="end"
></ha-icon-button>
</paper-icon-item>`;
</ha-md-list-item>`;
})}
${this._renderSpacer()}`
: ""}`;
@ -557,41 +553,34 @@ class HaSidebar extends SubscribeMixin(LitElement) {
return html`<div class="spacer" disabled></div>`;
}
private _renderConfiguration(title: string | null) {
return html`<a
class="configuration-container"
role="option"
aria-selected=${this.hass.panelUrl === "config"}
href="/config"
data-panel="config"
tabindex="-1"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item
class="configuration"
role="option"
aria-selected=${this.hass.panelUrl === "config"}
private _renderConfiguration(title: string | null, selectedPanel: string) {
return html`
<ha-md-list-item
class="configuration${selectedPanel === "config" ? " selected" : ""}"
type="button"
href="/config"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<ha-svg-icon slot="item-icon" .path=${mdiCog}></ha-svg-icon>
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
${!this.alwaysExpand &&
(this._updatesCount > 0 || this._issuesCount > 0)
? html`
<span class="configuration-badge" slot="item-icon">
<span class="badge" slot="start">
${this._updatesCount + this._issuesCount}
</span>
`
: ""}
<span class="item-text">${title}</span>
<span class="item-text" slot="headline">${title}</span>
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
? html`
<span class="configuration-badge"
<span class="badge" slot="end"
>${this._updatesCount + this._issuesCount}</span
>
`
: ""}
</paper-icon-item>
</a>`;
</ha-md-list-item>
`;
}
private _renderNotifications() {
@ -599,91 +588,67 @@ class HaSidebar extends SubscribeMixin(LitElement) {
? this._notifications.length
: 0;
return html`<div
class="notifications-container"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item
return html`
<ha-md-list-item
class="notifications"
role="option"
aria-selected="false"
@click=${this._handleShowNotificationDrawer}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
type="button"
>
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
<ha-svg-icon slot="start" .path=${mdiBell}></ha-svg-icon>
${!this.alwaysExpand && notificationCount > 0
? html`
<span class="notification-badge" slot="item-icon">
${notificationCount}
</span>
<span class="badge" slot="start"> ${notificationCount} </span>
`
: ""}
<span class="item-text">
${this.hass.localize("ui.notification_drawer.title")}
</span>
<span class="item-text" slot="headline"
>${this.hass.localize("ui.notification_drawer.title")}</span
>
${this.alwaysExpand && notificationCount > 0
? html` <span class="notification-badge">${notificationCount}</span> `
? html`<span class="badge" slot="end">${notificationCount}</span>`
: ""}
</paper-icon-item>
</div>`;
</ha-md-list-item>
`;
}
private _renderUserItem() {
return html`<a
class=${classMap({
profile: true,
// Mimic behavior that paper-listbox provides
"iron-selected": this.hass.panelUrl === "profile",
})}
href="/profile"
data-panel="panel"
tabindex="-1"
role="option"
aria-selected=${this.hass.panelUrl === "profile"}
aria-label=${this.hass.localize("panel.profile")}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
private _renderUserItem(selectedPanel: string) {
return html`
<ha-md-list-item
href="/profile"
type="link"
class="user ${selectedPanel === "profile" ? " selected" : ""}"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<ha-user-badge
slot="item-icon"
slot="start"
.user=${this.hass.user}
.hass=${this.hass}
></ha-user-badge>
<span class="item-text">
${this.hass.user ? this.hass.user.name : ""}
</span>
</paper-icon-item>
</a>`;
<span class="item-text" slot="headline"
>${this.hass.user ? this.hass.user.name : ""}</span
>
</ha-md-list-item>
`;
}
private _renderExternalConfiguration() {
return html`${!this.hass.user?.is_admin &&
this.hass.auth.external?.config.hasSettingsScreen
? html`
<a
role="option"
aria-label=${this.hass.localize(
"ui.sidebar.external_app_configuration"
)}
href="#external-app-configuration"
tabindex="-1"
aria-selected="false"
<ha-md-list-item
@click=${this._handleExternalAppConfiguration}
type="button"
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-svg-icon
slot="item-icon"
.path=${mdiCellphoneCog}
></ha-svg-icon>
<span class="item-text">
${this.hass.localize("ui.sidebar.external_app_configuration")}
</span>
</paper-icon-item>
</a>
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
<span class="item-text" slot="headline">
${this.hass.localize("ui.sidebar.external_app_configuration")}
</span>
</ha-md-list-item>
`
: ""}`;
}
@ -695,10 +660,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
});
}
private get _tooltip() {
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
}
private _handleAction(ev: CustomEvent<ActionHandlerDetail>) {
if (ev.detail.action !== "hold") {
return;
@ -761,7 +722,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
clearTimeout(this._mouseLeaveTimeout);
this._mouseLeaveTimeout = undefined;
}
this._showTooltip(ev.currentTarget as PaperIconItemElement);
this._showTooltip(ev.currentTarget as HaMdListItem);
}
private _itemMouseLeave() {
@ -774,10 +735,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
private _listboxFocusIn(ev) {
if (this.alwaysExpand || ev.target.nodeName !== "A") {
if (this.alwaysExpand || ev.target.localName !== "ha-md-list-item") {
return;
}
this._showTooltip(ev.target.querySelector("paper-icon-item"));
this._showTooltip(ev.target);
}
private _listboxFocusOut() {
@ -801,22 +762,25 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._recentKeydownActiveUntil = new Date().getTime() + 100;
}
private _showTooltip(item: PaperIconItemElement) {
private _showTooltip(item: HaMdListItem) {
if (this._tooltipHideTimeout) {
clearTimeout(this._tooltipHideTimeout);
this._tooltipHideTimeout = undefined;
}
const tooltip = this._tooltip;
const listbox = this.shadowRoot!.querySelector("paper-listbox")!;
const listbox = this.shadowRoot!.querySelector("ha-md-list")!;
let top = item.offsetTop + 11;
if (listbox.contains(item)) {
top += listbox.offsetTop;
top -= listbox.scrollTop;
}
tooltip.innerHTML = item.querySelector(".item-text")!.innerHTML;
tooltip.innerText = (
item.querySelector(".item-text") as HTMLElement
).innerText;
tooltip.style.display = "block";
tooltip.style.position = "fixed";
tooltip.style.top = `${top}px`;
tooltip.style.left = `${item.offsetLeft + item.clientWidth + 4}px`;
tooltip.style.left = `${item.offsetLeft + item.clientWidth + 8}px`;
}
private _hideTooltip() {
@ -905,12 +869,11 @@ class HaSidebar extends SubscribeMixin(LitElement) {
.menu mwc-button {
width: 100%;
}
.reorder-list,
.hidden-panel {
display: none;
}
paper-listbox {
ha-md-list {
padding: 4px 0;
display: flex;
flex-direction: column;
@ -922,90 +885,64 @@ class HaSidebar extends SubscribeMixin(LitElement) {
overflow-x: hidden;
background: none;
margin-left: env(safe-area-inset-left);
margin-inline-start: env(safe-area-inset-left);
margin-inline-end: initial;
}
a {
text-decoration: none;
color: var(--sidebar-text-color);
font-weight: 500;
font-size: 14px;
position: relative;
display: block;
outline: 0;
}
paper-icon-item {
ha-md-list-item {
box-sizing: border-box;
margin: 4px;
padding-left: 12px;
padding-inline-start: 12px;
padding-inline-end: initial;
border-radius: 4px;
--paper-item-min-height: 40px;
height: 40px;
--md-list-item-one-line-container-height: 40px;
width: 48px;
position: relative;
--md-list-item-label-text-color: var(--sidebar-text-color);
--md-list-item-leading-space: 12px;
--md-list-item-trailing-space: 12px;
--md-list-item-leading-icon-size: 24px;
}
:host([expanded]) paper-icon-item {
:host([expanded]) ha-md-list-item {
width: 248px;
width: calc(248px - env(safe-area-inset-left));
}
ha-icon[slot="item-icon"],
ha-svg-icon[slot="item-icon"] {
color: var(--sidebar-icon-color);
ha-md-list-item.selected {
--md-list-item-label-text-color: var(--sidebar-selected-icon-color);
--md-ripple-hover-color: var(--sidebar-selected-icon-color);
}
.iron-selected paper-icon-item::before,
a:not(.iron-selected):focus::before {
ha-md-list-item.selected::before {
border-radius: 4px;
position: absolute;
top: 0;
right: 2px;
right: 0;
bottom: 0;
left: 2px;
left: 0;
pointer-events: none;
content: "";
transition: opacity 15ms linear;
will-change: opacity;
}
.iron-selected paper-icon-item::before {
background-color: var(--sidebar-selected-icon-color);
opacity: 0.12;
}
a:not(.iron-selected):focus::before {
background-color: currentColor;
opacity: var(--dark-divider-opacity);
margin: 4px 8px;
}
.iron-selected paper-icon-item:focus::before,
.iron-selected:focus paper-icon-item::before {
opacity: 0.2;
}
.iron-selected paper-icon-item[pressed]:before {
opacity: 0.37;
ha-icon[slot="start"],
ha-svg-icon[slot="start"] {
width: 24px;
flex-shrink: 0;
color: var(--sidebar-icon-color);
}
paper-icon-item span {
color: var(--sidebar-text-color);
font-weight: 500;
font-size: 14px;
}
a.iron-selected paper-icon-item ha-icon,
a.iron-selected paper-icon-item ha-svg-icon {
ha-md-list-item.selected ha-svg-icon[slot="start"],
ha-md-list-item.selected ha-icon[slot="start"] {
color: var(--sidebar-selected-icon-color);
}
a.iron-selected .item-text {
color: var(--sidebar-selected-text-color);
}
paper-icon-item .item-text {
ha-md-list-item .item-text {
display: none;
max-width: calc(100% - 56px);
font-weight: 500;
font-size: 14px;
}
:host([expanded]) paper-icon-item .item-text {
:host([expanded]) ha-md-list-item .item-text {
display: block;
}
@ -1019,60 +956,38 @@ class HaSidebar extends SubscribeMixin(LitElement) {
height: 1px;
background-color: var(--divider-color);
}
.notifications-container,
.configuration-container {
.badge {
display: flex;
margin-left: env(safe-area-inset-left);
margin-inline-start: env(safe-area-inset-left);
margin-inline-end: initial;
}
.notifications {
cursor: pointer;
}
.notifications .item-text,
.configuration .item-text {
flex: 1;
}
.profile {
margin-left: env(safe-area-inset-left);
margin-inline-start: env(safe-area-inset-left);
margin-inline-end: initial;
}
.profile paper-icon-item {
padding-left: 4px;
padding-inline-start: 4px;
padding-inline-end: auto;
}
.profile .item-text {
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
}
.notification-badge,
.configuration-badge {
position: absolute;
left: calc(var(--app-drawer-width, 248px) - 42px);
inset-inline-start: calc(var(--app-drawer-width, 248px) - 42px);
inset-inline-end: initial;
min-width: 20px;
box-sizing: border-box;
border-radius: 50%;
justify-content: center;
align-items: center;
min-width: 8px;
border-radius: 10px;
font-weight: 400;
line-height: normal;
background-color: var(--accent-color);
line-height: 20px;
text-align: center;
padding: 0px 2px;
padding: 2px 6px;
color: var(--text-accent-color, var(--text-primary-color));
}
ha-svg-icon + .notification-badge,
ha-svg-icon + .configuration-badge {
ha-svg-icon + .badge {
position: absolute;
bottom: 14px;
top: 4px;
left: 26px;
inset-inline-start: 26px;
inset-inline-end: initial;
border-radius: 10px;
font-size: 0.65em;
line-height: 2;
padding: 0 4px;
}
ha-md-list-item.user {
--md-list-item-leading-icon-size: 40px;
--md-list-item-bottom-space: 12px;
--md-list-item-leading-space: 4px;
--md-list-item-trailing-space: 4px;
}
ha-user-badge {
flex-shrink: 0;
}
.spacer {
@ -1088,19 +1003,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
white-space: nowrap;
}
.dev-tools {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0 8px;
width: 256px;
box-sizing: border-box;
}
.dev-tools a {
color: var(--sidebar-icon-color);
}
.tooltip {
display: none;
position: absolute;

View File

@ -1,115 +0,0 @@
import type { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
import type { PaperTabElement } from "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import type { PaperTabsElement } from "@polymer/paper-tabs/paper-tabs";
import { customElement } from "lit/decorators";
import type { Constructor } from "../types";
// eslint-disable-next-line @typescript-eslint/naming-convention
const PaperTabs = customElements.get(
"paper-tabs"
) as Constructor<PaperTabsElement>;
let subTemplate: HTMLTemplateElement;
@customElement("ha-tabs")
export class HaTabs extends PaperTabs {
private _firstTabWidth = 0;
private _lastTabWidth = 0;
private _lastLeftHiddenState = false;
private _lastRightHiddenState = false;
static get template(): HTMLTemplateElement {
if (!subTemplate) {
subTemplate = (PaperTabs as any).template.cloneNode(true);
const superStyle = subTemplate.content.querySelector("style");
// Add "noink" attribute for scroll buttons to disable animation.
subTemplate.content
.querySelectorAll("paper-icon-button")
.forEach((arrow: PaperIconButtonElement) => {
arrow.setAttribute("noink", "");
});
superStyle!.appendChild(
document.createTextNode(`
#selectionBar {
box-sizing: border-box;
}
.not-visible {
display: none;
}
paper-icon-button {
width: 24px;
height: 48px;
padding: 0;
margin: 0;
}
`)
);
}
return subTemplate;
}
// Get first and last tab's width for _affectScroll
// eslint-disable-next-line @typescript-eslint/naming-convention
public _tabChanged(tab: PaperTabElement, old: PaperTabElement): void {
super._tabChanged(tab, old);
const tabs = this.querySelectorAll("paper-tab:not(.hide-tab)");
if (tabs.length > 0) {
this._firstTabWidth = tabs[0].clientWidth;
this._lastTabWidth = tabs[tabs.length - 1].clientWidth;
}
// Scroll active tab into view if needed.
const selected = this.querySelector(".iron-selected");
if (selected) {
selected.scrollIntoView();
this._affectScroll(0); // Ensure scroll arrows match scroll position
}
}
/**
* Modify _affectScroll so that when the scroll arrows appear
* while scrolling and the tab container shrinks we can counteract
* the jump in tab position so that the scroll still appears smooth.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
public _affectScroll(dx: number): void {
if (this._firstTabWidth === 0 || this._lastTabWidth === 0) {
return;
}
this.$.tabsContainer.scrollLeft += dx;
const scrollLeft = this.$.tabsContainer.scrollLeft;
const dirRTL = this.dir === "rtl";
const boolCondition1 = Math.abs(scrollLeft) < this._firstTabWidth;
const boolCondition2 =
Math.abs(scrollLeft) + this._lastTabWidth > this._tabContainerScrollSize;
this._leftHidden = !dirRTL ? boolCondition1 : boolCondition2;
this._rightHidden = !dirRTL ? boolCondition2 : boolCondition1;
if (!dirRTL) {
if (this._lastLeftHiddenState !== this._leftHidden) {
this._lastLeftHiddenState = this._leftHidden;
this.$.tabsContainer.scrollLeft += this._leftHidden ? -23 : 23;
}
} else if (this._lastRightHiddenState !== this._rightHidden) {
this._lastRightHiddenState = this._rightHidden;
this.$.tabsContainer.scrollLeft -= this._rightHidden ? -23 : 23;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-tabs": HaTabs;
}
}

View File

@ -136,13 +136,12 @@ export class HaTextField extends TextFieldBase {
}
.mdc-floating-label:not(.mdc-floating-label--float-above) {
text-overflow: ellipsis;
width: inherit;
padding-right: 30px;
padding-inline-end: 30px;
padding-inline-start: initial;
box-sizing: border-box;
direction: var(--direction);
max-width: calc(100% - 16px);
}
.mdc-floating-label--float-above {
max-width: calc((100% - 16px) / 0.75);
transition: none;
}
input {
@ -183,11 +182,15 @@ export class HaTextField extends TextFieldBase {
}
.mdc-floating-label {
padding-inline-end: 16px;
padding-inline-start: initial;
inset-inline-start: 16px !important;
inset-inline-end: initial !important;
transform-origin: var(--float-start);
direction: var(--direction);
text-align: var(--float-start);
box-sizing: border-box;
text-overflow: ellipsis;
}
.mdc-text-field--with-leading-icon.mdc-text-field--filled

View File

@ -1,8 +1,9 @@
import type { PropertyValues, TemplateResult } from "lit";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import {
addWebRtcCandidate,
@ -26,6 +27,10 @@ class HaWebRtcPlayer extends LitElement {
@property() public entityid?: string;
@property({ attribute: false }) public aspectRatio?: number;
@property({ attribute: false }) public fitMode?: "cover" | "contain" | "fill";
@property({ type: Boolean, attribute: "controls" })
public controls = false;
@ -69,6 +74,11 @@ class HaWebRtcPlayer extends LitElement {
?controls=${this.controls}
poster=${ifDefined(this.posterUrl)}
@loadeddata=${this._loadedData}
style=${styleMap({
height: this.aspectRatio == null ? "100%" : "auto",
aspectRatio: this.aspectRatio,
objectFit: this.fitMode,
})}
></video>
`;
}

View File

@ -0,0 +1,160 @@
import TabGroup from "@shoelace-style/shoelace/dist/components/tab-group/tab-group.component";
import TabGroupStyles from "@shoelace-style/shoelace/dist/components/tab-group/tab-group.styles";
import "@shoelace-style/shoelace/dist/components/tab/tab";
import type { PropertyValues } from "lit";
import { css } from "lit";
import { customElement, query } from "lit/decorators";
@customElement("sl-tab-group")
// @ts-ignore
export class HaSlTabGroup extends TabGroup {
private _mouseIsDown = false;
private _scrolled = false;
private _mouseReleasedAt?: number;
private _scrollStartX = 0;
private _scrollLeft = 0;
@query(".tab-group__nav", true) private _scrollContainer?: HTMLElement;
public disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener("mousemove", this._mouseMove);
window.removeEventListener("mouseup", this._mouseUp);
}
override setAriaLabels() {
// Override the method to prevent setting aria-labels, as we don't use panels
// and don't want to set aria-labels for the tabs
}
override getAllPanels() {
// Override the method to prevent querying for panels
// and return an empty array instead
// as we don't use panels
return [];
}
protected override firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
const scrollContainer = this._scrollContainer;
if (scrollContainer) {
scrollContainer.addEventListener("mousedown", this._mouseDown);
}
}
// @ts-ignore
protected override handleClick(event: MouseEvent) {
if (
this._mouseReleasedAt &&
new Date().getTime() - this._mouseReleasedAt < 100
) {
return;
}
// @ts-ignore
super.handleClick(event);
}
private _mouseDown = (event: MouseEvent) => {
const scrollContainer = this._scrollContainer;
if (!scrollContainer) {
return;
}
this._scrollStartX = event.pageX - scrollContainer.offsetLeft;
this._scrollLeft = scrollContainer.scrollLeft;
this._mouseIsDown = true;
this._scrolled = false;
window.addEventListener("mousemove", this._mouseMove);
window.addEventListener("mouseup", this._mouseUp, { once: true });
};
private _mouseUp = () => {
this._mouseIsDown = false;
if (this._scrolled) {
this._mouseReleasedAt = new Date().getTime();
}
window.removeEventListener("mousemove", this._mouseMove);
};
private _mouseMove = (event: MouseEvent) => {
if (!this._mouseIsDown) {
return;
}
const scrollContainer = this._scrollContainer;
if (!scrollContainer) {
return;
}
const x = event.pageX - scrollContainer.offsetLeft;
const scroll = x - this._scrollStartX;
if (!this._scrolled) {
this._scrolled = Math.abs(scroll) > 1;
}
scrollContainer.scrollLeft = this._scrollLeft - scroll;
};
static override styles = [
TabGroupStyles,
css`
:host {
--sl-spacing-3x-small: 0.125rem;
--sl-spacing-2x-small: 0.25rem;
--sl-spacing-x-small: 0.5rem;
--sl-spacing-small: 0.75rem;
--sl-spacing-medium: 1rem;
--sl-spacing-large: 1.25rem;
--sl-spacing-x-large: 1.75rem;
--sl-spacing-2x-large: 2.25rem;
--sl-spacing-3x-large: 3rem;
--sl-spacing-4x-large: 4.5rem;
--sl-transition-x-slow: 1000ms;
--sl-transition-slow: 500ms;
--sl-transition-medium: 250ms;
--sl-transition-fast: 150ms;
--sl-transition-x-fast: 50ms;
--transition-speed: var(--sl-transition-fast);
--sl-border-radius-small: 0.1875rem;
--sl-border-radius-medium: 0.25rem;
--sl-border-radius-large: 0.5rem;
--sl-border-radius-x-large: 1rem;
--sl-border-radius-circle: 50%;
--sl-border-radius-pill: 9999px;
--sl-color-neutral-600: inherit;
--sl-font-weight-semibold: 500;
--sl-font-size-small: 14px;
--sl-color-primary-600: var(
--ha-tab-active-text-color,
var(--primary-color)
);
--track-color: var(--ha-tab-track-color, var(--divider-color));
--indicator-color: var(--ha-tab-indicator-color, var(--primary-color));
}
::slotted(sl-tab:not([active])) {
opacity: 0.8;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
// @ts-ignore
"sl-tab-group": HaSlTabGroup;
}
}

View File

@ -26,7 +26,6 @@ export class HaTileInfo extends LitElement {
flex-direction: column;
align-items: flex-start;
justify-content: center;
height: 36px;
}
span {
text-overflow: ellipsis;

View File

@ -19,9 +19,16 @@ import "../../panels/logbook/ha-logbook-renderer";
import { traceTabStyles } from "./trace-tab-styles";
import type { HomeAssistant } from "../../types";
import type { NodeInfo } from "./hat-script-graph";
import { describeCondition } from "../../data/automation_i18n";
import { describeCondition, describeTrigger } from "../../data/automation_i18n";
import type { EntityRegistryEntry } from "../../data/entity_registry";
import { fullEntitiesContext } from "../../data/context";
import type { LabelRegistryEntry } from "../../data/label_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import {
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import { describeAction } from "../../data/script_i18n";
const TRACE_PATH_TABS = [
"step_config",
@ -52,6 +59,14 @@ export class HaTracePathDetails extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@state()
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: Record<string, FloorRegistryEntry>;
protected render(): TemplateResult {
return html`
<div class="padded-box trace-info">
@ -151,11 +166,46 @@ export class HaTracePathDetails extends LitElement {
)}`;
}
const selectedType = this.selected.type;
return html`
${curPath === this.selected.path
? currentDetail.alias
? html`<h2>${currentDetail.alias}</h2>`
: nothing
: selectedType === "trigger"
? html`<h2>
${describeTrigger(
currentDetail,
this.hass,
this._entityReg
)}
</h2>`
: selectedType === "condition"
? html`<h2>
${describeCondition(
currentDetail,
this.hass,
this._entityReg
)}
</h2>`
: selectedType === "action"
? html`<h2>
${describeAction(
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
currentDetail
)}
</h2>`
: selectedType === "chooseOption"
? html`<h2>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option",
{ number: pathParts[pathParts.length - 1] }
)}
</h2>`
: nothing
: html`<h2>
${curPath.substring(this.selected.path.length + 1)}
</h2>`}

View File

@ -53,9 +53,12 @@ import "./hat-graph-node";
import "./hat-graph-spacer";
import { ACTION_ICONS } from "../../data/action";
type NodeType = "trigger" | "condition" | "action" | "chooseOption" | undefined;
export interface NodeInfo {
path: string;
config: any;
type?: NodeType;
}
declare global {
@ -76,16 +79,16 @@ export class HatScriptGraph extends LitElement {
public trackedNodes: Record<string, NodeInfo> = {};
private _selectNode(config, path) {
private _selectNode(config, path, type?) {
return () => {
fireEvent(this, "graph-node-selected", { config, path });
fireEvent(this, "graph-node-selected", { config, path, type });
};
}
private _renderTrigger(config: Trigger, i: number) {
const path = `trigger/${i}`;
const track = this.trace && path in this.trace.trace;
this.renderedNodes[path] = { config, path };
this.renderedNodes[path] = { config, path, type: "trigger" };
if (track) {
this.trackedNodes[path] = this.renderedNodes[path];
}
@ -93,7 +96,7 @@ export class HatScriptGraph extends LitElement {
<hat-graph-node
graph-start
?track=${track}
@focus=${this._selectNode(config, path)}
@focus=${this._selectNode(config, path, "trigger")}
?active=${this.selected === path}
.iconPath=${mdiAsterisk}
.notEnabled=${"enabled" in config && config.enabled === false}
@ -105,7 +108,7 @@ export class HatScriptGraph extends LitElement {
private _renderCondition(config: Condition, i: number) {
const path = `condition/${i}`;
this.renderedNodes[path] = { config, path };
this.renderedNodes[path] = { config, path, type: "condition" };
if (this.trace && path in this.trace.trace) {
this.trackedNodes[path] = this.renderedNodes[path];
}
@ -136,7 +139,7 @@ export class HatScriptGraph extends LitElement {
) {
const type =
Object.keys(this._typeRenderers).find((key) => key in node) || "other";
this.renderedNodes[path] = { config: node, path };
this.renderedNodes[path] = { config: node, path, type: "action" };
if (this.trace && path in this.trace.trace) {
this.trackedNodes[path] = this.renderedNodes[path];
}
@ -166,7 +169,7 @@ export class HatScriptGraph extends LitElement {
return html`
<hat-graph-branch
tabindex=${trace === undefined ? "-1" : "0"}
@focus=${this._selectNode(config, path)}
@focus=${this._selectNode(config, path, "action")}
?track=${trace !== undefined}
?active=${this.selected === path}
.notEnabled=${disabled || config.enabled === false}
@ -186,7 +189,11 @@ export class HatScriptGraph extends LitElement {
? ensureArray(config.choose)?.map((branch, i) => {
const branchPath = `${path}/choose/${i}`;
const trackThis = tracePath.includes(i);
this.renderedNodes[branchPath] = { config, path: branchPath };
this.renderedNodes[branchPath] = {
config: branch,
path: branchPath,
type: "chooseOption",
};
if (trackThis) {
this.trackedNodes[branchPath] = this.renderedNodes[branchPath];
}
@ -196,7 +203,11 @@ export class HatScriptGraph extends LitElement {
.iconPath=${!trace || trackThis
? mdiCheckboxMarkedOutline
: mdiCheckboxBlankOutline}
@focus=${this._selectNode(config, branchPath)}
@focus=${this._selectNode(
branch,
branchPath,
"chooseOption"
)}
?track=${trackThis}
?active=${this.selected === branchPath}
.notEnabled=${disabled || config.enabled === false}
@ -256,7 +267,7 @@ export class HatScriptGraph extends LitElement {
return html`
<hat-graph-branch
tabindex=${trace === undefined ? "-1" : "0"}
@focus=${this._selectNode(config, path)}
@focus=${this._selectNode(config, path, "action")}
?track=${trace !== undefined}
?active=${this.selected === path}
.notEnabled=${disabled || config.enabled === false}
@ -337,7 +348,7 @@ export class HatScriptGraph extends LitElement {
}
return html`
<hat-graph-branch
@focus=${this._selectNode(node, path)}
@focus=${this._selectNode(node, path, "condition")}
?track=${track}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
@ -381,7 +392,7 @@ export class HatScriptGraph extends LitElement {
return html`
<hat-graph-branch
tabindex=${trace === undefined ? "-1" : "0"}
@focus=${this._selectNode(node, path)}
@focus=${this._selectNode(node, path, "action")}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
@ -427,7 +438,7 @@ export class HatScriptGraph extends LitElement {
<hat-graph-node
.graphStart=${graphStart}
.iconPath=${node.action ? undefined : mdiRoomService}
@focus=${this._selectNode(node, path)}
@focus=${this._selectNode(node, path, "action")}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
@ -455,7 +466,7 @@ export class HatScriptGraph extends LitElement {
<hat-graph-node
.graphStart=${graphStart}
.iconPath=${mdiCodeBraces}
@focus=${this._selectNode(node, path)}
@focus=${this._selectNode(node, path, "action")}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
@ -475,7 +486,7 @@ export class HatScriptGraph extends LitElement {
return html`
<hat-graph-branch
tabindex=${trace === undefined ? "-1" : "0"}
@focus=${this._selectNode(node, path)}
@focus=${this._selectNode(node, path, "action")}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
@ -513,7 +524,7 @@ export class HatScriptGraph extends LitElement {
return html`
<hat-graph-branch
tabindex=${trace === undefined ? "-1" : "0"}
@focus=${this._selectNode(node, path)}
@focus=${this._selectNode(node, path, "action")}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
@ -562,7 +573,7 @@ export class HatScriptGraph extends LitElement {
<hat-graph-node
.graphStart=${graphStart}
.iconPath=${ACTION_ICONS[getActionType(node)] || mdiCodeBrackets}
@focus=${this._selectNode(node, path)}
@focus=${this._selectNode(node, path, "action")}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}

View File

@ -84,21 +84,24 @@ class UserBadge extends LitElement {
static styles = css`
:host {
display: contents;
}
.picture {
display: block;
width: 40px;
height: 40px;
}
.picture {
width: 100%;
height: 100%;
background-size: cover;
border-radius: 50%;
}
.initials {
display: inline-block;
display: inline-flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
width: 40px;
line-height: 40px;
width: 100%;
height: 100%;
border-radius: 50%;
text-align: center;
background-color: var(--light-primary-color);
text-decoration: none;
color: var(--text-light-primary-color, var(--primary-text-color));

View File

@ -49,9 +49,13 @@ export const testAssistSatelliteConnection = (
export const assistSatelliteAnnounce = (
hass: HomeAssistant,
entity_id: string,
message: string
) =>
hass.callService("assist_satellite", "announce", { message }, { entity_id });
args: {
message?: string;
media_id?: string;
preannounce?: boolean;
preannounce_media_id?: string;
}
) => hass.callService("assist_satellite", "announce", args, { entity_id });
export const fetchAssistSatelliteConfiguration = (
hass: HomeAssistant,

View File

@ -66,11 +66,16 @@ export type ManagerStateEvent =
export const subscribeBackupEvents = (
hass: HomeAssistant,
callback: (event: ManagerStateEvent) => void
callback: (event: ManagerStateEvent) => void,
preCheck?: () => boolean | Promise<boolean>
) =>
hass.connection.subscribeMessage<ManagerStateEvent>(callback, {
type: "backup/subscribe_events",
});
hass.connection.subscribeMessage<ManagerStateEvent>(
callback,
{
type: "backup/subscribe_events",
},
{ preCheck }
);
export const DEFAULT_MANAGER_STATE: ManagerStateEvent = {
manager_state: "idle",

View File

@ -1,7 +1,5 @@
import type { Connection } from "home-assistant-js-websocket";
import { getCollection } from "home-assistant-js-websocket";
import type { LocalizeFunc } from "../common/translations/localize";
import { debounce } from "../common/util/debounce";
import type { HomeAssistant } from "../types";
import type {
DataEntryFlowProgress,
@ -93,31 +91,20 @@ export const fetchConfigFlowInProgress = (
type: "config_entries/flow/progress",
});
const subscribeConfigFlowInProgressUpdates = (conn: Connection, store) =>
conn.subscribeEvents(
debounce(
() =>
fetchConfigFlowInProgress(conn).then((flows: DataEntryFlowProgress[]) =>
store.setState(flows, true)
),
500,
true
),
"config_entry_discovered"
);
export const getConfigFlowInProgressCollection = (conn: Connection) =>
getCollection<DataEntryFlowProgress[]>(
conn,
"_configFlowProgress",
fetchConfigFlowInProgress,
subscribeConfigFlowInProgressUpdates
);
export interface ConfigFlowInProgressMessage {
type: null | "added" | "removed";
flow_id: string;
flow: DataEntryFlowProgress;
}
export const subscribeConfigFlowInProgress = (
hass: HomeAssistant,
onChange: (flows: DataEntryFlowProgress[]) => void
) => getConfigFlowInProgressCollection(hass.connection).subscribe(onChange);
onChange: (update: ConfigFlowInProgressMessage[]) => void
) =>
hass.connection.subscribeMessage<ConfigFlowInProgressMessage[]>(
(message) => onChange(message),
{ type: "config_entries/flow/subscribe" }
);
export const localizeConfigFlowTitle = (
localize: LocalizeFunc,

View File

@ -17,6 +17,15 @@ export interface DataEntryFlowProgressedEvent {
};
}
export interface DataEntryFlowProgressEvent {
type: "data_entry_flow_progress_update";
data: {
handler: string;
flow_id: string;
progress: number;
};
}
export interface DataEntryFlowProgress {
flow_id: string;
handler: string;
@ -108,3 +117,12 @@ export const subscribeDataEntryFlowProgressed = (
callback,
"data_entry_flow_progressed"
);
export const subscribeDataEntryFlowProgress = (
conn: Connection,
callback: (ev: DataEntryFlowProgressEvent) => void
) =>
conn.subscribeEvents<DataEntryFlowProgressEvent>(
callback,
"data_entry_flow_progress_update"
);

View File

@ -3,6 +3,7 @@ import { getOptimisticCollection } from "./collection";
export interface CoreFrontendUserData {
showAdvanced?: boolean;
showEntityIdPicker?: boolean;
}
declare global {

View File

@ -154,3 +154,9 @@ export const subscribeLogInfo = (
conn,
onChange
);
export const waitForIntegrationSetup = (hass: HomeAssistant, domain: string) =>
hass.callWS<{ integration_loaded: boolean }>({
type: "integration/wait",
domain,
});

View File

@ -128,3 +128,11 @@ export const forgotPasswordHaCloud = async (email: string) =>
body: JSON.stringify({ email }),
})
);
export const waitForIntegration = (domain: string) =>
handleFetchPromise<{ integration_loaded: boolean }>(
fetch("/api/onboarding/integration/wait", {
method: "POST",
body: JSON.stringify({ domain }),
})
);

View File

@ -38,7 +38,7 @@ export interface Statistic {
export enum StatisticMeanType {
NONE = 0,
ARIMETHIC = 1,
ARITHMETIC = 1,
CIRCULAR = 2,
}

View File

@ -94,7 +94,14 @@ const tryDescribeAction = <T extends ActionType>(
const targets: string[] = [];
const targetOrData = config.target || config.data;
if (targetOrData) {
if (typeof targetOrData === "string" && isTemplate(targetOrData)) {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_template`,
{ name: "target" }
)
);
} else if (targetOrData) {
for (const [key, name] of Object.entries({
area_id: "areas",
device_id: "devices",

View File

@ -0,0 +1,21 @@
import type { HomeAssistant } from "../../types";
export interface SupervisorUpdateConfig {
add_on_backup_before_update: boolean;
add_on_backup_retain_copies?: number;
core_backup_before_update: boolean;
}
export const getSupervisorUpdateConfig = async (hass: HomeAssistant) =>
hass.callWS<SupervisorUpdateConfig>({
type: "hassio/update/config/info",
});
export const updateSupervisorUpdateConfig = async (
hass: HomeAssistant,
config: Partial<SupervisorUpdateConfig>
) =>
hass.callWS({
type: "hassio/update/config/update",
...config,
});

View File

@ -207,7 +207,11 @@ export const computeUpdateStateDisplay = (
return hass.formatEntityState(stateObj);
};
type UpdateType = "addon" | "home_assistant" | "generic";
export type UpdateType =
| "addon"
| "home_assistant"
| "home_assistant_os"
| "generic";
export const getUpdateType = (
stateObj: UpdateEntity,
@ -215,6 +219,7 @@ export const getUpdateType = (
): UpdateType => {
const entity_id = stateObj.entity_id;
const domain = entitySources[entity_id]?.domain;
if (domain !== "hassio") {
return "generic";
}
@ -224,13 +229,11 @@ export const getUpdateType = (
return "home_assistant";
}
if (
![
HOME_ASSISTANT_CORE_TITLE,
HOME_ASSISTANT_SUPERVISOR_TITLE,
HOME_ASSISTANT_OS_TITLE,
].includes(title)
) {
if (title === HOME_ASSISTANT_OS_TITLE) {
return "home_assistant_os";
}
if (title !== HOME_ASSISTANT_SUPERVISOR_TITLE) {
return "addon";
}
return "generic";

View File

@ -80,7 +80,7 @@ enum QRCodeVersion {
SmartStart = 1,
}
enum Protocols {
export enum Protocols {
ZWave = 0,
ZWaveLongRange = 1,
}
@ -151,12 +151,35 @@ export interface QRProvisioningInformation {
maxInclusionRequestInterval?: number | undefined;
uuid?: string | undefined;
supportedProtocols?: Protocols[] | undefined;
status?: ProvisioningEntryStatus;
}
export interface PlannedProvisioningEntry {
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
dsk: string;
securityClasses: SecurityClass[];
status?: ProvisioningEntryStatus;
}
export enum ProvisioningEntryStatus {
Active = 0,
Inactive = 1,
}
export interface DeviceConfig {
filename: string;
manufacturer: string;
manufacturerId: number;
label: string;
description: string;
devices: {
productType: number;
productId: number;
}[];
firmwareVersion: {
min: string;
max: string;
};
}
export const MINIMUM_QR_STRING_LENGTH = 52;
@ -195,6 +218,7 @@ export interface ZWaveJSController {
is_rebuilding_routes: boolean;
inclusion_state: InclusionState;
nodes: ZWaveJSNodeStatus[];
supports_long_range: boolean;
}
export interface ZWaveJSNodeStatus {
@ -555,7 +579,7 @@ export const zwaveTryParseDskFromQrCode = (
export const zwaveValidateDskAndEnterPin = (
hass: HomeAssistant,
entry_id: string,
pin: string
pin: string | false
) =>
hass.callWS({
type: "zwave_js/validate_dsk_and_enter_pin",
@ -585,19 +609,38 @@ export const zwaveParseQrCode = (
qr_code_string,
});
export const lookupZwaveDevice = (
hass: HomeAssistant,
entry_id: string,
manufacturerId: number,
productType: number,
productId: number,
applicationVersion?: string
): Promise<DeviceConfig> =>
hass.callWS({
type: "zwave_js/lookup_device",
entry_id,
manufacturerId,
productType,
productId,
applicationVersion,
});
export const provisionZwaveSmartStartNode = (
hass: HomeAssistant,
entry_id: string,
qr_provisioning_information?: QRProvisioningInformation,
qr_code_string?: string,
planned_provisioning_entry?: PlannedProvisioningEntry
): Promise<QRProvisioningInformation> =>
protocol?: Protocols,
device_name?: string,
area_id?: string
): Promise<string> =>
hass.callWS({
type: "zwave_js/provision_smart_start_node",
entry_id,
qr_code_string,
qr_provisioning_information,
planned_provisioning_entry,
protocol,
device_name,
area_id,
});
export const unprovisionZwaveSmartStartNode = (
@ -613,6 +656,16 @@ export const unprovisionZwaveSmartStartNode = (
node_id,
});
export const subscribeNewDevices = (
hass: HomeAssistant,
entry_id: string,
callbackFunction: (message: any) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage((message) => callbackFunction(message), {
type: "zwave_js/subscribe_new_devices",
entry_id: entry_id,
});
export const fetchZwaveNodeStatus = (
hass: HomeAssistant,
device_id: string

View File

@ -9,7 +9,10 @@ import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog";
import "../../components/ha-icon-button";
import type { DataEntryFlowStep } from "../../data/data_entry_flow";
import { subscribeDataEntryFlowProgressed } from "../../data/data_entry_flow";
import {
subscribeDataEntryFlowProgress,
subscribeDataEntryFlowProgressed,
} from "../../data/data_entry_flow";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
@ -52,6 +55,8 @@ class DataEntryFlowDialog extends LitElement {
@state() private _loading?: LoadingReason;
@state() private _progress?: number;
private _instance = instance;
@state() private _step:
@ -62,7 +67,7 @@ class DataEntryFlowDialog extends LitElement {
@state() private _handler?: string;
private _unsubDataEntryFlowProgressed?: Promise<UnsubscribeFunc>;
private _unsubDataEntryFlowProgress?: UnsubscribeFunc;
public async showDialog(params: DataEntryFlowDialogParams): Promise<void> {
this._params = params;
@ -160,11 +165,9 @@ class DataEntryFlowDialog extends LitElement {
this._step = undefined;
this._params = undefined;
this._handler = undefined;
if (this._unsubDataEntryFlowProgressed) {
this._unsubDataEntryFlowProgressed.then((unsub) => {
unsub();
});
this._unsubDataEntryFlowProgressed = undefined;
if (this._unsubDataEntryFlowProgress) {
this._unsubDataEntryFlowProgress();
this._unsubDataEntryFlowProgress = undefined;
}
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@ -255,7 +258,9 @@ class DataEntryFlowDialog extends LitElement {
.params=${this._params}
.step=${this._step}
.hass=${this.hass}
.domain=${this._step.handler}
.handler=${this._step.handler}
.domain=${this._params.domain ??
this._step.handler}
></step-flow-abort>
`
: this._step.type === "progress"
@ -264,6 +269,7 @@ class DataEntryFlowDialog extends LitElement {
.flowConfig=${this._params.flowConfig}
.step=${this._step}
.hass=${this.hass}
.progress=${this._progress}
></step-flow-progress>
`
: this._step.type === "menu"
@ -339,20 +345,28 @@ class DataEntryFlowDialog extends LitElement {
}
private async _subscribeDataEntryFlowProgressed() {
if (this._unsubDataEntryFlowProgressed) {
if (this._unsubDataEntryFlowProgress) {
return;
}
this._unsubDataEntryFlowProgressed = subscribeDataEntryFlowProgressed(
this.hass.connection,
async (ev) => {
this._progress = undefined;
const unsubs = [
subscribeDataEntryFlowProgressed(this.hass.connection, (ev) => {
if (ev.data.flow_id !== this._step?.flow_id) {
return;
}
this._processStep(
this._params!.flowConfig.fetchFlow(this.hass, this._step.flow_id)
);
}
);
this._progress = undefined;
}),
subscribeDataEntryFlowProgress(this.hass.connection, (ev) => {
// ha-progress-ring has an issue with 0 so we round up
this._progress = Math.ceil(ev.data.progress * 100);
}),
];
this._unsubDataEntryFlowProgress = async () => {
(await Promise.all(unsubs)).map((unsub) => unsub());
};
}
static get styles(): CSSResultGroup {

View File

@ -20,6 +20,8 @@ class StepFlowAbort extends LitElement {
@property({ attribute: false }) public domain!: string;
@property({ attribute: false }) public handler!: string;
protected firstUpdated(changed: PropertyValues) {
super.firstUpdated(changed);
if (this.step.reason === "missing_credentials") {
@ -58,7 +60,7 @@ class StepFlowAbort extends LitElement {
applicationCredentialAddedCallback: () => {
showConfigFlowDialog(this.params.dialogParentElement!, {
dialogClosedCallback: this.params.dialogClosedCallback,
startFlowHandler: this.domain,
startFlowHandler: this.handler,
showAdvanced: this.hass.userData?.showAdvanced,
navigateToResult: this.params.navigateToResult,
});

View File

@ -84,7 +84,7 @@ class StepFlowForm extends LitElement {
${this._loading
? html`
<div class="submit-spinner">
<ha-spinner></ha-spinner>
<ha-spinner size="small"></ha-spinner>
</div>
`
: html`
@ -263,6 +263,9 @@ class StepFlowForm extends LitElement {
}
.submit-spinner {
height: 36px;
display: flex;
align-items: center;
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;

View File

@ -2,11 +2,13 @@ import "@material/mwc-button";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../components/ha-progress-ring";
import "../../components/ha-spinner";
import type { DataEntryFlowStepProgress } from "../../data/data_entry_flow";
import type { HomeAssistant } from "../../types";
import type { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
import { blankBeforePercent } from "../../common/translations/blank_before_percent";
@customElement("step-flow-progress")
class StepFlowProgress extends LitElement {
@ -19,13 +21,24 @@ class StepFlowProgress extends LitElement {
@property({ attribute: false })
public step!: DataEntryFlowStepProgress;
@property({ type: Number })
public progress?: number;
protected render(): TemplateResult {
return html`
<h2>
${this.flowConfig.renderShowFormProgressHeader(this.hass, this.step)}
</h2>
<div class="content">
<ha-spinner></ha-spinner>
${this.progress
? html`
<ha-progress-ring .value=${this.progress} size="large"
>${this.progress}${blankBeforePercent(
this.hass.locale
)}%</ha-progress-ring
>
`
: html` <ha-spinner size="large"></ha-spinner> `}
${this.flowConfig.renderShowFormProgressDescription(
this.hass,
this.step

View File

@ -45,7 +45,8 @@ class MoreInfoCamera extends LitElement {
<ha-progress-button
@click=${this._downloadSnapshot}
.progress=${this._waiting}
.disabled=${this.stateObj.state === UNAVAILABLE}
.disabled=${this.stateObj.state === UNAVAILABLE ||
this.stateObj.state === "idle"}
>
${this.hass.localize(
"ui.dialogs.more_info_control.camera.download_snapshot"

View File

@ -1,26 +1,27 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { BINARY_STATE_OFF } from "../../../common/const";
import { relativeTime } from "../../../common/datetime/relative_time";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-checkbox";
import "../../../components/ha-spinner";
import "../../../components/ha-faded";
import "../../../components/ha-formfield";
import "../../../components/ha-markdown";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import type { BackupConfig } from "../../../data/backup";
import { fetchBackupConfig } from "../../../data/backup";
import { isUnavailableState } from "../../../data/entity";
import type { EntitySources } from "../../../data/entity_sources";
import { fetchEntitySourcesWithCache } from "../../../data/entity_sources";
import type { UpdateEntity } from "../../../data/update";
import { getSupervisorUpdateConfig } from "../../../data/supervisor/update";
import type { UpdateEntity, UpdateType } from "../../../data/update";
import {
getUpdateType,
UpdateEntityFeature,
@ -44,17 +45,49 @@ class MoreInfoUpdate extends LitElement {
@state() private _backupConfig?: BackupConfig;
@state() private _createBackup = false;
@state() private _entitySources?: EntitySources;
private async _fetchBackupConfig() {
const { config } = await fetchBackupConfig(this.hass);
this._backupConfig = config;
try {
const { config } = await fetchBackupConfig(this.hass);
this._backupConfig = config;
} catch (err) {
// ignore error, because user will get a manual backup option
// eslint-disable-next-line no-console
console.error(err);
}
}
private async _fetchUpdateBackupConfig(type: UpdateType) {
try {
const config = await getSupervisorUpdateConfig(this.hass);
// for home assistant and OS updates
if (this._isHaOrOsUpdate(type)) {
this._createBackup = config.core_backup_before_update;
return;
}
if (type === "addon") {
this._createBackup = config.add_on_backup_before_update;
}
} catch (err) {
// ignore error, because user can still set the config
// eslint-disable-next-line no-console
console.error(err);
}
}
private async _fetchEntitySources() {
this._entitySources = await fetchEntitySourcesWithCache(this.hass);
}
private _isHaOrOsUpdate(type: UpdateType): boolean {
return ["home_assistant", "home_assistant_os"].includes(type);
}
private _computeCreateBackupTexts():
| { title: string; description?: string }
| undefined {
@ -69,8 +102,7 @@ class MoreInfoUpdate extends LitElement {
? getUpdateType(this.stateObj, this._entitySources)
: "generic";
// Automatic or manual for Home Assistant update
if (updateType === "home_assistant") {
if (this._isHaOrOsUpdate(updateType)) {
const isBackupConfigValid =
!!this._backupConfig &&
!!this._backupConfig.automatic_backups_configured &&
@ -256,7 +288,8 @@ class MoreInfoUpdate extends LitElement {
: nothing}
<ha-switch
slot="end"
id="create-backup"
.checked=${this._createBackup}
@change=${this._createBackupChanged}
.disabled=${updateIsInstalling(this.stateObj)}
></ha-switch>
</ha-md-list-item>
@ -319,7 +352,14 @@ class MoreInfoUpdate extends LitElement {
if (supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
this._fetchEntitySources().then(() => {
const type = getUpdateType(this.stateObj!, this._entitySources!);
if (type === "home_assistant") {
if (
isComponentLoaded(this.hass, "hassio") &&
["addon", "home_assistant", "home_assistant_os"].includes(type)
) {
this._fetchUpdateBackupConfig(type);
}
if (this._isHaOrOsUpdate(type)) {
this._fetchBackupConfig();
}
});
@ -347,13 +387,7 @@ class MoreInfoUpdate extends LitElement {
if (!supportsFeature(this.stateObj!, UpdateEntityFeature.BACKUP)) {
return false;
}
const createBackupSwitch = this.shadowRoot?.getElementById(
"create-backup"
) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
}
return false;
return this._createBackup;
}
private _handleInstall(): void {
@ -375,6 +409,10 @@ class MoreInfoUpdate extends LitElement {
this.hass.callService("update", "install", installData);
}
private _createBackupChanged(ev) {
this._createBackup = ev.target.checked;
}
private _handleSkip(): void {
if (this.stateObj!.attributes.auto_update) {
showAlertDialog(this, {

View File

@ -21,8 +21,10 @@ import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { getEntityContext } from "../../common/entity/get_entity_context";
import {
computeEntityEntryName,
computeEntityName,
} from "../../common/entity/compute_entity_name";
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
import { navigate } from "../../common/navigate";
import "../../components/ha-button-menu";
@ -56,6 +58,10 @@ import "./ha-more-info-history-and-logbook";
import "./ha-more-info-info";
import "./ha-more-info-settings";
import "./more-info-content";
import {
getEntityContext,
getEntityEntryContext,
} from "../../common/entity/get_entity_context";
export interface MoreInfoDialogParams {
entityId: string | null;
@ -270,6 +276,11 @@ export class MoreInfoDialog extends LitElement {
this._setView("related");
}
private _breadcrumbClick(ev: Event) {
ev.stopPropagation();
this._setView("related");
}
private async _loadNumericDeviceClasses() {
const deviceClasses = await getSensorNumericDeviceClasses(this.hass);
this._sensorNumericDeviceClasses = deviceClasses.numeric_device_classes;
@ -293,11 +304,18 @@ export class MoreInfoDialog extends LitElement {
this._initialView !== DEFAULT_VIEW && !this._childView;
const showCloseIcon = isDefaultView || isSpecificInitialView;
const context = stateObj ? getEntityContext(stateObj, this.hass) : null;
const context = stateObj
? getEntityContext(stateObj, this.hass)
: this._entry
? getEntityEntryContext(this._entry, this.hass)
: undefined;
const entityName = stateObj
? computeEntityName(stateObj, this.hass)
: undefined;
: this._entry
? computeEntityEntryName(this._entry, this.hass)
: entityId;
const deviceName = context?.device
? computeDeviceName(context.device)
: undefined;
@ -306,7 +324,7 @@ export class MoreInfoDialog extends LitElement {
const breadcrumb = [areaName, deviceName, entityName].filter(
(v): v is string => Boolean(v)
);
const title = this._childView?.viewTitle || breadcrumb.pop();
const title = this._childView?.viewTitle || breadcrumb.pop() || entityId;
return html`
<ha-dialog
@ -337,18 +355,23 @@ export class MoreInfoDialog extends LitElement {
)}
></ha-icon-button-prev>
`}
<span
slot="title"
.title=${title}
@click=${this._enlarge}
class="title"
>
<span slot="title" @click=${this._enlarge} class="title">
${breadcrumb.length > 0
? html`
<p class="breadcrumb">
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
</p>
`
? !__DEMO__ && isAdmin
? html`
<button
class="breadcrumb"
@click=${this._breadcrumbClick}
aria-label=${breadcrumb.join(" > ")}
>
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
</button>
`
: html`
<p class="breadcrumb">
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
</p>
`
: nothing}
<p class="main">${title}</p>
</span>
@ -643,6 +666,7 @@ export class MoreInfoDialog extends LitElement {
.title {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.title p {
@ -663,11 +687,30 @@ export class MoreInfoDialog extends LitElement {
color: var(--secondary-text-color);
font-size: 14px;
line-height: 16px;
margin-top: -6px;
--mdc-icon-size: 16px;
padding: 4px;
margin: -4px;
margin-top: -10px;
background: none;
border: none;
outline: none;
display: inline;
border-radius: 6px;
transition: background-color 180ms ease-in-out;
min-width: 0;
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
text-align: left;
}
.title .breadcrumb {
--mdc-icon-size: 16px;
.title button.breadcrumb {
cursor: pointer;
}
.title button.breadcrumb:focus-visible,
.title button.breadcrumb:hover {
background-color: rgba(var(--rgb-secondary-text-color), 0.08);
}
`,
];

View File

@ -20,6 +20,7 @@ import type {
import { fetchStatistics, getStatisticMetadata } from "../../data/recorder";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import type { HomeAssistant } from "../../types";
import { haStyle } from "../../resources/styles";
declare global {
interface HASSDomEvents {
@ -58,9 +59,9 @@ export class MoreInfoHistory extends LitElement {
return html`${isComponentLoaded(this.hass, "history")
? html`<div class="header">
<div class="title">
<h2>
${this.hass.localize("ui.dialogs.more_info_control.history")}
</div>
</h2>
${__DEMO__
? nothing
: html`<a href=${this._showMoreHref}
@ -231,27 +232,25 @@ export class MoreInfoHistory extends LitElement {
this._setRedrawTimer();
}
static styles = css`
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.header > a,
a:visited {
color: var(--primary-color);
}
.title {
font-family: var(--paper-font-title_-_font-family);
-webkit-font-smoothing: var(--paper-font-title_-_-webkit-font-smoothing);
font-size: var(--paper-font-subhead_-_font-size);
font-weight: var(--paper-font-title_-_font-weight);
letter-spacing: var(--paper-font-title_-_letter-spacing);
line-height: var(--paper-font-title_-_line-height);
}
`;
static styles = [
haStyle,
css`
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.header > a,
a:visited {
color: var(--primary-color);
}
h2 {
margin: 0;
}
`,
];
}
declare global {

View File

@ -133,8 +133,8 @@ export class MoreInfoInfo extends LitElement {
[data-domain="camera"] .content {
padding: 0;
/* max height of the video is full screen, minus the height of the header of the dialog and the padding of the dialog (mdc-dialog-max-height: calc(100% - 72px)) */
--video-max-height: calc(100vh - 65px - 72px);
/* max height of the video is full screen, minus the height of the header of the dialog (79px) and the max height of the dialog (mdc-dialog-max-height: calc(100% - 72px)) and the actions bar 60px */
--video-max-height: calc(100vh - 72px - 79px - 60px);
}
more-info-content {

View File

@ -7,6 +7,7 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { createSearchParam } from "../../common/url/search-params";
import "../../panels/logbook/ha-logbook";
import type { HomeAssistant } from "../../types";
import { haStyle } from "../../resources/styles";
@customElement("ha-more-info-logbook")
export class MoreInfoLogbook extends LitElement {
@ -32,9 +33,7 @@ export class MoreInfoLogbook extends LitElement {
return html`
<div class="header">
<div class="title">
${this.hass.localize("ui.dialogs.more_info_control.logbook")}
</div>
<h2>${this.hass.localize("ui.dialogs.more_info_control.logbook")}</h2>
<a href=${this._showMoreHref}
>${this.hass.localize("ui.dialogs.more_info_control.show_more")}</a
>
@ -68,6 +67,7 @@ export class MoreInfoLogbook extends LitElement {
static get styles() {
return [
haStyle,
css`
ha-logbook {
--logbook-max-height: 250px;
@ -88,15 +88,8 @@ export class MoreInfoLogbook extends LitElement {
a:visited {
color: var(--primary-color);
}
.title {
font-family: var(--paper-font-title_-_font-family);
-webkit-font-smoothing: var(
--paper-font-title_-_-webkit-font-smoothing
);
font-size: var(--paper-font-subhead_-_font-size);
font-weight: var(--paper-font-title_-_font-weight);
letter-spacing: var(--paper-font-title_-_letter-spacing);
line-height: var(--paper-font-title_-_line-height);
h2 {
margin: 0;
}
`,
];

View File

@ -25,15 +25,14 @@ export class HuiNotificationItemTemplate extends LitElement {
}
ha-card .header {
/* start paper-font-headline style */
font-family: "Roboto", "Noto", sans-serif;
-webkit-font-smoothing: antialiased; /* OS X subpixel AA bleed bug */
text-rendering: optimizeLegibility;
font-size: 24px;
font-weight: 400;
letter-spacing: -0.012em;
line-height: 32px;
/* end paper-font-headline style */
font-family: var(--paper-font-headline_-_font-family);
-webkit-font-smoothing: var(
--paper-font-headline_-_-webkit-font-smoothing
);
font-size: var(--paper-font-headline_-_font-size);
font-weight: var(--paper-font-headline_-_font-weight);
letter-spacing: var(--paper-font-headline_-_letter-spacing);
line-height: var(--paper-font-headline_-_line-height);
color: var(--primary-text-color);
padding: 16px 16px 0;

View File

@ -5,6 +5,7 @@ import {
mdiConsoleLine,
mdiDevices,
mdiEarth,
mdiKeyboard,
mdiMagnify,
mdiReload,
mdiServerNetwork,
@ -31,6 +32,7 @@ import "../../components/ha-label";
import "../../components/ha-list-item";
import "../../components/ha-spinner";
import "../../components/ha-textfield";
import "../../components/ha-tip";
import { fetchHassioAddonsInfo } from "../../data/hassio/addon";
import { domainToName } from "../../data/integration";
import { getPanelNameTranslationKey } from "../../data/panel";
@ -40,6 +42,7 @@ import { haStyleDialog, haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar";
interface QuickBarItem extends ScorableTextItem {
@ -422,10 +425,12 @@ export class QuickBar extends LitElement {
}
private _addSpinnerToCommandItem(index: number): void {
const div = document.createElement("div");
div.slot = "meta";
const spinner = document.createElement("ha-spinner");
spinner.size = "small";
spinner.slot = "meta";
this._getItemAtIndex(index)?.appendChild(spinner);
div.appendChild(spinner);
this._getItemAtIndex(index)?.appendChild(div);
}
private _handleSearchChange(ev: CustomEvent): void {
@ -735,10 +740,20 @@ export class QuickBar extends LitElement {
}
}
const additionalItems = [
{
path: "",
primaryText: this.hass.localize("ui.panel.config.info.shortcuts"),
action: () => showShortcutsDialog(this),
iconPath: mdiKeyboard,
},
];
return this._finalizeNavigationCommands([
...panelItems,
...sectionItems,
...supervisorItems,
...additionalItems,
]);
}
@ -815,12 +830,12 @@ export class QuickBar extends LitElement {
const categoryKey: CommandItem["categoryKey"] = "navigation";
const navItem = {
...item,
iconPath: mdiEarth,
categoryText: this.hass.localize(
`ui.dialogs.quick-bar.commands.types.${categoryKey}`
),
action: () => navigate(item.path),
...item,
};
return {

View File

@ -0,0 +1,228 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import { createCloseHeading } from "../../components/ha-dialog";
import type { HomeAssistant } from "../../types";
import { haStyleDialog } from "../../resources/styles";
import "../../components/ha-alert";
import "../../components/chips/ha-assist-chip";
import type { LocalizeKeys } from "../../common/translations/localize";
interface Text {
type: "text";
key: LocalizeKeys;
}
type ShortcutString = string | { key: LocalizeKeys };
interface Shortcut {
type: "shortcut";
shortcut: ShortcutString[];
key: LocalizeKeys;
}
interface Section {
key: LocalizeKeys;
items: (Text | Shortcut)[];
}
const _SHORTCUTS: Section[] = [
{
key: "ui.dialogs.shortcuts.searching.title",
items: [
{ type: "text", key: "ui.dialogs.shortcuts.searching.on_any_page" },
{
type: "shortcut",
shortcut: ["C"],
key: "ui.dialogs.shortcuts.searching.search_command",
},
{
type: "shortcut",
shortcut: ["E"],
key: "ui.dialogs.shortcuts.searching.search_entities",
},
{
type: "shortcut",
shortcut: ["D"],
key: "ui.dialogs.shortcuts.searching.search_devices",
},
{
type: "text",
key: "ui.dialogs.shortcuts.searching.on_pages_with_tables",
},
{
type: "shortcut",
shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "F"],
key: "ui.dialogs.shortcuts.searching.search_in_table",
},
],
},
{
key: "ui.dialogs.shortcuts.assist.title",
items: [
{
type: "shortcut",
shortcut: ["A"],
key: "ui.dialogs.shortcuts.assist.open_assist",
},
],
},
{
key: "ui.dialogs.shortcuts.charts.title",
items: [
{
type: "shortcut",
shortcut: [
{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" },
{ key: "ui.dialogs.shortcuts.shortcuts.drag" },
],
key: "ui.dialogs.shortcuts.charts.drag_to_zoom",
},
{
type: "shortcut",
shortcut: [
{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" },
{ key: "ui.dialogs.shortcuts.shortcuts.scroll_wheel" },
],
key: "ui.dialogs.shortcuts.charts.scroll_to_zoom",
},
{
type: "shortcut",
shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.double_click" }],
key: "ui.dialogs.shortcuts.charts.double_click",
},
],
},
{
key: "ui.dialogs.shortcuts.other.title",
items: [
{
type: "shortcut",
shortcut: ["M"],
key: "ui.dialogs.shortcuts.other.my_link",
},
],
},
];
@customElement("dialog-shortcuts")
class DialogShortcuts extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false;
public async showDialog(): Promise<void> {
this._opened = true;
}
public async closeDialog(): Promise<void> {
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _renderShortcut(
shortcuts: ShortcutString[],
translationKey: LocalizeKeys
) {
const keys = shortcuts.map((shortcut) =>
typeof shortcut === "string" ? shortcut : this.hass.localize(shortcut.key)
);
return html`
<div class="shortcut">
${keys.map((key) => html` <span>${key.toUpperCase()}</span>`)}
${this.hass.localize(translationKey)}
</div>
`;
}
protected render() {
if (!this._opened) {
return nothing;
}
return html`
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
defaultAction="ignore"
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.shortcuts.title")
)}
>
<div class="content">
${_SHORTCUTS.map(
(section) => html`
<h3>${this.hass.localize(section.key)}</h3>
<div class="items">
${section.items.map((item) => {
if (item.type === "text") {
return html`<p>${this.hass.localize(item.key)}</p>`;
}
if (item.type === "shortcut") {
return this._renderShortcut(item.shortcut, item.key);
}
return nothing;
})}
</div>
`
)}
</div>
<ha-alert>
${this.hass.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", {
user_profile: html`<a href="/profile/general#shortcuts"
>${this.hass.localize(
"ui.dialogs.shortcuts.enable_shortcuts_hint_user_profile"
)}</a
>`,
})}
</ha-alert>
</ha-dialog>
`;
}
static styles = [
haStyleDialog,
css`
ha-dialog {
--dialog-z-index: 15;
}
h3:first-of-type {
margin-top: 0;
}
.content {
margin-bottom: 24px;
}
.shortcut {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
margin: 4px 0;
}
span {
padding: 8px;
border: 1px solid var(--outline-color);
border-radius: 8px;
}
.items p {
margin-bottom: 8px;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"dialog-shortcuts": DialogShortcuts;
}
}

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