Compare commits

..

566 Commits

Author SHA1 Message Date
Paul Bottein
134681b4c9 Merge branch 'dev' into toggle_group_dialog 2025-07-10 18:50:14 +02:00
Paul Bottein
082f1ca55e Center content on mobile 2025-07-10 18:49:05 +02:00
Norbert Rittel
3b7d2869e5 Fix sentence-casing of two "More Info" button labels (#26135)
Fix sentence-casing of two "More Info" buttons

- the one in the Dev tools opens the "More info" dialog for the entity, so it's changed to that dialog's name
- the one for Thread configuration opens href=${documentationUrl(this.hass, `/integrations/thread`)}
therefore it's changed to "More information"
2025-07-10 16:07:11 +00:00
renovate[bot]
bcda5cd0cf Update dependency core-js to v3.44.0 (#26134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-10 16:02:35 +00:00
renovate[bot]
eeb64a25ff Update dependency @rsdoctor/rspack-plugin to v1.1.8 (#26133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-10 16:01:56 +00:00
Petar Petrov
9134132ba9 Only show loading for slow flow steps to avoid flickering (#26131) 2025-07-10 17:59:07 +02:00
Paul Bottein
341e63e878 Fix device class icon off state 2025-07-10 17:36:03 +02:00
Paul Bottein
5ed2d2fd2f Fix last updated 2025-07-10 17:33:15 +02:00
Paul Bottein
c6f92d1375 Add translations 2025-07-10 17:26:36 +02:00
Paul Bottein
e8201f7848 Use variable 2025-07-10 15:59:53 +02:00
Paul Bottein
6d7df18e82 Fix available entities and header 2025-07-10 15:58:09 +02:00
Paul Bottein
1471cfea66 Don't use new colors for now 2025-07-10 15:33:29 +02:00
Paul Bottein
9e4835107d Merge dialog with more info 2025-07-10 14:43:46 +02:00
karwosts
1ded254e5a Fix some weather-forecast card editor issues (#26125) 2025-07-10 11:27:37 +03:00
Christoph
fc104a7992 add floor column to datatable in config devices page (#26103)
* add floor column to datatable in config devices page

* refactor conditions related to floor column in config devices page
2025-07-10 11:25:56 +03:00
Paul Bottein
3269fd3c5b Feedbacks 2025-07-09 18:14:29 +02:00
Paul Bottein
17e63343c7 Handle multiple entities 2025-07-09 16:52:58 +02:00
karwosts
e7e062a222 Pause map autofit when user initiates pan/zoom (#26114)
* Pause map autofit when user initiates pan/zoom

* not a state

* a different approach
2025-07-09 17:32:20 +03:00
Franck Nijhof
5233086efb Add Task issue form (#26121) 2025-07-09 14:14:37 +02:00
Christoph
8d95f0d95d add unit tests for common/url/search-params.ts (#26115) 2025-07-09 14:11:28 +03:00
karwosts
5cf8b39703 Coerce all energy distribution values to the same unit (#26117) 2025-07-09 14:06:47 +03:00
Franck Nijhof
15dabe372c Adjust feature request links in issue reporting (#26123) 2025-07-09 12:40:37 +02:00
renovate[bot]
aab52a8bb2 Update dependency vis-data to v7.1.10 (#26122)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-09 10:27:49 +00:00
Paul Bottein
dc7ba0dac6 Fix dialog at the top 2025-07-09 10:02:12 +02:00
Paul Bottein
2ab4608884 Delete dashboard dialog 2025-07-09 09:49:44 +02:00
Paul Bottein
de7f5c1bb7 Add toggle group dialog 2025-07-08 19:18:13 +02:00
Paul Bottein
7144b7802e Invert order 2025-07-08 15:06:59 +02:00
Norbert Rittel
aa52825b40 Capitalize "REST", remove excessive commas (#26109) 2025-07-08 12:57:30 +02:00
Christoph
2809a306e6 do not set "___ADD_NEW___" value in ha-floor-picker (#26102) 2025-07-08 12:40:24 +02:00
Paul Bottein
ca315b88ce Add sections dialog 2025-07-08 12:35:51 +02:00
renovate[bot]
a6304d6284 Update rspack monorepo to v1.4.4 (#26105)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 08:50:51 +03:00
Paul Bottein
8e866e86d6 Use query params instead of path for media browser navigate ids (#26099) 2025-07-08 08:50:28 +03:00
Kevin Lakotko
2e8203f666 Sort groups if same as sort column (#26010)
* fix(grouping): if sorted by column sort group

* chore: use props to group for memoization
2025-07-07 19:23:27 +03:00
Paulus Schoutsen
b60f2e3201 Add extra margin AI Task pref (#26096)
Add extra margin AI Task
2025-07-07 12:11:35 +03:00
renovate[bot]
c5f57f436c Update rspack monorepo to v1.4.3 (#26093)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 10:17:53 +03:00
steinmn
3bb930b906 Fix flickering Edit sidebar dialog by locking content padding (#26084)
Fix flickering Edit sidebar dialog
2025-07-07 05:29:25 +00:00
Ezra Freedman
e75331e159 Weather card smallest width is not set correctly (#26082)
set result.width, not result.height
2025-07-06 10:12:59 +02:00
renovate[bot]
d6b66a7145 Update dependency @rsdoctor/rspack-plugin to v1.1.7 (#26087)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-06 08:07:11 +00:00
Yosi Levy
5c346798c8 RTL fixes for 7-25 (#26074) 2025-07-06 10:04:09 +02:00
renovate[bot]
5ffe37407a Update dependency hls.js to v1.6.6 (#26085)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-06 09:58:49 +02:00
renovate[bot]
2b056c0434 Update dependency @lokalise/node-api to v14.9.1 (#26081)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-06 09:58:06 +02:00
Paulus Schoutsen
27b36707e5 Automation save dialog to suggest name, description and labels (#26071)
* AI Task structure

* Suggest description and labels too
2025-07-06 09:57:16 +02:00
renovate[bot]
5760614b65 Update babel monorepo to v7.28.0 (#26079)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-05 12:17:24 +02:00
renovate[bot]
3835912b01 Update dependency @rsdoctor/rspack-plugin to v1.1.6 (#26078)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-05 12:14:01 +02:00
Petar Petrov
a385655c85 Remove deprecated dependency @types/glob (#26075) 2025-07-05 08:27:41 +02:00
karwosts
e177012108 Fix default range icon (#26069) 2025-07-04 23:34:38 +02:00
renovate[bot]
cc3234ad8f Update dependency eslint to v9.30.1 (#26072)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 22:47:44 +02:00
karwosts
4d932f0b4a Support translating number selector UoM (#26070) 2025-07-04 21:06:49 +03:00
renovate[bot]
257769cdc7 Update dependency globals to v16.3.0 (#26068)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 18:42:09 +02:00
renovate[bot]
6619f064eb Update dependency @lokalise/node-api to v14.9.0 (#26067)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 15:12:03 +02:00
renovate[bot]
382a47a082 Update rspack monorepo to v1.4.2 (#26066)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 15:11:40 +02:00
renovate[bot]
ad4be75fe1 Update dependency typescript-eslint to v8.35.1 (#26058)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-04 10:16:33 +03:00
Ezra Freedman
d605b67b41 Prevent uncaught TypeError on HuiWeatherForecastCard render (#26038) 2025-07-03 21:19:48 +02:00
renovate[bot]
dba6a3c756 Update fullcalendar monorepo to v6.1.18 (#26047)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 17:14:49 +03:00
c0ffeeca7
002e9ad071 Terminology: change controller to adapter (#26051)
* Terminology: change controller to adapter

* Update src/translations/en.json

Co-authored-by: AlCalzone <d.griesel@gmx.net>

* Apply suggestions from code review

---------

Co-authored-by: AlCalzone <d.griesel@gmx.net>
2025-07-03 15:51:35 +02:00
Paul Bottein
6e7874c2c9 Fix play media action (#26035) 2025-07-02 19:30:06 +02:00
Paul Bottein
978f9b0f83 Reduce media selector size (#26033) 2025-07-02 18:08:31 +02:00
renovate[bot]
2b88669a72 Update dependency eslint-plugin-lit-a11y to v5.1.0 (#26020)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-02 17:20:13 +02:00
renovate[bot]
252fd2bb6c Update dependency @bundle-stats/plugin-webpack-filter to v4.21.0 (#26032)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-02 17:19:41 +02:00
Paul Bottein
cc68a087a2 Fix zoom in statistic chart (#26034) 2025-07-02 17:18:39 +02:00
karwosts
3e1341a731 Fix glitchy 'show' checkboxes on integration page (#26021) 2025-07-02 13:43:06 +02:00
Bram Kragten
6be25270fd Dont fetch device actions on first updated (#26028) 2025-07-02 13:42:21 +02:00
Bram Kragten
ce929aea46 Disable fullscreen in trigger detail dialog (#26030) 2025-07-02 13:41:52 +02:00
Paul Bottein
8853bf6ea2 Improve styling of the code editor in fullscreen mode (#26029) 2025-07-02 13:41:26 +02:00
Paul Bottein
2241807745 Fix UI jump when using drag and drop in areas strategy editor (#26026) 2025-07-02 09:03:21 +00:00
Paul Bottein
50d705c943 Add missing domain icon import in area controls (#26023) 2025-07-01 21:47:20 +02:00
Paul Bottein
eb111d3c32 Add missing area helper (#26022) 2025-07-01 21:46:58 +02:00
Paul Bottein
1e59f9f4be Increase target area in tile card and area card (#26017) 2025-07-01 14:34:19 +02:00
Paul Bottein
523eb9522f Add dashboard title to strategy editor (#26015) 2025-07-01 14:33:36 +02:00
Paul Bottein
f6cb322819 Avoid selector to take to much space in action calls (#26014) 2025-07-01 14:32:54 +02:00
Paul Bottein
4f97756f4e Force narrow style for action, condition and trigger in blueprint (#26018) 2025-07-01 15:22:55 +03:00
Ezra Freedman
8644dd5271 Fix translation in the integration page for entities (#26009)
add call to localize
2025-07-01 06:48:13 +00:00
renovate[bot]
26d842f432 Update dependency @babel/helper-define-polyfill-provider to v0.6.5 (#26008)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 08:34:48 +02:00
renovate[bot]
ad4f14ffaf Update dependency eslint to v9.30.0 (#26012)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-01 08:31:30 +02:00
Paul Bottein
948c858e78 Fix object selector not displayed (#26007) 2025-06-30 16:15:37 +00:00
Paul Bottein
49099223d3 Do not display quality scale for custom integrations (#26006) 2025-06-30 16:10:08 +00:00
Paul Bottein
0fbd430594 Allow to re-order floors in areas dashboard (#26002)
* Allow to re-order floors in areas dashboard

* Move drag handle to right

* Improve typings

* Only show drag handle if there is at least 2 floors
2025-06-30 16:09:42 +00:00
Kevin Lakotko
8cc762d839 Fix use of numeric option for collator (#25917)
* fix(string): use numeric option for collator

* test: add natural sort comparison tests
2025-06-30 18:00:45 +02:00
Paul Bottein
89d9dd2893 Improve device row in integration page (#26005)
Improve device row in config entry page
2025-06-30 17:44:50 +02:00
renovate[bot]
b7d1ce1c37 Update rspack monorepo to v1.4.1 (#26001)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 17:22:39 +02:00
renovate[bot]
869d10ca3f Update CodeMirror (#26003)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 17:22:09 +02:00
Simon Lamon
f338089148 Pass area control service calls through hass (#25986)
Connection logging
2025-06-30 14:59:15 +02:00
renovate[bot]
06b0f9fcaf Update dependency @rsdoctor/rspack-plugin to v1.1.5 (#26000)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 13:49:05 +02:00
Simon Lamon
7ad07e4c55 Fix fullscreen yaml editor (transparency background) (#25989)
Fix fullscreen editor (transparency background)
2025-06-30 14:16:59 +03:00
renovate[bot]
ad65600d11 Update dependency marked to v16 (#25997)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 14:15:16 +03:00
renovate[bot]
e91d907e56 Update dependency prettier to v3.6.2 (#25996) 2025-06-30 08:26:28 +02:00
renovate[bot]
b35a1fc9e0 Update dependency @babel/core to v7.27.7 (#25992)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-29 20:54:40 +03:00
renovate[bot]
dd18ad96f3 Update dependency gulp-rename to v2.1.0 (#25985)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-29 17:16:40 +02:00
Norbert Rittel
62eec56e5f Fix grammar of Light, Sensor and Tile card descriptions (#25988)
* Fix grammar of Light, Sensor and Entity card descriptions

* Capitalize "Tile card" as a name

* Apply same change to Entity badge description
2025-06-29 17:16:09 +02:00
renovate[bot]
7187e25cad Update rspack monorepo to v1.4.0 (#25987)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-29 11:06:34 +02:00
Norbert Rittel
6d9e6a616d Fix sentence-casing, spelling and grammar issues (#25981)
* Fix sentence-casing, spelling and grammar issues

* Add "IP information" to the list

* More sentence-casing issues
2025-06-29 09:25:29 +02:00
renovate[bot]
44d87e3c66 Update dependency barcode-detector to v3.0.5 (#25980)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-28 17:41:15 +02:00
renovate[bot]
085e2460bc Update dependency prettier to v3.6.1 (#25978)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-28 17:40:47 +02:00
karwosts
1f8a9e4caf Improve settings page accessibility (No. 2) (#25965) 2025-06-27 19:38:11 +02:00
Franck Nijhof
f08877437e Add initial instructions file for GitHub Copilot and Claude Code (#25967) 2025-06-27 18:06:23 +02:00
renovate[bot]
6690d1ef22 Update dependency @types/leaflet to v1.9.19 (#25974)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-27 17:54:52 +02:00
Paul Bottein
9d8a5b366e Use entity format state if only one entity for that domain in the area card (#25964)
Use entity format state if only one entity is area card
2025-06-27 17:41:58 +02:00
Franck Nijhof
22c798c9d6 Add Claude to gitignore (#25966) 2025-06-27 15:59:14 +02:00
Norbert Rittel
8aabb1f32f Dev Tools: Remove excessive space from "Input date times" (#25973)
Remove excessive space from "input date times"
2025-06-27 15:57:52 +02:00
renovate[bot]
33d5cecc85 Update dependency ua-parser-js to v2.0.4 (#25968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-27 15:12:14 +03:00
Paul Bottein
7693a4dc24 Use areas dashboard name in the top bar (#25969) 2025-06-27 10:40:15 +02:00
Paul Bottein
9ec38c7dd9 Bump vaadin to 24.7.9 (#25963) 2025-06-26 21:22:06 +00:00
Bram Kragten
e8cb85f7ff Disable fullscreen editor for editors that are already fullscreen (#25959)
* Disabled fullscreen editor for editors that are already fullscreen

* Update ha-code-editor.ts
2025-06-26 23:17:30 +02:00
renovate[bot]
ef964a2717 Update dependency typescript-eslint to v8.35.0 (#25956) 2025-06-26 19:32:39 +02:00
Paul Bottein
369881f8a6 Fix expand icon for entries and sub entries (#25955) 2025-06-26 19:21:15 +02:00
Bram Kragten
68e22d23f1 Fix filtering on device in entities config panel (#25948)
* Fix filtering on device in entities config panel

* fix

* set filters from url twice to catch race...
2025-06-26 16:42:11 +02:00
Paul Bottein
696ba69a9e Revert vaadin to 24.7.7 (#25953) 2025-06-26 14:41:58 +00:00
Paul Bottein
e2ab52e10e Don't limit combo-box dropdown size (#25952) 2025-06-26 14:12:07 +00:00
Bram Kragten
b154bc1502 Load title when fetching flow (#25951) 2025-06-26 14:07:46 +00:00
Paul Bottein
a952b880d8 Disable escape key to close edit card dialog (#25947) 2025-06-26 15:25:26 +02:00
Bram Kragten
018aceb542 Add label to version number (#25942)
Add label
2025-06-26 15:38:33 +03:00
Bram Kragten
2fb86f118e make sure header is always shown in data entry flow (#25941) 2025-06-26 11:07:08 +00:00
Bram Kragten
6c8caccfec Use different icon for services (#25939) 2025-06-26 13:05:28 +02:00
Paul Bottein
3dd3a80054 Remove alert classes and only use slot sensors for areas dashboard (#25937)
* Remove alert classes and only used slot sensors for areas dashboard

* Rename group to sensors

* Rename group to sensors
2025-06-26 13:05:05 +02:00
Bram Kragten
675310afdf add version number to integration page (#25940)
* add version number to integration page

* Update ha-config-integration-page.ts
2025-06-26 11:04:13 +00:00
Paul Bottein
f5df91d4c7 Better handle case when no floors in areas dashboard (#25933) 2025-06-26 12:18:34 +02:00
Bram Kragten
d8ab9b73ba Prevent overflow of ripple on device row on integration page (#25922) 2025-06-26 12:17:00 +02:00
renovate[bot]
89ab0b4a3d Update dependency prettier to v3.6.0 (#25930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-26 10:18:03 +02:00
Eric Stern
dd4cb1df72 Fix logbook stream subscription (#25927) 2025-06-26 09:42:43 +02:00
Paulus Schoutsen
b81cd37776 Make the config entry row section wider on mobile (#25924) 2025-06-26 09:41:39 +02:00
Paulus Schoutsen
34112e7446 Fix wrapping of add subentry buttons (#25925) 2025-06-26 09:41:10 +02:00
Bram Kragten
2dee45b465 Update confirm disable messages (#25919) 2025-06-26 08:57:16 +03:00
Norbert Rittel
10eb0a8b87 Deduplicate weekdays in time conditions (#25915)
Replace weekdays conditions with references to common ui strings
2025-06-26 08:55:46 +03:00
Bram Kragten
551035238f Dont show internal quality scale (#25921)
dont show internal quality scale
2025-06-25 17:34:12 -04:00
Bram Kragten
eb9359e9e1 Only show own devices when there are devices... (#25920)
only show own devices when there are devices...
2025-06-25 17:31:43 -04:00
Bram Kragten
df2523a6a2 Merge branch 'rc' into dev 2025-06-25 17:25:23 +02:00
Bram Kragten
edb1e1bba7 Bumped version to 20250625.0 2025-06-25 17:24:02 +02:00
Bram Kragten
e5bc234ab3 make debug mode better visible, improve disabling device (#25910) 2025-06-25 17:23:22 +02:00
ildar170975
6006e926a7 fix hui-panel-view for a "warning_multiple_cards" (#25899) 2025-06-25 17:20:14 +02:00
Petar Petrov
93df473ad2 Translate select options in config flows (#25911) 2025-06-25 17:18:29 +02:00
Paul Bottein
af149dcfab Move exclude entities config to area card (#25909) 2025-06-25 16:18:47 +02:00
karwosts
3ab6a02994 Improve settings page accessibility (#25885) 2025-06-25 13:35:14 +00:00
Leon
e7a04eb3d2 Short-format negative and small numbers in energy-distribution-card (#25862) 2025-06-25 13:22:21 +00:00
karwosts
2dfe5f50a6 Support templates in action target (#25656) 2025-06-25 15:20:53 +02:00
Paul Bottein
174d54396f Add cover controls to area card and improve areas dashboard (#25892) 2025-06-25 13:14:41 +00:00
karwosts
5c1a8029bf Fix entity selector slicing value on load (#25854) 2025-06-25 15:11:23 +02:00
Franck Nijhof
884341656f Add full-screen button to code editors (#25903) 2025-06-25 13:07:31 +00:00
Bram Kragten
fcbc8de95a Update device row in integration page (#25907)
* Update ha-config-entry-device-row.ts

* Update src/panels/config/integrations/ha-config-entry-device-row.ts

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-06-25 12:16:11 +00:00
Paul Bottein
f90eb4fee0 Exclude entities in controls for areas dashboard (#25906) 2025-06-25 13:50:21 +03:00
Paulus Schoutsen
9afc4260c9 Move AI task prefs to system -> general (#25904)
Move AI task prefs
2025-06-25 08:27:09 +03:00
Franck Nijhof
3aafa47f6d Improve Entity ID auto-complete in YAML mode (#25901) 2025-06-24 16:22:31 -04:00
Paulus Schoutsen
35ba2fffda Also show sub entry services when sub entry expanded (#25900) 2025-06-24 19:46:47 +02:00
Paul Bottein
31bc708725 Improve alerts padding in area card (#25897) 2025-06-24 16:09:56 +00:00
Franck Nijhof
caa60e4e8c Fix warnings raised when migrating incomplete automation configuration (#25898) 2025-06-24 18:02:45 +02:00
Paul Bottein
edcca81acc Group area per floor in the areas strategy editor (#25895) 2025-06-24 15:28:46 +02:00
Stefan Agner
508e451f94 Add container arch to system health data (#25896) 2025-06-24 15:27:52 +02:00
Bram Kragten
bfcc36bb40 Update integration page layout (#25880) 2025-06-24 15:27:07 +02:00
Petar Petrov
976bf7c512 Update buttons in Z-Wave firmware update dialog (#25894)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-06-24 15:20:30 +02:00
Petar Petrov
fb28c8971b Update Z-Wave text (#25893) 2025-06-24 14:31:26 +02:00
Paul Bottein
641a2eb77c Use area card in area strategy (#25879)
* Bring area card to area strategy

* Add device classes

* Use subview
2025-06-24 14:46:12 +03:00
Bram Kragten
47b90feffa Bumped version to 20250531.4 2025-06-24 09:58:11 +02:00
Paul Bottein
45e66d8b6e Don't send double card updated event when rendering the card (#25883) 2025-06-24 09:56:53 +02:00
Petar Petrov
73cd1e8e9d Fix duplicated requests in statistics-graph (#25878) 2025-06-24 09:56:52 +02:00
Petar Petrov
1a8ff83e2d Another fix for history chart axis rounding (#25852) 2025-06-24 09:56:51 +02:00
Petar Petrov
e6fbe0d538 Round chart limits with fit_y_data (#25851) 2025-06-24 09:56:51 +02:00
Paul Bottein
5ac6781f7d Remove debug type in secondary line in statistic picker (#25835) 2025-06-24 09:56:50 +02:00
Petar Petrov
7f7e693547 Fix bar chart data order when using the legend (#25832)
* Fix bar chart data order when using the legend

* type fix
2025-06-24 09:56:49 +02:00
Paul Bottein
51246f119c Fix disabled color in dark mode in production (#25818) 2025-06-24 09:56:48 +02:00
Anthony Relle
d764187e8c Update ElectricityMaps URL in Energy Dashboard (#25816)
fix: update electricitymap domain
2025-06-24 09:56:47 +02:00
Petar Petrov
6738b7d708 Fix sankey total calculation to account for included_in_stat (#25805) 2025-06-24 09:56:47 +02:00
Petar Petrov
77c458a0e5 Reduce reset-zoom button size on timeline charts (#25796) 2025-06-24 09:56:46 +02:00
ildar170975
876e36b4e0 hui-graph-footer-editor: add margin to ha-switch to prevent a scrollbar (#25645)
* add padding-bottom to card-config to prevent a scrollbar

* revert padding for card-config

* add margin to ha-switch

* revert margin for ha-switch

* set margin for ha-switch
2025-06-24 08:27:16 +03:00
Paul Bottein
dd64fa228c Add color options to area card (#25881)
* Add color options to area card

* Color all controls with the same color

* Clean area card
2025-06-24 08:23:58 +03:00
renovate[bot]
b3f5eb256f Update dependency eslint-plugin-import to v2.32.0 (#25890)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 06:25:30 +02:00
ildar170975
a5005c0840 Device page: sort related automations, scenes, scripts (#24742)
* sort related items

* use memoize for sorted _related

* fix for "_entities" & "_getEntitiesSorted"
2025-06-24 06:25:09 +02:00
Paul Bottein
c73122d7ee Don't send double card updated event when rendering the card (#25883) 2025-06-23 14:10:27 +02:00
Petar Petrov
82da36825e Fix duplicated requests in statistics-graph (#25878) 2025-06-23 11:25:46 +02:00
Petar Petrov
3e2a5bff4d Zwave delete device button on device page (#25766)
* Handle ZWaveJS device delete in frontend according to status

* finishing touches

* Fix custom value selected when clicking item in combo box (#25734)

* Assist Chat: handle intent progress delta not always being there (#25730)

handle intent progress data type change

* More support for no-grid energy dashboard (#25644)

* More support for no-grid energy dashboard

* Update src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts

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

* lint

---------

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

* Z-Wave: apply sentence-style capitalization (#25739)

* Display full error for card preview mode (#25747)

* Fix edit card not working in chrome after editing (#25751)

* Change backup type order (#25759)

* Fix alerts refresh on device page (#25748)

* Fix alerts refresh on device page

* don't reset actions periodically

* reset stuff only on deviceId change

* Ensure grid options always return an object (#25760)

Assist Chat: handle intent progress delta not always being there (#25730)

handle intent progress data type change

More support for no-grid energy dashboard (#25644)

* More support for no-grid energy dashboard

* Update src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts

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

* lint

---------

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

Z-Wave: apply sentence-style capitalization (#25739)

Display full error for card preview mode (#25747)

Fix edit card not working in chrome after editing (#25751)

Change backup type order (#25759)

Fix alerts refresh on device page (#25748)

* Fix alerts refresh on device page

* don't reset actions periodically

* reset stuff only on deviceId change

Ensure grid options always return an object (#25760)

* fix merge issue

* lint

* Update text and buttons

* Update src/translations/en.json

Co-authored-by: Norbert Rittel <norbert@rittel.de>

* update text

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: karwosts <32912880+karwosts@users.noreply.github.com>
Co-authored-by: c0ffeeca7 <38767475+c0ffeeca7@users.noreply.github.com>
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-06-23 11:07:19 +03:00
Paulus Schoutsen
1349d9d8e3 Hide AI Task from default dashboard (#25877) 2025-06-23 09:40:16 +02:00
Petar Petrov
f6f2cb0fce Round chart limits with fit_y_data (#25851) 2025-06-23 08:42:54 +03:00
Paulus Schoutsen
589fa75b17 Add support for accept keyword in media selector (#25808) 2025-06-22 14:02:39 -04:00
renovate[bot]
fdd6ccf379 Update dependency @rsdoctor/rspack-plugin to v1.1.4 (#25868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-22 18:48:32 +03:00
Petar Petrov
f50d5d79a4 Another fix for history chart axis rounding (#25852) 2025-06-22 12:11:29 +02:00
iluvdata
ad589b32c9 allow previews in config_subentries_flow (#25859) 2025-06-21 08:09:13 +00:00
renovate[bot]
1990472970 Lock file maintenance (#25838)
* Lock file maintenance
2025-06-20 20:43:30 +00:00
Norbert Rittel
299713fd5e Fix inconsistently spelled occurrences of "add-on" (#25858)
* Fix inconsistently spelled occurrences of "add-on"

- add the missing hyphen on three occurrences of "addon"
- change one occurrence of "Add-on" to lowercase

* Also capitalize one inconsistent occurrence of "Ingress"
2025-06-20 22:09:09 +02:00
renovate[bot]
a13dae4745 Update vitest monorepo to v3.2.4 (#25857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-20 18:30:39 +00:00
Paulus Schoutsen
8324c23618 Update people dashboard intro (#25856) 2025-06-20 20:30:25 +02:00
Norbert Rittel
9c8d6a939b Fix spelling of "to log in", "to set up" and more (#25855)
- add missing space to "log in" where it's the verb
- add missing space to "set up" where it's the verb
- fix sentence-casing of "Create area"
- fix spelling of "ID" as abbreviation
2025-06-20 20:20:00 +02:00
Paulus Schoutsen
1059b519af Add LLM Task to suggest automation name (#25778)
* Add AI Task to suggest automation name

* Use state and update styling
2025-06-20 12:07:47 -04:00
Paulus Schoutsen
52a02093e3 Allow changing LLM Task preferences (#25779)
* Allow changing LLM Task preferences

* value-changed
2025-06-20 12:07:29 -04:00
Paul Bottein
b608bd949b Add fields and multiple support to object selector (#25843)
* Add schema and multiple for object selector

* Add selector to gallery

* Fix description

* Improve formatting

* Add ellipsis

* Update fields instead of schema

* Update gallery

* Update format

* Fix format value

* Fix dialog size
2025-06-20 15:48:59 +02:00
Paul Bottein
f87e20cae9 Redesign area card (#25802)
* Use entity filter to get device classes in editor

* Add name and sensor states to area card

* Fix area type

* Add basic controls

* Fix editor

* Add image

* Add image type

* Add translation key for area controls

* Improve editor

* Fix unknown entity id in area

* Fix default feature position

* Add alert badge

* Add helper

* Display all alerts when using big card

* Disable covers and re-enable switches

* Filter compatible controls

* Use state icon for alerts

* Rename to display type

* Delete deprecated show camera

* Fix aspect ratio

* Improve helper

* Undo domain icon changes

* Undo domain icon changes

* Update types

* Fix translation cases

* Fix card size

* Feedback

* Don't fallback to compact

* Use plural form

* Refactor active color
2025-06-20 15:33:26 +02:00
renovate[bot]
d58186fec9 Update dependency typescript-eslint to v8.34.1 (#25848)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-20 15:01:44 +03:00
Bastian
f47336392c Fix/dhcp config network sort (#25799)
* Add ip sort method to compare helper

* Add ip sort functionality to dhcp config panel datatable

* Add type ip to DataTableColumnData

* Change ip sorting to padStart method for better readablity

* Rename ip compare method to clarify ipv4

* Enhance IP compare method to include ipv6

* Add compare IP test
2025-06-20 15:01:02 +03:00
karwosts
e9272b9a27 Remove gray colors from chart color set (#25844) 2025-06-20 13:50:12 +02:00
Norbert Rittel
b20d489bdd Fix sentence-casing of Z-Wave strings (#25846)
* Fix sentence-casing of Z-Wave strings

* Also fix casing of "Z-Wave JS Device Database"
2025-06-19 21:29:07 +02:00
Petar Petrov
f7634c45c2 Fix bar chart data order when using the legend (#25832)
* Fix bar chart data order when using the legend

* type fix
2025-06-19 17:49:58 +02:00
renovate[bot]
8ee80586a8 Update vaadinWebComponents monorepo to v24.8.0 (#25842)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-19 16:12:02 +02:00
Bram Kragten
6aa8a24aad Fix for my links ending with a slash (#25841) 2025-06-19 15:51:11 +03:00
Norbert Rittel
65e78de41c Improve explanation of 'Ping a Matter device' dialog (#25839)
* Fix explanation of 'Ping a Matter device' dialog

- change to more descriptive wording using third-person singular
- replace first "on" with "of" to clarify (currently "on" contradicts "server-side")
- remove the wrong hyphen in "IP-addresses"
2025-06-19 11:00:22 +00:00
Norbert Rittel
573c1db081 Add hyphen to all compounds with "-powered" (#25840) 2025-06-19 12:49:07 +02:00
Paul Bottein
cf03e041a8 Fix combobox helper (#25834)
* Fix combobox helper

* Pass disabled to all helpers
2025-06-18 17:57:11 +02:00
Paul Bottein
b4dbfa6f70 Remove debug type in secondary line in statistic picker (#25835) 2025-06-18 15:44:27 +00:00
Bastian
723bb4dfeb Change duration input to ha-time-input component (#25800)
* Change duration input to ha-time-input component

* Change ha-time-input to duration-input component

* Add DurationDict to ForDict cast
2025-06-18 15:31:15 +00:00
renovate[bot]
98dcce6af1 Update dependency lint-staged to v16.1.2 (#25833)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-18 17:24:40 +02:00
karwosts
e3cdd69835 Display yaml errors in all ha-yaml-editor (#25819) 2025-06-18 09:32:06 +03:00
karwosts
0ddf3aa675 Uncap width of ha-statistics-picker (#25822) 2025-06-17 21:12:50 +02:00
renovate[bot]
9c9ce78ff9 Update dependency lint-staged to v16.1.1 (#25817)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-17 20:05:09 +02:00
Paulus Schoutsen
4f8c50aaa9 Show voice ID in Cloud Pref (#25809)
* Show voice ID in Cloud Pref

* Address style comments

* Update src/panels/config/cloud/account/cloud-tts-pref.ts

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-06-17 13:32:27 +00:00
Petar Petrov
3cfc6297b5 Fix sankey total calculation to account for included_in_stat (#25805) 2025-06-17 15:14:33 +02:00
Paul Bottein
cc0586bf36 Fix disabled color in dark mode in production (#25818) 2025-06-17 15:14:07 +02:00
Petar Petrov
bdf48140e4 Reduce reset-zoom button size on timeline charts (#25796) 2025-06-17 15:07:57 +02:00
Anthony Relle
d7f4a7acb0 Update ElectricityMaps URL in Energy Dashboard (#25816)
fix: update electricitymap domain
2025-06-17 09:54:34 +00:00
renovate[bot]
154e85eb45 Update dependency eslint-plugin-lit-a11y to v5.0.1 (#25812)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-17 10:16:13 +03:00
renovate[bot]
ea15e5a44e Update dependency eslint-plugin-lit-a11y to v5 (#25718)
* Update dependency eslint-plugin-lit-a11y to v5

* Update import

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2025-06-17 09:06:44 +03:00
renovate[bot]
d581c4b0aa Update dependency eslint to v9.29.0 (#25810)
* Update dependency eslint to v9.29.0

* fix warning

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-06-17 05:55:13 +00:00
karwosts
5b7655cf72 Fix automation drag&drop loses item (#25811) 2025-06-17 08:42:20 +03:00
dependabot[bot]
634e1dbde8 Bump softprops/action-gh-release from 2.2.2 to 2.3.2 (#25795)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.2.2 to 2.3.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.2...v2.3.2)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: 2.3.2
  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-06-16 11:03:16 +03:00
renovate[bot]
a255463a76 Update dependency sinon to v21 (#25794)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 08:53:23 +03:00
renovate[bot]
3c40b3112e Update dependency @codemirror/view to v6.37.2 (#25791)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 08:24:44 +03:00
renovate[bot]
9f33ec55f5 Update dependency glob to v11.0.3 (#25792)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 08:24:01 +03:00
Simon Lamon
c35c4d16f0 Remove Google Assistant link (#25780) 2025-06-15 17:17:38 +03:00
Simon Lamon
159c4d100a Add perform action with to translations (#25781) 2025-06-15 17:15:08 +03:00
quinnter
05035a281b Improved new dashboard dialog (#25676)
* tidied cards and more translations

* fix container heading padding

* updated filter to use fusejs, changes to how list is localized
2025-06-14 10:55:14 +02:00
renovate[bot]
67c7a3931f Update dependency typescript-eslint to v8.34.0 (#25770)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-13 10:18:48 +03:00
Paulus Schoutsen
cf41a43070 Hide backup during onboarding (#25768) 2025-06-12 13:48:59 -04:00
renovate[bot]
5c385d5368 Update vitest monorepo to v3.2.3 (#25765)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 16:25:22 +03:00
renovate[bot]
0ae78300c9 Update dependency core-js to v3.43.0 (#25761)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-12 16:24:57 +03:00
Paul Bottein
de42714505 20250531.3 (#25764) 2025-06-12 14:23:03 +02:00
Paul Bottein
359460b570 Bumped version to 20250531.3 2025-06-12 14:22:11 +02:00
Paul Bottein
38545a01dd Ensure grid options always return an object (#25760) 2025-06-12 14:21:51 +02:00
Paul Bottein
8cead75087 Change backup type order (#25759) 2025-06-12 14:21:50 +02:00
Petar Petrov
7f68447a4f Fix alerts refresh on device page (#25748)
* Fix alerts refresh on device page

* don't reset actions periodically

* reset stuff only on deviceId change
2025-06-12 14:21:49 +02:00
Paul Bottein
01d2ef13c6 Ensure grid options always return an object (#25760) 2025-06-12 10:44:20 +03:00
Petar Petrov
af6911e848 Fix alerts refresh on device page (#25748)
* Fix alerts refresh on device page

* don't reset actions periodically

* reset stuff only on deviceId change
2025-06-12 09:40:36 +02:00
Paul Bottein
21af10fd28 Change backup type order (#25759) 2025-06-12 07:29:58 +00:00
Paul Bottein
380f760f79 20250531.2 (#25752) 2025-06-11 16:27:54 +02:00
Paul Bottein
bffe38c827 Bumped version to 20250531.2 2025-06-11 16:27:06 +02:00
Paul Bottein
89d0746c7c Fix edit card not working in chrome after editing (#25751) 2025-06-11 16:25:20 +02:00
Paul Bottein
6d30d15638 Fix edit card not working in chrome after editing (#25751) 2025-06-11 14:15:50 +00:00
Paul Bottein
7e2059e836 20250531.1 (#25749) 2025-06-11 14:42:10 +02:00
Paul Bottein
0d97962578 Bumped version to 20250531.1 2025-06-11 14:30:59 +02:00
Paul Bottein
4c5015e178 Display full error for card preview mode (#25747) 2025-06-11 14:29:55 +02:00
Paul Bottein
28214aebc5 Reduce keypad gap and margin in alarm panel card (#25735) 2025-06-11 14:29:54 +02:00
Paul Bottein
a26ca7d065 Fix custom value selected when clicking item in combo box (#25734) 2025-06-11 14:29:54 +02:00
Paul Bottein
2ab20aef69 Allow to open more info using query params (#25733) 2025-06-11 14:29:53 +02:00
Petar Petrov
8149ee60cb Fix period boundaries in Energy dashboard (#25728) 2025-06-11 14:29:52 +02:00
Petar Petrov
89ce6870f6 Handle tiny values in a log chart (#25727) 2025-06-11 14:29:51 +02:00
Mathieu
4307e2350f Adjust tooltip positioning in ha-sidebar for not first lis… (#25696)
fix(tooltip): fix tooltip positioning in ha-sidebar for not first listbox
2025-06-11 14:29:50 +02:00
karwosts
7dc4d555ae Hoist integration card tooltips (#25679) 2025-06-11 14:29:50 +02:00
Bram Kragten
1483e8e38f Set answers to yes and no for cloud pipeline confirm (#25674) 2025-06-11 14:29:49 +02:00
Simon Lamon
5daec6bbc5 Calendar add event button gap alignment (#25662)
Calendar gap alignment
2025-06-11 14:29:48 +02:00
Paul Bottein
d542b52ebd Display full error for card preview mode (#25747) 2025-06-11 13:04:54 +03:00
c0ffeeca7
43cc49bb32 Z-Wave: apply sentence-style capitalization (#25739) 2025-06-11 09:13:32 +03:00
karwosts
b3f0a6328e More support for no-grid energy dashboard (#25644)
* More support for no-grid energy dashboard

* Update src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts

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

* lint

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-06-11 09:11:46 +03:00
Paulus Schoutsen
9d6a7e7e6f Assist Chat: handle intent progress delta not always being there (#25730)
handle intent progress data type change
2025-06-10 10:59:10 -04:00
Paul Bottein
78d7da21aa Fix custom value selected when clicking item in combo box (#25734) 2025-06-10 16:46:09 +02:00
Paul Bottein
0474a24df6 Allow to open more info using query params (#25733) 2025-06-10 13:20:10 +03:00
Paul Bottein
6e7ac6fdf7 Reduce keypad gap and margin in alarm panel card (#25735) 2025-06-10 13:16:06 +03:00
Petar Petrov
7b9683df89 Fix period boundaries in Energy dashboard (#25728) 2025-06-10 10:28:51 +02:00
renovate[bot]
8523ddfd29 Update vaadinWebComponents monorepo to v24.7.8 (#25729)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 15:50:36 +03:00
Petar Petrov
2589e1a49f Handle tiny values in a log chart (#25727) 2025-06-09 14:45:23 +03:00
Paul Schreiber
5ce5f9a189 fix spelling of JavaScript in bug report template (#25726)
Correctly capitalize JavaScript in the bug report template.
2025-06-09 06:08:58 +00:00
renovate[bot]
6dd7217a20 Update dependency @babel/runtime to v7.27.6 (#25722)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 09:01:08 +03:00
renovate[bot]
0d02d0d334 Update vitest monorepo to v3.2.2 (#25723)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 09:00:45 +03:00
renovate[bot]
fed0dfa091 Update vitest monorepo to v3.2.1 (#25715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 09:09:17 +02:00
renovate[bot]
39de40dec9 Update Yarn to v4.9.2 (#25714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-07 09:09:11 +02:00
renovate[bot]
e1c42d9985 Update dependency typescript-eslint to v8.33.1 (#25712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 20:33:20 +02:00
renovate[bot]
ad375c9b01 Update dependency hls.js to v1.6.5 (#25711)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 20:33:01 +02:00
renovate[bot]
07230e5ef5 Update dependency @codemirror/language to v6.11.1 (#25708)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 19:59:35 +02:00
renovate[bot]
52f5af6090 Update dependency @rsdoctor/rspack-plugin to v1.1.3 (#25709)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-06 19:59:08 +02:00
karwosts
3c07289077 Handle shorthand template conditions in trace (#25705) 2025-06-06 18:10:10 +02:00
Mathieu
8eb7fe8b0a Adjust tooltip positioning in ha-sidebar for not first lis… (#25696)
fix(tooltip): fix tooltip positioning in ha-sidebar for not first listbox
2025-06-05 04:35:32 +00:00
renovate[bot]
c8c2966d34 Update dependency gulp to v5.0.1 (#25698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 06:25:43 +02:00
renovate[bot]
a8768a5d9d Update dependency @bundle-stats/plugin-webpack-filter to v4.20.2 (#25692)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-05 06:25:20 +02:00
karwosts
02bb7086e7 Hoist integration card tooltips (#25679) 2025-06-03 21:41:27 +02:00
renovate[bot]
42d8b2ae19 Update dependency lint-staged to v16 (#25463)
* Update dependency lint-staged to v16

* Remove shell parameter

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2025-06-03 19:18:39 +00:00
renovate[bot]
e08f4a6bba Update dependency @types/chromecast-caf-receiver to v6.0.22 (#25681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-03 17:32:25 +02:00
renovate[bot]
2e6c35d977 Update dependency hls.js to v1.6.4 (#25682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-03 17:32:08 +02:00
renovate[bot]
17305a818b Update dependency eslint to v9.28.0 (#25683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-03 01:32:02 +02:00
renovate[bot]
08389dad04 Update babel monorepo to v7.27.4 (#25680)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 22:36:58 +03:00
Bram Kragten
ab6ace46b5 Set answers to yes and no for cloud pipeline confirm (#25674) 2025-06-02 14:02:06 +00:00
renovate[bot]
535dedbbc4 Update dependency hls.js to v1.6.3 (#25669)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 16:01:37 +02:00
renovate[bot]
412eb0c647 Update dependency @codemirror/view to v6.37.1 (#25672)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 16:01:29 +02:00
renovate[bot]
87c8ebd493 Update dependency @codemirror/view to v6.37.0 (#25664)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-01 09:31:21 +02:00
Simon Lamon
6e49f89126 Calendar add event button gap alignment (#25662)
Calendar gap alignment
2025-06-01 09:39:10 +03:00
Bram Kragten
77eae6044d Bumped version to 20250531.0 2025-05-31 16:15:54 +02:00
Bram Kragten
84ac0cd41e Add css variables for start and end padding of tabs (#25654) 2025-05-31 16:15:28 +02:00
Bram Kragten
74f9c1551e Add label to collapse button in data table groups (#25653) 2025-05-31 16:15:27 +02:00
Bram Kragten
aebd6350c0 fix line height entity card (#25652) 2025-05-31 16:15:27 +02:00
ildar170975
4c78eb4797 Add cyrilic letters to slugify() (#25647)
* add cyrilic

* Update src/common/string/slugify.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-05-31 16:15:26 +02:00
ildar170975
560e3890b9 Revert #25027 "more-info-camera: disable download_snapshot if idle" (#25643)
remove a check for "idle"
2025-05-31 16:15:25 +02:00
Ville Skyttä
e448268feb Spelling fixes (#25638) 2025-05-31 16:15:24 +02:00
Ville Skyttä
7ceba218fa Fix Zigbee capitalization in manage device button (#25637) 2025-05-31 16:15:24 +02:00
Petar Petrov
cd61725cf5 Fix Z-WaveJS device count in dashboard (#25635) 2025-05-31 16:15:23 +02:00
Petar Petrov
228860a1ee Use theme variables for network graph labels (#25634) 2025-05-31 16:15:22 +02:00
karwosts
9f69347e1d Cleanup some styling on disabled entity picker (#25632) 2025-05-31 16:15:21 +02:00
ildar170975
a099e65a9d Fix "unavailable" state in Area card (#25063)
* fix "unavailable" state

* Show "-" for unavailable/undefined
2025-05-31 10:45:58 +03:00
renovate[bot]
11e4a9f056 Update dependency typescript-eslint to v8.33.0 (#25657)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-30 17:52:29 +00:00
Bram Kragten
b617299eee Add css variables for start and end padding of tabs (#25654) 2025-05-30 16:48:15 +02:00
Bram Kragten
768f27b1b9 Add label to collapse button in data table groups (#25653) 2025-05-30 16:47:54 +02:00
Bram Kragten
5ed816df6d fix line height entity card (#25652) 2025-05-30 15:34:03 +02:00
renovate[bot]
6692ac7517 Update dependency @lokalise/node-api to v14.8.0 (#25651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-30 14:11:04 +02:00
renovate[bot]
65499db0cb Update babel monorepo to v7.27.3 (#25650)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-30 14:27:05 +03:00
renovate[bot]
11a1eabf61 Update rspack monorepo to v1.3.12 (#25649)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-30 14:26:40 +03:00
ildar170975
b30fa122ba Revert #25027 "more-info-camera: disable download_snapshot if idle" (#25643)
remove a check for "idle"
2025-05-30 10:59:03 +03:00
ildar170975
6730d08b85 Map card: add prefix & suffix for "label_mode: attribute" (#25033)
* added prefix & suffix

* added prefix & suffix

* added prefix & suffix

* added prefix & suffix

* added prefix & suffix

* remove prefix, rename suffix -> unit

* remove prefix, rename suffix -> unit

* remove prefix, rename suffix -> unit

* remove prefix, rename suffix -> unit

* remove prefix, rename suffix -> unit
2025-05-30 10:56:35 +03:00
ildar170975
67003d6fd1 Add cyrilic letters to slugify() (#25647)
* add cyrilic

* Update src/common/string/slugify.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-05-30 05:38:45 +00:00
Petar Petrov
414d46be65 Add device button on the Z-Wave controller's device page (#25636) 2025-05-29 15:29:34 +03:00
Petar Petrov
1485d1a1de Fix Z-WaveJS device count in dashboard (#25635) 2025-05-29 15:29:24 +03:00
Ville Skyttä
fd13e41524 Spelling fixes (#25638) 2025-05-29 10:48:50 +02:00
Petar Petrov
77f7ca0368 Use theme variables for network graph labels (#25634) 2025-05-29 10:05:26 +02:00
Ville Skyttä
7471250a07 Fix Zigbee capitalization in manage device button (#25637) 2025-05-29 07:59:58 +00:00
karwosts
8b0a63d791 Cleanup some styling on disabled entity picker (#25632) 2025-05-29 08:53:30 +03:00
renovate[bot]
57ffa814ed Update dependency @octokit/rest to v22 (#25633)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-29 08:51:54 +03:00
Bram Kragten
46e05f10d1 Bumped version to 20250528.0 2025-05-28 22:35:50 +02:00
Paul Bottein
a1819d6189 Improve search in add automation element dialog (#25626)
* Improve search in add automation element dialog

* Remove unexpected import

* Take min character search into account
2025-05-28 22:35:43 +02:00
Paul Bottein
48a3e1fd63 Put item at the top of picker result if there is an exact match with entity id (#25625) 2025-05-28 22:35:43 +02:00
Paul Bottein
139c8b3702 Fix picker field height (#25623) 2025-05-28 22:35:42 +02:00
Paul Bottein
9458946dcc Fix missing helper for entity picker (#25622) 2025-05-28 22:35:41 +02:00
Paul Bottein
b907dbefad Force narrow style for action, condition and trigger in flows (#25619) 2025-05-28 22:35:40 +02:00
Wendelin
5371fd649c Set markdown code line-height (#25618) 2025-05-28 22:35:40 +02:00
Paul Bottein
61d9b0d2a3 Improve action picker UI and search (#25525) 2025-05-28 22:35:39 +02:00
Paul Bottein
16e20456e2 Put item at the top of picker result if there is an exact match with entity id (#25625) 2025-05-28 22:33:42 +02:00
Paul Bottein
9c0ce41ebb Improve search in add automation element dialog (#25626)
* Improve search in add automation element dialog

* Remove unexpected import

* Take min character search into account
2025-05-28 19:38:33 +02:00
Paul Bottein
b458a1d7c6 Improve action picker UI and search (#25525) 2025-05-28 15:03:17 +02:00
renovate[bot]
0eaeeb1141 Update dependency globals to v16.2.0 (#25624)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-28 14:51:35 +03:00
Paul Bottein
b7e63e697f Fix picker field height (#25623) 2025-05-28 14:07:22 +03:00
Paul Bottein
06db0f4b98 Fix missing helper for entity picker (#25622) 2025-05-28 14:06:45 +03:00
Paul Bottein
d33636c6fb Force narrow style for action, condition and trigger in flows (#25619) 2025-05-28 12:04:54 +02:00
Wendelin
bbb546159c Set markdown code line-height (#25618) 2025-05-28 10:04:54 +02:00
karwosts
e8fc36026a More error handling for preview flow (#25611)
* More error handling for preview flow

* await unsub
2025-05-28 07:04:03 +03:00
Bram Kragten
06270c771f Bumped version to 20250527.0 2025-05-27 21:43:42 +02:00
Norbert Rittel
ae49de8e71 Fix typo in restore_entity_id_selected::confirm_text (#25615) 2025-05-27 21:42:52 +02:00
Petar Petrov
6abdeeae20 Fix for history graph with tiny values (#25612) 2025-05-27 21:42:51 +02:00
Petar Petrov
116716c51d Fix duplicate legend items when comparing energy data (#25610) 2025-05-27 21:42:51 +02:00
Wendelin
77ee69b64d Fix font settings for button card (#25607) 2025-05-27 21:42:50 +02:00
Wendelin
1a57eeddde Fix sidebar loading and demo (#25606) 2025-05-27 21:42:49 +02:00
Petar Petrov
9131bf6dfd Fix double history graphs for a disabled entity (#25604) 2025-05-27 21:42:48 +02:00
Paul Bottein
1611423ca5 Fix duplicated items in strategy editor (#25600) 2025-05-27 21:42:48 +02:00
Petar Petrov
38f8c804af Fix for history graph with tiny values (#25612) 2025-05-27 20:46:03 +02:00
Petar Petrov
7c5bf26240 Fix duplicate legend items when comparing energy data (#25610) 2025-05-27 20:43:32 +02:00
Norbert Rittel
189067d14b Fix typo in restore_entity_id_selected::confirm_text (#25615) 2025-05-27 19:56:55 +02:00
Petar Petrov
e79e0f77b8 Navigate to newly added device (#25608) 2025-05-27 16:19:53 +02:00
Wendelin
b226e5c697 Fix font settings for button card (#25607) 2025-05-27 12:55:20 +02:00
Wendelin
52ad31601c Fix sidebar loading and demo (#25606) 2025-05-27 10:22:04 +02:00
Petar Petrov
cba3e4df7f Fix double history graphs for a disabled entity (#25604) 2025-05-27 09:08:58 +02:00
Paul Bottein
3532cfa974 Fix duplicated items in strategy editor (#25600) 2025-05-26 19:21:44 +02:00
Bram Kragten
de56c3376e Bumped version to 20250526.0 2025-05-26 18:32:32 +02:00
Bram Kragten
629eb29d42 Merge branch 'rc' into dev 2025-05-26 18:32:04 +02:00
Bram Kragten
61019447cf Update alarm card styling (#25598) 2025-05-26 18:31:27 +02:00
Bram Kragten
cde2b436d1 Add restore entity id (#25592) 2025-05-26 16:23:23 +00:00
Alex Gustafsson
6c7d750734 Add support for grouping media players (#24736)
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-05-26 14:35:09 +00:00
Paul Bottein
fcf5ed7731 Use context instead of stateObj for card features (#25577)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-05-26 13:59:26 +00:00
Cretezy
3ce639946c Make weather-forecast card more responsive (#24900)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-05-26 13:26:32 +00:00
Wendelin
bb5f01ac81 Use failed add-ons and folders of backup (#25548)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-05-26 13:25:57 +00:00
Wendelin
208e863327 Save sidebar in user data (#25555)
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-05-26 13:25:11 +00:00
karwosts
9f5f100e98 Tweak rules for entity-filter card (#25570)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-05-26 13:24:10 +00:00
renovate[bot]
114c1fb98b Update vaadinWebComponents monorepo to v24.7.7 (#25596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 15:20:08 +02:00
Bram Kragten
3e5bd64b83 Update ha-selector-action.ts 2025-05-26 15:14:59 +02:00
karwosts
02b4b8e334 Fix device actions in step-flow-form (#24539)
* Fix device actions in step-flow-form

* Move xlation fetch to device action

* also conditions & triggers

* move to firstUpdated
2025-05-26 14:19:09 +02:00
Petar Petrov
da8d43f5d1 Allow href="data:..." in config flow step description (#25559)
* Allow href="data:..." in config flow step description

* Update src/dialogs/config-flow/show-dialog-config-flow.ts
2025-05-26 13:56:11 +02:00
Paul Bottein
18aaa44d2d Use constant for Home Assistant bluetooth node in graph (#25595) 2025-05-26 12:26:06 +02:00
Paul Bottein
7fa697a768 Add actions section and add domains to other section in area dashboard (#25558)
* Add actions section and add domains to other section in area dashboard

* Add timer and input button

* Better grouping
2025-05-26 10:50:42 +02:00
Jan-Philipp Benecke
28fe60f02b Improve broken cards on dashboards (#25557) 2025-05-26 08:45:22 +00:00
dependabot[bot]
549451eccb Bump relative-ci/agent-action from 2.2.0 to 3.0.0 (#25593)
Bumps [relative-ci/agent-action](https://github.com/relative-ci/agent-action) from 2.2.0 to 3.0.0.
- [Release notes](https://github.com/relative-ci/agent-action/releases)
- [Commits](https://github.com/relative-ci/agent-action/compare/v2.2.0...v3.0.0)

---
updated-dependencies:
- dependency-name: relative-ci/agent-action
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-26 08:26:06 +02:00
karwosts
113cc118cf Support for disabled selectors in config flow (#22592)
* Support for disabled selectors in config flow

* rename flag to readonly

* rename to read_only

* Merge branch 'dev' of https://github.com/home-assistant/frontend into disabled-fields-config-flow

* rework for new backend

* Fix disabled entity picker

* no longer used type

* Update src/dialogs/config-flow/step-flow-form.ts

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

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-05-23 20:52:42 +03:00
Bram Kragten
7ffb0f1e3b rename android-safe-area-inset to app-safe-area-inset (#25575) 2025-05-23 18:34:53 +02:00
renovate[bot]
60ef43044b Update octokit monorepo to v8.0.1 (#25576)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-23 17:37:26 +02:00
renovate[bot]
4e4a82e023 Update rspack monorepo to v1.3.11 (#25574)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-23 17:36:56 +02:00
Paulus Schoutsen
f563146165 Sort discoveries by title on integration page (#25578) 2025-05-23 17:33:47 +02:00
Petar Petrov
81ba2db93a Custom variable down sampling for line charts (#25561) 2025-05-23 15:20:41 +02:00
renovate[bot]
4d8176ad6e Update octokit monorepo to v8 (major) (#25573)
Update octokit monorepo to v8

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-23 14:13:11 +03:00
Petar Petrov
9b8be9f1af Fix chart labels for multi year periods (#25572) 2025-05-23 12:08:22 +02:00
Wendelin
c11d2c10df Enable keyboard sorting for items-display-editor (#25546)
* Enable keyboard sorting for items-display-editor

* Add alt + arrow and fix bugs

* Fix alt bug

* Improve selected drag highlight
2025-05-23 10:42:27 +02:00
Paul Bottein
412a0e9f6a Improve area floor picker UI and search (#25540)
* Improve area floor picker UI and search

* Improve area floor picker UI and search

* Remove noResultSorting
2025-05-23 10:35:47 +02:00
renovate[bot]
67dc830bbf Update dependency marked to v15.0.12 (#25571)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-23 08:52:22 +03:00
renovate[bot]
5581c10139 Update vitest monorepo to v3.1.4 (#25569)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 16:36:02 +00:00
renovate[bot]
ec26818c53 Update dependency @types/leaflet to v1.9.18 (#25568)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-22 18:26:19 +02:00
Paul Bottein
399458f811 Improve category picker UI and search (#25560) 2025-05-22 18:14:26 +02:00
Paul Bottein
3355986585 Restore screen readers support on pickers (#25553) 2025-05-22 09:51:12 +00:00
Franck Nijhof
9b7db191a6 Add range support to icon translations (#25541)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-05-22 11:32:44 +02:00
Paulus Schoutsen
2d549ba22f Voice wizard: Wait 10s for update entity to change state (#25556) 2025-05-22 09:49:29 +02:00
Grzegorz Libiszewski
c3e155a95c Unified safe area (insets) for Android and iOS (#24689)
* feat: Introduce new css variables for safe area

* feat: Replace all safe area env with variable
2025-05-22 08:11:04 +03:00
Petar Petrov
754829a836 Localize ZHA visualization text (#25543) 2025-05-21 12:54:38 +00:00
c0ffeeca7
87bd039239 Add comma (#25547) 2025-05-21 15:44:16 +03:00
Wendelin
32b3c83337 Edit sidebar in a dialog (#25532) 2025-05-21 13:42:43 +02:00
karwosts
f0beef22d2 Add hint helpers to fields that support markdown (#25528)
* Add hint helpers to fields that support markdown

* reduce duplication
2025-05-21 08:13:28 +03:00
Petar Petrov
07e5f53469 Bluetooth network visualization (#25512)
* Bluetooth network visualization

* fix category symbol

* Add translations

* memoize bluetooth data

* throttle data updates to 10s

* handle proxies that appear as end devices too

* fix tab highlighting

* memoize fix
2025-05-21 07:59:44 +03:00
c0ffeeca7
97e0217906 Remote access strings: tiny typo fix (#25539) 2025-05-20 12:47:51 +00:00
Paul Bottein
006c7e1ea8 Improve floor picker UI and search (#25535)
* Improve floor picker UI and search

* sort by level
2025-05-20 14:45:40 +02:00
Andrej Shumkovski
9736d0cb55 Added Albanian (Shqip) Language (#25538)
Update translationMetadata.json

Added Shqip language following: 

https://developers.home-assistant.io/docs/translations/#maintainer-steps-to-add-a-new-language
2025-05-20 14:35:49 +02:00
Wendelin
9ec689382b Add has-fab to hass-tabs-subpage (#25536) 2025-05-20 14:00:27 +03:00
Paul Bottein
4de95f6710 Improve label picker UI and search (#25522) 2025-05-20 12:47:33 +02:00
Paul Bottein
a55ef8ad47 Fix entity picker in footer graph and media card (#25531) 2025-05-20 11:32:37 +02:00
Paul Bottein
01b398c2a3 Fix clear area action in area-picker (#25511)
* Fix clear area action in area-picker

* Fix area not displayed after creation
2025-05-20 10:01:01 +02:00
Paulus Schoutsen
83df10ef29 Tweaks to Assist chat dialog (#25494) 2025-05-20 08:39:56 +02:00
renovate[bot]
a026c72230 Update dependency eslint to v9.27.0 (#25529)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-20 07:44:11 +03:00
Petar Petrov
c4e391c264 Echarts network graph for ZHA (#25457)
* Echarts network graph for ZHA

* improve layout

* better diff

* remove vis-network

* not bad layout

* fix LQI and clean up a bit

* Use ha-chart-base and remove header

* legend

* use color vars

* use colorVariables

* fix

* add physics toggle

* tweak lines

* remove vis-network

* minor tweaks

* dynamically load graph chart

* type fix

* fix height

* navigate to device page on label click

* PR comments

* aria tweak

* make extraComponents non reactive

* PR comments

* quick fix

* just make hass non reactive

* button tweak

* Update src/components/chart/ha-network-graph.ts

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

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-05-19 16:37:05 +03:00
karwosts
4b72a6029c Fix stack editor bugs (#25501)
* Fix stack editor bugs

* Update src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts

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

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-05-19 16:36:46 +03:00
Timothy
8b87188075 Add navigate command to the external bus (#25516) 2025-05-19 13:22:19 +00:00
Paul Bottein
2a4c6c9af5 Improve user picker UI and search (#25514)
Improve user picker
2025-05-19 13:53:19 +03:00
Paul Bottein
5cbadaa5f9 Fix labels in duration input (#25510) 2025-05-19 11:26:25 +02:00
Paul Bottein
15fd4134d0 Improve device picker UI and search (#25401)
* Improve device picker

* Improve device search quick bar

* Fix types

* Fix selected device in the dropdown

* Move filter to picker

* Rename filters

* Use generic picker

* Update src/dialogs/quick-bar/ha-quick-bar.ts

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-05-19 09:24:54 +00:00
renovate[bot]
eda9abc3c5 Lock file maintenance (#25508)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 07:16:33 +03:00
karwosts
7e56d5f351 Fix loading behavior when manually typing card type (#25502) 2025-05-18 08:57:59 +03:00
renovate[bot]
5d4805cde6 Update dependency @codemirror/search to v6.5.11 (#25499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-18 08:54:22 +03:00
karwosts
c94326bc08 Fix duplicate_id in energy-usage-graph (#25503) 2025-05-18 08:53:37 +03:00
karwosts
b61180baa6 Don't access hidden tabs via left/right arrow (#25495) 2025-05-17 12:43:12 +02:00
karwosts
bc15d1474e Force wrap too long expansion panel headers (#25496) 2025-05-17 12:42:08 +02:00
Bram Kragten
95c3013497 Merge branch 'rc' 2025-05-16 18:57:16 +02:00
Bram Kragten
69a156f352 Bumped version to 20250516.0 2025-05-16 18:57:02 +02:00
Paul Bottein
67f3d31a4b Fix domain not translated in entity picker and quick bar (#25444)
* Fix domain not translated in entity picker

* Remove unused param
2025-05-16 18:56:16 +02:00
Bram Kragten
98a2966432 Render choose and if action correctly on mobile (#25411) 2025-05-16 18:56:14 +02:00
karwosts
75a2c061c2 Fix card editor help links (#25408) 2025-05-16 18:56:13 +02:00
karwosts
724df18175 Fix devices graph max_devices (#25406) 2025-05-16 18:56:12 +02:00
Paul Bottein
c00b4120ab Fix selected entity in the entity picker dropdown (#25405) 2025-05-16 18:56:11 +02:00
Paul Bottein
d392bb4c83 Fallback to preferred pipeline if the pipeline doesn't exist (#25404) 2025-05-16 18:56:11 +02:00
Paul Bottein
30d46f2f8a Fix domain max-width in entity picker (#25400) 2025-05-16 18:56:10 +02:00
Paulus Schoutsen
7c879cc291 Add installation method to the about page (#25378)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-05-16 18:56:08 +02:00
renovate[bot]
9a731880f3 Update rspack monorepo to v1.3.10 (#25493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-16 18:06:17 +02:00
Yosi Levy
77efaaf7de RTL base font fix (#25478)
* RTL base font fix

* Fix comments
2025-05-16 08:20:19 +03:00
Paulus Schoutsen
da96266454 Update media content type for picked TTS (#25489) 2025-05-16 08:18:31 +03:00
renovate[bot]
036d739d6e Update dependency @codemirror/view to v6.36.8 (#25487) 2025-05-15 19:56:20 +02:00
renovate[bot]
c274a94ee5 Update dependency typescript-eslint to v8.32.1 (#25488)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-15 19:53:57 +02:00
Paul Bottein
da7d359696 Use generic picker for statistic picker (#25484) 2025-05-15 19:38:36 +02:00
Paul Bottein
20a7b3870c Improve area picker UI and search (#25472)
* Improve area picker UI and search

* Feedbacks
2025-05-15 15:53:22 +02:00
Wendelin
9749a64ae1 Preload Roboto for Chrome OS (#25480) 2025-05-15 14:48:09 +02:00
renovate[bot]
68741f6ba4 Update vaadinWebComponents monorepo to v24.7.6 (#25475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-15 14:33:08 +03:00
AlCalzone
e052ee04b4 Fix label for Thermostat Setback in Z-Wave JS installer panel (#25476) 2025-05-15 13:22:52 +02:00
Wendelin
d2cc4a624e Use ha-line-height css tokens in codebase (#25468) 2025-05-15 11:57:17 +02:00
Paul Bottein
5bcbe98f8e Create generic picker component (#25464)
* Create generic combo-box

* Fix focus

* Delete entity combo-box

* Rename components

* Create generic picker

* Fix undefined and placeholder

* Fix labels

* Fix icon and search

* Add missing hideClearIcon property to entity picker

* Make search string optional
2025-05-15 09:55:32 +00:00
Petar Petrov
beee76580d Keep screen on while adding new ZHA device (#25465) 2025-05-15 11:55:05 +02:00
karwosts
28e5a30772 Prioritize battery consumption before solar consumption (#25467) 2025-05-15 10:32:07 +03:00
David F. Mulcahey
785929b370 Add ZHA status details to configuration dashboard (#25425)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-05-14 11:57:30 +00:00
Petar Petrov
55cf7e635d Keep screen on while adding new ZwaveJS device (#25375)
* Keep screen on while adding new ZwaveJS device

* Add compatibility check

* Update src/mixins/wakelock-mixin.ts

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

* format

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-05-14 13:45:48 +03:00
Petar Petrov
4b270eb444 Periodically refresh alerts on the device page (#25385)
* Periodically refresh alerts on the device page

* Update src/panels/config/devices/ha-config-device-page.ts

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

* revert type change

* Apply suggestions from code review

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

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-05-14 11:28:56 +02:00
Jan-Philipp Benecke
d0e55719d1 Make map card clustering configurable (#25429)
* Persistent map clustering

* Make config option instead
2025-05-14 11:27:42 +02:00
renovate[bot]
87f9397643 Update dependency @bundle-stats/plugin-webpack-filter to v4.20.1 (#25461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-14 08:47:57 +02:00
Wendelin
910e7e10a7 Use ha-dialog-header for data-entry-flow (#25403)
* Use ha-dialog-header for data-entry-flow header

* Fix header wrap
2025-05-13 14:17:54 +03:00
Wendelin
e1b9b47ac7 Hide map cluster toggle when there is just one entry (#25454) 2025-05-13 12:23:24 +03:00
Maciej Suchanecki
89e04fcc45 Allow for shift click in data table checkboxes (#25024)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-05-13 11:08:45 +02:00
David Rapan
93f2e75fc9 Update network_adapter_info translation as all core discovery integrations will now use network (#25452) 2025-05-13 09:27:54 +02:00
Wendelin
6c671d398f Fix zwave creating nvm backup translation (#25453) 2025-05-13 05:59:50 +00:00
Paul Bottein
2e5c6a4d3f Fix domain not translated in entity picker and quick bar (#25444)
* Fix domain not translated in entity picker

* Remove unused param
2025-05-13 08:54:31 +03:00
renovate[bot]
04e736a51e Update dependency @lokalise/node-api to v14.7.0 (#25450)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-13 08:53:25 +03:00
Wendelin
ec3fdc0ea7 Subscribe to frontend user data (#25445) 2025-05-12 16:01:56 +02:00
Robert Resch
a7a8c25d24 Remove getCandidatesUpfront (#25399) 2025-05-12 14:55:39 +02:00
c0ffeeca7
60d457c3d9 Register account: apply sentence-style capitalization (#25440) 2025-05-12 10:09:47 +02:00
renovate[bot]
a58b1e636d Update dependency @rsdoctor/rspack-plugin to v1.1.2 (#25420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 09:12:13 +02:00
renovate[bot]
f67e7ae081 Update rspack monorepo to v1.3.9 (#25421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:52:00 +02:00
renovate[bot]
0032c5508e Update dependency eslint-config-prettier to v10.1.5 (#25439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 08:51:39 +02:00
renovate[bot]
036df78de8 Update dependency idb-keyval to v6.2.2 (#25432)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-11 21:02:47 +02:00
renovate[bot]
617a6ba938 Update dependency @lokalise/node-api to v14.6.0 (#25433)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-11 21:00:45 +02:00
Alex Gustafsson
76b9063aec Fix typo in lint config (#25419)
Fix a typo causing yaml and "aml" files to be formatted with prettier as
opposed to yaml and yml.
2025-05-10 13:22:25 +02:00
renovate[bot]
60c1d0a556 Update dependency globals to v16.1.0 (#25416)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-10 13:22:10 +02:00
Bram Kragten
193caec2df Render choose and if action correctly on mobile (#25411) 2025-05-10 09:03:53 +02:00
renovate[bot]
42c8d132bf Update dependency @babel/preset-env to v7.27.2 (#25409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-10 09:03:37 +02:00
renovate[bot]
0311a7c976 Update dependency @rsdoctor/rspack-plugin to v1.1.1 (#25414)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-10 09:03:26 +02:00
renovate[bot]
40ffd50b8a Update dependency barcode-detector to v3.0.4 (#25412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-10 09:03:22 +02:00
renovate[bot]
334991902a Update dependency eslint-config-prettier to v10.1.3 (#25415)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-10 09:03:14 +02:00
karwosts
c968266065 Fix card editor help links (#25408) 2025-05-09 18:38:03 +02:00
Paul Bottein
d4fc0318f7 Fix domain max-width in entity picker (#25400) 2025-05-09 18:37:14 +02:00
karwosts
8a0d3baf67 Fix devices graph max_devices (#25406) 2025-05-09 18:35:54 +02:00
Paul Bottein
8fc55cb6e2 Fix selected entity in the entity picker dropdown (#25405) 2025-05-09 18:35:38 +02:00
Paul Bottein
d6ebd9bfc4 Fallback to preferred pipeline if the pipeline doesn't exist (#25404) 2025-05-09 16:01:03 +02:00
Bram Kragten
15ae37d077 Move vaadin typography vars to typography globals (#25398) 2025-05-09 12:08:12 +00:00
Bram Kragten
461d5eb687 Add device rename to voice wizard (#25187)
Co-authored-by: Wendelin <w@pe8.at>
2025-05-09 13:56:50 +02:00
Wendelin
3058fcad46 Use ha-font-size typography css tokens (#25361) 2025-05-09 13:47:25 +02:00
renovate[bot]
06bd1ae4cd Update dependency @rsdoctor/rspack-plugin to v1.1.0 (#25389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-05-09 13:43:39 +02:00
renovate[bot]
00733357a1 Update dependency lint-staged to v15.5.2 (#25394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-09 13:19:20 +02:00
Bram Kragten
7f6ce97199 Merge branch 'rc' 2025-05-09 12:11:58 +02:00
Bram Kragten
0742aed8e7 Bumped version to 20250509.0 2025-05-09 12:11:36 +02:00
Bram Kragten
f675dd3388 Fix width of device in create entry step (#25392) 2025-05-09 12:11:08 +02:00
Bram Kragten
9751cb4e50 Fix bottom padding in service control when in narrow mode (#25390) 2025-05-09 12:11:07 +02:00
Bram Kragten
7919028780 Fix sidebar keyboard navigation in edit mode (#25376) 2025-05-09 12:11:07 +02:00
Wendelin
1508c7c905 Fix backup custom retention save issue (#25368) 2025-05-09 12:11:06 +02:00
Wendelin
3fe907f388 Fix backup settings undefined cloudStatus (#25366) 2025-05-09 12:11:05 +02:00
J. Nick Koston
5cc87661b9 Add a dialog to SSDP to show raw data (#25362)
* Add a dialog to SSDP to show raw data

* tweaks

* Add a dialog to SSDP to show raw data

* reduce

* tweaks

* Update src/panels/config/integrations/integration-panels/ssdp/dialog-ssdp-raw-data.ts

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

* Update src/panels/config/integrations/integration-panels/ssdp/dialog-ssdp-discovery-info.ts

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

* Update src/panels/config/integrations/integration-panels/ssdp/dialog-ssdp-discovery-info.ts

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

* add dump

* preen

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-05-09 12:11:04 +02:00
Wendelin
fc372172a6 Fix ha-dialog actions padding to use safe-area-inset (#25359)
* Fix ha-dialog actions padding to use safe-area-inset

* Update src/components/ha-dialog.ts

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

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-05-09 12:11:03 +02:00
Simon Lamon
7bd9a39bf5 Fix bluetooth device info is encoding data twice (#25353)
Data is already encoded
2025-05-09 12:11:02 +02:00
Bram Kragten
665c971822 Fix width of device in create entry step (#25392) 2025-05-09 12:00:06 +02:00
Bram Kragten
eff5471dd1 Fix bottom padding in service control when in narrow mode (#25390) 2025-05-09 11:01:58 +02:00
Paulus Schoutsen
4fba9c3c0a Add installation method to the about page (#25378)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-05-09 10:23:52 +02:00
renovate[bot]
0b32b51e2f Update dependency @types/leaflet-draw to v1.0.12 (#25386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-09 09:00:39 +02:00
Bram Kragten
6370b0b8e5 Fix sidebar keyboard navigation in edit mode (#25376) 2025-05-09 08:51:04 +02:00
renovate[bot]
681518f443 Update dependency typescript-eslint to v8.32.0 (#25383)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-08 23:51:36 +02:00
Simon Lamon
9f5b89978d Bump the hls patch (#25264) 2025-05-08 15:30:49 +00:00
Bram Kragten
130839ee7b Revert "Add left text alignment to call service button" (#25377) 2025-05-08 17:24:10 +02:00
Wendelin
ba4ec960c8 Use ha-font-weight typography css tokens (#25374)
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-05-08 15:11:53 +00:00
Petar Petrov
6692d9c6aa Use the new included_in_stat hierarchy in the Energy Sankey card (#25306) 2025-05-08 17:01:18 +02:00
renovate[bot]
4d2d94c54f Update dependency @lokalise/node-api to v14.5.2 (#25380)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-08 17:51:45 +03:00
Wendelin
d59c6612c6 Fix backup custom retention save issue (#25368) 2025-05-08 16:50:31 +02:00
Wendelin
498f158253 Use ha-font-smoothing typography css tokens (#25364) 2025-05-08 17:32:48 +03:00
renovate[bot]
b8026ccf46 Update vitest monorepo to v3.1.3 (#25379)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-08 17:29:57 +03:00
Wendelin
84def48222 Add tests for common name utils (#25373) 2025-05-08 16:59:28 +03:00
Wendelin
cea0ac02fe Improve ha-spinner gallery docs (#25369) 2025-05-08 15:50:03 +03:00
Wendelin
e1b099e88b Fix backup settings undefined cloudStatus (#25366) 2025-05-08 13:47:17 +02:00
Yosi Levy
d571ef3f18 Font style in side bar (#25367) 2025-05-08 13:21:39 +02:00
Lucas
c8cffef647 Add left text alignment to call service button (#24846) 2025-05-08 09:58:46 +02:00
Simon Lamon
6b568307a4 Fix bluetooth device info is encoding data twice (#25353)
Data is already encoded
2025-05-08 08:21:01 +03:00
J. Nick Koston
1b501907f1 Add a dialog to SSDP to show raw data (#25362)
* Add a dialog to SSDP to show raw data

* tweaks

* Add a dialog to SSDP to show raw data

* reduce

* tweaks

* Update src/panels/config/integrations/integration-panels/ssdp/dialog-ssdp-raw-data.ts

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

* Update src/panels/config/integrations/integration-panels/ssdp/dialog-ssdp-discovery-info.ts

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

* Update src/panels/config/integrations/integration-panels/ssdp/dialog-ssdp-discovery-info.ts

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

* add dump

* preen

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-05-08 08:18:26 +03:00
Wendelin
c7e79998a4 Fix ha-dialog actions padding to use safe-area-inset (#25359)
* Fix ha-dialog actions padding to use safe-area-inset

* Update src/components/ha-dialog.ts

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

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-05-07 18:04:43 +02:00
Wendelin
03ccf014d9 Use ha-font-family typography css token (#25355)
Use ha-font-family
2025-05-07 17:38:07 +03:00
renovate[bot]
ab41bdb87d Update dependency @lokalise/node-api to v14.5.1 (#25360)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-07 17:35:54 +03:00
karwosts
821a0bc418 New solar-consumed-gauge algorithm (#25326)
* New solar-consumed-gauge algorithm

* update tests

* Apply suggestion

* reduce duplicate code
2025-05-07 17:35:24 +03:00
karwosts
6d931b9e37 Add UoM to location selector (#25358) 2025-05-07 16:13:29 +02:00
Bram Kragten
f0341f28ab Merge branch 'rc' 2025-05-07 13:14:55 +02:00
Paul Bottein
603663e0cc Fix badge overlay in scrolling mode (#25352)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-05-07 13:14:21 +02:00
Bram Kragten
80dddc9eef Only enable drag scroll controller when needed (#25351) 2025-05-07 13:14:20 +02:00
Bram Kragten
8494d0542e Bumped version to 20250507.0 2025-05-07 10:45:48 +02:00
Wendelin
467a5bef0b Fix data-table search clear button alignment (#25348) 2025-05-07 10:45:19 +02:00
Petar Petrov
24736e36ff Set <=0 chart values to null so they render as gaps on a log graph (#25347) 2025-05-07 10:45:18 +02:00
Paulus Schoutsen
604c00d772 Update network browser descriptions (#25345) 2025-05-07 10:45:17 +02:00
Bram Kragten
12bbba1bff fix height of stats graph card in section (#25344) 2025-05-07 10:45:16 +02:00
Bram Kragten
e3221ad4ee Merge branch 'rc' 2025-05-06 20:46:09 +02:00
Bram Kragten
7d3a7fa1db Bumped version to 20250506.0 2025-05-06 20:45:47 +02:00
Paul Bottein
5e67bf1fa7 Fix outlined icon button style (#25340) 2025-05-06 20:45:16 +02:00
Paul Bottein
7147c7578d Use right theme variable for dashboard tab color in edit mode (#25339) 2025-05-06 20:45:16 +02:00
Paul Bottein
90710fedf2 Simplify entity combo-box code (#25338) 2025-05-06 20:45:15 +02:00
Paul Bottein
9629159ef1 Use middle dot 00B7 as separator (#25336) 2025-05-06 20:45:14 +02:00
Paul Bottein
bfac6e1516 Add covers to overview view for area strategy (#25334) 2025-05-06 20:45:13 +02:00
Wendelin
7c288d1769 Fix sidebar item text width (#25332)
* Fix sidebar item text width to utilize full available space

* Update src/components/ha-sidebar.ts

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

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-05-06 20:45:12 +02:00
Paul Bottein
39119eeb2a Align side bar title with items (#25330) 2025-05-06 20:45:11 +02:00
Wendelin
9c16ce3342 Fix flow form padding end (#25328)
* Fix flow form padding end

* Use more end padding if docs are present
2025-05-06 20:45:10 +02:00
Paulus Schoutsen
3595fab5cb Show voice ID in TTS media browser (#25324)
* Show voice ID in TTS media browser

* Apply suggestions from code review

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

* Add copy button

* Now copy correct clipboard icon

* Improve styling

* GAP

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2025-05-06 20:45:09 +02:00
Wendelin
78b2d17f10 Add custom retention info to backup locations (#25318)
* Add retention messages to backup locations

* Fix mobile

* Use join
2025-05-06 20:45:09 +02:00
Paul Bottein
7b8b8f9d0b 20250502.1 (#25323) 2025-05-05 20:14:29 +02:00
Paul Bottein
b7f866faff Bumped version to 20250502.1 2025-05-05 20:12:59 +02:00
Paulus Schoutsen
37671fd613 Populate integration domain My link (#25322)
* Populate integration domain My link

* break out of loop

* Actually just return from function

* Consolidate code
2025-05-05 20:12:06 +02:00
Paulus Schoutsen
92905c1433 Clean up network browser nav (#25321)
* Clean up network browser nav

* Add ha-md-list
2025-05-05 20:12:05 +02:00
Paulus Schoutsen
cb7251cb5e Reorder my links (#25319) 2025-05-05 20:12:04 +02:00
Paul Bottein
44485c0de4 Do not display no areas in entity pickers (#25317) 2025-05-05 20:12:03 +02:00
Paul Bottein
1191a09576 Use new entity naming in card entity picker (#25316) 2025-05-05 20:12:03 +02:00
karwosts
c178488aac Add energy hourly calculations to CSV report (#25315) 2025-05-05 20:12:02 +02:00
Wendelin
9f6463eec0 Fix zwave add device LR/mesh icons (#25313)
Fix zwave LR/mesh icons
2025-05-05 20:12:01 +02:00
Wendelin
70fef59401 Fix options and repair flow success (#25312) 2025-05-05 20:12:00 +02:00
Wendelin
3e053e07c6 Fix selected entity overflow (#25311) 2025-05-05 20:11:59 +02:00
Wendelin
376cac6002 Fix select entity change (#25310) 2025-05-05 20:11:58 +02:00
Paul Bottein
5cd68301ed Fix pasting yaml in automation code editor (#25309)
Fix pasting yaml in code editor
2025-05-05 20:11:57 +02:00
Wendelin
3121721ac7 Revert "Use md-select for entity-row and state-card" (#25308)
Revert "Use md-select for entity-row and state-card (#25307)"

This reverts commit 3c9dce20e2.
2025-05-05 20:11:56 +02:00
Wendelin
868daf692d Use md-select for entity-row and state-card (#25307) 2025-05-05 20:11:56 +02:00
Wendelin
75e9ac9e73 Fix flow-form header (#25305) 2025-05-05 20:11:55 +02:00
Paulus Schoutsen
f6e36f2038 Add profile security link to My Home Assistant (#25303) 2025-05-05 20:11:54 +02:00
Bram Kragten
55770f3e02 Fix display of disabled items in traces (#25293) 2025-05-05 20:11:53 +02:00
Bram Kragten
cfc7f91f03 Fix select entity row opening more info on select (#25292) 2025-05-05 20:11:52 +02:00
Jan-Philipp Benecke
74488c0b96 Use new entity picker style in quick bar (#25265)
* Use new entity picker style in quick bar

* Cleanup

* Add missing no area

* Process code review
2025-05-05 20:11:51 +02:00
Bram Kragten
1154d1769d Bumped version to 20250502.0 2025-05-02 21:34:23 +03:00
Bram Kragten
a820cd4576 Fix decorators with properties (#25282)
* Fix decorators with properties

* Green build

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2025-05-02 21:33:56 +03:00
Bram Kragten
678a8a85cb Fix alignment of ha-labeled-slider (#25279) 2025-05-02 21:33:55 +03:00
karwosts
358e450e60 Fix disabled language picker (#25278) 2025-05-02 21:33:54 +03:00
Jan-Philipp Benecke
32acef8fad Add save shortcut to shortcuts dialog (#25271) 2025-05-02 21:33:53 +03:00
J. Nick Koston
fae619085c Add my links for the Bluetooth monitors (#25270)
The plan is to link these in the Bluetooth docs for help debugging
2025-05-02 21:33:52 +03:00
Paulus Schoutsen
5d89563aa5 Import missing components on init page (#25269) 2025-05-02 21:33:52 +03:00
Bram Kragten
d7dd11ba7f Improve error handling in automation i18n (#25266) 2025-05-02 21:33:51 +03:00
Jan-Philipp Benecke
4624cc609f Always show backup location retention settings (#25261)
Always show backup location retention settings
2025-05-02 21:33:50 +03:00
karwosts
f24b6a4cb1 Fix typo in energy calculation (#25259)
* New energy calculation

* more tests and stricter tests. change priority order

* more test and fix error
2025-05-02 21:33:49 +03:00
Jan-Philipp Benecke
2b06742bb9 Hide the tab when view is a subview (#25256) 2025-05-02 21:33:48 +03:00
Jan-Philipp Benecke
9e5b7462af Add ? as shortcut for shortcuts dialog (#25253)
Bind shortcuts dialog to `?` key
2025-05-02 21:33:47 +03:00
J. Nick Koston
a544ff4c8a Improve message when no discovery data is found (#25252)
* Improve message when no discovery data is found

It was pointed out its a bit confusing when a device has not been
discovered yet for the discovery/network browser panels as we
only said there was no data. Give the user a better hint
as to why there is no data.

* Improve message when no discovery data is found

It was pointed out its a bit confusing when a device has not been
discovered yet for the discovery/network browser panels as we
only said there was no data. Give the user a better hint
as to why there is no data.

* Improve message when no discovery data is found

It was pointed out its a bit confusing when a device has not been
discovered yet for the discovery/network browser panels as we
only said there was no data. Give the user a better hint
as to why there is no data.

* Improve message when no discovery data is found

It was pointed out its a bit confusing when a device has not been
discovered yet for the discovery/network browser panels as we
only said there was no data. Give the user a better hint
as to why there is no data.
2025-05-02 21:33:46 +03:00
J. Nick Koston
b81d2013dc Fix formatting of mac address fields in device info (#25251)
Previous change lost the `:` seperator, and unexpectedly MAC became
title case instead of MAC when dhcp is loaded.
2025-05-02 21:33:45 +03:00
J. Nick Koston
9b7e2886b6 Better explain when DHCP discovery data will be available (#25250)
It was pointed out that users likely may not know what DHCP is
and wonder why the data is not available yet.
2025-05-02 21:33:44 +03:00
karwosts
69eaf178ca New energy calculation formula (#25242)
* New energy calculation

* more tests and stricter tests. change priority order
2025-05-02 21:33:44 +03:00
Yosi Levy
1a14511fa6 Various RTL fixes (#25231) 2025-05-02 21:33:43 +03:00
Paul Bottein
e517175f68 20250430.2 (#25246) 2025-04-30 17:52:11 +02:00
Paul Bottein
ee9cbf7370 Bumped version to 20250430.2 2025-04-30 17:50:44 +02:00
Bram Kragten
efd7b380a9 Handle errrors/wrong values on paste (#25245) 2025-04-30 17:49:56 +02:00
Paul Bottein
46cc254f77 Use code font family variable in combo-box (#25243) 2025-04-30 17:49:55 +02:00
Paul Bottein
d996ba818f 20250430.1 (#25244) 2025-04-30 16:10:12 +02:00
Paul Bottein
e5f41ceb3e Bumped version to 20250430.1 2025-04-30 16:08:42 +02:00
Bram Kragten
2e4ce71d06 Improve trigger condition check on paste (#25241) 2025-04-30 16:08:03 +02:00
Bram Kragten
50e39de974 Allow pasting more script config (#25240)
* Allow pasting more script config

* Update manual-script-editor.ts
2025-04-30 16:08:02 +02:00
Bram Kragten
1ba941282c Allow pasting more automation config formats (#25239) 2025-04-30 16:08:01 +02:00
J. Nick Koston
b656ddc1f0 Add DHCP Browser entry point to network (#25235)
* Add DHCP Browser entry point to network

* lint
2025-04-30 16:08:00 +02:00
Paul Bottein
a699149388 20250430.0 (#25237) 2025-04-30 13:10:20 +02:00
664 changed files with 22434 additions and 12242 deletions

View File

@@ -11,7 +11,7 @@ body:
**Please do not report issues for custom cards.**
[fr]: https://github.com/home-assistant/frontend/discussions
[fr]: https://github.com/orgs/home-assistant/discussions
[releases]: https://github.com/home-assistant/home-assistant/releases
- type: checkboxes
attributes:
@@ -108,9 +108,9 @@ body:
render: yaml
- type: textarea
attributes:
label: Javascript errors shown in your browser console/inspector
label: JavaScript errors shown in your browser console/inspector
description: >
If you come across any Javascript or other error logs, e.g., in your
If you come across any JavaScript or other error logs, e.g., in your
browser console/inspector please provide them.
render: txt
- type: textarea

View File

@@ -1,7 +1,7 @@
blank_issues_enabled: false
contact_links:
- name: Request a feature for the UI / Dashboards
url: https://github.com/home-assistant/frontend/discussions/category_choices
url: https://github.com/orgs/home-assistant/discussions
about: Request a new feature for the Home Assistant frontend.
- name: Report a bug that is NOT related to the UI / Dashboards
url: https://github.com/home-assistant/core/issues

53
.github/ISSUE_TEMPLATE/task.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: Task
description: For staff only - Create a task
type: Task
body:
- type: markdown
attributes:
value: |
## ⚠️ RESTRICTED ACCESS
**This form is restricted to Open Home Foundation staff and authorized contributors only.**
If you are a community member wanting to contribute, please:
- For bug reports: Use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml)
- For feature requests: Submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)
---
### For authorized contributors
Use this form to create tasks for development work, improvements, or other actionable items that need to be tracked.
- type: textarea
id: description
attributes:
label: Description
description: |
Provide a clear and detailed description of the task that needs to be accomplished.
Be specific about what needs to be done, why it's important, and any constraints or requirements.
placeholder: |
Describe the task, including:
- What needs to be done
- Why this task is needed
- Expected outcome
- Any constraints or requirements
validations:
required: true
- type: textarea
id: additional_context
attributes:
label: Additional context
description: |
Any additional information, links, research, or context that would be helpful.
Include links to related issues, research, prototypes, roadmap opportunities etc.
placeholder: |
- Roadmap opportunity: [link]
- Epic: [link]
- Feature request: [link]
- Technical design documents: [link]
- Prototype/mockup: [link]
- Dependencies: [links]
validations:
required: false

592
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,592 @@
# GitHub Copilot & Claude Code Instructions
You are an assistant helping with development of the Home Assistant frontend. The frontend is built using Lit-based Web Components and TypeScript, providing a responsive and performant interface for home automation control.
## Table of Contents
- [Quick Reference](#quick-reference)
- [Core Architecture](#core-architecture)
- [Development Standards](#development-standards)
- [Component Library](#component-library)
- [Common Patterns](#common-patterns)
- [Text and Copy Guidelines](#text-and-copy-guidelines)
- [Development Workflow](#development-workflow)
- [Review Guidelines](#review-guidelines)
## Quick Reference
### Essential Commands
```bash
yarn lint # ESLint + Prettier + TypeScript + Lit
yarn format # Auto-fix ESLint + Prettier
yarn lint:types # TypeScript compiler
yarn test # Vitest
script/develop # Development server
```
### Component Prefixes
- `ha-` - Home Assistant components
- `hui-` - Lovelace UI components
- `dialog-` - Dialog components
### Import Patterns
```typescript
import type { HomeAssistant } from "../types";
import { fireEvent } from "../common/dom/fire_event";
import { showAlertDialog } from "../dialogs/generic/show-alert-dialog";
```
## Core Architecture
The Home Assistant frontend is a modern web application that:
- Uses Web Components (custom elements) built with Lit framework
- Is written entirely in TypeScript with strict type checking
- Communicates with the backend via WebSocket API
- Provides comprehensive theming and internationalization
## Development Standards
### Code Quality Requirements
**Linting and Formatting (Enforced by Tools)**
- ESLint config extends Airbnb, TypeScript strict, Lit, Web Components, Accessibility
- Prettier with ES5 trailing commas enforced
- No console statements (`no-console: "error"`) - use proper logging
- Import organization: No unused imports, consistent type imports
**Naming Conventions**
- PascalCase for types and classes
- camelCase for variables, methods
- Private methods require leading underscore
- Public methods forbid leading underscore
### TypeScript Usage
- **Always use strict TypeScript**: Enable all strict flags, avoid `any` types
- **Proper type imports**: Use `import type` for type-only imports
- **Define interfaces**: Create proper interfaces for data structures
- **Type component properties**: All Lit properties must be properly typed
- **No unused variables**: Prefix with `_` if intentionally unused
- **Consistent imports**: Use `@typescript-eslint/consistent-type-imports`
```typescript
// Good
import type { HomeAssistant } from "../types";
interface EntityConfig {
entity: string;
name?: string;
}
@property({ type: Object })
hass!: HomeAssistant;
// Bad
@property()
hass: any;
```
### Web Components with Lit
- **Use Lit 3.x patterns**: Follow modern Lit practices
- **Extend appropriate base classes**: Use `LitElement`, `SubscribeMixin`, or other mixins as needed
- **Define custom element names**: Use `ha-` prefix for components
```typescript
@customElement("ha-my-component")
export class HaMyComponent extends LitElement {
@property({ attribute: false })
hass!: HomeAssistant;
@state()
private _config?: MyComponentConfig;
static get styles() {
return css`
:host {
display: block;
}
`;
}
render() {
return html`<div>Content</div>`;
}
}
```
### Component Guidelines
- **Use composition**: Prefer composition over inheritance
- **Lazy load panels**: Heavy panels should be dynamically imported
- **Optimize renders**: Use `@state()` for internal state, `@property()` for public API
- **Handle loading states**: Always show appropriate loading indicators
- **Support themes**: Use CSS custom properties from theme
### Data Management
- **Use WebSocket API**: All backend communication via home-assistant-js-websocket
- **Cache appropriately**: Use collections and caching for frequently accessed data
- **Handle errors gracefully**: All API calls should have error handling
- **Update real-time**: Subscribe to state changes for live updates
```typescript
// Good
try {
const result = await fetchEntityRegistry(this.hass.connection);
this._processResult(result);
} catch (err) {
showAlertDialog(this, {
text: `Failed to load: ${err.message}`,
});
}
```
### Styling Guidelines
- **Use CSS custom properties**: Leverage the theme system
- **Mobile-first responsive**: Design for mobile, enhance for desktop
- **Follow Material Design**: Use Material Web Components where appropriate
- **Support RTL**: Ensure all layouts work in RTL languages
```typescript
static get styles() {
return css`
:host {
--spacing: 16px;
padding: var(--spacing);
color: var(--primary-text-color);
background-color: var(--card-background-color);
}
@media (max-width: 600px) {
:host {
--spacing: 8px;
}
}
`;
}
```
### Performance Best Practices
- **Code split**: Split code at the panel/dialog level
- **Lazy load**: Use dynamic imports for heavy components
- **Optimize bundle**: Keep initial bundle size minimal
- **Use virtual scrolling**: For long lists, implement virtual scrolling
- **Memoize computations**: Cache expensive calculations
### Testing Requirements
- **Write tests**: Add tests for data processing and utilities
- **Test with Vitest**: Use the established test framework
- **Mock appropriately**: Mock WebSocket connections and API calls
- **Test accessibility**: Ensure components are accessible
## Component Library
### Dialog Components
**Available Dialog Types:**
- `ha-md-dialog` - Preferred for new code (Material Design 3)
- `ha-dialog` - Legacy component still widely used
**Opening Dialogs (Fire Event Pattern - Recommended):**
```typescript
fireEvent(this, "show-dialog", {
dialogTag: "dialog-example",
dialogImport: () => import("./dialog-example"),
dialogParams: { title: "Example", data: someData },
});
```
**Dialog Implementation Requirements:**
- Implement `HassDialog<T>` interface
- Use `createCloseHeading()` for standard headers
- Import `haStyleDialog` for consistent styling
- Return `nothing` when no params (loading state)
- Fire `dialog-closed` event when closing
- Add `dialogInitialFocus` for accessibility
````
### Form Component (ha-form)
- Schema-driven using `HaFormSchema[]`
- Supports entity, device, area, target, number, boolean, time, action, text, object, select, icon, media, location selectors
- Built-in validation with error display
- Use `dialogInitialFocus` in dialogs
- Use `computeLabel`, `computeError`, `computeHelper` for translations
```typescript
<ha-form
.hass=${this.hass}
.data=${this._data}
.schema=${this._schema}
.error=${this._errors}
.computeLabel=${(schema) => this.hass.localize(`ui.panel.${schema.name}`)}
@value-changed=${this._valueChanged}
></ha-form>
````
### Alert Component (ha-alert)
- Types: `error`, `warning`, `info`, `success`
- Properties: `title`, `alert-type`, `dismissable`, `icon`, `action`, `rtl`
- Content announced by screen readers when dynamically displayed
```html
<ha-alert alert-type="error">Error message</ha-alert>
<ha-alert alert-type="warning" title="Warning">Description</ha-alert>
<ha-alert alert-type="success" dismissable>Success message</ha-alert>
```
## Common Patterns
### Creating a Panel
```typescript
@customElement("ha-panel-myfeature")
export class HaPanelMyFeature extends SubscribeMixin(LitElement) {
@property({ attribute: false })
hass!: HomeAssistant;
@property({ type: Boolean, reflect: true })
narrow!: boolean;
@property()
route!: Route;
hassSubscribe() {
return [
subscribeEntityRegistry(this.hass.connection, (entities) => {
this._entities = entities;
}),
];
}
}
```
### Creating a Dialog
```typescript
@customElement("dialog-my-feature")
export class DialogMyFeature
extends LitElement
implements HassDialog<MyDialogParams>
{
@property({ attribute: false })
hass!: HomeAssistant;
@state()
private _params?: MyDialogParams;
public async showDialog(params: MyDialogParams): Promise<void> {
this._params = params;
}
public closeDialog(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(this.hass, this._params.title)}
>
<!-- Dialog content -->
<ha-button @click=${this.closeDialog} slot="secondaryAction">
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button @click=${this._submit} slot="primaryAction">
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog>
`;
}
static styles = [haStyleDialog, css``];
}
```
### Dialog Design Guidelines
- Max width: 560px (Alert/confirmation: 320px fixed width)
- Close X-icon on top left (all screen sizes)
- Submit button grouped with cancel at bottom right
- Keep button labels short: "Save", "Delete", "Enable"
- Destructive actions use red warning button
- Always use a title (best practice)
- Strive for minimalism
#### Creating a Lovelace Card
**Purpose**: Cards allow users to tell different stories about their house (based on gallery)
```typescript
@customElement("hui-my-card")
export class HuiMyCard extends LitElement implements LovelaceCard {
@property({ attribute: false })
hass!: HomeAssistant;
@state()
private _config?: MyCardConfig;
public setConfig(config: MyCardConfig): void {
if (!config.entity) {
throw new Error("Entity required");
}
this._config = config;
}
public getCardSize(): number {
return 3; // Height in grid units
}
// Optional: Editor for card configuration
public static getConfigElement(): LovelaceCardEditor {
return document.createElement("hui-my-card-editor");
}
// Optional: Stub config for card picker
public static getStubConfig(): object {
return { entity: "" };
}
}
```
**Card Guidelines:**
- Cards are highly customizable for different households
- Implement `LovelaceCard` interface with `setConfig()` and `getCardSize()`
- Use proper error handling in `setConfig()`
- Consider all possible states (loading, error, unavailable)
- Support different entity types and states
- Follow responsive design principles
- Add configuration editor when needed
### Internationalization
- **Use localize**: Always use the localization system
- **Add translation keys**: Add keys to src/translations/en.json
- **Support placeholders**: Use proper placeholder syntax
```typescript
this.hass.localize("ui.panel.config.updates.update_available", {
count: 5,
});
```
### Accessibility
- **ARIA labels**: Add appropriate ARIA labels
- **Keyboard navigation**: Ensure all interactions work with keyboard
- **Screen reader support**: Test with screen readers
- **Color contrast**: Meet WCAG AA standards
## Development Workflow
### Setup and Commands
1. **Setup**: `script/setup` - Install dependencies
2. **Develop**: `script/develop` - Development server
3. **Lint**: `yarn lint` - Run all linting before committing
4. **Test**: `yarn test` - Add and run tests
5. **Build**: `script/build_frontend` - Test production build
### Common Pitfalls to Avoid
- Don't use `querySelector` - Use refs or component properties
- Don't manipulate DOM directly - Let Lit handle rendering
- Don't use global styles - Scope styles to components
- Don't block the main thread - Use web workers for heavy computation
- Don't ignore TypeScript errors - Fix all type issues
### Security Best Practices
- Sanitize HTML - Never use `unsafeHTML` with user content
- Validate inputs - Always validate user inputs
- Use HTTPS - All external resources must use HTTPS
- CSP compliance - Ensure code works with Content Security Policy
### Text and Copy Guidelines
#### Terminology Standards
**Delete vs Remove** (Based on gallery/src/pages/Text/remove-delete-add-create.markdown)
- **Use "Remove"** for actions that can be restored or reapplied:
- Removing a user's permission
- Removing a user from a group
- Removing links between items
- Removing a widget from dashboard
- Removing an item from a cart
- **Use "Delete"** for permanent, non-recoverable actions:
- Deleting a field
- Deleting a value in a field
- Deleting a task
- Deleting a group
- Deleting a permission
- Deleting a calendar event
**Create vs Add** (Create pairs with Delete, Add pairs with Remove)
- **Use "Add"** for already-existing items:
- Adding a permission to a user
- Adding a user to a group
- Adding links between items
- Adding a widget to dashboard
- Adding an item to a cart
- **Use "Create"** for something made from scratch:
- Creating a new field
- Creating a new task
- Creating a new group
- Creating a new permission
- Creating a new calendar event
#### Writing Style (Consistent with Home Assistant Documentation)
- **Use American English**: Standard spelling and terminology
- **Friendly, informational tone**: Be inspiring, personal, comforting, engaging
- **Address users directly**: Use "you" and "your"
- **Be inclusive**: Objective, non-discriminatory language
- **Be concise**: Use clear, direct language
- **Be consistent**: Follow established terminology patterns
- **Use active voice**: "Delete the automation" not "The automation should be deleted"
- **Avoid jargon**: Use terms familiar to home automation users
#### Language Standards
- **Always use "Home Assistant"** in full, never "HA" or "HASS"
- **Avoid abbreviations**: Spell out terms when possible
- **Use sentence case everywhere**: Titles, headings, buttons, labels, UI elements
- ✅ "Create new automation"
- ❌ "Create New Automation"
- ✅ "Device settings"
- ❌ "Device Settings"
- **Oxford comma**: Use in lists (item 1, item 2, and item 3)
- **Replace Latin terms**: Use "like" instead of "e.g.", "for example" instead of "i.e."
- **Avoid CAPS for emphasis**: Use bold or italics instead
- **Write for all skill levels**: Both technical and non-technical users
#### Key Terminology
- **"add-on"** (hyphenated, not "addon")
- **"integration"** (preferred over "component")
- **Technical terms**: Use lowercase (automation, entity, device, service)
#### Translation Considerations
- **Add translation keys**: All user-facing text must be translatable
- **Use placeholders**: Support dynamic content in translations
- **Keep context**: Provide enough context for translators
```typescript
// Good
this.hass.localize("ui.panel.config.automation.delete_confirm", {
name: automation.alias,
});
// Bad - hardcoded text
("Are you sure you want to delete this automation?");
```
### Common Review Issues (From PR Analysis)
#### User Experience and Accessibility
- **Form validation**: Always provide proper field labels and validation feedback
- **Form accessibility**: Prevent password managers from incorrectly identifying fields
- **Loading states**: Show clear progress indicators during async operations
- **Error handling**: Display meaningful error messages when operations fail
- **Mobile responsiveness**: Ensure components work well on small screens
- **Hit targets**: Make clickable areas large enough for touch interaction
- **Visual feedback**: Provide clear indication of interactive states
#### Dialog and Modal Patterns
- **Dialog width constraints**: Respect minimum and maximum width requirements
- **Interview progress**: Show clear progress for multi-step operations
- **State persistence**: Handle dialog state properly during background operations
- **Cancel behavior**: Ensure cancel/close buttons work consistently
- **Form prefilling**: Use smart defaults but allow user override
#### Component Design Patterns
- **Terminology consistency**: Use "Join"/"Apply" instead of "Group" when appropriate
- **Visual hierarchy**: Ensure proper font sizes and spacing ratios
- **Grid alignment**: Components should align to the design grid system
- **Badge placement**: Position badges and indicators consistently
- **Color theming**: Respect theme variables and design system colors
#### Code Quality Issues
- **Null checking**: Always check if entities exist before accessing properties
- **TypeScript safety**: Handle potentially undefined array/object access
- **Import organization**: Remove unused imports and use proper type imports
- **Event handling**: Properly subscribe and unsubscribe from events
- **Memory leaks**: Clean up subscriptions and event listeners
#### Configuration and Props
- **Optional parameters**: Make configuration fields optional when sensible
- **Smart defaults**: Provide reasonable default values
- **Future extensibility**: Design APIs that can be extended later
- **Validation**: Validate configuration before applying changes
## Review Guidelines
### Core Requirements Checklist
- [ ] TypeScript strict mode passes (`yarn lint:types`)
- [ ] No ESLint errors or warnings (`yarn lint:eslint`)
- [ ] Prettier formatting applied (`yarn lint:prettier`)
- [ ] Lit analyzer passes (`yarn lint:lit`)
- [ ] Component follows Lit best practices
- [ ] Proper error handling implemented
- [ ] Loading states handled
- [ ] Mobile responsive
- [ ] Theme variables used
- [ ] Translations added
- [ ] Accessible to screen readers
- [ ] Tests added (where applicable)
- [ ] No console statements (use proper logging)
- [ ] Unused imports removed
- [ ] Proper naming conventions
### Text and Copy Checklist
- [ ] Follows terminology guidelines (Delete vs Remove, Create vs Add)
- [ ] Localization keys added for all user-facing text
- [ ] Uses "Home Assistant" (never "HA" or "HASS")
- [ ] Sentence case for ALL text (titles, headings, buttons, labels)
- [ ] American English spelling
- [ ] Friendly, informational tone
- [ ] Avoids abbreviations and jargon
- [ ] Correct terminology (add-on not addon, integration not component)
### Component-Specific Checks
- [ ] Dialogs implement HassDialog interface
- [ ] Dialog styling uses haStyleDialog
- [ ] Dialog accessibility includes dialogInitialFocus
- [ ] ha-alert used correctly for messages
- [ ] ha-form uses proper schema structure
- [ ] Components handle all states (loading, error, unavailable)
- [ ] Entity existence checked before property access
- [ ] Event subscriptions properly cleaned up

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@v2.2.0
uses: relative-ci/agent-action@v3.0.0
with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}

View File

@@ -55,7 +55,7 @@ jobs:
script/release
- name: Upload release assets
uses: softprops/action-gh-release@v2.2.2
uses: softprops/action-gh-release@v2.3.2
with:
files: |
dist/*.whl
@@ -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.2
uses: softprops/action-gh-release@v2.3.2
with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
@@ -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.2
uses: softprops/action-gh-release@v2.3.2
with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz

View File

@@ -0,0 +1,58 @@
name: Restrict task creation
# yamllint disable-line rule:truthy
on:
issues:
types: [opened]
jobs:
check-authorization:
runs-on: ubuntu-latest
# Only run if this is a Task issue type (from the issue form)
if: github.event.issue.issue_type == 'Task'
steps:
- name: Check if user is authorized
uses: actions/github-script@v7
with:
script: |
const issueAuthor = context.payload.issue.user.login;
// Check if user is an organization member
try {
await github.rest.orgs.checkMembershipForUser({
org: 'home-assistant',
username: issueAuthor
});
console.log(`✅ ${issueAuthor} is an organization member`);
return; // Authorized
} catch (error) {
console.log(`❌ ${issueAuthor} is not authorized to create Task issues`);
}
// Close the issue with a comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `Hi @${issueAuthor}, thank you for your contribution!\n\n` +
`Task issues are restricted to Open Home Foundation staff and authorized contributors.\n\n` +
`If you would like to:\n` +
`- Report a bug: Please use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml)\n` +
`- Request a feature: Please submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)\n\n` +
`If you believe you should have access to create Task issues, please contact the maintainers.`
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: 'closed'
});
// Add a label to indicate this was auto-closed
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['auto-closed']
});

4
.gitignore vendored
View File

@@ -53,3 +53,7 @@ src/cast/dev_const.ts
# test coverage
test/coverage/
# AI tooling
.claude

View File

@@ -1 +1 @@
yarn run lint-staged --relative --shell "/bin/bash"
yarn run lint-staged --relative

View File

@@ -1,18 +0,0 @@
diff --git a/dist/hls.light.mjs b/dist/hls.light.mjs
index eed9d788fafdb159975e1a2eb08ac88ba9c9ac33..ace881935e6665946f1c8110ebd2f739cde4427e 100644
--- a/dist/hls.light.mjs
+++ b/dist/hls.light.mjs
@@ -20523,9 +20523,9 @@ class Hls {
}
Hls.defaultConfig = void 0;
-var KeySystemFormats = empty.KeySystemFormats;
-var KeySystems = empty.KeySystems;
-var SubtitleStreamController = empty.SubtitleStreamController;
-var TimelineController = empty.TimelineController;
+var KeySystemFormats = empty;
+var KeySystems = empty;
+var SubtitleStreamController = empty;
+var TimelineController = empty;
export { AbrController, AttrList, Cues as AudioStreamController, Cues as AudioTrackController, BasePlaylistController, BaseSegment, BaseStreamController, BufferController, Cues as CMCDController, CapLevelController, ChunkMetadata, ContentSteeringController, DateRange, Cues as EMEController, ErrorActionFlags, ErrorController, ErrorDetails, ErrorTypes, Events, FPSController, Fragment, Hls, HlsSkip, HlsUrlParameters, KeySystemFormats, KeySystems, Level, LevelDetails, LevelKey, LoadStats, MetadataSchema, NetworkErrorAction, Part, PlaylistLevelType, SubtitleStreamController, Cues as SubtitleTrackController, TimelineController, Hls as default, getMediaSource, isMSESupported, isSupported };
//# sourceMappingURL=hls.light.mjs.map

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.9.1.cjs
yarnPath: .yarn/releases/yarn-4.9.2.cjs

1
CLAUDE.md Symbolic link
View File

@@ -0,0 +1 @@
.github/copilot-instructions.md

View File

@@ -302,7 +302,7 @@ export class HcConnect extends LitElement {
}
.error {
color: red;
font-weight: bold;
font-weight: var(--ha-font-weight-bold);
}
.error a {

View File

@@ -86,9 +86,9 @@ class HcLayout extends LitElement {
.card-header {
color: var(--ha-card-header-color, var(--primary-text-color));
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
letter-spacing: -0.012em;
line-height: 32px;
line-height: var(--ha-line-height-condensed);
padding: 24px 16px 16px;
display: block;
margin: 0;
@@ -98,7 +98,7 @@ class HcLayout extends LitElement {
border-radius: 4px 4px 0 0;
}
.subtitle {
font-size: 14px;
font-size: var(--ha-font-size-m);
color: var(--secondary-text-color);
line-height: initial;
}
@@ -113,7 +113,7 @@ class HcLayout extends LitElement {
}
:host ::slotted(.section-header) {
font-weight: 500;
font-weight: var(--ha-font-weight-medium);
padding: 4px 16px;
text-transform: uppercase;
}
@@ -135,7 +135,7 @@ class HcLayout extends LitElement {
.footer {
text-align: center;
font-size: 12px;
font-size: var(--ha-font-size-s);
padding: 8px 0 24px;
color: var(--secondary-text-color);
}

View File

@@ -29,7 +29,7 @@ class HcLaunchScreen extends LitElement {
display: block;
height: 100vh;
background-color: #f2f4f9;
font-size: 24px;
font-size: var(--ha-font-size-2xl);
}
.container {
display: flex;

View File

@@ -68,7 +68,7 @@
}
#ha-launch-screen .ha-launch-screen-spacer-top {
flex: 1;
margin-top: calc( 2 * max(env(safe-area-inset-bottom), 48px) + 46px );
margin-top: calc( 2 * max(var(--safe-area-inset-bottom), 48px) + 46px );
padding-top: 48px;
}
#ha-launch-screen .ha-launch-screen-spacer-bottom {
@@ -76,7 +76,7 @@
padding-top: 48px;
}
.ohf-logo {
margin: max(env(safe-area-inset-bottom), 48px) 0;
margin: max(var(--safe-area-inset-bottom), 48px) 0;
display: flex;
flex-direction: column;
align-items: center;

View File

@@ -1,7 +1,30 @@
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
let changeFunction;
export const mockFrontend = (hass: MockHomeAssistant) => {
hass.mockWS("frontend/get_user_data", () => ({
value: null,
}));
hass.mockWS("frontend/set_user_data", ({ key, value }) => {
if (key === "sidebar") {
changeFunction?.({
value: {
panelOrder: value.panelOrder || [],
hiddenPanels: value.hiddenPanels || [],
},
});
}
});
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
changeFunction = onChange;
onChange?.({
value: {
panelOrder: [],
hiddenPanels: [],
},
});
// eslint-disable-next-line @typescript-eslint/no-empty-function
return () => {};
});
};

View File

@@ -11,6 +11,7 @@ import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier";
import { configs as litConfigs } from "eslint-plugin-lit";
import { configs as wcConfigs } from "eslint-plugin-wc";
import { configs as a11yConfigs } from "eslint-plugin-lit-a11y";
const _filename = fileURLToPath(import.meta.url);
const _dirname = path.dirname(_filename);
@@ -21,13 +22,14 @@ const compat = new FlatCompat({
});
export default tseslint.config(
...compat.extends("airbnb-base", "plugin:lit-a11y/recommended"),
...compat.extends("airbnb-base"),
eslintConfigPrettier,
litConfigs["flat/all"],
tseslint.configs.recommended,
tseslint.configs.strict,
tseslint.configs.stylistic,
wcConfigs["flat/recommended"],
a11yConfigs.recommended,
{
plugins: {
"unused-imports": unusedImports,

View File

@@ -38,12 +38,12 @@ class PageDescription extends HaMarkdown {
}
.title {
font-size: 42px;
line-height: 56px;
line-height: var(--ha-line-height-condensed);
padding-bottom: 8px;
}
.subtitle {
font-size: 18px;
line-height: 24px;
font-size: var(--ha-font-size-l);
line-height: var(--ha-line-height-normal);
}
.root {
max-width: 800px;

View File

@@ -34,7 +34,7 @@ class HaDemoOptions extends LitElement {
height: 64px;
padding: 0 16px;
pointer-events: none;
font-size: 20px;
font-size: var(--ha-font-size-xl);
}
`,
];

View File

@@ -250,14 +250,14 @@ class HaGallery extends LitElement {
}
.page-footer .header {
font-size: 16px;
font-weight: 500;
line-height: 28px;
font-size: var(--ha-font-size-l);
font-weight: var(--ha-font-weight-medium);
line-height: var(--ha-line-height-normal);
text-align: center;
}
.page-footer .secondary {
line-height: 23px;
line-height: var(--ha-line-height-normal);
text-align: center;
}

View File

@@ -150,7 +150,7 @@ export class DemoHaBarButton extends LitElement {
margin: 0;
}
label {
font-weight: 600;
font-weight: var(--ha-font-weight-bold);
}
.custom {
--control-button-icon-color: var(--primary-color);

View File

@@ -86,7 +86,7 @@ export class DemoHarControlNumberButtons extends LitElement {
margin: 0;
}
label {
font-weight: 600;
font-weight: var(--ha-font-weight-bold);
}
.custom {
color: #2196f3;

View File

@@ -125,7 +125,7 @@ export class DemoHaControlSelectMenu extends LitElement {
margin: 0;
}
label {
font-weight: 600;
font-weight: var(--ha-font-weight-bold);
}
.custom {
--control-button-icon-color: var(--primary-color);

View File

@@ -181,7 +181,7 @@ export class DemoHaControlSelect extends LitElement {
margin: 0;
}
label {
font-weight: 600;
font-weight: var(--ha-font-weight-bold);
}
.custom {
--mdc-icon-size: 24px;

View File

@@ -144,7 +144,7 @@ export class DemoHaBarSlider extends LitElement {
margin: 0;
}
label {
font-weight: 600;
font-weight: var(--ha-font-weight-bold);
}
.custom {
--control-slider-color: #ffcf4c;

View File

@@ -112,7 +112,7 @@ export class DemoHaControlSwitch extends LitElement {
margin: 0;
}
label {
font-weight: 600;
font-weight: var(--ha-font-weight-bold);
}
.custom {
--control-switch-on-color: var(--green-color);

View File

@@ -105,8 +105,8 @@ export class DemoHaHsColorPicker extends LitElement {
width: 400px;
}
.value {
font-size: 22px;
font-weight: bold;
font-size: var(--ha-font-size-xl);
font-weight: var(--ha-font-weight-bold);
margin: 0 0 12px 0;
}
`;

View File

@@ -123,7 +123,7 @@ export class DemoHaSelectBox extends LitElement {
margin: 0;
}
label {
font-weight: 600;
font-weight: var(--ha-font-weight-bold);
margin-bottom: 8px;
display: block;
}

View File

@@ -416,6 +416,34 @@ const SCHEMAS: {
},
},
},
items: {
name: "Items",
selector: {
object: {
label_field: "name",
description_field: "value",
multiple: true,
fields: {
name: {
label: "Name",
selector: { text: {} },
required: true,
},
value: {
label: "Value",
selector: {
number: {
mode: "slider",
min: 0,
max: 100,
unit_of_measurement: "%",
},
},
},
},
},
},
},
},
},
];

View File

@@ -1,6 +1,7 @@
import type { TemplateResult } from "lit";
import { html, css, LitElement } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/ha-bar";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-spinner";
@@ -11,29 +12,66 @@ export class DemoHaSpinner extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
protected render(): TemplateResult {
return html`<ha-card header="Basic spinner">
<div class="card-content">
<ha-spinner></ha-spinner></div
></ha-card>
<ha-card header="Different spinner sizes">
<div class="card-content">
<ha-spinner size="tiny"></ha-spinner>
<ha-spinner size="small"></ha-spinner>
<ha-spinner size="medium"></ha-spinner>
<ha-spinner size="large"></ha-spinner></div
></ha-card>
<ha-card header="Spinner with an aria-label">
<div class="card-content">
<ha-spinner aria-label="Doing something..."></ha-spinner>
<ha-spinner .ariaLabel=${"Doing something..."}></ha-spinner></div
></ha-card>`;
return html`
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-badge ${mode} demo">
<div class="card-content">
<ha-spinner></ha-spinner>
<ha-spinner size="tiny"></ha-spinner>
<ha-spinner size="small"></ha-spinner>
<ha-spinner size="medium"></ha-spinner>
<ha-spinner size="large"></ha-spinner>
<ha-spinner aria-label="Doing something..."></ha-spinner>
<ha-spinner .ariaLabel=${"Doing something..."}></ha-spinner>
</div>
</ha-card>
</div>
`
)}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
static styles = css`
:host {
display: flex;
justify-content: center;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
margin: 16px;
border-radius: 8px;
}
ha-card {
max-width: 600px;
margin: 24px auto;
}
.card-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
`;
}

View File

@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeNumeric extends LitElement {
margin: 12px auto;
}
.header {
font-weight: bold;
font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;

View File

@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeSeconds extends LitElement {
margin: 12px auto;
}
.header {
font-weight: bold;
font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;

View File

@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeShortYear extends LitElement {
margin: 12px auto;
}
.header {
font-weight: bold;
font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;

View File

@@ -106,7 +106,7 @@ export class DemoDateTimeDateTimeShort extends LitElement {
margin: 12px auto;
}
.header {
font-weight: bold;
font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;

View File

@@ -106,7 +106,7 @@ export class DemoDateTimeDateTime extends LitElement {
margin: 12px auto;
}
.header {
font-weight: bold;
font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;

View File

@@ -92,7 +92,7 @@ export class DemoDateTimeDate extends LitElement {
static styles = css`
.header {
font-weight: bold;
font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;

View File

@@ -106,7 +106,7 @@ export class DemoDateTimeTimeSeconds extends LitElement {
margin: 12px auto;
}
.header {
font-weight: bold;
font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;

View File

@@ -106,7 +106,7 @@ export class DemoDateTimeTimeWeekday extends LitElement {
margin: 12px auto;
}
.header {
font-weight: bold;
font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;

View File

@@ -106,7 +106,7 @@ export class DemoDateTimeTime extends LitElement {
margin: 12px auto;
}
.header {
font-weight: bold;
font-weight: var(--ha-font-weight-bold);
}
.center {
text-align: center;

View File

@@ -428,13 +428,13 @@ class HassioAddonConfig extends LitElement {
.header h2 {
color: var(--ha-card-header-color, var(--primary-text-color));
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
letter-spacing: -0.012em;
line-height: 48px;
line-height: var(--ha-line-height-expanded);
padding: 12px 16px 16px;
display: block;
margin-block: 0px;
font-weight: normal;
font-weight: var(--ha-font-weight-normal);
}
.card-actions.right {
justify-content: flex-end;

View File

@@ -1280,12 +1280,12 @@ class HassioAddonInfo extends LitElement {
padding-left: 8px;
padding-inline-start: 8px;
padding-inline-end: initial;
font-size: 24px;
font-size: var(--ha-font-size-2xl);
color: var(--ha-card-header-color, var(--primary-text-color));
}
.addon-version {
float: var(--float-end);
font-size: 15px;
font-size: var(--ha-font-size-l);
vertical-align: middle;
}
.errors {

View File

@@ -391,7 +391,7 @@ export class HassioBackups extends LitElement {
top: -4px;
}
.selected-txt {
font-weight: bold;
font-weight: var(--ha-font-weight-bold);
padding-left: 16px;
padding-inline-start: 16px;
padding-inline-end: initial;
@@ -401,7 +401,7 @@ export class HassioBackups extends LitElement {
margin-top: 20px;
}
.header-toolbar .selected-txt {
font-size: 16px;
font-size: var(--ha-font-size-l);
}
.header-toolbar .header-btns {
margin-right: -12px;

View File

@@ -101,7 +101,7 @@ class HassioCardContent extends LitElement {
overflow: hidden;
position: relative;
height: 2.4em;
line-height: 1.2em;
line-height: var(--ha-line-height-condensed);
}
.icon_image img {
max-height: 40px;

View File

@@ -132,9 +132,9 @@ class HassioDashboard extends LitElement {
}
ha-fab.non-tabs {
position: fixed;
right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom));
inset-inline-end: calc(16px + env(safe-area-inset-right));
right: calc(16px + var(--safe-area-inset-right));
bottom: calc(16px + var(--safe-area-inset-bottom));
inset-inline-end: calc(16px + var(--safe-area-inset-right));
inset-inline-start: initial;
z-index: 1;
}

View File

@@ -131,7 +131,7 @@ export class HassioUpdate extends LitElement {
}
.update-heading {
font-size: var(--ha-font-size-l);
font-weight: 500;
font-weight: var(--ha-font-weight-medium);
margin-bottom: 0.5em;
color: var(--primary-text-color);
}

View File

@@ -173,7 +173,7 @@ class HassioHardwareDialog extends LitElement {
font-family: var(--ha-font-family-code);
}
code {
font-size: 85%;
font-size: var(--ha-font-size-s);
padding: 0.2em 0.4em;
}
search-input {

View File

@@ -610,7 +610,7 @@ export class DialogHassioNetwork
display: flex;
justify-content: space-between;
padding: 8px;
padding-bottom: max(env(safe-area-inset-bottom), 8px);
padding-bottom: max(var(--safe-area-inset-bottom), 8px);
background-color: var(--mdc-theme-surface, #fff);
}
.warning {

View File

@@ -1,3 +1,8 @@
import {
haFontFamilyBody,
haFontSmoothing,
haMozOsxFontSmoothing,
} from "../../src/resources/theme/typography.globals";
import "./hassio-main";
import("../../src/resources/append-ha-style");
@@ -5,10 +10,10 @@ import("../../src/resources/append-ha-style");
const styleEl = document.createElement("style");
styleEl.textContent = `
body {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-weight: 400;
font-family: ${haFontFamilyBody};
-moz-osx-font-smoothing: ${haMozOsxFontSmoothing};
-webkit-font-smoothing: ${haFontSmoothing};
font-weight: var(--ha-font-weight-normal);
margin: 0;
padding: 0;
height: 100vh;

View File

@@ -340,12 +340,12 @@ class HassioIngressView extends LitElement {
.header {
display: flex;
align-items: center;
font-size: 16px;
font-size: var(--ha-font-size-l);
height: 40px;
padding: 0 16px;
pointer-events: none;
background-color: var(--app-header-background-color);
font-weight: 400;
font-weight: var(--ha-font-weight-normal);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
box-sizing: border-box;
@@ -354,7 +354,7 @@ class HassioIngressView extends LitElement {
.main-title {
margin: var(--margin-title);
line-height: 20px;
line-height: var(--ha-line-height-condensed);
flex-grow: 1;
}

View File

@@ -14,6 +14,7 @@ export const hassioStyle = css`
margin-bottom: 8px;
font-family: var(--ha-font-family-body);
-webkit-font-smoothing: var(--ha-font-smoothing);
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
font-size: var(--ha-font-size-2xl);
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-condensed);

View File

@@ -4,7 +4,7 @@ export default {
"prettier --cache --write",
"lit-analyzer --quiet",
],
"*.{json,css,md,markdown,html,y?aml}": "prettier --cache --write",
"*.{json,css,md,markdown,html,ya?ml}": "prettier --cache --write",
"translations/*/*.json": (files) =>
'printf "%s\n" "Translation files should not be added or modified here. Instead, make the necessary modifications in src/translations/en.json. Other languages are managed externally. Please see https://developers.home-assistant.io/docs/translations/ for details." ' +
files.join(" ") +

View File

@@ -26,15 +26,15 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.27.1",
"@babel/runtime": "7.27.6",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.6",
"@codemirror/commands": "6.8.1",
"@codemirror/language": "6.11.0",
"@codemirror/language": "6.11.2",
"@codemirror/legacy-modes": "6.5.1",
"@codemirror/search": "6.5.10",
"@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.36.7",
"@codemirror/view": "6.38.0",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.0",
"@formatjs/intl-displaynames": "6.8.11",
@@ -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.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",
"@fullcalendar/core": "6.1.18",
"@fullcalendar/daygrid": "6.1.18",
"@fullcalendar/interaction": "6.1.18",
"@fullcalendar/list": "6.1.18",
"@fullcalendar/luxon3": "6.1.18",
"@fullcalendar/timegrid": "6.1.18",
"@lezer/highlight": "1.2.1",
"@lit-labs/motion": "1.0.8",
"@lit-labs/observers": "2.0.5",
@@ -89,17 +89,17 @@
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.8.1",
"@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.7.5",
"@vaadin/vaadin-themable-mixin": "24.7.5",
"@vaadin/combo-box": "24.7.9",
"@vaadin/vaadin-themable-mixin": "24.7.9",
"@vibrant/color": "4.0.0",
"@vue/web-component-wrapper": "1.3.0",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "3.0.1",
"barcode-detector": "3.0.5",
"color-name": "2.0.0",
"comlink": "4.4.2",
"core-js": "3.42.0",
"core-js": "3.44.0",
"cropperjs": "1.6.2",
"date-fns": "4.1.0",
"date-fns-tz": "3.2.0",
@@ -111,9 +111,9 @@
"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",
"hls.js": "1.6.6",
"home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.1",
"idb-keyval": "6.2.2",
"intl-messageformat": "10.7.16",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
@@ -122,7 +122,7 @@
"lit": "3.3.0",
"lit-html": "3.3.0",
"luxon": "3.6.1",
"marked": "15.0.11",
"marked": "16.0.0",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
@@ -135,9 +135,8 @@
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
"ua-parser-js": "2.0.3",
"vis-data": "7.1.9",
"vis-network": "9.1.9",
"ua-parser-js": "2.0.4",
"vis-data": "7.1.10",
"vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
@@ -150,27 +149,26 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.27.1",
"@babel/helper-define-polyfill-provider": "0.6.4",
"@babel/plugin-transform-runtime": "7.27.1",
"@babel/preset-env": "7.27.1",
"@bundle-stats/plugin-webpack-filter": "4.20.0",
"@lokalise/node-api": "14.5.0",
"@octokit/auth-oauth-device": "7.1.5",
"@octokit/plugin-retry": "7.2.1",
"@octokit/rest": "21.1.1",
"@rsdoctor/rspack-plugin": "1.0.2",
"@rspack/cli": "1.3.8",
"@rspack/core": "1.3.8",
"@babel/core": "7.28.0",
"@babel/helper-define-polyfill-provider": "0.6.5",
"@babel/plugin-transform-runtime": "7.28.0",
"@babel/preset-env": "7.28.0",
"@bundle-stats/plugin-webpack-filter": "4.21.0",
"@lokalise/node-api": "14.9.1",
"@octokit/auth-oauth-device": "8.0.1",
"@octokit/plugin-retry": "8.0.1",
"@octokit/rest": "22.0.0",
"@rsdoctor/rspack-plugin": "1.1.8",
"@rspack/cli": "1.4.4",
"@rspack/core": "1.4.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.21",
"@types/chromecast-caf-receiver": "6.0.22",
"@types/chromecast-caf-sender": "1.0.11",
"@types/color-name": "2.0.0",
"@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.17",
"@types/leaflet-draw": "1.0.11",
"@types/leaflet": "1.9.19",
"@types/leaflet-draw": "1.0.12",
"@types/leaflet.markercluster": "1.5.5",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.6.2",
@@ -180,48 +178,48 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "3.1.2",
"@vitest/coverage-v8": "3.2.4",
"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.26.0",
"eslint": "9.30.1",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.2",
"eslint-config-prettier": "10.1.5",
"eslint-import-resolver-webpack": "0.13.10",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-lit": "2.1.1",
"eslint-plugin-lit-a11y": "4.1.4",
"eslint-plugin-lit-a11y": "5.1.0",
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "3.0.1",
"fancy-log": "2.0.0",
"fs-extra": "11.3.0",
"glob": "11.0.2",
"gulp": "5.0.0",
"glob": "11.0.3",
"gulp": "5.0.1",
"gulp-brotli": "3.0.0",
"gulp-json-transform": "0.5.0",
"gulp-rename": "2.0.0",
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "26.1.0",
"jszip": "3.10.1",
"lint-staged": "15.5.1",
"lint-staged": "16.1.2",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
"map-stream": "0.0.7",
"pinst": "3.0.0",
"prettier": "3.5.3",
"prettier": "3.6.2",
"rspack-manifest-plugin": "5.0.3",
"serve": "14.2.4",
"sinon": "20.0.0",
"sinon": "21.0.0",
"tar": "7.4.3",
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.8.3",
"typescript-eslint": "8.31.1",
"typescript-eslint": "8.35.1",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.1.2",
"vitest": "3.2.4",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
@@ -232,10 +230,10 @@
"lit-html": "3.3.0",
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.0",
"@fullcalendar/daygrid": "6.1.17",
"globals": "16.0.0",
"@fullcalendar/daygrid": "6.1.18",
"globals": "16.3.0",
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
},
"packageManager": "yarn@4.9.1"
"packageManager": "yarn@4.9.2"
}

View File

@@ -0,0 +1,36 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1738_5532)">
<path d="M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V4C20 6.20914 18.2091 8 16 8H4C1.79086 8 0 6.20914 0 4V4Z" fill="white" fill-opacity="0.48"/>
<path d="M40 4C40 1.79086 41.7909 0 44 0V0C46.2091 0 48 1.79086 48 4V4C48 6.20914 46.2091 8 44 8V8C41.7909 8 40 6.20914 40 4V4Z" fill="white" fill-opacity="0.24"/>
<path d="M0 20C0 15.5817 3.58172 12 8 12H40C44.4183 12 48 15.5817 48 20V64C48 68.4183 44.4183 72 40 72H8C3.58172 72 0 68.4183 0 64V20Z" fill="#1C1C1C"/>
<path d="M8 12.5H40C44.1421 12.5 47.5 15.8579 47.5 20V64C47.5 68.1421 44.1421 71.5 40 71.5H8C3.85787 71.5 0.5 68.1421 0.5 64V20L0.509766 19.6143C0.704063 15.7792 3.77915 12.7041 7.61426 12.5098L8 12.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M18.9844 41.0156C18.9844 40.0781 18.7031 39.2656 18.1406 38.5781C17.5781 37.8594 16.8594 37.375 15.9844 37.125V36C15.9844 35.4375 16.125 34.9375 16.4062 34.5C16.6875 34.0312 17.0469 33.6719 17.4844 33.4219C17.9531 33.1406 18.4531 33 18.9844 33H29.0156C29.5469 33 30.0312 33.1406 30.4688 33.4219C30.9375 33.6719 31.3125 34.0312 31.5938 34.5C31.875 34.9375 32.0156 35.4375 32.0156 36V37.125C31.1406 37.375 30.4219 37.8594 29.8594 38.5781C29.2969 39.2656 29.0156 40.0781 29.0156 41.0156V42.9844H18.9844V41.0156ZM33 39C33.5625 39 34.0312 39.2031 34.4062 39.6094C34.8125 39.9844 35.0156 40.4531 35.0156 41.0156V45.9844C35.0156 46.5469 34.875 47.0625 34.5938 47.5312C34.3125 47.9688 33.9375 48.3281 33.4688 48.6094C33.0312 48.8594 32.5469 48.9844 32.0156 48.9844V50.0156C32.0156 50.2656 31.9062 50.5 31.6875 50.7188C31.5 50.9062 31.2656 51 30.9844 51C30.7344 51 30.5 50.9062 30.2812 50.7188C30.0938 50.5 30 50.2656 30 50.0156V48.9844H18V50.0156C18 50.2656 17.8906 50.5 17.6719 50.7188C17.4844 50.9062 17.2656 51 17.0156 51C16.7344 51 16.4844 50.9062 16.2656 50.7188C16.0781 50.5 15.9844 50.2656 15.9844 50.0156V48.9844C15.4531 48.9844 14.9531 48.8594 14.4844 48.6094C14.0469 48.3281 13.6875 47.9688 13.4062 47.5312C13.125 47.0625 12.9844 46.5469 12.9844 45.9844V41.0156C12.9844 40.4531 13.1719 39.9844 13.5469 39.6094C13.9531 39.2031 14.4375 39 15 39C15.5625 39 16.0312 39.2031 16.4062 39.6094C16.8125 39.9844 17.0156 40.4531 17.0156 41.0156V45H30.9844V41.0156C30.9844 40.4531 31.1719 39.9844 31.5469 39.6094C31.9531 39.2031 32.4375 39 33 39Z" fill="#03A9F4"/>
<path d="M56 4C56 1.79086 57.7909 0 60 0H72C74.2091 0 76 1.79086 76 4V4C76 6.20914 74.2091 8 72 8H60C57.7909 8 56 6.20914 56 4V4Z" fill="white" fill-opacity="0.48"/>
<path d="M96 4C96 1.79086 97.7909 0 100 0V0C102.209 0 104 1.79086 104 4V4C104 6.20914 102.209 8 100 8V8C97.7909 8 96 6.20914 96 4V4Z" fill="white" fill-opacity="0.24"/>
<path d="M56 20C56 15.5817 59.5817 12 64 12H96C100.418 12 104 15.5817 104 20V64C104 68.4183 100.418 72 96 72H64C59.5817 72 56 68.4183 56 64V20Z" fill="#1C1C1C"/>
<path d="M64 12.5H96C100.142 12.5 103.5 15.8579 103.5 20V64C103.5 68.1421 100.142 71.5 96 71.5H64C59.8579 71.5 56.5 68.1421 56.5 64V20L56.5098 19.6143C56.7041 15.7792 59.7792 12.7041 63.6143 12.5098L64 12.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M86 39.9844H89.9844V42H88.0156V50.0156H71.9844V42H70.0156V39.9844H74C73.4375 39.9844 72.9531 39.7969 72.5469 39.4219C72.1719 39.0156 71.9844 38.5469 71.9844 38.0156V33.9844H77.9844V38.0156C77.9844 38.5469 77.7812 39.0156 77.375 39.4219C77 39.7969 76.5469 39.9844 76.0156 39.9844H83.9844V36.9844C83.9844 36.7344 83.8906 36.5156 83.7031 36.3281C83.5156 36.1094 83.2812 36 83 36C82.7188 36 82.4844 36.1094 82.2969 36.3281C82.1094 36.5156 82.0156 36.7344 82.0156 36.9844H80C80 36.4531 80.125 35.9688 80.375 35.5312C80.6562 35.0625 81.0156 34.6875 81.4531 34.4062C81.9219 34.125 82.4375 33.9844 83 33.9844C83.5625 33.9844 84.0625 34.125 84.5 34.4062C84.9688 34.6875 85.3281 35.0625 85.5781 35.5312C85.8594 35.9688 86 36.4531 86 36.9844V39.9844ZM80.9844 48V42H79.0156V48H80.9844Z" fill="#03A9F4"/>
<path d="M112 4C112 1.79086 113.791 0 116 0H128C130.209 0 132 1.79086 132 4V4C132 6.20914 130.209 8 128 8H116C113.791 8 112 6.20914 112 4V4Z" fill="white" fill-opacity="0.48"/>
<path d="M152 4C152 1.79086 153.791 0 156 0V0C158.209 0 160 1.79086 160 4V4C160 6.20914 158.209 8 156 8V8C153.791 8 152 6.20914 152 4V4Z" fill="white" fill-opacity="0.24"/>
<path d="M112 20C112 15.5817 115.582 12 120 12H152C156.418 12 160 15.5817 160 20V64C160 68.4183 156.418 72 152 72H120C115.582 72 112 68.4183 112 64V20Z" fill="#1C1C1C"/>
<path d="M120 12.5H152C156.142 12.5 159.5 15.8579 159.5 20V64C159.5 68.1421 156.142 71.5 152 71.5H120C115.858 71.5 112.5 68.1421 112.5 64V20L112.51 19.6143C112.704 15.7792 115.779 12.7041 119.614 12.5098L120 12.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M142.984 36.9844C144.078 36.9844 145.016 37.3906 145.797 38.2031C146.609 38.9844 147.016 39.9219 147.016 41.0156V50.0156H145V47.0156H127V50.0156H124.984V35.0156H127V44.0156H135.016V36.9844H142.984ZM133.094 42.0938C132.5 42.6875 131.797 42.9844 130.984 42.9844C130.172 42.9844 129.469 42.6875 128.875 42.0938C128.281 41.5 127.984 40.7969 127.984 39.9844C127.984 39.1719 128.281 38.4688 128.875 37.875C129.469 37.2812 130.172 36.9844 130.984 36.9844C131.797 36.9844 132.5 37.2812 133.094 37.875C133.688 38.4688 133.984 39.1719 133.984 39.9844C133.984 40.7969 133.688 41.5 133.094 42.0938Z" fill="#03A9F4"/>
<path d="M0 84C0 81.7909 1.79086 80 4 80H16C18.2091 80 20 81.7909 20 84V84C20 86.2091 18.2091 88 16 88H4C1.79086 88 0 86.2091 0 84V84Z" fill="white" fill-opacity="0.48"/>
<path d="M28 84C28 81.7909 29.7909 80 32 80V80C34.2091 80 36 81.7909 36 84V84C36 86.2091 34.2091 88 32 88V88C29.7909 88 28 86.2091 28 84V84Z" fill="white" fill-opacity="0.24"/>
<path d="M40 84C40 81.7909 41.7909 80 44 80V80C46.2091 80 48 81.7909 48 84V84C48 86.2091 46.2091 88 44 88V88C41.7909 88 40 86.2091 40 84V84Z" fill="white" fill-opacity="0.24"/>
<path d="M0 100C0 95.5817 3.58172 92 8 92H40C44.4183 92 48 95.5817 48 100V144C48 148.418 44.4183 152 40 152H8C3.58172 152 0 148.418 0 144V100Z" fill="#1C1C1C"/>
<path d="M8 92.5H40C44.1421 92.5 47.5 95.8579 47.5 100V144C47.5 148.142 44.1421 151.5 40 151.5H8C3.85787 151.5 0.5 148.142 0.5 144V100L0.509766 99.6143C0.704063 95.7792 3.77915 92.7041 7.61426 92.5098L8 92.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M14.0156 116H33.9844V128H32.0156V125.984H27.9844V128H26.0156V118.016H15.9844V128H14.0156V116ZM32.0156 118.016H27.9844V119.984H32.0156V118.016ZM27.9844 124.016H32.0156V122H27.9844V124.016Z" fill="#03A9F4"/>
<path d="M56 84C56 81.7909 57.7909 80 60 80H72C74.2091 80 76 81.7909 76 84V84C76 86.2091 74.2091 88 72 88H60C57.7909 88 56 86.2091 56 84V84Z" fill="white" fill-opacity="0.48"/>
<path d="M84 84C84 81.7909 85.7909 80 88 80V80C90.2091 80 92 81.7909 92 84V84C92 86.2091 90.2091 88 88 88V88C85.7909 88 84 86.2091 84 84V84Z" fill="white" fill-opacity="0.24"/>
<path d="M96 84C96 81.7909 97.7909 80 100 80V80C102.209 80 104 81.7909 104 84V84C104 86.2091 102.209 88 100 88V88C97.7909 88 96 86.2091 96 84V84Z" fill="white" fill-opacity="0.24"/>
<path d="M56 100C56 95.5817 59.5817 92 64 92H96C100.418 92 104 95.5817 104 100V144C104 148.418 100.418 152 96 152H64C59.5817 152 56 148.418 56 144V100Z" fill="#1C1C1C"/>
<path d="M64 92.5H96C100.142 92.5 103.5 95.8579 103.5 100V144C103.5 148.142 100.142 151.5 96 151.5H64C59.8579 151.5 56.5 148.142 56.5 144V100L56.5098 99.6143C56.7041 95.7792 59.7792 92.7041 63.6143 92.5098L64 92.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M77.9844 121.016V122.984H80V121.016H77.9844ZM82.0156 116V131H71V128.984H73.0156V113H82.0156V113.984H86.9844V128.984H89V131H85.0156V116H82.0156Z" fill="#03A9F4"/>
</g>
<defs>
<clipPath id="clip0_1738_5532">
<rect width="160" height="160" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,63 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1738_5534)">
<path d="M0 8C0 3.58172 3.58172 0 8 0H42.6667C47.0849 0 50.6667 3.58172 50.6667 8V64C50.6667 68.4183 47.0849 72 42.6667 72H8.00001C3.58173 72 0 68.4183 0 64V8Z" fill="#1C1C1C"/>
<path d="M8 0.5H42.667C46.809 0.500178 50.167 3.85798 50.167 8V64C50.167 68.142 46.809 71.4998 42.667 71.5H8C3.85787 71.5 0.5 68.1421 0.5 64V8C0.5 3.85786 3.85786 0.5 8 0.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M8 12C8 9.79086 9.79086 8 12 8H38.6667C40.8758 8 42.6667 9.79086 42.6667 12V12C42.6667 14.2091 40.8758 16 38.6667 16H12C9.79086 16 8 14.2091 8 12V12Z" fill="white" fill-opacity="0.48"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H27.6667C29.3235 24 30.6667 25.3431 30.6667 27V29C30.6667 30.6569 29.3235 32 27.6667 32H11C9.34314 32 8 30.6569 8 29V27Z" fill="white" fill-opacity="0.24"/>
<path d="M34.6667 28C34.6667 25.7909 36.4575 24 38.6667 24V24C40.8758 24 42.6667 25.7909 42.6667 28V28C42.6667 30.2091 40.8758 32 38.6667 32V32C36.4575 32 34.6667 30.2091 34.6667 28V28Z" fill="white" fill-opacity="0.24"/>
<path d="M8 43C8 41.3431 9.34315 40 11 40H27.6667C29.3235 40 30.6667 41.3431 30.6667 43V45C30.6667 46.6569 29.3235 48 27.6667 48H11C9.34314 48 8 46.6569 8 45V43Z" fill="white" fill-opacity="0.24"/>
<path d="M34.6667 44C34.6667 41.7909 36.4575 40 38.6667 40V40C40.8758 40 42.6667 41.7909 42.6667 44V44C42.6667 46.2091 40.8758 48 38.6667 48V48C36.4575 48 34.6667 46.2091 34.6667 44V44Z" fill="white" fill-opacity="0.24"/>
<path d="M8 59C8 57.3431 9.34315 56 11 56H27.6667C29.3235 56 30.6667 57.3431 30.6667 59V61C30.6667 62.6569 29.3235 64 27.6667 64H11C9.34314 64 8 62.6569 8 61V59Z" fill="white" fill-opacity="0.24"/>
<path d="M34.6667 60C34.6667 57.7909 36.4575 56 38.6667 56V56C40.8758 56 42.6667 57.7909 42.6667 60V60C42.6667 62.2091 40.8758 64 38.6667 64V64C36.4575 64 34.6667 62.2091 34.6667 60V60Z" fill="white" fill-opacity="0.24"/>
<path d="M0 84C0 79.5817 3.58172 76 8 76H42.6667C47.0849 76 50.6667 79.5817 50.6667 84V124C50.6667 128.418 47.0849 132 42.6667 132H8.00001C3.58173 132 0 128.418 0 124V84Z" fill="#1C1C1C"/>
<path d="M8 76.5H42.667C46.809 76.5002 50.167 79.858 50.167 84V124C50.167 128.142 46.809 131.5 42.667 131.5H8C3.85787 131.5 0.5 128.142 0.5 124V84C0.5 79.8579 3.85786 76.5 8 76.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M8 88C8 85.7909 9.79086 84 12 84H38.6667C40.8758 84 42.6667 85.7909 42.6667 88V88C42.6667 90.2091 40.8758 92 38.6667 92H12C9.79086 92 8 90.2091 8 88V88Z" fill="white" fill-opacity="0.48"/>
<path d="M8 103C8 101.343 9.34315 100 11 100H27.6667C29.3235 100 30.6667 101.343 30.6667 103V105C30.6667 106.657 29.3235 108 27.6667 108H11C9.34314 108 8 106.657 8 105V103Z" fill="white" fill-opacity="0.24"/>
<path d="M34.6667 104C34.6667 101.791 36.4575 100 38.6667 100V100C40.8758 100 42.6667 101.791 42.6667 104V104C42.6667 106.209 40.8758 108 38.6667 108V108C36.4575 108 34.6667 106.209 34.6667 104V104Z" fill="white" fill-opacity="0.24"/>
<path d="M8 119C8 117.343 9.34315 116 11 116H27.6667C29.3235 116 30.6667 117.343 30.6667 119V121C30.6667 122.657 29.3235 124 27.6667 124H11C9.34314 124 8 122.657 8 121V119Z" fill="white" fill-opacity="0.24"/>
<path d="M34.6667 120C34.6667 117.791 36.4575 116 38.6667 116V116C40.8758 116 42.6667 117.791 42.6667 120V120C42.6667 122.209 40.8758 124 38.6667 124V124C36.4575 124 34.6667 122.209 34.6667 120V120Z" fill="white" fill-opacity="0.24"/>
<path d="M0 144C0 139.582 3.58172 136 8 136H42.6667C47.0849 136 50.6667 139.582 50.6667 144V152C50.6667 156.418 47.0849 160 42.6667 160H8.00001C3.58173 160 0 156.418 0 152V144Z" fill="#1C1C1C"/>
<path d="M8 136.5H42.667C46.809 136.5 50.167 139.858 50.167 144V152C50.167 156.142 46.809 159.5 42.667 159.5H8C3.85787 159.5 0.5 156.142 0.5 152V144C0.5 139.858 3.85786 136.5 8 136.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M8 148C8 145.791 9.79086 144 12 144H38.6667C40.8758 144 42.6667 145.791 42.6667 148V148C42.6667 150.209 40.8758 152 38.6667 152H12C9.79086 152 8 150.209 8 148V148Z" fill="white" fill-opacity="0.48"/>
<path d="M54.6667 8C54.6667 3.58172 58.2484 0 62.6667 0H97.3333C101.752 0 105.333 3.58172 105.333 8V48C105.333 52.4183 101.752 56 97.3334 56H62.6667C58.2484 56 54.6667 52.4183 54.6667 48V8Z" fill="#1C1C1C"/>
<path d="M62.6667 0.5H97.3337C101.476 0.50018 104.834 3.85798 104.834 8V48C104.834 52.142 101.476 55.4998 97.3337 55.5H62.6667C58.5246 55.5 55.1667 52.1421 55.1667 48V8C55.1667 3.85786 58.5246 0.5 62.6667 0.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M62.6667 12C62.6667 9.79086 64.4575 8 66.6667 8H93.3334C95.5425 8 97.3334 9.79086 97.3334 12V12C97.3334 14.2091 95.5425 16 93.3334 16H66.6667C64.4576 16 62.6667 14.2091 62.6667 12V12Z" fill="white" fill-opacity="0.48"/>
<path d="M62.6667 27C62.6667 25.3431 64.0098 24 65.6667 24H82.3334C83.9902 24 85.3334 25.3431 85.3334 27V29C85.3334 30.6569 83.9902 32 82.3334 32H65.6667C64.0098 32 62.6667 30.6569 62.6667 29V27Z" fill="white" fill-opacity="0.24"/>
<path d="M89.3333 28C89.3333 25.7909 91.1242 24 93.3333 24V24C95.5425 24 97.3333 25.7909 97.3333 28V28C97.3333 30.2091 95.5425 32 93.3333 32V32C91.1242 32 89.3333 30.2091 89.3333 28V28Z" fill="white" fill-opacity="0.24"/>
<path d="M62.6667 43C62.6667 41.3431 64.0098 40 65.6667 40H82.3334C83.9902 40 85.3334 41.3431 85.3334 43V45C85.3334 46.6569 83.9902 48 82.3334 48H65.6667C64.0098 48 62.6667 46.6569 62.6667 45V43Z" fill="white" fill-opacity="0.24"/>
<path d="M89.3333 44C89.3333 41.7909 91.1242 40 93.3333 40V40C95.5425 40 97.3333 41.7909 97.3333 44V44C97.3333 46.2091 95.5425 48 93.3333 48V48C91.1242 48 89.3333 46.2091 89.3333 44V44Z" fill="white" fill-opacity="0.24"/>
<path d="M54.6667 68C54.6667 63.5817 58.2484 60 62.6667 60H97.3333C101.752 60 105.333 63.5817 105.333 68V76C105.333 80.4183 101.752 84 97.3334 84H62.6667C58.2484 84 54.6667 80.4183 54.6667 76V68Z" fill="#1C1C1C"/>
<path d="M62.6667 60.5H97.3337C101.476 60.5002 104.834 63.858 104.834 68V76C104.834 80.142 101.476 83.4998 97.3337 83.5H62.6667C58.5246 83.5 55.1667 80.1421 55.1667 76V68C55.1667 63.8579 58.5246 60.5 62.6667 60.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M62.6667 72C62.6667 69.7909 64.4575 68 66.6667 68H93.3334C95.5425 68 97.3334 69.7909 97.3334 72V72C97.3334 74.2091 95.5425 76 93.3334 76H66.6667C64.4576 76 62.6667 74.2091 62.6667 72V72Z" fill="white" fill-opacity="0.48"/>
<path d="M54.6667 96C54.6667 91.5817 58.2484 88 62.6667 88H97.3333C101.752 88 105.333 91.5817 105.333 96V136C105.333 140.418 101.752 144 97.3334 144H62.6667C58.2484 144 54.6667 140.418 54.6667 136V96Z" fill="#1C1C1C"/>
<path d="M62.6667 88.5H97.3337C101.476 88.5002 104.834 91.858 104.834 96V136C104.834 140.142 101.476 143.5 97.3337 143.5H62.6667C58.5246 143.5 55.1667 140.142 55.1667 136V96C55.1667 91.8579 58.5246 88.5 62.6667 88.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M62.6667 100C62.6667 97.7909 64.4575 96 66.6667 96H93.3334C95.5425 96 97.3334 97.7909 97.3334 100V100C97.3334 102.209 95.5425 104 93.3334 104H66.6667C64.4576 104 62.6667 102.209 62.6667 100V100Z" fill="white" fill-opacity="0.48"/>
<path d="M62.6667 115C62.6667 113.343 64.0098 112 65.6667 112H82.3334C83.9902 112 85.3334 113.343 85.3334 115V117C85.3334 118.657 83.9902 120 82.3334 120H65.6667C64.0098 120 62.6667 118.657 62.6667 117V115Z" fill="white" fill-opacity="0.24"/>
<path d="M89.3333 116C89.3333 113.791 91.1242 112 93.3333 112V112C95.5425 112 97.3333 113.791 97.3333 116V116C97.3333 118.209 95.5425 120 93.3333 120V120C91.1242 120 89.3333 118.209 89.3333 116V116Z" fill="white" fill-opacity="0.24"/>
<path d="M62.6667 131C62.6667 129.343 64.0098 128 65.6667 128H82.3334C83.9902 128 85.3334 129.343 85.3334 131V133C85.3334 134.657 83.9902 136 82.3334 136H65.6667C64.0098 136 62.6667 134.657 62.6667 133V131Z" fill="white" fill-opacity="0.24"/>
<path d="M89.3333 132C89.3333 129.791 91.1242 128 93.3333 128V128C95.5425 128 97.3333 129.791 97.3333 132V132C97.3333 134.209 95.5425 136 93.3333 136V136C91.1242 136 89.3333 134.209 89.3333 132V132Z" fill="white" fill-opacity="0.24"/>
<path d="M109.333 8C109.333 3.58172 112.915 0 117.333 0H152C156.418 0 160 3.58172 160 8V112C160 116.418 156.418 120 152 120H117.333C112.915 120 109.333 116.418 109.333 112V8Z" fill="#1C1C1C"/>
<path d="M117.333 0.5H152C156.142 0.50018 159.5 3.85798 159.5 8V112C159.5 116.142 156.142 119.5 152 119.5H117.333C113.191 119.5 109.833 116.142 109.833 112V8C109.833 3.85786 113.191 0.5 117.333 0.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M117.333 12C117.333 9.79086 119.124 8 121.333 8H148C150.209 8 152 9.79086 152 12V12C152 14.2091 150.209 16 148 16H121.333C119.124 16 117.333 14.2091 117.333 12V12Z" fill="white" fill-opacity="0.48"/>
<path d="M117.333 27C117.333 25.3431 118.676 24 120.333 24H137C138.657 24 140 25.3431 140 27V29C140 30.6569 138.657 32 137 32H120.333C118.676 32 117.333 30.6569 117.333 29V27Z" fill="white" fill-opacity="0.24"/>
<path d="M144 28C144 25.7909 145.791 24 148 24V24C150.209 24 152 25.7909 152 28V28C152 30.2091 150.209 32 148 32V32C145.791 32 144 30.2091 144 28V28Z" fill="white" fill-opacity="0.24"/>
<path d="M117.333 43C117.333 41.3431 118.676 40 120.333 40H137C138.657 40 140 41.3431 140 43V45C140 46.6569 138.657 48 137 48H120.333C118.676 48 117.333 46.6569 117.333 45V43Z" fill="white" fill-opacity="0.24"/>
<path d="M144 44C144 41.7909 145.791 40 148 40V40C150.209 40 152 41.7909 152 44V44C152 46.2091 150.209 48 148 48V48C145.791 48 144 46.2091 144 44V44Z" fill="white" fill-opacity="0.24"/>
<path d="M117.333 59C117.333 57.3431 118.676 56 120.333 56H137C138.657 56 140 57.3431 140 59V61C140 62.6569 138.657 64 137 64H120.333C118.676 64 117.333 62.6569 117.333 61V59Z" fill="white" fill-opacity="0.24"/>
<path d="M144 60C144 57.7909 145.791 56 148 56V56C150.209 56 152 57.7909 152 60V60C152 62.2091 150.209 64 148 64V64C145.791 64 144 62.2091 144 60V60Z" fill="white" fill-opacity="0.24"/>
<path d="M117.333 75C117.333 73.3431 118.676 72 120.333 72H137C138.657 72 140 73.3431 140 75V77C140 78.6569 138.657 80 137 80H120.333C118.676 80 117.333 78.6569 117.333 77V75Z" fill="white" fill-opacity="0.24"/>
<path d="M144 76C144 73.7909 145.791 72 148 72V72C150.209 72 152 73.7909 152 76V76C152 78.2091 150.209 80 148 80V80C145.791 80 144 78.2091 144 76V76Z" fill="white" fill-opacity="0.24"/>
<path d="M117.333 91C117.333 89.3431 118.676 88 120.333 88H137C138.657 88 140 89.3431 140 91V93C140 94.6569 138.657 96 137 96H120.333C118.676 96 117.333 94.6569 117.333 93V91Z" fill="white" fill-opacity="0.24"/>
<path d="M144 92C144 89.7909 145.791 88 148 88V88C150.209 88 152 89.7909 152 92V92C152 94.2091 150.209 96 148 96V96C145.791 96 144 94.2091 144 92V92Z" fill="white" fill-opacity="0.24"/>
<path d="M117.333 107C117.333 105.343 118.676 104 120.333 104H137C138.657 104 140 105.343 140 107V109C140 110.657 138.657 112 137 112H120.333C118.676 112 117.333 110.657 117.333 109V107Z" fill="white" fill-opacity="0.24"/>
<path d="M144 108C144 105.791 145.791 104 148 104V104C150.209 104 152 105.791 152 108V108C152 110.209 150.209 112 148 112V112C145.791 112 144 110.209 144 108V108Z" fill="white" fill-opacity="0.24"/>
<path d="M109.333 132C109.333 127.582 112.915 124 117.333 124H152C156.418 124 160 127.582 160 132V140C160 144.418 156.418 148 152 148H117.333C112.915 148 109.333 144.418 109.333 140V132Z" fill="#1C1C1C"/>
<path d="M117.333 124.5H152C156.142 124.5 159.5 127.858 159.5 132V140C159.5 144.142 156.142 147.5 152 147.5H117.333C113.191 147.5 109.833 144.142 109.833 140V132C109.833 127.858 113.191 124.5 117.333 124.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M117.333 136C117.333 133.791 119.124 132 121.333 132H148C150.209 132 152 133.791 152 136V136C152 138.209 150.209 140 148 140H121.333C119.124 140 117.333 138.209 117.333 136V136Z" fill="white" fill-opacity="0.48"/>
</g>
<defs>
<clipPath id="clip0_1738_5534">
<rect width="160" height="160" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,16 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1738_5540)">
<path d="M0 8C0 3.58172 3.58172 0 8 0H152C156.418 0 160 3.58172 160 8V152C160 156.418 156.418 160 152 160H8C3.58172 160 0 156.418 0 152V8Z" fill="#1C1C1C"/>
<path d="M8 0.5H152C156.142 0.500001 159.5 3.85787 159.5 8V152C159.5 156.142 156.142 159.5 152 159.5H8C3.85787 159.5 0.5 156.142 0.5 152V8C0.500001 3.85787 3.85787 0.5 8 0.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M83 86.9844V75.125L77 73.0156V84.875L83 86.9844ZM88.4844 71C88.8281 71 89 71.1719 89 71.5156V86.6094C89 86.8594 88.875 87.0156 88.625 87.0781L83 89L77 86.8906L71.6562 88.9531L71.5156 89C71.1719 89 71 88.8281 71 88.4844V73.3906C71 73.1406 71.125 72.9844 71.375 72.9219L77 71L83 73.1094L88.3438 71.0469L88.4844 71Z" fill="#03A9F4"/>
<path d="M148 77.0508L139.288 85.8983M139.288 85.8983L116.852 108.684C115.724 109.83 114.184 110.475 112.576 110.475H81.6939M139.288 85.8983V69.4322M21.1957 82.9492H12M21.1957 82.9492L32.3274 71.6441M21.1957 82.9492L44.427 106.542M81.6939 110.475V124.237M81.6939 110.475H54.5907M81.6939 138V124.237M81.6939 124.237H148M32.3274 71.6441L43.4591 60.339L54.5907 49.0339L70.4941 32.8828C74.2533 29.0651 79.3871 26.9153 84.7451 26.9153H128.641V26.9153C134.521 26.9153 139.288 31.6824 139.288 37.5629V37.7288V49.0339M32.3274 71.6441L24.8569 64.0572C22.5573 61.7218 22.5573 57.9732 24.8569 55.6377L57.9786 22M139.288 49.0339H126.313C124.706 49.0339 123.166 48.389 122.038 47.2436L116.057 41.1695M139.288 49.0339V69.4322M139.288 69.4322H126.313C124.706 69.4322 123.166 68.7873 122.038 67.6419L116.057 61.5678M54.5907 110.475V110.475C54.5907 107.488 52.17 105.068 49.184 105.068H49.0249C45.951 105.068 43.4591 107.56 43.4591 110.634V110.807C43.4591 113.881 45.951 116.373 49.0249 116.373V116.373C52.0988 116.373 54.5907 113.881 54.5907 110.807V110.475ZM44.427 115.39L22.1637 138" stroke="white" stroke-opacity="0.24" stroke-width="5" stroke-linecap="round"/>
<circle cx="41" cy="39" r="14" fill="white" fill-opacity="0.48"/>
<circle cx="89" cy="117" r="14" fill="white" fill-opacity="0.48"/>
<path d="M116 62.325C115.767 62.325 115.533 62.2833 115.3 62.2C115.067 62.1167 114.858 61.9917 114.675 61.825C113.592 60.825 112.633 59.85 111.8 58.9C110.967 57.95 110.267 57.0333 109.7 56.15C109.15 55.25 108.725 54.3917 108.425 53.575C108.142 52.7417 108 51.95 108 51.2C108 48.7 108.8 46.7083 110.4 45.225C112.017 43.7417 113.883 43 116 43C118.117 43 119.975 43.7417 121.575 45.225C123.192 46.7083 124 48.7 124 51.2C124 51.95 123.85 52.7417 123.55 53.575C123.267 54.3917 122.842 55.25 122.275 56.15C121.725 57.0333 121.033 57.95 120.2 58.9C119.367 59.85 118.408 60.825 117.325 61.825C117.142 61.9917 116.933 62.1167 116.7 62.2C116.467 62.2833 116.233 62.325 116 62.325Z" fill="white" fill-opacity="0.48"/>
</g>
<defs>
<clipPath id="clip0_1738_5540">
<rect width="160" height="160" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,13 @@
<svg width="160" height="72" viewBox="0 0 160 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1738_5536)">
<path d="M0 4C0 1.79086 1.79086 0 4 0H28C30.2091 0 32 1.79086 32 4V4C32 6.20914 30.2091 8 28 8H4C1.79086 8 0 6.20914 0 4V4Z" fill="white" fill-opacity="0.48"/>
<path d="M0 20C0 15.5817 3.58172 12 8 12H42.6667C47.0849 12 50.6667 15.5817 50.6667 20V64C50.6667 68.4183 47.0849 72 42.6667 72H8.00001C3.58173 72 0 68.4183 0 64V20Z" fill="#1C1C1C"/>
<path d="M8 12.5H42.667C46.809 12.5002 50.167 15.858 50.167 20V64C50.167 68.142 46.809 71.4998 42.667 71.5H8C3.85787 71.5 0.5 68.1421 0.5 64V20L0.509766 19.6143C0.710536 15.6514 3.98724 12.5 8 12.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M32.3177 42.9844H26.3177V48.9844H24.349V42.9844H18.349V41.0156H24.349V35.0156H26.3177V41.0156H32.3177V42.9844Z" fill="#03A9F4"/>
</g>
<defs>
<clipPath id="clip0_1738_5536">
<rect width="160" height="72" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 973 B

View File

@@ -0,0 +1,19 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1738_5538)">
<path d="M0 8C0 3.58172 3.58172 0 8 0H152C156.418 0 160 3.58172 160 8V152C160 156.418 156.418 160 152 160H8C3.58172 160 0 156.418 0 152V8Z" fill="#1C1C1C"/>
<path d="M8 0.5H152C156.142 0.500001 159.5 3.85787 159.5 8V152C159.5 156.142 156.142 159.5 152 159.5H8C3.85787 159.5 0.5 156.142 0.5 152V8C0.500001 3.85787 3.85787 0.5 8 0.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M8 13.5C8 10.4624 10.4624 8 13.5 8H146.5C149.538 8 152 10.4624 152 13.5V13.5C152 16.5376 149.538 19 146.5 19H13.5C10.4624 19 8 16.5376 8 13.5V13.5Z" fill="white" fill-opacity="0.48"/>
<path d="M8 35C8 30.5817 11.5817 27 16 27H45.3333C49.7516 27 53.3333 30.5817 53.3333 35V125C53.3333 129.418 49.7516 133 45.3333 133H16C11.5817 133 8 129.418 8 125V35Z" fill="#1C1C1C"/>
<path d="M16 27.5H45.333C49.4751 27.5 52.833 30.8579 52.833 35V125C52.833 129.142 49.4751 132.5 45.333 132.5H16C11.8579 132.5 8.5 129.142 8.5 125V35C8.5 30.8579 11.8579 27.5 16 27.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M57.3333 35C57.3333 30.5817 60.9151 27 65.3333 27H94.6667C99.0849 27 102.667 30.5817 102.667 35V49C102.667 53.4183 99.0849 57 94.6667 57H65.3333C60.915 57 57.3333 53.4183 57.3333 49V35Z" fill="#1C1C1C"/>
<path d="M65.3333 27.5H94.6663C98.8085 27.5 102.166 30.8579 102.166 35V49C102.166 53.1421 98.8085 56.5 94.6663 56.5H65.3333C61.1912 56.5 57.8333 53.1421 57.8333 49V35C57.8333 30.8579 61.1912 27.5 65.3333 27.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M106.667 35C106.667 30.5817 110.248 27 114.667 27H144C148.418 27 152 30.5817 152 35V87C152 91.4183 148.418 95 144 95H114.667C110.248 95 106.667 91.4183 106.667 87V35Z" fill="#1C1C1C"/>
<path d="M114.667 27.5H144C148.142 27.5 151.5 30.8579 151.5 35V87C151.5 91.1421 148.142 94.5 144 94.5H114.667C110.525 94.5 107.167 91.1421 107.167 87V35C107.167 30.8579 110.525 27.5 114.667 27.5Z" stroke="white" stroke-opacity="0.24"/>
<path d="M84.3594 82.0156H87.7344C87.9219 81.1406 88.0156 80.4688 88.0156 80C88.0156 79.5312 87.9219 78.8594 87.7344 77.9844H84.3594C84.4531 78.6406 84.5 79.3125 84.5 80C84.5 80.6875 84.4531 81.3594 84.3594 82.0156ZM82.5781 87.5469C83.3594 87.2969 84.1719 86.8281 85.0156 86.1406C85.8594 85.4219 86.5 84.7031 86.9375 83.9844H83.9844C83.6719 85.2344 83.2031 86.4219 82.5781 87.5469ZM82.3438 82.0156C82.4375 81.3594 82.4844 80.6875 82.4844 80C82.4844 79.3125 82.4375 78.6406 82.3438 77.9844H77.6562C77.5625 78.6406 77.5156 79.3125 77.5156 80C77.5156 80.6875 77.5625 81.3594 77.6562 82.0156H82.3438ZM80 87.9688C80.875 86.6875 81.5156 85.3594 81.9219 83.9844H78.0781C78.4844 85.3594 79.125 86.6875 80 87.9688ZM76.0156 76.0156C76.3906 74.6719 76.8594 73.4844 77.4219 72.4531C76.6406 72.7031 75.8125 73.1875 74.9375 73.9062C74.0938 74.5938 73.4688 75.2969 73.0625 76.0156H76.0156ZM73.0625 83.9844C73.4688 84.7031 74.0938 85.4219 74.9375 86.1406C75.8125 86.8281 76.6406 87.2969 77.4219 87.5469C76.7969 86.4219 76.3281 85.2344 76.0156 83.9844H73.0625ZM72.2656 82.0156H75.6406C75.5469 81.3594 75.5 80.6875 75.5 80C75.5 79.3125 75.5469 78.6406 75.6406 77.9844H72.2656C72.0781 78.8594 71.9844 79.5312 71.9844 80C71.9844 80.4688 72.0781 81.1406 72.2656 82.0156ZM80 72.0312C79.125 73.3125 78.4844 74.6406 78.0781 76.0156H81.9219C81.5156 74.6406 80.875 73.3125 80 72.0312ZM86.9375 76.0156C86.5 75.2969 85.8594 74.5938 85.0156 73.9062C84.1719 73.1875 83.3594 72.7031 82.5781 72.4531C83.1406 73.4844 83.6094 74.6719 83.9844 76.0156H86.9375ZM72.9219 72.9688C74.8906 71 77.25 70.0156 80 70.0156C82.75 70.0156 85.0938 71 87.0312 72.9688C89 74.9062 89.9844 77.25 89.9844 80C89.9844 82.75 89 85.1094 87.0312 87.0781C85.0938 89.0156 82.75 89.9844 80 89.9844C77.25 89.9844 74.8906 89.0156 72.9219 87.0781C70.9844 85.1094 70.0156 82.75 70.0156 80C70.0156 77.25 70.9844 74.9062 72.9219 72.9688Z" fill="#03A9F4"/>
</g>
<defs>
<clipPath id="clip0_1738_5538">
<rect width="160" height="160" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,36 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1738_5531)">
<path d="M0 4C0 1.79086 1.79086 0 4 0H16C18.2091 0 20 1.79086 20 4V4C20 6.20914 18.2091 8 16 8H4C1.79086 8 0 6.20914 0 4V4Z" fill="black" fill-opacity="0.32"/>
<rect x="40" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M0 20C0 15.5817 3.58172 12 8 12H40C44.4183 12 48 15.5817 48 20V64C48 68.4183 44.4183 72 40 72H8C3.58172 72 0 68.4183 0 64V20Z" fill="white"/>
<path d="M8 12.5H40C44.1421 12.5 47.5 15.8579 47.5 20V64C47.5 68.1421 44.1421 71.5 40 71.5H8C3.85787 71.5 0.5 68.1421 0.5 64V20L0.509766 19.6143C0.704063 15.7792 3.77915 12.7041 7.61426 12.5098L8 12.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M18.9844 41.0156C18.9844 40.0781 18.7031 39.2656 18.1406 38.5781C17.5781 37.8594 16.8594 37.375 15.9844 37.125V36C15.9844 35.4375 16.125 34.9375 16.4062 34.5C16.6875 34.0312 17.0469 33.6719 17.4844 33.4219C17.9531 33.1406 18.4531 33 18.9844 33H29.0156C29.5469 33 30.0312 33.1406 30.4688 33.4219C30.9375 33.6719 31.3125 34.0312 31.5938 34.5C31.875 34.9375 32.0156 35.4375 32.0156 36V37.125C31.1406 37.375 30.4219 37.8594 29.8594 38.5781C29.2969 39.2656 29.0156 40.0781 29.0156 41.0156V42.9844H18.9844V41.0156ZM33 39C33.5625 39 34.0312 39.2031 34.4062 39.6094C34.8125 39.9844 35.0156 40.4531 35.0156 41.0156V45.9844C35.0156 46.5469 34.875 47.0625 34.5938 47.5312C34.3125 47.9688 33.9375 48.3281 33.4688 48.6094C33.0312 48.8594 32.5469 48.9844 32.0156 48.9844V50.0156C32.0156 50.2656 31.9062 50.5 31.6875 50.7188C31.5 50.9062 31.2656 51 30.9844 51C30.7344 51 30.5 50.9062 30.2812 50.7188C30.0938 50.5 30 50.2656 30 50.0156V48.9844H18V50.0156C18 50.2656 17.8906 50.5 17.6719 50.7188C17.4844 50.9062 17.2656 51 17.0156 51C16.7344 51 16.4844 50.9062 16.2656 50.7188C16.0781 50.5 15.9844 50.2656 15.9844 50.0156V48.9844C15.4531 48.9844 14.9531 48.8594 14.4844 48.6094C14.0469 48.3281 13.6875 47.9688 13.4062 47.5312C13.125 47.0625 12.9844 46.5469 12.9844 45.9844V41.0156C12.9844 40.4531 13.1719 39.9844 13.5469 39.6094C13.9531 39.2031 14.4375 39 15 39C15.5625 39 16.0312 39.2031 16.4062 39.6094C16.8125 39.9844 17.0156 40.4531 17.0156 41.0156V45H30.9844V41.0156C30.9844 40.4531 31.1719 39.9844 31.5469 39.6094C31.9531 39.2031 32.4375 39 33 39Z" fill="#03A9F4"/>
<path d="M56 4C56 1.79086 57.7909 0 60 0H72C74.2091 0 76 1.79086 76 4V4C76 6.20914 74.2091 8 72 8H60C57.7909 8 56 6.20914 56 4V4Z" fill="black" fill-opacity="0.32"/>
<rect x="96" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M56 20C56 15.5817 59.5817 12 64 12H96C100.418 12 104 15.5817 104 20V64C104 68.4183 100.418 72 96 72H64C59.5817 72 56 68.4183 56 64V20Z" fill="white"/>
<path d="M64 12.5H96C100.142 12.5 103.5 15.8579 103.5 20V64C103.5 68.1421 100.142 71.5 96 71.5H64C59.8579 71.5 56.5 68.1421 56.5 64V20L56.5098 19.6143C56.7041 15.7792 59.7792 12.7041 63.6143 12.5098L64 12.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M86 39.9844H89.9844V42H88.0156V50.0156H71.9844V42H70.0156V39.9844H74C73.4375 39.9844 72.9531 39.7969 72.5469 39.4219C72.1719 39.0156 71.9844 38.5469 71.9844 38.0156V33.9844H77.9844V38.0156C77.9844 38.5469 77.7812 39.0156 77.375 39.4219C77 39.7969 76.5469 39.9844 76.0156 39.9844H83.9844V36.9844C83.9844 36.7344 83.8906 36.5156 83.7031 36.3281C83.5156 36.1094 83.2812 36 83 36C82.7188 36 82.4844 36.1094 82.2969 36.3281C82.1094 36.5156 82.0156 36.7344 82.0156 36.9844H80C80 36.4531 80.125 35.9688 80.375 35.5312C80.6562 35.0625 81.0156 34.6875 81.4531 34.4062C81.9219 34.125 82.4375 33.9844 83 33.9844C83.5625 33.9844 84.0625 34.125 84.5 34.4062C84.9688 34.6875 85.3281 35.0625 85.5781 35.5312C85.8594 35.9688 86 36.4531 86 36.9844V39.9844ZM80.9844 48V42H79.0156V48H80.9844Z" fill="#03A9F4"/>
<path d="M112 4C112 1.79086 113.791 0 116 0H128C130.209 0 132 1.79086 132 4V4C132 6.20914 130.209 8 128 8H116C113.791 8 112 6.20914 112 4V4Z" fill="black" fill-opacity="0.32"/>
<rect x="152" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M112 20C112 15.5817 115.582 12 120 12H152C156.418 12 160 15.5817 160 20V64C160 68.4183 156.418 72 152 72H120C115.582 72 112 68.4183 112 64V20Z" fill="white"/>
<path d="M120 12.5H152C156.142 12.5 159.5 15.8579 159.5 20V64C159.5 68.1421 156.142 71.5 152 71.5H120C115.858 71.5 112.5 68.1421 112.5 64V20L112.51 19.6143C112.704 15.7792 115.779 12.7041 119.614 12.5098L120 12.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M142.984 36.9844C144.078 36.9844 145.016 37.3906 145.797 38.2031C146.609 38.9844 147.016 39.9219 147.016 41.0156V50.0156H145V47.0156H127V50.0156H124.984V35.0156H127V44.0156H135.016V36.9844H142.984ZM133.094 42.0938C132.5 42.6875 131.797 42.9844 130.984 42.9844C130.172 42.9844 129.469 42.6875 128.875 42.0938C128.281 41.5 127.984 40.7969 127.984 39.9844C127.984 39.1719 128.281 38.4688 128.875 37.875C129.469 37.2812 130.172 36.9844 130.984 36.9844C131.797 36.9844 132.5 37.2812 133.094 37.875C133.688 38.4688 133.984 39.1719 133.984 39.9844C133.984 40.7969 133.688 41.5 133.094 42.0938Z" fill="#03A9F4"/>
<path d="M0 84C0 81.7909 1.79086 80 4 80H16C18.2091 80 20 81.7909 20 84V84C20 86.2091 18.2091 88 16 88H4C1.79086 88 0 86.2091 0 84V84Z" fill="black" fill-opacity="0.32"/>
<rect x="28" y="80" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<rect x="40" y="80" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M0 100C0 95.5817 3.58172 92 8 92H40C44.4183 92 48 95.5817 48 100V144C48 148.418 44.4183 152 40 152H8C3.58172 152 0 148.418 0 144V100Z" fill="white"/>
<path d="M8 92.5H40C44.1421 92.5 47.5 95.8579 47.5 100V144C47.5 148.142 44.1421 151.5 40 151.5H8C3.85787 151.5 0.5 148.142 0.5 144V100L0.509766 99.6143C0.704063 95.7792 3.77915 92.7041 7.61426 92.5098L8 92.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M14.0156 116H33.9844V128H32.0156V125.984H27.9844V128H26.0156V118.016H15.9844V128H14.0156V116ZM32.0156 118.016H27.9844V119.984H32.0156V118.016ZM27.9844 124.016H32.0156V122H27.9844V124.016Z" fill="#03A9F4"/>
<path d="M56 84C56 81.7909 57.7909 80 60 80H72C74.2091 80 76 81.7909 76 84V84C76 86.2091 74.2091 88 72 88H60C57.7909 88 56 86.2091 56 84V84Z" fill="black" fill-opacity="0.32"/>
<rect x="84" y="80" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<rect x="96" y="80" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M56 100C56 95.5817 59.5817 92 64 92H96C100.418 92 104 95.5817 104 100V144C104 148.418 100.418 152 96 152H64C59.5817 152 56 148.418 56 144V100Z" fill="white"/>
<path d="M64 92.5H96C100.142 92.5 103.5 95.8579 103.5 100V144C103.5 148.142 100.142 151.5 96 151.5H64C59.8579 151.5 56.5 148.142 56.5 144V100L56.5098 99.6143C56.7041 95.7792 59.7792 92.7041 63.6143 92.5098L64 92.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M77.9844 121.016V122.984H80V121.016H77.9844ZM82.0156 116V131H71V128.984H73.0156V113H82.0156V113.984H86.9844V128.984H89V131H85.0156V116H82.0156Z" fill="#03A9F4"/>
</g>
<defs>
<clipPath id="clip0_1738_5531">
<rect width="160" height="160" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -0,0 +1,63 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1738_5533)">
<path d="M0 8C0 3.58172 3.58172 0 8 0H42.6667C47.0849 0 50.6667 3.58172 50.6667 8V64C50.6667 68.4183 47.0849 72 42.6667 72H8.00001C3.58173 72 0 68.4183 0 64V8Z" fill="white"/>
<path d="M8 0.5H42.667C46.809 0.500178 50.167 3.85798 50.167 8V64C50.167 68.142 46.809 71.4998 42.667 71.5H8C3.85787 71.5 0.5 68.1421 0.5 64V8C0.5 3.85786 3.85786 0.5 8 0.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M8 12C8 9.79086 9.79086 8 12 8H38.6667C40.8758 8 42.6667 9.79086 42.6667 12V12C42.6667 14.2091 40.8758 16 38.6667 16H12C9.79086 16 8 14.2091 8 12V12Z" fill="black" fill-opacity="0.32"/>
<path d="M8 27C8 25.3431 9.34315 24 11 24H27.6667C29.3235 24 30.6667 25.3431 30.6667 27V29C30.6667 30.6569 29.3235 32 27.6667 32H11C9.34314 32 8 30.6569 8 29V27Z" fill="black" fill-opacity="0.12"/>
<rect x="34.6667" y="24" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M8 43C8 41.3431 9.34315 40 11 40H27.6667C29.3235 40 30.6667 41.3431 30.6667 43V45C30.6667 46.6569 29.3235 48 27.6667 48H11C9.34314 48 8 46.6569 8 45V43Z" fill="black" fill-opacity="0.12"/>
<rect x="34.6667" y="40" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M8 59C8 57.3431 9.34315 56 11 56H27.6667C29.3235 56 30.6667 57.3431 30.6667 59V61C30.6667 62.6569 29.3235 64 27.6667 64H11C9.34314 64 8 62.6569 8 61V59Z" fill="black" fill-opacity="0.12"/>
<rect x="34.6667" y="56" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M0 84C0 79.5817 3.58172 76 8 76H42.6667C47.0849 76 50.6667 79.5817 50.6667 84V124C50.6667 128.418 47.0849 132 42.6667 132H8.00001C3.58173 132 0 128.418 0 124V84Z" fill="white"/>
<path d="M8 76.5H42.667C46.809 76.5002 50.167 79.858 50.167 84V124C50.167 128.142 46.809 131.5 42.667 131.5H8C3.85787 131.5 0.5 128.142 0.5 124V84C0.5 79.8579 3.85786 76.5 8 76.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M8 88C8 85.7909 9.79086 84 12 84H38.6667C40.8758 84 42.6667 85.7909 42.6667 88V88C42.6667 90.2091 40.8758 92 38.6667 92H12C9.79086 92 8 90.2091 8 88V88Z" fill="black" fill-opacity="0.32"/>
<path d="M8 103C8 101.343 9.34315 100 11 100H27.6667C29.3235 100 30.6667 101.343 30.6667 103V105C30.6667 106.657 29.3235 108 27.6667 108H11C9.34314 108 8 106.657 8 105V103Z" fill="black" fill-opacity="0.12"/>
<rect x="34.6667" y="100" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M8 119C8 117.343 9.34315 116 11 116H27.6667C29.3235 116 30.6667 117.343 30.6667 119V121C30.6667 122.657 29.3235 124 27.6667 124H11C9.34314 124 8 122.657 8 121V119Z" fill="black" fill-opacity="0.12"/>
<rect x="34.6667" y="116" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M0 144C0 139.582 3.58172 136 8 136H42.6667C47.0849 136 50.6667 139.582 50.6667 144V152C50.6667 156.418 47.0849 160 42.6667 160H8.00001C3.58173 160 0 156.418 0 152V144Z" fill="white"/>
<path d="M8 136.5H42.667C46.809 136.5 50.167 139.858 50.167 144V152C50.167 156.142 46.809 159.5 42.667 159.5H8C3.85787 159.5 0.5 156.142 0.5 152V144C0.5 139.858 3.85786 136.5 8 136.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M8 148C8 145.791 9.79086 144 12 144H38.6667C40.8758 144 42.6667 145.791 42.6667 148V148C42.6667 150.209 40.8758 152 38.6667 152H12C9.79086 152 8 150.209 8 148V148Z" fill="black" fill-opacity="0.32"/>
<path d="M54.6667 8C54.6667 3.58172 58.2484 0 62.6667 0H97.3333C101.752 0 105.333 3.58172 105.333 8V48C105.333 52.4183 101.752 56 97.3334 56H62.6667C58.2484 56 54.6667 52.4183 54.6667 48V8Z" fill="white"/>
<path d="M62.6667 0.5H97.3337C101.476 0.50018 104.834 3.85798 104.834 8V48C104.834 52.142 101.476 55.4998 97.3337 55.5H62.6667C58.5246 55.5 55.1667 52.1421 55.1667 48V8C55.1667 3.85786 58.5246 0.5 62.6667 0.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M62.6667 12C62.6667 9.79086 64.4575 8 66.6667 8H93.3334C95.5425 8 97.3334 9.79086 97.3334 12V12C97.3334 14.2091 95.5425 16 93.3334 16H66.6667C64.4576 16 62.6667 14.2091 62.6667 12V12Z" fill="black" fill-opacity="0.32"/>
<path d="M62.6667 27C62.6667 25.3431 64.0098 24 65.6667 24H82.3334C83.9902 24 85.3334 25.3431 85.3334 27V29C85.3334 30.6569 83.9902 32 82.3334 32H65.6667C64.0098 32 62.6667 30.6569 62.6667 29V27Z" fill="black" fill-opacity="0.12"/>
<rect x="89.3333" y="24" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M62.6667 43C62.6667 41.3431 64.0098 40 65.6667 40H82.3334C83.9902 40 85.3334 41.3431 85.3334 43V45C85.3334 46.6569 83.9902 48 82.3334 48H65.6667C64.0098 48 62.6667 46.6569 62.6667 45V43Z" fill="black" fill-opacity="0.12"/>
<rect x="89.3333" y="40" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M54.6667 68C54.6667 63.5817 58.2484 60 62.6667 60H97.3333C101.752 60 105.333 63.5817 105.333 68V76C105.333 80.4183 101.752 84 97.3334 84H62.6667C58.2484 84 54.6667 80.4183 54.6667 76V68Z" fill="white"/>
<path d="M62.6667 60.5H97.3337C101.476 60.5002 104.834 63.858 104.834 68V76C104.834 80.142 101.476 83.4998 97.3337 83.5H62.6667C58.5246 83.5 55.1667 80.1421 55.1667 76V68C55.1667 63.8579 58.5246 60.5 62.6667 60.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M62.6667 72C62.6667 69.7909 64.4575 68 66.6667 68H93.3334C95.5425 68 97.3334 69.7909 97.3334 72V72C97.3334 74.2091 95.5425 76 93.3334 76H66.6667C64.4576 76 62.6667 74.2091 62.6667 72V72Z" fill="black" fill-opacity="0.32"/>
<path d="M54.6667 96C54.6667 91.5817 58.2484 88 62.6667 88H97.3333C101.752 88 105.333 91.5817 105.333 96V136C105.333 140.418 101.752 144 97.3334 144H62.6667C58.2484 144 54.6667 140.418 54.6667 136V96Z" fill="white"/>
<path d="M62.6667 88.5H97.3337C101.476 88.5002 104.834 91.858 104.834 96V136C104.834 140.142 101.476 143.5 97.3337 143.5H62.6667C58.5246 143.5 55.1667 140.142 55.1667 136V96C55.1667 91.8579 58.5246 88.5 62.6667 88.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M62.6667 100C62.6667 97.7909 64.4575 96 66.6667 96H93.3334C95.5425 96 97.3334 97.7909 97.3334 100V100C97.3334 102.209 95.5425 104 93.3334 104H66.6667C64.4576 104 62.6667 102.209 62.6667 100V100Z" fill="black" fill-opacity="0.32"/>
<path d="M62.6667 115C62.6667 113.343 64.0098 112 65.6667 112H82.3334C83.9902 112 85.3334 113.343 85.3334 115V117C85.3334 118.657 83.9902 120 82.3334 120H65.6667C64.0098 120 62.6667 118.657 62.6667 117V115Z" fill="black" fill-opacity="0.12"/>
<rect x="89.3333" y="112" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M62.6667 131C62.6667 129.343 64.0098 128 65.6667 128H82.3334C83.9902 128 85.3334 129.343 85.3334 131V133C85.3334 134.657 83.9902 136 82.3334 136H65.6667C64.0098 136 62.6667 134.657 62.6667 133V131Z" fill="black" fill-opacity="0.12"/>
<rect x="89.3333" y="128" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M109.333 8C109.333 3.58172 112.915 0 117.333 0H152C156.418 0 160 3.58172 160 8V112C160 116.418 156.418 120 152 120H117.333C112.915 120 109.333 116.418 109.333 112V8Z" fill="white"/>
<path d="M117.333 0.5H152C156.142 0.50018 159.5 3.85798 159.5 8V112C159.5 116.142 156.142 119.5 152 119.5H117.333C113.191 119.5 109.833 116.142 109.833 112V8C109.833 3.85786 113.191 0.5 117.333 0.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M117.333 12C117.333 9.79086 119.124 8 121.333 8H148C150.209 8 152 9.79086 152 12V12C152 14.2091 150.209 16 148 16H121.333C119.124 16 117.333 14.2091 117.333 12V12Z" fill="black" fill-opacity="0.32"/>
<path d="M117.333 27C117.333 25.3431 118.676 24 120.333 24H137C138.657 24 140 25.3431 140 27V29C140 30.6569 138.657 32 137 32H120.333C118.676 32 117.333 30.6569 117.333 29V27Z" fill="black" fill-opacity="0.12"/>
<rect x="144" y="24" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M117.333 43C117.333 41.3431 118.676 40 120.333 40H137C138.657 40 140 41.3431 140 43V45C140 46.6569 138.657 48 137 48H120.333C118.676 48 117.333 46.6569 117.333 45V43Z" fill="black" fill-opacity="0.12"/>
<rect x="144" y="40" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M117.333 59C117.333 57.3431 118.676 56 120.333 56H137C138.657 56 140 57.3431 140 59V61C140 62.6569 138.657 64 137 64H120.333C118.676 64 117.333 62.6569 117.333 61V59Z" fill="black" fill-opacity="0.12"/>
<rect x="144" y="56" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M117.333 75C117.333 73.3431 118.676 72 120.333 72H137C138.657 72 140 73.3431 140 75V77C140 78.6569 138.657 80 137 80H120.333C118.676 80 117.333 78.6569 117.333 77V75Z" fill="black" fill-opacity="0.12"/>
<rect x="144" y="72" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M117.333 91C117.333 89.3431 118.676 88 120.333 88H137C138.657 88 140 89.3431 140 91V93C140 94.6569 138.657 96 137 96H120.333C118.676 96 117.333 94.6569 117.333 93V91Z" fill="black" fill-opacity="0.12"/>
<rect x="144" y="88" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M117.333 107C117.333 105.343 118.676 104 120.333 104H137C138.657 104 140 105.343 140 107V109C140 110.657 138.657 112 137 112H120.333C118.676 112 117.333 110.657 117.333 109V107Z" fill="black" fill-opacity="0.12"/>
<rect x="144" y="104" width="8" height="8" rx="4" fill="black" fill-opacity="0.12"/>
<path d="M109.333 132C109.333 127.582 112.915 124 117.333 124H152C156.418 124 160 127.582 160 132V140C160 144.418 156.418 148 152 148H117.333C112.915 148 109.333 144.418 109.333 140V132Z" fill="white"/>
<path d="M117.333 124.5H152C156.142 124.5 159.5 127.858 159.5 132V140C159.5 144.142 156.142 147.5 152 147.5H117.333C113.191 147.5 109.833 144.142 109.833 140V132C109.833 127.858 113.191 124.5 117.333 124.5Z" stroke="black" stroke-opacity="0.12"/>
<path d="M117.333 136C117.333 133.791 119.124 132 121.333 132H148C150.209 132 152 133.791 152 136V136C152 138.209 150.209 140 148 140H121.333C119.124 140 117.333 138.209 117.333 136V136Z" fill="black" fill-opacity="0.32"/>
</g>
<defs>
<clipPath id="clip0_1738_5533">
<rect width="160" height="160" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@@ -0,0 +1,16 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1738_5539)">
<rect width="160" height="160" rx="8" fill="white"/>
<rect x="0.5" y="0.5" width="159" height="159" rx="7.5" stroke="black" stroke-opacity="0.12"/>
<path d="M83 86.9844V75.125L77 73.0156V84.875L83 86.9844ZM88.4844 71C88.8281 71 89 71.1719 89 71.5156V86.6094C89 86.8594 88.875 87.0156 88.625 87.0781L83 89L77 86.8906L71.6562 88.9531L71.5156 89C71.1719 89 71 88.8281 71 88.4844V73.3906C71 73.1406 71.125 72.9844 71.375 72.9219L77 71L83 73.1094L88.3438 71.0469L88.4844 71Z" fill="#03A9F4"/>
<path d="M148 77.0508L139.288 85.8983M139.288 85.8983L116.852 108.684C115.724 109.83 114.184 110.475 112.576 110.475H81.6939M139.288 85.8983V69.4322M21.1957 82.9492H12M21.1957 82.9492L32.3274 71.6441M21.1957 82.9492L44.427 106.542M81.6939 110.475V124.237M81.6939 110.475H54.5907M81.6939 138V124.237M81.6939 124.237H148M32.3274 71.6441L43.4591 60.339L54.5907 49.0339L70.4941 32.8828C74.2533 29.0651 79.3871 26.9153 84.7451 26.9153H128.641V26.9153C134.521 26.9153 139.288 31.6824 139.288 37.5629V37.7288V49.0339M32.3274 71.6441L24.8569 64.0572C22.5573 61.7218 22.5573 57.9732 24.8569 55.6377L57.9786 22M139.288 49.0339H126.313C124.706 49.0339 123.166 48.389 122.038 47.2436L116.057 41.1695M139.288 49.0339V69.4322M139.288 69.4322H126.313C124.706 69.4322 123.166 68.7873 122.038 67.6419L116.057 61.5678M54.5907 110.475V110.475C54.5907 107.488 52.17 105.068 49.184 105.068H49.0249C45.951 105.068 43.4591 107.56 43.4591 110.634V110.807C43.4591 113.881 45.951 116.373 49.0249 116.373V116.373C52.0988 116.373 54.5907 113.881 54.5907 110.807V110.475ZM44.427 115.39L22.1637 138" stroke="black" stroke-opacity="0.12" stroke-width="5" stroke-linecap="round"/>
<circle cx="41" cy="39" r="14" fill="black" fill-opacity="0.32"/>
<circle cx="89" cy="117" r="14" fill="black" fill-opacity="0.32"/>
<path d="M116 62.325C115.767 62.325 115.533 62.2833 115.3 62.2C115.067 62.1167 114.858 61.9917 114.675 61.825C113.592 60.825 112.633 59.85 111.8 58.9C110.967 57.95 110.267 57.0333 109.7 56.15C109.15 55.25 108.725 54.3917 108.425 53.575C108.142 52.7417 108 51.95 108 51.2C108 48.7 108.8 46.7083 110.4 45.225C112.017 43.7417 113.883 43 116 43C118.117 43 119.975 43.7417 121.575 45.225C123.192 46.7083 124 48.7 124 51.2C124 51.95 123.85 52.7417 123.55 53.575C123.267 54.3917 122.842 55.25 122.275 56.15C121.725 57.0333 121.033 57.95 120.2 58.9C119.367 59.85 118.408 60.825 117.325 61.825C117.142 61.9917 116.933 62.1167 116.7 62.2C116.467 62.2833 116.233 62.325 116 62.325Z" fill="black" fill-opacity="0.32"/>
</g>
<defs>
<clipPath id="clip0_1738_5539">
<rect width="160" height="160" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,13 @@
<svg width="160" height="72" viewBox="0 0 160 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1738_5535)">
<path d="M0 4C0 1.79086 1.79086 0 4 0H28C30.2091 0 32 1.79086 32 4V4C32 6.20914 30.2091 8 28 8H4C1.79086 8 0 6.20914 0 4V4Z" fill="black" fill-opacity="0.32"/>
<rect y="12" width="50.6667" height="60" rx="8" fill="white"/>
<rect x="0.5" y="12.5" width="49.6667" height="59" rx="7.5" stroke="black" stroke-opacity="0.12"/>
<path d="M32.3177 42.9844H26.3177V48.9844H24.349V42.9844H18.349V41.0156H24.349V35.0156H26.3177V41.0156H32.3177V42.9844Z" fill="#03A9F4"/>
</g>
<defs>
<clipPath id="clip0_1738_5535">
<rect width="160" height="72" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 712 B

View File

@@ -0,0 +1,16 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1738_5537)">
<rect width="160" height="160" rx="6" fill="white"/>
<rect x="0.5" y="0.5" width="159" height="159" rx="5.5" stroke="black" stroke-opacity="0.12"/>
<rect x="8" y="8" width="144" height="11" rx="5.5" fill="black" fill-opacity="0.32"/>
<rect x="8" y="27" width="45.3333" height="106" rx="4" fill="#E9E9E9"/>
<rect x="57.3333" y="27" width="45.3333" height="30" rx="4" fill="#E9E9E9"/>
<rect x="106.667" y="27" width="45.3333" height="68" rx="4" fill="#E9E9E9"/>
<path d="M84.3594 82.0156H87.7344C87.9219 81.1406 88.0156 80.4688 88.0156 80C88.0156 79.5312 87.9219 78.8594 87.7344 77.9844H84.3594C84.4531 78.6406 84.5 79.3125 84.5 80C84.5 80.6875 84.4531 81.3594 84.3594 82.0156ZM82.5781 87.5469C83.3594 87.2969 84.1719 86.8281 85.0156 86.1406C85.8594 85.4219 86.5 84.7031 86.9375 83.9844H83.9844C83.6719 85.2344 83.2031 86.4219 82.5781 87.5469ZM82.3438 82.0156C82.4375 81.3594 82.4844 80.6875 82.4844 80C82.4844 79.3125 82.4375 78.6406 82.3438 77.9844H77.6562C77.5625 78.6406 77.5156 79.3125 77.5156 80C77.5156 80.6875 77.5625 81.3594 77.6562 82.0156H82.3438ZM80 87.9688C80.875 86.6875 81.5156 85.3594 81.9219 83.9844H78.0781C78.4844 85.3594 79.125 86.6875 80 87.9688ZM76.0156 76.0156C76.3906 74.6719 76.8594 73.4844 77.4219 72.4531C76.6406 72.7031 75.8125 73.1875 74.9375 73.9062C74.0938 74.5938 73.4688 75.2969 73.0625 76.0156H76.0156ZM73.0625 83.9844C73.4688 84.7031 74.0938 85.4219 74.9375 86.1406C75.8125 86.8281 76.6406 87.2969 77.4219 87.5469C76.7969 86.4219 76.3281 85.2344 76.0156 83.9844H73.0625ZM72.2656 82.0156H75.6406C75.5469 81.3594 75.5 80.6875 75.5 80C75.5 79.3125 75.5469 78.6406 75.6406 77.9844H72.2656C72.0781 78.8594 71.9844 79.5312 71.9844 80C71.9844 80.4688 72.0781 81.1406 72.2656 82.0156ZM80 72.0312C79.125 73.3125 78.4844 74.6406 78.0781 76.0156H81.9219C81.5156 74.6406 80.875 73.3125 80 72.0312ZM86.9375 76.0156C86.5 75.2969 85.8594 74.5938 85.0156 73.9062C84.1719 73.1875 83.3594 72.7031 82.5781 72.4531C83.1406 73.4844 83.6094 74.6719 83.9844 76.0156H86.9375ZM72.9219 72.9688C74.8906 71 77.25 70.0156 80 70.0156C82.75 70.0156 85.0938 71 87.0312 72.9688C89 74.9062 89.9844 77.25 89.9844 80C89.9844 82.75 89 85.1094 87.0312 87.0781C85.0938 89.0156 82.75 89.9844 80 89.9844C77.25 89.9844 74.8906 89.0156 72.9219 87.0781C70.9844 85.1094 70.0156 82.75 70.0156 80C70.0156 77.25 70.9844 74.9062 72.9219 72.9688Z" fill="#03A9F4"/>
</g>
<defs>
<clipPath id="clip0_1738_5537">
<rect width="160" height="160" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

View File

@@ -93,8 +93,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
background-color: var(--primary-background-color, #fafafa);
}
p {
font-size: 14px;
line-height: 20px;
font-size: var(--ha-font-size-m);
line-height: var(--ha-line-height-normal);
}
.card-content {
background: var(
@@ -151,8 +151,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
margin-inline-start: initial;
}
h1 {
font-size: 28px;
font-weight: 400;
font-size: var(--ha-font-size-3xl);
font-weight: var(--ha-font-weight-normal);
margin-top: 16px;
margin-bottom: 16px;
}

View File

@@ -57,9 +57,9 @@ export class HaPickAuthProvider extends LitElement {
position: relative;
z-index: 1;
text-align: center;
font-size: 14px;
font-weight: 400;
line-height: 20px;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-normal);
line-height: var(--ha-line-height-normal);
}
h3:before {
border-top: 1px solid var(--divider-color);

View File

@@ -11,7 +11,6 @@ export const COLORS = [
"#9c6b4e",
"#97bbf5",
"#01ab63",
"#9498a0",
"#094bad",
"#c99000",
"#d84f3e",
@@ -21,7 +20,6 @@ export const COLORS = [
"#8043ce",
"#7599d1",
"#7a4c31",
"#74787f",
"#6989f4",
"#ffd444",
"#ff957c",
@@ -31,7 +29,6 @@ export const COLORS = [
"#c884ff",
"#badeff",
"#bf8b6d",
"#b6bac2",
"#927acc",
"#97ee3f",
"#bf3947",
@@ -44,7 +41,6 @@ export const COLORS = [
"#d9b100",
"#9d7a00",
"#698cff",
"#d9d9d9",
"#00d27e",
"#d06800",
"#009f82",

View File

@@ -10,7 +10,6 @@ import type { LitElement } from "lit";
export interface DragScrollControllerConfig {
selector: string;
enabled?: boolean;
trackScroll?: boolean;
}
export class DragScrollController implements ReactiveController {
@@ -24,10 +23,6 @@ export class DragScrollController implements ReactiveController {
public scrollLeft = 0;
public scrolledStart = false;
public scrolledEnd = false;
private _host: ReactiveControllerHost & LitElement;
private _selector: string;
@@ -36,8 +31,6 @@ export class DragScrollController implements ReactiveController {
private _enabled = true;
private _trackScroll = false;
public get enabled(): boolean {
return this._enabled;
}
@@ -57,11 +50,10 @@ export class DragScrollController implements ReactiveController {
constructor(
host: ReactiveControllerHost & LitElement,
{ selector, enabled, trackScroll }: DragScrollControllerConfig
{ selector, enabled }: DragScrollControllerConfig
) {
this._selector = selector;
this._host = host;
this._trackScroll = trackScroll ?? false;
this.enabled = enabled ?? true;
host.addController(this);
}
@@ -83,14 +75,6 @@ export class DragScrollController implements ReactiveController {
);
if (this._scrollContainer) {
this._scrollContainer.addEventListener("mousedown", this._mouseDown);
if (this._trackScroll) {
this._scrollContainer.addEventListener("scroll", this._onScroll);
this.scrolledStart = this._scrollContainer.scrollLeft > 0;
this.scrolledEnd =
this._scrollContainer.scrollLeft + this._scrollContainer.offsetWidth <
this._scrollContainer.scrollWidth;
this._host.requestUpdate();
}
}
}
@@ -99,34 +83,15 @@ export class DragScrollController implements ReactiveController {
window.removeEventListener("mouseup", this._mouseUp);
if (this._scrollContainer) {
this._scrollContainer.removeEventListener("mousedown", this._mouseDown);
this._scrollContainer.removeEventListener("scroll", this._onScroll);
this._scrollContainer = undefined;
}
this.scrolled = false;
this.scrolling = false;
this.scrolledStart = false;
this.scrolledEnd = false;
this.mouseIsDown = false;
this.scrollStartX = 0;
this.scrollLeft = 0;
}
private _onScroll = (event: Event) => {
const oldScrolledStart = this.scrolledStart;
const oldScrolledEnd = this.scrolledEnd;
const container = event.currentTarget as HTMLElement;
this.scrolledStart = container.scrollLeft > 0;
this.scrolledEnd =
container.scrollLeft + container.offsetWidth < container.scrollWidth;
if (
this.scrolledStart !== oldScrolledStart ||
this.scrolledEnd !== oldScrolledEnd
) {
this._host.requestUpdate();
}
};
private _mouseDown = (event: MouseEvent) => {
const scrollContainer = this._scrollContainer;

View File

@@ -77,7 +77,7 @@ export const formatDateNumeric = (
const month = parts.find((value) => value.type === "month")?.value;
const year = parts.find((value) => value.type === "year")?.value;
const lastPart = parts.at(parts.length - 1);
const lastPart = parts[parts.length - 1];
let lastLiteral = lastPart?.type === "literal" ? lastPart?.value : "";
if (locale.language === "bg" && locale.date_format === DateFormat.YMD) {

View File

@@ -202,7 +202,6 @@ export function storage(options: {
// Don't set the initial value if we have a value in localStorage
if (this.__initialized || getValue() === undefined) {
setValue(this, value);
this.requestUpdate(propertyKey, undefined);
}
},
configurable: true,
@@ -212,11 +211,13 @@ export function storage(options: {
const oldSetter = descriptor.set;
newDescriptor = {
...descriptor,
get(this: ReactiveStorageElement) {
return getValue();
},
set(this: ReactiveStorageElement, value) {
// Don't set the initial value if we have a value in localStorage
if (this.__initialized || getValue() === undefined) {
setValue(this, value);
this.requestUpdate(propertyKey, undefined);
}
oldSetter?.call(this, value);
},

View File

@@ -0,0 +1,68 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "./compute_state_domain";
import { isUnavailableState, UNAVAILABLE } from "../../data/entity";
import type { HomeAssistant } from "../../types";
export const computeGroupEntitiesState = (states: HassEntity[]): string => {
if (!states.length) {
return UNAVAILABLE;
}
const validState = states.filter((stateObj) => isUnavailableState(stateObj));
if (!validState) {
return UNAVAILABLE;
}
// Use the first state to determine the domain
// This assumes all states in the group have the same domain
const domain = computeStateDomain(states[0]);
if (domain === "cover") {
for (const s of ["opening", "closing", "open"]) {
if (states.some((stateObj) => stateObj.state === s)) {
return s;
}
}
return "closed";
}
if (states.some((stateObj) => stateObj.state === "on")) {
return "on";
}
return "off";
};
export const toggleGroupEntities = (
hass: HomeAssistant,
states: HassEntity[]
) => {
if (!states.length) {
return;
}
// Use the first state to determine the domain
// This assumes all states in the group have the same domain
const domain = computeStateDomain(states[0]);
const state = computeGroupEntitiesState(states);
const isOn = state === "on" || state === "open";
let service = isOn ? "turn_off" : "turn_on";
if (domain === "cover") {
if (state === "opening" || state === "closing") {
// If the cover is opening or closing, we toggle it to stop it
service = "stop_cover";
} else {
// For covers, we use the open/close service
service = isOn ? "close_cover" : "open_cover";
}
}
const entitiesIds = states.map((stateObj) => stateObj.entity_id);
hass.callService(domain, service, {
entity_id: entitiesIds,
});
};

View File

@@ -64,15 +64,27 @@ export const domainStateColorProperties = (
const compareState = state !== undefined ? state : stateObj.state;
const active = stateActive(stateObj, state);
return domainColorProperties(
domain,
stateObj.attributes.device_class,
compareState,
active
);
};
export const domainColorProperties = (
domain: string,
deviceClass: string | undefined,
state: string,
active: boolean
) => {
const properties: string[] = [];
const stateKey = slugify(compareState, "_");
const stateKey = slugify(state, "_");
const activeKey = active ? "active" : "inactive";
const dc = stateObj.attributes.device_class;
if (dc) {
properties.push(`--state-${domain}-${dc}-${stateKey}-color`);
if (deviceClass) {
properties.push(`--state-${domain}-${deviceClass}-${stateKey}-color`);
}
properties.push(

View File

@@ -2,7 +2,6 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "./compute_state_domain";
import { updateIcon } from "./update_icon";
import { deviceTrackerIcon } from "./device_tracker_icon";
import { batteryIcon } from "./battery_icon";
export const stateIcon = (
stateObj: HassEntity,
@@ -10,17 +9,10 @@ export const stateIcon = (
): string | undefined => {
const domain = computeStateDomain(stateObj);
const compareState = state ?? stateObj.state;
const dc = stateObj.attributes.device_class;
switch (domain) {
case "update":
return updateIcon(stateObj, compareState);
case "sensor":
if (dc === "battery") {
return batteryIcon(stateObj, compareState);
}
break;
case "device_tracker":
return deviceTrackerIcon(stateObj, compareState);

View File

@@ -0,0 +1,4 @@
const validServiceId = /^(\w+)\.(\w+)$/;
export const isValidServiceId = (actionId: string) =>
validServiceId.test(actionId);

View File

@@ -1,12 +1,14 @@
import memoizeOne from "memoize-one";
import { isIPAddress } from "./is_ip_address";
const collator = memoizeOne(
(language: string | undefined) => new Intl.Collator(language)
(language: string | undefined) =>
new Intl.Collator(language, { numeric: true })
);
const caseInsensitiveCollator = memoizeOne(
(language: string | undefined) =>
new Intl.Collator(language, { sensitivity: "accent" })
new Intl.Collator(language, { sensitivity: "accent", numeric: true })
);
const fallbackStringCompare = (a: string, b: string) => {
@@ -33,6 +35,19 @@ export const stringCompare = (
return fallbackStringCompare(a, b);
};
export const ipCompare = (a: string, b: string) => {
const aIsIpV4 = isIPAddress(a);
const bIsIpV4 = isIPAddress(b);
if (aIsIpV4 && bIsIpV4) {
return ipv4Compare(a, b);
}
if (!aIsIpV4 && !bIsIpV4) {
return ipV6Compare(a, b);
}
return aIsIpV4 ? -1 : 1;
};
export const caseInsensitiveStringCompare = (
a: string,
b: string,
@@ -64,3 +79,42 @@ export const orderCompare = (order: string[]) => (a: string, b: string) => {
return idxA - idxB;
};
function ipv4Compare(a: string, b: string) {
const num1 = Number(
a
.split(".")
.map((num) => num.padStart(3, "0"))
.join("")
);
const num2 = Number(
b
.split(".")
.map((num) => num.padStart(3, "0"))
.join("")
);
return num1 - num2;
}
function ipV6Compare(a: string, b: string) {
const ipv6a = normalizeIPv6(a)
.split(":")
.map((part) => part.padStart(4, "0"))
.join("");
const ipv6b = normalizeIPv6(b)
.split(":")
.map((part) => part.padStart(4, "0"))
.join("");
return ipv6a.localeCompare(ipv6b);
}
function normalizeIPv6(ip) {
const parts = ip.split("::");
const head = parts[0].split(":");
const tail = parts[1] ? parts[1].split(":") : [];
const totalParts = 8;
const missing = totalParts - (head.length + tail.length);
const zeros = new Array(missing).fill("0");
return [...head, ...zeros, ...tail].join(":");
}

View File

@@ -1,9 +1,19 @@
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
export const slugify = (value: string, delimiter = "_") => {
const a =
"àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìıİłḿñńǹňôöòóœøōõőŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·";
const b = `aaaaaaaaaacccddeeeeeeeegghiiiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}`;
"àáâäæãåāăąабçćčđďдèéêëēėęěеёэфğǵгḧхîïíīįìıİийкłлḿмñńǹňнôöòóœøōõőоṕпŕřрßśšşșсťțтûüùúūǘůűųувẃẍÿýыžźżз·";
const b = `aaaaaaaaaaabcccdddeeeeeeeeeeefggghhiiiiiiiiijkllmmnnnnnoooooooooopprrrsssssstttuuuuuuuuuuvwxyyyzzzz${delimiter}`;
const p = new RegExp(a.split("").join("|"), "g");
const complex_cyrillic = {
ж: "zh",
х: "kh",
ц: "ts",
ч: "ch",
ш: "sh",
щ: "shch",
ю: "iu",
я: "ia",
};
let slugified;
@@ -14,6 +24,7 @@ export const slugify = (value: string, delimiter = "_") => {
.toString()
.toLowerCase()
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
.replace(/[а-я]/g, (c) => complex_cyrillic[c] || "") // Replace some cyrillic characters
.replace(/(\d),(?=\d)/g, "$1") // Remove Commas between numbers
.replace(/[^a-z0-9]+/g, delimiter) // Replace all non-word characters
.replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter

View File

@@ -2,14 +2,16 @@ import type { CSSResult } from "lit";
const _extractCssVars = (
cssString: string,
condition: (string) => boolean = () => true
condition: (string: string) => boolean = () => true
) => {
const variables: Record<string, string> = {};
cssString.split(";").forEach((rawLine) => {
const line = rawLine.substring(rawLine.indexOf("--")).trim();
if (line.startsWith("--") && condition(line)) {
const [name, value] = line.split(":").map((part) => part.trim());
const [name, value] = line
.split(":")
.map((part) => part.replaceAll("}", "").trim());
variables[name.substring(2, name.length)] = value;
}
});
@@ -25,7 +27,10 @@ export const extractVar = (css: CSSResult, varName: string) => {
}
const endIndex = cssString.indexOf(";", startIndex + search.length);
return cssString.substring(startIndex + search.length, endIndex).trim();
return cssString
.substring(startIndex + search.length, endIndex)
.replaceAll("}", "")
.trim();
};
export const extractVars = (css: CSSResult) => {

View File

@@ -31,7 +31,8 @@ export type LocalizeKeys =
| `ui.panel.lovelace.card.${string}`
| `ui.panel.lovelace.editor.${string}`
| `ui.panel.page-authorize.form.${string}`
| `component.${string}`;
| `component.${string}`
| `ui.entity.${string}`;
export type LandingPageKeys = FlattenObjectKeys<
TranslationDict["landing-page"]

View File

@@ -0,0 +1,14 @@
import { html } from "lit";
import type { LocalizeFunc } from "./localize";
const MARKDOWN_SUPPORT_URL = "https://commonmark.org/help/";
export const supportsMarkdownHelper = (localize: LocalizeFunc) =>
localize("ui.common.supports_markdown", {
markdown_help_link: html`<a
href=${MARKDOWN_SUPPORT_URL}
target="_blank"
rel="noreferrer"
>${localize("ui.common.markdown")}</a
>`,
});

View File

@@ -0,0 +1,72 @@
import type { LineSeriesOption } from "echarts";
export function downSampleLineData(
data: LineSeriesOption["data"],
chartWidth: number,
minX?: number,
maxX?: number
) {
if (!data || data.length < 10) {
return data;
}
const width = chartWidth * window.devicePixelRatio;
if (data.length <= width) {
return data;
}
const min = minX ?? getPointData(data[0]!)[0];
const max = maxX ?? getPointData(data[data.length - 1]!)[0];
const step = Math.floor((max - min) / width);
const frames = new Map<
number,
{
min: { point: (typeof data)[number]; x: number; y: number };
max: { point: (typeof data)[number]; x: number; y: number };
}
>();
// Group points into frames
for (const point of data) {
const pointData = getPointData(point);
if (!Array.isArray(pointData)) continue;
const x = Number(pointData[0]);
const y = Number(pointData[1]);
if (isNaN(x) || isNaN(y)) continue;
const frameIndex = Math.floor((x - min) / step);
const frame = frames.get(frameIndex);
if (!frame) {
frames.set(frameIndex, { min: { point, x, y }, max: { point, x, y } });
} else {
if (frame.min.y > y) {
frame.min = { point, x, y };
}
if (frame.max.y < y) {
frame.max = { point, x, y };
}
}
}
// Convert frames back to points
const result: typeof data = [];
for (const [_i, frame] of frames) {
// Use min/max points to preserve visual accuracy
// The order of the data must be preserved so max may be before min
if (frame.min.x > frame.max.x) {
result.push(frame.max.point);
}
result.push(frame.min.point);
if (frame.min.x < frame.max.x) {
result.push(frame.max.point);
}
}
return result;
}
function getPointData(point: NonNullable<LineSeriesOption["data"]>[number]) {
const pointData =
point && typeof point === "object" && "value" in point
? point.value
: point;
return pointData as number[];
}

View File

@@ -9,6 +9,7 @@ import type {
LegendComponentOption,
XAXisOption,
YAXisOption,
LineSeriesOption,
} from "echarts/types/dist/shared";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
@@ -27,6 +28,7 @@ import "../ha-icon-button";
import { formatTimeLabel } from "./axis-label";
import { ensureArray } from "../../common/array/ensure-array";
import "../chips/ha-assist-chip";
import { downSampleLineData } from "./down-sample";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
const LEGEND_OVERFLOW_LIMIT = 10;
@@ -48,7 +50,11 @@ export class HaChartBase extends LitElement {
@property({ attribute: "expand-legend", type: Boolean })
public expandLegend?: boolean;
@property({ attribute: false }) public extraComponents?: any[];
@property({ attribute: "small-controls", type: Boolean })
public smallControls?: boolean;
// extraComponents is not reactive and should not trigger updates
public extraComponents?: any[];
@state()
@consume({ context: themesContext, subscribe: true })
@@ -106,48 +112,49 @@ export class HaChartBase extends LitElement {
})
);
// Add keyboard event listeners
const handleKeyDown = (ev: KeyboardEvent) => {
if (
!this._modifierPressed &&
((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
) {
this._modifierPressed = true;
if (!this.options?.dataZoom) {
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
if (!this.options?.dataZoom) {
// Add keyboard event listeners
const handleKeyDown = (ev: KeyboardEvent) => {
if (
!this._modifierPressed &&
((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
) {
this._modifierPressed = true;
if (!this.options?.dataZoom) {
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
}
// drag to zoom
this.chart?.dispatchAction({
type: "takeGlobalCursor",
key: "dataZoomSelect",
dataZoomSelectActive: true,
});
}
// drag to zoom
this.chart?.dispatchAction({
type: "takeGlobalCursor",
key: "dataZoomSelect",
dataZoomSelectActive: true,
});
}
};
};
const handleKeyUp = (ev: KeyboardEvent) => {
if (
this._modifierPressed &&
((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
) {
this._modifierPressed = false;
if (!this.options?.dataZoom) {
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
const handleKeyUp = (ev: KeyboardEvent) => {
if (
this._modifierPressed &&
((isMac && ev.key === "Meta") || (!isMac && ev.key === "Control"))
) {
this._modifierPressed = false;
if (!this.options?.dataZoom) {
this._setChartOptions({ dataZoom: this._getDataZoomConfig() });
}
this.chart?.dispatchAction({
type: "takeGlobalCursor",
key: "dataZoomSelect",
dataZoomSelectActive: false,
});
}
this.chart?.dispatchAction({
type: "takeGlobalCursor",
key: "dataZoomSelect",
dataZoomSelectActive: false,
});
}
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
this._listeners.push(
() => window.removeEventListener("keydown", handleKeyDown),
() => window.removeEventListener("keyup", handleKeyUp)
);
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
this._listeners.push(
() => window.removeEventListener("keydown", handleKeyDown),
() => window.removeEventListener("keyup", handleKeyUp)
);
}
}
protected firstUpdated() {
@@ -191,16 +198,19 @@ export class HaChartBase extends LitElement {
<div class="chart"></div>
</div>
${this._renderLegend()}
${this._isZoomed
? html`<ha-icon-button
class="zoom-reset"
.path=${mdiRestart}
@click=${this._handleZoomReset}
title=${this.hass.localize(
"ui.components.history_charts.zoom_reset"
)}
></ha-icon-button>`
: nothing}
<div class="chart-controls ${classMap({ small: this.smallControls })}">
${this._isZoomed
? html`<ha-icon-button
class="zoom-reset"
.path=${mdiRestart}
@click=${this._handleZoomReset}
title=${this.hass.localize(
"ui.components.history_charts.zoom_reset"
)}
></ha-icon-button>`
: nothing}
<slot name="button"></slot>
</div>
</div>
`;
}
@@ -210,15 +220,15 @@ export class HaChartBase extends LitElement {
return nothing;
}
const legend = ensureArray(this.options.legend)[0] as LegendComponentOption;
if (!legend.show) {
if (!legend.show || legend.type !== "custom") {
return nothing;
}
const datasets = ensureArray(this.data);
const items = (legend.data ||
datasets
const items: LegendComponentOption["data"] =
legend.data ||
((datasets
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
.map((d) => d.name ?? d.id) ||
[]) as string[];
.map((d) => d.name ?? d.id) || []) as string[]);
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
@@ -233,20 +243,32 @@ export class HaChartBase extends LitElement {
})}
>
<ul>
${items.map((item: string, index: number) => {
${items.map((item, index) => {
if (!this.expandLegend && index >= overflowLimit) {
return nothing;
}
const dataset = datasets.find(
(d) => d.id === item || d.name === item
);
const color = dataset?.color as string;
const borderColor = dataset?.itemStyle?.borderColor as string;
let itemStyle: Record<string, any> = {};
let name = "";
if (typeof item === "string") {
name = item;
const dataset = datasets.find(
(d) => d.id === item || d.name === item
);
itemStyle = {
color: dataset?.color as string,
...(dataset?.itemStyle as { borderColor?: string }),
};
} else {
name = item.name ?? "";
itemStyle = item.itemStyle ?? {};
}
const color = itemStyle?.color as string;
const borderColor = itemStyle?.borderColor as string;
return html`<li
.name=${item}
.name=${name}
@click=${this._legendClick}
class=${classMap({ hidden: this._hiddenDatasets.has(item) })}
.title=${item}
class=${classMap({ hidden: this._hiddenDatasets.has(name) })}
.title=${name}
>
<div
class="bullet"
@@ -255,7 +277,7 @@ export class HaChartBase extends LitElement {
borderColor: borderColor || color,
})}
></div>
<div class="label">${item}</div>
<div class="label">${name}</div>
</li>`;
})}
${items.length > overflowLimit
@@ -315,7 +337,9 @@ export class HaChartBase extends LitElement {
this.chart.on("click", (e: ECElementEvent) => {
fireEvent(this, "chart-click", e);
});
this.chart.getZr().on("dblclick", this._handleClickZoom);
if (!this.options?.dataZoom) {
this.chart.getZr().on("dblclick", this._handleClickZoom);
}
if (this._isTouchDevice) {
this.chart.getZr().on("click", (e: ECElementEvent) => {
if (!e.zrByTouch) {
@@ -366,6 +390,7 @@ export class HaChartBase extends LitElement {
type: "inside",
orient: "horizontal",
filterMode: "none",
xAxisIndex: 0,
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
zoomLock: !this._isTouchDevice && !this._modifierPressed,
@@ -380,9 +405,9 @@ export class HaChartBase extends LitElement {
if (axis.type !== "time" || axis.show === false) {
return axis;
}
if (axis.max && axis.min) {
if (axis.min) {
this._minutesDifference = differenceInMinutes(
axis.max as Date,
(axis.max as Date) || new Date(),
axis.min as Date
);
}
@@ -410,6 +435,12 @@ export class HaChartBase extends LitElement {
} as XAXisOption;
});
}
let legend = this.options?.legend;
if (legend) {
legend = ensureArray(legend).map((l) =>
l.type === "custom" ? { show: false } : l
);
}
const options = {
animation: !this._reducedMotion,
darkMode: this._themes.darkMode ?? false,
@@ -424,7 +455,7 @@ export class HaChartBase extends LitElement {
iconStyle: { opacity: 0 },
},
...this.options,
legend: { show: false },
legend,
xAxis,
};
@@ -468,6 +499,13 @@ export class HaChartBase extends LitElement {
smooth: false,
},
bar: { itemStyle: { barBorderWidth: 1.5 } },
graph: {
label: {
color: style.getPropertyValue("--primary-text-color"),
textBorderColor: style.getPropertyValue("--primary-background-color"),
textBorderWidth: 2,
},
},
categoryAxis: {
axisLine: { show: false },
axisTick: { show: false },
@@ -600,32 +638,52 @@ export class HaChartBase extends LitElement {
}
private _getSeries() {
const series = ensureArray(this.data).filter(
(d) => !this._hiddenDatasets.has(String(d.name ?? d.id))
);
const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
| XAXisOption
| undefined;
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
| YAXisOption
| undefined;
if (yAxis?.type === "log") {
// set <=0 values to null so they render as gaps on a log graph
return series.map((d) =>
d.type === "line"
? {
...d,
data: d.data?.map((v) =>
Array.isArray(v)
? [
v[0],
typeof v[1] !== "number" || v[1] > 0 ? v[1] : null,
...v.slice(2),
]
: v
),
}
: d
);
}
return series;
const series = ensureArray(this.data).map((s) => {
const data = this._hiddenDatasets.has(String(s.name ?? s.id))
? undefined
: s.data;
if (data && s.type === "line") {
if (yAxis?.type === "log") {
// set <=0 values to null so they render as gaps on a log graph
return {
...s,
data: (data as LineSeriesOption["data"])!.map((v) =>
Array.isArray(v)
? [
v[0],
typeof v[1] !== "number" || v[1] > 0 ? v[1] : null,
...v.slice(2),
]
: v
),
};
}
if (s.sampling === "minmax") {
const minX =
xAxis?.min && typeof xAxis.min === "number" ? xAxis.min : undefined;
const maxX =
xAxis?.max && typeof xAxis.max === "number" ? xAxis.max : undefined;
return {
...s,
sampling: undefined,
data: downSampleLineData(
data as LineSeriesOption["data"],
this.clientWidth,
minX,
maxX
),
};
}
}
return { ...s, data };
});
return series as ECOption["series"];
}
private _getDefaultHeight() {
@@ -725,21 +783,40 @@ export class HaChartBase extends LitElement {
height: 100%;
width: 100%;
}
.zoom-reset {
.chart-controls {
position: absolute;
top: 16px;
right: 4px;
display: flex;
flex-direction: column;
gap: 4px;
}
.chart-controls.small {
top: 0;
flex-direction: row;
}
.chart-controls ha-icon-button,
.chart-controls ::slotted(ha-icon-button) {
background: var(--card-background-color);
border-radius: 4px;
--mdc-icon-button-size: 32px;
color: var(--primary-color);
border: 1px solid var(--divider-color);
}
.chart-controls.small ha-icon-button,
.chart-controls.small ::slotted(ha-icon-button) {
--mdc-icon-button-size: 22px;
--mdc-icon-size: 16px;
}
.chart-controls ha-icon-button.inactive,
.chart-controls ::slotted(ha-icon-button.inactive) {
color: var(--state-inactive-color);
}
.chart-legend {
max-height: 60%;
overflow-y: auto;
padding: 12px 0 0;
font-size: 12px;
font-size: var(--ha-font-size-s);
color: var(--primary-text-color);
}
.chart-legend ul {

View File

@@ -0,0 +1,299 @@
import type { EChartsType } from "echarts/core";
import type { GraphSeriesOption } from "echarts/charts";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import type { TopLevelFormatterParams } from "echarts/types/dist/shared";
import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
import memoizeOne from "memoize-one";
import { listenMediaQuery } from "../../common/dom/media_query";
import type { ECOption } from "../../resources/echarts";
import "./ha-chart-base";
import type { HaChartBase } from "./ha-chart-base";
import type { HomeAssistant } from "../../types";
export interface NetworkNode {
id: string;
name?: string;
category?: number;
label?: string;
value?: number;
symbolSize?: number;
symbol?: string;
itemStyle?: {
color?: string;
borderColor?: string;
borderWidth?: number;
};
fixed?: boolean;
/**
* Distance from the center, where 0 is the center and 1 is the edge
*/
polarDistance?: number;
}
export interface NetworkLink {
source: string;
target: string;
value?: number;
reverseValue?: number;
lineStyle?: {
width?: number;
color?: string;
type?: "solid" | "dashed" | "dotted";
};
symbolSize?: number | number[];
symbol?: string;
label?: {
show?: boolean;
formatter?: string;
};
ignoreForceLayout?: boolean;
}
export interface NetworkData {
nodes: NetworkNode[];
links: NetworkLink[];
categories?: { name: string; symbol: string }[];
}
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
let GraphChart: typeof import("echarts/lib/chart/graph/install");
@customElement("ha-network-graph")
export class HaNetworkGraph extends LitElement {
public chart?: EChartsType;
@property({ attribute: false }) public data!: NetworkData;
@property({ attribute: false }) public tooltipFormatter?: (
params: TopLevelFormatterParams
) => string;
public hass!: HomeAssistant;
@state() private _reducedMotion = false;
@state() private _physicsEnabled = true;
@state() private _showLabels = true;
private _listeners: (() => void)[] = [];
private _nodePositions: Record<string, { x: number; y: number }> = {};
@query("ha-chart-base") private _baseChart?: HaChartBase;
constructor() {
super();
if (!GraphChart) {
import("echarts/lib/chart/graph/install").then((module) => {
GraphChart = module;
this.requestUpdate();
});
}
}
public async connectedCallback() {
super.connectedCallback();
this._listeners.push(
listenMediaQuery("(prefers-reduced-motion)", (matches) => {
if (this._reducedMotion !== matches) {
this._reducedMotion = matches;
}
})
);
}
public disconnectedCallback() {
super.disconnectedCallback();
while (this._listeners.length) {
this._listeners.pop()!();
}
}
protected render() {
if (!GraphChart) {
return nothing;
}
return html`<ha-chart-base
.hass=${this.hass}
.data=${this._getSeries(
this.data,
this._physicsEnabled,
this._reducedMotion,
this._showLabels
)}
.options=${this._createOptions(this.data?.categories)}
height="100%"
.extraComponents=${[GraphChart]}
>
<slot name="button" slot="button"></slot>
<ha-icon-button
slot="button"
class=${this._physicsEnabled ? "active" : "inactive"}
.path=${mdiGoogleCirclesGroup}
@click=${this._togglePhysics}
label=${this.hass.localize(
"ui.panel.config.common.graph.toggle_physics"
)}
></ha-icon-button>
<ha-icon-button
slot="button"
class=${this._showLabels ? "active" : "inactive"}
.path=${mdiFormatTextVariant}
@click=${this._toggleLabels}
label=${this.hass.localize(
"ui.panel.config.common.graph.toggle_labels"
)}
></ha-icon-button>
</ha-chart-base>`;
}
private _createOptions = memoizeOne(
(categories?: NetworkData["categories"]): ECOption => ({
tooltip: {
trigger: "item",
confine: true,
formatter: this.tooltipFormatter,
},
legend: {
show: !!categories?.length,
data: categories?.map((category) => ({
...category,
icon: category.symbol,
})),
top: 8,
},
dataZoom: {
type: "inside",
filterMode: "none",
},
})
);
private _getSeries = memoizeOne(
(
data: NetworkData,
physicsEnabled: boolean,
reducedMotion: boolean,
showLabels: boolean
) => {
const containerWidth = this.clientWidth;
const containerHeight = this.clientHeight;
return [
{
id: "network",
type: "graph",
layout: physicsEnabled ? "force" : "none",
draggable: true,
roam: true,
selectedMode: "single",
label: {
show: showLabels,
position: "right",
},
emphasis: {
focus: "adjacency",
},
force: {
repulsion: [400, 600],
edgeLength: [200, 300],
gravity: 0.1,
layoutAnimation: !reducedMotion && data.nodes.length < 100,
},
edgeSymbol: ["none", "arrow"],
edgeSymbolSize: 10,
data: data.nodes.map((node) => {
const echartsNode: NonNullable<GraphSeriesOption["data"]>[number] =
{
id: node.id,
name: node.name,
category: node.category,
value: node.value,
symbolSize: node.symbolSize || 30,
symbol: node.symbol || "circle",
itemStyle: node.itemStyle || {},
fixed: node.fixed,
};
if (this._nodePositions[node.id]) {
echartsNode.x = this._nodePositions[node.id].x;
echartsNode.y = this._nodePositions[node.id].y;
} else if (typeof node.polarDistance === "number") {
// set the position of the node at polarDistance from the center in a random direction
const angle = Math.random() * 2 * Math.PI;
echartsNode.x =
containerWidth / 2 +
((Math.cos(angle) * containerWidth) / 2) * node.polarDistance;
echartsNode.y =
containerHeight / 2 +
((Math.sin(angle) * containerHeight) / 2) * node.polarDistance;
this._nodePositions[node.id] = {
x: echartsNode.x,
y: echartsNode.y,
};
}
return echartsNode;
}),
links: data.links.map((link) => ({
...link,
value: link.reverseValue
? Math.max(link.value ?? 0, link.reverseValue)
: link.value,
// remove arrow for bidirectional links
symbolSize: link.reverseValue ? 1 : link.symbolSize, // 0 doesn't work
})),
categories: data.categories || [],
},
] as any;
}
);
private _togglePhysics() {
if (this._baseChart?.chart) {
this._baseChart.chart
// @ts-ignore private method but no other way to get the graph positions
.getModel()
.getSeriesByIndex(0)
.getGraph()
.eachNode((node: any) => {
const layout = node.getLayout();
if (layout) {
this._nodePositions[node.id] = {
x: layout[0],
y: layout[1],
};
}
});
}
this._physicsEnabled = !this._physicsEnabled;
}
private _toggleLabels() {
this._showLabels = !this._showLabels;
}
static styles = css`
:host {
display: block;
position: relative;
}
ha-chart-base {
height: 100%;
--chart-max-height: 100%;
}
ha-icon-button,
::slotted(ha-icon-button) {
margin-right: 12px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-network-graph": HaNetworkGraph;
}
interface HASSDomEvents {
"node-selected": { id: string };
}
}

View File

@@ -105,10 +105,41 @@ export class HaSankeyChart extends LitElement {
private _createData = memoizeOne((data: SankeyChartData, width = 0) => {
const filteredNodes = data.nodes.filter((n) => n.value > 0);
const indexes = [...new Set(filteredNodes.map((n) => n.index))];
const indexes = [...new Set(filteredNodes.map((n) => n.index))].sort();
const depthMap = new Map<number, number>();
indexes.sort().forEach((index, i) => {
const sections: Node[][] = [];
indexes.forEach((index, i) => {
depthMap.set(index, i);
const nodesWithIndex = filteredNodes.filter((n) => n.index === index);
if (nodesWithIndex.length > 0) {
sections.push(
sections.length > 0
? nodesWithIndex.sort((a, b) => {
// sort by the order of their parents in the previous section with orphans at the end
const aParentIndex = this._findParentIndex(
a.id,
data.links,
sections
);
const bParentIndex = this._findParentIndex(
b.id,
data.links,
sections
);
if (aParentIndex === bParentIndex) {
return 0;
}
if (aParentIndex === -1) {
return 1;
}
if (bParentIndex === -1) {
return -1;
}
return aParentIndex - bParentIndex;
})
: nodesWithIndex
);
}
});
const links = this._processLinks(filteredNodes, data.links);
const sectionWidth = width / indexes.length;
@@ -117,7 +148,7 @@ export class HaSankeyChart extends LitElement {
return {
id: "sankey",
type: "sankey",
nodes: filteredNodes.map((node) => ({
nodes: sections.flat().map((node) => ({
id: node.id,
value: node.value,
itemStyle: {
@@ -227,6 +258,23 @@ export class HaSankeyChart extends LitElement {
return links;
}
private _findParentIndex(id: string, links: Link[], sections: Node[][]) {
const parent = links.find((l) => l.target === id)?.source;
if (!parent) {
return -1;
}
let offset = 0;
for (let i = sections.length - 1; i >= 0; i--) {
const section = sections[i];
const index = section.findIndex((n) => n.id === parent);
if (index !== -1) {
return offset + index;
}
offset += section.length;
}
return -1;
}
static styles = css`
:host {
display: block;

View File

@@ -82,6 +82,8 @@ export class StateHistoryChartLine extends LitElement {
private _chartTime: Date = new Date();
private _previousYAxisLabelValue = 0;
protected render() {
return html`
<ha-chart-base
@@ -224,17 +226,25 @@ export class StateHistoryChartLine extends LitElement {
this.maxYAxis;
if (typeof minYAxis === "number") {
if (this.fitYData) {
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
minYAxis = ({ min }) =>
Math.min(this._roundYAxis(min, Math.floor), this.minYAxis!);
}
} else if (this.logarithmicScale) {
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
minYAxis = ({ min }) => {
const value = min > 0 ? min * 0.95 : min * 1.05;
return this._roundYAxis(value, Math.floor);
};
}
if (typeof maxYAxis === "number") {
if (this.fitYData) {
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
maxYAxis = ({ max }) =>
Math.max(this._roundYAxis(max, Math.ceil), this.maxYAxis!);
}
} else if (this.logarithmicScale) {
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
maxYAxis = ({ max }) => {
const value = max > 0 ? max * 1.05 : max * 0.95;
return this._roundYAxis(value, Math.ceil);
};
}
this._chartOptions = {
xAxis: {
@@ -258,35 +268,11 @@ export class StateHistoryChartLine extends LitElement {
},
axisLabel: {
margin: 5,
formatter: (value: number) => {
const formatOptions =
value >= 1 || value <= -1
? undefined
: {
// show the first significant digit for tiny values
maximumFractionDigits: Math.max(
2,
-Math.floor(Math.log10(Math.abs(value % 1 || 1)))
),
};
const label = formatNumber(
value,
this.hass.locale,
formatOptions
);
const width = measureTextWidth(label, 12) + 5;
if (width > this._yWidth) {
this._yWidth = width;
fireEvent(this, "y-width-changed", {
value: this._yWidth,
chartIndex: this.chartIndex,
});
}
return label;
},
formatter: this._formatYAxisLabel,
},
} as YAXisOption,
legend: {
type: "custom",
show: this.showNames,
},
grid: {
@@ -744,18 +730,46 @@ export class StateHistoryChartLine extends LitElement {
this._visualMap = visualMap.length > 0 ? visualMap : undefined;
}
private _formatYAxisLabel = (value: number) => {
// show the first significant digit for tiny values
const maximumFractionDigits = Math.max(
1,
// use the difference to the previous value to determine the number of significant digits #25526
-Math.floor(
Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1))
)
);
const label = formatNumber(value, this.hass.locale, {
maximumFractionDigits,
});
const width = measureTextWidth(label, 12) + 5;
if (width > this._yWidth) {
this._yWidth = width;
fireEvent(this, "y-width-changed", {
value: this._yWidth,
chartIndex: this.chartIndex,
});
}
this._previousYAxisLabelValue = value;
return label;
};
private _clampYAxis(value?: number | ((values: any) => number)) {
if (this.logarithmicScale) {
// log(0) is -Infinity, so we need to set a minimum value
if (typeof value === "number") {
return Math.max(value, 0.1);
return Math.max(value, Number.EPSILON);
}
if (typeof value === "function") {
return (values: any) => Math.max(value(values), 0.1);
return (values: any) => Math.max(value(values), Number.EPSILON);
}
}
return value;
}
private _roundYAxis(value: number, roundingFn: (value: number) => number) {
return Math.abs(value) < 1 ? value : roundingFn(value);
}
}
customElements.define("state-history-chart-line", StateHistoryChartLine);

View File

@@ -66,6 +66,7 @@ export class StateHistoryChartTimeline extends LitElement {
.options=${this._chartOptions}
.height=${`${this.data.length * 30 + 30}px`}
.data=${this._chartData as ECOption["series"]}
small-controls
@chart-click=${this._handleChartClick}
></ha-chart-base>
`;

View File

@@ -238,17 +238,25 @@ export class StatisticsChart extends LitElement {
this.maxYAxis;
if (typeof minYAxis === "number") {
if (this.fitYData) {
minYAxis = ({ min }) => Math.min(min, this.minYAxis!);
minYAxis = ({ min }) =>
Math.min(this._roundYAxis(min, Math.floor), this.minYAxis!);
}
} else if (this.logarithmicScale) {
minYAxis = ({ min }) => Math.floor(min > 0 ? min * 0.95 : min * 1.05);
minYAxis = ({ min }) => {
const value = min > 0 ? min * 0.95 : min * 1.05;
return this._roundYAxis(value, Math.floor);
};
}
if (typeof maxYAxis === "number") {
if (this.fitYData) {
maxYAxis = ({ max }) => Math.max(max, this.maxYAxis!);
maxYAxis = ({ max }) =>
Math.max(this._roundYAxis(max, Math.ceil), this.maxYAxis!);
}
} else if (this.logarithmicScale) {
maxYAxis = ({ max }) => Math.ceil(max > 0 ? max * 1.05 : max * 0.95);
maxYAxis = ({ max }) => {
const value = max > 0 ? max * 1.05 : max * 0.95;
return this._roundYAxis(value, Math.ceil);
};
}
const endTime = this.endTime ?? new Date();
let startTime = this.startTime;
@@ -308,6 +316,7 @@ export class StatisticsChart extends LitElement {
},
},
legend: {
type: "custom",
show: !this.hideLegend,
data: this._legendData,
},
@@ -618,15 +627,19 @@ export class StatisticsChart extends LitElement {
if (this.logarithmicScale) {
// log(0) is -Infinity, so we need to set a minimum value
if (typeof value === "number") {
return Math.max(value, 0.1);
return Math.max(value, Number.EPSILON);
}
if (typeof value === "function") {
return (values: any) => Math.max(value(values), 0.1);
return (values: any) => Math.max(value(values), Number.EPSILON);
}
}
return value;
}
private _roundYAxis(value: number, roundingFn: (value: number) => number) {
return Math.abs(value) < 1 ? value : roundingFn(value);
}
static styles = css`
:host {
display: block;

View File

@@ -60,7 +60,7 @@ export class HaAssistChip extends AssistChip {
opacity: var(--ha-assist-chip-active-container-opacity);
}
.label {
font-family: Roboto, sans-serif;
font-family: var(--ha-font-family-body);
}
`,
];

View File

@@ -72,6 +72,7 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
label?: TemplateResult | string;
type?:
| "numeric"
| "ip"
| "icon"
| "icon-button"
| "overflow"
@@ -164,6 +165,8 @@ export class HaDataTable extends LitElement {
@state() private _collapsedGroups: string[] = [];
@state() private _lastSelectedRowId: string | null = null;
private _checkableRowsCount?: number;
private _checkedRows: string[] = [];
@@ -187,6 +190,7 @@ export class HaDataTable extends LitElement {
public clearSelection(): void {
this._checkedRows = [];
this._lastSelectedRowId = null;
this._checkedRowsChanged();
}
@@ -194,6 +198,7 @@ export class HaDataTable extends LitElement {
this._checkedRows = this._filteredData
.filter((data) => data.selectable !== false)
.map((data) => data[this.id]);
this._lastSelectedRowId = null;
this._checkedRowsChanged();
}
@@ -207,6 +212,7 @@ export class HaDataTable extends LitElement {
this._checkedRows.push(id);
}
});
this._lastSelectedRowId = null;
this._checkedRowsChanged();
}
@@ -217,6 +223,7 @@ export class HaDataTable extends LitElement {
this._checkedRows.splice(index, 1);
}
});
this._lastSelectedRowId = null;
this._checkedRowsChanged();
}
@@ -261,6 +268,7 @@ export class HaDataTable extends LitElement {
if (this.columns[columnId].direction) {
this.sortDirection = this.columns[columnId].direction!;
this.sortColumn = columnId;
this._lastSelectedRowId = null;
fireEvent(this, "sorting-changed", {
column: columnId,
@@ -286,6 +294,7 @@ export class HaDataTable extends LitElement {
if (properties.has("filter")) {
this._debounceSearch(this.filter);
this._lastSelectedRowId = null;
}
if (properties.has("data")) {
@@ -296,9 +305,11 @@ export class HaDataTable extends LitElement {
if (!this.hasUpdated && this.initialCollapsedGroups) {
this._collapsedGroups = this.initialCollapsedGroups;
this._lastSelectedRowId = null;
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
} else if (properties.has("groupColumn")) {
this._collapsedGroups = [];
this._lastSelectedRowId = null;
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
}
@@ -312,6 +323,14 @@ export class HaDataTable extends LitElement {
this._sortFilterData();
}
if (
properties.has("_filter") ||
properties.has("sortColumn") ||
properties.has("sortDirection")
) {
this._lastSelectedRowId = null;
}
if (properties.has("selectable") || properties.has("hiddenColumns")) {
this._filteredData = [...this._filteredData];
}
@@ -488,7 +507,9 @@ export class HaDataTable extends LitElement {
this.hasFab,
this.groupColumn,
this.groupOrder,
this._collapsedGroups
this._collapsedGroups,
this.sortColumn,
this.sortDirection
)}
.keyFunction=${this._keyFunction}
.renderItem=${renderRow}
@@ -542,7 +563,7 @@ export class HaDataTable extends LitElement {
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleRowCheckboxClick}
@click=${this._handleRowCheckboxClicked}
.rowId=${row[this.id]}
.disabled=${row.selectable === false}
.checked=${this._checkedRows.includes(String(row[this.id]))}
@@ -683,22 +704,37 @@ export class HaDataTable extends LitElement {
hasFab: boolean,
groupColumn: string | undefined,
groupOrder: string[] | undefined,
collapsedGroups: string[]
collapsedGroups: string[],
sortColumn: string | undefined,
sortDirection: SortingDirection
) => {
if (appendRow || hasFab || groupColumn) {
let items = [...data];
if (groupColumn) {
const isGroupSortColumn = sortColumn === groupColumn;
const grouped = groupBy(items, (item) => item[groupColumn]);
if (grouped.undefined) {
// make sure ungrouped items are at the bottom
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
delete grouped.undefined;
}
const sorted: Record<string, DataTableRowData[]> = Object.keys(
const sortedEntries: [string, DataTableRowData[]][] = Object.keys(
grouped
)
.sort((a, b) => {
if (!groupOrder && isGroupSortColumn) {
const comparison = stringCompare(
a,
b,
this.hass.locale.language
);
if (sortDirection === "asc") {
return comparison;
}
return comparison * -1;
}
const orderA = groupOrder?.indexOf(a) ?? -1;
const orderB = groupOrder?.indexOf(b) ?? -1;
if (orderA !== orderB) {
@@ -716,14 +752,22 @@ export class HaDataTable extends LitElement {
this.hass.locale.language
);
})
.reduce((obj, key) => {
obj[key] = grouped[key];
return obj;
}, {});
.reduce(
(entries, key) => {
const entry: [string, DataTableRowData[]] = [key, grouped[key]];
entries.push(entry);
return entries;
},
[] as [string, DataTableRowData[]][]
);
const groupedItems: DataTableRowData[] = [];
Object.entries(sorted).forEach(([groupName, rows]) => {
sortedEntries.forEach(([groupName, rows]) => {
const collapsed = collapsedGroups.includes(groupName);
groupedItems.push({
append: true,
selectable: false,
content: html`<div
class="mdc-data-table__cell group-header"
role="cell"
@@ -732,9 +776,10 @@ export class HaDataTable extends LitElement {
>
<ha-icon-button
.path=${mdiChevronUp}
class=${collapsedGroups.includes(groupName)
? "collapsed"
: ""}
.label=${this.hass.localize(
`ui.components.data-table.${collapsed ? "expand" : "collapse"}`
)}
class=${collapsed ? "collapsed" : ""}
>
</ha-icon-button>
${groupName === UNDEFINED_GROUP_KEY
@@ -750,7 +795,7 @@ export class HaDataTable extends LitElement {
}
if (appendRow) {
items.push({ append: true, content: appendRow });
items.push({ append: true, selectable: false, content: appendRow });
}
if (hasFab) {
@@ -800,23 +845,86 @@ export class HaDataTable extends LitElement {
this._checkedRows = [];
this._checkedRowsChanged();
}
this._lastSelectedRowId = null;
}
private _handleRowCheckboxClick = (ev: Event) => {
private _handleRowCheckboxClicked = (ev: Event) => {
const checkbox = ev.currentTarget as HaCheckbox;
const rowId = (checkbox as any).rowId;
if (checkbox.checked) {
if (this._checkedRows.includes(rowId)) {
return;
const groupedData = this._groupData(
this._filteredData,
this.localizeFunc || this.hass.localize,
this.appendRow,
this.hasFab,
this.groupColumn,
this.groupOrder,
this._collapsedGroups,
this.sortColumn,
this.sortDirection
);
if (
groupedData.find((data) => data[this.id] === rowId)?.selectable === false
) {
return;
}
const rowIndex = groupedData.findIndex((data) => data[this.id] === rowId);
if (
ev instanceof MouseEvent &&
ev.shiftKey &&
this._lastSelectedRowId !== null
) {
const lastSelectedRowIndex = groupedData.findIndex(
(data) => data[this.id] === this._lastSelectedRowId
);
if (lastSelectedRowIndex > -1 && rowIndex > -1) {
this._checkedRows = [
...this._checkedRows,
...this._selectRange(groupedData, lastSelectedRowIndex, rowIndex),
];
}
} else if (!checkbox.checked) {
if (!this._checkedRows.includes(rowId)) {
this._checkedRows = [...this._checkedRows, rowId];
}
this._checkedRows = [...this._checkedRows, rowId];
} else {
this._checkedRows = this._checkedRows.filter((row) => row !== rowId);
}
if (rowIndex > -1) {
this._lastSelectedRowId = rowId;
}
this._checkedRowsChanged();
};
private _selectRange(
groupedData: DataTableRowData[],
startIndex: number,
endIndex: number
) {
const start = Math.min(startIndex, endIndex);
const end = Math.max(startIndex, endIndex);
const checkedRows: string[] = [];
for (let i = start; i <= end; i++) {
const row = groupedData[i];
if (
row &&
row.selectable !== false &&
!this._checkedRows.includes(row[this.id])
) {
checkedRows.push(row[this.id]);
}
}
return checkedRows;
}
private _handleRowClick = (ev: Event) => {
if (
ev
@@ -858,6 +966,7 @@ export class HaDataTable extends LitElement {
if (this.filter) {
return;
}
this._lastSelectedRowId = null;
this._debounceSearch(ev.detail.value);
}
@@ -894,11 +1003,13 @@ export class HaDataTable extends LitElement {
} else {
this._collapsedGroups = [...this._collapsedGroups, groupName];
}
this._lastSelectedRowId = null;
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
};
public expandAllGroups() {
this._collapsedGroups = [];
this._lastSelectedRowId = null;
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
}
@@ -916,6 +1027,7 @@ export class HaDataTable extends LitElement {
delete grouped.undefined;
}
this._collapsedGroups = Object.keys(grouped);
this._lastSelectedRowId = null;
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
}
@@ -928,12 +1040,12 @@ export class HaDataTable extends LitElement {
height: 100%;
}
.mdc-data-table__content {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-family: var(--ha-font-family-body);
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
-webkit-font-smoothing: var(--ha-font-smoothing);
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
line-height: var(--ha-line-height-condensed);
font-weight: var(--ha-font-weight-normal);
letter-spacing: 0.0178571429em;
text-decoration: inherit;
text-transform: inherit;
@@ -1048,12 +1160,12 @@ export class HaDataTable extends LitElement {
}
.mdc-data-table__cell {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-family: var(--ha-font-family-body);
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
-webkit-font-smoothing: var(--ha-font-smoothing);
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
line-height: var(--ha-line-height-condensed);
font-weight: var(--ha-font-weight-normal);
letter-spacing: 0.0178571429em;
text-decoration: inherit;
text-transform: inherit;
@@ -1170,12 +1282,12 @@ export class HaDataTable extends LitElement {
}
.mdc-data-table__header-cell {
font-family: Roboto, sans-serif;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
font-size: 0.875rem;
line-height: 1.375rem;
font-weight: 500;
font-family: var(--ha-font-family-body);
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
-webkit-font-smoothing: var(--ha-font-smoothing);
font-size: var(--ha-font-size-s);
line-height: var(--ha-line-height-normal);
font-weight: var(--ha-font-weight-medium);
letter-spacing: 0.0071428571em;
text-decoration: inherit;
text-transform: inherit;
@@ -1199,7 +1311,7 @@ export class HaDataTable extends LitElement {
padding-inline-start: 12px;
padding-inline-end: initial;
width: 100%;
font-weight: 500;
font-weight: var(--ha-font-weight-medium);
display: flex;
align-items: center;
cursor: pointer;

View File

@@ -1,5 +1,5 @@
import { expose } from "comlink";
import { stringCompare } from "../../common/string/compare";
import { stringCompare, ipCompare } from "../../common/string/compare";
import { stripDiacritics } from "../../common/string/strip-diacritics";
import type {
ClonedDataTableColumnData,
@@ -57,6 +57,8 @@ const sortData = (
if (column.type === "numeric") {
valA = isNaN(valA) ? undefined : Number(valA);
valB = isNaN(valB) ? undefined : Number(valB);
} else if (column.type === "ip") {
return sort * ipCompare(valA, valB);
} else if (typeof valA === "string" && typeof valB === "string") {
return sort * stringCompare(valA, valB, language);
}

View File

@@ -12,6 +12,7 @@ import type { EntityRegistryEntry } from "../../data/entity_registry";
import type { HomeAssistant } from "../../types";
import "../ha-list-item";
import "../ha-select";
import { stopPropagation } from "../../common/dom/stop_propagation";
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
@@ -103,6 +104,7 @@ export abstract class HaDeviceAutomationPicker<
.label=${this.label}
.value=${value}
@selected=${this._automationChanged}
@closed=${stopPropagation}
.disabled=${this._automations.length === 0}
>
${value === NO_AUTOMATION_KEY

View File

@@ -1,33 +1,28 @@
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, nothing } from "lit";
import { html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
import { computeAreaName } from "../../common/entity/compute_area_name";
import {
computeDeviceName,
computeDeviceNameDisplay,
} from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
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 {
DeviceEntityDisplayLookup,
DeviceRegistryEntry,
import { getDeviceContext } from "../../common/entity/context/get_device_context";
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
import {
getDeviceEntityDisplayLookup,
type DeviceEntityDisplayLookup,
type DeviceRegistryEntry,
} from "../../data/device_registry";
import { getDeviceEntityDisplayLookup } from "../../data/device_registry";
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-combo-box-item";
interface Device {
name: string;
area: string;
id: string;
}
type ScorableDevice = ScorableTextItem & Device;
import { domainToName } from "../../data/integration";
import type { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import "../ha-generic-picker";
import type { HaGenericPicker } from "../ha-generic-picker";
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry
@@ -35,25 +30,35 @@ export type HaDevicePickerDeviceFilterFunc = (
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
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>
`;
interface DevicePickerItem extends PickerComboBoxItem {
domain?: string;
domain_name?: string;
}
@customElement("ha-device-picker")
export class HaDevicePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property() public label?: string;
@property() public value?: string;
@property() public helper?: string;
@property() public placeholder?: string;
@property({ type: String, attribute: "search-label" })
public searchLabel?: string;
@property({ attribute: false, type: Array }) public createDomains?: string[];
/**
* Show only devices with entities from specific domains.
* @type {Array}
@@ -92,38 +97,52 @@ export class HaDevicePicker extends LitElement {
@property({ attribute: false })
public entityFilter?: HaDevicePickerEntityFilterFunc;
@property({ type: Boolean }) public disabled = false;
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ type: Boolean }) public required = false;
@query("ha-generic-picker") private _picker?: HaGenericPicker;
@state() private _opened?: boolean;
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
@query("ha-combo-box", true) public comboBox!: HaComboBox;
protected firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
this._loadConfigEntries();
}
private _init = false;
private async _loadConfigEntries() {
const configEntries = await getConfigEntries(this.hass);
this._configEntryLookup = Object.fromEntries(
configEntries.map((entry) => [entry.entry_id, entry])
);
}
private _getItems = () =>
this._getDevices(
this.hass.devices,
this.hass.entities,
this._configEntryLookup,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.excludeDevices
);
private _getDevices = memoizeOne(
(
devices: DeviceRegistryEntry[],
areas: HomeAssistant["areas"],
entities: EntityRegistryDisplayEntry[],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
configEntryLookup: Record<string, ConfigEntry>,
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"],
entityFilter: this["entityFilter"],
excludeDevices: this["excludeDevices"]
): ScorableDevice[] => {
if (!devices.length) {
return [
{
id: "no_devices",
area: "",
name: this.hass.localize("ui.components.device-picker.no_devices"),
strings: [],
},
];
}
): DevicePickerItem[] => {
const devices = Object.values(haDevices);
const entities = Object.values(haEntities);
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
@@ -214,133 +233,158 @@ export class HaDevicePicker extends LitElement {
);
}
const outputDevices = inputDevices.map((device) => {
const name = computeDeviceNameDisplay(
const outputDevices = inputDevices.map<DevicePickerItem>((device) => {
const deviceName = computeDeviceNameDisplay(
device,
this.hass,
deviceEntityLookup[device.id]
);
const { area } = getDeviceContext(device, this.hass);
const areaName = area ? computeAreaName(area) : undefined;
const configEntry = device.primary_config_entry
? configEntryLookup?.[device.primary_config_entry]
: undefined;
const domain = configEntry?.domain;
const domainName = domain
? domainToName(this.hass.localize, domain)
: undefined;
return {
id: device.id,
name:
name ||
label: "",
primary:
deviceName ||
this.hass.localize("ui.components.device-picker.unnamed_device"),
area:
device.area_id && areas[device.area_id]
? areas[device.area_id].name
: this.hass.localize("ui.components.device-picker.no_area"),
strings: [name || ""],
secondary: areaName,
domain: configEntry?.domain,
domain_name: domainName,
search_labels: [deviceName, areaName, domain, domainName].filter(
Boolean
) as string[],
sorting_label: deviceName || "zzz",
};
});
if (!outputDevices.length) {
return [
{
id: "no_devices",
area: "",
name: this.hass.localize("ui.components.device-picker.no_match"),
strings: [],
},
];
}
if (outputDevices.length === 1) {
return outputDevices;
}
return outputDevices.sort((a, b) =>
stringCompare(a.name || "", b.name || "", this.hass.locale.language)
);
return outputDevices;
}
);
public async open() {
await this.updateComplete;
await this.comboBox?.open();
}
private _valueRenderer = memoizeOne(
(configEntriesLookup: Record<string, ConfigEntry>) => (value: string) => {
const deviceId = value;
const device = this.hass.devices[deviceId];
public async focus() {
await this.updateComplete;
await this.comboBox?.focus();
}
if (!device) {
return html`<span slot="headline">${deviceId}</span>`;
}
protected updated(changedProps: PropertyValues) {
if (
(!this._init && this.hass) ||
(this._init && changedProps.has("_opened") && this._opened)
) {
this._init = true;
const devices = this._getDevices(
Object.values(this.hass.devices),
this.hass.areas,
Object.values(this.hass.entities),
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.excludeDevices
);
this.comboBox.items = devices;
this.comboBox.filteredItems = devices;
const { area } = getDeviceContext(device, this.hass);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const primary = deviceName;
const secondary = areaName;
const configEntry = device.primary_config_entry
? configEntriesLookup[device.primary_config_entry]
: undefined;
return html`
${configEntry
? html`<img
slot="start"
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl({
domain: configEntry.domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
/>`
: nothing}
<span slot="headline">${primary}</span>
<span slot="supporting-text">${secondary}</span>
`;
}
}
);
private _rowRenderer: ComboBoxLitRenderer<DevicePickerItem> = (item) => html`
<ha-combo-box-item type="button">
${item.domain
? html`
<img
slot="start"
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl({
domain: item.domain,
type: "icon",
darkOptimized: this.hass.themes.darkMode,
})}
/>
`
: nothing}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.domain_name
? html`
<div slot="trailing-supporting-text" class="domain">
${item.domain_name}
</div>
`
: nothing}
</ha-combo-box-item>
`;
protected render() {
const placeholder =
this.placeholder ??
this.hass.localize("ui.components.device-picker.placeholder");
const notFoundLabel = this.hass.localize(
"ui.components.device-picker.no_match"
);
const valueRenderer = this._valueRenderer(this._configEntryLookup);
protected render(): TemplateResult {
return html`
<ha-combo-box
<ha-generic-picker
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.device-picker.device")
: this.label}
.value=${this._value}
.helper=${this.helper}
.renderer=${rowRenderer}
.disabled=${this.disabled}
.required=${this.required}
item-id-path="id"
item-value-path="id"
item-label-path="name"
@opened-changed=${this._openedChanged}
@value-changed=${this._deviceChanged}
@filter-changed=${this._filterChanged}
></ha-combo-box>
.autofocus=${this.autofocus}
.label=${this.label}
.searchLabel=${this.searchLabel}
.notFoundLabel=${notFoundLabel}
.placeholder=${placeholder}
.value=${this.value}
.rowRenderer=${this._rowRenderer}
.getItems=${this._getItems}
.hideClearIcon=${this.hideClearIcon}
.valueRenderer=${valueRenderer}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>
`;
}
private get _value() {
return this.value || "";
public async open() {
await this.updateComplete;
await this._picker?.open();
}
private _filterChanged(ev: CustomEvent): void {
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.toLowerCase();
target.filteredItems = filterString.length
? fuzzyFilterSort<ScorableDevice>(filterString, target.items || [])
: target.items;
}
private _deviceChanged(ev: ValueChangedEvent<string>) {
private _valueChanged(ev) {
ev.stopPropagation();
let newValue = ev.detail.value;
if (newValue === "no_devices") {
newValue = "";
}
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _setValue(value: string) {
const value = ev.detail.value;
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
fireEvent(this, "value-changed", { value });
}
}

View File

@@ -1,7 +1,7 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { ValueChangedEvent, HomeAssistant } from "../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "./ha-device-picker";
import type {
HaDevicePickerDeviceFilterFunc,

View File

@@ -4,8 +4,8 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import type { HaEntityComboBoxEntityFilterFunc } from "./ha-entity-combo-box";
import "./ha-entity-picker";
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
@customElement("ha-entities-picker")
class HaEntitiesPicker extends LitElement {
@@ -72,7 +72,7 @@ class HaEntitiesPicker extends LitElement {
public excludeEntities?: string[];
@property({ attribute: false })
public entityFilter?: HaEntityComboBoxEntityFilterFunc;
public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ attribute: false, type: Array }) public createDomains?: string[];

View File

@@ -1,510 +0,0 @@
import { mdiMagnify, mdiPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import Fuse from "fuse.js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
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 { getEntityContext } from "../../common/entity/context/get_entity_context";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
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 { HaFuse } from "../../resources/fuse";
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-svg-icon";
import "./state-badge";
interface EntityComboBoxItem {
// Force empty label to always display empty value by default in the search field
id: string;
label: "";
primary: string;
secondary?: string;
domain_name?: string;
search_labels?: string[];
sorting_label?: string;
icon_path?: string;
stateObj?: HassEntity;
}
export type HaEntityComboBoxEntityFilterFunc = (entity: HassEntity) => boolean;
const CREATE_ID = "___create-new-entity___";
const NO_ENTITIES_ID = "___no-entities___";
@customElement("ha-entity-combo-box")
export class HaEntityComboBox extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@property() public label?: string;
@property() public value?: string;
@property() public helper?: string;
@property({ attribute: false, type: Array }) public createDomains?: string[];
/**
* Show entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
/**
* Show only entities with these unit of measuments.
* @type {Array}
* @attr include-unit-of-measurement
*/
@property({ type: Array, attribute: "include-unit-of-measurement" })
public includeUnitOfMeasurement?: string[];
/**
* List of allowed entities to show.
* @type {Array}
* @attr include-entities
*/
@property({ type: Array, attribute: "include-entities" })
public includeEntities?: string[];
/**
* List of entities to be excluded.
* @type {Array}
* @attr exclude-entities
*/
@property({ type: Array, attribute: "exclude-entities" })
public excludeEntities?: string[];
@property({ attribute: false })
public entityFilter?: HaEntityComboBoxEntityFilterFunc;
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@state() private _opened = false;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
public async open() {
await this.updateComplete;
await this.comboBox?.open();
}
public async focus() {
await this.updateComplete;
await this.comboBox?.focus();
}
private _initialItems = false;
private _items: EntityComboBoxItem[] = [];
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.hass.loadBackendTranslation("title");
}
private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = (
item,
{ index }
) => {
const showEntityId = this.hass.userData?.showEntityIdPicker;
return 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.stateObj}
.hass=${this.hass}
></state-badge>
`}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.stateObj && showEntityId
? html`
<span slot="supporting-text" class="code">
${item.stateObj.entity_id}
</span>
`
: nothing}
${item.domain_name && !showEntityId
? html`
<div slot="trailing-supporting-text">${item.domain_name}</div>
`
: nothing}
</ha-combo-box-item>
`;
};
private _getItems = memoizeOne(
(
_opened: boolean,
hass: this["hass"],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
includeEntities: this["includeEntities"],
excludeEntities: this["excludeEntities"],
createDomains: this["createDomains"]
): EntityComboBoxItem[] => {
let items: EntityComboBoxItem[] = [];
let entityIds = Object.keys(hass.states);
const createItems = createDomains?.length
? createDomains.map((domain) => {
const primary = hass.localize(
"ui.components.entity.entity-picker.create_helper",
{
domain: isHelperDomain(domain)
? hass.localize(
`ui.panel.config.helpers.types.${domain as HelperDomain}`
)
: domainToName(hass.localize, domain),
}
);
return {
id: CREATE_ID + domain,
label: "",
primary: primary,
secondary: this.hass.localize(
"ui.components.entity.entity-picker.new_entity"
),
icon_path: mdiPlus,
} satisfies EntityComboBoxItem;
})
: [];
if (!entityIds.length) {
return [
{
id: NO_ENTITIES_ID,
label: "",
primary: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
icon_path: mdiMagnify,
},
...createItems,
];
}
if (includeEntities) {
entityIds = entityIds.filter((entityId) =>
includeEntities.includes(entityId)
);
}
if (excludeEntities) {
entityIds = entityIds.filter(
(entityId) => !excludeEntities.includes(entityId)
);
}
if (includeDomains) {
entityIds = entityIds.filter((eid) =>
includeDomains.includes(computeDomain(eid))
);
}
if (excludeDomains) {
entityIds = entityIds.filter(
(eid) => !excludeDomains.includes(computeDomain(eid))
);
}
const isRTL = computeRTL(this.hass);
items = entityIds
.map<EntityComboBoxItem>((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 domainName = domainToName(
this.hass.localize,
computeDomain(entityId)
);
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
return {
id: entityId,
label: "",
primary: primary,
secondary: secondary,
domain_name: domainName,
sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
search_labels: [
entityName,
deviceName,
areaName,
domainName,
friendlyName,
entityId,
].filter(Boolean) as string[],
stateObj: stateObj,
};
})
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.sorting_label!,
entityB.sorting_label!,
this.hass.locale.language
)
);
if (includeDeviceClasses) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj?.attributes.device_class &&
includeDeviceClasses.includes(
item.stateObj.attributes.device_class
))
);
}
if (includeUnitOfMeasurement) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj?.attributes.unit_of_measurement &&
includeUnitOfMeasurement.includes(
item.stateObj.attributes.unit_of_measurement
))
);
}
if (entityFilter) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj && entityFilter!(item.stateObj))
);
}
if (!items.length) {
return [
{
id: NO_ENTITIES_ID,
label: "",
primary: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
icon_path: mdiMagnify,
},
...createItems,
];
}
if (createItems?.length) {
items.push(...createItems);
}
return items;
}
);
protected shouldUpdate(changedProps: PropertyValues) {
if (
changedProps.has("value") ||
changedProps.has("label") ||
changedProps.has("disabled")
) {
return true;
}
return !(!changedProps.has("_opened") && this._opened);
}
public willUpdate(changedProps: PropertyValues) {
if (!this._initialItems || (changedProps.has("_opened") && this._opened)) {
this._items = this._getItems(
this._opened,
this.hass,
this.includeDomains,
this.excludeDomains,
this.entityFilter,
this.includeDeviceClasses,
this.includeUnitOfMeasurement,
this.includeEntities,
this.excludeEntities,
this.createDomains
);
if (this._initialItems) {
this.comboBox.filteredItems = this._items;
}
this._initialItems = true;
}
if (changedProps.has("createDomains") && this.createDomains?.length) {
this.hass.loadFragmentTranslation("config");
}
}
protected render(): TemplateResult {
return html`
<ha-combo-box
item-value-path="id"
.hass=${this.hass}
.value=${this._value}
.label=${this.label === undefined
? this.hass.localize("ui.components.entity.entity-picker.entity")
: this.label}
.helper=${this.helper}
.allowCustomValue=${this.allowCustomEntity}
.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}
>
</ha-combo-box>
`;
}
private get _value() {
return this.value || "";
}
private _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
ev.stopPropagation();
// Clear the input field to prevent showing the old value next time
this.comboBox.setTextFieldValue("");
const newValue = ev.detail.value?.trim();
if (newValue && newValue.startsWith(CREATE_ID)) {
const domain = newValue.substring(CREATE_ID.length);
showHelperDetailDialog(this, {
domain,
dialogClosedCallback: (item) => {
if (item.entityId) this._setValue(item.entityId);
},
});
return;
}
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _fuseIndex = memoizeOne((states: EntityComboBoxItem[]) =>
Fuse.createIndex(["search_labels"], states)
);
private _filterChanged(ev: CustomEvent): void {
if (!this._opened) return;
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.trim().toLowerCase() as string;
const index = this._fuseIndex(this._items);
const fuse = new HaFuse(this._items, {}, index);
const results = fuse.multiTermsSearch(filterString);
if (results) {
if (results.length === 0) {
target.filteredItems = [
{
id: NO_ENTITIES_ID,
label: "",
primary: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
icon_path: mdiMagnify,
},
] as EntityComboBoxItem[];
} else {
target.filteredItems = results.map((result) => result.item);
}
} else {
target.filteredItems = this._items;
}
}
private _setValue(value: string | undefined) {
if (!value || !isValidEntityId(value)) {
return;
}
setTimeout(() => {
fireEvent(this, "value-changed", { value });
}, 0);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-entity-combo-box": HaEntityComboBox;
}
}

View File

@@ -1,27 +1,45 @@
import { mdiClose, mdiMenuDown, mdiShape } from "@mdi/js";
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { mdiPlus, mdiShape } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
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 { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import { computeRTL } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce";
import { domainToName } from "../../data/integration";
import {
isHelperDomain,
type HelperDomain,
} from "../../panels/config/helpers/const";
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
import type { HomeAssistant } from "../../types";
import "../ha-combo-box-item";
import "../ha-icon-button";
import type { HaMdListItem } from "../ha-md-list-item";
import "../ha-svg-icon";
import "./ha-entity-combo-box";
import "../ha-generic-picker";
import type { HaGenericPicker } from "../ha-generic-picker";
import type {
HaEntityComboBox,
HaEntityComboBoxEntityFilterFunc,
} from "./ha-entity-combo-box";
PickerComboBoxItem,
PickerComboBoxSearchFn,
} from "../ha-picker-combo-box";
import type { PickerValueRenderer } from "../ha-picker-field";
import "../ha-svg-icon";
import "./state-badge";
interface EntityComboBoxItem extends PickerComboBoxItem {
domain_name?: string;
stateObj?: HassEntity;
}
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
const CREATE_ID = "___create-new-entity___";
@customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -36,6 +54,9 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@property({ type: Boolean, attribute: "show-entity-id" })
public showEntityId = false;
@property() public label?: string;
@property() public value?: string;
@@ -44,6 +65,9 @@ export class HaEntityPicker extends LitElement {
@property() public placeholder?: string;
@property({ type: String, attribute: "search-label" })
public searchLabel?: string;
@property({ attribute: false, type: Array }) public createDomains?: string[];
/**
@@ -95,50 +119,32 @@ export class HaEntityPicker extends LitElement {
public excludeEntities?: string[];
@property({ attribute: false })
public entityFilter?: HaEntityComboBoxEntityFilterFunc;
public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@query("#anchor") private _anchor?: HaMdListItem;
@query("ha-generic-picker") private _picker?: HaGenericPicker;
@query("#input") private _input?: HaEntityComboBox;
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
// Load title translations so it is available when the combo-box opens
this.hass.loadBackendTranslation("title");
}
@state() private _opened = false;
private _renderContent() {
const entityId = this.value || "";
if (!this.value) {
return html`
<span slot="headline" class="placeholder"
>${this.placeholder ??
this.hass.localize(
"ui.components.entity.entity-picker.placeholder"
)}</span
>
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
`;
}
private _valueRenderer: PickerValueRenderer = (value) => {
const entityId = value || "";
const stateObj = this.hass.states[entityId];
const showClearIcon =
!this.required && !this.disabled && !this.hideClearIcon;
if (!stateObj) {
return html`
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
<ha-svg-icon
slot="start"
.path=${mdiShape}
style="margin: 0 4px"
></ha-svg-icon>
<span slot="headline">${entityId}</span>
${showClearIcon
? html`<ha-icon-button
class="clear"
slot="end"
@click=${this._clear}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
`;
}
@@ -163,169 +169,309 @@ export class HaEntityPicker extends LitElement {
></state-badge>
<span slot="headline">${primary}</span>
<span slot="supporting-text">${secondary}</span>
${showClearIcon
? html`<ha-icon-button
class="clear"
slot="end"
@click=${this._clear}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
`;
};
private get _showEntityId() {
return this.showEntityId || this.hass.userData?.showEntityIdPicker;
}
protected render() {
private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = (
item,
{ index }
) => {
const showEntityId = this._showEntityId;
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<div class="container">
${!this._opened
? html`<ha-combo-box-item
.disabled=${this.disabled}
id="anchor"
type="button"
compact
@click=${this._showPicker}
>
${this._renderContent()}
</ha-combo-box-item>`
: html`<ha-entity-combo-box
id="input"
.hass=${this.hass}
.autofocus=${this.autofocus}
.allowCustomEntity=${this.allowCustomEntity}
.label=${this.hass.localize("ui.common.search")}
.value=${this.value}
.createDomains=${this.createDomains}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
.includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
.includeEntities=${this.includeEntities}
.excludeEntities=${this.excludeEntities}
.entityFilter=${this.entityFilter}
hide-clear-icon
@opened-changed=${this._debounceOpenedChanged}
@input=${stopPropagation}
></ha-entity-combo-box>`}
${this._renderHelper()}
</div>
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
${item.icon_path
? html`
<ha-svg-icon
slot="start"
style="margin: 0 4px"
.path=${item.icon_path}
></ha-svg-icon>
`
: html`
<state-badge
slot="start"
.stateObj=${item.stateObj}
.hass=${this.hass}
></state-badge>
`}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.stateObj && showEntityId
? html`
<span slot="supporting-text" class="code">
${item.stateObj.entity_id}
</span>
`
: nothing}
${item.domain_name && !showEntityId
? html`
<div slot="trailing-supporting-text" class="domain">
${item.domain_name}
</div>
`
: nothing}
</ha-combo-box-item>
`;
}
};
private _renderHelper() {
return this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: nothing;
}
private _getAdditionalItems = () =>
this._getCreateItems(this.hass.localize, this.createDomains);
private _clear(e) {
e.stopPropagation();
this.value = undefined;
fireEvent(this, "value-changed", { value: undefined });
fireEvent(this, "change");
}
private _getCreateItems = memoizeOne(
(
localize: this["hass"]["localize"],
createDomains: this["createDomains"]
) => {
if (!createDomains?.length) {
return [];
}
private async _showPicker() {
if (this.disabled) {
return;
return createDomains.map((domain) => {
const primary = localize(
"ui.components.entity.entity-picker.create_helper",
{
domain: isHelperDomain(domain)
? localize(
`ui.panel.config.helpers.types.${domain as HelperDomain}`
)
: domainToName(localize, domain),
}
);
return {
id: CREATE_ID + domain,
primary: primary,
secondary: localize("ui.components.entity.entity-picker.new_entity"),
icon_path: mdiPlus,
} satisfies EntityComboBoxItem;
});
}
this._opened = true;
await this.updateComplete;
this._input?.focus();
this._input?.open();
}
// Multiple calls to _openedChanged can be triggered in quick succession
// when the menu is opened
private _debounceOpenedChanged = debounce(
(ev) => this._openedChanged(ev),
10
);
private async _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
const opened = ev.detail.value;
if (this._opened && !opened) {
this._opened = false;
await this.updateComplete;
this._anchor?.focus();
private _getItems = () =>
this._getEntities(
this.hass,
this.includeDomains,
this.excludeDomains,
this.entityFilter,
this.includeDeviceClasses,
this.includeUnitOfMeasurement,
this.includeEntities,
this.excludeEntities
);
private _getEntities = memoizeOne(
(
hass: this["hass"],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
entityFilter: this["entityFilter"],
includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
includeEntities: this["includeEntities"],
excludeEntities: this["excludeEntities"]
): EntityComboBoxItem[] => {
let items: EntityComboBoxItem[] = [];
let entityIds = Object.keys(hass.states);
if (includeEntities) {
entityIds = entityIds.filter((entityId) =>
includeEntities.includes(entityId)
);
}
if (excludeEntities) {
entityIds = entityIds.filter(
(entityId) => !excludeEntities.includes(entityId)
);
}
if (includeDomains) {
entityIds = entityIds.filter((eid) =>
includeDomains.includes(computeDomain(eid))
);
}
if (excludeDomains) {
entityIds = entityIds.filter(
(eid) => !excludeDomains.includes(computeDomain(eid))
);
}
const isRTL = computeRTL(this.hass);
items = entityIds.map<EntityComboBoxItem>((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 domainName = domainToName(
this.hass.localize,
computeDomain(entityId)
);
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
return {
id: entityId,
primary: primary,
secondary: secondary,
domain_name: domainName,
sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
search_labels: [
entityName,
deviceName,
areaName,
domainName,
friendlyName,
entityId,
].filter(Boolean) as string[],
a11y_label: a11yLabel,
stateObj: stateObj,
};
});
if (includeDeviceClasses) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj?.attributes.device_class &&
includeDeviceClasses.includes(
item.stateObj.attributes.device_class
))
);
}
if (includeUnitOfMeasurement) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj?.attributes.unit_of_measurement &&
includeUnitOfMeasurement.includes(
item.stateObj.attributes.unit_of_measurement
))
);
}
if (entityFilter) {
items = items.filter(
(item) =>
// We always want to include the entity of the current value
item.id === this.value ||
(item.stateObj && entityFilter!(item.stateObj))
);
}
return items;
}
);
protected render() {
const placeholder =
this.placeholder ??
this.hass.localize("ui.components.entity.entity-picker.placeholder");
const notFoundLabel = this.hass.localize(
"ui.components.entity.entity-picker.no_match"
);
return html`
<ha-generic-picker
.hass=${this.hass}
.disabled=${this.disabled}
.autofocus=${this.autofocus}
.allowCustomValue=${this.allowCustomEntity}
.label=${this.label}
.helper=${this.helper}
.searchLabel=${this.searchLabel}
.notFoundLabel=${notFoundLabel}
.placeholder=${placeholder}
.value=${this.value}
.rowRenderer=${this._rowRenderer}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}
.hideClearIcon=${this.hideClearIcon}
.searchFn=${this._searchFn}
.valueRenderer=${this._valueRenderer}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>
`;
}
static get styles(): CSSResultGroup {
return [
css`
mwc-menu-surface {
--mdc-menu-min-width: 100%;
}
.container {
position: relative;
display: block;
}
ha-combo-box-item {
background-color: var(--mdc-text-field-fill-color, whitesmoke);
border-radius: 4px;
border-end-end-radius: 0;
border-end-start-radius: 0;
--md-list-item-one-line-container-height: 56px;
--md-list-item-two-line-container-height: 56px;
--md-list-item-top-space: 8px;
--md-list-item-bottom-space: 8px;
--md-list-item-leading-space: 8px;
--md-list-item-trailing-space: 8px;
--ha-md-list-item-gap: 8px;
/* Remove the default focus ring */
--md-focus-ring-width: 0px;
--md-focus-ring-duration: 0s;
}
private _searchFn: PickerComboBoxSearchFn<EntityComboBoxItem> = (
search,
filteredItems
) => {
// If there is exact match for entity id, put it first
const index = filteredItems.findIndex(
(item) => item.stateObj?.entity_id === search
);
if (index === -1) {
return filteredItems;
}
/* Add Similar focus style as the text field */
ha-combo-box-item:after {
display: block;
content: "";
position: absolute;
pointer-events: none;
bottom: 0;
left: 0;
right: 0;
height: 1px;
width: 100%;
background-color: var(
--mdc-text-field-idle-line-color,
rgba(0, 0, 0, 0.42)
);
transform:
height 180ms ease-in-out,
background-color 180ms ease-in-out;
}
const [exactMatch] = filteredItems.splice(index, 1);
filteredItems.unshift(exactMatch);
return filteredItems;
};
ha-combo-box-item:focus:after {
height: 2px;
background-color: var(--mdc-theme-primary);
}
public async open() {
await this.updateComplete;
await this._picker?.open();
}
ha-combo-box-item ha-svg-icon[slot="start"] {
margin: 0 4px;
}
.clear {
margin: 0 -8px;
--mdc-icon-button-size: 32px;
--mdc-icon-size: 20px;
}
.edit {
--mdc-icon-size: 20px;
width: 32px;
}
label {
display: block;
margin: 0 0 8px;
}
.placeholder {
color: var(--secondary-text-color);
padding: 0 8px;
}
`,
];
private _valueChanged(ev) {
ev.stopPropagation();
const value = ev.detail.value;
if (!value) {
this._setValue(undefined);
return;
}
if (value.startsWith(CREATE_ID)) {
const domain = value.substring(CREATE_ID.length);
showHelperDetailDialog(this, {
domain,
dialogClosedCallback: (item) => {
if (item.entityId) this._setValue(item.entityId);
},
});
return;
}
if (!isValidEntityId(value)) {
return;
}
this._setValue(value);
}
private _setValue(value: string | undefined) {
this.value = value;
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}
}

View File

@@ -267,7 +267,7 @@ export class HaStateLabelBadge extends LitElement {
cursor: pointer;
}
.big {
font-size: 70%;
font-size: var(--ha-font-size-xs);
}
ha-label-badge {
--ha-label-badge-color: var(--label-badge-red);

View File

@@ -1,481 +0,0 @@
import { mdiChartLine, mdiHelpCircle, mdiShape } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import Fuse from "fuse.js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "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 { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { computeRTL } from "../../common/util/compute_rtl";
import { domainToName } from "../../data/integration";
import type { StatisticsMetaData } from "../../data/recorder";
import { getStatisticIds, getStatisticLabel } from "../../data/recorder";
import { HaFuse } from "../../resources/fuse";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box-item";
import "../ha-svg-icon";
import "./state-badge";
import { documentationUrl } from "../../util/documentation-url";
type StatisticItemType = "entity" | "external" | "no_state";
interface StatisticItem {
// Force empty label to always display empty value by default in the search field
id: string;
statistic_id?: string;
label: "";
primary: string;
secondary?: string;
search_labels?: string[];
sorting_label?: string;
icon_path?: string;
type?: StatisticItemType;
stateObj?: HassEntity;
}
const MISSING_ID = "___missing-entity___";
const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[];
@customElement("ha-statistic-combo-box")
export class HaStatisticComboBox extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property({ attribute: "statistic-types" })
public statisticTypes?: "mean" | "sum";
@property({ type: Boolean, attribute: "allow-custom-entity" })
public allowCustomEntity;
@property({ attribute: false, type: Array })
public statisticIds?: StatisticsMetaData[];
@property({ type: Boolean }) public disabled = false;
/**
* Show only statistics natively stored with these units of measurements.
* @type {Array}
* @attr include-statistics-unit-of-measurement
*/
@property({
type: Array,
attribute: "include-statistics-unit-of-measurement",
})
public includeStatisticsUnitOfMeasurement?: string | string[];
/**
* Show only statistics with these unit classes.
* @attr include-unit-class
*/
@property({ attribute: "include-unit-class" })
public includeUnitClass?: string | string[];
/**
* Show only statistics with these device classes.
* @attr include-device-class
*/
@property({ attribute: "include-device-class" })
public includeDeviceClass?: string | string[];
/**
* Show only statistics on entities.
* @type {Boolean}
* @attr entities-only
*/
@property({ type: Boolean, attribute: "entities-only" })
public entitiesOnly = false;
/**
* List of statistics to be excluded.
* @type {Array}
* @attr exclude-statistics
*/
@property({ type: Array, attribute: "exclude-statistics" })
public excludeStatistics?: string[];
@property({ attribute: false }) public helpMissingEntityUrl =
"/more-info/statistics/";
@state() private _opened = false;
@query("ha-combo-box", true) public comboBox!: HaComboBox;
private _initialItems = false;
private _items: StatisticItem[] = [];
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.hass.loadBackendTranslation("title");
}
private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = (
item,
{ index }
) => {
const showEntityId = this.hass.userData?.showEntityIdPicker;
return html`
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
${item.icon_path
? html`
<ha-svg-icon
style="margin: 0 4px"
slot="start"
.path=${item.icon_path}
></ha-svg-icon>
`
: item.stateObj
? html`
<state-badge
slot="start"
.stateObj=${item.stateObj}
.hass=${this.hass}
></state-badge>
`
: nothing}
<span slot="headline">${item.primary} </span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.id && showEntityId
? html`<span slot="supporting-text" class="code">
${item.statistic_id}
</span>`
: nothing}
</ha-combo-box-item>
`;
};
private _getItems = memoizeOne(
(
_opened: boolean,
hass: this["hass"],
statisticIds: StatisticsMetaData[],
includeStatisticsUnitOfMeasurement?: string | string[],
includeUnitClass?: string | string[],
includeDeviceClass?: string | string[],
entitiesOnly?: boolean,
excludeStatistics?: string[],
value?: string
): StatisticItem[] => {
if (!statisticIds.length) {
return [
{
id: "",
label: "",
primary: this.hass.localize(
"ui.components.statistic-picker.no_statistics"
),
},
];
}
if (includeStatisticsUnitOfMeasurement) {
const includeUnits: (string | null)[] = ensureArray(
includeStatisticsUnitOfMeasurement
);
statisticIds = statisticIds.filter((meta) =>
includeUnits.includes(meta.statistics_unit_of_measurement)
);
}
if (includeUnitClass) {
const includeUnitClasses: (string | null)[] =
ensureArray(includeUnitClass);
statisticIds = statisticIds.filter((meta) =>
includeUnitClasses.includes(meta.unit_class)
);
}
if (includeDeviceClass) {
const includeDeviceClasses: (string | null)[] =
ensureArray(includeDeviceClass);
statisticIds = statisticIds.filter((meta) => {
const stateObj = this.hass.states[meta.statistic_id];
if (!stateObj) {
return true;
}
return includeDeviceClasses.includes(
stateObj.attributes.device_class || ""
);
});
}
const isRTL = computeRTL(this.hass);
const output: StatisticItem[] = [];
statisticIds.forEach((meta) => {
if (
excludeStatistics &&
meta.statistic_id !== value &&
excludeStatistics.includes(meta.statistic_id)
) {
return;
}
const stateObj = this.hass.states[meta.statistic_id];
if (!stateObj) {
if (!entitiesOnly) {
const id = meta.statistic_id;
const label = getStatisticLabel(this.hass, meta.statistic_id, meta);
const type =
meta.statistic_id.includes(":") &&
!meta.statistic_id.includes(".")
? "external"
: "no_state";
if (type === "no_state") {
output.push({
id,
primary: label,
secondary: this.hass.localize(
"ui.components.statistic-picker.no_state"
),
label: "",
type,
sorting_label: label,
search_labels: [label, id],
icon_path: mdiShape,
});
} else if (type === "external") {
const domain = id.split(":")[0];
const domainName = domainToName(this.hass.localize, domain);
output.push({
id,
statistic_id: id,
primary: label,
secondary: domainName,
label: "",
type,
sorting_label: label,
search_labels: [label, domainName, id],
icon_path: mdiChartLine,
});
}
}
return;
}
const id = meta.statistic_id;
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 || id;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
output.push({
id,
statistic_id: id,
label: "",
primary,
secondary,
stateObj: stateObj,
type: "entity",
sorting_label: [deviceName, entityName].join("_"),
search_labels: [
entityName,
deviceName,
areaName,
friendlyName,
id,
].filter(Boolean) as string[],
});
});
if (!output.length) {
return [
{
id: "",
primary: this.hass.localize(
"ui.components.statistic-picker.no_match"
),
label: "",
},
];
}
if (output.length > 1) {
output.sort((a, b) => {
const aPrefix = TYPE_ORDER.indexOf(a.type || "no_state");
const bPrefix = TYPE_ORDER.indexOf(b.type || "no_state");
return caseInsensitiveStringCompare(
`${aPrefix}_${a.sorting_label || ""}`,
`${bPrefix}_${b.sorting_label || ""}`,
this.hass.locale.language
);
});
}
output.push({
id: MISSING_ID,
primary: this.hass.localize(
"ui.components.statistic-picker.missing_entity"
),
label: "",
icon_path: mdiHelpCircle,
});
return output;
}
);
public async open() {
await this.updateComplete;
await this.comboBox?.open();
}
public async focus() {
await this.updateComplete;
await this.comboBox?.focus();
}
protected shouldUpdate(changedProps: PropertyValues) {
if (
changedProps.has("value") ||
changedProps.has("label") ||
changedProps.has("disabled")
) {
return true;
}
return !(!changedProps.has("_opened") && this._opened);
}
public willUpdate(changedProps: PropertyValues) {
if (
(!this.hasUpdated && !this.statisticIds) ||
changedProps.has("statisticTypes")
) {
this._getStatisticIds();
}
if (
this.statisticIds &&
(!this._initialItems || (changedProps.has("_opened") && this._opened))
) {
this._items = this._getItems(
this._opened,
this.hass,
this.statisticIds!,
this.includeStatisticsUnitOfMeasurement,
this.includeUnitClass,
this.includeDeviceClass,
this.entitiesOnly,
this.excludeStatistics,
this.value
);
if (this._initialItems) {
this.comboBox.filteredItems = this._items;
}
this._initialItems = true;
}
}
protected render(): TemplateResult | typeof nothing {
if (this._items.length === 0) {
return nothing;
}
return html`
<ha-combo-box
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.statistic-picker.statistic")
: this.label}
.value=${this._value}
.renderer=${this._rowRenderer}
.disabled=${this.disabled}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._items}
item-value-path="id"
item-id-path="id"
item-label-path="label"
@opened-changed=${this._openedChanged}
@value-changed=${this._statisticChanged}
@filter-changed=${this._filterChanged}
></ha-combo-box>
`;
}
private async _getStatisticIds() {
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
}
private get _value() {
return this.value || "";
}
private _statisticChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
let newValue = ev.detail.value;
if (newValue === MISSING_ID) {
newValue = "";
window.open(
documentationUrl(this.hass, this.helpMissingEntityUrl),
"_blank"
);
}
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _fuseIndex = memoizeOne((states: StatisticItem[]) =>
Fuse.createIndex(["search_labels"], states)
);
private _filterChanged(ev: CustomEvent): void {
if (!this._opened) return;
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.trim().toLowerCase() as string;
const index = this._fuseIndex(this._items);
const fuse = new HaFuse(this._items, {}, index);
const results = fuse.multiTermsSearch(filterString);
if (results) {
target.filteredItems = results.map((result) => result.item);
} else {
target.filteredItems = this._items;
}
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-statistic-combo-box": HaStatisticComboBox;
}
}

View File

@@ -1,45 +1,48 @@
import { mdiChartLine, mdiClose, mdiMenuDown, mdiShape } from "@mdi/js";
import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
import { mdiChartLine, mdiHelpCircle, mdiShape } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { HassEntity } from "home-assistant-js-websocket";
import {
css,
html,
LitElement,
nothing,
type CSSResultGroup,
type PropertyValues,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { computeRTL } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce";
import { domainToName } from "../../data/integration";
import {
getStatisticIds,
getStatisticLabel,
type StatisticsMetaData,
} from "../../data/recorder";
import type { HomeAssistant } from "../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
import "../ha-combo-box-item";
import "../ha-generic-picker";
import type { HaGenericPicker } from "../ha-generic-picker";
import "../ha-icon-button";
import type { HaMdListItem } from "../ha-md-list-item";
import "../ha-input-helper-text";
import type {
PickerComboBoxItem,
PickerComboBoxSearchFn,
} from "../ha-picker-combo-box";
import type { PickerValueRenderer } from "../ha-picker-field";
import "../ha-svg-icon";
import "./ha-entity-combo-box";
import type { HaEntityComboBox } from "./ha-entity-combo-box";
import "./ha-statistic-combo-box";
import "./state-badge";
interface StatisticItem {
primary: string;
secondary?: string;
iconPath?: string;
const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[];
const MISSING_ID = "___missing-entity___";
type StatisticItemType = "entity" | "external" | "no_state";
interface StatisticComboBoxItem extends PickerComboBoxItem {
statistic_id?: string;
stateObj?: HassEntity;
type?: StatisticItemType;
}
@customElement("ha-statistic-picker")
@@ -70,6 +73,9 @@ export class HaStatisticPicker extends LitElement {
@property({ attribute: false, type: Array })
public statisticIds?: StatisticsMetaData[];
@property({ attribute: false }) public helpMissingEntityUrl =
"/more-info/statistics/";
/**
* Show only statistics natively stored with these units of measurements.
* @type {Array}
@@ -114,11 +120,7 @@ export class HaStatisticPicker extends LitElement {
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@query("#anchor") private _anchor?: HaMdListItem;
@query("#input") private _input?: HaEntityComboBox;
@state() private _opened = false;
@query("ha-generic-picker") private _picker?: HaGenericPicker;
public willUpdate(changedProps: PropertyValues) {
if (
@@ -133,6 +135,167 @@ export class HaStatisticPicker extends LitElement {
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
}
private _getItems = () =>
this._getStatisticsItems(
this.hass,
this.statisticIds,
this.includeStatisticsUnitOfMeasurement,
this.includeUnitClass,
this.includeDeviceClass,
this.entitiesOnly,
this.excludeStatistics,
this.value
);
private _getAdditionalItems(): StatisticComboBoxItem[] {
return [
{
id: MISSING_ID,
primary: this.hass.localize(
"ui.components.statistic-picker.missing_entity"
),
icon_path: mdiHelpCircle,
},
];
}
private _getStatisticsItems = memoizeOne(
(
hass: HomeAssistant,
statisticIds?: StatisticsMetaData[],
includeStatisticsUnitOfMeasurement?: string | string[],
includeUnitClass?: string | string[],
includeDeviceClass?: string | string[],
entitiesOnly?: boolean,
excludeStatistics?: string[],
value?: string
): StatisticComboBoxItem[] => {
if (!statisticIds) {
return [];
}
if (includeStatisticsUnitOfMeasurement) {
const includeUnits: (string | null)[] = ensureArray(
includeStatisticsUnitOfMeasurement
);
statisticIds = statisticIds.filter((meta) =>
includeUnits.includes(meta.statistics_unit_of_measurement)
);
}
if (includeUnitClass) {
const includeUnitClasses: (string | null)[] =
ensureArray(includeUnitClass);
statisticIds = statisticIds.filter((meta) =>
includeUnitClasses.includes(meta.unit_class)
);
}
if (includeDeviceClass) {
const includeDeviceClasses: (string | null)[] =
ensureArray(includeDeviceClass);
statisticIds = statisticIds.filter((meta) => {
const stateObj = this.hass.states[meta.statistic_id];
if (!stateObj) {
return true;
}
return includeDeviceClasses.includes(
stateObj.attributes.device_class || ""
);
});
}
const isRTL = computeRTL(this.hass);
const output: StatisticComboBoxItem[] = [];
statisticIds.forEach((meta) => {
if (
excludeStatistics &&
meta.statistic_id !== value &&
excludeStatistics.includes(meta.statistic_id)
) {
return;
}
const stateObj = this.hass.states[meta.statistic_id];
if (!stateObj) {
if (!entitiesOnly) {
const id = meta.statistic_id;
const label = getStatisticLabel(this.hass, meta.statistic_id, meta);
const type =
meta.statistic_id.includes(":") &&
!meta.statistic_id.includes(".")
? "external"
: "no_state";
const sortingPrefix = `${TYPE_ORDER.indexOf(type)}`;
if (type === "no_state") {
output.push({
id,
primary: label,
secondary: this.hass.localize(
"ui.components.statistic-picker.no_state"
),
type,
sorting_label: [sortingPrefix, label].join("_"),
search_labels: [label, id],
icon_path: mdiShape,
});
} else if (type === "external") {
const domain = id.split(":")[0];
const domainName = domainToName(this.hass.localize, domain);
output.push({
id,
statistic_id: id,
primary: label,
secondary: domainName,
type,
sorting_label: [sortingPrefix, label].join("_"),
search_labels: [label, domainName, id],
icon_path: mdiChartLine,
});
}
}
return;
}
const id = meta.statistic_id;
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 || id;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
output.push({
id,
statistic_id: id,
primary,
secondary,
a11y_label: a11yLabel,
stateObj: stateObj,
type: "entity",
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
search_labels: [
entityName,
deviceName,
areaName,
friendlyName,
id,
].filter(Boolean) as string[],
});
});
return output;
}
);
private _statisticMetaData = memoizeOne(
(statisticId: string, statisticIds: StatisticsMetaData[]) => {
if (!statisticIds) {
@@ -144,26 +307,11 @@ export class HaStatisticPicker extends LitElement {
}
);
private _renderContent() {
const statisticId = this.value || "";
if (!this.value) {
return html`
<span slot="headline" class="placeholder"
>${this.placeholder ??
this.hass.localize(
"ui.components.statistic-picker.placeholder"
)}</span
>
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
`;
}
private _valueRenderer: PickerValueRenderer = (value) => {
const statisticId = value;
const item = this._computeItem(statisticId);
const showClearIcon =
!this.required && !this.disabled && !this.hideClearIcon;
return html`
${item.stateObj
? html`
@@ -173,29 +321,19 @@ export class HaStatisticPicker extends LitElement {
slot="start"
></state-badge>
`
: item.iconPath
? html`<ha-svg-icon
slot="start"
.path=${item.iconPath}
></ha-svg-icon>`
: item.icon_path
? html`
<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>
`
: nothing}
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${showClearIcon
? html`<ha-icon-button
class="clear"
slot="end"
@click=${this._clear}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
<ha-svg-icon class="edit" slot="end" .path=${mdiMenuDown}></ha-svg-icon>
`;
}
};
private _computeItem(statisticId: string): StatisticItem {
private _computeItem(statisticId: string): StatisticComboBoxItem {
const stateObj = this.hass.states[statisticId];
if (stateObj) {
@@ -211,11 +349,24 @@ export class HaStatisticPicker extends LitElement {
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const friendlyName = computeStateName(stateObj); // Keep this for search
const sortingPrefix = `${TYPE_ORDER.indexOf("entity")}`;
return {
id: statisticId,
statistic_id: statisticId,
primary,
secondary,
stateObj,
stateObj: stateObj,
type: "entity",
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
search_labels: [
entityName,
deviceName,
areaName,
friendlyName,
statisticId,
].filter(Boolean) as string[],
};
}
@@ -230,175 +381,141 @@ export class HaStatisticPicker extends LitElement {
: "no_state";
if (type === "external") {
const sortingPrefix = `${TYPE_ORDER.indexOf("external")}`;
const label = getStatisticLabel(this.hass, statisticId, statistic);
const domain = statisticId.split(":")[0];
const domainName = domainToName(this.hass.localize, domain);
return {
id: statisticId,
statistic_id: statisticId,
primary: label,
secondary: domainName,
iconPath: mdiChartLine,
type: "external",
sorting_label: [sortingPrefix, label].join("_"),
search_labels: [label, domainName, statisticId],
icon_path: mdiChartLine,
};
}
}
const sortingPrefix = `${TYPE_ORDER.indexOf("external")}`;
const label = getStatisticLabel(this.hass, statisticId, statistic);
return {
primary: statisticId,
iconPath: mdiShape,
id: statisticId,
primary: label,
secondary: this.hass.localize("ui.components.statistic-picker.no_state"),
type: "no_state",
sorting_label: [sortingPrefix, label].join("_"),
search_labels: [label, statisticId],
icon_path: mdiShape,
};
}
protected render() {
private _rowRenderer: ComboBoxLitRenderer<StatisticComboBoxItem> = (
item,
{ index }
) => {
const showEntityId = this.hass.userData?.showEntityIdPicker;
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<div class="container">
${!this._opened
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
${item.icon_path
? html`
<ha-combo-box-item
.disabled=${this.disabled}
id="anchor"
type="button"
compact
@click=${this._showPicker}
>
${this._renderContent()}
</ha-combo-box-item>
<ha-svg-icon
style="margin: 0 4px"
slot="start"
.path=${item.icon_path}
></ha-svg-icon>
`
: html`
<ha-statistic-combo-box
id="input"
.hass=${this.hass}
.autofocus=${this.autofocus}
.allowCustomEntity=${this.allowCustomEntity}
.label=${this.hass.localize("ui.common.search")}
.value=${this.value}
.includeStatisticsUnitOfMeasurement=${this
.includeStatisticsUnitOfMeasurement}
.includeUnitClass=${this.includeUnitClass}
.includeDeviceClass=${this.includeDeviceClass}
.statisticTypes=${this.statisticTypes}
.statisticIds=${this.statisticIds}
.excludeStatistics=${this.excludeStatistics}
hide-clear-icon
@opened-changed=${this._debounceOpenedChanged}
@input=${stopPropagation}
></ha-statistic-combo-box>
`}
${this._renderHelper()}
</div>
: item.stateObj
? html`
<state-badge
slot="start"
.stateObj=${item.stateObj}
.hass=${this.hass}
></state-badge>
`
: nothing}
<span slot="headline">${item.primary} </span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${item.statistic_id && showEntityId
? html`<span slot="supporting-text" class="code">
${item.statistic_id}
</span>`
: nothing}
</ha-combo-box-item>
`;
};
protected render() {
const placeholder =
this.placeholder ??
this.hass.localize("ui.components.statistic-picker.placeholder");
const notFoundLabel = this.hass.localize(
"ui.components.statistic-picker.no_match"
);
return html`
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
.allowCustomValue=${this.allowCustomEntity}
.label=${this.label}
.notFoundLabel=${notFoundLabel}
.placeholder=${placeholder}
.value=${this.value}
.rowRenderer=${this._rowRenderer}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}
.hideClearIcon=${this.hideClearIcon}
.searchFn=${this._searchFn}
.valueRenderer=${this._valueRenderer}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>
`;
}
private _renderHelper() {
return this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
: nothing;
}
private _searchFn: PickerComboBoxSearchFn<StatisticComboBoxItem> = (
search,
filteredItems
) => {
// If there is exact match for entity id or statistic id, put it first
const index = filteredItems.findIndex(
(item) =>
item.stateObj?.entity_id === search || item.statistic_id === search
);
if (index === -1) {
return filteredItems;
}
private _clear(e) {
e.stopPropagation();
this.value = undefined;
fireEvent(this, "value-changed", { value: undefined });
fireEvent(this, "change");
}
const [exactMatch] = filteredItems.splice(index, 1);
filteredItems.unshift(exactMatch);
return filteredItems;
};
private async _showPicker() {
if (this.disabled) {
private _valueChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
const value = ev.detail.value;
if (value === MISSING_ID) {
window.open(
documentationUrl(this.hass, this.helpMissingEntityUrl),
"_blank"
);
return;
}
this._opened = true;
this.value = value;
fireEvent(this, "value-changed", { value });
}
public async open() {
await this.updateComplete;
this._input?.focus();
this._input?.open();
}
// Multiple calls to _openedChanged can be triggered in quick succession
// when the menu is opened
private _debounceOpenedChanged = debounce(
(ev) => this._openedChanged(ev),
10
);
private async _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
const opened = ev.detail.value;
if (this._opened && !opened) {
this._opened = false;
await this.updateComplete;
this._anchor?.focus();
}
}
static get styles(): CSSResultGroup {
return [
css`
.container {
position: relative;
display: block;
}
ha-combo-box-item {
background-color: var(--mdc-text-field-fill-color, whitesmoke);
border-radius: 4px;
border-end-end-radius: 0;
border-end-start-radius: 0;
--md-list-item-one-line-container-height: 56px;
--md-list-item-two-line-container-height: 56px;
--md-list-item-top-space: 8px;
--md-list-item-bottom-space: 8px;
--md-list-item-leading-space: 8px;
--md-list-item-trailing-space: 8px;
--ha-md-list-item-gap: 8px;
/* Remove the default focus ring */
--md-focus-ring-width: 0px;
--md-focus-ring-duration: 0s;
}
/* Add Similar focus style as the text field */
ha-combo-box-item:after {
display: block;
content: "";
position: absolute;
pointer-events: none;
bottom: 0;
left: 0;
right: 0;
height: 1px;
width: 100%;
background-color: var(
--mdc-text-field-idle-line-color,
rgba(0, 0, 0, 0.42)
);
transform:
height 180ms ease-in-out,
background-color 180ms ease-in-out;
}
ha-combo-box-item:focus:after {
height: 2px;
background-color: var(--mdc-theme-primary);
}
ha-combo-box-item ha-svg-icon[slot="start"] {
margin: 0 4px;
}
.clear {
margin: 0 -8px;
--mdc-icon-button-size: 32px;
--mdc-icon-size: 20px;
}
.edit {
--mdc-icon-size: 20px;
width: 32px;
}
label {
display: block;
margin: 0 0 8px;
}
.placeholder {
color: var(--secondary-text-color);
padding: 0 8px;
}
`,
];
await this._picker?.open();
}
}

View File

@@ -173,7 +173,6 @@ class HaStatisticsPicker extends LitElement {
static styles = css`
:host {
width: 200px;
display: block;
}
ha-statistic-picker {

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