Compare commits

...

180 Commits

Author SHA1 Message Date
Simon Lamon fcab356639
Change netlify actions (#24815)
* Netlify fix

* Fix missing \

* Prettier

* PR review

* npx

* npx
2025-04-22 13:22:03 +02:00
Paul Bottein a70a0d4b4a
Fix event propagation in generic entity row (#25134) 2025-04-22 13:15:20 +02:00
Simon Lamon 5a34560381
Delete entity/device to Remove entity/device in scene editor (#25116)
Delete entity/device to Remove entity/device
2025-04-22 08:57:47 +03:00
renovate[bot] f71245893a
Update dependency eslint to v9.25.0 (#25126) 2025-04-21 21:06:12 +02:00
renovate[bot] 1e7bfd59f2
Update dependency @lokalise/node-api to v14.4.0 (#25123)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 20:43:27 +02:00
J. Nick Koston 1c15116052
Add initial DHCP discovery panel (#25086) 2025-04-21 08:09:58 -10:00
Simon Lamon 3647722824
Camera view options translated (#25119)
* Camera view options translated

* Swap around changes

* Clean up

* Clean up
2025-04-21 14:21:32 +03:00
Simon Lamon 713dd68089
Sensor card: Graph types options translated (#25118)
* Sensor card: Graph types options translated

* Clean up
2025-04-21 14:18:41 +03:00
karwosts 53dd0cbaa8
Fix some min/mean/max issues with statistics line charts (#25107)
* Fix some min/mean/max issues with statistics line charts

* minor simplification
2025-04-21 14:09:07 +03:00
Simon Lamon 6bf8faa96a
Translation for number of occurrences (#25117)
* Translation for number of occurrences

* Update src/translations/en.json
2025-04-21 11:47:43 +03:00
dependabot[bot] 09a17131ab
Bump softprops/action-gh-release from 2.2.1 to 2.2.2 (#25120)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2.2.1...v2.2.2)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 2.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-21 11:46:14 +03:00
renovate[bot] 7f20b2d6d2
Update dependency @rsdoctor/rspack-plugin to v1.0.2 (#25112)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-20 12:29:35 +02:00
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 8a2ab2eab4.
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 9cfcd21a93.
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
243 changed files with 9950 additions and 6313 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
@ -41,9 +41,8 @@ jobs:
- name: Deploy to Netlify
id: deploy
uses: netlify/actions/cli@master
with:
args: deploy --dir=cast/dist --alias dev
run: |
npx -y netlify-cli deploy --dir=cast/dist --alias dev
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}
@ -62,7 +61,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
@ -77,9 +76,8 @@ jobs:
- name: Deploy to Netlify
id: deploy
uses: netlify/actions/cli@master
with:
args: deploy --dir=cast/dist --prod
run: |
npx -y netlify-cli deploy --dir=cast/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }}

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
@ -42,9 +42,8 @@ jobs:
- name: Deploy to Netlify
id: deploy
uses: netlify/actions/cli@master
with:
args: deploy --dir=demo/dist --prod
run: |
npx -y netlify-cli deploy --dir=demo/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }}
@ -63,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
@ -78,9 +77,8 @@ jobs:
- name: Deploy to Netlify
id: deploy
uses: netlify/actions/cli@master
with:
args: deploy --dir=demo/dist --prod
run: |
npx -y netlify-cli deploy --dir=demo/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }}

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
@ -34,9 +34,8 @@ jobs:
- name: Deploy to Netlify
id: deploy
uses: netlify/actions/cli@master
with:
args: deploy --dir=gallery/dist --prod
run: |
npx -y netlify-cli deploy --dir=gallery/dist --prod
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}

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
@ -39,13 +39,14 @@ jobs:
- name: Deploy preview to Netlify
id: deploy
uses: netlify/actions/cli@master
with:
args: deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}"
run: |
npx -y netlify-cli deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \
--json > deploy_output.json
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }}
- name: Generate summary
run: |
echo "${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}" >> "$GITHUB_STEP_SUMMARY"
NETLIFY_LIVE_URL=$(jq -r '.deploy_url' deploy_output.json)
echo "$NETLIFY_LIVE_URL" >> "$GITHUB_STEP_SUMMARY"

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
@ -55,7 +55,7 @@ jobs:
script/release
- name: Upload release assets
uses: softprops/action-gh-release@v2.2.1
uses: softprops/action-gh-release@v2.2.2
with:
files: |
dist/*.whl
@ -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
@ -107,7 +107,7 @@ jobs:
- name: Tar folder
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
- name: Upload release asset
uses: softprops/action-gh-release@v2.2.1
uses: softprops/action-gh-release@v2.2.2
with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
@ -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
@ -136,6 +136,6 @@ jobs:
- name: Tar folder
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
- name: Upload release asset
uses: softprops/action-gh-release@v2.2.1
uses: softprops/action-gh-release@v2.2.2
with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz

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.4.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.2",
"@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.25.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: {
@ -446,12 +450,14 @@ export class StatisticsChart extends LitElement {
const hasMean =
this.statTypes.includes("mean") && statisticsHaveType(stats, "mean");
const drawBands =
hasMean ||
(this.statTypes.includes("min") &&
statisticsHaveType(stats, "min") &&
this.statTypes.includes("max") &&
statisticsHaveType(stats, "max"));
const hasMax =
this.statTypes.includes("max") && statisticsHaveType(stats, "max");
const hasMin =
this.statTypes.includes("min") && statisticsHaveType(stats, "min");
const drawBands = [hasMean, hasMax, hasMin].filter(Boolean).length > 1;
const bandTop = hasMax ? "max" : "mean";
const bandBottom = hasMin ? "min" : "mean";
const sortedTypes = drawBands
? [...this.statTypes].sort((a, b) => {
@ -468,10 +474,12 @@ export class StatisticsChart extends LitElement {
let displayedLegend = false;
sortedTypes.forEach((type) => {
if (statisticsHaveType(stats, type)) {
const band = drawBands && (type === "min" || type === "max");
const band = drawBands && (type === bandTop || type === bandBottom);
statTypes.push(type);
const borderColor =
band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color;
band && hasMin && hasMax && hasMean
? color + (this.hideLegend ? "00" : "7F")
: color;
const backgroundColor = band ? color + "3F" : color + "7F";
const series: LineSeriesOption | BarSeriesOption = {
id: `${statistic_id}-${type}`,
@ -506,7 +514,7 @@ export class StatisticsChart extends LitElement {
if (band && this.chartType === "line") {
series.stack = `band-${statistic_id}`;
series.stackStrategy = "all";
if (drawBands && type === "max") {
if (drawBands && type === bandTop) {
(series as LineSeriesOption).areaStyle = {
color: color + "3F",
};
@ -549,10 +557,14 @@ export class StatisticsChart extends LitElement {
} else {
val.push((stat.sum || 0) - firstSum);
}
} else if (type === "max" && this.chartType === "line") {
const max = stat.max || 0;
val.push(Math.abs(max - (stat.min || 0)));
val.push(max);
} else if (
type === bandTop &&
this.chartType === "line" &&
drawBands
) {
const top = stat[bandTop] || 0;
val.push(Math.abs(top - (stat[bandBottom] || 0)));
val.push(top);
} else {
val.push(stat[type] ?? null);
}

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"
);

83
src/data/dhcp.ts Normal file
View File

@ -0,0 +1,83 @@
import {
createCollection,
type Connection,
type UnsubscribeFunc,
} from "home-assistant-js-websocket";
import type { Store } from "home-assistant-js-websocket/dist/store";
import type { DataTableRowData } from "../components/data-table/ha-data-table";
export interface DHCPDiscoveryData extends DataTableRowData {
mac_address: string;
hostname: string;
ip_address: string;
}
interface DHCPRemoveDiscoveryData {
mac_address: string;
}
interface DHCPSubscriptionMessage {
add?: DHCPDiscoveryData[];
change?: DHCPDiscoveryData[];
remove?: DHCPRemoveDiscoveryData[];
}
const subscribeDHCPDiscoveryUpdates = (
conn: Connection,
store: Store<DHCPDiscoveryData[]>
): Promise<UnsubscribeFunc> =>
conn.subscribeMessage<DHCPSubscriptionMessage>(
(event) => {
const data = [...(store.state || [])];
if (event.add) {
for (const deviceData of event.add) {
const index = data.findIndex(
(d) => d.mac_address === deviceData.mac_address
);
if (index === -1) {
data.push(deviceData);
} else {
data[index] = deviceData;
}
}
}
if (event.change) {
for (const deviceData of event.change) {
const index = data.findIndex(
(d) => d.mac_address === deviceData.mac_address
);
if (index !== -1) {
data[index] = deviceData;
}
}
}
if (event.remove) {
for (const deviceData of event.remove) {
const index = data.findIndex(
(d) => d.mac_address === deviceData.mac_address
);
if (index !== -1) {
data.splice(index, 1);
}
}
}
store.setState(data, true);
},
{
type: `dhcp/subscribe_discovery`,
}
);
export const subscribeDHCPDiscovery = (
conn: Connection,
callbackFunction: (dhcpDiscoveryData: DHCPDiscoveryData[]) => void
) =>
createCollection<DHCPDiscoveryData[]>(
"_dhcpDiscoveryRows",
() => Promise.resolve<DHCPDiscoveryData[]>([]), // empty array as initial state
subscribeDHCPDiscoveryUpdates,
conn,
callbackFunction
);

View File

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

View File

@ -6,6 +6,7 @@ import { debounce } from "../common/util/debounce";
export const integrationsWithPanel = {
bluetooth: "config/bluetooth",
dhcp: "config/dhcp",
matter: "config/matter",
mqtt: "config/mqtt",
thread: "config/thread",
@ -154,3 +155,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 {

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