Compare commits
566 Commits
remove-pad
...
toggle_gro
Author | SHA1 | Date | |
---|---|---|---|
![]() |
134681b4c9 | ||
![]() |
082f1ca55e | ||
![]() |
3b7d2869e5 | ||
![]() |
bcda5cd0cf | ||
![]() |
eeb64a25ff | ||
![]() |
9134132ba9 | ||
![]() |
341e63e878 | ||
![]() |
5ed2d2fd2f | ||
![]() |
c6f92d1375 | ||
![]() |
e8201f7848 | ||
![]() |
6d7df18e82 | ||
![]() |
1471cfea66 | ||
![]() |
9e4835107d | ||
![]() |
1ded254e5a | ||
![]() |
fc104a7992 | ||
![]() |
3269fd3c5b | ||
![]() |
17e63343c7 | ||
![]() |
e7e062a222 | ||
![]() |
5233086efb | ||
![]() |
8d95f0d95d | ||
![]() |
5cf8b39703 | ||
![]() |
15dabe372c | ||
![]() |
aab52a8bb2 | ||
![]() |
dc7ba0dac6 | ||
![]() |
2ab4608884 | ||
![]() |
de7f5c1bb7 | ||
![]() |
7144b7802e | ||
![]() |
aa52825b40 | ||
![]() |
2809a306e6 | ||
![]() |
ca315b88ce | ||
![]() |
a6304d6284 | ||
![]() |
8e866e86d6 | ||
![]() |
2e8203f666 | ||
![]() |
b60f2e3201 | ||
![]() |
c5f57f436c | ||
![]() |
3bb930b906 | ||
![]() |
e75331e159 | ||
![]() |
d6b66a7145 | ||
![]() |
5c346798c8 | ||
![]() |
5ffe37407a | ||
![]() |
2b056c0434 | ||
![]() |
27b36707e5 | ||
![]() |
5760614b65 | ||
![]() |
3835912b01 | ||
![]() |
a385655c85 | ||
![]() |
e177012108 | ||
![]() |
cc3234ad8f | ||
![]() |
4d932f0b4a | ||
![]() |
257769cdc7 | ||
![]() |
6619f064eb | ||
![]() |
382a47a082 | ||
![]() |
ad4be75fe1 | ||
![]() |
d605b67b41 | ||
![]() |
dba6a3c756 | ||
![]() |
002e9ad071 | ||
![]() |
6e7874c2c9 | ||
![]() |
978f9b0f83 | ||
![]() |
2b88669a72 | ||
![]() |
252fd2bb6c | ||
![]() |
cc68a087a2 | ||
![]() |
3e1341a731 | ||
![]() |
6be25270fd | ||
![]() |
ce929aea46 | ||
![]() |
8853bf6ea2 | ||
![]() |
2241807745 | ||
![]() |
50d705c943 | ||
![]() |
eb111d3c32 | ||
![]() |
1e59f9f4be | ||
![]() |
523eb9522f | ||
![]() |
f6cb322819 | ||
![]() |
4f97756f4e | ||
![]() |
8644dd5271 | ||
![]() |
26d842f432 | ||
![]() |
ad4f14ffaf | ||
![]() |
948c858e78 | ||
![]() |
49099223d3 | ||
![]() |
0fbd430594 | ||
![]() |
8cc762d839 | ||
![]() |
89d9dd2893 | ||
![]() |
b7d1ce1c37 | ||
![]() |
869d10ca3f | ||
![]() |
f338089148 | ||
![]() |
06b0f9fcaf | ||
![]() |
7ad07e4c55 | ||
![]() |
ad65600d11 | ||
![]() |
e91d907e56 | ||
![]() |
b35a1fc9e0 | ||
![]() |
dd18ad96f3 | ||
![]() |
62eec56e5f | ||
![]() |
7187e25cad | ||
![]() |
6d9e6a616d | ||
![]() |
44d87e3c66 | ||
![]() |
085e2460bc | ||
![]() |
1f8a9e4caf | ||
![]() |
f08877437e | ||
![]() |
6690d1ef22 | ||
![]() |
9d8a5b366e | ||
![]() |
22c798c9d6 | ||
![]() |
8aabb1f32f | ||
![]() |
33d5cecc85 | ||
![]() |
7693a4dc24 | ||
![]() |
9ec38c7dd9 | ||
![]() |
e8cb85f7ff | ||
![]() |
ef964a2717 | ||
![]() |
369881f8a6 | ||
![]() |
68e22d23f1 | ||
![]() |
696ba69a9e | ||
![]() |
e2ab52e10e | ||
![]() |
b154bc1502 | ||
![]() |
a952b880d8 | ||
![]() |
018aceb542 | ||
![]() |
2fb86f118e | ||
![]() |
6c8caccfec | ||
![]() |
3dd3a80054 | ||
![]() |
675310afdf | ||
![]() |
f5df91d4c7 | ||
![]() |
d8ab9b73ba | ||
![]() |
89ab0b4a3d | ||
![]() |
dd4cb1df72 | ||
![]() |
b81cd37776 | ||
![]() |
34112e7446 | ||
![]() |
2dee45b465 | ||
![]() |
10eb0a8b87 | ||
![]() |
551035238f | ||
![]() |
eb9359e9e1 | ||
![]() |
df2523a6a2 | ||
![]() |
edb1e1bba7 | ||
![]() |
e5bc234ab3 | ||
![]() |
6006e926a7 | ||
![]() |
93df473ad2 | ||
![]() |
af149dcfab | ||
![]() |
3ab6a02994 | ||
![]() |
e7a04eb3d2 | ||
![]() |
2dfe5f50a6 | ||
![]() |
174d54396f | ||
![]() |
5c1a8029bf | ||
![]() |
884341656f | ||
![]() |
fcbc8de95a | ||
![]() |
f90eb4fee0 | ||
![]() |
9afc4260c9 | ||
![]() |
3aafa47f6d | ||
![]() |
35ba2fffda | ||
![]() |
31bc708725 | ||
![]() |
caa60e4e8c | ||
![]() |
edcca81acc | ||
![]() |
508e451f94 | ||
![]() |
bfcc36bb40 | ||
![]() |
976bf7c512 | ||
![]() |
fb28c8971b | ||
![]() |
641a2eb77c | ||
![]() |
47b90feffa | ||
![]() |
45e66d8b6e | ||
![]() |
73cd1e8e9d | ||
![]() |
1a8ff83e2d | ||
![]() |
e6fbe0d538 | ||
![]() |
5ac6781f7d | ||
![]() |
7f7e693547 | ||
![]() |
51246f119c | ||
![]() |
d764187e8c | ||
![]() |
6738b7d708 | ||
![]() |
77c458a0e5 | ||
![]() |
876e36b4e0 | ||
![]() |
dd64fa228c | ||
![]() |
b3f5eb256f | ||
![]() |
a5005c0840 | ||
![]() |
c73122d7ee | ||
![]() |
82da36825e | ||
![]() |
3e2a5bff4d | ||
![]() |
1349d9d8e3 | ||
![]() |
f6f2cb0fce | ||
![]() |
589fa75b17 | ||
![]() |
fdd6ccf379 | ||
![]() |
f50d5d79a4 | ||
![]() |
ad589b32c9 | ||
![]() |
1990472970 | ||
![]() |
299713fd5e | ||
![]() |
a13dae4745 | ||
![]() |
8324c23618 | ||
![]() |
9c8d6a939b | ||
![]() |
1059b519af | ||
![]() |
52a02093e3 | ||
![]() |
b608bd949b | ||
![]() |
f87e20cae9 | ||
![]() |
d58186fec9 | ||
![]() |
f47336392c | ||
![]() |
e9272b9a27 | ||
![]() |
b20d489bdd | ||
![]() |
f7634c45c2 | ||
![]() |
8ee80586a8 | ||
![]() |
6aa8a24aad | ||
![]() |
65e78de41c | ||
![]() |
573c1db081 | ||
![]() |
cf03e041a8 | ||
![]() |
b4dbfa6f70 | ||
![]() |
723bb4dfeb | ||
![]() |
98dcce6af1 | ||
![]() |
e3cdd69835 | ||
![]() |
0ddf3aa675 | ||
![]() |
9c9ce78ff9 | ||
![]() |
4f8c50aaa9 | ||
![]() |
3cfc6297b5 | ||
![]() |
cc0586bf36 | ||
![]() |
bdf48140e4 | ||
![]() |
d7f4a7acb0 | ||
![]() |
154e85eb45 | ||
![]() |
ea15e5a44e | ||
![]() |
d581c4b0aa | ||
![]() |
5b7655cf72 | ||
![]() |
634e1dbde8 | ||
![]() |
a255463a76 | ||
![]() |
3c40b3112e | ||
![]() |
9f33ec55f5 | ||
![]() |
c35c4d16f0 | ||
![]() |
159c4d100a | ||
![]() |
05035a281b | ||
![]() |
67c7a3931f | ||
![]() |
cf41a43070 | ||
![]() |
5c385d5368 | ||
![]() |
0ae78300c9 | ||
![]() |
de42714505 | ||
![]() |
359460b570 | ||
![]() |
38545a01dd | ||
![]() |
8cead75087 | ||
![]() |
7f68447a4f | ||
![]() |
01d2ef13c6 | ||
![]() |
af6911e848 | ||
![]() |
21af10fd28 | ||
![]() |
380f760f79 | ||
![]() |
bffe38c827 | ||
![]() |
89d0746c7c | ||
![]() |
6d30d15638 | ||
![]() |
7e2059e836 | ||
![]() |
0d97962578 | ||
![]() |
4c5015e178 | ||
![]() |
28214aebc5 | ||
![]() |
a26ca7d065 | ||
![]() |
2ab20aef69 | ||
![]() |
8149ee60cb | ||
![]() |
89ce6870f6 | ||
![]() |
4307e2350f | ||
![]() |
7dc4d555ae | ||
![]() |
1483e8e38f | ||
![]() |
5daec6bbc5 | ||
![]() |
d542b52ebd | ||
![]() |
43cc49bb32 | ||
![]() |
b3f0a6328e | ||
![]() |
9d6a7e7e6f | ||
![]() |
78d7da21aa | ||
![]() |
0474a24df6 | ||
![]() |
6e7ac6fdf7 | ||
![]() |
7b9683df89 | ||
![]() |
8523ddfd29 | ||
![]() |
2589e1a49f | ||
![]() |
5ce5f9a189 | ||
![]() |
6dd7217a20 | ||
![]() |
0d02d0d334 | ||
![]() |
fed0dfa091 | ||
![]() |
39de40dec9 | ||
![]() |
e1c42d9985 | ||
![]() |
ad375c9b01 | ||
![]() |
07230e5ef5 | ||
![]() |
52f5af6090 | ||
![]() |
3c07289077 | ||
![]() |
8eb7fe8b0a | ||
![]() |
c8c2966d34 | ||
![]() |
a8768a5d9d | ||
![]() |
02bb7086e7 | ||
![]() |
42d8b2ae19 | ||
![]() |
e08f4a6bba | ||
![]() |
2e6c35d977 | ||
![]() |
17305a818b | ||
![]() |
08389dad04 | ||
![]() |
ab6ace46b5 | ||
![]() |
535dedbbc4 | ||
![]() |
412eb0c647 | ||
![]() |
87c8ebd493 | ||
![]() |
6e49f89126 | ||
![]() |
77eae6044d | ||
![]() |
84ac0cd41e | ||
![]() |
74f9c1551e | ||
![]() |
aebd6350c0 | ||
![]() |
4c78eb4797 | ||
![]() |
560e3890b9 | ||
![]() |
e448268feb | ||
![]() |
7ceba218fa | ||
![]() |
cd61725cf5 | ||
![]() |
228860a1ee | ||
![]() |
9f69347e1d | ||
![]() |
a099e65a9d | ||
![]() |
11e4a9f056 | ||
![]() |
b617299eee | ||
![]() |
768f27b1b9 | ||
![]() |
5ed816df6d | ||
![]() |
6692ac7517 | ||
![]() |
65499db0cb | ||
![]() |
11a1eabf61 | ||
![]() |
b30fa122ba | ||
![]() |
6730d08b85 | ||
![]() |
67003d6fd1 | ||
![]() |
414d46be65 | ||
![]() |
1485d1a1de | ||
![]() |
fd13e41524 | ||
![]() |
77f7ca0368 | ||
![]() |
7471250a07 | ||
![]() |
8b0a63d791 | ||
![]() |
57ffa814ed | ||
![]() |
46e05f10d1 | ||
![]() |
a1819d6189 | ||
![]() |
48a3e1fd63 | ||
![]() |
139c8b3702 | ||
![]() |
9458946dcc | ||
![]() |
b907dbefad | ||
![]() |
5371fd649c | ||
![]() |
61d9b0d2a3 | ||
![]() |
16e20456e2 | ||
![]() |
9c0ce41ebb | ||
![]() |
b458a1d7c6 | ||
![]() |
0eaeeb1141 | ||
![]() |
b7e63e697f | ||
![]() |
06db0f4b98 | ||
![]() |
d33636c6fb | ||
![]() |
bbb546159c | ||
![]() |
e8fc36026a | ||
![]() |
06270c771f | ||
![]() |
ae49de8e71 | ||
![]() |
6abdeeae20 | ||
![]() |
116716c51d | ||
![]() |
77ee69b64d | ||
![]() |
1a57eeddde | ||
![]() |
9131bf6dfd | ||
![]() |
1611423ca5 | ||
![]() |
38f8c804af | ||
![]() |
7c5bf26240 | ||
![]() |
189067d14b | ||
![]() |
e79e0f77b8 | ||
![]() |
b226e5c697 | ||
![]() |
52ad31601c | ||
![]() |
cba3e4df7f | ||
![]() |
3532cfa974 | ||
![]() |
de56c3376e | ||
![]() |
629eb29d42 | ||
![]() |
61019447cf | ||
![]() |
cde2b436d1 | ||
![]() |
6c7d750734 | ||
![]() |
fcf5ed7731 | ||
![]() |
3ce639946c | ||
![]() |
bb5f01ac81 | ||
![]() |
208e863327 | ||
![]() |
9f5f100e98 | ||
![]() |
114c1fb98b | ||
![]() |
3e5bd64b83 | ||
![]() |
02b4b8e334 | ||
![]() |
da8d43f5d1 | ||
![]() |
18aaa44d2d | ||
![]() |
7fa697a768 | ||
![]() |
28fe60f02b | ||
![]() |
549451eccb | ||
![]() |
113cc118cf | ||
![]() |
7ffb0f1e3b | ||
![]() |
60ef43044b | ||
![]() |
4e4a82e023 | ||
![]() |
f563146165 | ||
![]() |
81ba2db93a | ||
![]() |
4d8176ad6e | ||
![]() |
9b8be9f1af | ||
![]() |
c11d2c10df | ||
![]() |
412a0e9f6a | ||
![]() |
67dc830bbf | ||
![]() |
5581c10139 | ||
![]() |
ec26818c53 | ||
![]() |
399458f811 | ||
![]() |
3355986585 | ||
![]() |
9b7db191a6 | ||
![]() |
2d549ba22f | ||
![]() |
c3e155a95c | ||
![]() |
754829a836 | ||
![]() |
87bd039239 | ||
![]() |
32b3c83337 | ||
![]() |
f0beef22d2 | ||
![]() |
07e5f53469 | ||
![]() |
97e0217906 | ||
![]() |
006c7e1ea8 | ||
![]() |
9736d0cb55 | ||
![]() |
9ec689382b | ||
![]() |
4de95f6710 | ||
![]() |
a55ef8ad47 | ||
![]() |
01b398c2a3 | ||
![]() |
83df10ef29 | ||
![]() |
a026c72230 | ||
![]() |
c4e391c264 | ||
![]() |
4b72a6029c | ||
![]() |
8b87188075 | ||
![]() |
2a4c6c9af5 | ||
![]() |
5cbadaa5f9 | ||
![]() |
15fd4134d0 | ||
![]() |
eda9abc3c5 | ||
![]() |
7e56d5f351 | ||
![]() |
5d4805cde6 | ||
![]() |
c94326bc08 | ||
![]() |
b61180baa6 | ||
![]() |
bc15d1474e | ||
![]() |
95c3013497 | ||
![]() |
69a156f352 | ||
![]() |
67f3d31a4b | ||
![]() |
98a2966432 | ||
![]() |
75a2c061c2 | ||
![]() |
724df18175 | ||
![]() |
c00b4120ab | ||
![]() |
d392bb4c83 | ||
![]() |
30d46f2f8a | ||
![]() |
7c879cc291 | ||
![]() |
9a731880f3 | ||
![]() |
77efaaf7de | ||
![]() |
da96266454 | ||
![]() |
036d739d6e | ||
![]() |
c274a94ee5 | ||
![]() |
da7d359696 | ||
![]() |
20a7b3870c | ||
![]() |
9749a64ae1 | ||
![]() |
68741f6ba4 | ||
![]() |
e052ee04b4 | ||
![]() |
d2cc4a624e | ||
![]() |
5bcbe98f8e | ||
![]() |
beee76580d | ||
![]() |
28e5a30772 | ||
![]() |
785929b370 | ||
![]() |
55cf7e635d | ||
![]() |
4b270eb444 | ||
![]() |
d0e55719d1 | ||
![]() |
87f9397643 | ||
![]() |
910e7e10a7 | ||
![]() |
e1b9b47ac7 | ||
![]() |
89e04fcc45 | ||
![]() |
93f2e75fc9 | ||
![]() |
6c671d398f | ||
![]() |
2e5c6a4d3f | ||
![]() |
04e736a51e | ||
![]() |
ec3fdc0ea7 | ||
![]() |
a7a8c25d24 | ||
![]() |
60d457c3d9 | ||
![]() |
a58b1e636d | ||
![]() |
f67e7ae081 | ||
![]() |
0032c5508e | ||
![]() |
036df78de8 | ||
![]() |
617a6ba938 | ||
![]() |
76b9063aec | ||
![]() |
60c1d0a556 | ||
![]() |
193caec2df | ||
![]() |
42c8d132bf | ||
![]() |
0311a7c976 | ||
![]() |
40ffd50b8a | ||
![]() |
334991902a | ||
![]() |
c968266065 | ||
![]() |
d4fc0318f7 | ||
![]() |
8a0d3baf67 | ||
![]() |
8fc55cb6e2 | ||
![]() |
d6ebd9bfc4 | ||
![]() |
15ae37d077 | ||
![]() |
461d5eb687 | ||
![]() |
3058fcad46 | ||
![]() |
06bd1ae4cd | ||
![]() |
00733357a1 | ||
![]() |
7f6ce97199 | ||
![]() |
0742aed8e7 | ||
![]() |
f675dd3388 | ||
![]() |
9751cb4e50 | ||
![]() |
7919028780 | ||
![]() |
1508c7c905 | ||
![]() |
3fe907f388 | ||
![]() |
5cc87661b9 | ||
![]() |
fc372172a6 | ||
![]() |
7bd9a39bf5 | ||
![]() |
665c971822 | ||
![]() |
eff5471dd1 | ||
![]() |
4fba9c3c0a | ||
![]() |
0b32b51e2f | ||
![]() |
6370b0b8e5 | ||
![]() |
681518f443 | ||
![]() |
9f5b89978d | ||
![]() |
130839ee7b | ||
![]() |
ba4ec960c8 | ||
![]() |
6692d9c6aa | ||
![]() |
4d2d94c54f | ||
![]() |
d59c6612c6 | ||
![]() |
498f158253 | ||
![]() |
b8026ccf46 | ||
![]() |
84def48222 | ||
![]() |
cea0ac02fe | ||
![]() |
e1b099e88b | ||
![]() |
d571ef3f18 | ||
![]() |
c8cffef647 | ||
![]() |
6b568307a4 | ||
![]() |
1b501907f1 | ||
![]() |
c7e79998a4 | ||
![]() |
03ccf014d9 | ||
![]() |
ab41bdb87d | ||
![]() |
821a0bc418 | ||
![]() |
6d931b9e37 | ||
![]() |
f0341f28ab | ||
![]() |
603663e0cc | ||
![]() |
80dddc9eef | ||
![]() |
8494d0542e | ||
![]() |
467a5bef0b | ||
![]() |
24736e36ff | ||
![]() |
604c00d772 | ||
![]() |
12bbba1bff | ||
![]() |
e3221ad4ee | ||
![]() |
7d3a7fa1db | ||
![]() |
5e67bf1fa7 | ||
![]() |
7147c7578d | ||
![]() |
90710fedf2 | ||
![]() |
9629159ef1 | ||
![]() |
bfac6e1516 | ||
![]() |
7c288d1769 | ||
![]() |
39119eeb2a | ||
![]() |
9c16ce3342 | ||
![]() |
3595fab5cb | ||
![]() |
78b2d17f10 | ||
![]() |
7b8b8f9d0b | ||
![]() |
b7f866faff | ||
![]() |
37671fd613 | ||
![]() |
92905c1433 | ||
![]() |
cb7251cb5e | ||
![]() |
44485c0de4 | ||
![]() |
1191a09576 | ||
![]() |
c178488aac | ||
![]() |
9f6463eec0 | ||
![]() |
70fef59401 | ||
![]() |
3e053e07c6 | ||
![]() |
376cac6002 | ||
![]() |
5cd68301ed | ||
![]() |
3121721ac7 | ||
![]() |
868daf692d | ||
![]() |
75e9ac9e73 | ||
![]() |
f6e36f2038 | ||
![]() |
55770f3e02 | ||
![]() |
cfc7f91f03 | ||
![]() |
74488c0b96 | ||
![]() |
1154d1769d | ||
![]() |
a820cd4576 | ||
![]() |
678a8a85cb | ||
![]() |
358e450e60 | ||
![]() |
32acef8fad | ||
![]() |
fae619085c | ||
![]() |
5d89563aa5 | ||
![]() |
d7dd11ba7f | ||
![]() |
4624cc609f | ||
![]() |
f24b6a4cb1 | ||
![]() |
2b06742bb9 | ||
![]() |
9e5b7462af | ||
![]() |
a544ff4c8a | ||
![]() |
b81d2013dc | ||
![]() |
9b7e2886b6 | ||
![]() |
69eaf178ca | ||
![]() |
1a14511fa6 | ||
![]() |
e517175f68 | ||
![]() |
ee9cbf7370 | ||
![]() |
efd7b380a9 | ||
![]() |
46cc254f77 | ||
![]() |
d996ba818f | ||
![]() |
e5f41ceb3e | ||
![]() |
2e4ce71d06 | ||
![]() |
50e39de974 | ||
![]() |
1ba941282c | ||
![]() |
b656ddc1f0 | ||
![]() |
a699149388 |
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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
|
||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
@@ -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
@@ -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
|
2
.github/workflows/relative-ci.yaml
vendored
@@ -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 }}
|
||||
|
6
.github/workflows/release.yaml
vendored
@@ -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
|
||||
|
58
.github/workflows/restrict-task-creation.yml
vendored
Normal 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
@@ -53,3 +53,7 @@ src/cast/dev_const.ts
|
||||
|
||||
# test coverage
|
||||
test/coverage/
|
||||
|
||||
# AI tooling
|
||||
.claude
|
||||
|
||||
|
@@ -1 +1 @@
|
||||
yarn run lint-staged --relative --shell "/bin/bash"
|
||||
yarn run lint-staged --relative
|
||||
|
@@ -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
|
@@ -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
|
||||
|
@@ -302,7 +302,7 @@ export class HcConnect extends LitElement {
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
}
|
||||
|
||||
.error a {
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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 () => {};
|
||||
});
|
||||
};
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
}
|
||||
`;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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: "%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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(" ") +
|
||||
|
102
package.json
@@ -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"
|
||||
}
|
||||
|
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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"
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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",
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
},
|
||||
|
68
src/common/entity/group_entities.ts
Normal 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,
|
||||
});
|
||||
};
|
@@ -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(
|
||||
|
@@ -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);
|
||||
|
||||
|
4
src/common/entity/valid_service_id.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
const validServiceId = /^(\w+)\.(\w+)$/;
|
||||
|
||||
export const isValidServiceId = (actionId: string) =>
|
||||
validServiceId.test(actionId);
|
@@ -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(":");
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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) => {
|
||||
|
@@ -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"]
|
||||
|
14
src/common/translations/markdown_support.ts
Normal 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
|
||||
>`,
|
||||
});
|
72
src/components/chart/down-sample.ts
Normal 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[];
|
||||
}
|
@@ -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 {
|
||||
|
299
src/components/chart/ha-network-graph.ts
Normal 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 };
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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>
|
||||
`;
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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[];
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -173,7 +173,6 @@ class HaStatisticsPicker extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
width: 200px;
|
||||
display: block;
|
||||
}
|
||||
ha-statistic-picker {
|
||||
|