mirror of
https://github.com/home-assistant/frontend.git
synced 2025-12-10 10:07:19 +00:00
Compare commits
260 Commits
number-ent
...
ha-icon-pi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10e7db1641 | ||
|
|
3d16719da7 | ||
|
|
cddede9e83 | ||
|
|
8a18026f78 | ||
|
|
4142a0917d | ||
|
|
5bef24f755 | ||
|
|
9cd38c7128 | ||
|
|
6322c19a45 | ||
|
|
74b51b77fe | ||
|
|
b80481b53e | ||
|
|
2ce1eaf8c6 | ||
|
|
4030ce3f88 | ||
|
|
41cabde393 | ||
|
|
47d1fdf673 | ||
|
|
e59e83fffe | ||
|
|
b896b78876 | ||
|
|
6e180c9fb4 | ||
|
|
b50daecdec | ||
|
|
f5bd0816a8 | ||
|
|
b6d1e65044 | ||
|
|
e2312e52e3 | ||
|
|
46a6da33d9 | ||
|
|
e7b0b9a090 | ||
|
|
63ffc3b7f9 | ||
|
|
ebfceae38c | ||
|
|
cccfc1716b | ||
|
|
6abf0dfcb9 | ||
|
|
9d2404763f | ||
|
|
6c84c9b44e | ||
|
|
edb2172bdd | ||
|
|
067e7645ac | ||
|
|
c3e4af3db8 | ||
|
|
f44be2d3b9 | ||
|
|
cff7ed9b05 | ||
|
|
c4e5f1dba6 | ||
|
|
d1011d691f | ||
|
|
7dda8c36bc | ||
|
|
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 | ||
|
|
e7e4407a09 | ||
|
|
3f0c9538bd | ||
|
|
5c3ccbfdad | ||
|
|
9710142c47 | ||
|
|
57640d17cd | ||
|
|
b5d93e7515 | ||
|
|
ca9b29d82a | ||
|
|
51efa4f61f | ||
|
|
a6c71719d1 | ||
|
|
44aa15809a | ||
|
|
0f99fcd58c | ||
|
|
0ff1b92fde | ||
|
|
98c9e0f2e4 | ||
|
|
493d650e15 | ||
|
|
c4e1ff0276 | ||
|
|
c8f5910d98 | ||
|
|
0ae49c7c3e | ||
|
|
d961aaace4 | ||
|
|
bd6bc25874 | ||
|
|
a45b07ea23 | ||
|
|
f7c446b257 | ||
|
|
71a29aa97d | ||
|
|
ae3a405a7b | ||
|
|
ebbcad812a | ||
|
|
d35a12d7e8 | ||
|
|
67e6dba85c | ||
|
|
fb666a7553 | ||
|
|
d767afb1e1 | ||
|
|
eb6191ab3a | ||
|
|
7fe9ae22f0 | ||
|
|
e3a1e5e0e3 | ||
|
|
01b8a5e01a | ||
|
|
a8ab035793 | ||
|
|
b36795ad3d | ||
|
|
ff0e173cfd | ||
|
|
11883525c2 | ||
|
|
f89f364add | ||
|
|
56391e32f2 | ||
|
|
b7f0f62949 | ||
|
|
ccc48d158a | ||
|
|
b11e787f09 | ||
|
|
cdb6562de8 | ||
|
|
d8e8c9aa02 | ||
|
|
be392be1e6 | ||
|
|
10dc432445 | ||
|
|
19187f887d | ||
|
|
dc76a42aaa | ||
|
|
1f2b8047a6 | ||
|
|
e8c9ed0528 | ||
|
|
c7ae78c02f | ||
|
|
dc8f1211e6 | ||
|
|
5c25a63ea5 | ||
|
|
c1787ab994 | ||
|
|
6fea535fdc | ||
|
|
e8cee84380 | ||
|
|
b4613edeb7 | ||
|
|
a8b6e5aa3d | ||
|
|
e842193cd6 | ||
|
|
bb0813333d | ||
|
|
ab4c6f80f4 | ||
|
|
89796e425a | ||
|
|
9c42c8bbc4 | ||
|
|
616237caee | ||
|
|
2d36a0d37f | ||
|
|
1ec432a20f | ||
|
|
afd91b2261 | ||
|
|
cdfb7f914f | ||
|
|
33b0897522 | ||
|
|
5f0cf1b522 | ||
|
|
afb2ad95a4 | ||
|
|
27beab3133 | ||
|
|
435c82489b | ||
|
|
3ba6bf272e | ||
|
|
eec99b2fa3 | ||
|
|
d23e45e410 | ||
|
|
3c82d12609 | ||
|
|
15d67997e7 | ||
|
|
a6dfcb3100 | ||
|
|
26c2369228 | ||
|
|
2eed446492 | ||
|
|
7ebdeab6b2 | ||
|
|
0c35278f51 | ||
|
|
561122f03d | ||
|
|
95311be034 | ||
|
|
1eda44a102 | ||
|
|
d76781eb91 | ||
|
|
82d44e051f | ||
|
|
fdc9f5a3b7 | ||
|
|
ee6c82aba9 | ||
|
|
11d3f5c2ba | ||
|
|
feb68ce373 | ||
|
|
7f9a9de157 | ||
|
|
8e1b6a3d3b | ||
|
|
6e6e5a53e2 | ||
|
|
0408734ec5 | ||
|
|
317519fc08 | ||
|
|
843d79eab4 | ||
|
|
165a757f06 | ||
|
|
ea8b730142 | ||
|
|
e88c97d625 | ||
|
|
7560988b76 | ||
|
|
eecd8077b6 | ||
|
|
cbab5c3f7b | ||
|
|
a5d27c8bb8 | ||
|
|
a6a340b5db |
8
.github/workflows/cast_deployment.yaml
vendored
8
.github/workflows/cast_deployment.yaml
vendored
@@ -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@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.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@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
16
.github/workflows/ci.yaml
vendored
16
.github/workflows/ci.yaml
vendored
@@ -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@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -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@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.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@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -100,9 +100,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@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -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@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
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@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
|
||||
# ℹ️ 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@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
|
||||
8
.github/workflows/demo_deployment.yaml
vendored
8
.github/workflows/demo_deployment.yaml
vendored
@@ -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@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.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@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
4
.github/workflows/design_deployment.yaml
vendored
4
.github/workflows/design_deployment.yaml
vendored
@@ -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@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
4
.github/workflows/design_preview.yaml
vendored
4
.github/workflows/design_preview.yaml
vendored
@@ -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@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
6
.github/workflows/nightly.yaml
vendored
6
.github/workflows/nightly.yaml
vendored
@@ -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@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@feb19ddc698445db27401f1490f6ac182da0816f # v3.2.0
|
||||
uses: relative-ci/agent-action@c45aaa919ef85620af54242a241ac17a8fa35983 # v3.2.1
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
token: ${{ github.token }}
|
||||
|
||||
22
.github/workflows/release.yaml
vendored
22
.github/workflows/release.yaml
vendored
@@ -23,10 +23,10 @@ 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: 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 +34,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@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
script/release
|
||||
|
||||
- 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 +75,7 @@ jobs:
|
||||
|
||||
# home-assistant/wheels doesn't support SHA pinning
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.10.0
|
||||
uses: home-assistant/wheels@2025.12.0
|
||||
with:
|
||||
abi: cp313
|
||||
tag: musllinux_1_2
|
||||
@@ -91,9 +91,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@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -108,7 +108,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 +120,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@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
@@ -137,6 +137,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
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -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: |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.11.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.12.0.cjs
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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!)
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger
|
||||
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
|
||||
import { HaHassTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant";
|
||||
import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list";
|
||||
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
|
||||
import { HaNumericStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state";
|
||||
import { HaPersistentNotificationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-persistent_notification";
|
||||
import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state";
|
||||
@@ -38,11 +37,6 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||
triggers: [{ ...HaStateTrigger.defaultConfig }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "MQTT",
|
||||
triggers: [{ ...HaMQTTTrigger.defaultConfig }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "GeoLocation",
|
||||
triggers: [{ ...HaGeolocationTrigger.defaultConfig }],
|
||||
|
||||
3
gallery/src/pages/components/ha-adaptive-dialog.markdown
Normal file
3
gallery/src/pages/components/ha-adaptive-dialog.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Adaptive dialog (ha-adaptive-dialog)
|
||||
---
|
||||
732
gallery/src/pages/components/ha-adaptive-dialog.ts
Normal file
732
gallery/src/pages/components/ha-adaptive-dialog.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -381,10 +381,6 @@ export class DemoHaWaDialog extends LitElement {
|
||||
<td><code>--dialog-z-index</code></td>
|
||||
<td>Z-index for the dialog.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--dialog-surface-position</code></td>
|
||||
<td>CSS position of the dialog surface.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--dialog-surface-margin-top</code></td>
|
||||
<td>Top margin for the dialog surface.</td>
|
||||
|
||||
60
package.json
60
package.json
@@ -28,13 +28,13 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.28.4",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.19.1",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.0",
|
||||
"@codemirror/language": "6.11.3",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.5.11",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.38.6",
|
||||
"@codemirror/view": "6.38.8",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.18.2",
|
||||
@@ -52,7 +52,7 @@
|
||||
"@fullcalendar/list": "6.1.19",
|
||||
"@fullcalendar/luxon3": "6.1.19",
|
||||
"@fullcalendar/timegrid": "6.1.19",
|
||||
"@home-assistant/webawesome": "3.0.0",
|
||||
"@home-assistant/webawesome": "3.0.0-ha.1",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.0.9",
|
||||
"@lit-labs/observers": "2.0.6",
|
||||
@@ -89,17 +89,17 @@
|
||||
"@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",
|
||||
"@vaadin/combo-box": "24.9.6",
|
||||
"@vaadin/vaadin-themable-mixin": "24.9.6",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"barcode-detector": "3.0.6",
|
||||
"color-name": "2.0.2",
|
||||
"barcode-detector": "3.0.8",
|
||||
"color-name": "2.1.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.46.0",
|
||||
"core-js": "3.47.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"culori": "4.0.2",
|
||||
"date-fns": "4.1.0",
|
||||
@@ -111,7 +111,7 @@
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"hls.js": "1.6.14",
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.18",
|
||||
@@ -122,7 +122,7 @@
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.0",
|
||||
"marked": "17.0.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -139,12 +139,12 @@
|
||||
"vue": "2.7.16",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.3.0",
|
||||
"workbox-core": "7.3.0",
|
||||
"workbox-expiration": "7.3.0",
|
||||
"workbox-precaching": "7.3.0",
|
||||
"workbox-routing": "7.3.0",
|
||||
"workbox-strategies": "7.3.0",
|
||||
"workbox-cacheable-response": "7.4.0",
|
||||
"workbox-core": "7.4.0",
|
||||
"workbox-expiration": "7.4.0",
|
||||
"workbox-precaching": "7.4.0",
|
||||
"workbox-routing": "7.4.0",
|
||||
"workbox-strategies": "7.4.0",
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -152,13 +152,13 @@
|
||||
"@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.3.1",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.7",
|
||||
"@lokalise/node-api": "15.4.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",
|
||||
"@rsdoctor/rspack-plugin": "1.3.12",
|
||||
"@rspack/core": "1.6.6",
|
||||
"@rspack/dev-server": "1.1.4",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.22",
|
||||
@@ -178,7 +178,7 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.0.8",
|
||||
"@vitest/coverage-v8": "4.0.15",
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
@@ -194,32 +194,32 @@
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.2",
|
||||
"glob": "12.0.0",
|
||||
"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.1.0",
|
||||
"jsdom": "27.2.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.6",
|
||||
"lint-staged": "16.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.6.2",
|
||||
"rspack-manifest-plugin": "5.1.0",
|
||||
"prettier": "3.7.4",
|
||||
"rspack-manifest-plugin": "5.2.0",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.0",
|
||||
"tar": "7.5.2",
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"terser-webpack-plugin": "5.3.15",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.46.3",
|
||||
"typescript-eslint": "8.48.1",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "4.0.8",
|
||||
"vitest": "4.0.15",
|
||||
"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"
|
||||
@@ -236,7 +236,7 @@
|
||||
"@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": "yarn@4.11.0",
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"volta": {
|
||||
"node": "22.21.1"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20251029.0"
|
||||
version = "20251203.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AuthData } from "home-assistant-js-websocket";
|
||||
import { extractSearchParam } from "../url/search-params";
|
||||
import { hassUrl } from "../../data/auth";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -30,7 +31,11 @@ export function askWrite() {
|
||||
export function saveTokens(tokens: AuthData | null) {
|
||||
tokenCache.tokens = tokens;
|
||||
|
||||
if (!tokenCache.writeEnabled && extractSearchParam("storeToken") === "true") {
|
||||
if (
|
||||
!tokenCache.writeEnabled &&
|
||||
(extractSearchParam("storeToken") === "true" ||
|
||||
hassUrl !== `${location.protocol}//${location.host}`)
|
||||
) {
|
||||
tokenCache.writeEnabled = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,64 +1,16 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { theme2hex } from "./convert-color";
|
||||
|
||||
export const COLORS = [
|
||||
"#4269d0",
|
||||
"#f4bd4a",
|
||||
"#ff725c",
|
||||
"#6cc5b0",
|
||||
"#a463f2",
|
||||
"#ff8ab7",
|
||||
"#9c6b4e",
|
||||
"#97bbf5",
|
||||
"#01ab63",
|
||||
"#094bad",
|
||||
"#c99000",
|
||||
"#d84f3e",
|
||||
"#49a28f",
|
||||
"#048732",
|
||||
"#d96895",
|
||||
"#8043ce",
|
||||
"#7599d1",
|
||||
"#7a4c31",
|
||||
"#6989f4",
|
||||
"#ffd444",
|
||||
"#ff957c",
|
||||
"#8fe9d3",
|
||||
"#62cc71",
|
||||
"#ffadda",
|
||||
"#c884ff",
|
||||
"#badeff",
|
||||
"#bf8b6d",
|
||||
"#927acc",
|
||||
"#97ee3f",
|
||||
"#bf3947",
|
||||
"#9f5b00",
|
||||
"#f48758",
|
||||
"#8caed6",
|
||||
"#f2b94f",
|
||||
"#eff26e",
|
||||
"#e43872",
|
||||
"#d9b100",
|
||||
"#9d7a00",
|
||||
"#698cff",
|
||||
"#00d27e",
|
||||
"#d06800",
|
||||
"#009f82",
|
||||
"#c49200",
|
||||
"#cbe8ff",
|
||||
"#fecddf",
|
||||
"#c27eb6",
|
||||
"#8cd2ce",
|
||||
"#c4b8d9",
|
||||
"#f883b0",
|
||||
"#a49100",
|
||||
"#f48800",
|
||||
"#27d0df",
|
||||
"#a04a9b",
|
||||
];
|
||||
// Total number of colors defined in CSS variables (--color-1 through --color-54)
|
||||
export const COLORS_COUNT = 54;
|
||||
|
||||
export function getColorByIndex(index: number) {
|
||||
return COLORS[index % COLORS.length];
|
||||
export function getColorByIndex(
|
||||
index: number,
|
||||
style: CSSStyleDeclaration
|
||||
): string {
|
||||
// Wrap around using modulo to support unlimited indices
|
||||
const colorIndex = (index % COLORS_COUNT) + 1;
|
||||
return style.getPropertyValue(`--color-${colorIndex}`);
|
||||
}
|
||||
|
||||
export function getGraphColorByIndex(
|
||||
@@ -68,15 +20,19 @@ export function getGraphColorByIndex(
|
||||
// The CSS vars for the colors use range 1..n, so we need to adjust the index from the internal 0..n color index range.
|
||||
const themeColor =
|
||||
style.getPropertyValue(`--graph-color-${index + 1}`) ||
|
||||
getColorByIndex(index);
|
||||
getColorByIndex(index, style);
|
||||
return theme2hex(themeColor);
|
||||
}
|
||||
|
||||
export const getAllGraphColors = memoizeOne(
|
||||
(style: CSSStyleDeclaration) =>
|
||||
COLORS.map((_color, index) => getGraphColorByIndex(index, style)),
|
||||
Array.from({ length: COLORS_COUNT }, (_, index) =>
|
||||
getGraphColorByIndex(index, style)
|
||||
),
|
||||
(newArgs: [CSSStyleDeclaration], lastArgs: [CSSStyleDeclaration]) =>
|
||||
// this is not ideal, but we need to memoize the colors
|
||||
newArgs[0].getPropertyValue("--graph-color-1") ===
|
||||
lastArgs[0].getPropertyValue("--graph-color-1")
|
||||
lastArgs[0].getPropertyValue("--graph-color-1") &&
|
||||
newArgs[0].getPropertyValue("--color-1") ===
|
||||
lastArgs[0].getPropertyValue("--color-1")
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -593,6 +593,7 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
const options = {
|
||||
animation: !this._reducedMotion,
|
||||
animationDuration: 500,
|
||||
darkMode: this._themes.darkMode ?? false,
|
||||
aria: { show: true },
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
|
||||
@@ -167,6 +167,7 @@ export class HaSankeyChart extends LitElement {
|
||||
curveness: 0.5,
|
||||
},
|
||||
layoutIterations: 0,
|
||||
animationDuration: 500,
|
||||
label: {
|
||||
formatter: (params) =>
|
||||
data.nodes.find((node) => node.id === (params.data as Node).id)
|
||||
@@ -279,6 +280,7 @@ export class HaSankeyChart extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
background: var(--ha-card-background, var(--card-background-color));
|
||||
}
|
||||
ha-chart-base {
|
||||
|
||||
@@ -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";
|
||||
@@ -27,6 +27,7 @@ const safeParseFloat = (value) => {
|
||||
return isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
@customElement("state-history-chart-line")
|
||||
export class StateHistoryChartLine extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@@ -795,7 +796,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 {
|
||||
|
||||
@@ -373,6 +373,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
itemName: 3,
|
||||
},
|
||||
renderItem: this._renderItem,
|
||||
progressive: 0,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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_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,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { expose } from "comlink";
|
||||
import Fuse from "fuse.js";
|
||||
import Fuse, { type FuseOptionKey } 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 { multiTermSearch } from "../../resources/fuseMultiTerm";
|
||||
import type {
|
||||
ClonedDataTableColumnData,
|
||||
DataTableRowData,
|
||||
@@ -11,9 +11,10 @@ import type {
|
||||
SortingDirection,
|
||||
} from "./ha-data-table";
|
||||
|
||||
const fuseIndex = memoizeOne(
|
||||
(data: DataTableRowData[], columns: SortableColumnContainer) => {
|
||||
const getSearchKeys = memoizeOne(
|
||||
(columns: SortableColumnContainer): FuseOptionKey<DataTableRowData>[] => {
|
||||
const searchKeys = new Set<string>();
|
||||
|
||||
Object.entries(columns).forEach(([key, column]) => {
|
||||
if (column.filterable) {
|
||||
searchKeys.add(
|
||||
@@ -23,10 +24,15 @@ const fuseIndex = memoizeOne(
|
||||
);
|
||||
}
|
||||
});
|
||||
return Fuse.createIndex([...searchKeys], data);
|
||||
return Array.from(searchKeys);
|
||||
}
|
||||
);
|
||||
|
||||
const fuseIndex = memoizeOne(
|
||||
(data: DataTableRowData[], keys: FuseOptionKey<DataTableRowData>[]) =>
|
||||
Fuse.createIndex(keys, data)
|
||||
);
|
||||
|
||||
const filterData = (
|
||||
data: DataTableRowData[],
|
||||
columns: SortableColumnContainer,
|
||||
@@ -38,21 +44,13 @@ const filterData = (
|
||||
return data;
|
||||
}
|
||||
|
||||
const index = fuseIndex(data, columns);
|
||||
const keys = getSearchKeys(columns);
|
||||
|
||||
const fuse = new HaFuse(
|
||||
data,
|
||||
{ shouldSort: false, minMatchCharLength: 1 },
|
||||
index
|
||||
);
|
||||
const index = fuseIndex(data, keys);
|
||||
|
||||
const searchResults = fuse.multiTermsSearch(filter);
|
||||
|
||||
if (searchResults) {
|
||||
return searchResults.map((result) => result.item);
|
||||
}
|
||||
|
||||
return data;
|
||||
return multiTermSearch<DataTableRowData>(data, filter, keys, index, {
|
||||
threshold: 0.2, // reduce fuzzy matches in data tables
|
||||
});
|
||||
};
|
||||
|
||||
const sortData = (
|
||||
|
||||
@@ -9,6 +9,7 @@ import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
||||
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
|
||||
import {
|
||||
deviceComboBoxKeys,
|
||||
getDevices,
|
||||
type DevicePickerItem,
|
||||
type DeviceRegistryEntry,
|
||||
@@ -216,6 +217,10 @@ export class HaDevicePicker extends LitElement {
|
||||
.getItems=${this._getItems}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.searchKeys=${deviceComboBoxKeys}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.device-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
||||
import {
|
||||
entityComboBoxKeys,
|
||||
getEntities,
|
||||
type EntityComboBoxItem,
|
||||
} from "../../data/entity_registry";
|
||||
@@ -288,10 +289,14 @@ export class HaEntityPicker extends LitElement {
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.searchFn=${this._searchFn}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
.searchKeys=${entityComboBoxKeys}
|
||||
.addButtonLabel=${this.addButton
|
||||
? this.hass.localize("ui.components.entity.entity-picker.add")
|
||||
: undefined}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
|
||||
@@ -38,9 +38,21 @@ type StatisticItemType = "entity" | "external" | "no_state";
|
||||
interface StatisticComboBoxItem extends PickerComboBoxItem {
|
||||
statistic_id?: string;
|
||||
stateObj?: HassEntity;
|
||||
domainName?: string;
|
||||
type?: StatisticItemType;
|
||||
}
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "label", weight: 10 },
|
||||
{ name: "search_labels.entityName", weight: 10 },
|
||||
{ name: "search_labels.friendlyName", weight: 9 },
|
||||
{ name: "search_labels.deviceName", weight: 8 },
|
||||
{ name: "search_labels.areaName", weight: 6 },
|
||||
{ name: "search_labels.domainName", weight: 4 },
|
||||
{ name: "statisticId", weight: 3 },
|
||||
{ name: "id", weight: 2 },
|
||||
];
|
||||
|
||||
@customElement("ha-statistic-picker")
|
||||
export class HaStatisticPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -233,7 +245,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
),
|
||||
type,
|
||||
sorting_label: [sortingPrefix, label].join("_"),
|
||||
search_labels: [label, id],
|
||||
icon_path: mdiShape,
|
||||
});
|
||||
} else if (type === "external") {
|
||||
@@ -246,7 +257,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
secondary: domainName,
|
||||
type,
|
||||
sorting_label: [sortingPrefix, label].join("_"),
|
||||
search_labels: [label, domainName, id],
|
||||
search_labels: { label, domainName },
|
||||
icon_path: mdiChartLine,
|
||||
});
|
||||
}
|
||||
@@ -280,13 +291,12 @@ export class HaStatisticPicker extends LitElement {
|
||||
stateObj: stateObj,
|
||||
type: "entity",
|
||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||
search_labels: [
|
||||
entityName,
|
||||
deviceName,
|
||||
areaName,
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
areaName: areaName || null,
|
||||
friendlyName,
|
||||
id,
|
||||
].filter(Boolean) as string[],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -361,13 +371,13 @@ export class HaStatisticPicker extends LitElement {
|
||||
stateObj: stateObj,
|
||||
type: "entity",
|
||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||
search_labels: [
|
||||
entityName,
|
||||
deviceName,
|
||||
areaName,
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
areaName: areaName || null,
|
||||
friendlyName,
|
||||
statisticId,
|
||||
].filter(Boolean) as string[],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -394,7 +404,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
secondary: domainName,
|
||||
type: "external",
|
||||
sorting_label: [sortingPrefix, label].join("_"),
|
||||
search_labels: [label, domainName, statisticId],
|
||||
search_labels: { label, domainName, statisticId },
|
||||
icon_path: mdiChartLine,
|
||||
};
|
||||
}
|
||||
@@ -409,7 +419,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
secondary: this.hass.localize("ui.components.statistic-picker.no_state"),
|
||||
type: "no_state",
|
||||
sorting_label: [sortingPrefix, label].join("_"),
|
||||
search_labels: [label, statisticId],
|
||||
search_labels: { label, statisticId },
|
||||
icon_path: mdiShape,
|
||||
};
|
||||
}
|
||||
@@ -475,6 +485,10 @@ export class HaStatisticPicker extends LitElement {
|
||||
.searchFn=${this._searchFn}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.helper=${this.helper}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.statistic-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mdiAlert } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
@@ -17,6 +17,7 @@ import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@customElement("state-badge")
|
||||
export class StateBadge extends LitElement {
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@@ -265,5 +266,3 @@ declare global {
|
||||
"state-badge": StateBadge;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("state-badge", StateBadge);
|
||||
|
||||
188
src/components/ha-adaptive-dialog.ts
Normal file
188
src/components/ha-adaptive-dialog.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { listenMediaQuery } from "../common/dom/media_query";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-wa-dialog";
|
||||
import type { DialogWidth } from "./ha-wa-dialog";
|
||||
|
||||
type DialogSheetMode = "dialog" | "bottom-sheet";
|
||||
|
||||
/**
|
||||
* Home Assistant adaptive dialog component
|
||||
*
|
||||
* @element ha-adaptive-dialog
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A responsive dialog component that automatically switches between a full dialog (ha-wa-dialog)
|
||||
* and a bottom sheet (ha-bottom-sheet) based on screen size. Uses dialog mode on larger screens
|
||||
* (>870px width and >500px height) and bottom sheet mode on smaller screens or mobile devices.
|
||||
*
|
||||
* @slot headerNavigationIcon - Leading header action (e.g. close/back button).
|
||||
* @slot headerTitle - Custom title content (used when header-title is not set).
|
||||
* @slot headerSubtitle - Custom subtitle content (used when header-subtitle is not set).
|
||||
* @slot headerActionItems - Trailing header actions (e.g. buttons, menus).
|
||||
* @slot - Dialog/sheet content body.
|
||||
* @slot footer - Dialog/sheet footer content.
|
||||
*
|
||||
* @cssprop --ha-dialog-surface-background - Dialog/sheet background color.
|
||||
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface (dialog mode only).
|
||||
* @cssprop --ha-dialog-show-duration - Show animation duration (dialog mode only).
|
||||
* @cssprop --ha-dialog-hide-duration - Hide animation duration (dialog mode only).
|
||||
*
|
||||
* @attr {boolean} open - Controls the dialog/sheet open state.
|
||||
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset (dialog mode only). Defaults to "medium".
|
||||
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used.
|
||||
* @attr {string} header-subtitle - Header subtitle text. If not set, the headerSubtitle slot is used.
|
||||
* @attr {("above"|"below")} header-subtitle-position - Position of the subtitle relative to the title. Defaults to "below".
|
||||
* @attr {boolean} block-mode-change - 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.
|
||||
*
|
||||
* @event opened - Fired when the dialog/sheet is shown (dialog mode only).
|
||||
* @event closed - Fired after the dialog/sheet is hidden.
|
||||
* @event after-show - Fired after show animation completes (dialog mode only).
|
||||
*
|
||||
* @remarks
|
||||
* **Responsive Behavior:**
|
||||
* The component automatically switches between dialog and bottom sheet modes based on viewport size.
|
||||
* Dialog mode is used for screens wider than 870px and taller than 500px.
|
||||
* Bottom sheet mode is used for mobile devices and smaller screens.
|
||||
*
|
||||
* When `block-mode-change` is set, the mode is determined once at mount time based on the initial
|
||||
* screen size. Subsequent viewport size changes will not trigger mode switches, which is useful
|
||||
* for preventing form resets or other state loss when users resize their browser window.
|
||||
*
|
||||
* **Focus Management:**
|
||||
* To automatically focus an element when opened, add the `autofocus` attribute to it.
|
||||
* Components with `delegatesFocus: true` (like `ha-form`) will forward focus to their first focusable child.
|
||||
* Example: `<ha-form autofocus .schema=${schema}></ha-form>`
|
||||
*/
|
||||
@customElement("ha-adaptive-dialog")
|
||||
export class HaAdaptiveDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "aria-labelledby" })
|
||||
public ariaLabelledBy?: string;
|
||||
|
||||
@property({ attribute: "aria-describedby" })
|
||||
public ariaDescribedBy?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public open = false;
|
||||
|
||||
@property({ type: String, reflect: true, attribute: "width" })
|
||||
public width: DialogWidth = "medium";
|
||||
|
||||
@property({ attribute: "header-title" })
|
||||
public headerTitle?: string;
|
||||
|
||||
@property({ attribute: "header-subtitle" })
|
||||
public headerSubtitle?: string;
|
||||
|
||||
@property({ type: String, attribute: "header-subtitle-position" })
|
||||
public headerSubtitlePosition: "above" | "below" = "below";
|
||||
|
||||
@property({ type: Boolean, attribute: "block-mode-change" })
|
||||
public blockModeChange = false;
|
||||
|
||||
@state() private _mode: DialogSheetMode = "dialog";
|
||||
|
||||
private _unsubMediaQuery?: () => void;
|
||||
|
||||
private _modeSet = false;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._unsubMediaQuery = listenMediaQuery(
|
||||
"(max-width: 870px), (max-height: 500px)",
|
||||
(matches) => {
|
||||
if (!this._modeSet || !this.blockModeChange) {
|
||||
this._mode = matches ? "bottom-sheet" : "dialog";
|
||||
this._modeSet = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubMediaQuery?.();
|
||||
this._unsubMediaQuery = undefined;
|
||||
this._modeSet = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this._mode === "bottom-sheet") {
|
||||
return html`
|
||||
<ha-bottom-sheet .open=${this.open} flexcontent>
|
||||
<ha-dialog-header
|
||||
slot="header"
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-drawer="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
<slot></slot>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</ha-bottom-sheet>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this.open}
|
||||
.width=${this.width}
|
||||
.ariaLabelledBy=${this.ariaLabelledBy}
|
||||
.ariaDescribedBy=${this.ariaDescribedBy}
|
||||
.headerTitle=${this.headerTitle}
|
||||
.headerSubtitle=${this.headerSubtitle}
|
||||
.headerSubtitlePosition=${this.headerSubtitlePosition}
|
||||
flexcontent
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="headerNavigationIcon"></slot>
|
||||
<slot name="headerTitle" slot="headerTitle"></slot>
|
||||
<slot name="headerSubtitle" slot="headerSubtitle"></slot>
|
||||
<slot name="headerActionItems" slot="headerActionItems"></slot>
|
||||
<slot></slot>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
css`
|
||||
ha-bottom-sheet {
|
||||
--ha-bottom-sheet-surface-background: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--card-background-color, var(--ha-color-surface-default))
|
||||
);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-adaptive-dialog": HaAdaptiveDialog;
|
||||
}
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeAreaName } from "../common/entity/compute_area_name";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import {
|
||||
getAreasAndFloors,
|
||||
type AreaFloorValue,
|
||||
type FloorComboBoxItem,
|
||||
} from "../data/area_floor";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import "./ha-icon-button";
|
||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tree-indicator";
|
||||
|
||||
const SEPARATOR = "________";
|
||||
|
||||
@customElement("ha-area-floor-picker")
|
||||
export class HaAreaFloorPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public value?: AreaFloorValue;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: String, attribute: "search-label" })
|
||||
public searchLabel?: string;
|
||||
|
||||
/**
|
||||
* Show only areas with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no areas with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only areas with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
/**
|
||||
* List of areas to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-areas
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-areas" })
|
||||
public excludeAreas?: string[];
|
||||
|
||||
/**
|
||||
* List of floors to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-floors
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-floors" })
|
||||
public excludeFloors?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: (entity: HassEntity) => boolean;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
||||
const item = this._parseValue(value);
|
||||
|
||||
const area = item.type === "area" && this.hass.areas[value];
|
||||
|
||||
if (area) {
|
||||
const areaName = computeAreaName(area);
|
||||
return html`
|
||||
${area.icon
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
<slot name="headline">${areaName}</slot>
|
||||
`;
|
||||
}
|
||||
|
||||
const floor = item.type === "floor" && this.hass.floors[value];
|
||||
|
||||
if (floor) {
|
||||
const floorName = computeFloorName(floor);
|
||||
return html`
|
||||
<ha-floor-icon slot="start" .floor=${floor}></ha-floor-icon>
|
||||
<span slot="headline">${floorName}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-svg-icon slot="start" .path=${mdiTextureBox}></ha-svg-icon>
|
||||
<span slot="headline">${value}</span>
|
||||
`;
|
||||
};
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = (
|
||||
item,
|
||||
{ index },
|
||||
combobox
|
||||
) => {
|
||||
const nextItem = combobox.filteredItems?.[index + 1];
|
||||
const isLastArea =
|
||||
!nextItem ||
|
||||
nextItem.type === "floor" ||
|
||||
(nextItem.type === "area" && !nextItem.area?.floor_id);
|
||||
|
||||
const rtl = computeRTL(this.hass);
|
||||
|
||||
const hasFloor = item.type === "area" && item.area?.floor_id;
|
||||
|
||||
return html`
|
||||
<ha-combo-box-item
|
||||
type="button"
|
||||
style=${item.type === "area" && hasFloor
|
||||
? "--md-list-item-leading-space: 48px;"
|
||||
: ""}
|
||||
>
|
||||
${item.type === "area" && hasFloor
|
||||
? html`
|
||||
<ha-tree-indicator
|
||||
style=${styleMap({
|
||||
width: "48px",
|
||||
position: "absolute",
|
||||
top: "0px",
|
||||
left: rtl ? undefined : "4px",
|
||||
right: rtl ? "4px" : undefined,
|
||||
transform: rtl ? "scaleX(-1)" : "",
|
||||
})}
|
||||
.end=${isLastArea}
|
||||
slot="start"
|
||||
></ha-tree-indicator>
|
||||
`
|
||||
: nothing}
|
||||
${item.type === "floor" && item.floor
|
||||
? html`<ha-floor-icon
|
||||
slot="start"
|
||||
.floor=${item.floor}
|
||||
></ha-floor-icon>`
|
||||
: item.icon
|
||||
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.icon_path || mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
${item.primary}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
};
|
||||
|
||||
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
|
||||
|
||||
private _getItems = () =>
|
||||
this._getAreasAndFloorsMemoized(
|
||||
this.hass.states,
|
||||
this.hass.floors,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._formatValue,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.excludeAreas,
|
||||
this.excludeFloors
|
||||
);
|
||||
|
||||
private _formatValue = memoizeOne((value: AreaFloorValue): string =>
|
||||
[value.type, value.id].join(SEPARATOR)
|
||||
);
|
||||
|
||||
private _parseValue = memoizeOne((value: string): AreaFloorValue => {
|
||||
const [type, id] = value.split(SEPARATOR);
|
||||
|
||||
return { id, type: type as "floor" | "area" };
|
||||
});
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const placeholder =
|
||||
this.placeholder ?? this.hass.localize("ui.components.area-picker.area");
|
||||
|
||||
const value = this.value ? this._formatValue(this.value) : undefined;
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
.label=${this.label}
|
||||
.searchLabel=${this.searchLabel}
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.area-picker.no_match"
|
||||
)}
|
||||
.placeholder=${placeholder}
|
||||
.value=${value}
|
||||
.getItems=${this._getItems}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (!value) {
|
||||
this._setValue(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = this._parseValue(value);
|
||||
this._setValue(selected);
|
||||
}
|
||||
|
||||
private _setValue(value?: AreaFloorValue) {
|
||||
this.value = value;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-area-floor-picker": HaAreaFloorPicker;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,12 @@ import "./ha-svg-icon";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "areaName", weight: 10 },
|
||||
{ name: "aliases", weight: 8 },
|
||||
{ name: "floorName", weight: 6 },
|
||||
{ name: "id", weight: 3 },
|
||||
];
|
||||
@customElement("ha-area-picker")
|
||||
export class HaAreaPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -291,12 +297,12 @@ export class HaAreaPicker extends LitElement {
|
||||
icon: area.icon || undefined,
|
||||
icon_path: area.icon ? undefined : mdiTextureBox,
|
||||
sorting_label: areaName,
|
||||
search_labels: [
|
||||
areaName,
|
||||
floorName,
|
||||
area.area_id,
|
||||
...area.aliases,
|
||||
].filter((v): v is string => Boolean(v)),
|
||||
search_labels: {
|
||||
areaName: areaName || null,
|
||||
floorName: floorName || null,
|
||||
id: area.area_id,
|
||||
aliases: area.aliases.join(" "),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -379,6 +385,10 @@ export class HaAreaPicker extends LitElement {
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.addButtonLabel=${this.addButtonLabel}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.area-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import { areaCompare } from "../data/area_registry";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-items-display-editor";
|
||||
@@ -37,11 +36,7 @@ export class HaAreasDisplayEditor extends LitElement {
|
||||
public showNavigationButton = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const compare = areaCompare(this.hass.areas);
|
||||
|
||||
const areas = Object.values(this.hass.areas).sort((areaA, areaB) =>
|
||||
compare(areaA.area_id, areaB.area_id)
|
||||
);
|
||||
const areas = Object.values(this.hass.areas);
|
||||
|
||||
const items: DisplayItem[] = areas.map((area) => {
|
||||
const { floor } = getAreaContext(area, this.hass.floors);
|
||||
|
||||
@@ -7,7 +7,6 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import { areaCompare } from "../data/area_registry";
|
||||
import type { FloorRegistryEntry } from "../data/floor_registry";
|
||||
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -131,11 +130,8 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
// update items if floors change
|
||||
_hassFloors: HomeAssistant["floors"]
|
||||
): Record<string, DisplayItem[]> => {
|
||||
const compare = areaCompare(hassAreas);
|
||||
const areas = Object.values(hassAreas);
|
||||
|
||||
const areas = Object.values(hassAreas).sort((areaA, areaB) =>
|
||||
compare(areaA.area_id, areaB.area_id)
|
||||
);
|
||||
const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
|
||||
(acc, area) => {
|
||||
const { floor } = getAreaContext(area, this.hass.floors);
|
||||
|
||||
@@ -134,6 +134,7 @@ export class HaAssistChat extends LitElement {
|
||||
})}"
|
||||
breaks
|
||||
cache
|
||||
assist
|
||||
.content=${message.text}
|
||||
>
|
||||
</ha-markdown>
|
||||
@@ -659,6 +660,7 @@ export class HaAssistChat extends LitElement {
|
||||
--markdown-table-border-color: var(--divider-color);
|
||||
--markdown-code-background-color: var(--primary-background-color);
|
||||
--markdown-code-text-color: var(--primary-text-color);
|
||||
--markdown-list-indent: 1rem;
|
||||
&:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
|
||||
@@ -2,12 +2,13 @@ import "@home-assistant/webawesome/dist/components/drawer/drawer";
|
||||
import { css, html, LitElement, type PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
|
||||
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
|
||||
|
||||
@customElement("ha-bottom-sheet")
|
||||
export class HaBottomSheet extends LitElement {
|
||||
export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ type: Boolean }) public open = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
|
||||
@@ -17,11 +18,18 @@ export class HaBottomSheet extends LitElement {
|
||||
|
||||
@query("#drawer") private _drawer!: HTMLElement;
|
||||
|
||||
@query("#body") private _bodyElement!: HTMLDivElement;
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this._bodyElement;
|
||||
}
|
||||
|
||||
private _gestureRecognizer = new SwipeGestureRecognizer();
|
||||
|
||||
private _isDragging = false;
|
||||
|
||||
private _handleAfterHide() {
|
||||
private _handleAfterHide(afterHideEvent: Event) {
|
||||
afterHideEvent.stopPropagation();
|
||||
this.open = false;
|
||||
const ev = new Event("closed", {
|
||||
bubbles: true,
|
||||
@@ -48,9 +56,13 @@ export class HaBottomSheet extends LitElement {
|
||||
@touchstart=${this._handleTouchStart}
|
||||
>
|
||||
<slot name="header"></slot>
|
||||
<div class="content-wrapper">
|
||||
<div id="body" class="body ha-scrollbar">
|
||||
<slot></slot>
|
||||
</div>
|
||||
${this.renderScrollableFades()}
|
||||
</div>
|
||||
<slot name="footer"></slot>
|
||||
</wa-drawer>
|
||||
`;
|
||||
}
|
||||
@@ -166,7 +178,9 @@ export class HaBottomSheet extends LitElement {
|
||||
this._isDragging = false;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
wa-drawer {
|
||||
@@ -207,6 +221,13 @@ export class HaBottomSheet extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
:host([flexcontent]) .body {
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
@@ -218,8 +239,26 @@ export class HaBottomSheet extends LitElement {
|
||||
var(--safe-area-inset-left)
|
||||
);
|
||||
}
|
||||
slot[name="footer"] {
|
||||
display: block;
|
||||
padding: var(--ha-space-0);
|
||||
}
|
||||
::slotted([slot="footer"]) {
|
||||
display: flex;
|
||||
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
|
||||
var(--ha-space-4);
|
||||
gap: var(--ha-space-3);
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:host([flexcontent]) slot[name="footer"] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -202,6 +202,7 @@ export class HaControlSelect extends LitElement {
|
||||
color: var(--primary-text-color);
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
border-radius: var(--control-select-border-radius);
|
||||
}
|
||||
:host([vertical]) {
|
||||
width: var(--control-select-thickness);
|
||||
@@ -211,7 +212,6 @@ export class HaControlSelect extends LitElement {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: var(--control-select-border-radius);
|
||||
transform: translateZ(0);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -167,30 +167,33 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _labelSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
if (!ev.detail.index.size) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const value: string[] = [];
|
||||
const filteredLabels = this._filteredLabels(
|
||||
this._labels,
|
||||
this._filter,
|
||||
this.value
|
||||
);
|
||||
|
||||
const filteredLabelIds = new Set(filteredLabels.map((l) => l.label_id));
|
||||
|
||||
// Keep previously selected labels that are not in the current filtered view
|
||||
const preservedLabels = (this.value || []).filter(
|
||||
(id) => !filteredLabelIds.has(id)
|
||||
);
|
||||
|
||||
// Build the new selection from the filtered labels based on selected indices
|
||||
const newlySelectedLabels: string[] = [];
|
||||
for (const index of ev.detail.index) {
|
||||
const labelId = filteredLabels[index].label_id;
|
||||
value.push(labelId);
|
||||
const labelId = filteredLabels[index]?.label_id;
|
||||
if (labelId) {
|
||||
newlySelectedLabels.push(labelId);
|
||||
}
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
const value = [...preservedLabels, ...newlySelectedLabels];
|
||||
this.value = value.length ? value : [];
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value,
|
||||
value: value.length ? value : undefined,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
mdiHomeFloor3,
|
||||
mdiHomeFloorNegative1,
|
||||
} from "@mdi/js";
|
||||
import { LitElement, html } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { FloorRegistryEntry } from "../data/floor_registry";
|
||||
import "./ha-icon";
|
||||
@@ -48,7 +48,7 @@ export const floorDefaultIcon = (floor: Pick<FloorRegistryEntry, "level">) => {
|
||||
|
||||
@customElement("ha-floor-icon")
|
||||
export class HaFloorIcon extends LitElement {
|
||||
@property({ attribute: false }) public floor!: Pick<
|
||||
@property({ attribute: false }) public floor?: Pick<
|
||||
FloorRegistryEntry,
|
||||
"icon" | "level"
|
||||
>;
|
||||
@@ -56,6 +56,9 @@ export class HaFloorIcon extends LitElement {
|
||||
@property() public icon?: string;
|
||||
|
||||
protected render() {
|
||||
if (!this.floor) {
|
||||
return nothing;
|
||||
}
|
||||
if (this.floor.icon) {
|
||||
return html`<ha-icon .icon=${this.floor.icon}></ha-icon>`;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,12 @@ import "./ha-svg-icon";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "floorName", weight: 10 },
|
||||
{ name: "aliases", weight: 8 },
|
||||
{ name: "floor_id", weight: 3 },
|
||||
];
|
||||
|
||||
interface FloorComboBoxItem extends PickerComboBoxItem {
|
||||
floor?: FloorRegistryEntry;
|
||||
}
|
||||
@@ -286,9 +292,11 @@ export class HaFloorPicker extends LitElement {
|
||||
primary: floorName,
|
||||
floor: floor,
|
||||
sorting_label: floor.level?.toString() || "zzzzz",
|
||||
search_labels: [floorName, floor.floor_id, ...floor.aliases].filter(
|
||||
(v): v is string => Boolean(v)
|
||||
),
|
||||
search_labels: {
|
||||
floorName,
|
||||
floor_id: floor.floor_id,
|
||||
aliases: floor.aliases.join(" "),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -393,6 +401,10 @@ export class HaFloorPicker extends LitElement {
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.floor-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
|
||||
@@ -4,8 +4,10 @@ import { mdiPlaylistPlus } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-button";
|
||||
@@ -46,8 +48,9 @@ export class HaGenericPicker extends LitElement {
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
|
||||
/** To prevent lags, getItems needs to be memoized */
|
||||
@property({ attribute: false })
|
||||
public getItems?: (
|
||||
public getItems!: (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[];
|
||||
@@ -64,6 +67,9 @@ export class HaGenericPicker extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||
|
||||
@property({ attribute: false })
|
||||
public searchKeys?: FuseWeightedKey[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public notFoundLabel?: string | ((search: string) => string);
|
||||
|
||||
@@ -107,6 +113,8 @@ export class HaGenericPicker extends LitElement {
|
||||
|
||||
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
||||
|
||||
@property({ attribute: "unknown-item-text" }) public unknownItemText?: string;
|
||||
|
||||
@query(".container") private _containerElement?: HTMLDivElement;
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
@@ -129,6 +137,10 @@ export class HaGenericPicker extends LitElement {
|
||||
// helper to set new value after closing picker, to avoid flicker
|
||||
private _newValue?: string;
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public invalid = false;
|
||||
|
||||
private _unsubscribeTinyKeys?: () => void;
|
||||
|
||||
protected render() {
|
||||
@@ -156,6 +168,8 @@ export class HaGenericPicker extends LitElement {
|
||||
type="button"
|
||||
class=${this._opened ? "opened" : ""}
|
||||
compact
|
||||
.unknown=${this._unknownValue(this.value, this.getItems())}
|
||||
.unknownItemText=${this.unknownItemText}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
@click=${this.open}
|
||||
@clear=${this._clear}
|
||||
@@ -163,6 +177,8 @@ export class HaGenericPicker extends LitElement {
|
||||
.value=${this.value}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.errorMessage=${this.errorMessage}
|
||||
.invalid=${this.invalid}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.valueRenderer=${this.valueRenderer}
|
||||
>
|
||||
@@ -229,16 +245,34 @@ export class HaGenericPicker extends LitElement {
|
||||
.sections=${this.sections}
|
||||
.sectionTitleFunction=${this.sectionTitleFunction}
|
||||
.selectedSection=${this.selectedSection}
|
||||
.searchKeys=${this.searchKeys}
|
||||
></ha-picker-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
private _unknownValue = memoizeOne(
|
||||
(value?: string, items?: (PickerComboBoxItem | string)[]) => {
|
||||
if (value === undefined || !items) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !items.some(
|
||||
(item) => typeof item !== "string" && item.id === value
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: nothing;
|
||||
const showError = this.invalid && this.errorMessage;
|
||||
const showHelper = !showError && this.helper;
|
||||
|
||||
if (!showError && !showHelper) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${showError ? this.errorMessage : this.helper}
|
||||
</ha-input-helper-text>`;
|
||||
}
|
||||
|
||||
private _dialogOpened = () => {
|
||||
@@ -248,7 +282,7 @@ export class HaGenericPicker extends LitElement {
|
||||
});
|
||||
};
|
||||
|
||||
private _hidePicker(ev) {
|
||||
private _hidePicker(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
if (this._newValue) {
|
||||
fireEvent(this, "value-changed", { value: this._newValue });
|
||||
@@ -337,6 +371,9 @@ export class HaGenericPicker extends LitElement {
|
||||
display: block;
|
||||
margin: var(--ha-space-2) 0 0;
|
||||
}
|
||||
:host([invalid]) ha-input-helper-text {
|
||||
color: var(--mdc-theme-error, var(--error-color, #b00020));
|
||||
}
|
||||
|
||||
wa-popover {
|
||||
--wa-space-l: var(--ha-space-0);
|
||||
|
||||
@@ -32,6 +32,12 @@ export class HaGridSizeEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public step = 1;
|
||||
|
||||
@property({ type: Boolean, attribute: "rows-disabled" })
|
||||
public rowsDisabled?: boolean;
|
||||
|
||||
@property({ type: Boolean, attribute: "columns-disabled" })
|
||||
public columnsDisabled?: boolean;
|
||||
|
||||
@state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
@@ -42,9 +48,11 @@ export class HaGridSizeEditor extends LitElement {
|
||||
|
||||
protected render() {
|
||||
const disabledColumns =
|
||||
this.columnMin !== undefined && this.columnMin === this.columnMax;
|
||||
this.columnsDisabled ||
|
||||
(this.columnMin !== undefined && this.columnMin === this.columnMax);
|
||||
const disabledRows =
|
||||
this.rowMin !== undefined && this.rowMin === this.rowMax;
|
||||
this.rowsDisabled ||
|
||||
(this.rowMin !== undefined && this.rowMin === this.rowMax);
|
||||
|
||||
const autoHeight = this._localValue?.rows === "auto";
|
||||
const fullWidth = this._localValue?.columns === "full";
|
||||
@@ -72,7 +80,7 @@ export class HaGridSizeEditor extends LitElement {
|
||||
@value-changed=${this._valueChanged}
|
||||
@slider-moved=${this._sliderMoved}
|
||||
.disabled=${disabledColumns}
|
||||
tooltip-mode="always"
|
||||
tooltip-mode=${disabledColumns ? "never" : "always"}
|
||||
></ha-grid-layout-slider>
|
||||
|
||||
<ha-grid-layout-slider
|
||||
@@ -88,7 +96,7 @@ export class HaGridSizeEditor extends LitElement {
|
||||
@value-changed=${this._valueChanged}
|
||||
@slider-moved=${this._sliderMoved}
|
||||
.disabled=${disabledRows}
|
||||
tooltip-mode="always"
|
||||
tooltip-mode=${disabledRows ? "never" : "always"}
|
||||
></ha-grid-layout-slider>
|
||||
${!this.isDefault
|
||||
? html`
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type {
|
||||
ComboBoxDataProviderCallback,
|
||||
ComboBoxDataProviderParams,
|
||||
} from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -10,9 +6,10 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { customIcons } from "../data/custom_icons";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import "./ha-icon";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-generic-picker";
|
||||
import "./ha-icon";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
|
||||
interface IconItem {
|
||||
icon: string;
|
||||
@@ -21,7 +18,7 @@ interface IconItem {
|
||||
}
|
||||
|
||||
interface RankedIcon {
|
||||
icon: string;
|
||||
item: PickerComboBoxItem;
|
||||
rank: number;
|
||||
}
|
||||
|
||||
@@ -67,13 +64,18 @@ const loadCustomIconItems = async (iconsetPrefix: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) => html`
|
||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
|
||||
${item.icon}
|
||||
<ha-icon .icon=${item.id} slot="start"></ha-icon>
|
||||
${item.id}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
const valueRenderer = (value: string) => html`
|
||||
<ha-icon .icon=${value} slot="start"></ha-icon>
|
||||
<span slot="headline">${value}</span>
|
||||
`;
|
||||
|
||||
@customElement("ha-icon-picker")
|
||||
export class HaIconPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@@ -96,13 +98,11 @@ export class HaIconPicker extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
item-value-path="icon"
|
||||
item-label-path="icon"
|
||||
.value=${this._value}
|
||||
allow-custom-value
|
||||
.dataProvider=${ICONS_LOADED ? this._iconProvider : undefined}
|
||||
.getItems=${this._getItems}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@@ -110,69 +110,97 @@ export class HaIconPicker extends LitElement {
|
||||
.placeholder=${this.placeholder}
|
||||
.errorMessage=${this.errorMessage}
|
||||
.invalid=${this.invalid}
|
||||
.renderer=${rowRenderer}
|
||||
icon
|
||||
@opened-changed=${this._openedChanged}
|
||||
.rowRenderer=${rowRenderer}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.searchFn=${this._filterIcons}
|
||||
.notFoundLabel=${this.hass?.localize(
|
||||
"ui.components.icon-picker.no_match"
|
||||
)}
|
||||
popover-placement="bottom-start"
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
${this._value || this.placeholder
|
||||
? html`
|
||||
<ha-icon .icon=${this._value || this.placeholder} slot="icon">
|
||||
</ha-icon>
|
||||
`
|
||||
: html`<slot slot="icon" name="fallback"></slot>`}
|
||||
</ha-combo-box>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
// Filter can take a significant chunk of frame (up to 3-5 ms)
|
||||
private _filterIcons = memoizeOne(
|
||||
(filter: string, iconItems: IconItem[] = ICONS) => {
|
||||
(filter: string, items: PickerComboBoxItem[]): PickerComboBoxItem[] => {
|
||||
if (!filter) {
|
||||
return iconItems;
|
||||
return items;
|
||||
}
|
||||
|
||||
const filteredItems: RankedIcon[] = [];
|
||||
const addIcon = (icon: string, rank: number) =>
|
||||
filteredItems.push({ icon, rank });
|
||||
const addIcon = (item: PickerComboBoxItem, rank: number) =>
|
||||
filteredItems.push({ item, rank });
|
||||
|
||||
// Filter and rank such that exact matches rank higher, and prefer icon name matches over keywords
|
||||
for (const item of iconItems) {
|
||||
if (item.parts.has(filter)) {
|
||||
addIcon(item.icon, 1);
|
||||
} else if (item.keywords.includes(filter)) {
|
||||
addIcon(item.icon, 2);
|
||||
} else if (item.icon.includes(filter)) {
|
||||
addIcon(item.icon, 3);
|
||||
} else if (item.keywords.some((word) => word.includes(filter))) {
|
||||
addIcon(item.icon, 4);
|
||||
for (const item of items) {
|
||||
const iconName = item.id.split(":")[1] || item.id;
|
||||
const parts = iconName.split("-");
|
||||
const keywords = item.search_labels
|
||||
? Object.values(item.search_labels).filter(
|
||||
(v): v is string => v !== null
|
||||
)
|
||||
: [];
|
||||
|
||||
if (parts.includes(filter)) {
|
||||
addIcon(item, 1);
|
||||
} else if (keywords.includes(filter)) {
|
||||
addIcon(item, 2);
|
||||
} else if (item.id.includes(filter)) {
|
||||
addIcon(item, 3);
|
||||
} else if (keywords.some((word) => word.includes(filter))) {
|
||||
addIcon(item, 4);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow preview for custom icon not in list
|
||||
if (filteredItems.length === 0) {
|
||||
addIcon(filter, 0);
|
||||
addIcon(
|
||||
{
|
||||
id: filter,
|
||||
primary: filter,
|
||||
icon: filter,
|
||||
search_labels: { keyword: filter },
|
||||
sorting_label: filter,
|
||||
},
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
return filteredItems.sort((itemA, itemB) => itemA.rank - itemB.rank);
|
||||
return filteredItems
|
||||
.sort((itemA, itemB) => itemA.rank - itemB.rank)
|
||||
.map((item) => item.item);
|
||||
}
|
||||
);
|
||||
|
||||
private _iconProvider = (
|
||||
params: ComboBoxDataProviderParams,
|
||||
callback: ComboBoxDataProviderCallback<IconItem | RankedIcon>
|
||||
) => {
|
||||
const filteredItems = this._filterIcons(params.filter.toLowerCase(), ICONS);
|
||||
const iStart = params.page * params.pageSize;
|
||||
const iEnd = iStart + params.pageSize;
|
||||
callback(filteredItems.slice(iStart, iEnd), filteredItems.length);
|
||||
private _getItems = (): PickerComboBoxItem[] =>
|
||||
ICONS.map((icon: IconItem) => {
|
||||
const iconName = icon.icon.split(":")[1] || icon.icon;
|
||||
const searchLabels: Record<string, string> = {
|
||||
iconName,
|
||||
};
|
||||
Array.from(icon.parts).forEach((part, index) => {
|
||||
searchLabels[`part${index}`] = part;
|
||||
});
|
||||
icon.keywords.forEach((keyword, index) => {
|
||||
searchLabels[`keyword${index}`] = keyword;
|
||||
});
|
||||
return {
|
||||
id: icon.icon,
|
||||
primary: icon.icon,
|
||||
icon: icon.icon,
|
||||
search_labels: searchLabels,
|
||||
sorting_label: icon.icon,
|
||||
};
|
||||
});
|
||||
|
||||
private async _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
const opened = ev.detail.value;
|
||||
if (opened && !ICONS_LOADED) {
|
||||
await loadIcons();
|
||||
protected firstUpdated() {
|
||||
if (!ICONS_LOADED) {
|
||||
loadIcons().then(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,15 +227,9 @@ export class HaIconPicker extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
*[slot="icon"] {
|
||||
color: var(--primary-text-color);
|
||||
position: relative;
|
||||
bottom: 2px;
|
||||
}
|
||||
*[slot="prefix"] {
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
margin-inline-start: initial;
|
||||
ha-generic-picker {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { LabelRegistryEntry } from "../data/label_registry";
|
||||
import {
|
||||
createLabelRegistryEntry,
|
||||
getLabels,
|
||||
labelComboBoxKeys,
|
||||
subscribeLabelRegistry,
|
||||
} from "../data/label_registry";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
@@ -154,7 +155,10 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
return this._getLabelsMemoized(
|
||||
this.hass,
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._labels,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
@@ -234,6 +238,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
.getItems=${this._getItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.searchKeys=${labelComboBoxKeys}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
<slot .slot=${this._slotNodes?.length ? "field" : undefined}></slot>
|
||||
|
||||
@@ -81,6 +81,7 @@ class HaLabel extends LitElement {
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
|
||||
@@ -40,14 +40,12 @@ export const getLanguageOptions = (
|
||||
return {
|
||||
id: lang,
|
||||
primary,
|
||||
search_labels: [primary],
|
||||
};
|
||||
});
|
||||
} else if (locale) {
|
||||
options = languages.map((lang) => ({
|
||||
id: lang,
|
||||
primary: formatLanguageCode(lang, locale),
|
||||
search_labels: [formatLanguageCode(lang, locale)],
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -73,6 +71,8 @@ export class HaLanguagePicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ attribute: "native-name", type: Boolean })
|
||||
public nativeName = false;
|
||||
|
||||
@@ -135,6 +135,7 @@ export class HaLanguagePicker extends LitElement {
|
||||
.value=${value}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
.getItems=${this._getItems}
|
||||
@value-changed=${this._changed}
|
||||
hide-clear-icon
|
||||
|
||||
@@ -71,7 +71,7 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
if (!this.innerHTML && this.cache) {
|
||||
const key = this._computeCacheKey();
|
||||
if (markdownCache.has(key)) {
|
||||
render(markdownCache.get(key)!, this.renderRoot);
|
||||
render(h(unsafeHTML(markdownCache.get(key))), this.renderRoot);
|
||||
this._resize();
|
||||
}
|
||||
}
|
||||
@@ -99,10 +99,7 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
}
|
||||
);
|
||||
|
||||
render(
|
||||
elements.map((e) => h(unsafeHTML(e))),
|
||||
this.renderRoot
|
||||
);
|
||||
render(h(unsafeHTML(elements.join(""))), this.renderRoot);
|
||||
|
||||
this._resize();
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@ export class HaMarkdown extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public cache = false;
|
||||
|
||||
@query("ha-markdown-element") private _markdownElement!: ReactiveElement;
|
||||
@query("ha-markdown-element") private _markdownElement?: ReactiveElement;
|
||||
|
||||
protected async getUpdateComplete() {
|
||||
const result = await super.getUpdateComplete();
|
||||
await this._markdownElement.updateComplete;
|
||||
await this._markdownElement?.updateComplete;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -70,26 +70,25 @@ export class HaMarkdown extends LitElement {
|
||||
a {
|
||||
color: var(--markdown-link-color, var(--primary-color));
|
||||
}
|
||||
img {
|
||||
background-color: rgba(10, 10, 10, 0.15);
|
||||
border-radius: var(--markdown-image-border-radius);
|
||||
max-width: 100%;
|
||||
min-height: 2lh;
|
||||
:host([assist]) img {
|
||||
height: auto;
|
||||
width: auto;
|
||||
text-indent: 4px;
|
||||
transition: height 0.2s ease-in-out;
|
||||
}
|
||||
img {
|
||||
background-color: var(--markdown-image-background-color);
|
||||
border-radius: var(--markdown-image-border-radius);
|
||||
max-width: 100%;
|
||||
}
|
||||
p:first-child > img:first-child {
|
||||
vertical-align: top;
|
||||
}
|
||||
p:first-child > img:last-child {
|
||||
vertical-align: top;
|
||||
}
|
||||
ol,
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
padding-inline-start: 0;
|
||||
:host > ul,
|
||||
:host > ol {
|
||||
padding-inline-start: var(--markdown-list-indent, revert);
|
||||
}
|
||||
li {
|
||||
&:has(input[type="checkbox"]) {
|
||||
@@ -140,16 +139,19 @@ export class HaMarkdown extends LitElement {
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
border-collapse: var(--markdown-table-border-collapse, collapse);
|
||||
}
|
||||
div:has(> table) {
|
||||
overflow: auto;
|
||||
}
|
||||
th {
|
||||
text-align: start;
|
||||
}
|
||||
td,
|
||||
th {
|
||||
border: 1px solid var(--markdown-table-border-color, transparent);
|
||||
border-width: var(--markdown-table-border-width, 1px);
|
||||
border-style: var(--markdown-table-border-style, solid);
|
||||
border-color: var(--markdown-table-border-color, var(--divider-color));
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
blockquote {
|
||||
|
||||
@@ -14,7 +14,11 @@ import memoizeOne from "memoize-one";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { HaFuse } from "../resources/fuse";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import {
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
} from "../resources/fuseMultiTerm";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -25,11 +29,26 @@ import "./ha-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
export const DEFAULT_SEARCH_KEYS: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "secondary",
|
||||
weight: 7,
|
||||
},
|
||||
{
|
||||
name: "id",
|
||||
weight: 3,
|
||||
},
|
||||
];
|
||||
|
||||
export interface PickerComboBoxItem {
|
||||
id: string;
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
search_labels?: string[];
|
||||
search_labels?: Record<string, string | null>;
|
||||
sorting_label?: string;
|
||||
icon_path?: string;
|
||||
icon?: string;
|
||||
@@ -59,7 +78,7 @@ export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
|
||||
) => T[];
|
||||
|
||||
@customElement("ha-picker-combo-box")
|
||||
export class HaPickerComboBox extends LitElement {
|
||||
export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@@ -76,10 +95,13 @@ export class HaPickerComboBox extends LitElement {
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public searchKeys?: FuseWeightedKey[];
|
||||
|
||||
@state() private _listScrolled = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public getItems?: (
|
||||
public getItems!: (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[];
|
||||
@@ -126,8 +148,14 @@ export class HaPickerComboBox extends LitElement {
|
||||
|
||||
@state() private _items: (PickerComboBoxItem | string)[] = [];
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this._virtualizerElement as HTMLElement | null;
|
||||
}
|
||||
|
||||
@state() private _sectionTitle?: string;
|
||||
|
||||
@state() private _valuePinned = true;
|
||||
|
||||
private _allItems: (PickerComboBoxItem | string)[] = [];
|
||||
|
||||
private _selectedItemIndex = -1;
|
||||
@@ -180,6 +208,7 @@ export class HaPickerComboBox extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="virtualizer-wrapper">
|
||||
<lit-virtualizer
|
||||
.keyFunction=${this._keyFunction}
|
||||
tabindex="0"
|
||||
@@ -188,11 +217,22 @@ export class HaPickerComboBox extends LitElement {
|
||||
.renderItem=${this._renderItem}
|
||||
style="min-height: 36px;"
|
||||
class=${this._listScrolled ? "scrolled" : ""}
|
||||
.layout=${this.value && this._valuePinned
|
||||
? {
|
||||
pin: {
|
||||
index: this._getInitialSelectedIndex(),
|
||||
block: "center",
|
||||
},
|
||||
}
|
||||
: undefined}
|
||||
@unpinned=${this._handleUnpinned}
|
||||
@scroll=${this._onScrollList}
|
||||
@focus=${this._focusList}
|
||||
@visibilityChanged=${this._visibilityChanged}
|
||||
>
|
||||
</lit-virtualizer> `;
|
||||
</lit-virtualizer>
|
||||
${this.renderScrollableFades()}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderSectionButtons() {
|
||||
@@ -236,15 +276,16 @@ export class HaPickerComboBox extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _handleUnpinned() {
|
||||
this._valuePinned = false;
|
||||
}
|
||||
|
||||
private _getAdditionalItems = (searchString?: string) =>
|
||||
this.getAdditionalItems?.(searchString) || [];
|
||||
|
||||
private _getItems = () => {
|
||||
let items = [
|
||||
...(this.getItems
|
||||
? this.getItems(this._search, this.selectedSection)
|
||||
: []),
|
||||
];
|
||||
let items = [...this.getItems(this._search, this.selectedSection)];
|
||||
|
||||
if (!this.sections?.length) {
|
||||
items = items.sort((entityA, entityB) =>
|
||||
@@ -271,6 +312,9 @@ export class HaPickerComboBox extends LitElement {
|
||||
};
|
||||
|
||||
private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
|
||||
if (!item) {
|
||||
return nothing;
|
||||
}
|
||||
if (item === "padding") {
|
||||
return html`<div class="bottom-padding"></div>`;
|
||||
}
|
||||
@@ -331,8 +375,9 @@ export class HaPickerComboBox extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
};
|
||||
|
||||
private _fuseIndex = memoizeOne((states: PickerComboBoxItem[]) =>
|
||||
Fuse.createIndex(["search_labels"], states)
|
||||
private _fuseIndex = memoizeOne(
|
||||
(states: PickerComboBoxItem[], searchKeys?: FuseWeightedKey[]) =>
|
||||
Fuse.createIndex(searchKeys || DEFAULT_SEARCH_KEYS, states)
|
||||
);
|
||||
|
||||
private _filterChanged = (ev: Event) => {
|
||||
@@ -348,33 +393,25 @@ export class HaPickerComboBox extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this._fuseIndex(this._allItems as PickerComboBoxItem[]);
|
||||
const fuse = new HaFuse(
|
||||
const index = this._fuseIndex(
|
||||
this._allItems as PickerComboBoxItem[],
|
||||
{
|
||||
shouldSort: false,
|
||||
minMatchCharLength: Math.min(searchString.length, 2),
|
||||
},
|
||||
this.searchKeys
|
||||
);
|
||||
|
||||
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
|
||||
this._allItems as PickerComboBoxItem[],
|
||||
searchString,
|
||||
this.searchKeys || DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id,
|
||||
index
|
||||
);
|
||||
) as (PickerComboBoxItem | string)[];
|
||||
|
||||
const results = fuse.multiTermsSearch(searchString);
|
||||
let filteredItems = [...this._allItems];
|
||||
|
||||
if (results) {
|
||||
const items: (PickerComboBoxItem | string)[] = results.map(
|
||||
(result) => result.item
|
||||
);
|
||||
|
||||
if (!items.length) {
|
||||
if (!filteredItems.length) {
|
||||
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
|
||||
}
|
||||
|
||||
const additionalItems = this._getAdditionalItems();
|
||||
items.push(...additionalItems);
|
||||
|
||||
filteredItems = items;
|
||||
}
|
||||
filteredItems.push(...additionalItems);
|
||||
|
||||
if (this.searchFn) {
|
||||
filteredItems = this.searchFn(
|
||||
@@ -582,9 +619,29 @@ export class HaPickerComboBox extends LitElement {
|
||||
}
|
||||
|
||||
private _keyFunction = (item: PickerComboBoxItem | string) =>
|
||||
typeof item === "string" ? item : item.id;
|
||||
typeof item === "string" ? item : item?.id;
|
||||
|
||||
static styles = [
|
||||
private _getInitialSelectedIndex() {
|
||||
if (!this._virtualizerElement || !this.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const index = this._virtualizerElement.items.findIndex(
|
||||
(item) =>
|
||||
typeof item !== "string" &&
|
||||
(item as PickerComboBoxItem).id === this.value
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
@@ -617,6 +674,14 @@ export class HaPickerComboBox extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
.virtualizer-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
lit-virtualizer {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -734,6 +799,7 @@ export class HaPickerComboBox extends LitElement {
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiClose, mdiMenuDown } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
@@ -7,8 +8,10 @@ import {
|
||||
type CSSResultGroup,
|
||||
type TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { localizeContext } from "../data/context";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-combo-box-item";
|
||||
import type { HaComboBoxItem } from "./ha-combo-box-item";
|
||||
import "./ha-icon-button";
|
||||
@@ -33,14 +36,26 @@ export class HaPickerField extends LitElement {
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public unknown = false;
|
||||
|
||||
@property({ attribute: "unknown-item-text" }) public unknownItemText?: string;
|
||||
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public valueRenderer?: PickerValueRenderer;
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public invalid = false;
|
||||
|
||||
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: HomeAssistant["localize"];
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
await this.item?.focus();
|
||||
@@ -61,6 +76,12 @@ export class HaPickerField extends LitElement {
|
||||
${this.placeholder}
|
||||
</span>
|
||||
`}
|
||||
${this.unknown
|
||||
? html`<div slot="supporting-text" class="unknown">
|
||||
${this.unknownItemText ||
|
||||
this.localize("ui.components.combo-box.unknown_item")}
|
||||
</div>`
|
||||
: nothing}
|
||||
${showClearIcon
|
||||
? html`
|
||||
<ha-icon-button
|
||||
@@ -103,8 +124,8 @@ export class HaPickerField extends LitElement {
|
||||
--md-list-item-two-line-container-height: 56px;
|
||||
--md-list-item-top-space: 0px;
|
||||
--md-list-item-bottom-space: 0px;
|
||||
--md-list-item-leading-space: 8px;
|
||||
--md-list-item-trailing-space: 8px;
|
||||
--md-list-item-leading-space: var(--ha-space-4);
|
||||
--md-list-item-trailing-space: var(--ha-space-2);
|
||||
--ha-md-list-item-gap: var(--ha-space-2);
|
||||
/* Remove the default focus ring */
|
||||
--md-focus-ring-width: 0px;
|
||||
@@ -142,6 +163,15 @@ export class HaPickerField extends LitElement {
|
||||
background-color: var(--mdc-theme-primary);
|
||||
}
|
||||
|
||||
:host([unknown]) ha-combo-box-item {
|
||||
background-color: var(--ha-color-fill-warning-quiet-resting);
|
||||
}
|
||||
|
||||
:host([invalid]) ha-combo-box-item:after {
|
||||
height: 2px;
|
||||
background-color: var(--mdc-theme-error, var(--error-color, #b00020));
|
||||
}
|
||||
|
||||
.clear {
|
||||
margin: 0 -8px;
|
||||
--mdc-icon-button-size: 32px;
|
||||
@@ -156,6 +186,10 @@ export class HaPickerField extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.unknown {
|
||||
color: var(--ha-color-on-warning-normal);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -9,13 +9,13 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { prepareZXingModule } from "barcode-detector";
|
||||
import type QrScanner from "qr-scanner";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { addExternalBarCodeListener } from "../external_app/external_app_entrypoint";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import "./ha-button";
|
||||
import "./ha-button-menu";
|
||||
import "./ha-list-item";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import type { HaDropdownItem } from "./ha-dropdown-item";
|
||||
import "./ha-spinner";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
@@ -52,6 +52,8 @@ class HaQrScanner extends LitElement {
|
||||
|
||||
@state() private _warning?: string;
|
||||
|
||||
@state() private _selectedCamera?: string;
|
||||
|
||||
private _qrScanner?: QrScanner;
|
||||
|
||||
private _qrNotFoundCount = 0;
|
||||
@@ -121,7 +123,7 @@ class HaQrScanner extends LitElement {
|
||||
!this._error &&
|
||||
this._cameras &&
|
||||
this._cameras.length > 1
|
||||
? html`<ha-button-menu fixed @closed=${stopPropagation}>
|
||||
? html`<ha-dropdown @wa-select=${this._handleDropdownSelect}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
@@ -131,15 +133,17 @@ class HaQrScanner extends LitElement {
|
||||
></ha-icon-button>
|
||||
${this._cameras!.map(
|
||||
(camera) => html`
|
||||
<ha-list-item
|
||||
<ha-dropdown-item
|
||||
.value=${camera.id}
|
||||
@click=${this._cameraChanged}
|
||||
class=${this._selectedCamera === camera.id
|
||||
? "selected"
|
||||
: ""}
|
||||
>
|
||||
${camera.label}
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>`
|
||||
</ha-dropdown>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: html`<ha-alert alert-type="warning">
|
||||
@@ -205,6 +209,9 @@ class HaQrScanner extends LitElement {
|
||||
|
||||
private async _listCameras(qrScanner: typeof QrScanner): Promise<void> {
|
||||
this._cameras = await qrScanner.listCameras(true);
|
||||
if (this._cameras.length > 0) {
|
||||
this._selectedCamera = this._cameras[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
private _qrCodeError = (err: any) => {
|
||||
@@ -252,8 +259,12 @@ class HaQrScanner extends LitElement {
|
||||
this._qrCodeScanned(this._manualInput!.value);
|
||||
}
|
||||
|
||||
private _cameraChanged(ev: CustomEvent): void {
|
||||
this._qrScanner?.setCamera((ev.target as any).value);
|
||||
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
|
||||
const cameraId = ev.detail?.item?.value;
|
||||
if (cameraId) {
|
||||
this._selectedCamera = cameraId;
|
||||
this._qrScanner?.setCamera(cameraId);
|
||||
}
|
||||
}
|
||||
|
||||
private _openExternalScanner() {
|
||||
@@ -359,7 +370,7 @@ class HaQrScanner extends LitElement {
|
||||
#canvas-container {
|
||||
position: relative;
|
||||
}
|
||||
ha-button-menu {
|
||||
ha-icon-button {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
@@ -369,6 +380,9 @@ class HaQrScanner extends LitElement {
|
||||
color: white;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
}
|
||||
ha-dropdown-item.selected {
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
28
src/components/ha-section-title.ts
Normal file
28
src/components/ha-section-title.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-section-title")
|
||||
class HaSectionTitle extends LitElement {
|
||||
protected render() {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--secondary-text-color);
|
||||
min-height: var(--ha-space-6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-section-title": HaSectionTitle;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { NumberSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-picker";
|
||||
import "../ha-button-toggle-group";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-slider";
|
||||
import "../ha-textfield";
|
||||
@@ -18,7 +15,7 @@ export class HaNumberSelector extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public selector!: NumberSelector;
|
||||
|
||||
@property({ type: Number }) public value?: number | string;
|
||||
@property({ type: Number }) public value?: number;
|
||||
|
||||
@property({ type: Number }) public placeholder?: number;
|
||||
|
||||
@@ -33,30 +30,13 @@ export class HaNumberSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _mode: "number" | "entity" = "number";
|
||||
|
||||
private _valueStr = "";
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
if (
|
||||
this.selector.number?.entity?.domains.length &&
|
||||
typeof this.value === "string" &&
|
||||
this.selector.number?.entity?.domains.some((domain) =>
|
||||
(this.value as string).startsWith(`${domain}.`)
|
||||
)
|
||||
) {
|
||||
this._mode = "entity";
|
||||
}
|
||||
}
|
||||
if (changedProps.has("value")) {
|
||||
if (this._valueStr === "" || this.value !== Number(this._valueStr)) {
|
||||
this._valueStr =
|
||||
this.value == null ||
|
||||
typeof this.value === "string" ||
|
||||
isNaN(this.value)
|
||||
? ""
|
||||
: this.value.toString();
|
||||
this.value == null || isNaN(this.value) ? "" : this.value.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,8 +47,6 @@ export class HaNumberSelector extends LitElement {
|
||||
this.selector.number?.min === undefined ||
|
||||
this.selector.number?.max === undefined;
|
||||
|
||||
const multiMode = Boolean(this.selector.number?.entity?.domains.length);
|
||||
|
||||
let sliderStep;
|
||||
|
||||
if (!isBox) {
|
||||
@@ -94,32 +72,11 @@ export class HaNumberSelector extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.label && !isBox && !multiMode
|
||||
${this.label && !isBox
|
||||
? html`${this.label}${this.required ? "*" : ""}`
|
||||
: nothing}
|
||||
${multiMode
|
||||
? html`<div class="multi-header">
|
||||
<span>${this.label}${this.required ? "*" : ""}</span>
|
||||
<ha-button-toggle-group
|
||||
size="small"
|
||||
.buttons=${this._toggleButtons(this.hass.localize)}
|
||||
.active=${this._mode}
|
||||
@value-changed=${this._modeChanged}
|
||||
></ha-button-toggle-group>
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="input">
|
||||
${multiMode && this._mode === "entity"
|
||||
? html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.selector.number!.entity!.domains}
|
||||
.value=${this.value}
|
||||
.placeholder=${this.placeholder}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-entity-picker>`
|
||||
: html`${!isBox
|
||||
${!isBox
|
||||
? html`
|
||||
<ha-slider
|
||||
labeled
|
||||
@@ -130,8 +87,7 @@ export class HaNumberSelector extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@change=${this._handleSliderChange}
|
||||
.withMarkers=${this.selector.number?.slider_ticks ||
|
||||
false}
|
||||
.withMarkers=${this.selector.number?.slider_ticks || false}
|
||||
>
|
||||
</ha-slider>
|
||||
`
|
||||
@@ -158,9 +114,9 @@ export class HaNumberSelector extends LitElement {
|
||||
?no-spinner=${!isBox}
|
||||
@input=${this._handleInputChange}
|
||||
>
|
||||
</ha-textfield>`}
|
||||
</ha-textfield>
|
||||
</div>
|
||||
${!isBox && !(multiMode && this._mode === "entity") && this.helper
|
||||
${!isBox && this.helper
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
@@ -168,22 +124,6 @@ export class HaNumberSelector extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _toggleButtons = memoizeOne((localize: HomeAssistant["localize"]) => [
|
||||
{
|
||||
label: localize("ui.components.selectors.number.value"),
|
||||
value: "number",
|
||||
},
|
||||
{
|
||||
label: localize("ui.components.selectors.number.entity_value"),
|
||||
value: "entity",
|
||||
},
|
||||
]);
|
||||
|
||||
private _modeChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
this._mode = ev.detail?.value || ev.target.value;
|
||||
}
|
||||
|
||||
private _handleInputChange(ev) {
|
||||
ev.stopPropagation();
|
||||
this._valueStr = ev.target.value;
|
||||
@@ -215,32 +155,17 @@ export class HaNumberSelector extends LitElement {
|
||||
}
|
||||
ha-slider {
|
||||
flex: 1;
|
||||
margin-right: var(--ha-space-4);
|
||||
margin-inline-end: var(--ha-space-4);
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
ha-textfield {
|
||||
--ha-textfield-input-width: 40px;
|
||||
}
|
||||
.multi-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.single {
|
||||
--ha-textfield-input-width: unset;
|
||||
flex: 1;
|
||||
}
|
||||
ha-entity-picker {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
ha-button-toggle-group {
|
||||
display: block;
|
||||
justify-self: end;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -465,10 +465,16 @@ export class HaServiceControl extends LitElement {
|
||||
? computeObjectId(this._value.action)
|
||||
: undefined;
|
||||
|
||||
const descriptionPlaceholders =
|
||||
domain && serviceName
|
||||
? this.hass.services[domain]?.[serviceName]?.description_placeholders
|
||||
: undefined;
|
||||
|
||||
const description =
|
||||
(serviceName &&
|
||||
this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.description`
|
||||
`component.${domain}.services.${serviceName}.description`,
|
||||
descriptionPlaceholders
|
||||
)) ||
|
||||
serviceData?.description;
|
||||
|
||||
@@ -537,7 +543,8 @@ export class HaServiceControl extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.value=${this._value?.data?.entity_id}
|
||||
.label=${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.entity_id.description`
|
||||
`component.${domain}.services.${serviceName}.fields.entity_id.description`,
|
||||
descriptionPlaceholders
|
||||
) || entityId.description}
|
||||
@value-changed=${this._entityPicked}
|
||||
allow-custom-entity
|
||||
@@ -575,7 +582,8 @@ export class HaServiceControl extends LitElement {
|
||||
left-chevron
|
||||
.expanded=${!dataField.collapsed}
|
||||
.header=${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name`
|
||||
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name`,
|
||||
descriptionPlaceholders
|
||||
) ||
|
||||
dataField.name ||
|
||||
dataField.key}
|
||||
@@ -611,7 +619,10 @@ export class HaServiceControl extends LitElement {
|
||||
serviceName: string | undefined
|
||||
) {
|
||||
return this.hass!.localize(
|
||||
`component.${domain}.services.${serviceName}.sections.${dataField.key}.description`
|
||||
`component.${domain}.services.${serviceName}.sections.${dataField.key}.description`,
|
||||
domain && serviceName
|
||||
? this.hass.services[domain][serviceName].description_placeholders
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
|
||||
@@ -658,6 +669,10 @@ export class HaServiceControl extends LitElement {
|
||||
}
|
||||
|
||||
const showOptional = showOptionalToggle(dataField);
|
||||
const descriptionPlaceholders =
|
||||
domain && serviceName
|
||||
? this.hass.services[domain][serviceName].description_placeholders
|
||||
: undefined;
|
||||
|
||||
return dataField.selector &&
|
||||
(!dataField.advanced ||
|
||||
@@ -679,7 +694,8 @@ export class HaServiceControl extends LitElement {
|
||||
></ha-checkbox>`}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`,
|
||||
descriptionPlaceholders
|
||||
) ||
|
||||
dataField.name ||
|
||||
dataField.key}</span
|
||||
@@ -689,7 +705,8 @@ export class HaServiceControl extends LitElement {
|
||||
breaks
|
||||
allow-svg
|
||||
.content=${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`,
|
||||
descriptionPlaceholders
|
||||
) || dataField?.description}
|
||||
></ha-markdown>
|
||||
</span>
|
||||
|
||||
@@ -21,6 +21,13 @@ interface ServiceComboBoxItem extends PickerComboBoxItem {
|
||||
service_id?: string;
|
||||
}
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "name", weight: 10 },
|
||||
{ name: "description", weight: 8 },
|
||||
{ name: "domainName", weight: 6 },
|
||||
{ name: "serviceId", weight: 3 },
|
||||
];
|
||||
|
||||
@customElement("ha-service-picker")
|
||||
class HaServicePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -92,8 +99,14 @@ class HaServicePicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
const descriptionPlaceholders =
|
||||
this.hass.services[domain][service].description_placeholders;
|
||||
|
||||
const serviceName =
|
||||
localize(`component.${domain}.services.${service}.name`) ||
|
||||
localize(
|
||||
`component.${domain}.services.${service}.name`,
|
||||
descriptionPlaceholders
|
||||
) ||
|
||||
services[domain][service].name ||
|
||||
service;
|
||||
|
||||
@@ -135,6 +148,10 @@ class HaServicePicker extends LitElement {
|
||||
this.hass.localize,
|
||||
this.hass.services
|
||||
)}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.service-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
@@ -163,16 +180,21 @@ class HaServicePicker extends LitElement {
|
||||
const serviceId = `${domain}.${service}`;
|
||||
const domainName = domainToName(localize, domain);
|
||||
|
||||
const descriptionPlaceholders =
|
||||
this.hass.services[domain][service].description_placeholders;
|
||||
|
||||
const name =
|
||||
this.hass.localize(
|
||||
`component.${domain}.services.${service}.name`
|
||||
`component.${domain}.services.${service}.name`,
|
||||
descriptionPlaceholders
|
||||
) ||
|
||||
services[domain][service].name ||
|
||||
service;
|
||||
|
||||
const description =
|
||||
this.hass.localize(
|
||||
`component.${domain}.services.${service}.description`
|
||||
`component.${domain}.services.${service}.description`,
|
||||
descriptionPlaceholders
|
||||
) ||
|
||||
services[domain][service].description ||
|
||||
"";
|
||||
@@ -183,9 +205,7 @@ class HaServicePicker extends LitElement {
|
||||
secondary: description,
|
||||
domain_name: domainName,
|
||||
service_id: serviceId,
|
||||
search_labels: [serviceId, domainName, name, description].filter(
|
||||
Boolean
|
||||
),
|
||||
search_labels: { serviceId, domainName, name, description },
|
||||
sorting_label: serviceId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -197,6 +197,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _mouseLeaveTimeout?: number;
|
||||
|
||||
private _touchendTimeout?: number;
|
||||
|
||||
private _tooltipHideTimeout?: number;
|
||||
|
||||
private _recentKeydownActiveUntil = 0;
|
||||
@@ -237,6 +239,18 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
// clear timeouts
|
||||
clearTimeout(this._mouseLeaveTimeout);
|
||||
clearTimeout(this._tooltipHideTimeout);
|
||||
clearTimeout(this._touchendTimeout);
|
||||
// set undefined values
|
||||
this._mouseLeaveTimeout = undefined;
|
||||
this._tooltipHideTimeout = undefined;
|
||||
this._touchendTimeout = undefined;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
@@ -406,6 +420,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
class="ha-scrollbar"
|
||||
@focusin=${this._listboxFocusIn}
|
||||
@focusout=${this._listboxFocusOut}
|
||||
@touchend=${this._listboxTouchend}
|
||||
@scroll=${this._listboxScroll}
|
||||
@keydown=${this._listboxKeydown}
|
||||
>
|
||||
@@ -620,6 +635,14 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
this._hideTooltip();
|
||||
}
|
||||
|
||||
private _listboxTouchend() {
|
||||
clearTimeout(this._touchendTimeout);
|
||||
this._touchendTimeout = window.setTimeout(() => {
|
||||
// Allow 1 second for users to read the tooltip on touch devices
|
||||
this._hideTooltip();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
@eventOptions({
|
||||
passive: true,
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { subscribeLabFeatures } from "../data/labs";
|
||||
import { subscribeLabFeature } from "../data/labs";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
|
||||
interface Snowflake {
|
||||
@@ -10,7 +10,7 @@ interface Snowflake {
|
||||
size: number;
|
||||
duration: number;
|
||||
delay: number;
|
||||
blur: number;
|
||||
rotation: number;
|
||||
}
|
||||
|
||||
@customElement("ha-snowflakes")
|
||||
@@ -27,13 +27,14 @@ export class HaSnowflakes extends SubscribeMixin(LitElement) {
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
subscribeLabFeatures(this.hass!.connection, (features) => {
|
||||
this._enabled =
|
||||
features.find(
|
||||
(f) =>
|
||||
f.domain === "frontend" && f.preview_feature === "winter_mode"
|
||||
)?.enabled ?? false;
|
||||
}),
|
||||
subscribeLabFeature(
|
||||
this.hass!.connection,
|
||||
"frontend",
|
||||
"winter_mode",
|
||||
(feature) => {
|
||||
this._enabled = feature.enabled;
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -51,7 +52,7 @@ export class HaSnowflakes extends SubscribeMixin(LitElement) {
|
||||
size: Math.random() * 12 + 8, // Random size between 8-20px
|
||||
duration: Math.random() * 8 + 8, // Random duration between 8-16s
|
||||
delay: Math.random() * 8, // Random delay between 0-8s
|
||||
blur: Math.random() * 1, // Random blur between 0-1px
|
||||
rotation: Math.random() * 720 - 360, // Random starting rotation -360 to 360deg
|
||||
});
|
||||
}
|
||||
this._snowflakes = snowflakes;
|
||||
@@ -75,20 +76,27 @@ export class HaSnowflakes extends SubscribeMixin(LitElement) {
|
||||
<div class="snowflakes ${isDark ? "dark" : "light"}" aria-hidden="true">
|
||||
${this._snowflakes.map(
|
||||
(flake) => html`
|
||||
<div
|
||||
<svg
|
||||
class="snowflake ${this.narrow && flake.id >= 30
|
||||
? "hide-narrow"
|
||||
: ""}"
|
||||
style="
|
||||
left: ${flake.left}%;
|
||||
font-size: ${flake.size}px;
|
||||
width: ${flake.size}px;
|
||||
height: ${flake.size}px;
|
||||
animation-duration: ${flake.duration}s;
|
||||
animation-delay: ${flake.delay}s;
|
||||
filter: blur(${flake.blur}px);
|
||||
--rotation: ${flake.rotation}deg;
|
||||
"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
❄
|
||||
</div>
|
||||
<path
|
||||
d="M7.991 0a.644.644 0 0 1 .283 1.221v2.553l.986-.988a.645.645 0 0 1 .612-.839.644.644 0 1 1-.222 1.247l-1.376 1.38V7.52l1.65-.954.466-1.879a.645.645 0 0 1 .1-1.042.643.643 0 1 1 .445 1.189l-.363 1.356 3.145-1.82a.643.643 0 1 1 .282.49l-2.205 1.277 1.347.361a.643.643 0 1 1-.158.543l-1.88-.505L8.573 8l1.632.945 1.858-.535a.64.64 0 0 1 .95-.434.643.643 0 1 1-.805.98l-1.354.364L14 11.14a.641.641 0 0 1 .914.855.643.643 0 0 1-1.197-.366l-2.205-1.276.36 1.35a.642.642 0 0 1 .419.95.643.643 0 1 1-.967-.816l-.503-1.884L8.273 8.48v1.909l1.39 1.344a.644.644 0 1 1 .208 1.252.644.644 0 0 1-.606-.852l-.991-.994v3.64A.644.644 0 0 1 7.99 16a.644.644 0 0 1-.282-1.221v-2.553l-.986.988a.645.645 0 0 1-.612.839.644.644 0 1 1 .222-1.247l1.376-1.38V8.5l-1.632.945-.467 1.879q.079.068.134.163a.643.643 0 1 1-.68-.31l.364-1.357-3.145 1.82A.643.643 0 1 1 2 11.15l2.205-1.276-1.347-.361a.643.643 0 1 1 .158-.543l1.88.505L7.444 8l-1.65-.954-1.857.534a.64.64 0 0 1-.95.434.643.643 0 1 1 .805-.98l1.354-.364L2 4.85a.641.641 0 0 1-.914-.855.643.643 0 0 1 1.197.366l2.205 1.276-.36-1.35a.642.642 0 0 1-.419-.95.643.643 0 1 1 .967.816l.503 1.884L7.71 7.5V5.611L6.32 4.267a.644.644 0 1 1-.208-1.252.644.644 0 0 1 .607.852l.991.994V1.22A.644.644 0 0 1 7.991 0"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
@@ -128,16 +136,10 @@ export class HaSnowflakes extends SubscribeMixin(LitElement) {
|
||||
|
||||
.light .snowflake {
|
||||
color: #00bcd4;
|
||||
text-shadow:
|
||||
0 0 5px #00bcd4,
|
||||
0 0 10px #00e5ff;
|
||||
}
|
||||
|
||||
.dark .snowflake {
|
||||
color: #fff;
|
||||
text-shadow:
|
||||
0 0 5px rgba(255, 255, 255, 0.8),
|
||||
0 0 10px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.snowflake.hide-narrow {
|
||||
@@ -146,19 +148,23 @@ export class HaSnowflakes extends SubscribeMixin(LitElement) {
|
||||
|
||||
@keyframes fall {
|
||||
0% {
|
||||
transform: translateY(-10vh) translateX(0);
|
||||
transform: translateY(-10vh) translateX(0) rotate(var(--rotation));
|
||||
}
|
||||
25% {
|
||||
transform: translateY(30vh) translateX(10px);
|
||||
transform: translateY(30vh) translateX(10px)
|
||||
rotate(calc(var(--rotation) + 25deg));
|
||||
}
|
||||
50% {
|
||||
transform: translateY(60vh) translateX(-10px);
|
||||
transform: translateY(60vh) translateX(-10px)
|
||||
rotate(calc(var(--rotation) + 50deg));
|
||||
}
|
||||
75% {
|
||||
transform: translateY(85vh) translateX(10px);
|
||||
transform: translateY(85vh) translateX(10px)
|
||||
rotate(calc(var(--rotation) + 75deg));
|
||||
}
|
||||
100% {
|
||||
transform: translateY(120vh) translateX(0);
|
||||
transform: translateY(120vh) translateX(0)
|
||||
rotate(calc(var(--rotation) + 100deg));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,28 +15,46 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { isValidEntityId } from "../common/entity/valid_entity_id";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import {
|
||||
areaFloorComboBoxKeys,
|
||||
getAreasAndFloors,
|
||||
type AreaFloorValue,
|
||||
type FloorComboBoxItem,
|
||||
} from "../data/area_floor";
|
||||
import { getConfigEntries, type ConfigEntry } from "../data/config_entries";
|
||||
import { labelsContext } from "../data/context";
|
||||
import { getDevices, type DevicePickerItem } from "../data/device_registry";
|
||||
import {
|
||||
deviceComboBoxKeys,
|
||||
getDevices,
|
||||
type DevicePickerItem,
|
||||
} from "../data/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../data/entity";
|
||||
import { getEntities, type EntityComboBoxItem } from "../data/entity_registry";
|
||||
import {
|
||||
entityComboBoxKeys,
|
||||
getEntities,
|
||||
type EntityComboBoxItem,
|
||||
} from "../data/entity_registry";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { getLabels, type LabelRegistryEntry } from "../data/label_registry";
|
||||
import {
|
||||
getLabels,
|
||||
labelComboBoxKeys,
|
||||
type LabelRegistryEntry,
|
||||
} from "../data/label_registry";
|
||||
import {
|
||||
areaMeetsFilter,
|
||||
deviceMeetsFilter,
|
||||
entityRegMeetsFilter,
|
||||
getTargetComboBoxItemType,
|
||||
type TargetType,
|
||||
type TargetTypeFloorless,
|
||||
} from "../data/target";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { isHelperDomain } from "../panels/config/helpers/const";
|
||||
import { showHelperDetailDialog } from "../panels/config/helpers/show-dialog-helper-detail";
|
||||
import { HaFuse } from "../resources/fuse";
|
||||
import {
|
||||
multiTermSearch,
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
} from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { brandsUrl } from "../util/brands-url";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
@@ -47,7 +65,6 @@ import "./ha-tree-indicator";
|
||||
import "./target-picker/ha-target-picker-item-group";
|
||||
import "./target-picker/ha-target-picker-value-chip";
|
||||
|
||||
const EMPTY_SEARCH = "___EMPTY_SEARCH___";
|
||||
const SEPARATOR = "________";
|
||||
const CREATE_ID = "___create-new-entity___";
|
||||
|
||||
@@ -113,16 +130,16 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _fuseIndexes = {
|
||||
area: memoizeOne((states: FloorComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
this._createFuseIndex(states, areaFloorComboBoxKeys)
|
||||
),
|
||||
entity: memoizeOne((states: EntityComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
this._createFuseIndex(states, entityComboBoxKeys)
|
||||
),
|
||||
device: memoizeOne((states: DevicePickerItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
this._createFuseIndex(states, deviceComboBoxKeys)
|
||||
),
|
||||
label: memoizeOne((states: PickerComboBoxItem[]) =>
|
||||
this._createFuseIndex(states)
|
||||
this._createFuseIndex(states, labelComboBoxKeys)
|
||||
),
|
||||
};
|
||||
|
||||
@@ -134,8 +151,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _createFuseIndex = (states) =>
|
||||
Fuse.createIndex(["search_labels"], states);
|
||||
private _createFuseIndex = (states, keys: FuseWeightedKey[]) =>
|
||||
Fuse.createIndex(keys, states);
|
||||
|
||||
protected render() {
|
||||
if (this.addOnTop) {
|
||||
@@ -634,35 +651,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _getRowType = (
|
||||
item:
|
||||
| PickerComboBoxItem
|
||||
| (FloorComboBoxItem & { last?: boolean | undefined })
|
||||
| EntityComboBoxItem
|
||||
| DevicePickerItem
|
||||
) => {
|
||||
if (
|
||||
(item as FloorComboBoxItem).type === "area" ||
|
||||
(item as FloorComboBoxItem).type === "floor"
|
||||
) {
|
||||
return (item as FloorComboBoxItem).type;
|
||||
}
|
||||
|
||||
if ("domain" in item) {
|
||||
return "device";
|
||||
}
|
||||
|
||||
if ("stateObj" in item) {
|
||||
return "entity";
|
||||
}
|
||||
|
||||
if (item.id === EMPTY_SEARCH) {
|
||||
return "empty";
|
||||
}
|
||||
|
||||
return "label";
|
||||
};
|
||||
|
||||
private _sectionTitleFunction = ({
|
||||
firstIndex,
|
||||
lastIndex,
|
||||
@@ -686,7 +674,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const type = this._getRowType(firstItem as PickerComboBoxItem);
|
||||
const type = getTargetComboBoxItemType(firstItem as PickerComboBoxItem);
|
||||
const translationType:
|
||||
| "areas"
|
||||
| "entities"
|
||||
@@ -764,8 +752,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
"entity",
|
||||
entityItems,
|
||||
searchTerm,
|
||||
(item: EntityComboBoxItem) =>
|
||||
item.stateObj?.entity_id === searchTerm
|
||||
entityComboBoxKeys
|
||||
) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
@@ -794,7 +781,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
deviceItems = this._filterGroup("device", deviceItems, searchTerm);
|
||||
deviceItems = this._filterGroup(
|
||||
"device",
|
||||
deviceItems,
|
||||
searchTerm,
|
||||
deviceComboBoxKeys
|
||||
);
|
||||
}
|
||||
|
||||
if (!filterType && deviceItems.length) {
|
||||
@@ -828,7 +820,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
areasAndFloors = this._filterGroup(
|
||||
"area",
|
||||
areasAndFloors,
|
||||
searchTerm
|
||||
searchTerm,
|
||||
areaFloorComboBoxKeys,
|
||||
false
|
||||
) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
@@ -858,7 +852,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
if (!filterType || filterType === "label") {
|
||||
let labels = this._getLabelsMemoized(
|
||||
this.hass,
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._labelRegistry,
|
||||
includeDomains,
|
||||
undefined,
|
||||
@@ -870,7 +867,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
if (searchTerm) {
|
||||
labels = this._filterGroup("label", labels, searchTerm);
|
||||
labels = this._filterGroup(
|
||||
"label",
|
||||
labels,
|
||||
searchTerm,
|
||||
labelComboBoxKeys
|
||||
);
|
||||
}
|
||||
|
||||
if (!filterType && labels.length) {
|
||||
@@ -889,40 +891,24 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
type: TargetType,
|
||||
items: (FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem)[],
|
||||
searchTerm: string,
|
||||
checkExact?: (
|
||||
item: FloorComboBoxItem | PickerComboBoxItem | EntityComboBoxItem
|
||||
) => boolean
|
||||
weightedKeys: FuseWeightedKey[],
|
||||
sort = true
|
||||
) {
|
||||
const fuseIndex = this._fuseIndexes[type](items);
|
||||
const fuse = new HaFuse(
|
||||
|
||||
if (sort) {
|
||||
return multiTermSortedSearch(
|
||||
items,
|
||||
{
|
||||
shouldSort: false,
|
||||
minMatchCharLength: Math.min(searchTerm.length, 2),
|
||||
},
|
||||
searchTerm,
|
||||
weightedKeys,
|
||||
(item) => item.id,
|
||||
fuseIndex
|
||||
);
|
||||
|
||||
const results = fuse.multiTermsSearch(searchTerm);
|
||||
let filteredItems = items;
|
||||
if (results) {
|
||||
filteredItems = results.map((result) => result.item);
|
||||
}
|
||||
|
||||
if (!checkExact) {
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
// If there is exact match for entity id, put it first
|
||||
const index = filteredItems.findIndex((item) => checkExact(item));
|
||||
if (index === -1) {
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
const [exactMatch] = filteredItems.splice(index, 1);
|
||||
filteredItems.unshift(exactMatch);
|
||||
|
||||
return filteredItems;
|
||||
return multiTermSearch(items, searchTerm, weightedKeys, fuseIndex, {
|
||||
ignoreLocation: true,
|
||||
});
|
||||
}
|
||||
|
||||
private _getAdditionalItems = () => this._getCreateItems(this.createDomains);
|
||||
@@ -974,7 +960,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const type = this._getRowType(item);
|
||||
const type = getTargetComboBoxItemType(item);
|
||||
let hasFloor = false;
|
||||
let rtl = false;
|
||||
let showEntityId = false;
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
mdiDevices,
|
||||
mdiFormatListBulleted,
|
||||
mdiGestureDoubleTap,
|
||||
mdiHomeAssistant,
|
||||
mdiMapMarker,
|
||||
mdiMapMarkerRadius,
|
||||
mdiMessageAlert,
|
||||
@@ -23,6 +22,7 @@ import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { FALLBACK_DOMAIN_ICONS, triggerIcon } from "../data/icons";
|
||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
@@ -7,9 +9,8 @@ import {
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-dialog-header";
|
||||
@@ -49,10 +50,10 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
* @cssprop --ha-dialog-surface-background - Dialog background color.
|
||||
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface.
|
||||
* @cssprop --dialog-z-index - Z-index for the dialog.
|
||||
* @cssprop --dialog-surface-position - CSS position of the dialog surface.
|
||||
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
|
||||
*
|
||||
* @attr {boolean} open - Controls the dialog open state.
|
||||
* @attr {("alert"|"standard")} type - Dialog type. Defaults to "standard".
|
||||
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset. Defaults to "medium".
|
||||
* @attr {boolean} prevent-scrim-close - Prevents closing the dialog by clicking the scrim/overlay. Defaults to false.
|
||||
* @attr {string} header-title - Header title text. If not set, the headerTitle slot is used.
|
||||
@@ -72,7 +73,7 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
* @see https://github.com/home-assistant/frontend/issues/27143
|
||||
*/
|
||||
@customElement("ha-wa-dialog")
|
||||
export class HaWaDialog extends LitElement {
|
||||
export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "aria-labelledby" })
|
||||
@@ -84,6 +85,9 @@ export class HaWaDialog extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public open = false;
|
||||
|
||||
@property({ reflect: true })
|
||||
public type: "alert" | "standard" = "standard";
|
||||
|
||||
@property({ type: String, reflect: true, attribute: "width" })
|
||||
public width: DialogWidth = "medium";
|
||||
|
||||
@@ -110,6 +114,10 @@ export class HaWaDialog extends LitElement {
|
||||
@state()
|
||||
private _bodyScrolled = false;
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this.bodyContainer;
|
||||
}
|
||||
|
||||
protected updated(
|
||||
changedProperties: Map<string | number | symbol, unknown>
|
||||
): void {
|
||||
@@ -158,9 +166,12 @@ export class HaWaDialog extends LitElement {
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>
|
||||
<div class="content-wrapper">
|
||||
<div class="body ha-scrollbar" @scroll=${this._handleBodyScroll}>
|
||||
<slot></slot>
|
||||
</div>
|
||||
${this.renderScrollableFades()}
|
||||
</div>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</wa-dialog>
|
||||
`;
|
||||
@@ -172,7 +183,9 @@ export class HaWaDialog extends LitElement {
|
||||
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
private _handleAfterShow = () => {
|
||||
@@ -194,21 +207,15 @@ export class HaWaDialog extends LitElement {
|
||||
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
wa-dialog {
|
||||
--full-width: var(
|
||||
--ha-dialog-width-full,
|
||||
min(
|
||||
95vw,
|
||||
calc(
|
||||
100vw - var(--safe-area-inset-left, var(--ha-space-0)) - var(
|
||||
--safe-area-inset-right,
|
||||
var(--ha-space-0)
|
||||
)
|
||||
)
|
||||
)
|
||||
min(95vw, var(--safe-width))
|
||||
);
|
||||
--width: min(var(--ha-dialog-width-md, 580px), var(--full-width));
|
||||
--spacing: var(--dialog-content-padding, var(--ha-space-6));
|
||||
@@ -226,8 +233,7 @@ export class HaWaDialog extends LitElement {
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-3xl)
|
||||
);
|
||||
max-width: var(--ha-dialog-max-width, 100vw);
|
||||
max-width: var(--ha-dialog-max-width, 100svw);
|
||||
max-width: var(--ha-dialog-max-width, var(--safe-width));
|
||||
}
|
||||
|
||||
:host([width="small"]) wa-dialog {
|
||||
@@ -235,7 +241,7 @@ export class HaWaDialog extends LitElement {
|
||||
}
|
||||
|
||||
:host([width="large"]) wa-dialog {
|
||||
--width: min(var(--ha-dialog-width-lg, 720px), var(--full-width));
|
||||
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
|
||||
}
|
||||
|
||||
:host([width="full"]) wa-dialog {
|
||||
@@ -247,34 +253,56 @@ export class HaWaDialog extends LitElement {
|
||||
max-width: var(--width, var(--full-width));
|
||||
max-height: var(
|
||||
--ha-dialog-max-height,
|
||||
calc(100% - var(--ha-space-20))
|
||||
calc(var(--safe-height) - var(--ha-space-20))
|
||||
);
|
||||
min-height: var(--ha-dialog-min-height);
|
||||
position: var(--dialog-surface-position, relative);
|
||||
margin-top: var(--dialog-surface-margin-top, auto);
|
||||
/* Used to offset the dialog from the safe areas when space is limited */
|
||||
transform: translate(
|
||||
calc(
|
||||
var(--safe-area-offset-left, var(--ha-space-0)) - var(
|
||||
--safe-area-offset-right,
|
||||
var(--ha-space-0)
|
||||
)
|
||||
),
|
||||
calc(
|
||||
var(--safe-area-offset-top, var(--ha-space-0)) - var(
|
||||
--safe-area-offset-bottom,
|
||||
var(--ha-space-0)
|
||||
)
|
||||
)
|
||||
);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host {
|
||||
:host([type="standard"]) {
|
||||
--ha-dialog-border-radius: var(--ha-space-0);
|
||||
}
|
||||
|
||||
wa-dialog {
|
||||
/* Make the container fill the whole screen width and not the safe width */
|
||||
--full-width: var(--ha-dialog-width-full, 100vw);
|
||||
--width: var(--full-width);
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
/* Make the dialog fill the whole screen height and not the safe height */
|
||||
min-height: var(--ha-dialog-min-height, 100vh);
|
||||
min-height: var(--ha-dialog-min-height, 100svh);
|
||||
min-height: var(--ha-dialog-min-height, 100dvh);
|
||||
max-height: var(--ha-dialog-max-height, 100vh);
|
||||
max-height: var(--ha-dialog-max-height, 100svh);
|
||||
padding-top: var(--safe-area-inset-top, var(--ha-space-0));
|
||||
padding-bottom: var(--safe-area-inset-bottom, var(--ha-space-0));
|
||||
padding-left: var(--safe-area-inset-left, var(--ha-space-0));
|
||||
padding-right: var(--safe-area-inset-right, var(--ha-space-0));
|
||||
max-height: var(--ha-dialog-max-height, 100dvh);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
/* Use safe area as padding instead of the container size */
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
/* Reset the transform to center the dialog */
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,11 +341,20 @@ export class HaWaDialog extends LitElement {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: var(--dialog-content-position, relative);
|
||||
padding: 0 var(--dialog-content-padding, var(--ha-space-6))
|
||||
var(--dialog-content-padding, var(--ha-space-6))
|
||||
var(--dialog-content-padding, var(--ha-space-6));
|
||||
padding: var(
|
||||
--dialog-content-padding,
|
||||
0 var(--ha-space-6) var(--ha-space-6) var(--ha-space-6)
|
||||
);
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
@@ -343,6 +380,7 @@ export class HaWaDialog extends LitElement {
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { property } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-state-icon";
|
||||
|
||||
@customElement("ha-entity-marker")
|
||||
class HaEntityMarker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@@ -89,8 +90,6 @@ class HaEntityMarker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
customElements.define("ha-entity-marker", HaEntityMarker);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-entity-marker": HaEntityMarker;
|
||||
|
||||
@@ -17,6 +17,12 @@ interface UserComboBoxItem extends PickerComboBoxItem {
|
||||
user?: User;
|
||||
}
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "primary", weight: 10 },
|
||||
{ name: "search_labels.username", weight: 6 },
|
||||
{ name: "id", weight: 3 },
|
||||
];
|
||||
|
||||
@customElement("ha-user-picker")
|
||||
class HaUserPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -109,9 +115,7 @@ class HaUserPicker extends LitElement {
|
||||
id: user.id,
|
||||
primary: user.name,
|
||||
domain_name: user.name,
|
||||
search_labels: [user.name, user.id, user.username].filter(
|
||||
Boolean
|
||||
) as string[],
|
||||
search_labels: { username: user.username },
|
||||
sorting_label: user.name,
|
||||
user,
|
||||
}));
|
||||
@@ -134,6 +138,10 @@ class HaUserPicker extends LitElement {
|
||||
.getItems=${this._getItems}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.user-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import {
|
||||
@@ -21,11 +22,53 @@ export interface FloorComboBoxItem extends PickerComboBoxItem {
|
||||
area?: AreaRegistryEntry;
|
||||
}
|
||||
|
||||
export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
|
||||
floor?: FloorRegistryEntry;
|
||||
areas: FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
export interface UnassignedAreasFloorComboBoxItem {
|
||||
id: undefined;
|
||||
areas: FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
export interface AreaFloorValue {
|
||||
id: string;
|
||||
type: "floor" | "area";
|
||||
}
|
||||
|
||||
export const getAreasNestedInFloors = (
|
||||
states: HomeAssistant["states"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
formatId: (value: AreaFloorValue) => string,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
excludeFloors?: string[]
|
||||
) =>
|
||||
getAreasAndFloorsItems(
|
||||
states,
|
||||
haFloors,
|
||||
haAreas,
|
||||
haDevices,
|
||||
haEntities,
|
||||
formatId,
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeAreas,
|
||||
excludeFloors,
|
||||
true
|
||||
) as (FloorNestedComboBoxItem | UnassignedAreasFloorComboBoxItem)[];
|
||||
|
||||
export const getAreasAndFloors = (
|
||||
states: HomeAssistant["states"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
@@ -40,7 +83,66 @@ export const getAreasAndFloors = (
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
excludeFloors?: string[]
|
||||
): FloorComboBoxItem[] => {
|
||||
) =>
|
||||
getAreasAndFloorsItems(
|
||||
states,
|
||||
haFloors,
|
||||
haAreas,
|
||||
haDevices,
|
||||
haEntities,
|
||||
formatId,
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeAreas,
|
||||
excludeFloors
|
||||
) as FloorComboBoxItem[];
|
||||
|
||||
export const areaFloorComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "search_labels.name",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "search_labels.aliases",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "search_labels.floorName",
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
name: "search_labels.relatedAreas",
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
name: "search_labels.id",
|
||||
weight: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const getAreasAndFloorsItems = (
|
||||
states: HomeAssistant["states"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
formatId: (value: AreaFloorValue) => string,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
excludeFloors?: string[],
|
||||
nested = false
|
||||
): (
|
||||
| FloorComboBoxItem
|
||||
| FloorNestedComboBoxItem
|
||||
| UnassignedAreasFloorComboBoxItem
|
||||
)[] => {
|
||||
const floors = Object.values(haFloors);
|
||||
const areas = Object.values(haAreas);
|
||||
const devices = Object.values(haDevices);
|
||||
@@ -146,6 +248,7 @@ export const getAreasAndFloors = (
|
||||
}
|
||||
|
||||
let outputAreas = areas;
|
||||
let outputFloors = floors;
|
||||
|
||||
let areaIds: string[] | undefined;
|
||||
|
||||
@@ -177,11 +280,35 @@ export const getAreasAndFloors = (
|
||||
outputAreas = outputAreas.filter(
|
||||
(area) => !area.floor_id || !excludeFloors!.includes(area.floor_id)
|
||||
);
|
||||
|
||||
outputFloors = outputFloors.filter(
|
||||
(floor) => !excludeFloors!.includes(floor.floor_id)
|
||||
);
|
||||
}
|
||||
|
||||
const hierarchy = getAreasFloorHierarchy(floors, outputAreas);
|
||||
if (
|
||||
entityFilter ||
|
||||
deviceFilter ||
|
||||
includeDomains ||
|
||||
excludeDomains ||
|
||||
includeDeviceClasses
|
||||
) {
|
||||
// Ensure we only include floors that have areas with the filtered entities/devices
|
||||
const validFloorIds = new Set(
|
||||
outputAreas.map((area) => area.floor_id).filter((id) => id)
|
||||
);
|
||||
outputFloors = outputFloors.filter((floor) =>
|
||||
validFloorIds.has(floor.floor_id)
|
||||
);
|
||||
}
|
||||
|
||||
const items: FloorComboBoxItem[] = [];
|
||||
const hierarchy = getAreasFloorHierarchy(outputFloors, outputAreas);
|
||||
|
||||
const items: (
|
||||
| FloorComboBoxItem
|
||||
| FloorNestedComboBoxItem
|
||||
| UnassignedAreasFloorComboBoxItem
|
||||
)[] = [];
|
||||
|
||||
hierarchy.floors.forEach((f) => {
|
||||
const floor = haFloors[f.id];
|
||||
@@ -196,22 +323,23 @@ export const getAreasAndFloors = (
|
||||
})
|
||||
.flat();
|
||||
|
||||
items.push({
|
||||
const floorItem: FloorComboBoxItem | FloorNestedComboBoxItem = {
|
||||
id: formatId({ id: floor.floor_id, type: "floor" }),
|
||||
type: "floor",
|
||||
primary: floorName,
|
||||
floor: floor,
|
||||
icon: floor.icon || undefined,
|
||||
search_labels: [
|
||||
floor.floor_id,
|
||||
floorName,
|
||||
...floor.aliases,
|
||||
...areaSearchLabels,
|
||||
],
|
||||
});
|
||||
search_labels: {
|
||||
id: floor.floor_id,
|
||||
name: floorName || null,
|
||||
aliases: floor.aliases.join(", ") || null,
|
||||
relatedAreas: areaSearchLabels.join(" ") || null,
|
||||
},
|
||||
};
|
||||
|
||||
items.push(
|
||||
...floorAreas.map((area) => {
|
||||
items.push(floorItem);
|
||||
|
||||
const floorAreasItems = floorAreas.map((area) => {
|
||||
const areaName = computeAreaName(area);
|
||||
return {
|
||||
id: formatId({ id: area.area_id, type: "area" }),
|
||||
@@ -219,30 +347,47 @@ export const getAreasAndFloors = (
|
||||
primary: areaName || area.area_id,
|
||||
area: area,
|
||||
icon: area.icon || undefined,
|
||||
search_labels: [
|
||||
area.area_id,
|
||||
...(areaName ? [areaName] : []),
|
||||
...area.aliases,
|
||||
],
|
||||
search_labels: {
|
||||
id: area.area_id,
|
||||
name: areaName || null,
|
||||
aliases: area.aliases.join(", ") || null,
|
||||
floorName: floorName || null,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
items.push(
|
||||
...hierarchy.areas.map((areaId) => {
|
||||
if (nested && floor) {
|
||||
(floorItem as unknown as FloorNestedComboBoxItem).areas = floorAreasItems;
|
||||
} else {
|
||||
items.push(...floorAreasItems);
|
||||
}
|
||||
});
|
||||
|
||||
const unassignedAreaItems = hierarchy.areas.map((areaId) => {
|
||||
const area = haAreas[areaId];
|
||||
const areaName = computeAreaName(area) || area.area_id;
|
||||
const areaName = computeAreaName(area);
|
||||
return {
|
||||
id: formatId({ id: area.area_id, type: "area" }),
|
||||
type: "area" as const,
|
||||
primary: areaName,
|
||||
primary: areaName || area.area_id,
|
||||
area: area,
|
||||
icon: area.icon || undefined,
|
||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||
search_labels: {
|
||||
id: area.area_id,
|
||||
name: areaName || null,
|
||||
aliases: area.aliases.join(", ") || null,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (nested && unassignedAreaItems.length) {
|
||||
items.push({
|
||||
id: undefined,
|
||||
areas: unassignedAreaItems,
|
||||
} as UnassignedAreasFloorComboBoxItem);
|
||||
} else {
|
||||
items.push(...unassignedAreaItems);
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { DeviceRegistryEntry } from "./device_registry";
|
||||
import type { EntityRegistryEntry } from "./entity_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "./entity_registry";
|
||||
import type { RegistryEntry } from "./registry";
|
||||
|
||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||
@@ -18,7 +20,10 @@ export interface AreaRegistryEntry extends RegistryEntry {
|
||||
temperature_entity_id: string | null;
|
||||
}
|
||||
|
||||
export type AreaEntityLookup = Record<string, EntityRegistryEntry[]>;
|
||||
export type AreaEntityLookup = Record<
|
||||
string,
|
||||
(EntityRegistryEntry | EntityRegistryDisplayEntry)[]
|
||||
>;
|
||||
|
||||
export type AreaDeviceLookup = Record<string, DeviceRegistryEntry[]>;
|
||||
|
||||
@@ -69,7 +74,7 @@ export const reorderAreaRegistryEntries = (
|
||||
});
|
||||
|
||||
export const getAreaEntityLookup = (
|
||||
entities: EntityRegistryEntry[]
|
||||
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[]
|
||||
): AreaEntityLookup => {
|
||||
const areaEntityLookup: AreaEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
@@ -99,22 +104,3 @@ export const getAreaDeviceLookup = (
|
||||
}
|
||||
return areaDeviceLookup;
|
||||
};
|
||||
|
||||
export const areaCompare =
|
||||
(entries?: HomeAssistant["areas"], order?: string[]) =>
|
||||
(a: string, b: string) => {
|
||||
const indexA = order ? order.indexOf(a) : -1;
|
||||
const indexB = order ? order.indexOf(b) : -1;
|
||||
if (indexA === -1 && indexB === -1) {
|
||||
const nameA = entries?.[a]?.name ?? a;
|
||||
const nameB = entries?.[b]?.name ?? b;
|
||||
return stringCompare(nameA, nameB);
|
||||
}
|
||||
if (indexA === -1) {
|
||||
return 1;
|
||||
}
|
||||
if (indexB === -1) {
|
||||
return -1;
|
||||
}
|
||||
return indexA - indexB;
|
||||
};
|
||||
|
||||
@@ -114,12 +114,6 @@ export interface StateTrigger extends BaseTrigger {
|
||||
for?: string | number | ForDict;
|
||||
}
|
||||
|
||||
export interface MqttTrigger extends BaseTrigger {
|
||||
trigger: "mqtt";
|
||||
topic: string;
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
export interface GeoLocationTrigger extends BaseTrigger {
|
||||
trigger: "geo_location";
|
||||
source: string;
|
||||
@@ -127,6 +121,12 @@ export interface GeoLocationTrigger extends BaseTrigger {
|
||||
event: "enter" | "leave";
|
||||
}
|
||||
|
||||
export interface MqttTrigger extends BaseTrigger {
|
||||
trigger: "mqtt";
|
||||
topic: string;
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
export interface HassTrigger extends BaseTrigger {
|
||||
trigger: "homeassistant";
|
||||
event: "start" | "shutdown";
|
||||
|
||||
@@ -144,9 +144,7 @@ const tryDescribeTrigger = (
|
||||
const type = getTriggerObjectId(trigger.trigger);
|
||||
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${domain}.triggers.${type}.description_configured`
|
||||
) ||
|
||||
hass.localize(`component.${domain}.triggers.${type}.name`) ||
|
||||
hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.${triggerType as LegacyTrigger["trigger"]}.label`
|
||||
) ||
|
||||
@@ -919,9 +917,7 @@ const tryDescribeCondition = (
|
||||
const type = getConditionObjectId(condition.condition);
|
||||
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${domain}.conditions.${type}.description_configured`
|
||||
) ||
|
||||
hass.localize(`component.${domain}.conditions.${type}.name`) ||
|
||||
hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${conditionType as LegacyCondition["condition"]}.label`
|
||||
) ||
|
||||
|
||||
@@ -137,8 +137,12 @@ const getCalendarDate = (dateObj: any): string | undefined => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getCalendars = (hass: HomeAssistant): Calendar[] =>
|
||||
Object.keys(hass.states)
|
||||
export const getCalendars = (
|
||||
hass: HomeAssistant,
|
||||
element: Element
|
||||
): Calendar[] => {
|
||||
const computedStyles = getComputedStyle(element);
|
||||
return Object.keys(hass.states)
|
||||
.filter(
|
||||
(eid) =>
|
||||
computeDomain(eid) === "calendar" &&
|
||||
@@ -149,8 +153,9 @@ export const getCalendars = (hass: HomeAssistant): Calendar[] =>
|
||||
.map((eid, idx) => ({
|
||||
...hass.states[eid],
|
||||
name: computeStateName(hass.states[eid]),
|
||||
backgroundColor: getColorByIndex(idx),
|
||||
backgroundColor: getColorByIndex(idx, computedStyles),
|
||||
}));
|
||||
};
|
||||
|
||||
export const createCalendarEvent = (
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -17,11 +17,19 @@ export const CONDITION_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey:
|
||||
"ui.panel.config.automation.editor.conditions.groups.helpers.label",
|
||||
groups: {
|
||||
helpers: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey: "ui.panel.config.automation.editor.conditions.groups.other.label",
|
||||
groups: {
|
||||
template: {},
|
||||
trigger: {},
|
||||
other: {},
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getDeviceContext } from "../common/entity/context/get_device_context";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { ConfigEntry } from "./config_entries";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||
@@ -50,7 +51,11 @@ export type DeviceEntityDisplayLookup = Record<
|
||||
EntityRegistryDisplayEntry[]
|
||||
>;
|
||||
|
||||
export type DeviceEntityLookup = Record<string, EntityRegistryEntry[]>;
|
||||
export type DeviceEntityLookup<
|
||||
T extends EntityRegistryEntry | EntityRegistryDisplayEntry =
|
||||
| EntityRegistryEntry
|
||||
| EntityRegistryDisplayEntry,
|
||||
> = Record<string, T[]>;
|
||||
|
||||
export interface DeviceRegistryEntryMutableParams {
|
||||
area_id?: string | null;
|
||||
@@ -107,7 +112,7 @@ export const sortDeviceRegistryByName = (
|
||||
);
|
||||
|
||||
export const getDeviceEntityLookup = (
|
||||
entities: EntityRegistryEntry[]
|
||||
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[]
|
||||
): DeviceEntityLookup => {
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
@@ -177,6 +182,25 @@ export interface DevicePickerItem extends PickerComboBoxItem {
|
||||
domain_name?: string;
|
||||
}
|
||||
|
||||
export const deviceComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "search_labels.deviceName",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "search_labels.areaName",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "search_labels.domainName",
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
name: "search_labels.domain",
|
||||
weight: 4,
|
||||
},
|
||||
];
|
||||
|
||||
export const getDevices = (
|
||||
hass: HomeAssistant,
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
@@ -307,9 +331,12 @@ export const getDevices = (
|
||||
secondary: areaName,
|
||||
domain: configEntry?.domain,
|
||||
domain_name: domainName,
|
||||
search_labels: [deviceName, areaName, domain, domainName].filter(
|
||||
Boolean
|
||||
) as string[],
|
||||
search_labels: {
|
||||
deviceName,
|
||||
areaName: areaName || null,
|
||||
domain: domain || null,
|
||||
domainName: domainName || null,
|
||||
},
|
||||
sorting_label: deviceName || "zzz",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
isLastDayOfMonth,
|
||||
addYears,
|
||||
} from "date-fns";
|
||||
import type { Collection } from "home-assistant-js-websocket";
|
||||
import type { Collection, HassEntity } from "home-assistant-js-websocket";
|
||||
import { getCollection } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
@@ -1361,3 +1361,37 @@ export const calculateSolarConsumedGauge = (
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current power value from entity state, normalized to kW
|
||||
* @param stateObj - The entity state object to get power value from
|
||||
* @returns Power value in kW, or 0 if entity not found or invalid
|
||||
*/
|
||||
export const getPowerFromState = (stateObj: HassEntity): number | undefined => {
|
||||
if (!stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
const value = parseFloat(stateObj.state);
|
||||
if (isNaN(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Normalize to kW based on unit of measurement (case-sensitive)
|
||||
// Supported units: GW, kW, MW, mW, TW, W
|
||||
const unit = stateObj.attributes.unit_of_measurement;
|
||||
switch (unit) {
|
||||
case "W":
|
||||
return value / 1000;
|
||||
case "mW":
|
||||
return value / 1000000;
|
||||
case "MW":
|
||||
return value * 1000;
|
||||
case "GW":
|
||||
return value * 1000000;
|
||||
case "TW":
|
||||
return value * 1000000000;
|
||||
default:
|
||||
// Assume kW if no unit or unit is kW
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||
import { domainToName } from "./integration";
|
||||
@@ -335,6 +336,33 @@ export interface EntityComboBoxItem extends PickerComboBoxItem {
|
||||
stateObj?: HassEntity;
|
||||
}
|
||||
|
||||
export const entityComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "search_labels.entityName",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "search_labels.friendlyName",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "search_labels.deviceName",
|
||||
weight: 7,
|
||||
},
|
||||
{
|
||||
name: "search_labels.areaName",
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
name: "search_labels.domainName",
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
name: "search_labels.entityId",
|
||||
weight: 3,
|
||||
},
|
||||
];
|
||||
|
||||
export const getEntities = (
|
||||
hass: HomeAssistant,
|
||||
includeDomains?: string[],
|
||||
@@ -403,14 +431,14 @@ export const getEntities = (
|
||||
secondary: secondary,
|
||||
domain_name: domainName,
|
||||
sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
|
||||
search_labels: [
|
||||
entityName,
|
||||
deviceName,
|
||||
areaName,
|
||||
domainName,
|
||||
friendlyName,
|
||||
entityId,
|
||||
].filter(Boolean) as string[],
|
||||
search_labels: {
|
||||
entityName: entityName || null,
|
||||
deviceName: deviceName || null,
|
||||
areaName: areaName || null,
|
||||
domainName: domainName || null,
|
||||
friendlyName: friendlyName || null,
|
||||
entityId: entityId,
|
||||
},
|
||||
stateObj: stateObj,
|
||||
};
|
||||
});
|
||||
|
||||
14
src/data/esphome.ts
Normal file
14
src/data/esphome.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface ESPHomeEncryptionKey {
|
||||
encryption_key: string;
|
||||
}
|
||||
|
||||
export const fetchESPHomeEncryptionKey = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string
|
||||
): Promise<ESPHomeEncryptionKey> =>
|
||||
hass.callWS({
|
||||
type: "esphome/get_encryption_key",
|
||||
entry_id,
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import type { RegistryEntry } from "./registry";
|
||||
@@ -75,27 +74,3 @@ export const getFloorAreaLookup = (
|
||||
}
|
||||
return floorAreaLookup;
|
||||
};
|
||||
|
||||
export const floorCompare =
|
||||
(entries?: HomeAssistant["floors"], order?: string[]) =>
|
||||
(a: string, b: string) => {
|
||||
const indexA = order ? order.indexOf(a) : -1;
|
||||
const indexB = order ? order.indexOf(b) : -1;
|
||||
if (indexA === -1 && indexB === -1) {
|
||||
const floorA = entries?.[a];
|
||||
const floorB = entries?.[b];
|
||||
if (floorA && floorB && floorA.level !== floorB.level) {
|
||||
return (floorB.level ?? -9999) - (floorA.level ?? -9999);
|
||||
}
|
||||
const nameA = floorA?.name ?? a;
|
||||
const nameB = floorB?.name ?? b;
|
||||
return stringCompare(nameA, nameB);
|
||||
}
|
||||
if (indexA === -1) {
|
||||
return 1;
|
||||
}
|
||||
if (indexB === -1) {
|
||||
return -1;
|
||||
}
|
||||
return indexA - indexB;
|
||||
};
|
||||
|
||||
@@ -47,8 +47,7 @@ export interface HassioFullBackupCreateParams {
|
||||
confirm_password?: string;
|
||||
background?: boolean;
|
||||
}
|
||||
export interface HassioPartialBackupCreateParams
|
||||
extends HassioFullBackupCreateParams {
|
||||
export interface HassioPartialBackupCreateParams extends HassioFullBackupCreateParams {
|
||||
folders?: string[];
|
||||
addons?: string[];
|
||||
homeassistant?: boolean;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export const integrationsWithPanel = {
|
||||
bluetooth: "config/bluetooth",
|
||||
@@ -25,6 +25,8 @@ export type IntegrationType =
|
||||
| "entity"
|
||||
| "system";
|
||||
|
||||
export type DomainManifestLookup = Record<string, IntegrationManifest>;
|
||||
|
||||
export interface IntegrationManifest {
|
||||
is_built_in: boolean;
|
||||
overwrites_built_in?: boolean;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { stringCompare } from "../common/string/compare";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import {
|
||||
getDeviceEntityDisplayLookup,
|
||||
@@ -100,8 +101,26 @@ export const deleteLabelRegistryEntry = (
|
||||
label_id: labelId,
|
||||
});
|
||||
|
||||
export const labelComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "search_labels.name",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "search_labels.description",
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
name: "search_labels.id",
|
||||
weight: 4,
|
||||
},
|
||||
];
|
||||
|
||||
export const getLabels = (
|
||||
hass: HomeAssistant,
|
||||
hassStates: HomeAssistant["states"],
|
||||
hassAreas: HomeAssistant["areas"],
|
||||
hassDevices: HomeAssistant["devices"],
|
||||
hassEntities: HomeAssistant["entities"],
|
||||
labels?: LabelRegistryEntry[],
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
@@ -115,8 +134,8 @@ export const getLabels = (
|
||||
return [];
|
||||
}
|
||||
|
||||
const devices = Object.values(hass.devices);
|
||||
const entities = Object.values(hass.entities);
|
||||
const devices = Object.values(hassDevices);
|
||||
const entities = Object.values(hassEntities);
|
||||
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
@@ -170,7 +189,7 @@ export const getLabels = (
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
@@ -181,8 +200,9 @@ export const getLabels = (
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
return (
|
||||
stateObj &&
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
@@ -200,7 +220,7 @@ export const getLabels = (
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
@@ -208,7 +228,7 @@ export const getLabels = (
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
@@ -245,8 +265,8 @@ export const getLabels = (
|
||||
|
||||
if (areaIds) {
|
||||
areaIds.forEach((areaId) => {
|
||||
const area = hass.areas[areaId];
|
||||
area.labels.forEach((label) => usedLabels.add(label));
|
||||
const area = hassAreas[areaId];
|
||||
area?.labels.forEach((label) => usedLabels.add(label));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -269,9 +289,11 @@ export const getLabels = (
|
||||
icon: label.icon || undefined,
|
||||
icon_path: label.icon ? undefined : mdiLabel,
|
||||
sorting_label: label.name,
|
||||
search_labels: [label.name, label.label_id, label.description].filter(
|
||||
(v): v is string => Boolean(v)
|
||||
),
|
||||
search_labels: {
|
||||
name: label.name,
|
||||
description: label.description,
|
||||
id: label.label_id,
|
||||
},
|
||||
}));
|
||||
|
||||
return items;
|
||||
|
||||
@@ -18,6 +18,11 @@ export interface LabPreviewFeaturesResponse {
|
||||
features: LabPreviewFeature[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all lab features
|
||||
* @param hass - The Home Assistant instance
|
||||
* @returns A promise to fetch the lab features
|
||||
*/
|
||||
export const fetchLabFeatures = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<LabPreviewFeature[]> => {
|
||||
@@ -27,6 +32,15 @@ export const fetchLabFeatures = async (
|
||||
return response.features;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a specific lab feature
|
||||
* @param hass - The Home Assistant instance
|
||||
* @param domain - The domain of the lab feature
|
||||
* @param preview_feature - The preview feature of the lab feature
|
||||
* @param enabled - Whether the lab feature is enabled
|
||||
* @param create_backup - Whether to create a backup of the lab feature
|
||||
* @returns A promise to update the lab feature
|
||||
*/
|
||||
export const labsUpdatePreviewFeature = (
|
||||
hass: HomeAssistant,
|
||||
domain: string,
|
||||
@@ -65,6 +79,12 @@ const subscribeLabUpdates = (
|
||||
"labs_updated"
|
||||
);
|
||||
|
||||
/**
|
||||
* Subscribe to a collection of lab features
|
||||
* @param conn - The connection to the Home Assistant instance
|
||||
* @param onChange - The function to call when the lab features change
|
||||
* @returns The unsubscribe function
|
||||
*/
|
||||
export const subscribeLabFeatures = (
|
||||
conn: Connection,
|
||||
onChange: (features: LabPreviewFeature[]) => void
|
||||
@@ -76,3 +96,23 @@ export const subscribeLabFeatures = (
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
/**
|
||||
* Subscribe to a specific lab feature
|
||||
* @param conn - The connection to the Home Assistant instance
|
||||
* @param domain - The domain of the lab feature
|
||||
* @param previewFeature - The preview feature identifier
|
||||
* @param onChange - The function to call when the lab feature changes
|
||||
* @returns A promise that resolves to the unsubscribe function
|
||||
*/
|
||||
export const subscribeLabFeature = (
|
||||
conn: Connection,
|
||||
domain: string,
|
||||
previewFeature: string,
|
||||
onChange: (feature: LabPreviewFeature) => void
|
||||
): Promise<() => void> =>
|
||||
conn.subscribeMessage<LabPreviewFeature>(onChange, {
|
||||
type: "labs/subscribe",
|
||||
domain,
|
||||
preview_feature: previewFeature,
|
||||
});
|
||||
|
||||
@@ -18,8 +18,7 @@ export const enum LawnMowerEntityFeature {
|
||||
}
|
||||
|
||||
interface LawnMowerEntityAttributes
|
||||
extends HassEntityAttributeBase,
|
||||
Record<string, any> {}
|
||||
extends HassEntityAttributeBase, Record<string, any> {}
|
||||
|
||||
export interface LawnMowerEntity extends HassEntityBase {
|
||||
attributes: LawnMowerEntityAttributes;
|
||||
|
||||
@@ -18,8 +18,7 @@ export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
|
||||
cards?: LovelaceCardConfig[];
|
||||
}
|
||||
|
||||
export interface LovelaceStrategySectionConfig
|
||||
extends LovelaceBaseSectionConfig {
|
||||
export interface LovelaceStrategySectionConfig extends LovelaceBaseSectionConfig {
|
||||
strategy: LovelaceStrategyConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ export interface LovelaceConfig extends LovelaceDashboardBaseConfig {
|
||||
views: LovelaceViewRawConfig[];
|
||||
}
|
||||
|
||||
export interface LovelaceDashboardStrategyConfig
|
||||
extends LovelaceDashboardBaseConfig {
|
||||
export interface LovelaceDashboardStrategyConfig extends LovelaceDashboardBaseConfig {
|
||||
strategy: LovelaceStrategyConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { MediaSelectorValue } from "../../selector";
|
||||
import type { LovelaceBadgeConfig } from "./badge";
|
||||
import type { LovelaceCardConfig } from "./card";
|
||||
import type { LovelaceSectionRawConfig } from "./section";
|
||||
import type {
|
||||
LovelaceSectionConfig,
|
||||
LovelaceSectionRawConfig,
|
||||
} from "./section";
|
||||
import type { LovelaceStrategyConfig } from "./strategy";
|
||||
|
||||
export interface ShowViewConfig {
|
||||
@@ -33,6 +36,12 @@ export interface LovelaceViewHeaderConfig {
|
||||
badges_wrap?: "wrap" | "scroll";
|
||||
}
|
||||
|
||||
export interface LovelaceViewSidebarConfig {
|
||||
sections?: LovelaceSectionConfig[];
|
||||
content_label?: string;
|
||||
sidebar_label?: string;
|
||||
}
|
||||
|
||||
export interface LovelaceBaseViewConfig {
|
||||
index?: number;
|
||||
title?: string;
|
||||
@@ -56,6 +65,8 @@ export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
|
||||
cards?: LovelaceCardConfig[];
|
||||
sections?: LovelaceSectionRawConfig[];
|
||||
header?: LovelaceViewHeaderConfig;
|
||||
// Only used for section view, it should move to a section view config type when the views will have dedicated editor.
|
||||
sidebar?: LovelaceViewSidebarConfig;
|
||||
}
|
||||
|
||||
export interface LovelaceStrategyViewConfig extends LovelaceBaseViewConfig {
|
||||
|
||||
@@ -29,8 +29,7 @@ export interface LovelaceDashboardMutableParams {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface LovelaceDashboardCreateParams
|
||||
extends LovelaceDashboardMutableParams {
|
||||
export interface LovelaceDashboardCreateParams extends LovelaceDashboardMutableParams {
|
||||
url_path: string;
|
||||
mode: "storage";
|
||||
}
|
||||
|
||||
@@ -219,9 +219,13 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
|
||||
if (config.action) {
|
||||
const [domain, serviceName] = config.action.split(".", 2);
|
||||
const descriptionPlaceholders =
|
||||
hass.services[domain]?.[serviceName]?.description_placeholders;
|
||||
const service =
|
||||
hass.localize(`component.${domain}.services.${serviceName}.name`) ||
|
||||
hass.services[domain][serviceName]?.name;
|
||||
hass.localize(
|
||||
`component.${domain}.services.${serviceName}.name`,
|
||||
descriptionPlaceholders
|
||||
) || hass.services[domain]?.[serviceName]?.name;
|
||||
|
||||
if (config.metadata) {
|
||||
return hass.localize(
|
||||
|
||||
@@ -332,7 +332,6 @@ export interface NumberSelector {
|
||||
max?: number;
|
||||
step?: number | "any";
|
||||
mode?: "box" | "slider";
|
||||
entity?: { domains: readonly string[] };
|
||||
unit_of_measurement?: string;
|
||||
slider_ticks?: boolean;
|
||||
translation_key?: string;
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { FloorComboBoxItem } from "./area_floor";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import type { DeviceRegistryEntry } from "./device_registry";
|
||||
import type { DevicePickerItem, DeviceRegistryEntry } from "./device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||
import type { EntityRegistryDisplayEntry } from "./entity_registry";
|
||||
import type {
|
||||
EntityComboBoxItem,
|
||||
EntityRegistryDisplayEntry,
|
||||
} from "./entity_registry";
|
||||
|
||||
export const TARGET_SEPARATOR = "________";
|
||||
|
||||
export type TargetType = "entity" | "device" | "area" | "label" | "floor";
|
||||
export type TargetTypeFloorless = Exclude<TargetType, "floor">;
|
||||
|
||||
export interface SingleHassServiceTarget {
|
||||
entity_id?: string;
|
||||
device_id?: string;
|
||||
area_id?: string;
|
||||
floor_id?: string;
|
||||
label_id?: string;
|
||||
}
|
||||
|
||||
export interface ExtractFromTargetResult {
|
||||
missing_areas: string[];
|
||||
missing_devices: string[];
|
||||
@@ -35,6 +50,39 @@ export const extractFromTarget = async (
|
||||
target,
|
||||
});
|
||||
|
||||
export const getTriggersForTarget = async (
|
||||
callWS: HomeAssistant["callWS"],
|
||||
target: HassServiceTarget,
|
||||
expandGroup = true
|
||||
) =>
|
||||
callWS<string[]>({
|
||||
type: "get_triggers_for_target",
|
||||
target,
|
||||
expand_group: expandGroup,
|
||||
});
|
||||
|
||||
export const getConditionsForTarget = async (
|
||||
callWS: HomeAssistant["callWS"],
|
||||
target: HassServiceTarget,
|
||||
expandGroup = true
|
||||
) =>
|
||||
callWS<string[]>({
|
||||
type: "get_conditions_for_target",
|
||||
target,
|
||||
expand_group: expandGroup,
|
||||
});
|
||||
|
||||
export const getServicesForTarget = async (
|
||||
callWS: HomeAssistant["callWS"],
|
||||
target: HassServiceTarget,
|
||||
expandGroup = true
|
||||
) =>
|
||||
callWS<string[]>({
|
||||
type: "get_services_for_target",
|
||||
target,
|
||||
expand_group: expandGroup,
|
||||
});
|
||||
|
||||
export const areaMeetsFilter = (
|
||||
area: AreaRegistryEntry,
|
||||
devices: HomeAssistant["devices"],
|
||||
@@ -162,3 +210,32 @@ export const entityRegMeetsFilter = (
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getTargetComboBoxItemType = (
|
||||
item:
|
||||
| PickerComboBoxItem
|
||||
| (FloorComboBoxItem & { last?: boolean | undefined })
|
||||
| EntityComboBoxItem
|
||||
| DevicePickerItem
|
||||
) => {
|
||||
if (
|
||||
(item as FloorComboBoxItem).type === "area" ||
|
||||
(item as FloorComboBoxItem).type === "floor"
|
||||
) {
|
||||
return (item as FloorComboBoxItem).type;
|
||||
}
|
||||
|
||||
if ("domain" in item) {
|
||||
return "device";
|
||||
}
|
||||
|
||||
if ("stateObj" in item) {
|
||||
return "entity";
|
||||
}
|
||||
|
||||
if (item.id === "___EMPTY_SEARCH___") {
|
||||
return "empty";
|
||||
}
|
||||
|
||||
return "label";
|
||||
};
|
||||
|
||||
@@ -106,8 +106,7 @@ export interface AutomationTrace extends BaseTrace {
|
||||
}
|
||||
|
||||
export interface AutomationTraceExtended
|
||||
extends AutomationTrace,
|
||||
BaseTraceExtended {
|
||||
extends AutomationTrace, BaseTraceExtended {
|
||||
config: ManualAutomationConfig;
|
||||
blueprint_inputs?: BlueprintAutomationConfig;
|
||||
}
|
||||
|
||||
@@ -28,18 +28,24 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey: "ui.panel.config.automation.editor.triggers.groups.helpers.label",
|
||||
groups: {
|
||||
helpers: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey: "ui.panel.config.automation.editor.triggers.groups.other.label",
|
||||
groups: {
|
||||
event: {},
|
||||
geo_location: {},
|
||||
homeassistant: {},
|
||||
mqtt: {},
|
||||
conversation: {},
|
||||
tag: {},
|
||||
template: {},
|
||||
webhook: {},
|
||||
persistent_notification: {},
|
||||
other: {},
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -82,6 +82,12 @@ export interface WeatherEntity extends HassEntityBase {
|
||||
attributes: WeatherEntityAttributes;
|
||||
}
|
||||
|
||||
export const WEATHER_TEMPERATURE_ATTRIBUTES = new Set<string>([
|
||||
"temperature",
|
||||
"apparent_temperature",
|
||||
"dew_point",
|
||||
]);
|
||||
|
||||
export const weatherSVGs = new Set<string>([
|
||||
"clear-night",
|
||||
"cloudy",
|
||||
@@ -256,9 +262,15 @@ export const getWeatherUnit = (
|
||||
export const getSecondaryWeatherAttribute = (
|
||||
hass: HomeAssistant,
|
||||
stateObj: WeatherEntity,
|
||||
forecast: ForecastAttribute[]
|
||||
forecast: ForecastAttribute[],
|
||||
temperatureFractionDigits?: number
|
||||
): TemplateResult | undefined => {
|
||||
const extrema = getWeatherExtrema(hass, stateObj, forecast);
|
||||
const extrema = getWeatherExtrema(
|
||||
hass,
|
||||
stateObj,
|
||||
forecast,
|
||||
temperatureFractionDigits
|
||||
);
|
||||
|
||||
if (extrema) {
|
||||
return extrema;
|
||||
@@ -298,7 +310,8 @@ export const getSecondaryWeatherAttribute = (
|
||||
const getWeatherExtrema = (
|
||||
hass: HomeAssistant,
|
||||
stateObj: WeatherEntity,
|
||||
forecast: ForecastAttribute[]
|
||||
forecast: ForecastAttribute[],
|
||||
temperatureFractionDigits?: number
|
||||
): TemplateResult | undefined => {
|
||||
if (!forecast?.length) {
|
||||
return undefined;
|
||||
@@ -313,13 +326,22 @@ const getWeatherExtrema = (
|
||||
break;
|
||||
}
|
||||
if (!tempHigh || fc.temperature > tempHigh) {
|
||||
tempHigh = fc.temperature;
|
||||
tempHigh =
|
||||
temperatureFractionDigits === undefined
|
||||
? fc.temperature
|
||||
: round(fc.temperature, temperatureFractionDigits);
|
||||
}
|
||||
if (!tempLow || (fc.templow && fc.templow < tempLow)) {
|
||||
tempLow = fc.templow;
|
||||
if (fc.templow !== undefined && (!tempLow || fc.templow < tempLow)) {
|
||||
tempLow =
|
||||
temperatureFractionDigits === undefined
|
||||
? fc.templow
|
||||
: round(fc.templow, temperatureFractionDigits);
|
||||
}
|
||||
if (!fc.templow && (!tempLow || fc.temperature < tempLow)) {
|
||||
tempLow = fc.temperature;
|
||||
tempLow =
|
||||
temperatureFractionDigits === undefined
|
||||
? fc.temperature
|
||||
: round(fc.temperature, temperatureFractionDigits);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { mdiAlertOutline } from "@mdi/js";
|
||||
import { mdiAlertOutline, mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-dialog-header";
|
||||
import "../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../components/ha-md-dialog";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../components/ha-textfield";
|
||||
import "../../components/ha-wa-dialog";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { DialogBoxParams } from "./show-dialog-box";
|
||||
|
||||
@@ -19,12 +20,12 @@ class DialogBox extends LitElement {
|
||||
|
||||
@state() private _params?: DialogBoxParams;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _closeState?: "canceled" | "confirmed";
|
||||
|
||||
@query("ha-textfield") private _textField?: HaTextField;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
|
||||
private _closePromise?: Promise<void>;
|
||||
|
||||
private _closeResolve?: () => void;
|
||||
@@ -34,6 +35,7 @@ class DialogBox extends LitElement {
|
||||
await this._closePromise;
|
||||
}
|
||||
this._params = params;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
@@ -60,16 +62,30 @@ class DialogBox extends LitElement {
|
||||
this.hass.localize("ui.dialogs.generic.default_confirmation_title"));
|
||||
|
||||
return html`
|
||||
<ha-md-dialog
|
||||
open
|
||||
.disableCancelAction=${confirmPrompt}
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
type=${confirmPrompt ? "alert" : "standard"}
|
||||
?prevent-scrim-close=${confirmPrompt}
|
||||
@closed=${this._dialogClosed}
|
||||
type="alert"
|
||||
aria-labelledby="dialog-box-title"
|
||||
aria-describedby="dialog-box-description"
|
||||
>
|
||||
<div slot="headline">
|
||||
<span .title=${dialogTitle} id="dialog-box-title">
|
||||
<ha-dialog-header slot="header">
|
||||
${!confirmPrompt
|
||||
? html`<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button
|
||||
></slot>`
|
||||
: nothing}
|
||||
<span
|
||||
class=${classMap({ title: true, alert: confirmPrompt })}
|
||||
slot="title"
|
||||
id="dialog-box-title"
|
||||
>
|
||||
${this._params.warning
|
||||
? html`<ha-svg-icon
|
||||
.path=${mdiAlertOutline}
|
||||
@@ -78,13 +94,13 @@ class DialogBox extends LitElement {
|
||||
: nothing}
|
||||
${dialogTitle}
|
||||
</span>
|
||||
</div>
|
||||
<div slot="content" id="dialog-box-description">
|
||||
</ha-dialog-header>
|
||||
<div id="dialog-box-description">
|
||||
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
|
||||
${this._params.prompt
|
||||
? html`
|
||||
<ha-textfield
|
||||
dialogInitialFocus
|
||||
autofocus
|
||||
value=${ifDefined(this._params.defaultValue)}
|
||||
.placeholder=${this._params.placeholder}
|
||||
.label=${this._params.inputLabel
|
||||
@@ -99,10 +115,11 @@ class DialogBox extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<ha-dialog-footer slot="footer">
|
||||
${confirmPrompt
|
||||
? html`
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._dismiss}
|
||||
?autofocus=${!this._params.prompt && this._params.destructive}
|
||||
appearance="plain"
|
||||
@@ -114,6 +131,7 @@ class DialogBox extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._confirm}
|
||||
?autofocus=${!this._params.prompt && !this._params.destructive}
|
||||
variant=${this._params.destructive ? "danger" : "brand"}
|
||||
@@ -122,8 +140,8 @@ class DialogBox extends LitElement {
|
||||
? this._params.confirmText
|
||||
: this.hass.localize("ui.common.ok")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -148,20 +166,20 @@ class DialogBox extends LitElement {
|
||||
}
|
||||
|
||||
private _closeDialog() {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
this._dialog?.close();
|
||||
this._open = false;
|
||||
this._closePromise = new Promise((resolve) => {
|
||||
this._closeResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
private _dialogClosed() {
|
||||
if (!this._closeState) {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
if (!this._closeState) {
|
||||
this._cancel();
|
||||
}
|
||||
this._closeState = undefined;
|
||||
this._params = undefined;
|
||||
this._open = false;
|
||||
this._closeResolve?.();
|
||||
this._closeResolve = undefined;
|
||||
}
|
||||
@@ -187,6 +205,14 @@ class DialogBox extends LitElement {
|
||||
ha-textfield {
|
||||
width: 100%;
|
||||
}
|
||||
.title.alert {
|
||||
padding: 0 var(--ha-space-2);
|
||||
}
|
||||
@media all and (min-width: 450px) and (min-height: 500px) {
|
||||
.title.alert {
|
||||
padding: 0 var(--ha-space-1);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,7 @@ export interface PromptDialogParams extends BaseDialogBoxParams {
|
||||
}
|
||||
|
||||
export interface DialogBoxParams
|
||||
extends ConfirmationDialogParams,
|
||||
PromptDialogParams {
|
||||
extends ConfirmationDialogParams, PromptDialogParams {
|
||||
confirm?: (out?: string) => void;
|
||||
confirmation?: boolean;
|
||||
prompt?: boolean;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { HASSDomEvent, ValidHassDomEvent } from "../common/dom/fire_event";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import type { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
|
||||
import { ancestorsWithProperty } from "../common/dom/ancestors-with-property";
|
||||
import { deepActiveElement } from "../common/dom/deep-active-element";
|
||||
import type { HASSDomEvent, ValidHassDomEvent } from "../common/dom/fire_event";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import type { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -19,10 +19,11 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export interface HassDialog<T = HASSDomEvents[ValidHassDomEvent]>
|
||||
extends HTMLElement {
|
||||
export interface HassDialog<
|
||||
T = HASSDomEvents[ValidHassDomEvent],
|
||||
> extends HTMLElement {
|
||||
showDialog(params: T);
|
||||
closeDialog?: () => boolean;
|
||||
closeDialog?: (historyState?: any) => boolean;
|
||||
}
|
||||
|
||||
interface ShowDialogParams<T> {
|
||||
@@ -143,27 +144,32 @@ export const showDialog = async (
|
||||
return true;
|
||||
};
|
||||
|
||||
export const closeDialog = async (dialogTag: string): Promise<boolean> => {
|
||||
export const closeDialog = async (
|
||||
dialogTag: string,
|
||||
historyState?: any
|
||||
): Promise<boolean> => {
|
||||
if (!(dialogTag in LOADED)) {
|
||||
return true;
|
||||
}
|
||||
const dialogElement = await LOADED[dialogTag].element;
|
||||
if (dialogElement.closeDialog) {
|
||||
return dialogElement.closeDialog() !== false;
|
||||
return dialogElement.closeDialog(historyState) !== false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// called on back()
|
||||
export const closeLastDialog = async () => {
|
||||
export const closeLastDialog = async (historyState?: any) => {
|
||||
if (OPEN_DIALOG_STACK.length) {
|
||||
const lastDialog = OPEN_DIALOG_STACK.pop();
|
||||
const closed = await closeDialog(lastDialog!.dialogTag);
|
||||
const lastDialog = OPEN_DIALOG_STACK.pop() as DialogState;
|
||||
const closed = await closeDialog(lastDialog.dialogTag, historyState);
|
||||
if (!closed) {
|
||||
// if the dialog was not closed, put it back on the stack
|
||||
OPEN_DIALOG_STACK.push(lastDialog!);
|
||||
}
|
||||
if (OPEN_DIALOG_STACK.length && mainWindow.history.state?.opensDialog) {
|
||||
OPEN_DIALOG_STACK.push(lastDialog);
|
||||
} else if (
|
||||
OPEN_DIALOG_STACK.length &&
|
||||
mainWindow.history.state?.opensDialog
|
||||
) {
|
||||
// if there are more dialogs open, push a new state so back() will close the next top dialog
|
||||
mainWindow.history.pushState(
|
||||
{ dialog: OPEN_DIALOG_STACK[OPEN_DIALOG_STACK.length - 1].dialogTag },
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user