mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-17 07:30:24 +00:00
Compare commits
527
Commits
20251203.0
...
supervisor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73a1ce90c3 | ||
|
|
ea1b7b9dec | ||
|
|
fd506d4d72 | ||
|
|
a3be09018c | ||
|
|
3364d4f578 | ||
|
|
1f04379974 | ||
|
|
e060c179f6 | ||
|
|
54b72ce2b8 | ||
|
|
5795b8787d | ||
|
|
ec742d3342 | ||
|
|
e4d6f3c9d7 | ||
|
|
2d496afdbc | ||
|
|
681b60614f | ||
|
|
1654a67d30 | ||
|
|
8f00494d53 | ||
|
|
d9c7c0422b | ||
|
|
2d24447c3c | ||
|
|
3b8d485ec6 | ||
|
|
4e4a00e3e9 | ||
|
|
14f7328f92 | ||
|
|
c5ad074dfb | ||
|
|
07aa8706ce | ||
|
|
1665fa3775 | ||
|
|
3be6a87658 | ||
|
|
9092de5c28 | ||
|
|
e0fc661920 | ||
|
|
aaad8e5434 | ||
|
|
35c668744a | ||
|
|
081b0a0222 | ||
|
|
829cd96e9b | ||
|
|
c404e66ee5 | ||
|
|
d47d3f9694 | ||
|
|
622df52167 | ||
|
|
20345c3771 | ||
|
|
fc7468a43b | ||
|
|
c8ab65cde9 | ||
|
|
001ade24ea | ||
|
|
f987cfe91e | ||
|
|
0bc0acebe0 | ||
|
|
2087efca51 | ||
|
|
16a4a07080 | ||
|
|
58eefcb216 | ||
|
|
92a36ac687 | ||
|
|
8221ca8971 | ||
|
|
8f69cbb6c1 | ||
|
|
4c111e1a7d | ||
|
|
af8659d8ed | ||
|
|
c9e1c9e0a3 | ||
|
|
9d15499953 | ||
|
|
7d54dd4940 | ||
|
|
8ef717df6e | ||
|
|
7b4a7403c8 | ||
|
|
22b7c52828 | ||
|
|
04dbeb5e84 | ||
|
|
db5f823b6b | ||
|
|
1d241aa49a | ||
|
|
fece231faf | ||
|
|
fffb3c3a28 | ||
|
|
fe14d436ff | ||
|
|
42e02be928 | ||
|
|
6213b6cd2a | ||
|
|
cd75c55392 | ||
|
|
ca325020d7 | ||
|
|
6250402661 | ||
|
|
0bfca79851 | ||
|
|
49bddf6139 | ||
|
|
0daf94e98f | ||
|
|
00a3237611 | ||
|
|
53deb3f419 | ||
|
|
6c1c7cead3 | ||
|
|
f8d65cc0ec | ||
|
|
5be7bad176 | ||
|
|
0a54a93a39 | ||
|
|
156583aff1 | ||
|
|
7572257821 | ||
|
|
4703cf802f | ||
|
|
55c2315329 | ||
|
|
7d7e95ac55 | ||
|
|
6d7694caff | ||
|
|
d7b6243698 | ||
|
|
73feef9e92 | ||
|
|
453a546574 | ||
|
|
52c0e6f1f5 | ||
|
|
444f8d87b3 | ||
|
|
57a586c3a7 | ||
|
|
1975265e6b | ||
|
|
66e6cb8dbc | ||
|
|
9ce9d254f8 | ||
|
|
1beca4bfa6 | ||
|
|
82ab29cfc5 | ||
|
|
3579c66f71 | ||
|
|
c042a8e310 | ||
|
|
8d2794a4ee | ||
|
|
50be1d9345 | ||
|
|
c551bf03b6 | ||
|
|
cd062293fc | ||
|
|
e89ea47d3a | ||
|
|
2cd209a6a4 | ||
|
|
9bbc761736 | ||
|
|
9097faa04b | ||
|
|
fcf844cf1a | ||
|
|
8808c31e98 | ||
|
|
e0a9f5a08a | ||
|
|
56d71c8e54 | ||
|
|
125ab4c671 | ||
|
|
8014216c45 | ||
|
|
55ba331489 | ||
|
|
ad2ff672b0 | ||
|
|
00907ecd17 | ||
|
|
07d8219136 | ||
|
|
f37241c84c | ||
|
|
65d046132d | ||
|
|
122cf40092 | ||
|
|
28ed5c86c7 | ||
|
|
1f99c3d895 | ||
|
|
f2293713de | ||
|
|
b3f202400c | ||
|
|
010d87bd0d | ||
|
|
b403b8f09e | ||
|
|
b9a3dc795b | ||
|
|
35dbfdebcf | ||
|
|
c5e5fb3ace | ||
|
|
e649472b20 | ||
|
|
3cbb24a4c5 | ||
|
|
f92608a9d3 | ||
|
|
6591cdc5c1 | ||
|
|
0ae1ac367d | ||
|
|
6d3a1b93e1 | ||
|
|
6d7b22a21c | ||
|
|
784ee22623 | ||
|
|
c03654ef8e | ||
|
|
826cb3117d | ||
|
|
f77fa26ffe | ||
|
|
35e30f9184 | ||
|
|
7dd3ade678 | ||
|
|
6d1e15d11a | ||
|
|
f5b33922ff | ||
|
|
ceb7baf851 | ||
|
|
d195fd3244 | ||
|
|
231cd632d6 | ||
|
|
82d72ea39c | ||
|
|
022bebb14f | ||
|
|
0981ae1b4a | ||
|
|
9608824a28 | ||
|
|
33d215533e | ||
|
|
5c503ecac0 | ||
|
|
d114693fed | ||
|
|
7a8cb80413 | ||
|
|
f5cd234c4b | ||
|
|
49bed5e6a6 | ||
|
|
b84a51235d | ||
|
|
602d6a2337 | ||
|
|
6e614cd3f2 | ||
|
|
6793edd68b | ||
|
|
ad6e3267c3 | ||
|
|
f941117ca4 | ||
|
|
aef0bf03e3 | ||
|
|
f22f6b74db | ||
|
|
913c4ae24e | ||
|
|
4b7b5fa21a | ||
|
|
bf6887541b | ||
|
|
26da9f3a37 | ||
|
|
d48520efdf | ||
|
|
d462356122 | ||
|
|
9a5cdb0a99 | ||
|
|
eaf012d5ff | ||
|
|
19934dad72 | ||
|
|
6194f73442 | ||
|
|
dbc880fe35 | ||
|
|
be4e46a3c6 | ||
|
|
2fce89a689 | ||
|
|
81d21b0907 | ||
|
|
65381b1dc5 | ||
|
|
7cbede2f6e | ||
|
|
0a13dddaea | ||
|
|
662be980e8 | ||
|
|
209abf466d | ||
|
|
db9a3bd562 | ||
|
|
36ecaa6610 | ||
|
|
4f46d0f4a3 | ||
|
|
42ad47649d | ||
|
|
c62ee6e692 | ||
|
|
b38c8d7d5f | ||
|
|
83bcc39d5f | ||
|
|
8d317d1e2c | ||
|
|
9acad2e83c | ||
|
|
9099c5a92c | ||
|
|
60c4d60d66 | ||
|
|
e8a4cde643 | ||
|
|
148eab31b6 | ||
|
|
9f7683021e | ||
|
|
549bd25f5c | ||
|
|
eb74dd541a | ||
|
|
4c84c7b54f | ||
|
|
3a8f964ebd | ||
|
|
211ddcf7a4 | ||
|
|
021e5f5ce0 | ||
|
|
4398e2d6c2 | ||
|
|
e95af3c661 | ||
|
|
53af746466 | ||
|
|
18103c0e36 | ||
|
|
d6d235d032 | ||
|
|
3761dec700 | ||
|
|
606fa41e6e | ||
|
|
0ee37e9544 | ||
|
|
b0027b8c18 | ||
|
|
a397368e02 | ||
|
|
cac89e94df | ||
|
|
cc2e001990 | ||
|
|
b1e2724aca | ||
|
|
5a60750841 | ||
|
|
94c180d64b | ||
|
|
6121f425c4 | ||
|
|
91028fd0dd | ||
|
|
082a2c08b9 | ||
|
|
d447dad28d | ||
|
|
4358ccdff8 | ||
|
|
8f7dd9b0ef | ||
|
|
52db6a16d1 | ||
|
|
ba213bf11c | ||
|
|
5dea6b2c89 | ||
|
|
97d51094df | ||
|
|
0904a1116c | ||
|
|
282458e645 | ||
|
|
063c2d776a | ||
|
|
97e0bc8080 | ||
|
|
21e2c676b8 | ||
|
|
214b8cd5c7 | ||
|
|
3bd5481274 | ||
|
|
ac63f991b2 | ||
|
|
97e9129832 | ||
|
|
704887999b | ||
|
|
3194fe9a30 | ||
|
|
5ce7308194 | ||
|
|
f9a9cf0ba0 | ||
|
|
bf90c6829f | ||
|
|
36d4097ff8 | ||
|
|
92bf8c3d47 | ||
|
|
4251f3468b | ||
|
|
a6869e7c14 | ||
|
|
bd46c358fb | ||
|
|
30b8ea1ae8 | ||
|
|
a24dacf50d | ||
|
|
7cbd07e33e | ||
|
|
c72ad83532 | ||
|
|
f2aba45dfe | ||
|
|
639c2ce077 | ||
|
|
1bddc02ae0 | ||
|
|
ebea5176e2 | ||
|
|
39f550cf9f | ||
|
|
cdcbd00a92 | ||
|
|
5b0bd9d577 | ||
|
|
d839152fd1 | ||
|
|
407cb79805 | ||
|
|
7817ebe983 | ||
|
|
7e58cedd49 | ||
|
|
06334a039c | ||
|
|
6e5853a1c0 | ||
|
|
f4f4520773 | ||
|
|
94453dfba5 | ||
|
|
86cd0e81ad | ||
|
|
6efe444af3 | ||
|
|
09c9665b2f | ||
|
|
23e394fec9 | ||
|
|
4b02a11634 | ||
|
|
ed9c00cab5 | ||
|
|
eaa1fb4107 | ||
|
|
c0a49b3d0b | ||
|
|
a7a00228a2 | ||
|
|
c85f7a71b2 | ||
|
|
c95e914219 | ||
|
|
362a0b96ab | ||
|
|
ef984fc438 | ||
|
|
ce9bbc9972 | ||
|
|
a3921f0559 | ||
|
|
f8ec5d27a4 | ||
|
|
d679916fa5 | ||
|
|
96be4768d3 | ||
|
|
6ff3b9f761 | ||
|
|
9026009842 | ||
|
|
54398a4784 | ||
|
|
fa3cc970ec | ||
|
|
1cf0560003 | ||
|
|
2a4ac15987 | ||
|
|
f264eebe49 | ||
|
|
dae27e091f | ||
|
|
7ca681e417 | ||
|
|
1adfe63322 | ||
|
|
119a505a0d | ||
|
|
1f8403f6c1 | ||
|
|
7dd7309a47 | ||
|
|
736afe2530 | ||
|
|
92980dfddf | ||
|
|
9f3d6e1fea | ||
|
|
9e9adfcc90 | ||
|
|
9b6ebdfcc0 | ||
|
|
88faedba65 | ||
|
|
b125cd5f3e | ||
|
|
3425837de3 | ||
|
|
24a797e46a | ||
|
|
4a486ff28b | ||
|
|
6be390a07e | ||
|
|
c93942919b | ||
|
|
ca06269a91 | ||
|
|
b3d7c0b6dc | ||
|
|
e709703f79 | ||
|
|
fd42614a23 | ||
|
|
4050dd0384 | ||
|
|
9996b1bfea | ||
|
|
8092340d2a | ||
|
|
443e205395 | ||
|
|
8c5d0c4b80 | ||
|
|
3005f12ef5 | ||
|
|
3bcf530200 | ||
|
|
af2fa98666 | ||
|
|
8f335668db | ||
|
|
51acd271ea | ||
|
|
830e9b5089 | ||
|
|
b13cb84baf | ||
|
|
f8df7f37ea | ||
|
|
54e4f4e60a | ||
|
|
81cb483163 | ||
|
|
eb5b14ea00 | ||
|
|
b221fbb387 | ||
|
|
3033bfb1fb | ||
|
|
8db967eb2f | ||
|
|
10af86cc02 | ||
|
|
dfc7116819 | ||
|
|
685c642bfc | ||
|
|
2e547937b8 | ||
|
|
e3e01c327a | ||
|
|
abd706fed0 | ||
|
|
3bd45dd29b | ||
|
|
206f067d2b | ||
|
|
caefa7530a | ||
|
|
de142e33a1 | ||
|
|
5d4c3ebfcd | ||
|
|
eb910c5ac5 | ||
|
|
72726a2e0f | ||
|
|
253b49871e | ||
|
|
6306890922 | ||
|
|
fee16ed4e4 | ||
|
|
055f6c82fb | ||
|
|
0c1627c69a | ||
|
|
780c03ece8 | ||
|
|
62dc66abd8 | ||
|
|
08aaee754e | ||
|
|
bba400443e | ||
|
|
a7a937e197 | ||
|
|
2a97913520 | ||
|
|
9f16ce7341 | ||
|
|
3e20e9b388 | ||
|
|
6891eb9ff8 | ||
|
|
f649b3783d | ||
|
|
c46f67d572 | ||
|
|
86acfa67dd | ||
|
|
3c5c19270f | ||
|
|
6db7817032 | ||
|
|
05ca8253f0 | ||
|
|
071161e82d | ||
|
|
9cd38c7128 | ||
|
|
6322c19a45 | ||
|
|
74b51b77fe | ||
|
|
b80481b53e | ||
|
|
2ce1eaf8c6 | ||
|
|
4030ce3f88 | ||
|
|
41cabde393 | ||
|
|
47d1fdf673 | ||
|
|
e59e83fffe | ||
|
|
b896b78876 | ||
|
|
6e180c9fb4 | ||
|
|
0ce0247a2c | ||
|
|
ce8cabbad9 | ||
|
|
0802841606 | ||
|
|
cb93e1b741 | ||
|
|
30c383a2fc | ||
|
|
73ee235fef | ||
|
|
b50daecdec | ||
|
|
f5bd0816a8 | ||
|
|
b6d1e65044 | ||
|
|
e2312e52e3 | ||
|
|
46a6da33d9 | ||
|
|
e7b0b9a090 | ||
|
|
63ffc3b7f9 | ||
|
|
ebfceae38c | ||
|
|
cccfc1716b | ||
|
|
6abf0dfcb9 | ||
|
|
9d2404763f | ||
|
|
6c84c9b44e | ||
|
|
edb2172bdd | ||
|
|
067e7645ac | ||
|
|
c3e4af3db8 | ||
|
|
f44be2d3b9 | ||
|
|
cff7ed9b05 | ||
|
|
c4e5f1dba6 | ||
|
|
d1011d691f | ||
|
|
7dda8c36bc | ||
|
|
31603ea7b2 | ||
|
|
17c1043cfc | ||
|
|
da255dce40 | ||
|
|
0c68072f8f | ||
|
|
d197fd8f76 | ||
|
|
a961a87872 | ||
|
|
cc96c707b9 | ||
|
|
4b73713f2a | ||
|
|
c001102f15 | ||
|
|
c1e5e0bfcb | ||
|
|
a1412e90fd | ||
|
|
f6f40c1679 | ||
|
|
45b2376616 | ||
|
|
2f70a82d02 | ||
|
|
00868b2450 | ||
|
|
e573a726aa | ||
|
|
ef82bc2abb | ||
|
|
92e3864f63 | ||
|
|
a4af975bb3 | ||
|
|
025ffd7b56 | ||
|
|
3ea4a28931 | ||
|
|
3b092b834e | ||
|
|
ed8ccbe12c | ||
|
|
420f88f73a | ||
|
|
086aa5fa28 | ||
|
|
cca4cc512b | ||
|
|
8eb65f327a | ||
|
|
f3495feacb | ||
|
|
2161bcfa3f | ||
|
|
1400398422 | ||
|
|
506d466c03 | ||
|
|
46735c72ed | ||
|
|
c43d41053b | ||
|
|
844d53a0ba | ||
|
|
1c8b78eae9 | ||
|
|
a918e878fa | ||
|
|
ebc354bf55 | ||
|
|
98a1f5ca3a | ||
|
|
48015ab312 | ||
|
|
09515b1937 | ||
|
|
5a40627676 | ||
|
|
cd34447603 | ||
|
|
803fabbf64 | ||
|
|
78c4dc48d0 | ||
|
|
147600ea43 | ||
|
|
2f91f0dd15 | ||
|
|
3fa330acfb | ||
|
|
e0a6b671ce | ||
|
|
d6edd150a8 | ||
|
|
2c00889921 | ||
|
|
0447d87f18 | ||
|
|
d7e18b0520 | ||
|
|
e7254b1587 | ||
|
|
8681a7d450 | ||
|
|
fff12acb6b | ||
|
|
3d327ed628 | ||
|
|
0d51648de1 | ||
|
|
c5642c15b8 | ||
|
|
7f885010de | ||
|
|
356d51f974 | ||
|
|
38a907e51e | ||
|
|
87c0b1d887 | ||
|
|
0f195015b7 | ||
|
|
17a976af67 | ||
|
|
a41a7e822a | ||
|
|
5473bf56c6 | ||
|
|
029eba7ab8 | ||
|
|
824a3f288d | ||
|
|
fdd89c05d3 | ||
|
|
c33cb7fff9 | ||
|
|
de53ad8dce | ||
|
|
2dec7490b6 | ||
|
|
6ed4bd5ce8 | ||
|
|
4e899c56ed | ||
|
|
53c20a0493 | ||
|
|
be2d6e9212 | ||
|
|
915442c571 | ||
|
|
de6ebc2d0a | ||
|
|
bcb12fa062 | ||
|
|
078915743d | ||
|
|
54c524127f | ||
|
|
334e1c35e1 | ||
|
|
528c7727e2 | ||
|
|
d1043e33df | ||
|
|
b088f2c0e5 | ||
|
|
b33f407493 | ||
|
|
502d76b316 | ||
|
|
1ddc07c215 | ||
|
|
a611a5fc4e | ||
|
|
c13bece6d0 | ||
|
|
28a89ff9e6 | ||
|
|
81b5ddec9d | ||
|
|
ce86aabe32 | ||
|
|
a8910bcbe4 | ||
|
|
0e4cf9f62d | ||
|
|
506635d649 | ||
|
|
27e24ee49b | ||
|
|
bcd712b48c | ||
|
|
461ef9b916 | ||
|
|
b794989daa | ||
|
|
6727ffaae0 | ||
|
|
4df8501b20 | ||
|
|
539d0e443f | ||
|
|
9c9d274b5c | ||
|
|
d6e6bc0e80 | ||
|
|
530a70b168 | ||
|
|
23137500f8 | ||
|
|
63e7ed21a4 | ||
|
|
92611f46f4 | ||
|
|
9bc896241d | ||
|
|
2baafe620c | ||
|
|
ce52bbaf8c | ||
|
|
0b4b8d9082 | ||
|
|
bddbb773b7 | ||
|
|
d52e1e8835 | ||
|
|
0a9dccfd19 | ||
|
|
bfd78670cc | ||
|
|
11276af1a0 | ||
|
|
d7be46c00b | ||
|
|
94f32ce242 | ||
|
|
ef3e8186bc | ||
|
|
50fcf622aa | ||
|
|
77c2444be8 | ||
|
|
e5cb26cd3d | ||
|
|
2896519bfd | ||
|
|
0b6e35eb53 | ||
|
|
e80a855f87 | ||
|
|
7c88cf4e30 | ||
|
|
9001cd3e65 | ||
|
|
ca8923d8f4 |
@@ -22,11 +22,13 @@ You are an assistant helping with development of the Home Assistant frontend. Th
|
||||
```bash
|
||||
yarn lint # ESLint + Prettier + TypeScript + Lit
|
||||
yarn format # Auto-fix ESLint + Prettier
|
||||
yarn lint:types # TypeScript compiler
|
||||
yarn lint:types # TypeScript compiler (run WITHOUT file arguments)
|
||||
yarn test # Vitest
|
||||
script/develop # Development server
|
||||
```
|
||||
|
||||
> **WARNING:** Never run `tsc` or `yarn lint:types` with file arguments (e.g., `yarn lint:types src/file.ts`). When `tsc` receives file arguments, it ignores `tsconfig.json` and emits `.js` files into `src/`, polluting the codebase. Always run `yarn lint:types` without arguments. For individual file type checking, rely on IDE diagnostics. If `.js` files are accidentally generated, clean up with `git clean -fd src/`.
|
||||
|
||||
### Component Prefixes
|
||||
|
||||
- `ha-` - Home Assistant components
|
||||
@@ -154,7 +156,7 @@ try {
|
||||
|
||||
- **Use CSS custom properties**: Leverage the theme system
|
||||
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
|
||||
- Spacing scale: `--ha-space-0` (0px) through `--ha-space-20` (80px) in 4px increments
|
||||
- Spacing scale: `--ha-space-1` (4px) through `--ha-space-20` (80px) in 4px increments
|
||||
- Defined in `src/resources/theme/core.globals.ts`
|
||||
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
|
||||
- **Mobile-first responsive**: Design for mobile, enhance for desktop
|
||||
@@ -619,7 +621,6 @@ this.hass.localize("ui.panel.config.updates.update_available", {
|
||||
|
||||
#### Key Terminology
|
||||
|
||||
- **"add-on"** (hyphenated, not "addon")
|
||||
- **"integration"** (preferred over "component")
|
||||
- **Technical terms**: Use lowercase (automation, entity, device, service)
|
||||
|
||||
@@ -711,7 +712,7 @@ this.hass.localize("ui.panel.config.automation.delete_confirm", {
|
||||
- [ ] American English spelling
|
||||
- [ ] Friendly, informational tone
|
||||
- [ ] Avoids abbreviations and jargon
|
||||
- [ ] Correct terminology (add-on not addon, integration not component)
|
||||
- [ ] Correct terminology (integration not component)
|
||||
|
||||
### Component-Specific Checks
|
||||
|
||||
|
||||
@@ -21,12 +21,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -56,12 +56,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
+18
-11
@@ -24,9 +24,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -58,9 +58,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -76,9 +76,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -89,20 +89,27 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
if-no-files-found: error
|
||||
- name: Upload frontend build
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: frontend-build
|
||||
path: hass_frontend/
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
supervisor:
|
||||
name: Build supervisor
|
||||
needs: [lint, test]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -113,7 +120,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
|
||||
@@ -22,12 +22,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -57,12 +57,12 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
@@ -16,10 +16,10 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
@@ -21,10 +21,10 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
process-only: "issues, prs"
|
||||
|
||||
@@ -20,15 +20,15 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -57,14 +57,14 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@feb19ddc698445db27401f1490f6ac182da0816f # v3.2.0
|
||||
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
token: ${{ github.token }}
|
||||
|
||||
@@ -19,14 +19,17 @@ jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
permissions:
|
||||
contents: write # Required to upload release assets
|
||||
id-token: write # For "Trusted Publisher" to PyPi
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
@@ -34,7 +37,7 @@ jobs:
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -46,16 +49,20 @@ jobs:
|
||||
run: ./script/translations_download
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
|
||||
- name: Build and release package
|
||||
run: |
|
||||
python3 -m pip install twine build
|
||||
export TWINE_USERNAME="__token__"
|
||||
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||
python3 -m pip install build
|
||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
||||
script/release
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
@@ -75,7 +82,7 @@ jobs:
|
||||
|
||||
# home-assistant/wheels doesn't support SHA pinning
|
||||
- name: Build wheels
|
||||
uses: home-assistant/[email protected]0.0
|
||||
uses: home-assistant/[email protected]2.0
|
||||
with:
|
||||
abi: cp313
|
||||
tag: musllinux_1_2
|
||||
@@ -91,9 +98,9 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -108,7 +115,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@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
|
||||
|
||||
@@ -120,9 +127,9 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -137,6 +144,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@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
||||
+2
-1
@@ -15,7 +15,7 @@ dist/
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
/node_modules/
|
||||
node_modules/
|
||||
yarn-error.log
|
||||
npm-debug.log
|
||||
|
||||
@@ -56,4 +56,5 @@ test/coverage/
|
||||
|
||||
# AI tooling
|
||||
.claude
|
||||
.cursor
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
compressionLevel: mixed
|
||||
|
||||
npmMinimalAgeGate: "3d"
|
||||
|
||||
defaultSemverRangePrefix: ""
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
@@ -20,8 +20,6 @@ module.exports.ignorePackages = () => [];
|
||||
// Files from NPM packages that we should replace with empty file
|
||||
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
|
||||
[
|
||||
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
||||
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
||||
// Icons in supervisor conflict with icons in HA so we don't load.
|
||||
(isHassioBuild || isLandingPageBuild) &&
|
||||
require.resolve(
|
||||
|
||||
@@ -156,7 +156,9 @@ const createTestTranslation = () =>
|
||||
*/
|
||||
const createMasterTranslation = () =>
|
||||
gulp
|
||||
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
|
||||
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])], {
|
||||
allowEmpty: true,
|
||||
})
|
||||
.pipe(new CustomJSON(lokaliseTransform))
|
||||
.pipe(new MergeJSON("en"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
|
||||
@@ -168,12 +168,16 @@ const createRspackConfig = ({
|
||||
);
|
||||
},
|
||||
}),
|
||||
new rspack.NormalModuleReplacementPlugin(
|
||||
new RegExp(
|
||||
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).join("|")
|
||||
),
|
||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||
),
|
||||
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).length
|
||||
? new rspack.NormalModuleReplacementPlugin(
|
||||
new RegExp(
|
||||
bundle
|
||||
.emptyPackages({ isHassioBuild, isLandingPageBuild })
|
||||
.join("|")
|
||||
),
|
||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||
)
|
||||
: false,
|
||||
!isProdBuild && new LogStartCompilePlugin(),
|
||||
isProdBuild &&
|
||||
new StatsWriterPlugin({
|
||||
@@ -201,6 +205,7 @@ const createRspackConfig = ({
|
||||
"lit/decorators$": "lit/decorators.js",
|
||||
"lit/directive$": "lit/directive.js",
|
||||
"lit/directives/until$": "lit/directives/until.js",
|
||||
"lit/directives/ref$": "lit/directives/ref.js",
|
||||
"lit/directives/class-map$": "lit/directives/class-map.js",
|
||||
"lit/directives/style-map$": "lit/directives/style-map.js",
|
||||
"lit/directives/if-defined$": "lit/directives/if-defined.js",
|
||||
@@ -209,7 +214,9 @@ const createRspackConfig = ({
|
||||
"lit/directives/join$": "lit/directives/join.js",
|
||||
"lit/directives/repeat$": "lit/directives/repeat.js",
|
||||
"lit/directives/live$": "lit/directives/live.js",
|
||||
"lit/directives/keyed$": "lit/directives/keyed.js",
|
||||
"lit/directives/keyed$": latestBuild
|
||||
? "lit/directives/keyed.js"
|
||||
: path.resolve(__dirname, "../src/common/lit/keyed-es5.ts"),
|
||||
"lit/polyfill-support$": "lit/polyfill-support.js",
|
||||
"@lit-labs/virtualizer/layouts/grid":
|
||||
"@lit-labs/virtualizer/layouts/grid.js",
|
||||
@@ -217,6 +224,42 @@ const createRspackConfig = ({
|
||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
|
||||
"@lit-labs/observers/resize-controller":
|
||||
"@lit-labs/observers/resize-controller.js",
|
||||
"@formatjs/intl-durationformat/should-polyfill$":
|
||||
"@formatjs/intl-durationformat/should-polyfill.js",
|
||||
"@formatjs/intl-durationformat/polyfill-force$":
|
||||
"@formatjs/intl-durationformat/polyfill-force.js",
|
||||
"@formatjs/intl-datetimeformat/should-polyfill":
|
||||
"@formatjs/intl-datetimeformat/should-polyfill.js",
|
||||
"@formatjs/intl-datetimeformat/polyfill-force":
|
||||
"@formatjs/intl-datetimeformat/polyfill-force.js",
|
||||
"@formatjs/intl-displaynames/should-polyfill":
|
||||
"@formatjs/intl-displaynames/should-polyfill.js",
|
||||
"@formatjs/intl-displaynames/polyfill-force":
|
||||
"@formatjs/intl-displaynames/polyfill-force.js",
|
||||
"@formatjs/intl-getcanonicallocales/should-polyfill":
|
||||
"@formatjs/intl-getcanonicallocales/should-polyfill.js",
|
||||
"@formatjs/intl-getcanonicallocales/polyfill-force":
|
||||
"@formatjs/intl-getcanonicallocales/polyfill-force.js",
|
||||
"@formatjs/intl-listformat/should-polyfill":
|
||||
"@formatjs/intl-listformat/should-polyfill.js",
|
||||
"@formatjs/intl-listformat/polyfill-force":
|
||||
"@formatjs/intl-listformat/polyfill-force.js",
|
||||
"@formatjs/intl-locale/should-polyfill":
|
||||
"@formatjs/intl-locale/should-polyfill.js",
|
||||
"@formatjs/intl-locale/polyfill-force":
|
||||
"@formatjs/intl-locale/polyfill-force.js",
|
||||
"@formatjs/intl-numberformat/should-polyfill":
|
||||
"@formatjs/intl-numberformat/should-polyfill.js",
|
||||
"@formatjs/intl-numberformat/polyfill-force":
|
||||
"@formatjs/intl-numberformat/polyfill-force.js",
|
||||
"@formatjs/intl-pluralrules/should-polyfill":
|
||||
"@formatjs/intl-pluralrules/should-polyfill.js",
|
||||
"@formatjs/intl-pluralrules/polyfill-force":
|
||||
"@formatjs/intl-pluralrules/polyfill-force.js",
|
||||
"@formatjs/intl-relativetimeformat/should-polyfill":
|
||||
"@formatjs/intl-relativetimeformat/should-polyfill.js",
|
||||
"@formatjs/intl-relativetimeformat/polyfill-force":
|
||||
"@formatjs/intl-relativetimeformat/polyfill-force.js",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
|
||||
@@ -5,17 +5,19 @@ const castContext = framework.CastReceiverContext.getInstance();
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor(
|
||||
framework.messages.MessageType.LOAD,
|
||||
"LOAD" as framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
const media = loadRequestData.media;
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = framework.messages.StreamType.LIVE;
|
||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// @ts-ignore
|
||||
// type definition is wrong, should be "FMP4" instead of "fmp4"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
||||
media.hlsVideoSegmentFormat =
|
||||
framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
return loadRequestData;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { framework } from "./cast_framework";
|
||||
import { CAST_NS } from "../../../src/cast/const";
|
||||
import type { HassMessage } from "../../../src/cast/receiver_messages";
|
||||
import "../../../src/resources/custom-card-support";
|
||||
import { castContext } from "./cast_context";
|
||||
import { framework } from "./cast_framework";
|
||||
import { HcMain } from "./layout/hc-main";
|
||||
import type { ReceivedMessage } from "./types";
|
||||
|
||||
const lovelaceController = new HcMain();
|
||||
document.body.append(lovelaceController);
|
||||
@@ -40,7 +39,8 @@ const playDummyMedia = (viewTitle?: string) => {
|
||||
loadRequestData.media.contentId =
|
||||
"https://cast.home-assistant.io/images/google-nest-hub.png";
|
||||
loadRequestData.media.contentType = "image/jpeg";
|
||||
loadRequestData.media.streamType = framework.messages.StreamType.NONE;
|
||||
loadRequestData.media.streamType =
|
||||
"NONE" as framework.messages.StreamType.NONE;
|
||||
const metadata = new framework.messages.GenericMediaMetadata();
|
||||
metadata.title = viewTitle;
|
||||
loadRequestData.media.metadata = metadata;
|
||||
@@ -89,31 +89,30 @@ const showMediaPlayer = () => {
|
||||
const options = new framework.CastReceiverOptions();
|
||||
options.disableIdleTimeout = true;
|
||||
options.customNamespaces = {
|
||||
[CAST_NS]: framework.system.MessageType.JSON,
|
||||
// type definition is wrong, should be "JSON" instead of "json"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.system#.MessageType
|
||||
[CAST_NS]: "JSON" as framework.system.MessageType.JSON,
|
||||
};
|
||||
|
||||
castContext.addCustomMessageListener(
|
||||
CAST_NS,
|
||||
// @ts-ignore
|
||||
(ev: ReceivedMessage<HassMessage>) => {
|
||||
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
|
||||
if (
|
||||
playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE
|
||||
) {
|
||||
playerManager.stop();
|
||||
} else {
|
||||
showLovelaceController();
|
||||
}
|
||||
const msg = ev.data;
|
||||
msg.senderId = ev.senderId;
|
||||
lovelaceController.processIncomingMessage(msg);
|
||||
castContext.addCustomMessageListener(CAST_NS, (ev) => {
|
||||
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
|
||||
if (
|
||||
playerManager.getPlayerState() !==
|
||||
("IDLE" as framework.messages.PlayerState.IDLE)
|
||||
) {
|
||||
playerManager.stop();
|
||||
} else {
|
||||
showLovelaceController();
|
||||
}
|
||||
);
|
||||
const msg = ev.data as HassMessage;
|
||||
msg.senderId = ev.senderId;
|
||||
lovelaceController.processIncomingMessage(msg);
|
||||
});
|
||||
|
||||
const playerManager = castContext.getPlayerManager();
|
||||
|
||||
playerManager.setMessageInterceptor(
|
||||
framework.messages.MessageType.LOAD,
|
||||
"LOAD" as framework.messages.MessageType.LOAD,
|
||||
(loadRequestData) => {
|
||||
if (
|
||||
loadRequestData.media.contentId ===
|
||||
@@ -127,24 +126,26 @@ playerManager.setMessageInterceptor(
|
||||
// Special handling if it came from Google Assistant
|
||||
if (media.entity) {
|
||||
media.contentId = media.entity;
|
||||
media.streamType = framework.messages.StreamType.LIVE;
|
||||
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||
media.contentType = "application/vnd.apple.mpegurl";
|
||||
// @ts-ignore
|
||||
// type definition is wrong, should be "FMP4" instead of "fmp4"
|
||||
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
||||
media.hlsVideoSegmentFormat =
|
||||
framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||
}
|
||||
return loadRequestData;
|
||||
}
|
||||
);
|
||||
|
||||
playerManager.addEventListener(
|
||||
framework.events.EventType.MEDIA_STATUS,
|
||||
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS,
|
||||
(event) => {
|
||||
if (
|
||||
event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
|
||||
event.mediaStatus?.playerState ===
|
||||
("IDLE" as framework.messages.PlayerState.IDLE) &&
|
||||
event.mediaStatus?.idleReason &&
|
||||
event.mediaStatus?.idleReason !==
|
||||
framework.messages.IdleReason.INTERRUPTED
|
||||
("INTERRUPTED" as framework.messages.IdleReason.INTERRUPTED)
|
||||
) {
|
||||
// media finished or stopped, return to default Lovelace
|
||||
showLovelaceController();
|
||||
|
||||
@@ -305,9 +305,8 @@ export class HcMain extends HassElement {
|
||||
await llColl.refresh();
|
||||
this._unsubLovelace = llColl.subscribe(async (rawConfig) => {
|
||||
if (isStrategyDashboard(rawConfig)) {
|
||||
const { generateLovelaceDashboardStrategy } = await import(
|
||||
"../../../../src/panels/lovelace/strategies/get-strategy"
|
||||
);
|
||||
const { generateLovelaceDashboardStrategy } =
|
||||
await import("../../../../src/panels/lovelace/strategies/get-strategy");
|
||||
const config = await generateLovelaceDashboardStrategy(
|
||||
rawConfig,
|
||||
this.hass!
|
||||
@@ -347,9 +346,8 @@ export class HcMain extends HassElement {
|
||||
}
|
||||
|
||||
private async _generateDefaultLovelaceConfig() {
|
||||
const { generateLovelaceDashboardStrategy } = await import(
|
||||
"../../../../src/panels/lovelace/strategies/get-strategy"
|
||||
);
|
||||
const { generateLovelaceDashboardStrategy } =
|
||||
await import("../../../../src/panels/lovelace/strategies/get-strategy");
|
||||
this._handleNewLovelaceConfig(
|
||||
await generateLovelaceDashboardStrategy(DEFAULT_CONFIG, this.hass!)
|
||||
);
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export interface ReceivedMessage<T> {
|
||||
gj: boolean;
|
||||
data: T;
|
||||
senderId: string;
|
||||
type: "message";
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockAreaRegistry = (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DeviceRegistryEntry } from "../../../src/data/device_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../src/data/device/device_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockDeviceRegistry = (
|
||||
|
||||
@@ -44,18 +44,24 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||
number_energy_price: null,
|
||||
},
|
||||
],
|
||||
power: [
|
||||
{ stat_rate: "sensor.power_grid" },
|
||||
{ stat_rate: "sensor.power_grid_return" },
|
||||
],
|
||||
cost_adjustment_day: 0,
|
||||
},
|
||||
{
|
||||
type: "solar",
|
||||
stat_energy_from: "sensor.solar_production",
|
||||
stat_rate: "sensor.power_solar",
|
||||
config_entry_solar_forecast: ["solar_forecast"],
|
||||
},
|
||||
/* {
|
||||
{
|
||||
type: "battery",
|
||||
stat_energy_from: "sensor.battery_output",
|
||||
stat_energy_to: "sensor.battery_input",
|
||||
}, */
|
||||
stat_rate: "sensor.power_battery",
|
||||
},
|
||||
{
|
||||
type: "gas",
|
||||
stat_energy_from: "sensor.energy_gas",
|
||||
@@ -63,28 +69,48 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
{
|
||||
type: "water",
|
||||
stat_energy_from: "sensor.energy_water",
|
||||
stat_cost: "sensor.energy_water_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
],
|
||||
device_consumption: [
|
||||
{
|
||||
stat_consumption: "sensor.energy_car",
|
||||
stat_rate: "sensor.power_car",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_ac",
|
||||
stat_rate: "sensor.power_ac",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_washing_machine",
|
||||
stat_rate: "sensor.power_washing_machine",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_dryer",
|
||||
stat_rate: "sensor.power_dryer",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_heat_pump",
|
||||
stat_rate: "sensor.power_heat_pump",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_boiler",
|
||||
stat_rate: "sensor.power_boiler",
|
||||
},
|
||||
],
|
||||
device_consumption_water: [
|
||||
{
|
||||
stat_consumption: "sensor.water_kitchen",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.water_garden",
|
||||
},
|
||||
],
|
||||
device_consumption_water: [],
|
||||
})
|
||||
);
|
||||
hass.mockWS(
|
||||
|
||||
@@ -154,6 +154,38 @@ export const energyEntities = () =>
|
||||
unit_of_measurement: "EUR",
|
||||
},
|
||||
},
|
||||
"sensor.power_grid": {
|
||||
entity_id: "sensor.power_grid",
|
||||
state: "500",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_grid_return": {
|
||||
entity_id: "sensor.power_grid_return",
|
||||
state: "-100",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_solar": {
|
||||
entity_id: "sensor.power_solar",
|
||||
state: "200",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_battery": {
|
||||
entity_id: "sensor.power_battery",
|
||||
state: "100",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.energy_gas_cost": {
|
||||
entity_id: "sensor.energy_gas_cost",
|
||||
state: "2",
|
||||
@@ -171,6 +203,15 @@ export const energyEntities = () =>
|
||||
unit_of_measurement: "m³",
|
||||
},
|
||||
},
|
||||
"sensor.energy_water": {
|
||||
entity_id: "sensor.energy_water",
|
||||
state: "4000",
|
||||
attributes: {
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Water",
|
||||
unit_of_measurement: "L",
|
||||
},
|
||||
},
|
||||
"sensor.energy_car": {
|
||||
entity_id: "sensor.energy_car",
|
||||
state: "4",
|
||||
@@ -225,4 +266,58 @@ export const energyEntities = () =>
|
||||
unit_of_measurement: "kWh",
|
||||
},
|
||||
},
|
||||
"sensor.power_car": {
|
||||
entity_id: "sensor.power_car",
|
||||
state: "40",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
friendly_name: "Electric car",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_ac": {
|
||||
entity_id: "sensor.power_ac",
|
||||
state: "30",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
friendly_name: "Air conditioning",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_washing_machine": {
|
||||
entity_id: "sensor.power_washing_machine",
|
||||
state: "60",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
friendly_name: "Washing machine",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_dryer": {
|
||||
entity_id: "sensor.power_dryer",
|
||||
state: "55",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
friendly_name: "Dryer",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_heat_pump": {
|
||||
entity_id: "sensor.power_heat_pump",
|
||||
state: "60",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
friendly_name: "Heat pump",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
"sensor.power_boiler": {
|
||||
entity_id: "sensor.power_boiler",
|
||||
state: "70",
|
||||
attributes: {
|
||||
state_class: "measurement",
|
||||
friendly_name: "Boiler",
|
||||
unit_of_measurement: "W",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { EntityRegistryEntry } from "../../../src/data/entity_registry";
|
||||
import type { EntityRegistryEntry } from "../../../src/data/entity/entity_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockEntityRegistry = (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LabelRegistryEntry } from "../../../src/data/label_registry";
|
||||
import type { LabelRegistryEntry } from "../../../src/data/label/label_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockLabelRegistry = (
|
||||
|
||||
@@ -17,17 +17,15 @@ const generateMeanStatistics = (
|
||||
end: Date,
|
||||
// eslint-disable-next-line default-param-last
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
initValue: number,
|
||||
maxDiff: number
|
||||
): StatisticValue[] => {
|
||||
const statistics: StatisticValue[] = [];
|
||||
let currentDate = new Date(start);
|
||||
currentDate.setMinutes(0, 0, 0);
|
||||
let lastVal = initValue;
|
||||
const now = new Date();
|
||||
while (end > currentDate && currentDate < now) {
|
||||
const delta = Math.random() * maxDiff;
|
||||
const mean = lastVal + delta;
|
||||
const mean = delta;
|
||||
statistics.push({
|
||||
start: currentDate.getTime(),
|
||||
end: currentDate.getTime(),
|
||||
@@ -38,7 +36,6 @@ const generateMeanStatistics = (
|
||||
state: mean,
|
||||
sum: null,
|
||||
});
|
||||
lastVal = mean;
|
||||
currentDate =
|
||||
period === "day"
|
||||
? addDays(currentDate, 1)
|
||||
@@ -336,7 +333,6 @@ export const mockRecorder = (mockHass: MockHomeAssistant) => {
|
||||
start,
|
||||
end,
|
||||
period,
|
||||
state,
|
||||
state * (state > 80 ? 0.05 : 0.1)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -187,5 +187,11 @@ export default tseslint.config(
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/util/recorder-worklet.js"],
|
||||
languageOptions: {
|
||||
globals: globals.audioWorklet,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
<div class="action">
|
||||
<span>
|
||||
${this._action
|
||||
? describeAction(this.hass, [], [], {}, this._action)
|
||||
? describeAction(this.hass, [], this._action)
|
||||
: "<invalid YAML>"}
|
||||
</span>
|
||||
<ha-yaml-editor
|
||||
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
${ACTIONS.map(
|
||||
(conf) => html`
|
||||
<div class="action">
|
||||
<span>${describeAction(this.hass, [], [], {}, conf as any)}</span>
|
||||
<span>${describeAction(this.hass, [], conf as any)}</span>
|
||||
<pre>${dump(conf)}</pre>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Adaptive dialog (ha-adaptive-dialog)
|
||||
---
|
||||
@@ -0,0 +1,732 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { mdiCog, mdiHelp } from "@mdi/js";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-dialog-footer";
|
||||
import "../../../../src/components/ha-adaptive-dialog";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
const SCHEMA: HaFormSchema[] = [
|
||||
{ type: "string", name: "Name", default: "", autofocus: true },
|
||||
{ type: "string", name: "Email", default: "" },
|
||||
];
|
||||
|
||||
type DialogType =
|
||||
| false
|
||||
| "basic"
|
||||
| "basic-subtitle-below"
|
||||
| "basic-subtitle-above"
|
||||
| "form"
|
||||
| "form-block-mode"
|
||||
| "actions"
|
||||
| "large"
|
||||
| "small";
|
||||
|
||||
@customElement("demo-components-ha-adaptive-dialog")
|
||||
export class DemoHaAdaptiveDialog extends LitElement {
|
||||
@state() private _openDialog: DialogType = false;
|
||||
|
||||
@state() private _hass?: HomeAssistant;
|
||||
|
||||
protected firstUpdated() {
|
||||
const hass = provideHass(this);
|
||||
this._hass = hass;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>Adaptive dialog <code><ha-adaptive-dialog></code></h1>
|
||||
|
||||
<p class="subtitle">
|
||||
Responsive dialog component that automatically switches between a full
|
||||
dialog and bottom sheet based on screen size.
|
||||
</p>
|
||||
|
||||
<h2>Demos</h2>
|
||||
|
||||
<div class="buttons">
|
||||
<ha-button @click=${this._handleOpenDialog("basic")}
|
||||
>Basic adaptive dialog</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("basic-subtitle-below")}
|
||||
>Adaptive dialog with subtitle below</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("basic-subtitle-above")}
|
||||
>Adaptive dialog with subtitle above</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("small")}
|
||||
>Small width adaptive dialog</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("large")}
|
||||
>Large width adaptive dialog</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("form")}
|
||||
>Adaptive dialog with form</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("form-block-mode")}
|
||||
>Adaptive dialog with form (block mode change)</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("actions")}
|
||||
>Adaptive dialog with actions</ha-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
<strong>Tip:</strong> Resize your browser window to see the
|
||||
responsive behavior. The dialog automatically switches to a bottom
|
||||
sheet on narrow screens (<870px width) or short screens
|
||||
(<500px height).
|
||||
</p>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "basic"}
|
||||
header-title="Basic adaptive dialog"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>Adaptive dialog content</div>
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "basic-subtitle-below"}
|
||||
header-title="Adaptive dialog with subtitle"
|
||||
header-subtitle="This is an adaptive dialog with a subtitle below"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>Adaptive dialog content</div>
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "basic-subtitle-above"}
|
||||
header-title="Adaptive dialog with subtitle above"
|
||||
header-subtitle="This is an adaptive dialog with a subtitle above"
|
||||
header-subtitle-position="above"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>Adaptive dialog content</div>
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "small"}
|
||||
width="small"
|
||||
header-title="Small adaptive dialog"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>This dialog uses the small width preset (320px).</div>
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "large"}
|
||||
width="large"
|
||||
header-title="Large adaptive dialog"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>This dialog uses the large width preset (1024px).</div>
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "form"}
|
||||
header-title="Adaptive dialog with form"
|
||||
header-subtitle="This is an adaptive dialog with a form"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<ha-form autofocus .schema=${SCHEMA}></ha-form>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
@click=${this._handleClosed}
|
||||
slot="secondaryAction"
|
||||
variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
<ha-button
|
||||
@click=${this._handleClosed}
|
||||
slot="primaryAction"
|
||||
variant="accent"
|
||||
>Submit</ha-button
|
||||
>
|
||||
</ha-dialog-footer>
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "form-block-mode"}
|
||||
header-title="Adaptive dialog with form (block mode change)"
|
||||
header-subtitle="This form will not reset when the viewport size changes"
|
||||
block-mode-change
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<ha-form autofocus .schema=${SCHEMA}></ha-form>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
@click=${this._handleClosed}
|
||||
slot="secondaryAction"
|
||||
variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
<ha-button
|
||||
@click=${this._handleClosed}
|
||||
slot="primaryAction"
|
||||
variant="accent"
|
||||
>Submit</ha-button
|
||||
>
|
||||
</ha-dialog-footer>
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this._hass}
|
||||
.open=${this._openDialog === "actions"}
|
||||
header-title="Adaptive dialog with actions"
|
||||
header-subtitle="This is an adaptive dialog with header actions"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div slot="headerActionItems">
|
||||
<ha-icon-button label="Settings" path=${mdiCog}></ha-icon-button>
|
||||
<ha-icon-button label="Help" path=${mdiHelp}></ha-icon-button>
|
||||
</div>
|
||||
|
||||
<div>Adaptive dialog content</div>
|
||||
</ha-adaptive-dialog>
|
||||
|
||||
<h2>Design</h2>
|
||||
|
||||
<h3>Responsive behavior</h3>
|
||||
|
||||
<p>
|
||||
The <code>ha-adaptive-dialog</code> component automatically switches
|
||||
between two modes based on screen size:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Dialog mode:</strong> Used on larger screens (width >
|
||||
870px and height > 500px). Renders as a centered dialog using
|
||||
<code>ha-wa-dialog</code>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Bottom sheet mode:</strong> Used on mobile devices and
|
||||
smaller screens (width ≤ 870px or height ≤ 500px). Renders as a
|
||||
drawer from the bottom using <code>ha-bottom-sheet</code>.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
The mode is determined automatically and updates when the window is
|
||||
resized. To prevent mode changes after the initial mount (useful for
|
||||
preventing form resets), use the <code>block-mode-change</code>
|
||||
attribute.
|
||||
</p>
|
||||
|
||||
<h3>Width</h3>
|
||||
|
||||
<p>
|
||||
In dialog mode, there are multiple width presets available. These are
|
||||
ignored in bottom sheet mode.
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>small</code></td>
|
||||
<td><code>min(320px, var(--full-width))</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>medium</code></td>
|
||||
<td><code>min(580px, var(--full-width))</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>large</code></td>
|
||||
<td><code>min(1024px, var(--full-width))</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>full</code></td>
|
||||
<td><code>var(--full-width)</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>Adaptive dialogs have a default width of <code>medium</code>.</p>
|
||||
|
||||
<h3>Header</h3>
|
||||
|
||||
<p>
|
||||
The header contains a navigation icon, title, subtitle, and action
|
||||
items.
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Slot</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>headerNavigationIcon</code></td>
|
||||
<td>
|
||||
Leading header action (e.g., close/back button). In bottom sheet
|
||||
mode, defaults to a close button if not provided.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>headerTitle</code></td>
|
||||
<td>The header title content.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>headerSubtitle</code></td>
|
||||
<td>The header subtitle content.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>headerActionItems</code></td>
|
||||
<td>Trailing header actions (e.g., icon buttons, menus).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Header title</h4>
|
||||
|
||||
<p>
|
||||
The header title can be set using the <code>header-title</code>
|
||||
attribute or by providing custom content in the
|
||||
<code>headerTitle</code> slot.
|
||||
</p>
|
||||
|
||||
<h4>Header subtitle</h4>
|
||||
|
||||
<p>
|
||||
The header subtitle can be set using the
|
||||
<code>header-subtitle</code> attribute or by providing custom content
|
||||
in the <code>headerSubtitle</code> slot. The subtitle position
|
||||
relative to the title can be controlled with the
|
||||
<code>header-subtitle-position</code> attribute.
|
||||
</p>
|
||||
|
||||
<h4>Header navigation icon</h4>
|
||||
|
||||
<p>
|
||||
In bottom sheet mode, a close button is automatically provided if no
|
||||
custom navigation icon is specified. In dialog mode, the dialog can be
|
||||
closed via the standard dialog close button.
|
||||
</p>
|
||||
|
||||
<h4>Header action items</h4>
|
||||
|
||||
<p>
|
||||
The header action items usually contain icon buttons and/or menu
|
||||
buttons.
|
||||
</p>
|
||||
|
||||
<h3>Body</h3>
|
||||
|
||||
<p>The body is the content of the adaptive dialog.</p>
|
||||
|
||||
<h3>Footer</h3>
|
||||
|
||||
<p>The footer is the footer of the adaptive dialog.</p>
|
||||
|
||||
<p>
|
||||
It is recommended to use the <code>ha-dialog-footer</code> component
|
||||
for the footer and to style the buttons inside the footer as follows:
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Slot</th>
|
||||
<th>Description</th>
|
||||
<th>Variant to use</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>secondaryAction</code></td>
|
||||
<td>The secondary action button(s).</td>
|
||||
<td><code>plain</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>primaryAction</code></td>
|
||||
<td>The primary action button(s).</td>
|
||||
<td><code>accent</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Implementation</h2>
|
||||
|
||||
<h3>When to use</h3>
|
||||
|
||||
<p>
|
||||
Use <code>ha-adaptive-dialog</code> when you need a dialog that should
|
||||
adapt to different screen sizes automatically. This is particularly
|
||||
useful for:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>Forms and data entry that need to work well on mobile devices</li>
|
||||
<li>
|
||||
Content that benefits from full-screen presentation on small devices
|
||||
</li>
|
||||
<li>
|
||||
Interfaces that need consistent behavior across desktop and mobile
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
If you don't need responsive behavior, use
|
||||
<code>ha-wa-dialog</code> directly for desktop-only dialogs or
|
||||
<code>ha-bottom-sheet</code> for mobile-only sheets.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Use the <code>block-mode-change</code> attribute when you want to
|
||||
prevent the dialog from switching modes after it's opened. This is
|
||||
especially useful for forms, as it prevents form data from being lost
|
||||
when users resize their browser window.
|
||||
</p>
|
||||
|
||||
<h3>Example usage</h3>
|
||||
|
||||
<pre><code><ha-adaptive-dialog
|
||||
.hass=\${this.hass}
|
||||
open
|
||||
width="medium"
|
||||
header-title="Dialog title"
|
||||
header-subtitle="Dialog subtitle"
|
||||
>
|
||||
<div slot="headerActionItems">
|
||||
<ha-icon-button label="Settings" path="mdiCog"></ha-icon-button>
|
||||
<ha-icon-button label="Help" path="mdiHelp"></ha-icon-button>
|
||||
</div>
|
||||
<div>Dialog content</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button slot="secondaryAction" variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
<ha-button slot="primaryAction" variant="accent">Submit</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-adaptive-dialog></code></pre>
|
||||
|
||||
<p>Example with <code>block-mode-change</code> for forms:</p>
|
||||
|
||||
<pre><code><ha-adaptive-dialog
|
||||
.hass=\${this.hass}
|
||||
open
|
||||
header-title="Edit configuration"
|
||||
block-mode-change
|
||||
>
|
||||
<ha-form .schema=\${schema} .data=\${data}></ha-form>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button slot="secondaryAction" variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
<ha-button slot="primaryAction" variant="accent">Save</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-adaptive-dialog></code></pre>
|
||||
|
||||
<h3>API</h3>
|
||||
|
||||
<p>
|
||||
This component combines <code>ha-wa-dialog</code> and
|
||||
<code>ha-bottom-sheet</code> with automatic mode switching based on
|
||||
screen size.
|
||||
</p>
|
||||
|
||||
<h4>Attributes</h4>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Attribute</th>
|
||||
<th>Description</th>
|
||||
<th>Default</th>
|
||||
<th>Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>open</code></td>
|
||||
<td>Controls the adaptive dialog open state.</td>
|
||||
<td><code>false</code></td>
|
||||
<td><code>false</code>, <code>true</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>width</code></td>
|
||||
<td>
|
||||
Preferred dialog width preset (dialog mode only, ignored in
|
||||
bottom sheet mode).
|
||||
</td>
|
||||
<td><code>medium</code></td>
|
||||
<td>
|
||||
<code>small</code>, <code>medium</code>, <code>large</code>,
|
||||
<code>full</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>header-title</code></td>
|
||||
<td>Header title text when no custom title slot is provided.</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>header-subtitle</code></td>
|
||||
<td>
|
||||
Header subtitle text when no custom subtitle slot is provided.
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>header-subtitle-position</code></td>
|
||||
<td>Position of the subtitle relative to the title.</td>
|
||||
<td><code>below</code></td>
|
||||
<td><code>above</code>, <code>below</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>aria-labelledby</code></td>
|
||||
<td>
|
||||
The ID of the element that labels the dialog (for
|
||||
accessibility).
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>aria-describedby</code></td>
|
||||
<td>
|
||||
The ID of the element that describes the dialog (for
|
||||
accessibility).
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>block-mode-change</code></td>
|
||||
<td>
|
||||
When set, the mode is determined at mount time based on the
|
||||
current screen size, but subsequent mode changes are blocked.
|
||||
Useful for preventing forms from resetting when the viewport
|
||||
size changes.
|
||||
</td>
|
||||
<td><code>false</code></td>
|
||||
<td><code>false</code>, <code>true</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>CSS custom properties</h4>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CSS Property</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>--ha-dialog-surface-background</code></td>
|
||||
<td>Dialog/sheet background color.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-dialog-border-radius</code></td>
|
||||
<td>Border radius of the dialog surface (dialog mode only).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-dialog-show-duration</code></td>
|
||||
<td>Show animation duration (dialog mode only).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-dialog-hide-duration</code></td>
|
||||
<td>Hide animation duration (dialog mode only).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Events</h4>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Event</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>opened</code></td>
|
||||
<td>
|
||||
Fired when the adaptive dialog is shown (dialog mode only).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>closed</code></td>
|
||||
<td>
|
||||
Fired after the adaptive dialog is hidden (dialog mode only).
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>after-show</code></td>
|
||||
<td>Fired after show animation completes (dialog mode only).</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Focus management</h3>
|
||||
|
||||
<p>
|
||||
To automatically focus an element when the adaptive dialog opens, add
|
||||
the
|
||||
<code>autofocus</code> attribute to it. Components with
|
||||
<code>delegatesFocus: true</code> (like <code>ha-form</code>) will
|
||||
forward focus to their first focusable child.
|
||||
</p>
|
||||
|
||||
<p>Example:</p>
|
||||
|
||||
<pre><code><ha-adaptive-dialog .hass=\${this.hass} open>
|
||||
<ha-form autofocus .schema=\${schema}></ha-form>
|
||||
</ha-adaptive-dialog></code></pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleOpenDialog = (dialog: DialogType) => () => {
|
||||
this._openDialog = dialog;
|
||||
};
|
||||
|
||||
private _handleClosed = () => {
|
||||
this._openDialog = false;
|
||||
};
|
||||
|
||||
static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: var(--ha-space-6);
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
|
||||
h3,
|
||||
h4 {
|
||||
margin-top: var(--ha-space-4);
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: var(--ha-space-2) 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: var(--ha-space-2) 0;
|
||||
padding-left: var(--ha-space-5);
|
||||
}
|
||||
|
||||
li {
|
||||
margin: var(--ha-space-1) 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 1.1em;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: var(--ha-space-3) 0;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: var(--ha-space-2);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: var(--secondary-background-color);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: var(--secondary-background-color);
|
||||
padding: var(--ha-space-3);
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: var(--ha-space-3) 0;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-2);
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-adaptive-dialog": DemoHaAdaptiveDialog;
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,12 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
|
||||
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/demo-black-white-row";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||
@@ -169,7 +169,7 @@ const SCHEMAS: {
|
||||
{
|
||||
title: "Selectors",
|
||||
translations: {
|
||||
addon: "Addon",
|
||||
addon: "App",
|
||||
entity: "Entity",
|
||||
device: "Device",
|
||||
area: "Area",
|
||||
|
||||
@@ -11,11 +11,11 @@ import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
import type { LabelRegistryEntry } from "../../../../src/data/label_registry";
|
||||
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry";
|
||||
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
@@ -40,6 +40,9 @@ const ENTITIES = [
|
||||
getEntity("switch", "coffee", "off", {
|
||||
friendly_name: "Coffee",
|
||||
}),
|
||||
getEntity("number", "number", 5, {
|
||||
friendly_name: "Number",
|
||||
}),
|
||||
];
|
||||
|
||||
const DEVICES: DeviceRegistryEntry[] = [
|
||||
@@ -236,7 +239,7 @@ const SCHEMAS: {
|
||||
selector: { config_entry: {} },
|
||||
},
|
||||
duration: { name: "Duration", selector: { duration: {} } },
|
||||
addon: { name: "Addon", selector: { addon: {} } },
|
||||
addon: { name: "App", selector: { addon: {} } },
|
||||
number_box: {
|
||||
name: "Number Box",
|
||||
selector: {
|
||||
@@ -377,6 +380,33 @@ const SCHEMAS: {
|
||||
name: "Constant",
|
||||
selector: { constant: { value: true, label: "Yes!" } },
|
||||
},
|
||||
choose: {
|
||||
name: "Choose",
|
||||
selector: {
|
||||
choose: {
|
||||
choices: {
|
||||
number: {
|
||||
selector: {
|
||||
number: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 0.1,
|
||||
},
|
||||
},
|
||||
},
|
||||
entity: {
|
||||
selector: {
|
||||
entity: {
|
||||
filter: {
|
||||
domain: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -139,7 +139,7 @@ export class DemoHaWaDialog extends LitElement {
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>large</code></td>
|
||||
<td><code>min(720px, var(--full-width))</code></td>
|
||||
<td><code>min(1024px, var(--full-width))</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>full</code></td>
|
||||
|
||||
@@ -6,8 +6,8 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { IntegrationManifest } from "../../../../src/data/integration";
|
||||
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../../src/data/entity_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../../src/data/entity/entity_registry";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import "../../../../src/panels/config/integrations/ha-config-flow-card";
|
||||
import type {
|
||||
|
||||
@@ -83,10 +83,10 @@ export class HassioAddonRepositoryEl extends LitElement {
|
||||
? this.supervisor.localize(
|
||||
"common.new_version_available"
|
||||
)
|
||||
: this.supervisor.localize("addon.state.installed")
|
||||
: this.supervisor.localize("app.state.installed")
|
||||
: addon.available
|
||||
? this.supervisor.localize("addon.state.not_installed")
|
||||
: this.supervisor.localize("addon.state.not_available")}
|
||||
? this.supervisor.localize("app.state.not_installed")
|
||||
: this.supervisor.localize("app.state.not_available")}
|
||||
.iconClass=${addon.installed
|
||||
? addon.update_available
|
||||
? "update"
|
||||
|
||||
@@ -120,7 +120,7 @@ export class HassioAddonStore extends LitElement {
|
||||
? html`
|
||||
<div class="advanced">
|
||||
<a href="/profile" target="_top">
|
||||
${this.supervisor.localize("store.missing_addons")}
|
||||
${this.supervisor.localize("store.missing_apps")}
|
||||
</a>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -44,7 +44,7 @@ class HassioAddonAudio extends LitElement {
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.supervisor.localize("addon.configuration.audio.header")}
|
||||
.header=${this.supervisor.localize("app.configuration.audio.header")}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
@@ -52,9 +52,7 @@ class HassioAddonAudio extends LitElement {
|
||||
: nothing}
|
||||
${this._inputDevices &&
|
||||
html`<ha-select
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.configuration.audio.input"
|
||||
)}
|
||||
.label=${this.supervisor.localize("app.configuration.audio.input")}
|
||||
@selected=${this._setInputDevice}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
@@ -72,9 +70,7 @@ class HassioAddonAudio extends LitElement {
|
||||
</ha-select>`}
|
||||
${this._outputDevices &&
|
||||
html`<ha-select
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.configuration.audio.output"
|
||||
)}
|
||||
.label=${this.supervisor.localize("app.configuration.audio.output")}
|
||||
@selected=${this._setOutputDevice}
|
||||
@closed=${stopPropagation}
|
||||
fixedMenuPosition
|
||||
@@ -153,7 +149,7 @@ class HassioAddonAudio extends LitElement {
|
||||
|
||||
const noDevice: HassioHardwareAudioDevice = {
|
||||
device: "default",
|
||||
name: this.supervisor.localize("addon.configuration.audio.default"),
|
||||
name: this.supervisor.localize("app.configuration.audio.default"),
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -81,7 +81,7 @@ class HassioAddonConfigDashboard extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: this.supervisor.localize("addon.configuration.no_configuration")}
|
||||
: this.supervisor.localize("app.configuration.no_configuration")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ class HassioAddonConfig extends LitElement {
|
||||
<ha-card outlined>
|
||||
<div class="header">
|
||||
<h2>
|
||||
${this.supervisor.localize("addon.configuration.options.header")}
|
||||
${this.supervisor.localize("app.configuration.options.header")}
|
||||
</h2>
|
||||
<div class="card-menu">
|
||||
<ha-button-menu @action=${this._handleAction}>
|
||||
@@ -231,10 +231,10 @@ class HassioAddonConfig extends LitElement {
|
||||
<ha-list-item .disabled=${!this._canShowSchema || this.disabled}>
|
||||
${this._yamlMode
|
||||
? this.supervisor.localize(
|
||||
"addon.configuration.options.edit_in_ui"
|
||||
"app.configuration.options.edit_in_ui"
|
||||
)
|
||||
: this.supervisor.localize(
|
||||
"addon.configuration.options.edit_in_yaml"
|
||||
"app.configuration.options.edit_in_yaml"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
@@ -279,7 +279,7 @@ class HassioAddonConfig extends LitElement {
|
||||
: html`
|
||||
<ha-alert alert-type="error">
|
||||
${this.supervisor.localize(
|
||||
"addon.configuration.options.invalid_yaml"
|
||||
"app.configuration.options.invalid_yaml"
|
||||
)}
|
||||
</ha-alert>
|
||||
`}
|
||||
@@ -288,7 +288,7 @@ class HassioAddonConfig extends LitElement {
|
||||
? html`<ha-formfield
|
||||
class="show-additional"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.configuration.options.show_unused_optional"
|
||||
"app.configuration.options.show_unused_optional"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
@@ -397,7 +397,7 @@ class HassioAddonConfig extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_reset", {
|
||||
this._error = this.supervisor.localize("app.failed_to_reset", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -440,7 +440,7 @@ class HassioAddonConfig extends LitElement {
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
eventdata.success = false;
|
||||
|
||||
@@ -56,14 +56,12 @@ class HassioAddonNetwork extends LitElement {
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.supervisor.localize(
|
||||
"addon.configuration.network.header"
|
||||
)}
|
||||
.header=${this.supervisor.localize("app.configuration.network.header")}
|
||||
>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.supervisor.localize(
|
||||
"addon.configuration.network.introduction"
|
||||
"app.configuration.network.introduction"
|
||||
)}
|
||||
</p>
|
||||
${this._error
|
||||
@@ -87,7 +85,7 @@ class HassioAddonNetwork extends LitElement {
|
||||
? html`<ha-formfield
|
||||
class="show-optional"
|
||||
.label=${this.supervisor.localize(
|
||||
"addon.configuration.network.show_disabled"
|
||||
"app.configuration.network.show_disabled"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
@@ -187,7 +185,7 @@ class HassioAddonNetwork extends LitElement {
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_reset", {
|
||||
this._error = this.supervisor.localize("app.failed_to_reset", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
button.actionError();
|
||||
@@ -229,7 +227,7 @@ class HassioAddonNetwork extends LitElement {
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
button.actionError();
|
||||
|
||||
@@ -83,7 +83,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.documentation.get_documentation",
|
||||
"app.documentation.get_documentation",
|
||||
{ error: extractApiErrorMessage(err) }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
mdiCogs,
|
||||
mdiFileDocument,
|
||||
mdiInformationVariant,
|
||||
mdiMathLog,
|
||||
mdiTextBoxOutline,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
@@ -89,7 +89,7 @@ class HassioAddonDashboard extends LitElement {
|
||||
|
||||
const addonTabs: PageNavigation[] = [
|
||||
{
|
||||
translationKey: "addon.panel.info",
|
||||
translationKey: "app.panel.info",
|
||||
path: `/hassio/addon/${this.addon.slug}/info`,
|
||||
iconPath: mdiInformationVariant,
|
||||
},
|
||||
@@ -97,7 +97,7 @@ class HassioAddonDashboard extends LitElement {
|
||||
|
||||
if (this.addon.documentation) {
|
||||
addonTabs.push({
|
||||
translationKey: "addon.panel.documentation",
|
||||
translationKey: "app.panel.documentation",
|
||||
path: `/hassio/addon/${this.addon.slug}/documentation`,
|
||||
iconPath: mdiFileDocument,
|
||||
});
|
||||
@@ -106,14 +106,14 @@ class HassioAddonDashboard extends LitElement {
|
||||
if (this.addon.version) {
|
||||
addonTabs.push(
|
||||
{
|
||||
translationKey: "addon.panel.configuration",
|
||||
translationKey: "app.panel.configuration",
|
||||
path: `/hassio/addon/${this.addon.slug}/config`,
|
||||
iconPath: mdiCogs,
|
||||
},
|
||||
{
|
||||
translationKey: "addon.panel.log",
|
||||
translationKey: "app.panel.log",
|
||||
path: `/hassio/addon/${this.addon.slug}/logs`,
|
||||
iconPath: mdiMathLog,
|
||||
iconPath: mdiTextBoxOutline,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -195,10 +195,10 @@ class HassioAddonDashboard extends LitElement {
|
||||
) {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("my.add_addon_repository_title"),
|
||||
title: this.supervisor.localize("my.add_app_repository_title"),
|
||||
text: this.supervisor.localize(
|
||||
"my.add_addon_repository_description",
|
||||
{ addon: requestedAddon, repository: requestedAddonRepository }
|
||||
"my.add_app_repository_description",
|
||||
{ app: requestedAddon, repository: requestedAddonRepository }
|
||||
),
|
||||
confirmText: this.supervisor.localize("common.add"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
@@ -224,7 +224,7 @@ class HassioAddonDashboard extends LitElement {
|
||||
(addon) => addon.slug === requestedAddon
|
||||
);
|
||||
if (!validAddon) {
|
||||
this._error = this.supervisor.localize("my.error_addon_not_found");
|
||||
this._error = this.supervisor.localize("my.error_app_not_found");
|
||||
} else {
|
||||
navigate(`/hassio/addon/${requestedAddon}`, { replace: true });
|
||||
}
|
||||
|
||||
@@ -150,11 +150,11 @@ class HassioAddonInfo extends LitElement {
|
||||
: undefined;
|
||||
const metrics = [
|
||||
{
|
||||
description: this.supervisor.localize("addon.dashboard.cpu_usage"),
|
||||
description: this.supervisor.localize("app.dashboard.cpu_usage"),
|
||||
value: this._metrics?.cpu_percent,
|
||||
},
|
||||
{
|
||||
description: this.supervisor.localize("addon.dashboard.ram_usage"),
|
||||
description: this.supervisor.localize("app.dashboard.ram_usage"),
|
||||
value: this._metrics?.memory_percent,
|
||||
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
|
||||
this._metrics?.memory_limit
|
||||
@@ -181,11 +181,11 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.supervisor.localize(
|
||||
"addon.dashboard.protection_mode.title"
|
||||
"app.dashboard.protection_mode.title"
|
||||
)}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.protection_mode.content"
|
||||
"app.dashboard.protection_mode.content"
|
||||
)}
|
||||
<ha-button
|
||||
variant="danger"
|
||||
@@ -193,7 +193,7 @@ class HassioAddonInfo extends LitElement {
|
||||
@click=${this._protectionToggled}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.protection_mode.enable"
|
||||
"app.dashboard.protection_mode.enable"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-alert>
|
||||
@@ -220,7 +220,7 @@ class HassioAddonInfo extends LitElement {
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.title=${this.supervisor.localize(
|
||||
"dashboard.addon_running"
|
||||
"dashboard.app_running"
|
||||
)}
|
||||
class="running"
|
||||
.path=${mdiPlayCircle}
|
||||
@@ -229,7 +229,7 @@ class HassioAddonInfo extends LitElement {
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
.title=${this.supervisor.localize(
|
||||
"dashboard.addon_stopped"
|
||||
"dashboard.app_stopped"
|
||||
)}
|
||||
class="stopped"
|
||||
.path=${mdiCircleOffOutline}
|
||||
@@ -242,22 +242,19 @@ class HassioAddonInfo extends LitElement {
|
||||
<div class="description light-color">
|
||||
${this.addon.version
|
||||
? html`
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.current_version",
|
||||
{ version: this.addon.version }
|
||||
)}
|
||||
${this.supervisor.localize("app.dashboard.current_version", {
|
||||
version: this.addon.version,
|
||||
})}
|
||||
<div class="changelog" @click=${this._openChangelog}>
|
||||
(<span class="changelog-link"
|
||||
>${this.supervisor.localize(
|
||||
"addon.dashboard.changelog"
|
||||
"app.dashboard.changelog"
|
||||
)}</span
|
||||
>)
|
||||
</div>
|
||||
`
|
||||
: html`<span class="changelog-link" @click=${this._openChangelog}
|
||||
>${this.supervisor.localize(
|
||||
"addon.dashboard.changelog"
|
||||
)}</span
|
||||
>${this.supervisor.localize("app.dashboard.changelog")}</span
|
||||
>`}
|
||||
</div>
|
||||
|
||||
@@ -274,7 +271,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="stage"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
`addon.dashboard.capability.stages.${this.addon.stage}`
|
||||
`app.dashboard.capability.stages.${this.addon.stage}`
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -298,7 +295,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="rating"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.rating"
|
||||
"app.dashboard.capability.label.rating"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -313,7 +310,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="host_network"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.host"
|
||||
"app.dashboard.capability.label.host"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -329,7 +326,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="full_access"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.hardware"
|
||||
"app.dashboard.capability.label.hardware"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -345,7 +342,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="homeassistant_api"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.core"
|
||||
"app.dashboard.capability.label.core"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -364,7 +361,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="hassio_api"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
`addon.dashboard.capability.role.${this.addon.hassio_role}`
|
||||
`app.dashboard.capability.role.${this.addon.hassio_role}`
|
||||
) || this.addon.hassio_role
|
||||
)}
|
||||
>
|
||||
@@ -383,7 +380,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="docker_api"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.docker"
|
||||
"app.dashboard.capability.label.docker"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -399,7 +396,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="host_pid"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.host_pid"
|
||||
"app.dashboard.capability.label.host_pid"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -416,7 +413,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="apparmor"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.apparmor"
|
||||
"app.dashboard.capability.label.apparmor"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -432,7 +429,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="auth_api"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.auth"
|
||||
"app.dashboard.capability.label.auth"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -448,7 +445,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="ingress"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.ingress"
|
||||
"app.dashboard.capability.label.ingress"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -467,7 +464,7 @@ class HassioAddonInfo extends LitElement {
|
||||
id="signed"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize(
|
||||
"addon.dashboard.capability.label.signed"
|
||||
"app.dashboard.capability.label.signed"
|
||||
)
|
||||
)}
|
||||
>
|
||||
@@ -482,7 +479,7 @@ class HassioAddonInfo extends LitElement {
|
||||
@click=${this._showSystemManagedDialog}
|
||||
id="system_managed"
|
||||
.label=${capitalizeFirstLetter(
|
||||
this.supervisor.localize("addon.system_managed.badge")
|
||||
this.supervisor.localize("app.system_managed.badge")
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
@@ -496,7 +493,7 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
<div class="description light-color">
|
||||
${this.addon.description}.<br />
|
||||
${this.supervisor.localize("addon.dashboard.visit_addon_page", {
|
||||
${this.supervisor.localize("app.dashboard.visit_app_page", {
|
||||
name: html`<a
|
||||
href=${this.addon.url!}
|
||||
target="_blank"
|
||||
@@ -527,12 +524,12 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.boot.title"
|
||||
"app.dashboard.option.boot.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.boot.description"
|
||||
"app.dashboard.option.boot.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@@ -548,12 +545,12 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.watchdog.title"
|
||||
"app.dashboard.option.watchdog.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.watchdog.description"
|
||||
"app.dashboard.option.watchdog.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@@ -572,12 +569,12 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.auto_update.title"
|
||||
"app.dashboard.option.auto_update.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.auto_update.description"
|
||||
"app.dashboard.option.auto_update.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@@ -595,12 +592,12 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.ingress_panel.title"
|
||||
"app.dashboard.option.ingress_panel.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.ingress_panel.description"
|
||||
"app.dashboard.option.ingress_panel.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@@ -618,12 +615,12 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.protected.title"
|
||||
"app.dashboard.option.protected.title"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.option.protected.description"
|
||||
"app.dashboard.option.protected.description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@@ -644,7 +641,7 @@ class HassioAddonInfo extends LitElement {
|
||||
${this.addon.version && this.addon.state === "started"
|
||||
? html`<ha-settings-row ?three-line=${this.narrow}>
|
||||
<span slot="heading">
|
||||
${this.supervisor.localize("addon.dashboard.hostname")}
|
||||
${this.supervisor.localize("app.dashboard.hostname")}
|
||||
</span>
|
||||
<code slot="description"> ${this.addon.hostname} </code>
|
||||
</ha-settings-row>
|
||||
@@ -671,14 +668,14 @@ class HassioAddonInfo extends LitElement {
|
||||
? html`
|
||||
<ha-alert alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.not_available_arch"
|
||||
"app.dashboard.not_available_arch"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: html`
|
||||
<ha-alert alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.not_available_version",
|
||||
"app.dashboard.not_available_version",
|
||||
{
|
||||
core_version_installed: this.supervisor.core.version,
|
||||
core_version_needed: addonStoreInfo!.homeassistant,
|
||||
@@ -699,14 +696,14 @@ class HassioAddonInfo extends LitElement {
|
||||
@click=${this._stopClicked}
|
||||
.disabled=${systemManaged && !this.controlEnabled}
|
||||
>
|
||||
${this.supervisor.localize("addon.dashboard.stop")}
|
||||
${this.supervisor.localize("app.dashboard.stop")}
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
@click=${this._restartClicked}
|
||||
>
|
||||
${this.supervisor.localize("addon.dashboard.restart")}
|
||||
${this.supervisor.localize("app.dashboard.restart")}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: html`
|
||||
@@ -715,7 +712,7 @@ class HassioAddonInfo extends LitElement {
|
||||
.progress=${this.addon.state === "startup"}
|
||||
appearance="plain"
|
||||
>
|
||||
${this.supervisor.localize("addon.dashboard.start")}
|
||||
${this.supervisor.localize("app.dashboard.start")}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: nothing}
|
||||
@@ -729,7 +726,7 @@ class HassioAddonInfo extends LitElement {
|
||||
@click=${this._uninstallClicked}
|
||||
.disabled=${systemManaged && !this.controlEnabled}
|
||||
>
|
||||
${this.supervisor.localize("addon.dashboard.uninstall")}
|
||||
${this.supervisor.localize("app.dashboard.uninstall")}
|
||||
</ha-progress-button>
|
||||
${this.addon.build
|
||||
? html`
|
||||
@@ -738,7 +735,7 @@ class HassioAddonInfo extends LitElement {
|
||||
appearance="plain"
|
||||
@click=${this._rebuildClicked}
|
||||
>
|
||||
${this.supervisor.localize("addon.dashboard.rebuild")}
|
||||
${this.supervisor.localize("app.dashboard.rebuild")}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: nothing}
|
||||
@@ -761,7 +758,7 @@ class HassioAddonInfo extends LitElement {
|
||||
: undefined}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.open_web_ui"
|
||||
"app.dashboard.open_web_ui"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
@@ -772,7 +769,7 @@ class HassioAddonInfo extends LitElement {
|
||||
.disabled=${!this.addon.available}
|
||||
@click=${this._installClicked}
|
||||
>
|
||||
${this.supervisor.localize("addon.dashboard.install")}
|
||||
${this.supervisor.localize("app.dashboard.install")}
|
||||
</ha-progress-button>
|
||||
`}
|
||||
</div>
|
||||
@@ -804,7 +801,7 @@ class HassioAddonInfo extends LitElement {
|
||||
"state" in this.addon &&
|
||||
this.addon.state === "startup"
|
||||
) {
|
||||
// Addon is starting up, wait for it to start
|
||||
// App is starting up, wait for it to start
|
||||
this._scheduleDataUpdate();
|
||||
}
|
||||
}
|
||||
@@ -858,11 +855,11 @@ class HassioAddonInfo extends LitElement {
|
||||
private _showMoreInfo(ev): void {
|
||||
const id = ev.currentTarget.id as AddonCapability;
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: this.supervisor.localize(`addon.dashboard.capability.${id}.title`),
|
||||
title: this.supervisor.localize(`app.dashboard.capability.${id}.title`),
|
||||
content:
|
||||
id === "stage"
|
||||
? this.supervisor.localize(
|
||||
`addon.dashboard.capability.${id}.description`,
|
||||
`app.dashboard.capability.${id}.description`,
|
||||
{
|
||||
icon_stable: `<ha-svg-icon path="${STAGE_ICON.stable}"></ha-svg-icon>`,
|
||||
icon_experimental: `<ha-svg-icon path="${STAGE_ICON.experimental}"></ha-svg-icon>`,
|
||||
@@ -870,7 +867,7 @@ class HassioAddonInfo extends LitElement {
|
||||
}
|
||||
)
|
||||
: this.supervisor.localize(
|
||||
`addon.dashboard.capability.${id}.description`
|
||||
`app.dashboard.capability.${id}.description`
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -936,7 +933,7 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -956,7 +953,7 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -976,7 +973,7 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -996,7 +993,7 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1016,7 +1013,7 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
this._error = this.supervisor.localize("app.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1030,13 +1027,13 @@ class HassioAddonInfo extends LitElement {
|
||||
);
|
||||
|
||||
showHassioMarkdownDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.changelog"),
|
||||
title: this.supervisor.localize("app.dashboard.changelog"),
|
||||
content: extractChangelog(this.addon as HassioAddonDetails, content),
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"addon.dashboard.action_error.get_changelog"
|
||||
"app.dashboard.action_error.get_changelog"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
@@ -1066,7 +1063,7 @@ class HassioAddonInfo extends LitElement {
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.action_error.install"),
|
||||
title: this.supervisor.localize("app.dashboard.action_error.install"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1091,7 +1088,7 @@ class HassioAddonInfo extends LitElement {
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.action_error.stop"),
|
||||
title: this.supervisor.localize("app.dashboard.action_error.stop"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1112,7 +1109,7 @@ class HassioAddonInfo extends LitElement {
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.action_error.restart"),
|
||||
title: this.supervisor.localize("app.dashboard.action_error.restart"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1127,7 +1124,7 @@ class HassioAddonInfo extends LitElement {
|
||||
await rebuildLocalAddon(this.hass, this.addon.slug);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.action_error.rebuild"),
|
||||
title: this.supervisor.localize("app.dashboard.action_error.rebuild"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
@@ -1145,12 +1142,12 @@ class HassioAddonInfo extends LitElement {
|
||||
if (!validate.valid) {
|
||||
await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"addon.dashboard.action_error.start_invalid_config"
|
||||
"app.dashboard.action_error.start_invalid_config"
|
||||
),
|
||||
text: validate.message.split(" Got ")[0],
|
||||
confirm: () => this._openConfiguration(),
|
||||
confirmText: this.supervisor.localize(
|
||||
"addon.dashboard.action_error.go_to_config"
|
||||
"app.dashboard.action_error.go_to_config"
|
||||
),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
@@ -1162,7 +1159,7 @@ class HassioAddonInfo extends LitElement {
|
||||
button.actionError();
|
||||
button.progress = false;
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to validate addon configuration",
|
||||
title: "Failed to validate app configuration",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
@@ -1181,7 +1178,7 @@ class HassioAddonInfo extends LitElement {
|
||||
button.actionError();
|
||||
button.progress = false;
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("addon.dashboard.action_error.start"),
|
||||
title: this.supervisor.localize("app.dashboard.action_error.start"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
return;
|
||||
@@ -1207,13 +1204,13 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("dialog.uninstall_addon.title", {
|
||||
title: this.supervisor.localize("dialog.uninstall_app.title", {
|
||||
name: this.addon.name,
|
||||
}),
|
||||
text: html`
|
||||
<ha-formfield
|
||||
.label=${html`<p>
|
||||
${this.supervisor.localize("dialog.uninstall_addon.remove_data")}
|
||||
${this.supervisor.localize("dialog.uninstall_app.remove_data")}
|
||||
</p>`}
|
||||
>
|
||||
<ha-switch
|
||||
@@ -1223,7 +1220,7 @@ class HassioAddonInfo extends LitElement {
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
`,
|
||||
confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"),
|
||||
confirmText: this.supervisor.localize("dialog.uninstall_app.uninstall"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
@@ -1245,9 +1242,7 @@ class HassioAddonInfo extends LitElement {
|
||||
button.actionSuccess();
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize(
|
||||
"addon.dashboard.action_error.uninstall"
|
||||
),
|
||||
title: this.supervisor.localize("app.dashboard.action_error.uninstall"),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
button.actionError();
|
||||
|
||||
@@ -19,14 +19,14 @@ class HassioAddonSystemManaged extends LitElement {
|
||||
return html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.supervisor.localize("addon.system_managed.title")}
|
||||
.title=${this.supervisor.localize("app.system_managed.title")}
|
||||
.narrow=${this.narrow}
|
||||
>
|
||||
${this.supervisor.localize("addon.system_managed.description")}
|
||||
${this.supervisor.localize("app.system_managed.description")}
|
||||
${!this.hideButton
|
||||
? html`
|
||||
<ha-button slot="action" @click=${this._takeControl}>
|
||||
${this.supervisor.localize("addon.system_managed.take_control")}
|
||||
${this.supervisor.localize("app.system_managed.take_control")}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
@@ -216,7 +216,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${html`<supervisor-formfield-label
|
||||
.label=${this.supervisor?.localize("backup.addons")}
|
||||
.label=${this.supervisor?.localize("backup.apps")}
|
||||
.iconPath=${mdiPuzzle}
|
||||
>
|
||||
</supervisor-formfield-label>`}
|
||||
|
||||
@@ -33,13 +33,13 @@ class HassioAddons extends LitElement {
|
||||
suffix
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.supervisor.localize("dashboard.search_addons")}
|
||||
.label=${this.supervisor.localize("dashboard.search_apps")}
|
||||
>
|
||||
</search-input>
|
||||
</div>
|
||||
<div class="content">
|
||||
${!atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? html`<h1>${this.supervisor.localize("dashboard.addons")}</h1>`
|
||||
? html`<h1>${this.supervisor.localize("dashboard.apps")}</h1>`
|
||||
: ""}
|
||||
<div class="card-group">
|
||||
${!this.supervisor.addon.addons.length
|
||||
@@ -47,7 +47,7 @@ class HassioAddons extends LitElement {
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<button class="link" @click=${this._openStore}>
|
||||
${this.supervisor.localize("dashboard.no_addons")}
|
||||
${this.supervisor.localize("dashboard.no_apps")}
|
||||
</button>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -67,14 +67,12 @@ class HassioAddons extends LitElement {
|
||||
? mdiArrowUpBoldCircle
|
||||
: mdiPuzzle}
|
||||
.iconTitle=${addon.state !== "started"
|
||||
? this.supervisor.localize("dashboard.addon_stopped")
|
||||
? this.supervisor.localize("dashboard.app_stopped")
|
||||
: addon.update_available!
|
||||
? this.supervisor.localize(
|
||||
"dashboard.addon_new_version"
|
||||
"dashboard.app_new_version"
|
||||
)
|
||||
: this.supervisor.localize(
|
||||
"dashboard.addon_running"
|
||||
)}
|
||||
: this.supervisor.localize("dashboard.app_running")}
|
||||
.iconClass=${addon.update_available
|
||||
? addon.state === "started"
|
||||
? "update"
|
||||
|
||||
@@ -39,7 +39,7 @@ class HassioDashboard extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config"
|
||||
.header=${this.supervisor.localize("panel.addons")}
|
||||
.header=${this.supervisor.localize("panel.apps")}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
@@ -81,7 +81,7 @@ class HassioDashboard extends LitElement {
|
||||
<span slot="header">
|
||||
${this.supervisor.localize(
|
||||
atLeastVersion(this.hass.config.version, 2021, 12)
|
||||
? "panel.addons"
|
||||
? "panel.apps"
|
||||
: "panel.dashboard"
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -88,8 +88,8 @@ class HassioRegistriesDialog extends LitElement {
|
||||
<ha-button
|
||||
?disabled=${Boolean(
|
||||
!this._input.registry ||
|
||||
!this._input.username ||
|
||||
!this._input.password
|
||||
!this._input.username ||
|
||||
!this._input.password
|
||||
)}
|
||||
@click=${this._addNewRegistry}
|
||||
appearance="filled"
|
||||
|
||||
@@ -64,9 +64,9 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
repos
|
||||
.filter(
|
||||
(repo) =>
|
||||
repo.slug !== "core" && // The core add-ons repository
|
||||
repo.slug !== "local" && // Locally managed add-ons
|
||||
repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons
|
||||
repo.slug !== "core" && // The core apps repository
|
||||
repo.slug !== "local" && // Locally managed apps
|
||||
repo.slug !== "a0d7b954" && // Home Assistant Community Apps
|
||||
repo.slug !== "5c53de3b" && // The ESPHome repository
|
||||
repo.slug !== "d5369777" // Music Assistant repository
|
||||
)
|
||||
|
||||
@@ -16,11 +16,11 @@ export const suggestAddonRestart = async (
|
||||
addon: HassioAddonDetails
|
||||
): Promise<void> => {
|
||||
const confirmed = await showConfirmationDialog(element, {
|
||||
title: supervisor.localize("dialog.restart_addon.title", {
|
||||
title: supervisor.localize("dialog.restart_app.title", {
|
||||
name: addon.name,
|
||||
}),
|
||||
text: supervisor.localize("dialog.restart_addon.text"),
|
||||
confirmText: supervisor.localize("dialog.restart_addon.restart"),
|
||||
text: supervisor.localize("dialog.restart_app.text"),
|
||||
confirmText: supervisor.localize("dialog.restart_app.restart"),
|
||||
dismissText: supervisor.localize("common.cancel"),
|
||||
});
|
||||
if (confirmed) {
|
||||
|
||||
@@ -89,14 +89,12 @@ class HassioSystemManagedDialog extends LitElement {
|
||||
? html`<img src=${addonImage} alt=${this._addon.name} />`
|
||||
: html`<ha-svg-icon .path=${mdiPuzzle}></ha-svg-icon>`}
|
||||
</div>
|
||||
${this._supervisor.localize("addon.system_managed.title")}.<br />
|
||||
${this._supervisor.localize("addon.system_managed.description")}
|
||||
${this._supervisor.localize("app.system_managed.title")}.<br />
|
||||
${this._supervisor.localize("app.system_managed.description")}
|
||||
${this._configEntry
|
||||
? html`
|
||||
<h3>
|
||||
${this._supervisor.localize(
|
||||
"addon.system_managed.managed_by"
|
||||
)}:
|
||||
${this._supervisor.localize("app.system_managed.managed_by")}:
|
||||
</h3>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item
|
||||
|
||||
@@ -14,7 +14,7 @@ export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] =>
|
||||
: [
|
||||
{
|
||||
translationKey: atLeastVersion(hass.config.version, 2021, 12)
|
||||
? "panel.addons"
|
||||
? "panel.apps"
|
||||
: "panel.dashboard",
|
||||
path: `/hassio/dashboard`,
|
||||
iconPath: atLeastVersion(hass.config.version, 2021, 12)
|
||||
|
||||
@@ -114,14 +114,14 @@ class HassioIngressView extends LitElement {
|
||||
}
|
||||
if (!addonInfo.version) {
|
||||
await showAlertDialog(this, {
|
||||
text: this.supervisor.localize("my.error_addon_not_installed"),
|
||||
text: this.supervisor.localize("my.error_app_not_installed"),
|
||||
title: addonInfo.name,
|
||||
});
|
||||
await nextRender();
|
||||
navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true });
|
||||
} else if (!addonInfo.ingress) {
|
||||
await showAlertDialog(this, {
|
||||
text: this.supervisor.localize("my.error_addon_no_ingress"),
|
||||
text: this.supervisor.localize("my.error_app_no_ingress"),
|
||||
title: addonInfo.name,
|
||||
});
|
||||
await nextRender();
|
||||
@@ -162,8 +162,8 @@ class HassioIngressView extends LitElement {
|
||||
await this.updateComplete;
|
||||
await showAlertDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_addon_info") ||
|
||||
"Unable to fetch add-on info to start Ingress",
|
||||
this.supervisor.localize("ingress.error_app_info") ||
|
||||
"Unable to fetch app info to start Ingress",
|
||||
title: "Supervisor",
|
||||
});
|
||||
await nextRender();
|
||||
@@ -175,8 +175,8 @@ class HassioIngressView extends LitElement {
|
||||
await this.updateComplete;
|
||||
await showAlertDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_addon_not_installed") ||
|
||||
"The add-on is not installed. Please install it first",
|
||||
this.supervisor.localize("ingress.error_app_not_installed") ||
|
||||
"The app is not installed. Please install it first",
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
@@ -188,8 +188,8 @@ class HassioIngressView extends LitElement {
|
||||
await this.updateComplete;
|
||||
await showAlertDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_addon_not_supported") ||
|
||||
"This add-on does not support Ingress",
|
||||
this.supervisor.localize("ingress.error_app_not_supported") ||
|
||||
"This app does not support Ingress",
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
@@ -201,18 +201,18 @@ class HassioIngressView extends LitElement {
|
||||
await this.updateComplete;
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_addon_not_running") ||
|
||||
"The add-on is not running. Do you want to start it now?",
|
||||
this.supervisor.localize("ingress.error_app_not_running") ||
|
||||
"The app is not running. Do you want to start it now?",
|
||||
title: addon.name,
|
||||
confirmText:
|
||||
this.supervisor.localize("ingress.start_addon") || "Start add-on",
|
||||
this.supervisor.localize("ingress.start_app") || "Start app",
|
||||
dismissText: this.supervisor.localize("common.no") || "No",
|
||||
});
|
||||
if (confirm) {
|
||||
try {
|
||||
this._loadingMessage =
|
||||
this.supervisor.localize("ingress.addon_starting") ||
|
||||
"The add-on is starting, this can take some time...";
|
||||
this.supervisor.localize("ingress.app_starting") ||
|
||||
"The app is starting, this can take some time...";
|
||||
await startHassioAddon(this.hass, addonSlug);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "addon",
|
||||
@@ -222,8 +222,8 @@ class HassioIngressView extends LitElement {
|
||||
} catch (_err) {
|
||||
await showAlertDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_starting_addon") ||
|
||||
"Error starting the add-on",
|
||||
this.supervisor.localize("ingress.error_starting_app") ||
|
||||
"Error starting the app",
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
@@ -238,10 +238,10 @@ class HassioIngressView extends LitElement {
|
||||
}
|
||||
|
||||
if (addon.state === "startup") {
|
||||
// Addon is starting up, wait for it to start
|
||||
// App is starting up, wait for it to start
|
||||
this._loadingMessage =
|
||||
this.supervisor.localize("ingress.addon_starting") ||
|
||||
"The add-on is starting, this can take some time...";
|
||||
this.supervisor.localize("ingress.app_starting") ||
|
||||
"The app is starting, this can take some time...";
|
||||
|
||||
this._fetchDataTimeout = window.setTimeout(() => {
|
||||
this._fetchData(addonSlug);
|
||||
@@ -301,8 +301,8 @@ class HassioIngressView extends LitElement {
|
||||
await this.updateComplete;
|
||||
showConfirmationDialog(this, {
|
||||
text:
|
||||
this.supervisor.localize("ingress.error_addon_not_ready") ||
|
||||
"The add-on seems to not be ready, it might still be starting. Do you want to try again?",
|
||||
this.supervisor.localize("ingress.error_app_not_ready") ||
|
||||
"The app seems to not be ready, it might still be starting. Do you want to try again?",
|
||||
title: this._addon.name,
|
||||
confirmText: this.supervisor.localize("ingress.retry") || "Retry",
|
||||
dismissText: this.supervisor.localize("common.no") || "No",
|
||||
|
||||
@@ -261,16 +261,16 @@ class UpdateAvailableCard extends LitElement {
|
||||
private _computeCreateBackupTexts():
|
||||
| { title: string; description?: string }
|
||||
| undefined {
|
||||
// Addon backup
|
||||
// App backup
|
||||
if (
|
||||
this._updateType === "addon" &&
|
||||
atLeastVersion(this.hass.config.version, 2025, 2, 0)
|
||||
) {
|
||||
const version = this._version;
|
||||
return {
|
||||
title: this.supervisor.localize("update_available.create_backup.addon"),
|
||||
title: this.supervisor.localize("update_available.create_backup.app"),
|
||||
description: this.supervisor.localize(
|
||||
"update_available.create_backup.addon_description",
|
||||
"update_available.create_backup.app_description",
|
||||
{ version: version }
|
||||
),
|
||||
};
|
||||
@@ -363,11 +363,11 @@ class UpdateAvailableCard extends LitElement {
|
||||
)
|
||||
) {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.dashboard.not_available_arch"
|
||||
"app.dashboard.not_available_arch"
|
||||
);
|
||||
} else {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.dashboard.not_available_version",
|
||||
"app.dashboard.not_available_version",
|
||||
{
|
||||
core_version_installed: this.supervisor.core.version,
|
||||
core_version_needed: addonStoreInfo.homeassistant,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { type CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
+57
-59
@@ -26,39 +26,39 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.4",
|
||||
"@babel/runtime": "7.28.6",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.0",
|
||||
"@codemirror/language": "6.11.3",
|
||||
"@codemirror/commands": "6.10.1",
|
||||
"@codemirror/language": "6.12.1",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.5.11",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.38.8",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.5.4",
|
||||
"@codemirror/view": "6.39.11",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.18.2",
|
||||
"@formatjs/intl-displaynames": "6.8.13",
|
||||
"@formatjs/intl-durationformat": "0.7.6",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.6",
|
||||
"@formatjs/intl-listformat": "7.7.13",
|
||||
"@formatjs/intl-locale": "4.2.13",
|
||||
"@formatjs/intl-numberformat": "8.15.6",
|
||||
"@formatjs/intl-pluralrules": "5.4.6",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.13",
|
||||
"@fullcalendar/core": "6.1.19",
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
"@fullcalendar/interaction": "6.1.19",
|
||||
"@fullcalendar/list": "6.1.19",
|
||||
"@fullcalendar/luxon3": "6.1.19",
|
||||
"@fullcalendar/timegrid": "6.1.19",
|
||||
"@home-assistant/webawesome": "3.0.0-ha.0",
|
||||
"@formatjs/intl-datetimeformat": "7.2.0",
|
||||
"@formatjs/intl-displaynames": "7.2.0",
|
||||
"@formatjs/intl-durationformat": "0.10.0",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.0",
|
||||
"@formatjs/intl-listformat": "8.2.0",
|
||||
"@formatjs/intl-locale": "5.2.0",
|
||||
"@formatjs/intl-numberformat": "9.2.0",
|
||||
"@formatjs/intl-pluralrules": "6.2.0",
|
||||
"@formatjs/intl-relativetimeformat": "12.2.0",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.0.0-ha.2",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.0.9",
|
||||
"@lit-labs/observers": "2.0.6",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
"@lit-labs/virtualizer": "2.1.1",
|
||||
"@lit/context": "1.1.6",
|
||||
"@lit/reactive-element": "2.1.1",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-base": "0.27.0",
|
||||
@@ -85,12 +85,10 @@
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.17",
|
||||
"@swc/helpers": "0.5.18",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vaadin/combo-box": "24.9.5",
|
||||
"@vaadin/vaadin-themable-mixin": "24.9.5",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
@@ -114,13 +112,13 @@
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.18",
|
||||
"intl-messageformat": "11.1.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.1",
|
||||
"memoize-one": "6.0.0",
|
||||
@@ -135,7 +133,7 @@
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"ua-parser-js": "2.0.6",
|
||||
"ua-parser-js": "2.0.8",
|
||||
"vue": "2.7.16",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
@@ -148,20 +146,20 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.5",
|
||||
"@babel/core": "7.28.6",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.5",
|
||||
"@babel/plugin-transform-runtime": "7.28.5",
|
||||
"@babel/preset-env": "7.28.5",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.6",
|
||||
"@lokalise/node-api": "15.4.0",
|
||||
"@babel/preset-env": "7.28.6",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.8",
|
||||
"@lokalise/node-api": "15.6.0",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.3.11",
|
||||
"@rspack/core": "1.6.4",
|
||||
"@rspack/dev-server": "1.1.4",
|
||||
"@rsdoctor/rspack-plugin": "1.5.0",
|
||||
"@rspack/core": "1.7.2",
|
||||
"@rspack/dev-server": "1.1.5",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.22",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/culori": "4.0.1",
|
||||
@@ -178,12 +176,12 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.0.13",
|
||||
"@vitest/coverage-v8": "4.0.17",
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.1",
|
||||
"eslint": "9.39.1",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
@@ -193,15 +191,15 @@
|
||||
"eslint-plugin-unused-imports": "4.3.0",
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.2",
|
||||
"glob": "12.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"glob": "13.0.0",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "27.2.0",
|
||||
"jsdom": "27.4.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
@@ -209,35 +207,35 @@
|
||||
"lodash.template": "4.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.6.2",
|
||||
"rspack-manifest-plugin": "5.2.0",
|
||||
"prettier": "3.8.0",
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.0",
|
||||
"tar": "7.5.2",
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"sinon": "21.0.1",
|
||||
"tar": "7.5.3",
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.47.0",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "4.0.13",
|
||||
"typescript-eslint": "8.53.0",
|
||||
"vite-tsconfig-paths": "6.0.4",
|
||||
"vitest": "4.0.17",
|
||||
"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"
|
||||
},
|
||||
"resolutions": {
|
||||
"@material/mwc-button@^0.25.3": "^0.27.0",
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.1",
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
"globals": "16.5.0",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"globals": "17.0.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",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
},
|
||||
"packageManager": "[email protected]",
|
||||
"volta": {
|
||||
"node": "22.21.1"
|
||||
"node": "24.13.0"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20251203.0"
|
||||
version = "20251229.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
+1
-3
@@ -1,5 +1,4 @@
|
||||
#!/bin/sh
|
||||
# Pushes a new version to PyPi.
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
@@ -12,5 +11,4 @@ yarn install
|
||||
script/build_frontend
|
||||
|
||||
rm -rf dist home_assistant_frontend.egg-info
|
||||
python3 -m build
|
||||
python3 -m twine upload dist/*.whl --skip-existing
|
||||
python3 -m build -q
|
||||
|
||||
@@ -38,13 +38,11 @@ export class HaAuthFormString extends HaFormString {
|
||||
}
|
||||
</style>
|
||||
<ha-auth-textfield
|
||||
.type=${
|
||||
!this.isPassword
|
||||
.type=${!this.isPassword
|
||||
? this.stringType
|
||||
: this.unmaskedPassword
|
||||
? "text"
|
||||
: "password"
|
||||
}
|
||||
: "password"}
|
||||
.label=${this.label}
|
||||
.value=${this.data || ""}
|
||||
.helper=${this.helper}
|
||||
@@ -55,18 +53,17 @@ export class HaAuthFormString extends HaFormString {
|
||||
.name=${this.schema.name}
|
||||
.autocomplete=${this.schema.autocomplete}
|
||||
?autofocus=${this.schema.autofocus}
|
||||
.suffix=${
|
||||
this.isPassword
|
||||
? // reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
: this.schema.description?.suffix
|
||||
}
|
||||
.validationMessage=${this.schema.required ? this.localize?.("ui.panel.page-authorize.form.error_required") : undefined}
|
||||
.suffix=${this.isPassword
|
||||
? // reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
: this.schema.description?.suffix}
|
||||
.validationMessage=${this.schema.required
|
||||
? this.localize?.("ui.panel.page-authorize.form.error_required")
|
||||
: undefined}
|
||||
@input=${this._valueChanged}
|
||||
@change=${this._valueChanged}
|
||||
></ha-auth-textfield>
|
||||
${this.renderIcon()}
|
||||
</ha-auth-textfield>
|
||||
></ha-auth-textfield>
|
||||
${this.renderIcon()}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
|
||||
export interface AreasFloorHierarchy {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export const THEME_COLORS = new Set([
|
||||
"primary",
|
||||
"accent",
|
||||
"disabled",
|
||||
"red",
|
||||
"pink",
|
||||
"purple",
|
||||
@@ -25,6 +24,9 @@ export const THEME_COLORS = new Set([
|
||||
"blue-grey",
|
||||
"black",
|
||||
"white",
|
||||
"primary-text",
|
||||
"secondary-text",
|
||||
"disabled",
|
||||
]);
|
||||
|
||||
export function computeCssColor(color: string): string {
|
||||
|
||||
@@ -79,7 +79,7 @@ export const generateColorPalette = (
|
||||
}
|
||||
|
||||
return steps.map((step) => {
|
||||
const name = `color-${label}-${step}`;
|
||||
const name = `ha-color-${label}-${step}`;
|
||||
|
||||
// Base color at 50%
|
||||
if (step === 50) {
|
||||
|
||||
@@ -93,8 +93,8 @@ export const calcDateRange = (
|
||||
];
|
||||
case "now-12m":
|
||||
return [
|
||||
calcDate(subMonths(today, 12), startOfMonth, hass.locale, hass.config),
|
||||
calcDate(subMonths(today, 1), endOfMonth, hass.locale, hass.config),
|
||||
calcDate(today, subMonths, hass.locale, hass.config, 12),
|
||||
calcDate(today, subMonths, hass.locale, hass.config, 0),
|
||||
];
|
||||
case "now-1h":
|
||||
return [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
|
||||
export const computeAreaName = (area: AreaRegistryEntry): string | undefined =>
|
||||
area.name?.trim();
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
DOMAIN_ATTRIBUTES_FORMATERS,
|
||||
DOMAIN_ATTRIBUTES_UNITS,
|
||||
TEMPERATURE_ATTRIBUTES,
|
||||
} from "../../data/entity_attributes";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
} from "../../data/entity/entity_attributes";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { WeatherEntity } from "../../data/weather";
|
||||
import { getWeatherUnit } from "../../data/weather";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "../../data/entity_registry";
|
||||
} from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
import { getDuplicates } from "../string/get_duplicates";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
|
||||
export const computeDeviceName = (
|
||||
device: DeviceRegistryEntry
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "../../data/entity_registry";
|
||||
} from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeDeviceName } from "./compute_device_name";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { DURATION_UNITS, formatDuration } from "../datetime/format_duration";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { DURATION_UNITS, formatDuration } from "../datetime/format_duration";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
import {
|
||||
formatNumber,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
ExtEntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { IntegrationManifest } from "../../data/integration";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { HELPERS_CRUD } from "../../data/helpers_crud";
|
||||
import type { Helper } from "../../panels/config/helpers/const";
|
||||
import { isHelperDomain } from "../../panels/config/helpers/const";
|
||||
import { isComponentLoaded } from "../config/is_component_loaded";
|
||||
import type { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { removeEntityRegistryEntry } from "../../data/entity_registry";
|
||||
import type { ConfigEntry } from "../../data/config_entries";
|
||||
import { deleteConfigEntry } from "../../data/config_entries";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import { removeEntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import { HELPERS_CRUD } from "../../data/helpers_crud";
|
||||
import type { IntegrationManifest } from "../../data/integration";
|
||||
import type { Helper } from "../../panels/config/helpers/const";
|
||||
import { isHelperDomain } from "../../panels/config/helpers/const";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { isComponentLoaded } from "../config/is_component_loaded";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
|
||||
export const isDeletableEntity = (
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface EntityFilter {
|
||||
label?: string | string[];
|
||||
entity_category?: EntityCategory | EntityCategory[];
|
||||
hidden_platform?: string | string[];
|
||||
hidden_domains?: string | string[];
|
||||
}
|
||||
|
||||
export type EntityFilterFunc = (entityId: string) => boolean;
|
||||
@@ -38,6 +39,9 @@ export const generateEntityFilter = (
|
||||
const domains = filter.domain
|
||||
? new Set(ensureArray(filter.domain))
|
||||
: undefined;
|
||||
const hiddenDomains = filter.hidden_domains
|
||||
? new Set(ensureArray(filter.hidden_domains))
|
||||
: undefined;
|
||||
const deviceClasses = filter.device_class
|
||||
? new Set(ensureArray(filter.device_class))
|
||||
: undefined;
|
||||
@@ -57,12 +61,16 @@ export const generateEntityFilter = (
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
if (domains) {
|
||||
if (domains || hiddenDomains) {
|
||||
const domain = computeDomain(entityId);
|
||||
if (!domains.has(domain)) {
|
||||
if (domains && !domains.has(domain)) {
|
||||
return false;
|
||||
}
|
||||
if (hiddenDomains && hiddenDomains.has(domain)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceClasses) {
|
||||
const dc = stateObj.attributes.device_class || "none";
|
||||
if (!deviceClasses.has(dc)) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { UNAVAILABLE_STATES } from "../../data/entity";
|
||||
import { UNAVAILABLE_STATES } from "../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { stringCompare } from "../string/compare";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
|
||||
export const FIXED_DOMAIN_STATES = {
|
||||
alarm_control_panel: [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../data/entity";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
|
||||
export const computeGroupEntitiesState = (states: HassEntity[]): string => {
|
||||
if (!states.length) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity";
|
||||
import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity/entity";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
|
||||
export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE } from "../../data/entity";
|
||||
import { UNAVAILABLE } from "../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { DOMAINS_WITH_CARD } from "../const";
|
||||
import { canToggleState } from "./can_toggle_state";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Return a color representing a state. */
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE } from "../../data/entity";
|
||||
import { UNAVAILABLE } from "../../data/entity/entity";
|
||||
import type { GroupEntity } from "../../data/group";
|
||||
import { computeGroupDomain } from "../../data/group";
|
||||
import { computeCssVariable } from "../../resources/css-variables";
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* ES5-compatible implementation of the keyed directive.
|
||||
* Based on lit-html's keyed directive but written to avoid ES5 minification issues.
|
||||
*
|
||||
* This implementation avoids parameter destructuring in the update() method,
|
||||
* which causes Terser with ecma: 5 to generate invalid references like `_k`.
|
||||
*
|
||||
* Used only for ES5 builds (legacy browsers). Modern builds use the original
|
||||
* lit-html keyed directive.
|
||||
*
|
||||
* @see https://github.com/home-assistant/frontend/issues/28732
|
||||
*/
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { directive, Directive } from "lit-html/directive.js";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { setCommittedValue } from "lit-html/directive-helpers.js";
|
||||
// eslint-disable-next-line lit/no-legacy-imports
|
||||
import { nothing } from "lit-html";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import type { Part } from "lit-html/directive.js";
|
||||
|
||||
class KeyedES5 extends Directive {
|
||||
private _key: unknown = nothing;
|
||||
|
||||
render(k: unknown, v: unknown) {
|
||||
this._key = k;
|
||||
return v;
|
||||
}
|
||||
|
||||
update(part: unknown, args: [unknown, unknown]) {
|
||||
const k = args[0];
|
||||
const v = args[1];
|
||||
if (k !== this._key) {
|
||||
// Clear the part before returning a value. The one-arg form of
|
||||
// setCommittedValue sets the value to a sentinel which forces a
|
||||
// commit the next render.
|
||||
setCommittedValue(part as Part);
|
||||
this._key = k;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates a renderable value with a unique key. When the key changes, the
|
||||
* previous DOM is removed and disposed before rendering the next value, even
|
||||
* if the value - such as a template - is the same.
|
||||
*
|
||||
* This is useful for forcing re-renders of stateful components, or working
|
||||
* with code that expects new data to generate new HTML elements, such as some
|
||||
* animation techniques.
|
||||
*/
|
||||
export const keyed = directive(KeyedES5);
|
||||
+42
-25
@@ -17,32 +17,45 @@ export interface NavigateOptions {
|
||||
// max time to wait for dialogs to close before navigating
|
||||
const DIALOG_WAIT_TIMEOUT = 500;
|
||||
|
||||
export const navigate = async (
|
||||
path: string,
|
||||
options?: NavigateOptions,
|
||||
timestamp = Date.now()
|
||||
) => {
|
||||
/**
|
||||
* Ensures all dialogs are closed before navigation.
|
||||
* Returns true if navigation can proceed, false if a dialog refused to close.
|
||||
*/
|
||||
const ensureDialogsClosed = async (timestamp: number): Promise<boolean> => {
|
||||
const { history } = mainWindow;
|
||||
if (history.state?.dialog && Date.now() - timestamp < DIALOG_WAIT_TIMEOUT) {
|
||||
const closed = await closeAllDialogs();
|
||||
if (!closed) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Navigation blocked, because dialog refused to close");
|
||||
return false;
|
||||
}
|
||||
return new Promise<boolean>((resolve) => {
|
||||
// need to wait for history state to be updated in case a dialog was closed
|
||||
setTimeout(() => {
|
||||
navigate(path, options, timestamp).then(resolve);
|
||||
});
|
||||
});
|
||||
|
||||
if (!history.state?.dialog || Date.now() - timestamp >= DIALOG_WAIT_TIMEOUT) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const closed = await closeAllDialogs();
|
||||
if (!closed) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Navigation blocked, because dialog refused to close");
|
||||
return false;
|
||||
}
|
||||
|
||||
// wait for history state to be updated after dialog closed
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve);
|
||||
});
|
||||
|
||||
return ensureDialogsClosed(timestamp);
|
||||
};
|
||||
|
||||
export const navigate = async (path: string, options?: NavigateOptions) => {
|
||||
const canProceed = await ensureDialogsClosed(Date.now());
|
||||
if (!canProceed) {
|
||||
return false;
|
||||
}
|
||||
const replace = options?.replace || false;
|
||||
|
||||
if (__DEMO__) {
|
||||
if (replace) {
|
||||
history.replaceState(
|
||||
history.state?.root ? { root: true } : (options?.data ?? null),
|
||||
mainWindow.history.replaceState(
|
||||
mainWindow.history.state?.root
|
||||
? { root: true }
|
||||
: (options?.data ?? null),
|
||||
"",
|
||||
`${mainWindow.location.pathname}#${path}`
|
||||
);
|
||||
@@ -50,13 +63,13 @@ export const navigate = async (
|
||||
mainWindow.location.hash = path;
|
||||
}
|
||||
} else if (replace) {
|
||||
history.replaceState(
|
||||
history.state?.root ? { root: true } : (options?.data ?? null),
|
||||
mainWindow.history.replaceState(
|
||||
mainWindow.history.state?.root ? { root: true } : (options?.data ?? null),
|
||||
"",
|
||||
path
|
||||
);
|
||||
} else {
|
||||
history.pushState(options?.data ?? null, "", path);
|
||||
mainWindow.history.pushState(options?.data ?? null, "", path);
|
||||
}
|
||||
fireEvent(mainWindow, "location-changed", {
|
||||
replace,
|
||||
@@ -68,10 +81,14 @@ export const navigate = async (
|
||||
* Navigate back in history, with fallback to a default path if no history exists.
|
||||
* This prevents a user from getting stuck when they navigate directly to a page with no history.
|
||||
*/
|
||||
export const goBack = (fallbackPath?: string) => {
|
||||
const { history } = mainWindow;
|
||||
export const goBack = async (fallbackPath?: string): Promise<void> => {
|
||||
const canProceed = await ensureDialogsClosed(Date.now());
|
||||
if (!canProceed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have history to go back to
|
||||
const { history } = mainWindow;
|
||||
if (history.length > 1) {
|
||||
history.back();
|
||||
return;
|
||||
|
||||
@@ -2,7 +2,7 @@ import type {
|
||||
HassEntity,
|
||||
HassEntityAttributeBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { NumberFormat } from "../../data/translation";
|
||||
import { round } from "./round";
|
||||
|
||||
@@ -45,9 +45,8 @@ export const computeFormatFunctions = async (
|
||||
formatEntityAttributeName: FormatEntityAttributeNameFunc;
|
||||
formatEntityName: FormatEntityNameFunc;
|
||||
}> => {
|
||||
const { computeStateDisplay } = await import(
|
||||
"../entity/compute_state_display"
|
||||
);
|
||||
const { computeStateDisplay } =
|
||||
await import("../entity/compute_state_display");
|
||||
const { computeAttributeValueDisplay, computeAttributeNameDisplay } =
|
||||
await import("../entity/compute_attribute_display");
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Route } from "../../types";
|
||||
|
||||
export const computeRouteTail = (route: Route) => {
|
||||
const dividerPos = route.path.indexOf("/", 1);
|
||||
return dividerPos === -1
|
||||
? {
|
||||
prefix: route.prefix + route.path,
|
||||
path: "",
|
||||
}
|
||||
: {
|
||||
prefix: route.prefix + route.path.substring(0, dividerPos),
|
||||
path: route.path.substring(dividerPos),
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,16 @@
|
||||
// From https://github.com/epoberezkin/fast-deep-equal
|
||||
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
|
||||
export const deepEqual = (a: any, b: any): boolean => {
|
||||
|
||||
interface DeepEqualOptions {
|
||||
/** Compare Symbol properties in addition to string keys */
|
||||
compareSymbols?: boolean;
|
||||
}
|
||||
|
||||
export const deepEqual = (
|
||||
a: any,
|
||||
b: any,
|
||||
options?: DeepEqualOptions
|
||||
): boolean => {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
@@ -18,7 +28,7 @@ export const deepEqual = (a: any, b: any): boolean => {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (!deepEqual(a[i], b[i])) {
|
||||
if (!deepEqual(a[i], b[i], options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +45,7 @@ export const deepEqual = (a: any, b: any): boolean => {
|
||||
}
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (!deepEqual(i[1], b.get(i[0]))) {
|
||||
if (!deepEqual(i[1], b.get(i[0]), options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -93,11 +103,28 @@ export const deepEqual = (a: any, b: any): boolean => {
|
||||
for (i = length; i-- !== 0; ) {
|
||||
const key = keys[i];
|
||||
|
||||
if (!deepEqual(a[key], b[key])) {
|
||||
if (!deepEqual(a[key], b[key], options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Compare Symbol properties if requested
|
||||
if (options?.compareSymbols) {
|
||||
const symbolsA = Object.getOwnPropertySymbols(a);
|
||||
const symbolsB = Object.getOwnPropertySymbols(b);
|
||||
if (symbolsA.length !== symbolsB.length) {
|
||||
return false;
|
||||
}
|
||||
for (const sym of symbolsA) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, sym)) {
|
||||
return false;
|
||||
}
|
||||
if (!deepEqual(a[sym], b[sym], options)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export const startMediaProgressInterval = (
|
||||
interval: number | undefined,
|
||||
callback: () => void,
|
||||
intervalMs = 1000
|
||||
): number => {
|
||||
if (interval) {
|
||||
return interval;
|
||||
}
|
||||
return window.setInterval(callback, intervalMs);
|
||||
};
|
||||
|
||||
export const stopMediaProgressInterval = (
|
||||
interval: number | undefined
|
||||
): number | undefined => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -1,30 +1,45 @@
|
||||
/**
|
||||
* Executes a callback within a View Transition if supported, otherwise runs it directly.
|
||||
* Executes a synchronous callback within a View Transition if supported, otherwise runs it directly.
|
||||
*
|
||||
* @param callback - Function to execute. Can be synchronous or return a Promise. The callback will be passed a boolean indicating whether the view transition is available.
|
||||
* @param callback - Synchronous function to execute. The callback will be passed a boolean indicating whether the view transition is available.
|
||||
* @returns Promise that resolves when the transition completes (or immediately if not supported)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Synchronous callback
|
||||
* withViewTransition(() => {
|
||||
* this.large = !this.large;
|
||||
* });
|
||||
*
|
||||
* // Async callback
|
||||
* await withViewTransition(async () => {
|
||||
* await this.updateData();
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export const withViewTransition = (
|
||||
callback: (viewTransitionAvailable: boolean) => void | Promise<void>
|
||||
callback: (viewTransitionAvailable: boolean) => void
|
||||
): Promise<void> => {
|
||||
if (document.startViewTransition) {
|
||||
return document.startViewTransition(() => callback(true)).finished;
|
||||
if (!document.startViewTransition) {
|
||||
callback(false);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Fallback: Execute callback directly without transition
|
||||
const result = callback(false);
|
||||
return result instanceof Promise ? result : Promise.resolve();
|
||||
let callbackInvoked = false;
|
||||
|
||||
try {
|
||||
// View Transitions require DOM updates to happen synchronously within
|
||||
// the callback. Execute the callback immediately (synchronously).
|
||||
const transition = document.startViewTransition(() => {
|
||||
callbackInvoked = true;
|
||||
callback(true);
|
||||
});
|
||||
return transition.finished;
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
"View transition failed, falling back to direct execution.",
|
||||
err
|
||||
);
|
||||
// Make sure the callback is invoked exactly once.
|
||||
if (!callbackInvoked) {
|
||||
callback(false);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import type { HaSlider } from "../../components/ha-slider";
|
||||
|
||||
interface VolumeSliderControllerOptions {
|
||||
getSlider: () => HaSlider | undefined;
|
||||
step: number;
|
||||
onSetVolume: (value: number) => void;
|
||||
onSetVolumeDebounced?: (value: number) => void;
|
||||
onValueUpdated?: (value: number) => void;
|
||||
}
|
||||
|
||||
export class VolumeSliderController {
|
||||
private _touchStartX = 0;
|
||||
|
||||
private _touchStartY = 0;
|
||||
|
||||
private _touchStartValue = 0;
|
||||
|
||||
private _touchDragging = false;
|
||||
|
||||
private _touchScrolling = false;
|
||||
|
||||
private _dragging = false;
|
||||
|
||||
private _lastValue = 0;
|
||||
|
||||
private _options: VolumeSliderControllerOptions;
|
||||
|
||||
constructor(options: VolumeSliderControllerOptions) {
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
public get isInteracting(): boolean {
|
||||
return this._touchDragging || this._dragging;
|
||||
}
|
||||
|
||||
public setStep(step: number): void {
|
||||
this._options.step = step;
|
||||
}
|
||||
|
||||
public handleInput = (ev: Event): void => {
|
||||
ev.stopPropagation();
|
||||
const value = Number((ev.target as HaSlider).value);
|
||||
this._dragging = true;
|
||||
this._updateValue(value);
|
||||
this._options.onSetVolumeDebounced?.(value);
|
||||
};
|
||||
|
||||
public handleChange = (ev: Event): void => {
|
||||
ev.stopPropagation();
|
||||
const value = Number((ev.target as HaSlider).value);
|
||||
this._dragging = false;
|
||||
this._updateValue(value);
|
||||
this._options.onSetVolume(value);
|
||||
};
|
||||
|
||||
public handleTouchStart = (ev: TouchEvent): void => {
|
||||
ev.stopPropagation();
|
||||
const touch = ev.touches[0];
|
||||
this._touchStartX = touch.clientX;
|
||||
this._touchStartY = touch.clientY;
|
||||
this._touchStartValue = this._getSliderValue();
|
||||
this._touchDragging = false;
|
||||
this._touchScrolling = false;
|
||||
this._showTooltip();
|
||||
};
|
||||
|
||||
public handleTouchMove = (ev: TouchEvent): void => {
|
||||
if (this._touchScrolling) {
|
||||
return;
|
||||
}
|
||||
const touch = ev.touches[0];
|
||||
const deltaX = touch.clientX - this._touchStartX;
|
||||
const deltaY = touch.clientY - this._touchStartY;
|
||||
const absDeltaX = Math.abs(deltaX);
|
||||
const absDeltaY = Math.abs(deltaY);
|
||||
|
||||
if (!this._touchDragging) {
|
||||
if (absDeltaY > 10 && absDeltaY > absDeltaX * 2) {
|
||||
this._touchScrolling = true;
|
||||
return;
|
||||
}
|
||||
if (absDeltaX > 8) {
|
||||
this._touchDragging = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._touchDragging) {
|
||||
ev.preventDefault();
|
||||
const newValue = this._getVolumeFromTouch(touch.clientX);
|
||||
this._updateValue(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
public handleTouchEnd = (ev: TouchEvent): void => {
|
||||
if (this._touchScrolling) {
|
||||
this._touchScrolling = false;
|
||||
this._hideTooltip();
|
||||
return;
|
||||
}
|
||||
|
||||
const touch = ev.changedTouches[0];
|
||||
if (!this._touchDragging) {
|
||||
const tapValue = this._getVolumeFromTouch(touch.clientX);
|
||||
const delta =
|
||||
tapValue > this._touchStartValue
|
||||
? this._options.step
|
||||
: -this._options.step;
|
||||
const newValue = this._roundVolumeValue(this._touchStartValue + delta);
|
||||
this._updateValue(newValue);
|
||||
this._options.onSetVolume(newValue);
|
||||
} else {
|
||||
const finalValue = this._getVolumeFromTouch(touch.clientX);
|
||||
this._updateValue(finalValue);
|
||||
this._options.onSetVolume(finalValue);
|
||||
}
|
||||
|
||||
this._touchDragging = false;
|
||||
this._dragging = false;
|
||||
this._hideTooltip();
|
||||
};
|
||||
|
||||
public handleTouchCancel = (): void => {
|
||||
this._touchDragging = false;
|
||||
this._touchScrolling = false;
|
||||
this._dragging = false;
|
||||
this._updateValue(this._touchStartValue);
|
||||
this._hideTooltip();
|
||||
};
|
||||
|
||||
public handleWheel = (ev: WheelEvent): void => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const direction = ev.deltaY > 0 ? -1 : 1;
|
||||
const currentValue = this._getSliderValue();
|
||||
const newValue = this._roundVolumeValue(
|
||||
currentValue + direction * this._options.step
|
||||
);
|
||||
this._updateValue(newValue);
|
||||
this._options.onSetVolume(newValue);
|
||||
};
|
||||
|
||||
private _getVolumeFromTouch(clientX: number): number {
|
||||
const slider = this._options.getSlider();
|
||||
if (!slider) {
|
||||
return 0;
|
||||
}
|
||||
const rect = slider.getBoundingClientRect();
|
||||
const x = Math.min(Math.max(clientX - rect.left, 0), rect.width);
|
||||
const percentage = (x / rect.width) * 100;
|
||||
return this._roundVolumeValue(percentage);
|
||||
}
|
||||
|
||||
private _roundVolumeValue(value: number): number {
|
||||
return Math.min(
|
||||
Math.max(Math.round(value / this._options.step) * this._options.step, 0),
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
private _getSliderValue(): number {
|
||||
const slider = this._options.getSlider();
|
||||
if (slider) {
|
||||
return Number(slider.value);
|
||||
}
|
||||
return this._lastValue;
|
||||
}
|
||||
|
||||
private _updateValue(value: number): void {
|
||||
this._lastValue = value;
|
||||
this._options.onValueUpdated?.(value);
|
||||
const slider = this._options.getSlider();
|
||||
if (slider) {
|
||||
slider.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private _showTooltip(): void {
|
||||
const slider = this._options.getSlider() as any;
|
||||
slider?.showTooltip?.();
|
||||
}
|
||||
|
||||
private _hideTooltip(): void {
|
||||
const slider = this._options.getSlider() as any;
|
||||
slider?.hideTooltip?.();
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,13 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { LitElement, html, css } from "lit";
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type { SankeySeriesOption } from "echarts/types/dist/echarts";
|
||||
import type { CallbackDataParams } from "echarts/types/src/util/types";
|
||||
import type {
|
||||
CallbackDataParams,
|
||||
ECElementEvent,
|
||||
} from "echarts/types/src/util/types";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import SankeyChart from "../../resources/echarts/components/sankey/install";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
@@ -21,6 +25,7 @@ export interface Node {
|
||||
label?: string;
|
||||
color?: string;
|
||||
passThrough?: boolean;
|
||||
entityId?: string;
|
||||
}
|
||||
export interface Link {
|
||||
source: string;
|
||||
@@ -83,6 +88,7 @@ export class HaSankeyChart extends LitElement {
|
||||
.options=${options}
|
||||
height="100%"
|
||||
.extraComponents=${[SankeyChart]}
|
||||
@chart-click=${this._handleChartClick}
|
||||
></ha-chart-base>`;
|
||||
}
|
||||
|
||||
@@ -103,6 +109,22 @@ export class HaSankeyChart extends LitElement {
|
||||
return null;
|
||||
};
|
||||
|
||||
private _handleChartClick = (ev: CustomEvent<ECElementEvent>) => {
|
||||
const detail = ev.detail;
|
||||
// Only handle node clicks (not links)
|
||||
if (detail.dataType !== "node") {
|
||||
return;
|
||||
}
|
||||
const nodeId = (detail.data as Record<string, any>)?.id;
|
||||
if (!nodeId) {
|
||||
return;
|
||||
}
|
||||
const node = this.data.nodes.find((n) => n.id === nodeId);
|
||||
if (node?.entityId) {
|
||||
fireEvent(this, "node-click", { node });
|
||||
}
|
||||
};
|
||||
|
||||
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))].sort();
|
||||
@@ -294,4 +316,7 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-sankey-chart": HaSankeyChart;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"node-click": { node: Node };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type { SunburstSeriesOption } from "echarts/types/dist/echarts";
|
||||
import type { CallbackDataParams } from "echarts/types/src/util/types";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
|
||||
let SunburstChart: typeof import("echarts/lib/chart/sunburst/install");
|
||||
|
||||
export interface SunburstNode {
|
||||
id: string;
|
||||
name?: string;
|
||||
value: number;
|
||||
itemStyle?: {
|
||||
color?: string;
|
||||
};
|
||||
children?: SunburstNode[];
|
||||
}
|
||||
|
||||
@customElement("ha-sunburst-chart")
|
||||
export class HaSunburstChart extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data?: SunburstNode;
|
||||
|
||||
@property({ type: String, attribute: false }) public valueFormatter?: (
|
||||
value: number
|
||||
) => string;
|
||||
|
||||
public chart?: EChartsType;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (!SunburstChart) {
|
||||
import("echarts/lib/chart/sunburst/install").then((module) => {
|
||||
SunburstChart = module;
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!SunburstChart || !this.data) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const options = {
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
formatter: this._renderTooltip,
|
||||
appendTo: document.body,
|
||||
},
|
||||
} as ECOption;
|
||||
|
||||
return html`<ha-chart-base
|
||||
.data=${this._createData(this.data)}
|
||||
.options=${options}
|
||||
height="100%"
|
||||
.extraComponents=${[SunburstChart]}
|
||||
></ha-chart-base>`;
|
||||
}
|
||||
|
||||
private _renderTooltip = (params: CallbackDataParams) => {
|
||||
const data = params.data as { name: string; value: number };
|
||||
const value = this.valueFormatter
|
||||
? this.valueFormatter(data.value)
|
||||
: data.value;
|
||||
return `${params.marker} ${filterXSS(data.name)}<br>${value}`;
|
||||
};
|
||||
|
||||
private _createData = memoizeOne(
|
||||
(data: SunburstNode): SunburstSeriesOption => {
|
||||
const computedStyles = getComputedStyle(this);
|
||||
|
||||
// Transform to echarts format (uses 'name' instead of 'id')
|
||||
const transformNode = (
|
||||
node: SunburstNode,
|
||||
index: number,
|
||||
depth: number,
|
||||
parentColor?: string
|
||||
) => {
|
||||
const result = {
|
||||
...node,
|
||||
name: node.name || node.id,
|
||||
};
|
||||
|
||||
if (depth > 0 && !node.itemStyle?.color) {
|
||||
// Don't assign color to root node
|
||||
result.itemStyle = {
|
||||
color: parentColor ?? getGraphColorByIndex(index, computedStyles),
|
||||
};
|
||||
}
|
||||
|
||||
if (node.children && node.children.length > 0) {
|
||||
result.children = node.children.map((child, i) =>
|
||||
transformNode(child, i, depth + 1, result.itemStyle?.color)
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const transformedData = transformNode(data, 0, 0);
|
||||
|
||||
return {
|
||||
type: "sunburst",
|
||||
data: transformedData.children || [transformedData],
|
||||
radius: [0, "90%"],
|
||||
sort: undefined, // Keep original order
|
||||
label: {
|
||||
show: false,
|
||||
align: "center",
|
||||
rotate: "radial",
|
||||
minAngle: 15,
|
||||
hideOverlap: true,
|
||||
},
|
||||
emphasis: {
|
||||
focus: "ancestor",
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
borderRadius: 2,
|
||||
},
|
||||
levels: this._generateLevels(this._getMaxDepth(data)),
|
||||
} as SunburstSeriesOption;
|
||||
}
|
||||
);
|
||||
|
||||
private _getMaxDepth(node: SunburstNode, currentDepth = 0): number {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return currentDepth;
|
||||
}
|
||||
return Math.max(
|
||||
...node.children.map((child) =>
|
||||
this._getMaxDepth(child, currentDepth + 1)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private _generateLevels(depth: number): SunburstSeriesOption["levels"] {
|
||||
const levels: SunburstSeriesOption["levels"] = [];
|
||||
|
||||
// Root level (center) - transparent, small fixed size
|
||||
const rootRadius = 15;
|
||||
const outerRadius = 95;
|
||||
const availableRadius = outerRadius - rootRadius;
|
||||
|
||||
levels.push({
|
||||
r0: "0%",
|
||||
r: `${rootRadius}%`,
|
||||
itemStyle: {
|
||||
color: "transparent",
|
||||
},
|
||||
});
|
||||
|
||||
if (depth === 0) {
|
||||
return levels;
|
||||
}
|
||||
|
||||
// Distribute remaining radius among data levels using weighted distribution
|
||||
// First level gets most space, each subsequent level gets progressively smaller
|
||||
const weights = Array.from({ length: depth }, (_, i) => depth - i);
|
||||
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
|
||||
|
||||
let currentRadius = rootRadius;
|
||||
for (let i = 0; i < depth; i++) {
|
||||
const levelRadius = (weights[i] / totalWeight) * availableRadius;
|
||||
const r0 = currentRadius;
|
||||
const r = currentRadius + levelRadius;
|
||||
currentRadius = r;
|
||||
|
||||
levels.push({
|
||||
r0: `${r0}%`,
|
||||
r: `${r}%`,
|
||||
// Show labels only on first level
|
||||
...(i === 0 ? { label: { show: true } } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
return levels;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
flex: 1;
|
||||
}
|
||||
ha-chart-base {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-sunburst-chart": HaSunburstChart;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { VisualMapComponentOption } from "echarts/components";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
import type { YAXisOption } from "echarts/types/dist/shared";
|
||||
@@ -21,12 +21,14 @@ import { measureTextWidth } from "../../util/text";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
const parsed = parseFloat(value);
|
||||
return isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
@customElement("state-history-chart-line")
|
||||
export class StateHistoryChartLine extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@@ -183,7 +185,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
|
||||
if (param.seriesName) {
|
||||
return `${param.marker} ${param.seriesName}: ${value}`;
|
||||
return `${param.marker} ${filterXSS(param.seriesName)}: ${value}`;
|
||||
}
|
||||
return `${param.marker} ${value}`;
|
||||
})
|
||||
@@ -795,7 +797,6 @@ export class StateHistoryChartLine extends LitElement {
|
||||
return Math.abs(value) < 1 ? value : roundingFn(value);
|
||||
}
|
||||
}
|
||||
customElements.define("state-history-chart-line", StateHistoryChartLine);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
|
||||
@@ -184,17 +184,11 @@ export class StatisticsChart extends LitElement {
|
||||
}
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
if (!this._legendData) {
|
||||
return;
|
||||
}
|
||||
this._hiddenStats.add(ev.detail.id);
|
||||
this.requestUpdate("_hiddenStats");
|
||||
}
|
||||
|
||||
private _datasetUnhidden(ev: CustomEvent) {
|
||||
if (!this._legendData) {
|
||||
return;
|
||||
}
|
||||
this._hiddenStats.delete(ev.detail.id);
|
||||
this.requestUpdate("_hiddenStats");
|
||||
}
|
||||
@@ -521,7 +515,9 @@ export class StatisticsChart extends LitElement {
|
||||
`ui.components.statistics_charts.statistic_types.${type}`
|
||||
),
|
||||
symbol: "none",
|
||||
sampling: "minmax",
|
||||
// minmax sampling operates independently per series, breaking stacking alignment
|
||||
// https://github.com/apache/echarts/issues/11879
|
||||
sampling: band && drawBands ? "lttb" : "minmax",
|
||||
animationDurationUpdate: 0,
|
||||
lineStyle: {
|
||||
width: 1.5,
|
||||
@@ -539,10 +535,17 @@ export class StatisticsChart extends LitElement {
|
||||
if (band && this.chartType === "line") {
|
||||
series.stack = `band-${statistic_id}`;
|
||||
series.stackStrategy = "all";
|
||||
if (drawBands && type === bandTop) {
|
||||
(series as LineSeriesOption).areaStyle = {
|
||||
color: color + "3F",
|
||||
};
|
||||
if (this._hiddenStats.has(`${statistic_id}-${bandBottom}`)) {
|
||||
// changing the stackOrder forces echarts to render the stacked series that are not hidden #28472
|
||||
series.stackOrder = "seriesDesc";
|
||||
(series as LineSeriesOption).areaStyle = undefined;
|
||||
} else {
|
||||
series.stackOrder = "seriesAsc";
|
||||
if (drawBands && type === bandTop) {
|
||||
(series as LineSeriesOption).areaStyle = {
|
||||
color: color + "3F",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!this.hideLegend) {
|
||||
@@ -586,7 +589,8 @@ export class StatisticsChart extends LitElement {
|
||||
} else if (
|
||||
type === bandTop &&
|
||||
this.chartType === "line" &&
|
||||
drawBands
|
||||
drawBands &&
|
||||
!this._hiddenStats.has(`${statistic_id}-${bandBottom}`)
|
||||
) {
|
||||
const top = stat[bandTop] || 0;
|
||||
val.push(Math.abs(top - (stat[bandBottom] || 0)));
|
||||
|
||||
@@ -3,11 +3,11 @@ import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import { hex2rgb, lab2hex, rgb2lab } from "../../common/color/convert-color";
|
||||
import { labBrighten } from "../../common/color/lab";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { stateColorProperties } from "../../common/entity/state_color";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { computeCssValue } from "../../resources/css-variables";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||
import { stateColorProperties } from "../../common/entity/state_color";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||
import { computeCssValue } from "../../resources/css-variables";
|
||||
|
||||
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
|
||||
media_player: {
|
||||
|
||||
@@ -2,15 +2,17 @@ import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import type { LabelRegistryEntry } from "../../data/label_registry";
|
||||
import { computeCssColor } from "../../common/color/compute-color";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-label";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import type { LabelRegistryEntry } from "../../data/label/label_registry";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../ha-button-menu";
|
||||
import "../ha-dropdown";
|
||||
import "../ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "../ha-dropdown-item";
|
||||
import "../ha-icon";
|
||||
import "../ha-list-item";
|
||||
import "../ha-label";
|
||||
|
||||
@customElement("ha-data-table-labels")
|
||||
class HaDataTableLabels extends LitElement {
|
||||
@@ -26,12 +28,11 @@ class HaDataTableLabels extends LitElement {
|
||||
(label) => this._renderLabel(label, true)
|
||||
)}
|
||||
${labels.length > 2
|
||||
? html`<ha-button-menu
|
||||
absolute
|
||||
? html`<ha-dropdown
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click=${this._handleIconOverflowMenuOpened}
|
||||
@closed=${this._handleIconOverflowMenuClosed}
|
||||
@click=${stopPropagation}
|
||||
@wa-select=${this._handleDropdownSelect}
|
||||
>
|
||||
<ha-label slot="trigger" class="plus" dense>
|
||||
+${labels.length - 2}
|
||||
@@ -40,12 +41,12 @@ class HaDataTableLabels extends LitElement {
|
||||
labels.slice(2),
|
||||
(label) => label.label_id,
|
||||
(label) => html`
|
||||
<ha-list-item @click=${this._labelClicked} .item=${label}>
|
||||
<ha-dropdown-item .value=${label.label_id} .item=${label}>
|
||||
${this._renderLabel(label, false)}
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>`
|
||||
</ha-dropdown>`
|
||||
: nothing}
|
||||
</ha-chip-set>
|
||||
`;
|
||||
@@ -81,21 +82,12 @@ class HaDataTableLabels extends LitElement {
|
||||
fireEvent(this, "label-clicked", { label });
|
||||
}
|
||||
|
||||
protected _handleIconOverflowMenuOpened(e) {
|
||||
e.stopPropagation();
|
||||
// If this component is used inside a data table, the z-index of the row
|
||||
// needs to be increased. Otherwise the ha-button-menu would be displayed
|
||||
// underneath the next row in the table.
|
||||
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
|
||||
if (row) {
|
||||
row.style.zIndex = "1";
|
||||
}
|
||||
}
|
||||
|
||||
protected _handleIconOverflowMenuClosed() {
|
||||
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
|
||||
if (row) {
|
||||
row.style.zIndex = "";
|
||||
private _handleDropdownSelect(
|
||||
ev: CustomEvent<{ item: HaDropdownItem & { item?: LabelRegistryEntry } }>
|
||||
) {
|
||||
const label = ev.detail?.item?.item;
|
||||
if (label) {
|
||||
fireEvent(this, "label-clicked", { label });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,9 +106,6 @@ class HaDataTableLabels extends LitElement {
|
||||
--ha-label-background-color: var(--color, var(--grey-color));
|
||||
--ha-label-background-opacity: 0.5;
|
||||
}
|
||||
ha-button-menu {
|
||||
border-radius: 10px;
|
||||
}
|
||||
.plus {
|
||||
--ha-label-background-color: transparent;
|
||||
border: 1px solid var(--divider-color);
|
||||
|
||||
@@ -16,8 +16,10 @@ import memoizeOne from "memoize-one";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { groupBy } from "../../common/util/group-by";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -26,8 +28,6 @@ import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-svg-icon";
|
||||
import "../search-input";
|
||||
import { filterData, sortData } from "./sort-filter";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
|
||||
export interface RowClickedEvent {
|
||||
id: string;
|
||||
@@ -838,10 +838,10 @@ export class HaDataTable extends LitElement {
|
||||
} else if (this.sortDirection === "asc") {
|
||||
this.sortDirection = "desc";
|
||||
} else {
|
||||
this.sortDirection = null;
|
||||
this.sortDirection = "asc";
|
||||
}
|
||||
|
||||
this.sortColumn = this.sortDirection === null ? undefined : columnId;
|
||||
this.sortColumn = columnId;
|
||||
|
||||
fireEvent(this, "sorting-changed", {
|
||||
column: columnId,
|
||||
@@ -1364,6 +1364,9 @@ export class HaDataTable extends LitElement {
|
||||
.mdc-data-table__header-cell > * {
|
||||
transition: var(--float-start) 0.2s ease;
|
||||
}
|
||||
.mdc-data-table__header-cell--numeric > span {
|
||||
transition: none;
|
||||
}
|
||||
.mdc-data-table__header-cell ha-svg-icon {
|
||||
top: -3px;
|
||||
position: absolute;
|
||||
@@ -1402,6 +1405,9 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.scroller {
|
||||
height: calc(100% - 57px);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { expose } from "comlink";
|
||||
import type { FuseOptionKey, IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ipCompare, stringCompare } from "../../common/string/compare";
|
||||
import { stripDiacritics } from "../../common/string/strip-diacritics";
|
||||
import { HaFuse } from "../../resources/fuse";
|
||||
import type {
|
||||
ClonedDataTableColumnData,
|
||||
DataTableRowData,
|
||||
@@ -11,48 +11,159 @@ import type {
|
||||
SortingDirection,
|
||||
} from "./ha-data-table";
|
||||
|
||||
const fuseIndex = memoizeOne(
|
||||
(data: DataTableRowData[], columns: SortableColumnContainer) => {
|
||||
const searchKeys = new Set<string>();
|
||||
Object.entries(columns).forEach(([key, column]) => {
|
||||
if (column.filterable) {
|
||||
searchKeys.add(
|
||||
column.filterKey
|
||||
? `${column.valueColumn || key}.${column.filterKey}`
|
||||
: key
|
||||
);
|
||||
}
|
||||
});
|
||||
return Fuse.createIndex([...searchKeys], data);
|
||||
}
|
||||
interface FilterKeyConfig {
|
||||
key: string;
|
||||
filterKey?: string;
|
||||
}
|
||||
|
||||
const getFilterKeys = memoizeOne(
|
||||
(columns: SortableColumnContainer): FilterKeyConfig[] =>
|
||||
Object.entries(columns)
|
||||
.filter(([, column]) => column.filterable)
|
||||
.map(([key, column]) => ({
|
||||
key: column.valueColumn || key,
|
||||
filterKey: column.filterKey,
|
||||
}))
|
||||
);
|
||||
|
||||
const getSearchableValue = (
|
||||
row: DataTableRowData,
|
||||
{ key, filterKey }: FilterKeyConfig
|
||||
): string => {
|
||||
let value = row[key];
|
||||
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (filterKey && typeof value === "object" && !Array.isArray(value)) {
|
||||
value = value[filterKey];
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const stringValues = value
|
||||
.filter((item) => item != null && typeof item !== "object")
|
||||
.map(String);
|
||||
return stripDiacritics(stringValues.join(" ").toLowerCase());
|
||||
}
|
||||
|
||||
return stripDiacritics(String(value).toLowerCase());
|
||||
};
|
||||
|
||||
/** Filters data using exact substring matching (all terms must match). */
|
||||
const filterDataExact = (
|
||||
data: DataTableRowData[],
|
||||
filterKeys: FilterKeyConfig[],
|
||||
terms: string[]
|
||||
): DataTableRowData[] => {
|
||||
if (terms.length === 1) {
|
||||
const term = terms[0];
|
||||
return data.filter((row) =>
|
||||
filterKeys.some((config) =>
|
||||
getSearchableValue(row, config).includes(term)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return data.filter((row) => {
|
||||
const searchString = filterKeys
|
||||
.map((config) => getSearchableValue(row, config))
|
||||
.join(" ");
|
||||
return terms.every((term) => searchString.includes(term));
|
||||
});
|
||||
};
|
||||
|
||||
const FUZZY_OPTIONS: IFuseOptions<DataTableRowData> = {
|
||||
ignoreDiacritics: true,
|
||||
isCaseSensitive: false,
|
||||
threshold: 0.2, // Stricter than default 0.3
|
||||
minMatchCharLength: 2,
|
||||
ignoreLocation: true,
|
||||
shouldSort: false,
|
||||
};
|
||||
|
||||
interface FuseKeyConfig {
|
||||
name: string | string[];
|
||||
getFn: (row: DataTableRowData) => string;
|
||||
}
|
||||
|
||||
/** Filters data using fuzzy matching with Fuse.js (all terms must match). */
|
||||
const filterDataFuzzy = (
|
||||
data: DataTableRowData[],
|
||||
filterKeys: FilterKeyConfig[],
|
||||
terms: string[]
|
||||
): DataTableRowData[] => {
|
||||
// Build Fuse.js search keys from filter keys
|
||||
const fuseKeys: FuseKeyConfig[] = filterKeys.map((config) => ({
|
||||
name: config.filterKey ? [config.key, config.filterKey] : config.key,
|
||||
getFn: (row: DataTableRowData) => getSearchableValue(row, config),
|
||||
}));
|
||||
|
||||
// Find minimum term length to adjust minMatchCharLength
|
||||
const minTermLength = Math.min(...terms.map((t) => t.length));
|
||||
const minMatchCharLength = Math.min(minTermLength, 2);
|
||||
|
||||
const fuse = new Fuse<DataTableRowData>(data, {
|
||||
...FUZZY_OPTIONS,
|
||||
keys: fuseKeys as FuseOptionKey<DataTableRowData>[],
|
||||
minMatchCharLength,
|
||||
});
|
||||
|
||||
// For single term, simple search
|
||||
if (terms.length === 1) {
|
||||
return fuse.search(terms[0]).map((r) => r.item);
|
||||
}
|
||||
|
||||
// For multiple terms, all must match (AND logic)
|
||||
const expression = {
|
||||
$and: terms.map((term) => ({
|
||||
$or: fuseKeys.map((key) => ({
|
||||
$path: Array.isArray(key.name) ? key.name : [key.name],
|
||||
$val: term,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
return fuse.search(expression).map((r) => r.item);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters data with exact match priority and fuzzy fallback.
|
||||
* - First tries exact substring matching
|
||||
* - If exact matches found, returns only those
|
||||
* - If no exact matches, falls back to fuzzy search with strict scoring
|
||||
*/
|
||||
const filterData = (
|
||||
data: DataTableRowData[],
|
||||
columns: SortableColumnContainer,
|
||||
filter: string
|
||||
) => {
|
||||
filter = stripDiacritics(filter.toLowerCase());
|
||||
): DataTableRowData[] => {
|
||||
const normalizedFilter = stripDiacritics(filter.toLowerCase().trim());
|
||||
|
||||
if (filter === "") {
|
||||
if (!normalizedFilter) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const index = fuseIndex(data, columns);
|
||||
const filterKeys = getFilterKeys(columns);
|
||||
|
||||
const fuse = new HaFuse(
|
||||
data,
|
||||
{ shouldSort: false, minMatchCharLength: 1 },
|
||||
index
|
||||
);
|
||||
|
||||
const searchResults = fuse.multiTermsSearch(filter);
|
||||
|
||||
if (searchResults) {
|
||||
return searchResults.map((result) => result.item);
|
||||
if (!filterKeys.length) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return data;
|
||||
const terms = normalizedFilter.split(/\s+/);
|
||||
|
||||
// First, try exact substring matching
|
||||
const exactMatches = filterDataExact(data, filterKeys, terms);
|
||||
|
||||
if (exactMatches.length > 0) {
|
||||
return exactMatches;
|
||||
}
|
||||
|
||||
// No exact matches, fall back to fuzzy search
|
||||
return filterDataFuzzy(data, filterKeys, terms);
|
||||
};
|
||||
|
||||
const sortData = (
|
||||
|
||||
@@ -101,6 +101,10 @@ const Component = Vue.extend({
|
||||
type: String,
|
||||
default: "en",
|
||||
},
|
||||
opensVertical: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
render(createElement) {
|
||||
// @ts-expect-error
|
||||
@@ -129,6 +133,11 @@ const Component = Vue.extend({
|
||||
},
|
||||
expression: "dateRange",
|
||||
},
|
||||
on: {
|
||||
toggle: (open: boolean) => {
|
||||
fireEvent(this.$el as HTMLElement, "toggle", { open });
|
||||
},
|
||||
},
|
||||
scopedSlots: {
|
||||
input() {
|
||||
return createElement("slot", {
|
||||
@@ -309,6 +318,10 @@ class DateRangePickerElement extends WrappedElement {
|
||||
min-width: unset !important;
|
||||
display: block !important;
|
||||
}
|
||||
:host([opens-vertical="up"]) .daterangepicker {
|
||||
bottom: 100%;
|
||||
top: auto !important;
|
||||
}
|
||||
`;
|
||||
if (mainWindow.document.dir === "rtl") {
|
||||
style.innerHTML += `
|
||||
@@ -340,4 +353,7 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"date-range-picker": DateRangePickerElement;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
toggle: { open: boolean };
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user