mirror of
https://github.com/home-assistant/frontend.git
synced 2025-12-20 15:07:21 +00:00
Compare commits
260 Commits
20251203.3
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd46c358fb | ||
|
|
30b8ea1ae8 | ||
|
|
a24dacf50d | ||
|
|
7cbd07e33e | ||
|
|
c72ad83532 | ||
|
|
f2aba45dfe | ||
|
|
639c2ce077 | ||
|
|
1bddc02ae0 | ||
|
|
ebea5176e2 | ||
|
|
39f550cf9f | ||
|
|
cdcbd00a92 | ||
|
|
86cd0e81ad | ||
|
|
6efe444af3 | ||
|
|
09c9665b2f | ||
|
|
23e394fec9 | ||
|
|
4b02a11634 | ||
|
|
ed9c00cab5 | ||
|
|
eaa1fb4107 | ||
|
|
c0a49b3d0b | ||
|
|
a7a00228a2 | ||
|
|
c85f7a71b2 | ||
|
|
c95e914219 | ||
|
|
362a0b96ab | ||
|
|
ef984fc438 | ||
|
|
ce9bbc9972 | ||
|
|
a3921f0559 | ||
|
|
f8ec5d27a4 | ||
|
|
d679916fa5 | ||
|
|
96be4768d3 | ||
|
|
6ff3b9f761 | ||
|
|
9026009842 | ||
|
|
54398a4784 | ||
|
|
fa3cc970ec | ||
|
|
1cf0560003 | ||
|
|
2a4ac15987 | ||
|
|
f264eebe49 | ||
|
|
dae27e091f | ||
|
|
7ca681e417 | ||
|
|
1adfe63322 | ||
|
|
119a505a0d | ||
|
|
1f8403f6c1 | ||
|
|
7dd7309a47 | ||
|
|
736afe2530 | ||
|
|
92980dfddf | ||
|
|
9f3d6e1fea | ||
|
|
9e9adfcc90 | ||
|
|
9b6ebdfcc0 | ||
|
|
88faedba65 | ||
|
|
b125cd5f3e | ||
|
|
3425837de3 | ||
|
|
24a797e46a | ||
|
|
4a486ff28b | ||
|
|
6be390a07e | ||
|
|
c93942919b | ||
|
|
ca06269a91 | ||
|
|
b3d7c0b6dc | ||
|
|
e709703f79 | ||
|
|
fd42614a23 | ||
|
|
4050dd0384 | ||
|
|
9996b1bfea | ||
|
|
8092340d2a | ||
|
|
443e205395 | ||
|
|
8c5d0c4b80 | ||
|
|
3005f12ef5 | ||
|
|
3bcf530200 | ||
|
|
af2fa98666 | ||
|
|
8f335668db | ||
|
|
51acd271ea | ||
|
|
830e9b5089 | ||
|
|
b13cb84baf | ||
|
|
f8df7f37ea | ||
|
|
54e4f4e60a | ||
|
|
81cb483163 | ||
|
|
eb5b14ea00 | ||
|
|
b221fbb387 | ||
|
|
3033bfb1fb | ||
|
|
8db967eb2f | ||
|
|
10af86cc02 | ||
|
|
dfc7116819 | ||
|
|
685c642bfc | ||
|
|
2e547937b8 | ||
|
|
e3e01c327a | ||
|
|
abd706fed0 | ||
|
|
3bd45dd29b | ||
|
|
206f067d2b | ||
|
|
caefa7530a | ||
|
|
de142e33a1 | ||
|
|
5d4c3ebfcd | ||
|
|
eb910c5ac5 | ||
|
|
72726a2e0f | ||
|
|
253b49871e | ||
|
|
6306890922 | ||
|
|
fee16ed4e4 | ||
|
|
055f6c82fb | ||
|
|
0c1627c69a | ||
|
|
780c03ece8 | ||
|
|
62dc66abd8 | ||
|
|
08aaee754e | ||
|
|
bba400443e | ||
|
|
a7a937e197 | ||
|
|
2a97913520 | ||
|
|
9f16ce7341 | ||
|
|
3e20e9b388 | ||
|
|
6891eb9ff8 | ||
|
|
f649b3783d | ||
|
|
c46f67d572 | ||
|
|
86acfa67dd | ||
|
|
3c5c19270f | ||
|
|
6db7817032 | ||
|
|
05ca8253f0 | ||
|
|
071161e82d | ||
|
|
9cd38c7128 | ||
|
|
6322c19a45 | ||
|
|
74b51b77fe | ||
|
|
b80481b53e | ||
|
|
2ce1eaf8c6 | ||
|
|
4030ce3f88 | ||
|
|
41cabde393 | ||
|
|
47d1fdf673 | ||
|
|
e59e83fffe | ||
|
|
b896b78876 | ||
|
|
6e180c9fb4 | ||
|
|
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 |
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
@@ -154,7 +154,7 @@ try {
|
|||||||
|
|
||||||
- **Use CSS custom properties**: Leverage the theme system
|
- **Use CSS custom properties**: Leverage the theme system
|
||||||
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
|
- **Use spacing tokens**: Prefer `--ha-space-*` tokens over hardcoded values for consistent spacing
|
||||||
- Spacing scale: `--ha-space-0` (0px) through `--ha-space-20` (80px) in 4px increments
|
- Spacing scale: `--ha-space-1` (4px) through `--ha-space-20` (80px) in 4px increments
|
||||||
- Defined in `src/resources/theme/core.globals.ts`
|
- Defined in `src/resources/theme/core.globals.ts`
|
||||||
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
|
- Common values: `--ha-space-2` (8px), `--ha-space-4` (16px), `--ha-space-8` (32px)
|
||||||
- **Mobile-first responsive**: Design for mobile, enhance for desktop
|
- **Mobile-first responsive**: Design for mobile, enhance for desktop
|
||||||
|
|||||||
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 }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -56,12 +56,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
|||||||
22
.github/workflows/ci.yaml
vendored
22
.github/workflows/ci.yaml
vendored
@@ -24,9 +24,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
- name: Build resources
|
- name: Build resources
|
||||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||||
- name: Setup lint cache
|
- name: Setup lint cache
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
node_modules/.cache/prettier
|
node_modules/.cache/prettier
|
||||||
@@ -58,9 +58,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -76,9 +76,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IS_TEST: "true"
|
IS_TEST: "true"
|
||||||
- name: Upload bundle stats
|
- name: Upload bundle stats
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: frontend-bundle-stats
|
name: frontend-bundle-stats
|
||||||
path: build/stats/*.json
|
path: build/stats/*.json
|
||||||
@@ -100,9 +100,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IS_TEST: "true"
|
IS_TEST: "true"
|
||||||
- name: Upload bundle stats
|
- name: Upload bundle stats
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: supervisor-bundle-stats
|
name: supervisor-bundle-stats
|
||||||
path: build/stats/*.json
|
path: build/stats/*.json
|
||||||
|
|||||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
@@ -36,14 +36,14 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# 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)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
uses: github/codeql-action/autobuild@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -57,4 +57,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||||
|
|||||||
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 }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -57,12 +57,12 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
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 }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
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')
|
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
|||||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
process-only: "issues, prs"
|
process-only: "issues, prs"
|
||||||
|
|||||||
10
.github/workflows/nightly.yaml
vendored
10
.github/workflows/nightly.yaml
vendored
@@ -20,15 +20,15 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- 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 }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -57,14 +57,14 @@ jobs:
|
|||||||
run: tar -czvf translations.tar.gz translations
|
run: tar -czvf translations.tar.gz translations
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: wheels
|
name: wheels
|
||||||
path: dist/home_assistant_frontend*.whl
|
path: dist/home_assistant_frontend*.whl
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload translations
|
- name: Upload translations
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
|
|||||||
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Send bundle stats and build information to RelativeCI
|
- 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:
|
with:
|
||||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||||
token: ${{ github.token }}
|
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
|
contents: write # Required to upload release assets
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- 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 }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.PYTHON_VERSION }}
|
python-version: ${{ env.PYTHON_VERSION }}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
uses: home-assistant/actions/helpers/verify-version@master
|
uses: home-assistant/actions/helpers/verify-version@master
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
script/release
|
script/release
|
||||||
|
|
||||||
- name: Upload release assets
|
- name: Upload release assets
|
||||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
dist/*.whl
|
dist/*.whl
|
||||||
@@ -75,7 +75,7 @@ jobs:
|
|||||||
|
|
||||||
# home-assistant/wheels doesn't support SHA pinning
|
# home-assistant/wheels doesn't support SHA pinning
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2025.10.0
|
uses: home-assistant/wheels@2025.12.0
|
||||||
with:
|
with:
|
||||||
abi: cp313
|
abi: cp313
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
@@ -91,9 +91,9 @@ jobs:
|
|||||||
contents: write # Required to upload release assets
|
contents: write # Required to upload release assets
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -108,7 +108,7 @@ jobs:
|
|||||||
- name: Tar folder
|
- name: Tar folder
|
||||||
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
|
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
|
||||||
- name: Upload release asset
|
- name: Upload release asset
|
||||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||||
with:
|
with:
|
||||||
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
|
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
|
contents: write # Required to upload release assets
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
cache: yarn
|
cache: yarn
|
||||||
@@ -137,6 +137,6 @@ jobs:
|
|||||||
- name: Tar folder
|
- name: Tar folder
|
||||||
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
|
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
|
||||||
- name: Upload release asset
|
- name: Upload release asset
|
||||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||||
with:
|
with:
|
||||||
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: 90 days stale policy
|
- name: 90 days stale policy
|
||||||
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 90
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Upload Translations
|
- name: Upload Translations
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -56,4 +56,5 @@ test/coverage/
|
|||||||
|
|
||||||
# AI tooling
|
# AI tooling
|
||||||
.claude
|
.claude
|
||||||
|
.cursor
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
compressionLevel: mixed
|
compressionLevel: mixed
|
||||||
|
|
||||||
|
npmMinimalAgeGate: "3d"
|
||||||
|
|
||||||
defaultSemverRangePrefix: ""
|
defaultSemverRangePrefix: ""
|
||||||
|
|
||||||
enableGlobalCache: false
|
enableGlobalCache: false
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ module.exports.ignorePackages = () => [];
|
|||||||
// Files from NPM packages that we should replace with empty file
|
// Files from NPM packages that we should replace with empty file
|
||||||
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
|
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
|
||||||
[
|
[
|
||||||
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
|
|
||||||
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
|
|
||||||
// Icons in supervisor conflict with icons in HA so we don't load.
|
// Icons in supervisor conflict with icons in HA so we don't load.
|
||||||
(isHassioBuild || isLandingPageBuild) &&
|
(isHassioBuild || isLandingPageBuild) &&
|
||||||
require.resolve(
|
require.resolve(
|
||||||
|
|||||||
@@ -156,7 +156,9 @@ const createTestTranslation = () =>
|
|||||||
*/
|
*/
|
||||||
const createMasterTranslation = () =>
|
const createMasterTranslation = () =>
|
||||||
gulp
|
gulp
|
||||||
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
|
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])], {
|
||||||
|
allowEmpty: true,
|
||||||
|
})
|
||||||
.pipe(new CustomJSON(lokaliseTransform))
|
.pipe(new CustomJSON(lokaliseTransform))
|
||||||
.pipe(new MergeJSON("en"))
|
.pipe(new MergeJSON("en"))
|
||||||
.pipe(gulp.dest(workDir));
|
.pipe(gulp.dest(workDir));
|
||||||
|
|||||||
@@ -168,12 +168,16 @@ const createRspackConfig = ({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
new rspack.NormalModuleReplacementPlugin(
|
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).length
|
||||||
|
? new rspack.NormalModuleReplacementPlugin(
|
||||||
new RegExp(
|
new RegExp(
|
||||||
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).join("|")
|
bundle
|
||||||
|
.emptyPackages({ isHassioBuild, isLandingPageBuild })
|
||||||
|
.join("|")
|
||||||
),
|
),
|
||||||
path.resolve(paths.root_dir, "src/util/empty.js")
|
path.resolve(paths.root_dir, "src/util/empty.js")
|
||||||
),
|
)
|
||||||
|
: false,
|
||||||
!isProdBuild && new LogStartCompilePlugin(),
|
!isProdBuild && new LogStartCompilePlugin(),
|
||||||
isProdBuild &&
|
isProdBuild &&
|
||||||
new StatsWriterPlugin({
|
new StatsWriterPlugin({
|
||||||
@@ -217,6 +221,42 @@ const createRspackConfig = ({
|
|||||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
|
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
|
||||||
"@lit-labs/observers/resize-controller":
|
"@lit-labs/observers/resize-controller":
|
||||||
"@lit-labs/observers/resize-controller.js",
|
"@lit-labs/observers/resize-controller.js",
|
||||||
|
"@formatjs/intl-durationformat/should-polyfill$":
|
||||||
|
"@formatjs/intl-durationformat/should-polyfill.js",
|
||||||
|
"@formatjs/intl-durationformat/polyfill-force$":
|
||||||
|
"@formatjs/intl-durationformat/polyfill-force.js",
|
||||||
|
"@formatjs/intl-datetimeformat/should-polyfill":
|
||||||
|
"@formatjs/intl-datetimeformat/should-polyfill.js",
|
||||||
|
"@formatjs/intl-datetimeformat/polyfill-force":
|
||||||
|
"@formatjs/intl-datetimeformat/polyfill-force.js",
|
||||||
|
"@formatjs/intl-displaynames/should-polyfill":
|
||||||
|
"@formatjs/intl-displaynames/should-polyfill.js",
|
||||||
|
"@formatjs/intl-displaynames/polyfill-force":
|
||||||
|
"@formatjs/intl-displaynames/polyfill-force.js",
|
||||||
|
"@formatjs/intl-getcanonicallocales/should-polyfill":
|
||||||
|
"@formatjs/intl-getcanonicallocales/should-polyfill.js",
|
||||||
|
"@formatjs/intl-getcanonicallocales/polyfill-force":
|
||||||
|
"@formatjs/intl-getcanonicallocales/polyfill-force.js",
|
||||||
|
"@formatjs/intl-listformat/should-polyfill":
|
||||||
|
"@formatjs/intl-listformat/should-polyfill.js",
|
||||||
|
"@formatjs/intl-listformat/polyfill-force":
|
||||||
|
"@formatjs/intl-listformat/polyfill-force.js",
|
||||||
|
"@formatjs/intl-locale/should-polyfill":
|
||||||
|
"@formatjs/intl-locale/should-polyfill.js",
|
||||||
|
"@formatjs/intl-locale/polyfill-force":
|
||||||
|
"@formatjs/intl-locale/polyfill-force.js",
|
||||||
|
"@formatjs/intl-numberformat/should-polyfill":
|
||||||
|
"@formatjs/intl-numberformat/should-polyfill.js",
|
||||||
|
"@formatjs/intl-numberformat/polyfill-force":
|
||||||
|
"@formatjs/intl-numberformat/polyfill-force.js",
|
||||||
|
"@formatjs/intl-pluralrules/should-polyfill":
|
||||||
|
"@formatjs/intl-pluralrules/should-polyfill.js",
|
||||||
|
"@formatjs/intl-pluralrules/polyfill-force":
|
||||||
|
"@formatjs/intl-pluralrules/polyfill-force.js",
|
||||||
|
"@formatjs/intl-relativetimeformat/should-polyfill":
|
||||||
|
"@formatjs/intl-relativetimeformat/should-polyfill.js",
|
||||||
|
"@formatjs/intl-relativetimeformat/polyfill-force":
|
||||||
|
"@formatjs/intl-relativetimeformat/polyfill-force.js",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
|||||||
@@ -5,17 +5,19 @@ const castContext = framework.CastReceiverContext.getInstance();
|
|||||||
const playerManager = castContext.getPlayerManager();
|
const playerManager = castContext.getPlayerManager();
|
||||||
|
|
||||||
playerManager.setMessageInterceptor(
|
playerManager.setMessageInterceptor(
|
||||||
framework.messages.MessageType.LOAD,
|
"LOAD" as framework.messages.MessageType.LOAD,
|
||||||
(loadRequestData) => {
|
(loadRequestData) => {
|
||||||
const media = loadRequestData.media;
|
const media = loadRequestData.media;
|
||||||
// Special handling if it came from Google Assistant
|
// Special handling if it came from Google Assistant
|
||||||
if (media.entity) {
|
if (media.entity) {
|
||||||
media.contentId = media.entity;
|
media.contentId = media.entity;
|
||||||
media.streamType = framework.messages.StreamType.LIVE;
|
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||||
media.contentType = "application/vnd.apple.mpegurl";
|
media.contentType = "application/vnd.apple.mpegurl";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
// type definition is wrong, should be "FMP4" instead of "fmp4"
|
||||||
|
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
||||||
media.hlsVideoSegmentFormat =
|
media.hlsVideoSegmentFormat =
|
||||||
framework.messages.HlsVideoSegmentFormat.FMP4;
|
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||||
}
|
}
|
||||||
return loadRequestData;
|
return loadRequestData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { framework } from "./cast_framework";
|
|
||||||
import { CAST_NS } from "../../../src/cast/const";
|
import { CAST_NS } from "../../../src/cast/const";
|
||||||
import type { HassMessage } from "../../../src/cast/receiver_messages";
|
import type { HassMessage } from "../../../src/cast/receiver_messages";
|
||||||
import "../../../src/resources/custom-card-support";
|
import "../../../src/resources/custom-card-support";
|
||||||
import { castContext } from "./cast_context";
|
import { castContext } from "./cast_context";
|
||||||
|
import { framework } from "./cast_framework";
|
||||||
import { HcMain } from "./layout/hc-main";
|
import { HcMain } from "./layout/hc-main";
|
||||||
import type { ReceivedMessage } from "./types";
|
|
||||||
|
|
||||||
const lovelaceController = new HcMain();
|
const lovelaceController = new HcMain();
|
||||||
document.body.append(lovelaceController);
|
document.body.append(lovelaceController);
|
||||||
@@ -40,7 +39,8 @@ const playDummyMedia = (viewTitle?: string) => {
|
|||||||
loadRequestData.media.contentId =
|
loadRequestData.media.contentId =
|
||||||
"https://cast.home-assistant.io/images/google-nest-hub.png";
|
"https://cast.home-assistant.io/images/google-nest-hub.png";
|
||||||
loadRequestData.media.contentType = "image/jpeg";
|
loadRequestData.media.contentType = "image/jpeg";
|
||||||
loadRequestData.media.streamType = framework.messages.StreamType.NONE;
|
loadRequestData.media.streamType =
|
||||||
|
"NONE" as framework.messages.StreamType.NONE;
|
||||||
const metadata = new framework.messages.GenericMediaMetadata();
|
const metadata = new framework.messages.GenericMediaMetadata();
|
||||||
metadata.title = viewTitle;
|
metadata.title = viewTitle;
|
||||||
loadRequestData.media.metadata = metadata;
|
loadRequestData.media.metadata = metadata;
|
||||||
@@ -89,31 +89,30 @@ const showMediaPlayer = () => {
|
|||||||
const options = new framework.CastReceiverOptions();
|
const options = new framework.CastReceiverOptions();
|
||||||
options.disableIdleTimeout = true;
|
options.disableIdleTimeout = true;
|
||||||
options.customNamespaces = {
|
options.customNamespaces = {
|
||||||
[CAST_NS]: framework.system.MessageType.JSON,
|
// type definition is wrong, should be "JSON" instead of "json"
|
||||||
|
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.system#.MessageType
|
||||||
|
[CAST_NS]: "JSON" as framework.system.MessageType.JSON,
|
||||||
};
|
};
|
||||||
|
|
||||||
castContext.addCustomMessageListener(
|
castContext.addCustomMessageListener(CAST_NS, (ev) => {
|
||||||
CAST_NS,
|
|
||||||
// @ts-ignore
|
|
||||||
(ev: ReceivedMessage<HassMessage>) => {
|
|
||||||
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
|
// We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller
|
||||||
if (
|
if (
|
||||||
playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE
|
playerManager.getPlayerState() !==
|
||||||
|
("IDLE" as framework.messages.PlayerState.IDLE)
|
||||||
) {
|
) {
|
||||||
playerManager.stop();
|
playerManager.stop();
|
||||||
} else {
|
} else {
|
||||||
showLovelaceController();
|
showLovelaceController();
|
||||||
}
|
}
|
||||||
const msg = ev.data;
|
const msg = ev.data as HassMessage;
|
||||||
msg.senderId = ev.senderId;
|
msg.senderId = ev.senderId;
|
||||||
lovelaceController.processIncomingMessage(msg);
|
lovelaceController.processIncomingMessage(msg);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const playerManager = castContext.getPlayerManager();
|
const playerManager = castContext.getPlayerManager();
|
||||||
|
|
||||||
playerManager.setMessageInterceptor(
|
playerManager.setMessageInterceptor(
|
||||||
framework.messages.MessageType.LOAD,
|
"LOAD" as framework.messages.MessageType.LOAD,
|
||||||
(loadRequestData) => {
|
(loadRequestData) => {
|
||||||
if (
|
if (
|
||||||
loadRequestData.media.contentId ===
|
loadRequestData.media.contentId ===
|
||||||
@@ -127,24 +126,26 @@ playerManager.setMessageInterceptor(
|
|||||||
// Special handling if it came from Google Assistant
|
// Special handling if it came from Google Assistant
|
||||||
if (media.entity) {
|
if (media.entity) {
|
||||||
media.contentId = media.entity;
|
media.contentId = media.entity;
|
||||||
media.streamType = framework.messages.StreamType.LIVE;
|
media.streamType = "LIVE" as framework.messages.StreamType.LIVE;
|
||||||
media.contentType = "application/vnd.apple.mpegurl";
|
media.contentType = "application/vnd.apple.mpegurl";
|
||||||
// @ts-ignore
|
// type definition is wrong, should be "FMP4" instead of "fmp4"
|
||||||
|
// https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages#.HlsVideoSegmentFormat
|
||||||
media.hlsVideoSegmentFormat =
|
media.hlsVideoSegmentFormat =
|
||||||
framework.messages.HlsVideoSegmentFormat.FMP4;
|
"FMP4" as framework.messages.HlsVideoSegmentFormat.FMP4;
|
||||||
}
|
}
|
||||||
return loadRequestData;
|
return loadRequestData;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
playerManager.addEventListener(
|
playerManager.addEventListener(
|
||||||
framework.events.EventType.MEDIA_STATUS,
|
"MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS,
|
||||||
(event) => {
|
(event) => {
|
||||||
if (
|
if (
|
||||||
event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE &&
|
event.mediaStatus?.playerState ===
|
||||||
|
("IDLE" as framework.messages.PlayerState.IDLE) &&
|
||||||
event.mediaStatus?.idleReason &&
|
event.mediaStatus?.idleReason &&
|
||||||
event.mediaStatus?.idleReason !==
|
event.mediaStatus?.idleReason !==
|
||||||
framework.messages.IdleReason.INTERRUPTED
|
("INTERRUPTED" as framework.messages.IdleReason.INTERRUPTED)
|
||||||
) {
|
) {
|
||||||
// media finished or stopped, return to default Lovelace
|
// media finished or stopped, return to default Lovelace
|
||||||
showLovelaceController();
|
showLovelaceController();
|
||||||
|
|||||||
@@ -305,9 +305,8 @@ export class HcMain extends HassElement {
|
|||||||
await llColl.refresh();
|
await llColl.refresh();
|
||||||
this._unsubLovelace = llColl.subscribe(async (rawConfig) => {
|
this._unsubLovelace = llColl.subscribe(async (rawConfig) => {
|
||||||
if (isStrategyDashboard(rawConfig)) {
|
if (isStrategyDashboard(rawConfig)) {
|
||||||
const { generateLovelaceDashboardStrategy } = await import(
|
const { generateLovelaceDashboardStrategy } =
|
||||||
"../../../../src/panels/lovelace/strategies/get-strategy"
|
await import("../../../../src/panels/lovelace/strategies/get-strategy");
|
||||||
);
|
|
||||||
const config = await generateLovelaceDashboardStrategy(
|
const config = await generateLovelaceDashboardStrategy(
|
||||||
rawConfig,
|
rawConfig,
|
||||||
this.hass!
|
this.hass!
|
||||||
@@ -347,9 +346,8 @@ export class HcMain extends HassElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _generateDefaultLovelaceConfig() {
|
private async _generateDefaultLovelaceConfig() {
|
||||||
const { generateLovelaceDashboardStrategy } = await import(
|
const { generateLovelaceDashboardStrategy } =
|
||||||
"../../../../src/panels/lovelace/strategies/get-strategy"
|
await import("../../../../src/panels/lovelace/strategies/get-strategy");
|
||||||
);
|
|
||||||
this._handleNewLovelaceConfig(
|
this._handleNewLovelaceConfig(
|
||||||
await generateLovelaceDashboardStrategy(DEFAULT_CONFIG, this.hass!)
|
await generateLovelaceDashboardStrategy(DEFAULT_CONFIG, this.hass!)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export interface ReceivedMessage<T> {
|
|
||||||
gj: boolean;
|
|
||||||
data: T;
|
|
||||||
senderId: string;
|
|
||||||
type: "message";
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { DeviceRegistryEntry } from "../../../src/data/device_registry";
|
import type { DeviceRegistryEntry } from "../../../src/data/device/device_registry";
|
||||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
|
|
||||||
export const mockDeviceRegistry = (
|
export const mockDeviceRegistry = (
|
||||||
|
|||||||
@@ -44,18 +44,24 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
|||||||
number_energy_price: null,
|
number_energy_price: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
power: [
|
||||||
|
{ stat_rate: "sensor.power_grid" },
|
||||||
|
{ stat_rate: "sensor.power_grid_return" },
|
||||||
|
],
|
||||||
cost_adjustment_day: 0,
|
cost_adjustment_day: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "solar",
|
type: "solar",
|
||||||
stat_energy_from: "sensor.solar_production",
|
stat_energy_from: "sensor.solar_production",
|
||||||
|
stat_rate: "sensor.power_solar",
|
||||||
config_entry_solar_forecast: ["solar_forecast"],
|
config_entry_solar_forecast: ["solar_forecast"],
|
||||||
},
|
},
|
||||||
/* {
|
{
|
||||||
type: "battery",
|
type: "battery",
|
||||||
stat_energy_from: "sensor.battery_output",
|
stat_energy_from: "sensor.battery_output",
|
||||||
stat_energy_to: "sensor.battery_input",
|
stat_energy_to: "sensor.battery_input",
|
||||||
}, */
|
stat_rate: "sensor.power_battery",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "gas",
|
type: "gas",
|
||||||
stat_energy_from: "sensor.energy_gas",
|
stat_energy_from: "sensor.energy_gas",
|
||||||
@@ -63,28 +69,48 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
|||||||
entity_energy_price: null,
|
entity_energy_price: null,
|
||||||
number_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: [
|
device_consumption: [
|
||||||
{
|
{
|
||||||
stat_consumption: "sensor.energy_car",
|
stat_consumption: "sensor.energy_car",
|
||||||
|
stat_rate: "sensor.power_car",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
stat_consumption: "sensor.energy_ac",
|
stat_consumption: "sensor.energy_ac",
|
||||||
|
stat_rate: "sensor.power_ac",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
stat_consumption: "sensor.energy_washing_machine",
|
stat_consumption: "sensor.energy_washing_machine",
|
||||||
|
stat_rate: "sensor.power_washing_machine",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
stat_consumption: "sensor.energy_dryer",
|
stat_consumption: "sensor.energy_dryer",
|
||||||
|
stat_rate: "sensor.power_dryer",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
stat_consumption: "sensor.energy_heat_pump",
|
stat_consumption: "sensor.energy_heat_pump",
|
||||||
|
stat_rate: "sensor.power_heat_pump",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
stat_consumption: "sensor.energy_boiler",
|
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(
|
hass.mockWS(
|
||||||
|
|||||||
@@ -154,6 +154,38 @@ export const energyEntities = () =>
|
|||||||
unit_of_measurement: "EUR",
|
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": {
|
"sensor.energy_gas_cost": {
|
||||||
entity_id: "sensor.energy_gas_cost",
|
entity_id: "sensor.energy_gas_cost",
|
||||||
state: "2",
|
state: "2",
|
||||||
@@ -171,6 +203,15 @@ export const energyEntities = () =>
|
|||||||
unit_of_measurement: "m³",
|
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": {
|
"sensor.energy_car": {
|
||||||
entity_id: "sensor.energy_car",
|
entity_id: "sensor.energy_car",
|
||||||
state: "4",
|
state: "4",
|
||||||
@@ -225,4 +266,58 @@ export const energyEntities = () =>
|
|||||||
unit_of_measurement: "kWh",
|
unit_of_measurement: "kWh",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"sensor.power_car": {
|
||||||
|
entity_id: "sensor.power_car",
|
||||||
|
state: "40",
|
||||||
|
attributes: {
|
||||||
|
state_class: "measurement",
|
||||||
|
friendly_name: "Electric car",
|
||||||
|
unit_of_measurement: "W",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sensor.power_ac": {
|
||||||
|
entity_id: "sensor.power_ac",
|
||||||
|
state: "30",
|
||||||
|
attributes: {
|
||||||
|
state_class: "measurement",
|
||||||
|
friendly_name: "Air conditioning",
|
||||||
|
unit_of_measurement: "W",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sensor.power_washing_machine": {
|
||||||
|
entity_id: "sensor.power_washing_machine",
|
||||||
|
state: "60",
|
||||||
|
attributes: {
|
||||||
|
state_class: "measurement",
|
||||||
|
friendly_name: "Washing machine",
|
||||||
|
unit_of_measurement: "W",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sensor.power_dryer": {
|
||||||
|
entity_id: "sensor.power_dryer",
|
||||||
|
state: "55",
|
||||||
|
attributes: {
|
||||||
|
state_class: "measurement",
|
||||||
|
friendly_name: "Dryer",
|
||||||
|
unit_of_measurement: "W",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sensor.power_heat_pump": {
|
||||||
|
entity_id: "sensor.power_heat_pump",
|
||||||
|
state: "60",
|
||||||
|
attributes: {
|
||||||
|
state_class: "measurement",
|
||||||
|
friendly_name: "Heat pump",
|
||||||
|
unit_of_measurement: "W",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"sensor.power_boiler": {
|
||||||
|
entity_id: "sensor.power_boiler",
|
||||||
|
state: "70",
|
||||||
|
attributes: {
|
||||||
|
state_class: "measurement",
|
||||||
|
friendly_name: "Boiler",
|
||||||
|
unit_of_measurement: "W",
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { EntityRegistryEntry } from "../../../src/data/entity_registry";
|
import type { EntityRegistryEntry } from "../../../src/data/entity/entity_registry";
|
||||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
|
|
||||||
export const mockEntityRegistry = (
|
export const mockEntityRegistry = (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { LabelRegistryEntry } from "../../../src/data/label_registry";
|
import type { LabelRegistryEntry } from "../../../src/data/label/label_registry";
|
||||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
|
|
||||||
export const mockLabelRegistry = (
|
export const mockLabelRegistry = (
|
||||||
|
|||||||
@@ -17,17 +17,15 @@ const generateMeanStatistics = (
|
|||||||
end: Date,
|
end: Date,
|
||||||
// eslint-disable-next-line default-param-last
|
// eslint-disable-next-line default-param-last
|
||||||
period: "5minute" | "hour" | "day" | "month" = "hour",
|
period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||||
initValue: number,
|
|
||||||
maxDiff: number
|
maxDiff: number
|
||||||
): StatisticValue[] => {
|
): StatisticValue[] => {
|
||||||
const statistics: StatisticValue[] = [];
|
const statistics: StatisticValue[] = [];
|
||||||
let currentDate = new Date(start);
|
let currentDate = new Date(start);
|
||||||
currentDate.setMinutes(0, 0, 0);
|
currentDate.setMinutes(0, 0, 0);
|
||||||
let lastVal = initValue;
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
while (end > currentDate && currentDate < now) {
|
while (end > currentDate && currentDate < now) {
|
||||||
const delta = Math.random() * maxDiff;
|
const delta = Math.random() * maxDiff;
|
||||||
const mean = lastVal + delta;
|
const mean = delta;
|
||||||
statistics.push({
|
statistics.push({
|
||||||
start: currentDate.getTime(),
|
start: currentDate.getTime(),
|
||||||
end: currentDate.getTime(),
|
end: currentDate.getTime(),
|
||||||
@@ -38,7 +36,6 @@ const generateMeanStatistics = (
|
|||||||
state: mean,
|
state: mean,
|
||||||
sum: null,
|
sum: null,
|
||||||
});
|
});
|
||||||
lastVal = mean;
|
|
||||||
currentDate =
|
currentDate =
|
||||||
period === "day"
|
period === "day"
|
||||||
? addDays(currentDate, 1)
|
? addDays(currentDate, 1)
|
||||||
@@ -336,7 +333,6 @@ export const mockRecorder = (mockHass: MockHomeAssistant) => {
|
|||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
period,
|
period,
|
||||||
state,
|
|
||||||
state * (state > 80 ? 0.05 : 0.1)
|
state * (state > 80 ? 0.05 : 0.1)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
|||||||
<div class="action">
|
<div class="action">
|
||||||
<span>
|
<span>
|
||||||
${this._action
|
${this._action
|
||||||
? describeAction(this.hass, [], [], {}, this._action)
|
? describeAction(this.hass, [], this._action)
|
||||||
: "<invalid YAML>"}
|
: "<invalid YAML>"}
|
||||||
</span>
|
</span>
|
||||||
<ha-yaml-editor
|
<ha-yaml-editor
|
||||||
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
|||||||
${ACTIONS.map(
|
${ACTIONS.map(
|
||||||
(conf) => html`
|
(conf) => html`
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<span>${describeAction(this.hass, [], [], {}, conf as any)}</span>
|
<span>${describeAction(this.hass, [], conf as any)}</span>
|
||||||
<pre>${dump(conf)}</pre>
|
<pre>${dump(conf)}</pre>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,11 +11,11 @@ import { computeInitialHaFormData } from "../../../../src/components/ha-form/com
|
|||||||
import "../../../../src/components/ha-form/ha-form";
|
import "../../../../src/components/ha-form/ha-form";
|
||||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||||
|
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||||
import { getEntity } from "../../../../src/fake_data/entity";
|
import { getEntity } from "../../../../src/fake_data/entity";
|
||||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||||
import type { HomeAssistant } from "../../../../src/types";
|
import type { HomeAssistant } from "../../../../src/types";
|
||||||
import "../../components/demo-black-white-row";
|
import "../../components/demo-black-white-row";
|
||||||
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
|
|
||||||
|
|
||||||
const ENTITIES = [
|
const ENTITIES = [
|
||||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import "../../../../src/components/ha-selector/ha-selector";
|
|||||||
import "../../../../src/components/ha-settings-row";
|
import "../../../../src/components/ha-settings-row";
|
||||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||||
import type { BlueprintInput } from "../../../../src/data/blueprint";
|
import type { BlueprintInput } from "../../../../src/data/blueprint";
|
||||||
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
|
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||||
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||||
import type { LabelRegistryEntry } from "../../../../src/data/label_registry";
|
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry";
|
||||||
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
import { showDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||||
import { getEntity } from "../../../../src/fake_data/entity";
|
import { getEntity } from "../../../../src/fake_data/entity";
|
||||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||||
@@ -40,6 +40,9 @@ const ENTITIES = [
|
|||||||
getEntity("switch", "coffee", "off", {
|
getEntity("switch", "coffee", "off", {
|
||||||
friendly_name: "Coffee",
|
friendly_name: "Coffee",
|
||||||
}),
|
}),
|
||||||
|
getEntity("number", "number", 5, {
|
||||||
|
friendly_name: "Number",
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
const DEVICES: DeviceRegistryEntry[] = [
|
const DEVICES: DeviceRegistryEntry[] = [
|
||||||
@@ -377,6 +380,33 @@ const SCHEMAS: {
|
|||||||
name: "Constant",
|
name: "Constant",
|
||||||
selector: { constant: { value: true, label: "Yes!" } },
|
selector: { constant: { value: true, label: "Yes!" } },
|
||||||
},
|
},
|
||||||
|
choose: {
|
||||||
|
name: "Choose",
|
||||||
|
selector: {
|
||||||
|
choose: {
|
||||||
|
choices: {
|
||||||
|
number: {
|
||||||
|
selector: {
|
||||||
|
number: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
step: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
entity: {
|
||||||
|
selector: {
|
||||||
|
entity: {
|
||||||
|
filter: {
|
||||||
|
domain: "number",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export class DemoHaWaDialog extends LitElement {
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>large</code></td>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>full</code></td>
|
<td><code>full</code></td>
|
||||||
@@ -381,10 +381,6 @@ export class DemoHaWaDialog extends LitElement {
|
|||||||
<td><code>--dialog-z-index</code></td>
|
<td><code>--dialog-z-index</code></td>
|
||||||
<td>Z-index for the dialog.</td>
|
<td>Z-index for the dialog.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td><code>--dialog-surface-position</code></td>
|
|
||||||
<td>CSS position of the dialog surface.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>--dialog-surface-margin-top</code></td>
|
<td><code>--dialog-surface-margin-top</code></td>
|
||||||
<td>Top margin for the dialog surface.</td>
|
<td>Top margin for the dialog surface.</td>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import type { IntegrationManifest } from "../../../../src/data/integration";
|
import type { IntegrationManifest } from "../../../../src/data/integration";
|
||||||
|
|
||||||
import type { DeviceRegistryEntry } from "../../../../src/data/device_registry";
|
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||||
import type { EntityRegistryEntry } from "../../../../src/data/entity_registry";
|
import type { EntityRegistryEntry } from "../../../../src/data/entity/entity_registry";
|
||||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||||
import "../../../../src/panels/config/integrations/ha-config-flow-card";
|
import "../../../../src/panels/config/integrations/ha-config-flow-card";
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
58
package.json
58
package.json
@@ -29,30 +29,30 @@
|
|||||||
"@babel/runtime": "7.28.4",
|
"@babel/runtime": "7.28.4",
|
||||||
"@braintree/sanitize-url": "7.1.1",
|
"@braintree/sanitize-url": "7.1.1",
|
||||||
"@codemirror/autocomplete": "6.20.0",
|
"@codemirror/autocomplete": "6.20.0",
|
||||||
"@codemirror/commands": "6.10.0",
|
"@codemirror/commands": "6.10.1",
|
||||||
"@codemirror/language": "6.11.3",
|
"@codemirror/language": "6.11.3",
|
||||||
"@codemirror/legacy-modes": "6.5.2",
|
"@codemirror/legacy-modes": "6.5.2",
|
||||||
"@codemirror/search": "6.5.11",
|
"@codemirror/search": "6.5.11",
|
||||||
"@codemirror/state": "6.5.2",
|
"@codemirror/state": "6.5.2",
|
||||||
"@codemirror/view": "6.38.8",
|
"@codemirror/view": "6.39.4",
|
||||||
"@date-fns/tz": "1.4.1",
|
"@date-fns/tz": "1.4.1",
|
||||||
"@egjs/hammerjs": "2.0.17",
|
"@egjs/hammerjs": "2.0.17",
|
||||||
"@formatjs/intl-datetimeformat": "6.18.2",
|
"@formatjs/intl-datetimeformat": "7.0.2",
|
||||||
"@formatjs/intl-displaynames": "6.8.13",
|
"@formatjs/intl-displaynames": "7.0.2",
|
||||||
"@formatjs/intl-durationformat": "0.7.6",
|
"@formatjs/intl-durationformat": "0.8.2",
|
||||||
"@formatjs/intl-getcanonicallocales": "2.5.6",
|
"@formatjs/intl-getcanonicallocales": "3.0.2",
|
||||||
"@formatjs/intl-listformat": "7.7.13",
|
"@formatjs/intl-listformat": "8.0.2",
|
||||||
"@formatjs/intl-locale": "4.2.13",
|
"@formatjs/intl-locale": "5.0.2",
|
||||||
"@formatjs/intl-numberformat": "8.15.6",
|
"@formatjs/intl-numberformat": "9.0.3",
|
||||||
"@formatjs/intl-pluralrules": "5.4.6",
|
"@formatjs/intl-pluralrules": "6.0.2",
|
||||||
"@formatjs/intl-relativetimeformat": "11.4.13",
|
"@formatjs/intl-relativetimeformat": "12.0.3",
|
||||||
"@fullcalendar/core": "6.1.19",
|
"@fullcalendar/core": "6.1.19",
|
||||||
"@fullcalendar/daygrid": "6.1.19",
|
"@fullcalendar/daygrid": "6.1.19",
|
||||||
"@fullcalendar/interaction": "6.1.19",
|
"@fullcalendar/interaction": "6.1.19",
|
||||||
"@fullcalendar/list": "6.1.19",
|
"@fullcalendar/list": "6.1.19",
|
||||||
"@fullcalendar/luxon3": "6.1.19",
|
"@fullcalendar/luxon3": "6.1.19",
|
||||||
"@fullcalendar/timegrid": "6.1.19",
|
"@fullcalendar/timegrid": "6.1.19",
|
||||||
"@home-assistant/webawesome": "3.0.0-ha.0",
|
"@home-assistant/webawesome": "3.0.0-ha.2",
|
||||||
"@lezer/highlight": "1.2.3",
|
"@lezer/highlight": "1.2.3",
|
||||||
"@lit-labs/motion": "1.0.9",
|
"@lit-labs/motion": "1.0.9",
|
||||||
"@lit-labs/observers": "2.0.6",
|
"@lit-labs/observers": "2.0.6",
|
||||||
@@ -89,8 +89,6 @@
|
|||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@tsparticles/engine": "3.9.1",
|
"@tsparticles/engine": "3.9.1",
|
||||||
"@tsparticles/preset-links": "3.2.0",
|
"@tsparticles/preset-links": "3.2.0",
|
||||||
"@vaadin/combo-box": "24.9.5",
|
|
||||||
"@vaadin/vaadin-themable-mixin": "24.9.5",
|
|
||||||
"@vibrant/color": "4.0.0",
|
"@vibrant/color": "4.0.0",
|
||||||
"@vue/web-component-wrapper": "1.3.0",
|
"@vue/web-component-wrapper": "1.3.0",
|
||||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||||
@@ -114,7 +112,7 @@
|
|||||||
"hls.js": "1.6.15",
|
"hls.js": "1.6.15",
|
||||||
"home-assistant-js-websocket": "9.6.0",
|
"home-assistant-js-websocket": "9.6.0",
|
||||||
"idb-keyval": "6.2.2",
|
"idb-keyval": "6.2.2",
|
||||||
"intl-messageformat": "10.7.18",
|
"intl-messageformat": "11.0.2",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||||
@@ -135,7 +133,7 @@
|
|||||||
"stacktrace-js": "2.0.2",
|
"stacktrace-js": "2.0.2",
|
||||||
"superstruct": "2.0.2",
|
"superstruct": "2.0.2",
|
||||||
"tinykeys": "3.0.0",
|
"tinykeys": "3.0.0",
|
||||||
"ua-parser-js": "2.0.6",
|
"ua-parser-js": "2.0.7",
|
||||||
"vue": "2.7.16",
|
"vue": "2.7.16",
|
||||||
"vue2-daterange-picker": "0.6.8",
|
"vue2-daterange-picker": "0.6.8",
|
||||||
"weekstart": "2.0.0",
|
"weekstart": "2.0.0",
|
||||||
@@ -152,16 +150,16 @@
|
|||||||
"@babel/helper-define-polyfill-provider": "0.6.5",
|
"@babel/helper-define-polyfill-provider": "0.6.5",
|
||||||
"@babel/plugin-transform-runtime": "7.28.5",
|
"@babel/plugin-transform-runtime": "7.28.5",
|
||||||
"@babel/preset-env": "7.28.5",
|
"@babel/preset-env": "7.28.5",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.21.6",
|
"@bundle-stats/plugin-webpack-filter": "4.21.7",
|
||||||
"@lokalise/node-api": "15.4.0",
|
"@lokalise/node-api": "15.4.0",
|
||||||
"@octokit/auth-oauth-device": "8.0.3",
|
"@octokit/auth-oauth-device": "8.0.3",
|
||||||
"@octokit/plugin-retry": "8.0.3",
|
"@octokit/plugin-retry": "8.0.3",
|
||||||
"@octokit/rest": "22.0.1",
|
"@octokit/rest": "22.0.1",
|
||||||
"@rsdoctor/rspack-plugin": "1.3.11",
|
"@rsdoctor/rspack-plugin": "1.3.16",
|
||||||
"@rspack/core": "1.6.4",
|
"@rspack/core": "1.6.8",
|
||||||
"@rspack/dev-server": "1.1.4",
|
"@rspack/dev-server": "1.1.4",
|
||||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||||
"@types/chromecast-caf-receiver": "6.0.22",
|
"@types/chromecast-caf-receiver": "6.0.25",
|
||||||
"@types/chromecast-caf-sender": "1.0.11",
|
"@types/chromecast-caf-sender": "1.0.11",
|
||||||
"@types/color-name": "2.0.0",
|
"@types/color-name": "2.0.0",
|
||||||
"@types/culori": "4.0.1",
|
"@types/culori": "4.0.1",
|
||||||
@@ -178,12 +176,12 @@
|
|||||||
"@types/tar": "6.1.13",
|
"@types/tar": "6.1.13",
|
||||||
"@types/ua-parser-js": "0.7.39",
|
"@types/ua-parser-js": "0.7.39",
|
||||||
"@types/webspeechapi": "0.0.29",
|
"@types/webspeechapi": "0.0.29",
|
||||||
"@vitest/coverage-v8": "4.0.13",
|
"@vitest/coverage-v8": "4.0.16",
|
||||||
"babel-loader": "10.0.0",
|
"babel-loader": "10.0.0",
|
||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"browserslist-useragent-regexp": "4.1.3",
|
"browserslist-useragent-regexp": "4.1.3",
|
||||||
"del": "8.0.1",
|
"del": "8.0.1",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.2",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-import-resolver-webpack": "0.13.10",
|
"eslint-import-resolver-webpack": "0.13.10",
|
||||||
@@ -194,14 +192,14 @@
|
|||||||
"eslint-plugin-wc": "3.0.2",
|
"eslint-plugin-wc": "3.0.2",
|
||||||
"fancy-log": "2.0.0",
|
"fancy-log": "2.0.0",
|
||||||
"fs-extra": "11.3.2",
|
"fs-extra": "11.3.2",
|
||||||
"glob": "12.0.0",
|
"glob": "13.0.0",
|
||||||
"gulp": "5.0.1",
|
"gulp": "5.0.1",
|
||||||
"gulp-brotli": "3.0.0",
|
"gulp-brotli": "3.0.0",
|
||||||
"gulp-json-transform": "0.5.0",
|
"gulp-json-transform": "0.5.0",
|
||||||
"gulp-rename": "2.1.0",
|
"gulp-rename": "2.1.0",
|
||||||
"html-minifier-terser": "7.2.0",
|
"html-minifier-terser": "7.2.0",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"jsdom": "27.2.0",
|
"jsdom": "27.3.0",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lint-staged": "16.2.7",
|
"lint-staged": "16.2.7",
|
||||||
"lit-analyzer": "2.0.3",
|
"lit-analyzer": "2.0.3",
|
||||||
@@ -209,17 +207,17 @@
|
|||||||
"lodash.template": "4.5.0",
|
"lodash.template": "4.5.0",
|
||||||
"map-stream": "0.0.7",
|
"map-stream": "0.0.7",
|
||||||
"pinst": "3.0.0",
|
"pinst": "3.0.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.7.4",
|
||||||
"rspack-manifest-plugin": "5.2.0",
|
"rspack-manifest-plugin": "5.2.0",
|
||||||
"serve": "14.2.5",
|
"serve": "14.2.5",
|
||||||
"sinon": "21.0.0",
|
"sinon": "21.0.0",
|
||||||
"tar": "7.5.2",
|
"tar": "7.5.2",
|
||||||
"terser-webpack-plugin": "5.3.14",
|
"terser-webpack-plugin": "5.3.16",
|
||||||
"ts-lit-plugin": "2.0.2",
|
"ts-lit-plugin": "2.0.2",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.47.0",
|
"typescript-eslint": "8.50.0",
|
||||||
"vite-tsconfig-paths": "5.1.4",
|
"vite-tsconfig-paths": "6.0.2",
|
||||||
"vitest": "4.0.13",
|
"vitest": "4.0.16",
|
||||||
"webpack-stats-plugin": "1.1.3",
|
"webpack-stats-plugin": "1.1.3",
|
||||||
"webpackbar": "7.0.0",
|
"webpackbar": "7.0.0",
|
||||||
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
|
||||||
@@ -238,6 +236,6 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "yarn@4.12.0",
|
"packageManager": "yarn@4.12.0",
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.21.1"
|
"node": "24.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20251029.0"
|
version = "20251203.0"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
license-files = ["LICENSE*"]
|
license-files = ["LICENSE*"]
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { AuthData } from "home-assistant-js-websocket";
|
import type { AuthData } from "home-assistant-js-websocket";
|
||||||
import { extractSearchParam } from "../url/search-params";
|
import { extractSearchParam } from "../url/search-params";
|
||||||
|
import { hassUrl } from "../../data/auth";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -30,7 +31,11 @@ export function askWrite() {
|
|||||||
export function saveTokens(tokens: AuthData | null) {
|
export function saveTokens(tokens: AuthData | null) {
|
||||||
tokenCache.tokens = tokens;
|
tokenCache.tokens = tokens;
|
||||||
|
|
||||||
if (!tokenCache.writeEnabled && extractSearchParam("storeToken") === "true") {
|
if (
|
||||||
|
!tokenCache.writeEnabled &&
|
||||||
|
(extractSearchParam("storeToken") === "true" ||
|
||||||
|
hassUrl !== `${location.protocol}//${location.host}`)
|
||||||
|
) {
|
||||||
tokenCache.writeEnabled = true;
|
tokenCache.writeEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
DOMAIN_ATTRIBUTES_FORMATERS,
|
DOMAIN_ATTRIBUTES_FORMATERS,
|
||||||
DOMAIN_ATTRIBUTES_UNITS,
|
DOMAIN_ATTRIBUTES_UNITS,
|
||||||
TEMPERATURE_ATTRIBUTES,
|
TEMPERATURE_ATTRIBUTES,
|
||||||
} from "../../data/entity_attributes";
|
} from "../../data/entity/entity_attributes";
|
||||||
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||||
import type { FrontendLocaleData } from "../../data/translation";
|
import type { FrontendLocaleData } from "../../data/translation";
|
||||||
import type { WeatherEntity } from "../../data/weather";
|
import type { WeatherEntity } from "../../data/weather";
|
||||||
import { getWeatherUnit } from "../../data/weather";
|
import { getWeatherUnit } from "../../data/weather";
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
||||||
import type {
|
import type {
|
||||||
EntityRegistryDisplayEntry,
|
EntityRegistryDisplayEntry,
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
} from "../../data/entity_registry";
|
} from "../../data/entity/entity_registry";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { computeStateName } from "./compute_state_name";
|
|
||||||
import { getDuplicates } from "../string/get_duplicates";
|
import { getDuplicates } from "../string/get_duplicates";
|
||||||
|
import { computeStateName } from "./compute_state_name";
|
||||||
|
|
||||||
export const computeDeviceName = (
|
export const computeDeviceName = (
|
||||||
device: DeviceRegistryEntry
|
device: DeviceRegistryEntry
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
|
|||||||
import type {
|
import type {
|
||||||
EntityRegistryDisplayEntry,
|
EntityRegistryDisplayEntry,
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
} from "../../data/entity_registry";
|
} from "../../data/entity/entity_registry";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { computeDeviceName } from "./compute_device_name";
|
import { computeDeviceName } from "./compute_device_name";
|
||||||
import { computeStateName } from "./compute_state_name";
|
import { computeStateName } from "./compute_state_name";
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||||
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||||
import type { FrontendLocaleData } from "../../data/translation";
|
import type { FrontendLocaleData } from "../../data/translation";
|
||||||
import { TimeZone } from "../../data/translation";
|
import { TimeZone } from "../../data/translation";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { DURATION_UNITS, formatDuration } from "../datetime/format_duration";
|
|
||||||
import { formatDate } from "../datetime/format_date";
|
import { formatDate } from "../datetime/format_date";
|
||||||
import { formatDateTime } from "../datetime/format_date_time";
|
import { formatDateTime } from "../datetime/format_date_time";
|
||||||
|
import { DURATION_UNITS, formatDuration } from "../datetime/format_duration";
|
||||||
import { formatTime } from "../datetime/format_time";
|
import { formatTime } from "../datetime/format_time";
|
||||||
import {
|
import {
|
||||||
formatNumber,
|
formatNumber,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||||
import type {
|
import type {
|
||||||
EntityRegistryDisplayEntry,
|
EntityRegistryDisplayEntry,
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
ExtEntityRegistryEntry,
|
ExtEntityRegistryEntry,
|
||||||
} from "../../../data/entity_registry";
|
} from "../../../data/entity/entity_registry";
|
||||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
import type { IntegrationManifest } from "../../data/integration";
|
|
||||||
import { computeDomain } from "./compute_domain";
|
|
||||||
import { HELPERS_CRUD } from "../../data/helpers_crud";
|
|
||||||
import type { Helper } from "../../panels/config/helpers/const";
|
|
||||||
import { isHelperDomain } from "../../panels/config/helpers/const";
|
|
||||||
import { isComponentLoaded } from "../config/is_component_loaded";
|
|
||||||
import type { EntityRegistryEntry } from "../../data/entity_registry";
|
|
||||||
import { removeEntityRegistryEntry } from "../../data/entity_registry";
|
|
||||||
import type { ConfigEntry } from "../../data/config_entries";
|
import type { ConfigEntry } from "../../data/config_entries";
|
||||||
import { deleteConfigEntry } from "../../data/config_entries";
|
import { deleteConfigEntry } from "../../data/config_entries";
|
||||||
|
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||||
|
import { removeEntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||||
|
import { HELPERS_CRUD } from "../../data/helpers_crud";
|
||||||
|
import type { IntegrationManifest } from "../../data/integration";
|
||||||
|
import type { Helper } from "../../panels/config/helpers/const";
|
||||||
|
import { isHelperDomain } from "../../panels/config/helpers/const";
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import { isComponentLoaded } from "../config/is_component_loaded";
|
||||||
|
import { computeDomain } from "./compute_domain";
|
||||||
|
|
||||||
export const isDeletableEntity = (
|
export const isDeletableEntity = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { computeStateDomain } from "./compute_state_domain";
|
import { UNAVAILABLE_STATES } from "../../data/entity/entity";
|
||||||
import { UNAVAILABLE_STATES } from "../../data/entity";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { computeDomain } from "./compute_domain";
|
|
||||||
import { stringCompare } from "../string/compare";
|
import { stringCompare } from "../string/compare";
|
||||||
|
import { computeDomain } from "./compute_domain";
|
||||||
|
import { computeStateDomain } from "./compute_state_domain";
|
||||||
|
|
||||||
export const FIXED_DOMAIN_STATES = {
|
export const FIXED_DOMAIN_STATES = {
|
||||||
alarm_control_panel: [
|
alarm_control_panel: [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { computeStateDomain } from "./compute_state_domain";
|
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
|
||||||
import { isUnavailableState, UNAVAILABLE } from "../../data/entity";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import { computeStateDomain } from "./compute_state_domain";
|
||||||
|
|
||||||
export const computeGroupEntitiesState = (states: HassEntity[]): string => {
|
export const computeGroupEntitiesState = (states: HassEntity[]): string => {
|
||||||
if (!states.length) {
|
if (!states.length) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity";
|
import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity/entity";
|
||||||
import { computeDomain } from "./compute_domain";
|
import { computeDomain } from "./compute_domain";
|
||||||
|
|
||||||
export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { UNAVAILABLE } from "../../data/entity";
|
import { UNAVAILABLE } from "../../data/entity/entity";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { DOMAINS_WITH_CARD } from "../const";
|
import { DOMAINS_WITH_CARD } from "../const";
|
||||||
import { canToggleState } from "./can_toggle_state";
|
import { canToggleState } from "./can_toggle_state";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** Return a color representing a state. */
|
/** Return a color representing a state. */
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { UNAVAILABLE } from "../../data/entity";
|
import { UNAVAILABLE } from "../../data/entity/entity";
|
||||||
import type { GroupEntity } from "../../data/group";
|
import type { GroupEntity } from "../../data/group";
|
||||||
import { computeGroupDomain } from "../../data/group";
|
import { computeGroupDomain } from "../../data/group";
|
||||||
import { computeCssVariable } from "../../resources/css-variables";
|
import { computeCssVariable } from "../../resources/css-variables";
|
||||||
|
|||||||
@@ -17,25 +17,36 @@ export interface NavigateOptions {
|
|||||||
// max time to wait for dialogs to close before navigating
|
// max time to wait for dialogs to close before navigating
|
||||||
const DIALOG_WAIT_TIMEOUT = 500;
|
const DIALOG_WAIT_TIMEOUT = 500;
|
||||||
|
|
||||||
export const navigate = async (
|
/**
|
||||||
path: string,
|
* Ensures all dialogs are closed before navigation.
|
||||||
options?: NavigateOptions,
|
* Returns true if navigation can proceed, false if a dialog refused to close.
|
||||||
timestamp = Date.now()
|
*/
|
||||||
) => {
|
const ensureDialogsClosed = async (timestamp: number): Promise<boolean> => {
|
||||||
const { history } = mainWindow;
|
const { history } = mainWindow;
|
||||||
if (history.state?.dialog && Date.now() - timestamp < DIALOG_WAIT_TIMEOUT) {
|
|
||||||
|
if (!history.state?.dialog || Date.now() - timestamp >= DIALOG_WAIT_TIMEOUT) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const closed = await closeAllDialogs();
|
const closed = await closeAllDialogs();
|
||||||
if (!closed) {
|
if (!closed) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn("Navigation blocked, because dialog refused to close");
|
console.warn("Navigation blocked, because dialog refused to close");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return new Promise<boolean>((resolve) => {
|
|
||||||
// need to wait for history state to be updated in case a dialog was closed
|
// wait for history state to be updated after dialog closed
|
||||||
setTimeout(() => {
|
await new Promise<void>((resolve) => {
|
||||||
navigate(path, options, timestamp).then(resolve);
|
setTimeout(resolve);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return ensureDialogsClosed(timestamp);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const navigate = async (path: string, options?: NavigateOptions) => {
|
||||||
|
const canProceed = await ensureDialogsClosed(Date.now());
|
||||||
|
if (!canProceed) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
const replace = options?.replace || false;
|
const replace = options?.replace || false;
|
||||||
|
|
||||||
@@ -68,10 +79,14 @@ export const navigate = async (
|
|||||||
* Navigate back in history, with fallback to a default path if no history exists.
|
* Navigate back in history, with fallback to a default path if no history exists.
|
||||||
* This prevents a user from getting stuck when they navigate directly to a page with no history.
|
* This prevents a user from getting stuck when they navigate directly to a page with no history.
|
||||||
*/
|
*/
|
||||||
export const goBack = (fallbackPath?: string) => {
|
export const goBack = async (fallbackPath?: string): Promise<void> => {
|
||||||
const { history } = mainWindow;
|
const canProceed = await ensureDialogsClosed(Date.now());
|
||||||
|
if (!canProceed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we have history to go back to
|
// Check if we have history to go back to
|
||||||
|
const { history } = mainWindow;
|
||||||
if (history.length > 1) {
|
if (history.length > 1) {
|
||||||
history.back();
|
history.back();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type {
|
|||||||
HassEntity,
|
HassEntity,
|
||||||
HassEntityAttributeBase,
|
HassEntityAttributeBase,
|
||||||
} from "home-assistant-js-websocket";
|
} from "home-assistant-js-websocket";
|
||||||
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||||
import type { FrontendLocaleData } from "../../data/translation";
|
import type { FrontendLocaleData } from "../../data/translation";
|
||||||
import { NumberFormat } from "../../data/translation";
|
import { NumberFormat } from "../../data/translation";
|
||||||
import { round } from "./round";
|
import { round } from "./round";
|
||||||
|
|||||||
@@ -45,9 +45,8 @@ export const computeFormatFunctions = async (
|
|||||||
formatEntityAttributeName: FormatEntityAttributeNameFunc;
|
formatEntityAttributeName: FormatEntityAttributeNameFunc;
|
||||||
formatEntityName: FormatEntityNameFunc;
|
formatEntityName: FormatEntityNameFunc;
|
||||||
}> => {
|
}> => {
|
||||||
const { computeStateDisplay } = await import(
|
const { computeStateDisplay } =
|
||||||
"../entity/compute_state_display"
|
await import("../entity/compute_state_display");
|
||||||
);
|
|
||||||
const { computeAttributeValueDisplay, computeAttributeNameDisplay } =
|
const { computeAttributeValueDisplay, computeAttributeNameDisplay } =
|
||||||
await import("../entity/compute_attribute_display");
|
await import("../entity/compute_attribute_display");
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,45 @@
|
|||||||
/**
|
/**
|
||||||
* Executes a callback within a View Transition if supported, otherwise runs it directly.
|
* Executes a synchronous callback within a View Transition if supported, otherwise runs it directly.
|
||||||
*
|
*
|
||||||
* @param callback - Function to execute. Can be synchronous or return a Promise. The callback will be passed a boolean indicating whether the view transition is available.
|
* @param callback - Synchronous function to execute. The callback will be passed a boolean indicating whether the view transition is available.
|
||||||
* @returns Promise that resolves when the transition completes (or immediately if not supported)
|
* @returns Promise that resolves when the transition completes (or immediately if not supported)
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // Synchronous callback
|
|
||||||
* withViewTransition(() => {
|
* withViewTransition(() => {
|
||||||
* this.large = !this.large;
|
* this.large = !this.large;
|
||||||
* });
|
* });
|
||||||
*
|
|
||||||
* // Async callback
|
|
||||||
* await withViewTransition(async () => {
|
|
||||||
* await this.updateData();
|
|
||||||
* });
|
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const withViewTransition = (
|
export const withViewTransition = (
|
||||||
callback: (viewTransitionAvailable: boolean) => void | Promise<void>
|
callback: (viewTransitionAvailable: boolean) => void
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (document.startViewTransition) {
|
if (!document.startViewTransition) {
|
||||||
return document.startViewTransition(() => callback(true)).finished;
|
callback(false);
|
||||||
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Execute callback directly without transition
|
let callbackInvoked = false;
|
||||||
const result = callback(false);
|
|
||||||
return result instanceof Promise ? result : Promise.resolve();
|
try {
|
||||||
|
// View Transitions require DOM updates to happen synchronously within
|
||||||
|
// the callback. Execute the callback immediately (synchronously).
|
||||||
|
const transition = document.startViewTransition(() => {
|
||||||
|
callbackInvoked = true;
|
||||||
|
callback(true);
|
||||||
|
});
|
||||||
|
return transition.finished;
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn(
|
||||||
|
"View transition failed, falling back to direct execution.",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
// Make sure the callback is invoked exactly once.
|
||||||
|
if (!callbackInvoked) {
|
||||||
|
callback(false);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -593,6 +593,7 @@ export class HaChartBase extends LitElement {
|
|||||||
}
|
}
|
||||||
const options = {
|
const options = {
|
||||||
animation: !this._reducedMotion,
|
animation: !this._reducedMotion,
|
||||||
|
animationDuration: 500,
|
||||||
darkMode: this._themes.darkMode ?? false,
|
darkMode: this._themes.darkMode ?? false,
|
||||||
aria: { show: true },
|
aria: { show: true },
|
||||||
dataZoom: this._getDataZoomConfig(),
|
dataZoom: this._getDataZoomConfig(),
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ export class HaSankeyChart extends LitElement {
|
|||||||
curveness: 0.5,
|
curveness: 0.5,
|
||||||
},
|
},
|
||||||
layoutIterations: 0,
|
layoutIterations: 0,
|
||||||
|
animationDuration: 500,
|
||||||
label: {
|
label: {
|
||||||
formatter: (params) =>
|
formatter: (params) =>
|
||||||
data.nodes.find((node) => node.id === (params.data as Node).id)
|
data.nodes.find((node) => node.id === (params.data as Node).id)
|
||||||
@@ -279,6 +280,7 @@ export class HaSankeyChart extends LitElement {
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
max-width: 100%;
|
||||||
background: var(--ha-card-background, var(--card-background-color));
|
background: var(--ha-card-background, var(--card-background-color));
|
||||||
}
|
}
|
||||||
ha-chart-base {
|
ha-chart-base {
|
||||||
|
|||||||
207
src/components/chart/ha-sunburst-chart.ts
Normal file
207
src/components/chart/ha-sunburst-chart.ts
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
import type { EChartsType } from "echarts/core";
|
||||||
|
import type { SunburstSeriesOption } from "echarts/types/dist/echarts";
|
||||||
|
import type { CallbackDataParams } from "echarts/types/src/util/types";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||||
|
import { filterXSS } from "../../common/util/xss";
|
||||||
|
import type { ECOption } from "../../resources/echarts/echarts";
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import "./ha-chart-base";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
|
||||||
|
let SunburstChart: typeof import("echarts/lib/chart/sunburst/install");
|
||||||
|
|
||||||
|
export interface SunburstNode {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
value: number;
|
||||||
|
itemStyle?: {
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
children?: SunburstNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("ha-sunburst-chart")
|
||||||
|
export class HaSunburstChart extends LitElement {
|
||||||
|
public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public data?: SunburstNode;
|
||||||
|
|
||||||
|
@property({ type: String, attribute: false }) public valueFormatter?: (
|
||||||
|
value: number
|
||||||
|
) => string;
|
||||||
|
|
||||||
|
public chart?: EChartsType;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
if (!SunburstChart) {
|
||||||
|
import("echarts/lib/chart/sunburst/install").then((module) => {
|
||||||
|
SunburstChart = module;
|
||||||
|
this.requestUpdate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!SunburstChart || !this.data) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "item",
|
||||||
|
formatter: this._renderTooltip,
|
||||||
|
appendTo: document.body,
|
||||||
|
},
|
||||||
|
} as ECOption;
|
||||||
|
|
||||||
|
return html`<ha-chart-base
|
||||||
|
.data=${this._createData(this.data)}
|
||||||
|
.options=${options}
|
||||||
|
height="100%"
|
||||||
|
.extraComponents=${[SunburstChart]}
|
||||||
|
></ha-chart-base>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderTooltip = (params: CallbackDataParams) => {
|
||||||
|
const data = params.data as { name: string; value: number };
|
||||||
|
const value = this.valueFormatter
|
||||||
|
? this.valueFormatter(data.value)
|
||||||
|
: data.value;
|
||||||
|
return `${params.marker} ${filterXSS(data.name)}<br>${value}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _createData = memoizeOne(
|
||||||
|
(data: SunburstNode): SunburstSeriesOption => {
|
||||||
|
const computedStyles = getComputedStyle(this);
|
||||||
|
|
||||||
|
// Transform to echarts format (uses 'name' instead of 'id')
|
||||||
|
const transformNode = (
|
||||||
|
node: SunburstNode,
|
||||||
|
index: number,
|
||||||
|
depth: number,
|
||||||
|
parentColor?: string
|
||||||
|
) => {
|
||||||
|
const result = {
|
||||||
|
...node,
|
||||||
|
name: node.name || node.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (depth > 0 && !node.itemStyle?.color) {
|
||||||
|
// Don't assign color to root node
|
||||||
|
result.itemStyle = {
|
||||||
|
color: parentColor ?? getGraphColorByIndex(index, computedStyles),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children && node.children.length > 0) {
|
||||||
|
result.children = node.children.map((child, i) =>
|
||||||
|
transformNode(child, i, depth + 1, result.itemStyle?.color)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformedData = transformNode(data, 0, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "sunburst",
|
||||||
|
data: transformedData.children || [transformedData],
|
||||||
|
radius: [0, "90%"],
|
||||||
|
sort: undefined, // Keep original order
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
align: "center",
|
||||||
|
rotate: "radial",
|
||||||
|
minAngle: 15,
|
||||||
|
hideOverlap: true,
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
focus: "ancestor",
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: 2,
|
||||||
|
},
|
||||||
|
levels: this._generateLevels(this._getMaxDepth(data)),
|
||||||
|
} as SunburstSeriesOption;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _getMaxDepth(node: SunburstNode, currentDepth = 0): number {
|
||||||
|
if (!node.children || node.children.length === 0) {
|
||||||
|
return currentDepth;
|
||||||
|
}
|
||||||
|
return Math.max(
|
||||||
|
...node.children.map((child) =>
|
||||||
|
this._getMaxDepth(child, currentDepth + 1)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _generateLevels(depth: number): SunburstSeriesOption["levels"] {
|
||||||
|
const levels: SunburstSeriesOption["levels"] = [];
|
||||||
|
|
||||||
|
// Root level (center) - transparent, small fixed size
|
||||||
|
const rootRadius = 15;
|
||||||
|
const outerRadius = 95;
|
||||||
|
const availableRadius = outerRadius - rootRadius;
|
||||||
|
|
||||||
|
levels.push({
|
||||||
|
r0: "0%",
|
||||||
|
r: `${rootRadius}%`,
|
||||||
|
itemStyle: {
|
||||||
|
color: "transparent",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (depth === 0) {
|
||||||
|
return levels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distribute remaining radius among data levels using weighted distribution
|
||||||
|
// First level gets most space, each subsequent level gets progressively smaller
|
||||||
|
const weights = Array.from({ length: depth }, (_, i) => depth - i);
|
||||||
|
const totalWeight = weights.reduce((sum, w) => sum + w, 0);
|
||||||
|
|
||||||
|
let currentRadius = rootRadius;
|
||||||
|
for (let i = 0; i < depth; i++) {
|
||||||
|
const levelRadius = (weights[i] / totalWeight) * availableRadius;
|
||||||
|
const r0 = currentRadius;
|
||||||
|
const r = currentRadius + levelRadius;
|
||||||
|
currentRadius = r;
|
||||||
|
|
||||||
|
levels.push({
|
||||||
|
r0: `${r0}%`,
|
||||||
|
r: `${r}%`,
|
||||||
|
// Show labels only on first level
|
||||||
|
...(i === 0 ? { label: { show: true } } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return levels;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
ha-chart-base {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-sunburst-chart": HaSunburstChart;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PropertyValues } from "lit";
|
import type { PropertyValues } from "lit";
|
||||||
import { html, LitElement } 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 { VisualMapComponentOption } from "echarts/components";
|
||||||
import type { LineSeriesOption } from "echarts/charts";
|
import type { LineSeriesOption } from "echarts/charts";
|
||||||
import type { YAXisOption } from "echarts/types/dist/shared";
|
import type { YAXisOption } from "echarts/types/dist/shared";
|
||||||
@@ -27,6 +27,7 @@ const safeParseFloat = (value) => {
|
|||||||
return isFinite(parsed) ? parsed : null;
|
return isFinite(parsed) ? parsed : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@customElement("state-history-chart-line")
|
||||||
export class StateHistoryChartLine extends LitElement {
|
export class StateHistoryChartLine extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@@ -795,7 +796,6 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
return Math.abs(value) < 1 ? value : roundingFn(value);
|
return Math.abs(value) < 1 ? value : roundingFn(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
customElements.define("state-history-chart-line", StateHistoryChartLine);
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
|||||||
itemName: 3,
|
itemName: 3,
|
||||||
},
|
},
|
||||||
renderItem: this._renderItem,
|
renderItem: this._renderItem,
|
||||||
|
progressive: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -184,17 +184,11 @@ export class StatisticsChart extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _datasetHidden(ev: CustomEvent) {
|
private _datasetHidden(ev: CustomEvent) {
|
||||||
if (!this._legendData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._hiddenStats.add(ev.detail.id);
|
this._hiddenStats.add(ev.detail.id);
|
||||||
this.requestUpdate("_hiddenStats");
|
this.requestUpdate("_hiddenStats");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _datasetUnhidden(ev: CustomEvent) {
|
private _datasetUnhidden(ev: CustomEvent) {
|
||||||
if (!this._legendData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._hiddenStats.delete(ev.detail.id);
|
this._hiddenStats.delete(ev.detail.id);
|
||||||
this.requestUpdate("_hiddenStats");
|
this.requestUpdate("_hiddenStats");
|
||||||
}
|
}
|
||||||
@@ -521,7 +515,9 @@ export class StatisticsChart extends LitElement {
|
|||||||
`ui.components.statistics_charts.statistic_types.${type}`
|
`ui.components.statistics_charts.statistic_types.${type}`
|
||||||
),
|
),
|
||||||
symbol: "none",
|
symbol: "none",
|
||||||
sampling: "minmax",
|
// minmax sampling operates independently per series, breaking stacking alignment
|
||||||
|
// https://github.com/apache/echarts/issues/11879
|
||||||
|
sampling: band && drawBands ? "lttb" : "minmax",
|
||||||
animationDurationUpdate: 0,
|
animationDurationUpdate: 0,
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
@@ -539,12 +535,19 @@ export class StatisticsChart extends LitElement {
|
|||||||
if (band && this.chartType === "line") {
|
if (band && this.chartType === "line") {
|
||||||
series.stack = `band-${statistic_id}`;
|
series.stack = `band-${statistic_id}`;
|
||||||
series.stackStrategy = "all";
|
series.stackStrategy = "all";
|
||||||
|
if (this._hiddenStats.has(`${statistic_id}-${bandBottom}`)) {
|
||||||
|
// changing the stackOrder forces echarts to render the stacked series that are not hidden #28472
|
||||||
|
series.stackOrder = "seriesDesc";
|
||||||
|
(series as LineSeriesOption).areaStyle = undefined;
|
||||||
|
} else {
|
||||||
|
series.stackOrder = "seriesAsc";
|
||||||
if (drawBands && type === bandTop) {
|
if (drawBands && type === bandTop) {
|
||||||
(series as LineSeriesOption).areaStyle = {
|
(series as LineSeriesOption).areaStyle = {
|
||||||
color: color + "3F",
|
color: color + "3F",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!this.hideLegend) {
|
if (!this.hideLegend) {
|
||||||
const showLegend = hasMean
|
const showLegend = hasMean
|
||||||
? type === "mean"
|
? type === "mean"
|
||||||
@@ -586,7 +589,8 @@ export class StatisticsChart extends LitElement {
|
|||||||
} else if (
|
} else if (
|
||||||
type === bandTop &&
|
type === bandTop &&
|
||||||
this.chartType === "line" &&
|
this.chartType === "line" &&
|
||||||
drawBands
|
drawBands &&
|
||||||
|
!this._hiddenStats.has(`${statistic_id}-${bandBottom}`)
|
||||||
) {
|
) {
|
||||||
const top = stat[bandTop] || 0;
|
const top = stat[bandTop] || 0;
|
||||||
val.push(Math.abs(top - (stat[bandBottom] || 0)));
|
val.push(Math.abs(top - (stat[bandBottom] || 0)));
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { getGraphColorByIndex } from "../../common/color/colors";
|
|||||||
import { hex2rgb, lab2hex, rgb2lab } from "../../common/color/convert-color";
|
import { hex2rgb, lab2hex, rgb2lab } from "../../common/color/convert-color";
|
||||||
import { labBrighten } from "../../common/color/lab";
|
import { labBrighten } from "../../common/color/lab";
|
||||||
import { computeDomain } from "../../common/entity/compute_domain";
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
import { stateColorProperties } from "../../common/entity/state_color";
|
|
||||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
|
||||||
import { computeCssValue } from "../../resources/css-variables";
|
|
||||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||||
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
|
||||||
|
import { stateColorProperties } from "../../common/entity/state_color";
|
||||||
|
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
|
||||||
|
import { computeCssValue } from "../../resources/css-variables";
|
||||||
|
|
||||||
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
|
const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = {
|
||||||
media_player: {
|
media_player: {
|
||||||
|
|||||||
@@ -2,15 +2,17 @@ import type { TemplateResult } from "lit";
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import type { LabelRegistryEntry } from "../../data/label_registry";
|
|
||||||
import { computeCssColor } from "../../common/color/compute-color";
|
import { computeCssColor } from "../../common/color/compute-color";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import "../ha-label";
|
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||||
import { stringCompare } from "../../common/string/compare";
|
import { stringCompare } from "../../common/string/compare";
|
||||||
|
import type { LabelRegistryEntry } from "../../data/label/label_registry";
|
||||||
import "../chips/ha-chip-set";
|
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-icon";
|
||||||
import "../ha-list-item";
|
import "../ha-label";
|
||||||
|
|
||||||
@customElement("ha-data-table-labels")
|
@customElement("ha-data-table-labels")
|
||||||
class HaDataTableLabels extends LitElement {
|
class HaDataTableLabels extends LitElement {
|
||||||
@@ -26,12 +28,11 @@ class HaDataTableLabels extends LitElement {
|
|||||||
(label) => this._renderLabel(label, true)
|
(label) => this._renderLabel(label, true)
|
||||||
)}
|
)}
|
||||||
${labels.length > 2
|
${labels.length > 2
|
||||||
? html`<ha-button-menu
|
? html`<ha-dropdown
|
||||||
absolute
|
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click=${this._handleIconOverflowMenuOpened}
|
@click=${stopPropagation}
|
||||||
@closed=${this._handleIconOverflowMenuClosed}
|
@wa-select=${this._handleDropdownSelect}
|
||||||
>
|
>
|
||||||
<ha-label slot="trigger" class="plus" dense>
|
<ha-label slot="trigger" class="plus" dense>
|
||||||
+${labels.length - 2}
|
+${labels.length - 2}
|
||||||
@@ -40,12 +41,12 @@ class HaDataTableLabels extends LitElement {
|
|||||||
labels.slice(2),
|
labels.slice(2),
|
||||||
(label) => label.label_id,
|
(label) => label.label_id,
|
||||||
(label) => html`
|
(label) => html`
|
||||||
<ha-list-item @click=${this._labelClicked} .item=${label}>
|
<ha-dropdown-item .value=${label.label_id} .item=${label}>
|
||||||
${this._renderLabel(label, false)}
|
${this._renderLabel(label, false)}
|
||||||
</ha-list-item>
|
</ha-dropdown-item>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</ha-button-menu>`
|
</ha-dropdown>`
|
||||||
: nothing}
|
: nothing}
|
||||||
</ha-chip-set>
|
</ha-chip-set>
|
||||||
`;
|
`;
|
||||||
@@ -81,21 +82,12 @@ class HaDataTableLabels extends LitElement {
|
|||||||
fireEvent(this, "label-clicked", { label });
|
fireEvent(this, "label-clicked", { label });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _handleIconOverflowMenuOpened(e) {
|
private _handleDropdownSelect(
|
||||||
e.stopPropagation();
|
ev: CustomEvent<{ item: HaDropdownItem & { item?: LabelRegistryEntry } }>
|
||||||
// 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
|
const label = ev.detail?.item?.item;
|
||||||
// underneath the next row in the table.
|
if (label) {
|
||||||
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
|
fireEvent(this, "label-clicked", { label });
|
||||||
if (row) {
|
|
||||||
row.style.zIndex = "1";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _handleIconOverflowMenuClosed() {
|
|
||||||
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
|
|
||||||
if (row) {
|
|
||||||
row.style.zIndex = "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,9 +106,6 @@ class HaDataTableLabels extends LitElement {
|
|||||||
--ha-label-background-color: var(--color, var(--grey-color));
|
--ha-label-background-color: var(--color, var(--grey-color));
|
||||||
--ha-label-background-opacity: 0.5;
|
--ha-label-background-opacity: 0.5;
|
||||||
}
|
}
|
||||||
ha-button-menu {
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
.plus {
|
.plus {
|
||||||
--ha-label-background-color: transparent;
|
--ha-label-background-color: transparent;
|
||||||
border: 1px solid var(--divider-color);
|
border: 1px solid var(--divider-color);
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ import memoizeOne from "memoize-one";
|
|||||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { stringCompare } from "../../common/string/compare";
|
import { stringCompare } from "../../common/string/compare";
|
||||||
|
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||||
import { debounce } from "../../common/util/debounce";
|
import { debounce } from "../../common/util/debounce";
|
||||||
import { groupBy } from "../../common/util/group-by";
|
import { groupBy } from "../../common/util/group-by";
|
||||||
|
import { nextRender } from "../../common/util/render-status";
|
||||||
import { haStyleScrollbar } from "../../resources/styles";
|
import { haStyleScrollbar } from "../../resources/styles";
|
||||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
@@ -26,8 +28,6 @@ import type { HaCheckbox } from "../ha-checkbox";
|
|||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "../search-input";
|
import "../search-input";
|
||||||
import { filterData, sortData } from "./sort-filter";
|
import { filterData, sortData } from "./sort-filter";
|
||||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
|
||||||
import { nextRender } from "../../common/util/render-status";
|
|
||||||
|
|
||||||
export interface RowClickedEvent {
|
export interface RowClickedEvent {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -838,10 +838,10 @@ export class HaDataTable extends LitElement {
|
|||||||
} else if (this.sortDirection === "asc") {
|
} else if (this.sortDirection === "asc") {
|
||||||
this.sortDirection = "desc";
|
this.sortDirection = "desc";
|
||||||
} else {
|
} else {
|
||||||
this.sortDirection = null;
|
this.sortDirection = "asc";
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sortColumn = this.sortDirection === null ? undefined : columnId;
|
this.sortColumn = columnId;
|
||||||
|
|
||||||
fireEvent(this, "sorting-changed", {
|
fireEvent(this, "sorting-changed", {
|
||||||
column: columnId,
|
column: columnId,
|
||||||
@@ -1402,6 +1402,9 @@ export class HaDataTable extends LitElement {
|
|||||||
}
|
}
|
||||||
.secondary {
|
.secondary {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.scroller {
|
.scroller {
|
||||||
height: calc(100% - 57px);
|
height: calc(100% - 57px);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { expose } from "comlink";
|
import { expose } from "comlink";
|
||||||
import Fuse from "fuse.js";
|
import Fuse, { type FuseOptionKey } from "fuse.js";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { ipCompare, stringCompare } from "../../common/string/compare";
|
import { ipCompare, stringCompare } from "../../common/string/compare";
|
||||||
import { stripDiacritics } from "../../common/string/strip-diacritics";
|
import { stripDiacritics } from "../../common/string/strip-diacritics";
|
||||||
import { HaFuse } from "../../resources/fuse";
|
import { multiTermSearch } from "../../resources/fuseMultiTerm";
|
||||||
import type {
|
import type {
|
||||||
ClonedDataTableColumnData,
|
ClonedDataTableColumnData,
|
||||||
DataTableRowData,
|
DataTableRowData,
|
||||||
@@ -11,9 +11,10 @@ import type {
|
|||||||
SortingDirection,
|
SortingDirection,
|
||||||
} from "./ha-data-table";
|
} from "./ha-data-table";
|
||||||
|
|
||||||
const fuseIndex = memoizeOne(
|
const getSearchKeys = memoizeOne(
|
||||||
(data: DataTableRowData[], columns: SortableColumnContainer) => {
|
(columns: SortableColumnContainer): FuseOptionKey<DataTableRowData>[] => {
|
||||||
const searchKeys = new Set<string>();
|
const searchKeys = new Set<string>();
|
||||||
|
|
||||||
Object.entries(columns).forEach(([key, column]) => {
|
Object.entries(columns).forEach(([key, column]) => {
|
||||||
if (column.filterable) {
|
if (column.filterable) {
|
||||||
searchKeys.add(
|
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 = (
|
const filterData = (
|
||||||
data: DataTableRowData[],
|
data: DataTableRowData[],
|
||||||
columns: SortableColumnContainer,
|
columns: SortableColumnContainer,
|
||||||
@@ -38,21 +44,13 @@ const filterData = (
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = fuseIndex(data, columns);
|
const keys = getSearchKeys(columns);
|
||||||
|
|
||||||
const fuse = new HaFuse(
|
const index = fuseIndex(data, keys);
|
||||||
data,
|
|
||||||
{ shouldSort: false, minMatchCharLength: 1 },
|
|
||||||
index
|
|
||||||
);
|
|
||||||
|
|
||||||
const searchResults = fuse.multiTermsSearch(filter);
|
return multiTermSearch<DataTableRowData>(data, filter, keys, index, {
|
||||||
|
threshold: 0.2, // reduce fuzzy matches in data tables
|
||||||
if (searchResults) {
|
});
|
||||||
return searchResults.map((result) => result.item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortData = (
|
const sortData = (
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ const Component = Vue.extend({
|
|||||||
type: String,
|
type: String,
|
||||||
default: "en",
|
default: "en",
|
||||||
},
|
},
|
||||||
|
opensVertical: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
@@ -129,6 +133,11 @@ const Component = Vue.extend({
|
|||||||
},
|
},
|
||||||
expression: "dateRange",
|
expression: "dateRange",
|
||||||
},
|
},
|
||||||
|
on: {
|
||||||
|
toggle: (open: boolean) => {
|
||||||
|
fireEvent(this.$el as HTMLElement, "toggle", { open });
|
||||||
|
},
|
||||||
|
},
|
||||||
scopedSlots: {
|
scopedSlots: {
|
||||||
input() {
|
input() {
|
||||||
return createElement("slot", {
|
return createElement("slot", {
|
||||||
@@ -309,6 +318,10 @@ class DateRangePickerElement extends WrappedElement {
|
|||||||
min-width: unset !important;
|
min-width: unset !important;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
:host([opens-vertical="up"]) .daterangepicker {
|
||||||
|
bottom: 100%;
|
||||||
|
top: auto !important;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
if (mainWindow.document.dir === "rtl") {
|
if (mainWindow.document.dir === "rtl") {
|
||||||
style.innerHTML += `
|
style.innerHTML += `
|
||||||
@@ -340,4 +353,7 @@ declare global {
|
|||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"date-range-picker": DateRangePickerElement;
|
"date-range-picker": DateRangePickerElement;
|
||||||
}
|
}
|
||||||
|
interface HASSDomEvents {
|
||||||
|
toggle: { open: boolean };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import type { DeviceAction } from "../../data/device_automation";
|
import type { DeviceAction } from "../../data/device/device_automation";
|
||||||
import {
|
import {
|
||||||
fetchDeviceActions,
|
fetchDeviceActions,
|
||||||
localizeDeviceAutomationAction,
|
localizeDeviceAutomationAction,
|
||||||
} from "../../data/device_automation";
|
} from "../../data/device/device_automation";
|
||||||
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||||
|
|
||||||
@customElement("ha-device-action-picker")
|
@customElement("ha-device-action-picker")
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ import { consume } from "@lit/context";
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { property, state } from "lit/decorators";
|
import { property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||||
import { fullEntitiesContext } from "../../data/context";
|
import { fullEntitiesContext } from "../../data/context";
|
||||||
import type { DeviceAutomation } from "../../data/device_automation";
|
import type { DeviceAutomation } from "../../data/device/device_automation";
|
||||||
import {
|
import {
|
||||||
deviceAutomationsEqual,
|
deviceAutomationsEqual,
|
||||||
sortDeviceAutomations,
|
sortDeviceAutomations,
|
||||||
} from "../../data/device_automation";
|
} from "../../data/device/device_automation";
|
||||||
import type { EntityRegistryEntry } from "../../data/entity_registry";
|
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../ha-md-select-option";
|
|
||||||
import "../ha-md-select";
|
import "../ha-md-select";
|
||||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
import "../ha-md-select-option";
|
||||||
|
|
||||||
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
||||||
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
|
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import type { DeviceCondition } from "../../data/device_automation";
|
import type { DeviceCondition } from "../../data/device/device_automation";
|
||||||
import {
|
import {
|
||||||
fetchDeviceConditions,
|
fetchDeviceConditions,
|
||||||
localizeDeviceAutomationCondition,
|
localizeDeviceAutomationCondition,
|
||||||
} from "../../data/device_automation";
|
} from "../../data/device/device_automation";
|
||||||
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||||
|
|
||||||
@customElement("ha-device-condition-picker")
|
@customElement("ha-device-condition-picker")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
@@ -9,10 +9,11 @@ import { computeDeviceName } from "../../common/entity/compute_device_name";
|
|||||||
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
||||||
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
|
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
|
||||||
import {
|
import {
|
||||||
|
deviceComboBoxKeys,
|
||||||
getDevices,
|
getDevices,
|
||||||
type DevicePickerItem,
|
type DevicePickerItem,
|
||||||
type DeviceRegistryEntry,
|
} from "../../data/device/device_picker";
|
||||||
} from "../../data/device_registry";
|
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { brandsUrl } from "../../util/brands-url";
|
import { brandsUrl } from "../../util/brands-url";
|
||||||
import "../ha-generic-picker";
|
import "../ha-generic-picker";
|
||||||
@@ -161,7 +162,7 @@ export class HaDevicePicker extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private _rowRenderer: ComboBoxLitRenderer<DevicePickerItem> = (item) => html`
|
private _rowRenderer: RenderItemFunction<DevicePickerItem> = (item) => html`
|
||||||
<ha-combo-box-item type="button">
|
<ha-combo-box-item type="button">
|
||||||
${item.domain
|
${item.domain
|
||||||
? html`
|
? html`
|
||||||
@@ -204,6 +205,8 @@ export class HaDevicePicker extends LitElement {
|
|||||||
<ha-generic-picker
|
<ha-generic-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.helper=${this.helper}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
.searchLabel=${this.searchLabel}
|
.searchLabel=${this.searchLabel}
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
@@ -216,6 +219,10 @@ export class HaDevicePicker extends LitElement {
|
|||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.hideClearIcon=${this.hideClearIcon}
|
.hideClearIcon=${this.hideClearIcon}
|
||||||
.valueRenderer=${valueRenderer}
|
.valueRenderer=${valueRenderer}
|
||||||
|
.searchKeys=${deviceComboBoxKeys}
|
||||||
|
.unknownItemText=${this.hass.localize(
|
||||||
|
"ui.components.device-picker.unknown"
|
||||||
|
)}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
import type { DeviceTrigger } from "../../data/device_automation";
|
import type { DeviceTrigger } from "../../data/device/device_automation";
|
||||||
import {
|
import {
|
||||||
fetchDeviceTriggers,
|
fetchDeviceTriggers,
|
||||||
localizeDeviceAutomationTrigger,
|
localizeDeviceAutomationTrigger,
|
||||||
} from "../../data/device_automation";
|
} from "../../data/device/device_automation";
|
||||||
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||||
|
|
||||||
@customElement("ha-device-trigger-picker")
|
@customElement("ha-device-trigger-picker")
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ class HaDevicesPicker extends LitElement {
|
|||||||
(entityId) => html`
|
(entityId) => html`
|
||||||
<div>
|
<div>
|
||||||
<ha-device-picker
|
<ha-device-picker
|
||||||
allow-custom-entity
|
|
||||||
.curValue=${entityId}
|
.curValue=${entityId}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.deviceFilter=${this.deviceFilter}
|
.deviceFilter=${this.deviceFilter}
|
||||||
@@ -79,7 +78,6 @@ class HaDevicesPicker extends LitElement {
|
|||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<ha-device-picker
|
<ha-device-picker
|
||||||
allow-custom-entity
|
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.deviceFilter=${this.deviceFilter}
|
.deviceFilter=${this.deviceFilter}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { customElement, property } from "lit/decorators";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||||
import "../ha-sortable";
|
import "../ha-sortable";
|
||||||
import "./ha-entity-picker";
|
import "./ha-entity-picker";
|
||||||
@@ -99,7 +99,6 @@ class HaEntitiesPicker extends LitElement {
|
|||||||
(entityId) => html`
|
(entityId) => html`
|
||||||
<div class="entity">
|
<div class="entity">
|
||||||
<ha-entity-picker
|
<ha-entity-picker
|
||||||
allow-custom-entity
|
|
||||||
.curValue=${entityId}
|
.curValue=${entityId}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.includeDomains=${this.includeDomains}
|
.includeDomains=${this.includeDomains}
|
||||||
@@ -129,7 +128,6 @@ class HaEntitiesPicker extends LitElement {
|
|||||||
</ha-sortable>
|
</ha-sortable>
|
||||||
<div>
|
<div>
|
||||||
<ha-entity-picker
|
<ha-entity-picker
|
||||||
allow-custom-entity
|
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.includeDomains=${this.includeDomains}
|
.includeDomains=${this.includeDomains}
|
||||||
.excludeDomains=${this.excludeDomains}
|
.excludeDomains=${this.excludeDomains}
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import type { PropertyValues } from "lit";
|
|
||||||
import { LitElement, html, nothing } from "lit";
|
import { LitElement, html, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||||
import "../ha-combo-box";
|
import "../ha-generic-picker";
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||||
|
|
||||||
interface AttributeOption {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ha-entity-attribute-picker")
|
@customElement("ha-entity-attribute-picker")
|
||||||
class HaEntityAttributePicker extends LitElement {
|
class HaEntityAttributePicker extends LitElement {
|
||||||
@@ -42,51 +37,44 @@ class HaEntityAttributePicker extends LitElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
@state() private _opened = false;
|
private _getItemsMemoized = memoizeOne(
|
||||||
|
(
|
||||||
|
entityId: string | string[] | undefined,
|
||||||
|
hideAttributes: string[] | undefined,
|
||||||
|
hass: HomeAssistant
|
||||||
|
): PickerComboBoxItem[] => {
|
||||||
|
const entityIds = entityId ? ensureArray(entityId) : [];
|
||||||
|
const options: PickerComboBoxItem[] = [];
|
||||||
|
const optionsSet = new Set<string>();
|
||||||
|
|
||||||
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
for (const id of entityIds) {
|
||||||
|
const stateObj = hass.states[id];
|
||||||
protected shouldUpdate(changedProps: PropertyValues) {
|
|
||||||
return !(!changedProps.has("_opened") && this._opened);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
|
||||||
if (
|
|
||||||
(changedProps.has("_opened") && this._opened) ||
|
|
||||||
changedProps.has("entityId") ||
|
|
||||||
changedProps.has("attribute")
|
|
||||||
) {
|
|
||||||
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
|
|
||||||
const entitiesOptions = entityIds.map<AttributeOption[]>((entityId) => {
|
|
||||||
const stateObj = this.hass.states[entityId];
|
|
||||||
if (!stateObj) {
|
if (!stateObj) {
|
||||||
return [];
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attributes = Object.keys(stateObj.attributes).filter(
|
const attributes = Object.keys(stateObj.attributes).filter(
|
||||||
(a) => !this.hideAttributes?.includes(a)
|
(a) => !hideAttributes?.includes(a)
|
||||||
);
|
);
|
||||||
|
|
||||||
return attributes.map((a) => ({
|
for (const attribute of attributes) {
|
||||||
value: a,
|
if (!optionsSet.has(attribute)) {
|
||||||
label: this.hass.formatEntityAttributeName(stateObj, a),
|
optionsSet.add(attribute);
|
||||||
}));
|
options.push({
|
||||||
|
id: attribute,
|
||||||
|
primary: hass.formatEntityAttributeName(stateObj, attribute),
|
||||||
|
sorting_label: attribute,
|
||||||
});
|
});
|
||||||
|
|
||||||
const options: AttributeOption[] = [];
|
|
||||||
const optionsSet = new Set<string>();
|
|
||||||
for (const entityOptions of entitiesOptions) {
|
|
||||||
for (const option of entityOptions) {
|
|
||||||
if (!optionsSet.has(option.value)) {
|
|
||||||
optionsSet.add(option.value);
|
|
||||||
options.push(option);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(this._comboBox as any).filteredItems = options;
|
return options;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _getItems = () =>
|
||||||
|
this._getItemsMemoized(this.entityId, this.hideAttributes, this.hass);
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
@@ -94,10 +82,9 @@ class HaEntityAttributePicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-combo-box
|
<ha-generic-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.autofocus=${this.autofocus}
|
|
||||||
.label=${this.label ??
|
.label=${this.label ??
|
||||||
this.hass.localize(
|
this.hass.localize(
|
||||||
"ui.components.entity.entity-attribute-picker.attribute"
|
"ui.components.entity.entity-attribute-picker.attribute"
|
||||||
@@ -106,38 +93,21 @@ class HaEntityAttributePicker extends LitElement {
|
|||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.allowCustomValue=${this.allowCustomValue}
|
.allowCustomValue=${this.allowCustomValue}
|
||||||
item-id-path="value"
|
.getItems=${this._getItems}
|
||||||
item-value-path="value"
|
|
||||||
item-label-path="label"
|
|
||||||
@opened-changed=${this._openedChanged}
|
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-combo-box>
|
</ha-generic-picker>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _value() {
|
|
||||||
return this.value || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
|
||||||
this._opened = ev.detail.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const newValue = ev.detail.value;
|
const newValue = ev.detail.value;
|
||||||
if (newValue !== this._value) {
|
if (newValue !== this.value) {
|
||||||
this._setValue(newValue);
|
this.value = newValue;
|
||||||
}
|
fireEvent(this, "value-changed", { value: newValue });
|
||||||
}
|
|
||||||
|
|
||||||
private _setValue(value: string) {
|
|
||||||
this.value = value;
|
|
||||||
setTimeout(() => {
|
|
||||||
fireEvent(this, "value-changed", { value });
|
|
||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}, 0);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import "@material/mwc-menu/mwc-menu-surface";
|
|
||||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||||
import type { IFuseOptions } from "fuse.js";
|
|
||||||
import Fuse from "fuse.js";
|
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
|
||||||
import type { EntityNameItem } from "../../common/entity/compute_entity_name_display";
|
import type { EntityNameItem } from "../../common/entity/compute_entity_name_display";
|
||||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||||
import type { EntityNameType } from "../../common/translations/entity-state";
|
import type { EntityNameType } from "../../common/translations/entity-state";
|
||||||
@@ -18,20 +14,18 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
|||||||
import "../chips/ha-assist-chip";
|
import "../chips/ha-assist-chip";
|
||||||
import "../chips/ha-chip-set";
|
import "../chips/ha-chip-set";
|
||||||
import "../chips/ha-input-chip";
|
import "../chips/ha-input-chip";
|
||||||
import "../ha-combo-box";
|
import "../ha-combo-box-item";
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
import "../ha-generic-picker";
|
||||||
|
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||||
import "../ha-input-helper-text";
|
import "../ha-input-helper-text";
|
||||||
|
import {
|
||||||
|
NO_ITEMS_AVAILABLE_ID,
|
||||||
|
type PickerComboBoxItem,
|
||||||
|
} from "../ha-picker-combo-box";
|
||||||
import "../ha-sortable";
|
import "../ha-sortable";
|
||||||
|
|
||||||
interface EntityNameOption {
|
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||||
primary: string;
|
<ha-combo-box-item type="button" compact>
|
||||||
secondary?: string;
|
|
||||||
field_label: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<EntityNameOption> = (item) => html`
|
|
||||||
<ha-combo-box-item type="button">
|
|
||||||
<span slot="headline">${item.primary}</span>
|
<span slot="headline">${item.primary}</span>
|
||||||
${item.secondary
|
${item.secondary
|
||||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||||
@@ -79,11 +73,7 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||||
|
|
||||||
@query(".container", true) private _container?: HTMLDivElement;
|
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
|
||||||
|
|
||||||
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
|
||||||
|
|
||||||
@state() private _opened = false;
|
|
||||||
|
|
||||||
private _editIndex?: number;
|
private _editIndex?: number;
|
||||||
|
|
||||||
@@ -115,7 +105,7 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
return options;
|
return options;
|
||||||
});
|
});
|
||||||
|
|
||||||
private _getOptions = memoizeOne((entityId?: string) => {
|
private _getItems = memoizeOne((entityId?: string) => {
|
||||||
if (!entityId) {
|
if (!entityId) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -124,7 +114,7 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
|
|
||||||
const items = (
|
const items = (
|
||||||
["entity", "device", "area", "floor"] as const
|
["entity", "device", "area", "floor"] as const
|
||||||
).map<EntityNameOption>((name) => {
|
).map<PickerComboBoxItem>((name) => {
|
||||||
const stateObj = this.hass.states[entityId];
|
const stateObj = this.hass.states[entityId];
|
||||||
const isValid = types.has(name);
|
const isValid = types.has(name);
|
||||||
const primary = this.hass.localize(
|
const primary = this.hass.localize(
|
||||||
@@ -137,25 +127,39 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
`ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys
|
`ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys
|
||||||
)) || "-";
|
)) || "-";
|
||||||
|
|
||||||
|
const id = formatOptionValue({ type: name });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
id,
|
||||||
primary,
|
primary,
|
||||||
secondary,
|
secondary,
|
||||||
field_label: primary,
|
search_labels: {
|
||||||
value: formatOptionValue({ type: name }),
|
primary,
|
||||||
|
secondary: secondary || null,
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
sorting_label: primary,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
});
|
});
|
||||||
|
|
||||||
private _customNameOption = memoizeOne((text: string) => ({
|
private _customNameOption = memoizeOne(
|
||||||
|
(text: string): PickerComboBoxItem => ({
|
||||||
|
id: formatOptionValue({ type: "text", text }),
|
||||||
primary: this.hass.localize(
|
primary: this.hass.localize(
|
||||||
"ui.components.entity.entity-name-picker.custom_name"
|
"ui.components.entity.entity-name-picker.custom_name"
|
||||||
),
|
),
|
||||||
secondary: `"${text}"`,
|
secondary: `"${text}"`,
|
||||||
field_label: text,
|
search_labels: {
|
||||||
value: formatOptionValue({ type: "text", text }),
|
primary: text,
|
||||||
}));
|
secondary: `"${text}"`,
|
||||||
|
id: formatOptionValue({ type: "text", text }),
|
||||||
|
},
|
||||||
|
sorting_label: text,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
private _formatItem = (item: EntityNameItem) => {
|
private _formatItem = (item: EntityNameItem) => {
|
||||||
if (item.type === "text") {
|
if (item.type === "text") {
|
||||||
@@ -171,12 +175,29 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const value = this._items;
|
const value = this._items;
|
||||||
const options = this._getOptions(this.entityId);
|
|
||||||
const validTypes = this._validTypes(this.entityId);
|
const validTypes = this._validTypes(this.entityId);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||||
<div class="container">
|
<ha-generic-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required && !value.length}
|
||||||
|
.getItems=${this._getFilteredItems}
|
||||||
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
|
.rowRenderer=${rowRenderer}
|
||||||
|
.searchFn=${this._searchFn}
|
||||||
|
.notFoundLabel=${this.hass.localize(
|
||||||
|
"ui.components.entity.entity-name-picker.no_match"
|
||||||
|
)}
|
||||||
|
.value=${this._getPickerValue()}
|
||||||
|
allow-custom-value
|
||||||
|
.customValueLabel=${this.hass.localize(
|
||||||
|
"ui.components.entity.entity-name-picker.custom_name"
|
||||||
|
)}
|
||||||
|
@value-changed=${this._pickerValueChanged}
|
||||||
|
>
|
||||||
|
<div slot="field" class="container">
|
||||||
<ha-sortable
|
<ha-sortable
|
||||||
no-style
|
no-style
|
||||||
@item-moved=${this._moveItem}
|
@item-moved=${this._moveItem}
|
||||||
@@ -226,33 +247,8 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
`}
|
`}
|
||||||
</ha-chip-set>
|
</ha-chip-set>
|
||||||
</ha-sortable>
|
</ha-sortable>
|
||||||
|
|
||||||
<mwc-menu-surface
|
|
||||||
.open=${this._opened}
|
|
||||||
@closed=${this._onClosed}
|
|
||||||
@opened=${this._onOpened}
|
|
||||||
@input=${stopPropagation}
|
|
||||||
.anchor=${this._container}
|
|
||||||
>
|
|
||||||
<ha-combo-box
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${""}
|
|
||||||
.autofocus=${this.autofocus}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
.required=${this.required && !value.length}
|
|
||||||
.items=${options}
|
|
||||||
allow-custom-value
|
|
||||||
item-id-path="value"
|
|
||||||
item-value-path="value"
|
|
||||||
item-label-path="field_label"
|
|
||||||
.renderer=${rowRenderer}
|
|
||||||
@opened-changed=${this._openedChanged}
|
|
||||||
@value-changed=${this._comboBoxValueChanged}
|
|
||||||
@filter-changed=${this._filterChanged}
|
|
||||||
>
|
|
||||||
</ha-combo-box>
|
|
||||||
</mwc-menu-surface>
|
|
||||||
</div>
|
</div>
|
||||||
|
</ha-generic-picker>
|
||||||
${this._renderHelper()}
|
${this._renderHelper()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -267,32 +263,22 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
: nothing;
|
: nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onClosed(ev) {
|
private async _addItem(ev: Event) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this._opened = false;
|
|
||||||
this._editIndex = undefined;
|
this._editIndex = undefined;
|
||||||
|
await this.updateComplete;
|
||||||
|
await this._picker?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _onOpened(ev) {
|
private async _editItem(ev: Event) {
|
||||||
if (!this._opened) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this._opened = true;
|
const idx = parseInt(
|
||||||
await this._comboBox?.focus();
|
(ev.currentTarget as HTMLElement).dataset.idx || "",
|
||||||
await this._comboBox?.open();
|
10
|
||||||
}
|
);
|
||||||
|
|
||||||
private async _addItem(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this._opened = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _editItem(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const idx = parseInt(ev.currentTarget.dataset.idx, 10);
|
|
||||||
this._editIndex = idx;
|
this._editIndex = idx;
|
||||||
this._opened = true;
|
await this.updateComplete;
|
||||||
|
await this._picker?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _items(): EntityNameItem[] {
|
private get _items(): EntityNameItem[] {
|
||||||
@@ -322,78 +308,80 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
private _getPickerValue(): string | undefined {
|
||||||
const open = ev.detail.value;
|
if (this._editIndex != null) {
|
||||||
if (open) {
|
const item = this._items[this._editIndex];
|
||||||
const options = this._comboBox.items || [];
|
return item ? formatOptionValue(item) : undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const initialItem =
|
private _getFilteredItems = (
|
||||||
|
searchString?: string,
|
||||||
|
_section?: string
|
||||||
|
): PickerComboBoxItem[] => {
|
||||||
|
const items = this._getItems(this.entityId);
|
||||||
|
const currentItem =
|
||||||
this._editIndex != null ? this._items[this._editIndex] : undefined;
|
this._editIndex != null ? this._items[this._editIndex] : undefined;
|
||||||
|
const currentValue = currentItem ? formatOptionValue(currentItem) : "";
|
||||||
const initialValue = initialItem ? formatOptionValue(initialItem) : "";
|
|
||||||
|
|
||||||
const filteredItems = this._filterSelectedOptions(options, initialValue);
|
|
||||||
|
|
||||||
if (initialItem?.type === "text" && initialItem.text) {
|
|
||||||
filteredItems.push(this._customNameOption(initialItem.text));
|
|
||||||
}
|
|
||||||
|
|
||||||
this._comboBox.filteredItems = filteredItems;
|
|
||||||
this._comboBox.setInputValue(initialValue);
|
|
||||||
} else {
|
|
||||||
this._opened = false;
|
|
||||||
this._comboBox.setInputValue("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _filterSelectedOptions = (
|
|
||||||
options: EntityNameOption[],
|
|
||||||
current?: string
|
|
||||||
) => {
|
|
||||||
const items = this._items;
|
|
||||||
|
|
||||||
const excludedValues = new Set(
|
const excludedValues = new Set(
|
||||||
items
|
this._items
|
||||||
.filter((item) => UNIQUE_TYPES.has(item.type))
|
.filter((item) => UNIQUE_TYPES.has(item.type))
|
||||||
.map((item) => formatOptionValue(item))
|
.map((item) => formatOptionValue(item))
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredOptions = options.filter(
|
const filteredItems = items.filter(
|
||||||
(option) => !excludedValues.has(option.value) || option.value === current
|
(item) => !excludedValues.has(item.id) || item.id === currentValue
|
||||||
);
|
);
|
||||||
return filteredOptions;
|
|
||||||
|
// When editing an existing text item, include it in the base items
|
||||||
|
if (currentItem?.type === "text" && currentItem.text && !searchString) {
|
||||||
|
filteredItems.push(this._customNameOption(currentItem.text));
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredItems;
|
||||||
};
|
};
|
||||||
|
|
||||||
private _filterChanged(ev: ValueChangedEvent<string>) {
|
private _getAdditionalItems = (
|
||||||
const input = ev.detail.value;
|
searchString?: string
|
||||||
const filter = input?.toLowerCase() || "";
|
): PickerComboBoxItem[] => {
|
||||||
const options = this._comboBox.items || [];
|
if (!searchString) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const currentItem =
|
const currentItem =
|
||||||
this._editIndex != null ? this._items[this._editIndex] : undefined;
|
this._editIndex != null ? this._items[this._editIndex] : undefined;
|
||||||
|
|
||||||
const currentValue = currentItem ? formatOptionValue(currentItem) : "";
|
// Don't add if it's the same as the current item being edited
|
||||||
|
if (
|
||||||
let filteredItems = this._filterSelectedOptions(options, currentValue);
|
currentItem?.type === "text" &&
|
||||||
|
currentItem.text &&
|
||||||
if (!filter) {
|
currentItem.text === searchString
|
||||||
this._comboBox.filteredItems = filteredItems;
|
) {
|
||||||
return;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const fuseOptions: IFuseOptions<EntityNameOption> = {
|
// Always return custom name option when there's a search string
|
||||||
keys: ["primary", "secondary", "value"],
|
// This prevents "No matching items found" from showing
|
||||||
isCaseSensitive: false,
|
return [this._customNameOption(searchString)];
|
||||||
minMatchCharLength: Math.min(filter.length, 2),
|
|
||||||
threshold: 0.2,
|
|
||||||
ignoreDiacritics: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fuse = new Fuse(filteredItems, fuseOptions);
|
private _searchFn = (
|
||||||
filteredItems = fuse.search(filter).map((result) => result.item);
|
search: string,
|
||||||
filteredItems.push(this._customNameOption(input));
|
filteredItems: PickerComboBoxItem[],
|
||||||
this._comboBox.filteredItems = filteredItems;
|
_allItems: PickerComboBoxItem[]
|
||||||
|
): PickerComboBoxItem[] => {
|
||||||
|
// Remove NO_ITEMS_AVAILABLE_ID if we have additional items (custom name option)
|
||||||
|
// This prevents "No matching items found" from showing when custom values are allowed
|
||||||
|
const hasAdditionalItems = this._getAdditionalItems(search).length > 0;
|
||||||
|
if (hasAdditionalItems) {
|
||||||
|
return filteredItems.filter(
|
||||||
|
(item) => typeof item !== "string" || item !== NO_ITEMS_AVAILABLE_ID
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
return filteredItems;
|
||||||
|
};
|
||||||
|
|
||||||
private async _moveItem(ev: CustomEvent) {
|
private async _moveItem(ev: CustomEvent) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
@@ -403,25 +391,21 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
const element = newValue.splice(oldIndex, 1)[0];
|
const element = newValue.splice(oldIndex, 1)[0];
|
||||||
newValue.splice(newIndex, 0, element);
|
newValue.splice(newIndex, 0, element);
|
||||||
this._setValue(newValue);
|
this._setValue(newValue);
|
||||||
await this.updateComplete;
|
|
||||||
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _removeItem(ev) {
|
private async _removeItem(ev: Event) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const value = [...this._items];
|
const value = [...this._items];
|
||||||
const idx = parseInt(ev.target.dataset.idx, 10);
|
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
||||||
value.splice(idx, 1);
|
value.splice(idx, 1);
|
||||||
this._setValue(value);
|
this._setValue(value);
|
||||||
await this.updateComplete;
|
|
||||||
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
|
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const value = ev.detail.value;
|
const value = ev.detail.value;
|
||||||
|
|
||||||
if (this.disabled || value === "") {
|
if (this.disabled || !value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,11 +415,16 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
|
|
||||||
if (this._editIndex != null) {
|
if (this._editIndex != null) {
|
||||||
newValue[this._editIndex] = item;
|
newValue[this._editIndex] = item;
|
||||||
|
this._editIndex = undefined;
|
||||||
} else {
|
} else {
|
||||||
newValue.push(item);
|
newValue.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setValue(newValue);
|
this._setValue(newValue);
|
||||||
|
|
||||||
|
if (this._picker) {
|
||||||
|
this._picker.value = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(value: EntityNameItem[]) {
|
private _setValue(value: EntityNameItem[]) {
|
||||||
@@ -497,10 +486,6 @@ export class HaEntityNamePicker extends LitElement {
|
|||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
mwc-menu-surface {
|
|
||||||
--mdc-menu-min-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-chip-set {
|
ha-chip-set {
|
||||||
padding: var(--ha-space-2) var(--ha-space-2);
|
padding: var(--ha-space-2) var(--ha-space-2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||||
import { mdiPlus, mdiShape } from "@mdi/js";
|
import { mdiPlus, mdiShape } from "@mdi/js";
|
||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
|
||||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
@@ -7,11 +7,12 @@ import { fireEvent } from "../../common/dom/fire_event";
|
|||||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||||
import { computeRTL } from "../../common/util/compute_rtl";
|
import { computeRTL } from "../../common/util/compute_rtl";
|
||||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
|
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||||
import {
|
import {
|
||||||
|
entityComboBoxKeys,
|
||||||
getEntities,
|
getEntities,
|
||||||
type EntityComboBoxItem,
|
type EntityComboBoxItem,
|
||||||
} from "../../data/entity_registry";
|
} from "../../data/entity/entity_picker";
|
||||||
import { domainToName } from "../../data/integration";
|
import { domainToName } from "../../data/integration";
|
||||||
import {
|
import {
|
||||||
isHelperDomain,
|
isHelperDomain,
|
||||||
@@ -171,9 +172,9 @@ export class HaEntityPicker extends LitElement {
|
|||||||
return this.showEntityId || this.hass.userData?.showEntityIdPicker;
|
return this.showEntityId || this.hass.userData?.showEntityIdPicker;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = (
|
private _rowRenderer: RenderItemFunction<EntityComboBoxItem> = (
|
||||||
item,
|
item,
|
||||||
{ index }
|
index
|
||||||
) => {
|
) => {
|
||||||
const showEntityId = this._showEntityId;
|
const showEntityId = this._showEntityId;
|
||||||
|
|
||||||
@@ -227,7 +228,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
if (!createDomains?.length) {
|
if (!createDomains?.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
this.hass.loadFragmentTranslation("config");
|
||||||
return createDomains.map((domain) => {
|
return createDomains.map((domain) => {
|
||||||
const primary = localize(
|
const primary = localize(
|
||||||
"ui.components.entity.entity-picker.create_helper",
|
"ui.components.entity.entity-picker.create_helper",
|
||||||
@@ -235,7 +236,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
domain: isHelperDomain(domain)
|
domain: isHelperDomain(domain)
|
||||||
? localize(
|
? localize(
|
||||||
`ui.panel.config.helpers.types.${domain as HelperDomain}`
|
`ui.panel.config.helpers.types.${domain as HelperDomain}`
|
||||||
)
|
) || domain
|
||||||
: domainToName(localize, domain),
|
: domainToName(localize, domain),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -276,22 +277,28 @@ export class HaEntityPicker extends LitElement {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.allowCustomValue=${this.allowCustomEntity}
|
.allowCustomValue=${this.allowCustomEntity}
|
||||||
|
.required=${this.required}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
|
.placeholder=${placeholder}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
|
.value=${this.addButton ? undefined : this.value}
|
||||||
.searchLabel=${this.searchLabel}
|
.searchLabel=${this.searchLabel}
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
.placeholder=${placeholder}
|
|
||||||
.value=${this.addButton ? undefined : this.value}
|
|
||||||
.rowRenderer=${this._rowRenderer}
|
.rowRenderer=${this._rowRenderer}
|
||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
.hideClearIcon=${this.hideClearIcon}
|
.hideClearIcon=${this.hideClearIcon}
|
||||||
.searchFn=${this._searchFn}
|
.searchFn=${this._searchFn}
|
||||||
.valueRenderer=${this._valueRenderer}
|
.valueRenderer=${this._valueRenderer}
|
||||||
@value-changed=${this._valueChanged}
|
.searchKeys=${entityComboBoxKeys}
|
||||||
|
use-top-label
|
||||||
.addButtonLabel=${this.addButton
|
.addButtonLabel=${this.addButton
|
||||||
? this.hass.localize("ui.components.entity.entity-picker.add")
|
? this.hass.localize("ui.components.entity.entity-picker.add")
|
||||||
: undefined}
|
: undefined}
|
||||||
|
.unknownItemText=${this.hass.localize(
|
||||||
|
"ui.components.entity.entity-picker.unknown"
|
||||||
|
)}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import "@material/mwc-menu/mwc-menu-surface";
|
|
||||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
|
||||||
import type { IFuseOptions } from "fuse.js";
|
|
||||||
import Fuse from "fuse.js";
|
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
|
||||||
import { computeDomain } from "../../common/entity/compute_domain";
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
import {
|
import {
|
||||||
STATE_DISPLAY_SPECIAL_CONTENT,
|
STATE_DISPLAY_SPECIAL_CONTENT,
|
||||||
@@ -20,21 +15,13 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
|||||||
import "../chips/ha-assist-chip";
|
import "../chips/ha-assist-chip";
|
||||||
import "../chips/ha-chip-set";
|
import "../chips/ha-chip-set";
|
||||||
import "../chips/ha-input-chip";
|
import "../chips/ha-input-chip";
|
||||||
import "../ha-combo-box";
|
import "../ha-combo-box-item";
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
import "../ha-generic-picker";
|
||||||
|
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||||
|
import "../ha-input-helper-text";
|
||||||
|
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||||
import "../ha-sortable";
|
import "../ha-sortable";
|
||||||
|
|
||||||
interface StateContentOption {
|
|
||||||
primary: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<StateContentOption> = (item) => html`
|
|
||||||
<ha-combo-box-item type="button">
|
|
||||||
<span slot="headline">${item.primary}</span>
|
|
||||||
</ha-combo-box-item>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HIDDEN_ATTRIBUTES = [
|
const HIDDEN_ATTRIBUTES = [
|
||||||
"access_token",
|
"access_token",
|
||||||
"available_modes",
|
"available_modes",
|
||||||
@@ -111,63 +98,88 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
@query(".container", true) private _container?: HTMLDivElement;
|
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
|
||||||
|
|
||||||
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
|
||||||
|
|
||||||
@state() private _opened = false;
|
|
||||||
|
|
||||||
private _editIndex?: number;
|
private _editIndex?: number;
|
||||||
|
|
||||||
private _options = memoizeOne(
|
private _getItems = memoizeOne(
|
||||||
(entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
|
(entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
|
||||||
const domain = entityId ? computeDomain(entityId) : undefined;
|
const domain = entityId ? computeDomain(entityId) : undefined;
|
||||||
return [
|
const items: PickerComboBoxItem[] = [
|
||||||
{
|
{
|
||||||
|
id: "state",
|
||||||
primary: this.hass.localize(
|
primary: this.hass.localize(
|
||||||
"ui.components.state-content-picker.state"
|
"ui.components.state-content-picker.state"
|
||||||
),
|
),
|
||||||
value: "state",
|
sorting_label: this.hass.localize(
|
||||||
|
"ui.components.state-content-picker.state"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
...(allowName
|
...(allowName
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
id: "name",
|
||||||
primary: this.hass.localize(
|
primary: this.hass.localize(
|
||||||
"ui.components.state-content-picker.name"
|
"ui.components.state-content-picker.name"
|
||||||
),
|
),
|
||||||
value: "name",
|
sorting_label: this.hass.localize(
|
||||||
},
|
"ui.components.state-content-picker.name"
|
||||||
|
),
|
||||||
|
} satisfies PickerComboBoxItem,
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
{
|
{
|
||||||
|
id: "last_changed",
|
||||||
primary: this.hass.localize(
|
primary: this.hass.localize(
|
||||||
"ui.components.state-content-picker.last_changed"
|
"ui.components.state-content-picker.last_changed"
|
||||||
),
|
),
|
||||||
value: "last_changed",
|
sorting_label: this.hass.localize(
|
||||||
|
"ui.components.state-content-picker.last_changed"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "last_updated",
|
||||||
primary: this.hass.localize(
|
primary: this.hass.localize(
|
||||||
"ui.components.state-content-picker.last_updated"
|
"ui.components.state-content-picker.last_updated"
|
||||||
),
|
),
|
||||||
value: "last_updated",
|
sorting_label: this.hass.localize(
|
||||||
|
"ui.components.state-content-picker.last_updated"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
...(domain
|
...(domain
|
||||||
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
|
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
|
||||||
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
|
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
|
||||||
).map((content) => ({
|
).map(
|
||||||
|
(content) =>
|
||||||
|
({
|
||||||
|
id: content,
|
||||||
primary: this.hass.localize(
|
primary: this.hass.localize(
|
||||||
`ui.components.state-content-picker.${content}`
|
`ui.components.state-content-picker.${content}`
|
||||||
),
|
),
|
||||||
value: content,
|
sorting_label: this.hass.localize(
|
||||||
}))
|
`ui.components.state-content-picker.${content}`
|
||||||
|
),
|
||||||
|
}) satisfies PickerComboBoxItem
|
||||||
|
)
|
||||||
: []),
|
: []),
|
||||||
...Object.keys(stateObj?.attributes ?? {})
|
...Object.keys(stateObj?.attributes ?? {})
|
||||||
.filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
|
.filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
|
||||||
.map((attribute) => ({
|
.map(
|
||||||
primary: this.hass.formatEntityAttributeName(stateObj!, attribute),
|
(attribute) =>
|
||||||
value: attribute,
|
({
|
||||||
})),
|
id: attribute,
|
||||||
] satisfies StateContentOption[];
|
primary: this.hass.formatEntityAttributeName(
|
||||||
|
stateObj!,
|
||||||
|
attribute
|
||||||
|
),
|
||||||
|
sorting_label: this.hass.formatEntityAttributeName(
|
||||||
|
stateObj!,
|
||||||
|
attribute
|
||||||
|
),
|
||||||
|
}) satisfies PickerComboBoxItem
|
||||||
|
),
|
||||||
|
];
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -178,11 +190,23 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
? this.hass.states[this.entityId]
|
? this.hass.states[this.entityId]
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const options = this._options(this.entityId, stateObj, this.allowName);
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||||
<div class="container ${this.disabled ? "disabled" : ""}">
|
<ha-generic-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required && !value.length}
|
||||||
|
.value=${this._getPickerValue()}
|
||||||
|
.getItems=${this._getFilteredItems}
|
||||||
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
|
.notFoundLabel=${this.hass.localize("ui.components.combo-box.no_match")}
|
||||||
|
allow-custom-value
|
||||||
|
.customValueLabel=${this.hass.localize(
|
||||||
|
"ui.components.entity.entity-state-content-picker.custom_state"
|
||||||
|
)}
|
||||||
|
@value-changed=${this._pickerValueChanged}
|
||||||
|
>
|
||||||
|
<div slot="field" class="container">
|
||||||
<ha-sortable
|
<ha-sortable
|
||||||
no-style
|
no-style
|
||||||
@item-moved=${this._moveItem}
|
@item-moved=${this._moveItem}
|
||||||
@@ -195,7 +219,7 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
this._value,
|
this._value,
|
||||||
(item) => item,
|
(item) => item,
|
||||||
(item: string, idx) => {
|
(item: string, idx) => {
|
||||||
const label = options.find((o) => o.value === item)?.primary;
|
const label = this._getItemLabel(item, stateObj);
|
||||||
const isValid = !!label;
|
const isValid = !!label;
|
||||||
return html`
|
return html`
|
||||||
<ha-input-chip
|
<ha-input-chip
|
||||||
@@ -231,69 +255,58 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
`}
|
`}
|
||||||
</ha-chip-set>
|
</ha-chip-set>
|
||||||
</ha-sortable>
|
</ha-sortable>
|
||||||
|
|
||||||
<mwc-menu-surface
|
|
||||||
.open=${this._opened}
|
|
||||||
@closed=${this._onClosed}
|
|
||||||
@opened=${this._onOpened}
|
|
||||||
@input=${stopPropagation}
|
|
||||||
.anchor=${this._container}
|
|
||||||
>
|
|
||||||
<ha-combo-box
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${""}
|
|
||||||
.autofocus=${this.autofocus}
|
|
||||||
.disabled=${this.disabled || !this.entityId}
|
|
||||||
.required=${this.required && !value.length}
|
|
||||||
.helper=${this.helper}
|
|
||||||
.items=${options}
|
|
||||||
allow-custom-value
|
|
||||||
item-id-path="value"
|
|
||||||
item-value-path="value"
|
|
||||||
item-label-path="primary"
|
|
||||||
.renderer=${rowRenderer}
|
|
||||||
@opened-changed=${this._openedChanged}
|
|
||||||
@value-changed=${this._comboBoxValueChanged}
|
|
||||||
@filter-changed=${this._filterChanged}
|
|
||||||
>
|
|
||||||
</ha-combo-box>
|
|
||||||
</mwc-menu-surface>
|
|
||||||
</div>
|
</div>
|
||||||
|
</ha-generic-picker>
|
||||||
|
${this._renderHelper()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onClosed(ev) {
|
private _renderHelper() {
|
||||||
|
return this.helper
|
||||||
|
? html`
|
||||||
|
<ha-input-helper-text .disabled=${this.disabled}>
|
||||||
|
${this.helper}
|
||||||
|
</ha-input-helper-text>
|
||||||
|
`
|
||||||
|
: nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _addItem(ev: Event) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this._opened = false;
|
|
||||||
this._editIndex = undefined;
|
this._editIndex = undefined;
|
||||||
|
await this.updateComplete;
|
||||||
|
await this._picker?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _onOpened(ev) {
|
private async _editItem(ev: Event) {
|
||||||
if (!this._opened) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this._opened = true;
|
const idx = parseInt(
|
||||||
await this._comboBox?.focus();
|
(ev.currentTarget as HTMLElement).dataset.idx || "",
|
||||||
await this._comboBox?.open();
|
10
|
||||||
}
|
);
|
||||||
|
|
||||||
private async _addItem(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this._opened = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _editItem(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const idx = parseInt(ev.currentTarget.dataset.idx, 10);
|
|
||||||
this._editIndex = idx;
|
this._editIndex = idx;
|
||||||
this._opened = true;
|
await this.updateComplete;
|
||||||
|
await this._picker?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _value() {
|
private get _value() {
|
||||||
return !this.value ? [] : ensureArray(this.value);
|
return !this.value ? [] : ensureArray(this.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getItemLabel = memoizeOne(
|
||||||
|
(value: string, stateObj?: HassEntity): string | undefined => {
|
||||||
|
const stateObjForItems = this.entityId
|
||||||
|
? this.hass.states[this.entityId]
|
||||||
|
: stateObj;
|
||||||
|
const items = this._getItems(
|
||||||
|
this.entityId,
|
||||||
|
stateObjForItems,
|
||||||
|
this.allowName
|
||||||
|
);
|
||||||
|
return items.find((item) => item.id === value)?.primary;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
private _toValue = memoizeOne((value: string[]): typeof this.value => {
|
private _toValue = memoizeOne((value: string[]): typeof this.value => {
|
||||||
if (value.length === 0) {
|
if (value.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -304,64 +317,88 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
|
|
||||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
private _getPickerValue(): string | undefined {
|
||||||
const open = ev.detail.value;
|
if (this._editIndex != null) {
|
||||||
if (open) {
|
return this._value[this._editIndex];
|
||||||
const options = this._comboBox.items || [];
|
|
||||||
|
|
||||||
const initialValue =
|
|
||||||
this._editIndex != null ? this._value[this._editIndex] : "";
|
|
||||||
const filteredItems = this._filterSelectedOptions(options, initialValue);
|
|
||||||
|
|
||||||
this._comboBox.filteredItems = filteredItems;
|
|
||||||
this._comboBox.setInputValue(initialValue);
|
|
||||||
} else {
|
|
||||||
this._opened = false;
|
|
||||||
}
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _filterSelectedOptions = (
|
private _customValueOption = memoizeOne(
|
||||||
options: StateContentOption[],
|
(text: string): PickerComboBoxItem => ({
|
||||||
current?: string
|
id: text,
|
||||||
) => {
|
primary: this.hass.localize(
|
||||||
|
"ui.components.entity.entity-state-content-picker.custom_state"
|
||||||
|
),
|
||||||
|
secondary: `"${text}"`,
|
||||||
|
search_labels: {
|
||||||
|
primary: text,
|
||||||
|
secondary: `"${text}"`,
|
||||||
|
id: text,
|
||||||
|
},
|
||||||
|
sorting_label: text,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
private _getFilteredItems = (
|
||||||
|
searchString?: string,
|
||||||
|
_section?: string
|
||||||
|
): PickerComboBoxItem[] => {
|
||||||
|
const stateObj = this.entityId
|
||||||
|
? this.hass.states[this.entityId]
|
||||||
|
: undefined;
|
||||||
|
const items = this._getItems(this.entityId, stateObj, this.allowName);
|
||||||
|
const currentValue =
|
||||||
|
this._editIndex != null ? this._value[this._editIndex] : undefined;
|
||||||
|
|
||||||
const value = this._value;
|
const value = this._value;
|
||||||
|
|
||||||
return options.filter(
|
const filteredItems = items.filter(
|
||||||
(option) => !value.includes(option.value) || option.value === current
|
(item) => !value.includes(item.id) || item.id === currentValue
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// When editing an existing custom value, include it in the base items
|
||||||
|
if (
|
||||||
|
currentValue &&
|
||||||
|
!items.find((item) => item.id === currentValue) &&
|
||||||
|
!searchString
|
||||||
|
) {
|
||||||
|
filteredItems.push(this._customValueOption(currentValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredItems;
|
||||||
};
|
};
|
||||||
|
|
||||||
private _filterChanged(ev: ValueChangedEvent<string>) {
|
private _getAdditionalItems = (
|
||||||
const input = ev.detail.value;
|
searchString?: string
|
||||||
const filter = input?.toLowerCase() || "";
|
): PickerComboBoxItem[] => {
|
||||||
const options = this._comboBox.items || [];
|
if (!searchString) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const currentValue =
|
const currentValue =
|
||||||
this._editIndex != null ? this._value[this._editIndex] : "";
|
this._editIndex != null ? this._value[this._editIndex] : undefined;
|
||||||
|
|
||||||
this._comboBox.filteredItems = this._filterSelectedOptions(
|
// Don't add if it's the same as the current item being edited
|
||||||
options,
|
if (currentValue && currentValue === searchString) {
|
||||||
currentValue
|
return [];
|
||||||
);
|
|
||||||
|
|
||||||
if (!filter) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fuseOptions: IFuseOptions<StateContentOption> = {
|
// Check if the search string matches an existing item
|
||||||
keys: ["primary", "secondary", "value"],
|
const stateObj = this.entityId
|
||||||
isCaseSensitive: false,
|
? this.hass.states[this.entityId]
|
||||||
minMatchCharLength: Math.min(filter.length, 2),
|
: undefined;
|
||||||
threshold: 0.2,
|
const items = this._getItems(this.entityId, stateObj, this.allowName);
|
||||||
ignoreDiacritics: true,
|
const existingItem = items.find((item) => item.id === searchString);
|
||||||
|
|
||||||
|
// Only return custom value option if it doesn't match an existing item
|
||||||
|
if (!existingItem) {
|
||||||
|
return [this._customValueOption(searchString)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions);
|
|
||||||
const filteredItems = fuse.search(filter).map((result) => result.item);
|
|
||||||
|
|
||||||
this._comboBox.filteredItems = filteredItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _moveItem(ev: CustomEvent) {
|
private async _moveItem(ev: CustomEvent) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const { oldIndex, newIndex } = ev.detail;
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
@@ -370,25 +407,21 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
const element = newValue.splice(oldIndex, 1)[0];
|
const element = newValue.splice(oldIndex, 1)[0];
|
||||||
newValue.splice(newIndex, 0, element);
|
newValue.splice(newIndex, 0, element);
|
||||||
this._setValue(newValue);
|
this._setValue(newValue);
|
||||||
await this.updateComplete;
|
|
||||||
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _removeItem(ev) {
|
private async _removeItem(ev: Event) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const value = [...this._value];
|
const value = [...this._value];
|
||||||
const idx = parseInt(ev.target.dataset.idx, 10);
|
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
||||||
value.splice(idx, 1);
|
value.splice(idx, 1);
|
||||||
this._setValue(value);
|
this._setValue(value);
|
||||||
await this.updateComplete;
|
|
||||||
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
|
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const value = ev.detail.value;
|
const value = ev.detail.value;
|
||||||
|
|
||||||
if (this.disabled || value === "") {
|
if (this.disabled || !value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,11 +429,16 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
|
|
||||||
if (this._editIndex != null) {
|
if (this._editIndex != null) {
|
||||||
newValue[this._editIndex] = value;
|
newValue[this._editIndex] = value;
|
||||||
|
this._editIndex = undefined;
|
||||||
} else {
|
} else {
|
||||||
newValue.push(value);
|
newValue.push(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setValue(newValue);
|
this._setValue(newValue);
|
||||||
|
|
||||||
|
if (this._picker) {
|
||||||
|
this._picker.value = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(value: string[]) {
|
private _setValue(value: string[]) {
|
||||||
@@ -442,7 +480,7 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
height 180ms ease-in-out,
|
height 180ms ease-in-out,
|
||||||
background-color 180ms ease-in-out;
|
background-color 180ms ease-in-out;
|
||||||
}
|
}
|
||||||
.container.disabled:after {
|
:host([disabled]) .container:after {
|
||||||
background-color: var(
|
background-color: var(
|
||||||
--mdc-text-field-disabled-line-color,
|
--mdc-text-field-disabled-line-color,
|
||||||
rgba(0, 0, 0, 0.42)
|
rgba(0, 0, 0, 0.42)
|
||||||
@@ -462,10 +500,6 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
order: 1;
|
order: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
mwc-menu-surface {
|
|
||||||
--mdc-menu-min-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-chip-set {
|
ha-chip-set {
|
||||||
padding: var(--ha-space-2) var(--ha-space-2);
|
padding: var(--ha-space-2) var(--ha-space-2);
|
||||||
}
|
}
|
||||||
@@ -486,6 +520,11 @@ export class HaStateContentPicker extends LitElement {
|
|||||||
.sortable-drag {
|
.sortable-drag {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ha-input-helper-text {
|
||||||
|
display: block;
|
||||||
|
margin: var(--ha-space-2) 0 0;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,22 @@
|
|||||||
import type { PropertyValues } from "lit";
|
|
||||||
import { LitElement, html, nothing } from "lit";
|
import { LitElement, html, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { ensureArray } from "../../common/array/ensure-array";
|
import { ensureArray } from "../../common/array/ensure-array";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { getStates } from "../../common/entity/get_states";
|
import { getStates } from "../../common/entity/get_states";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||||
import "../ha-combo-box";
|
import "../ha-generic-picker";
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||||
|
|
||||||
interface StateOption {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ha-entity-state-picker")
|
@customElement("ha-entity-state-picker")
|
||||||
class HaEntityStatePicker extends LitElement {
|
export class HaEntityStatePicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public entityId?: string | string[];
|
@property({ attribute: false }) public entityId?: string | string[];
|
||||||
|
|
||||||
@property() public attribute?: string;
|
@property() public attribute?: string;
|
||||||
|
|
||||||
@property({ attribute: false }) public extraOptions?: any[];
|
@property({ attribute: false }) public extraOptions?: PickerComboBoxItem[];
|
||||||
|
|
||||||
// eslint-disable-next-line lit/no-native-attributes
|
// eslint-disable-next-line lit/no-native-attributes
|
||||||
@property({ type: Boolean }) public autofocus = false;
|
@property({ type: Boolean }) public autofocus = false;
|
||||||
@@ -42,59 +37,76 @@ class HaEntityStatePicker extends LitElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
@state() private _opened = false;
|
private _getItems = memoizeOne(
|
||||||
|
(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityId: string | string[] | undefined,
|
||||||
|
attribute: string | undefined,
|
||||||
|
hideStates: string[] | undefined,
|
||||||
|
extraOptions: PickerComboBoxItem[] | undefined
|
||||||
|
): PickerComboBoxItem[] => {
|
||||||
|
const entityIds = entityId ? ensureArray(entityId) : [];
|
||||||
|
|
||||||
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
const entitiesOptions = entityIds.map<PickerComboBoxItem[]>(
|
||||||
|
(entityIdItem) => {
|
||||||
protected shouldUpdate(changedProps: PropertyValues) {
|
const stateObj = hass.states[entityIdItem] || {
|
||||||
return !(!changedProps.has("_opened") && this._opened);
|
entity_id: entityIdItem,
|
||||||
}
|
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
|
||||||
if (
|
|
||||||
(changedProps.has("_opened") && this._opened) ||
|
|
||||||
changedProps.has("entityId") ||
|
|
||||||
changedProps.has("attribute") ||
|
|
||||||
changedProps.has("extraOptions")
|
|
||||||
) {
|
|
||||||
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
|
|
||||||
|
|
||||||
const entitiesOptions = entityIds.map<StateOption[]>((entityId) => {
|
|
||||||
const stateObj = this.hass.states[entityId] || {
|
|
||||||
entity_id: entityId,
|
|
||||||
attributes: {},
|
attributes: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const states = getStates(this.hass, stateObj, this.attribute).filter(
|
const states = getStates(hass, stateObj, attribute).filter(
|
||||||
(s) => !this.hideStates?.includes(s)
|
(s) => !hideStates?.includes(s)
|
||||||
);
|
);
|
||||||
|
|
||||||
return states.map((s) => ({
|
return states
|
||||||
value: s,
|
.map((s) => {
|
||||||
label: this.attribute
|
const primary = attribute
|
||||||
? this.hass.formatEntityAttributeValue(stateObj, this.attribute, s)
|
? hass.formatEntityAttributeValue(stateObj, attribute, s)
|
||||||
: this.hass.formatEntityState(stateObj, s),
|
: hass.formatEntityState(stateObj, s);
|
||||||
}));
|
return {
|
||||||
});
|
id: s,
|
||||||
|
primary,
|
||||||
|
sorting_label: primary,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((option) => option.id && option.primary);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const options: StateOption[] = [];
|
const options: PickerComboBoxItem[] = [];
|
||||||
const optionsSet = new Set<string>();
|
const optionsSet = new Set<string>();
|
||||||
for (const entityOptions of entitiesOptions) {
|
for (const entityOptions of entitiesOptions) {
|
||||||
for (const option of entityOptions) {
|
for (const option of entityOptions) {
|
||||||
if (!optionsSet.has(option.value)) {
|
if (!optionsSet.has(option.id)) {
|
||||||
optionsSet.add(option.value);
|
optionsSet.add(option.id);
|
||||||
options.push(option);
|
options.push(option);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.extraOptions) {
|
if (extraOptions) {
|
||||||
options.unshift(...this.extraOptions);
|
// Filter out any extraOptions with empty primary or id fields
|
||||||
|
const validExtraOptions = extraOptions.filter(
|
||||||
|
(option) => option.id && option.primary
|
||||||
|
);
|
||||||
|
options.unshift(...validExtraOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
(this._comboBox as any).filteredItems = options;
|
return options;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _getFilteredItems = (
|
||||||
|
_searchString?: string,
|
||||||
|
_section?: string
|
||||||
|
): PickerComboBoxItem[] =>
|
||||||
|
this._getItems(
|
||||||
|
this.hass,
|
||||||
|
this.entityId,
|
||||||
|
this.attribute,
|
||||||
|
this.hideStates,
|
||||||
|
this.extraOptions
|
||||||
|
);
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
@@ -102,48 +114,39 @@ class HaEntityStatePicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-combo-box
|
<ha-generic-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._value}
|
.allowCustomValue=${this.allowCustomValue}
|
||||||
|
.disabled=${this.disabled || !this.entityId}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
|
.required=${this.required}
|
||||||
.label=${this.label ??
|
.label=${this.label ??
|
||||||
this.hass.localize("ui.components.entity.entity-state-picker.state")}
|
this.hass.localize("ui.components.entity.entity-state-picker.state")}
|
||||||
.disabled=${this.disabled || !this.entityId}
|
|
||||||
.required=${this.required}
|
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.allowCustomValue=${this.allowCustomValue}
|
.value=${this.value}
|
||||||
item-id-path="value"
|
.getItems=${this._getFilteredItems}
|
||||||
item-value-path="value"
|
.notFoundLabel=${this.hass.localize("ui.components.combo-box.no_match")}
|
||||||
item-label-path="label"
|
.customValueLabel=${this.hass.localize(
|
||||||
@opened-changed=${this._openedChanged}
|
"ui.components.entity.entity-state-picker.add_custom_state"
|
||||||
|
)}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-combo-box>
|
</ha-generic-picker>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _value() {
|
|
||||||
return this.value || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
|
||||||
this._opened = ev.detail.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const newValue = ev.detail.value;
|
const newValue = ev.detail.value;
|
||||||
if (newValue !== this._value) {
|
if (newValue !== this.value) {
|
||||||
this._setValue(newValue);
|
this._setValue(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(value: string) {
|
private _setValue(value: string | undefined) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
setTimeout(() => {
|
|
||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import { STATES_OFF } from "../../common/const";
|
import { STATES_OFF } from "../../common/const";
|
||||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
import { UNAVAILABLE, UNKNOWN, isUnavailableState } from "../../data/entity";
|
import {
|
||||||
|
UNAVAILABLE,
|
||||||
|
UNKNOWN,
|
||||||
|
isUnavailableState,
|
||||||
|
} from "../../data/entity/entity";
|
||||||
import { forwardHaptic } from "../../data/haptics";
|
import { forwardHaptic } from "../../data/haptics";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../ha-formfield";
|
import "../ha-formfield";
|
||||||
|
|||||||
@@ -14,8 +14,12 @@ import {
|
|||||||
getNumberFormatOptions,
|
getNumberFormatOptions,
|
||||||
isNumericState,
|
isNumericState,
|
||||||
} from "../../common/number/format_number";
|
} from "../../common/number/format_number";
|
||||||
import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
import {
|
||||||
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
isUnavailableState,
|
||||||
|
UNAVAILABLE,
|
||||||
|
UNKNOWN,
|
||||||
|
} from "../../data/entity/entity";
|
||||||
|
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||||
import { timerTimeRemaining } from "../../data/timer";
|
import { timerTimeRemaining } from "../../data/timer";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import "../ha-label-badge";
|
import "../ha-label-badge";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||||
import { mdiChartLine, mdiHelpCircle, mdiShape } from "@mdi/js";
|
import { mdiChartLine, mdiHelpCircle, mdiShape } from "@mdi/js";
|
||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
@@ -38,9 +38,21 @@ type StatisticItemType = "entity" | "external" | "no_state";
|
|||||||
interface StatisticComboBoxItem extends PickerComboBoxItem {
|
interface StatisticComboBoxItem extends PickerComboBoxItem {
|
||||||
statistic_id?: string;
|
statistic_id?: string;
|
||||||
stateObj?: HassEntity;
|
stateObj?: HassEntity;
|
||||||
|
domainName?: string;
|
||||||
type?: StatisticItemType;
|
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")
|
@customElement("ha-statistic-picker")
|
||||||
export class HaStatisticPicker extends LitElement {
|
export class HaStatisticPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -233,7 +245,6 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
),
|
),
|
||||||
type,
|
type,
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
sorting_label: [sortingPrefix, label].join("_"),
|
||||||
search_labels: [label, id],
|
|
||||||
icon_path: mdiShape,
|
icon_path: mdiShape,
|
||||||
});
|
});
|
||||||
} else if (type === "external") {
|
} else if (type === "external") {
|
||||||
@@ -246,7 +257,7 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
secondary: domainName,
|
secondary: domainName,
|
||||||
type,
|
type,
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
sorting_label: [sortingPrefix, label].join("_"),
|
||||||
search_labels: [label, domainName, id],
|
search_labels: { label, domainName },
|
||||||
icon_path: mdiChartLine,
|
icon_path: mdiChartLine,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -280,13 +291,12 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
stateObj: stateObj,
|
stateObj: stateObj,
|
||||||
type: "entity",
|
type: "entity",
|
||||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||||
search_labels: [
|
search_labels: {
|
||||||
entityName,
|
entityName: entityName || null,
|
||||||
deviceName,
|
deviceName: deviceName || null,
|
||||||
areaName,
|
areaName: areaName || null,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
id,
|
},
|
||||||
].filter(Boolean) as string[],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -361,13 +371,13 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
stateObj: stateObj,
|
stateObj: stateObj,
|
||||||
type: "entity",
|
type: "entity",
|
||||||
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
sorting_label: [sortingPrefix, deviceName, entityName].join("_"),
|
||||||
search_labels: [
|
search_labels: {
|
||||||
entityName,
|
entityName: entityName || null,
|
||||||
deviceName,
|
deviceName: deviceName || null,
|
||||||
areaName,
|
areaName: areaName || null,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
statisticId,
|
statisticId,
|
||||||
].filter(Boolean) as string[],
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,7 +404,7 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
secondary: domainName,
|
secondary: domainName,
|
||||||
type: "external",
|
type: "external",
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
sorting_label: [sortingPrefix, label].join("_"),
|
||||||
search_labels: [label, domainName, statisticId],
|
search_labels: { label, domainName, statisticId },
|
||||||
icon_path: mdiChartLine,
|
icon_path: mdiChartLine,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -409,14 +419,14 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
secondary: this.hass.localize("ui.components.statistic-picker.no_state"),
|
secondary: this.hass.localize("ui.components.statistic-picker.no_state"),
|
||||||
type: "no_state",
|
type: "no_state",
|
||||||
sorting_label: [sortingPrefix, label].join("_"),
|
sorting_label: [sortingPrefix, label].join("_"),
|
||||||
search_labels: [label, statisticId],
|
search_labels: { label, statisticId },
|
||||||
icon_path: mdiShape,
|
icon_path: mdiShape,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _rowRenderer: ComboBoxLitRenderer<StatisticComboBoxItem> = (
|
private _rowRenderer: RenderItemFunction<StatisticComboBoxItem> = (
|
||||||
item,
|
item,
|
||||||
{ index }
|
index
|
||||||
) => {
|
) => {
|
||||||
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
||||||
return html`
|
return html`
|
||||||
@@ -461,13 +471,14 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.allowCustomValue=${this.allowCustomEntity}
|
.allowCustomValue=${this.allowCustomEntity}
|
||||||
|
.disabled=${this.disabled}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
|
.placeholder=${placeholder}
|
||||||
|
.value=${this.value}
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
.emptyLabel=${this.hass.localize(
|
.emptyLabel=${this.hass.localize(
|
||||||
"ui.components.statistic-picker.no_statistics"
|
"ui.components.statistic-picker.no_statistics"
|
||||||
)}
|
)}
|
||||||
.placeholder=${placeholder}
|
|
||||||
.value=${this.value}
|
|
||||||
.rowRenderer=${this._rowRenderer}
|
.rowRenderer=${this._rowRenderer}
|
||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
@@ -475,6 +486,10 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
.searchFn=${this._searchFn}
|
.searchFn=${this._searchFn}
|
||||||
.valueRenderer=${this._valueRenderer}
|
.valueRenderer=${this._valueRenderer}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
|
.searchKeys=${SEARCH_KEYS}
|
||||||
|
.unknownItemText=${this.hass.localize(
|
||||||
|
"ui.components.statistic-picker.unknown"
|
||||||
|
)}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { mdiAlert } from "@mdi/js";
|
|||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||||
import { LitElement, css, html, nothing } 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 { ifDefined } from "lit/directives/if-defined";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import { computeDomain } from "../../common/entity/compute_domain";
|
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 type { HomeAssistant } from "../../types";
|
||||||
import "../ha-state-icon";
|
import "../ha-state-icon";
|
||||||
|
|
||||||
|
@customElement("state-badge")
|
||||||
export class StateBadge extends LitElement {
|
export class StateBadge extends LitElement {
|
||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
@@ -265,5 +266,3 @@ declare global {
|
|||||||
"state-badge": StateBadge;
|
"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,29 +1,29 @@
|
|||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||||
import { html, LitElement, nothing } from "lit";
|
import { html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { stringCompare } from "../common/string/compare";
|
|
||||||
import type { HassioAddonInfo } from "../data/hassio/addon";
|
|
||||||
import { fetchHassioAddonsInfo } from "../data/hassio/addon";
|
import { fetchHassioAddonsInfo } from "../data/hassio/addon";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
import "./ha-alert";
|
import "./ha-alert";
|
||||||
import "./ha-combo-box";
|
|
||||||
import type { HaComboBox } from "./ha-combo-box";
|
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box-item";
|
||||||
|
import "./ha-generic-picker";
|
||||||
|
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||||
|
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`
|
const SEARCH_KEYS = [
|
||||||
|
{ name: "primary", weight: 10 },
|
||||||
|
{ name: "secondary", weight: 8 },
|
||||||
|
{ name: "search_labels.description", weight: 6 },
|
||||||
|
{ name: "search_labels.repository", weight: 5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||||
<ha-combo-box-item type="button">
|
<ha-combo-box-item type="button">
|
||||||
<span slot="headline">${item.name}</span>
|
<span slot="headline">${item.primary}</span>
|
||||||
<span slot="supporting-text">${item.slug}</span>
|
<span slot="supporting-text">${item.secondary}</span>
|
||||||
${item.icon
|
${item.icon
|
||||||
? html`
|
? html` <img alt="" slot="start" .src=${item.icon} /> `
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
slot="start"
|
|
||||||
.src="/api/hassio/addons/${item.slug}/icon"
|
|
||||||
/>
|
|
||||||
`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
</ha-combo-box-item>
|
</ha-combo-box-item>
|
||||||
`;
|
`;
|
||||||
@@ -38,22 +38,22 @@ class HaAddonPicker extends LitElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
@state() private _addons?: HassioAddonInfo[];
|
@state() private _addons?: PickerComboBoxItem[];
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
@query("ha-combo-box") private _comboBox!: HaComboBox;
|
@query("ha-generic-picker") private _genericPicker!: HaGenericPicker;
|
||||||
|
|
||||||
@state() private _error?: string;
|
@state() private _error?: string;
|
||||||
|
|
||||||
public open() {
|
public open() {
|
||||||
this._comboBox?.open();
|
this._genericPicker?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
public focus() {
|
public focus() {
|
||||||
this._comboBox?.focus();
|
this._genericPicker?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated() {
|
protected firstUpdated() {
|
||||||
@@ -61,29 +61,34 @@ class HaAddonPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
|
const label =
|
||||||
|
this.label === undefined && this.hass
|
||||||
|
? this.hass.localize("ui.components.addon-picker.addon")
|
||||||
|
: this.label;
|
||||||
|
|
||||||
if (this._error) {
|
if (this._error) {
|
||||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||||
}
|
}
|
||||||
if (!this._addons) {
|
if (!this._addons) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-combo-box
|
<ha-generic-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.label=${this.label === undefined && this.hass
|
.autofocus=${this.autofocus}
|
||||||
? this.hass.localize("ui.components.addon-picker.addon")
|
.label=${label}
|
||||||
: this.label}
|
.valueRenderer=${this._valueRenderer}
|
||||||
.value=${this._value}
|
|
||||||
.required=${this.required}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.renderer=${rowRenderer}
|
.disabled=${this.disabled}
|
||||||
.items=${this._addons}
|
.required=${this.required}
|
||||||
item-value-path="slug"
|
.value=${this.value}
|
||||||
item-id-path="slug"
|
.getItems=${this._getItems}
|
||||||
item-label-path="name"
|
.searchKeys=${SEARCH_KEYS}
|
||||||
|
.rowRenderer=${rowRenderer}
|
||||||
@value-changed=${this._addonChanged}
|
@value-changed=${this._addonChanged}
|
||||||
></ha-combo-box>
|
>
|
||||||
|
</ha-generic-picker>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,9 +98,19 @@ class HaAddonPicker extends LitElement {
|
|||||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||||
this._addons = addonsInfo.addons
|
this._addons = addonsInfo.addons
|
||||||
.filter((addon) => addon.version)
|
.filter((addon) => addon.version)
|
||||||
.sort((a, b) =>
|
.map((addon) => ({
|
||||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
id: addon.slug,
|
||||||
);
|
primary: addon.name,
|
||||||
|
secondary: addon.slug,
|
||||||
|
icon: addon.icon
|
||||||
|
? `/api/hassio/addons/${addon.slug}/icon`
|
||||||
|
: undefined,
|
||||||
|
search_labels: {
|
||||||
|
description: addon.description || null,
|
||||||
|
repository: addon.repository || null,
|
||||||
|
},
|
||||||
|
sorting_label: [addon.name, addon.slug].filter(Boolean).join("_"),
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
this._error = this.hass.localize(
|
this._error = this.hass.localize(
|
||||||
"ui.components.addon-picker.error.no_supervisor"
|
"ui.components.addon-picker.error.no_supervisor"
|
||||||
@@ -108,6 +123,8 @@ class HaAddonPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getItems = () => this._addons!;
|
||||||
|
|
||||||
private get _value() {
|
private get _value() {
|
||||||
return this.value || "";
|
return this.value || "";
|
||||||
}
|
}
|
||||||
@@ -128,6 +145,17 @@ class HaAddonPicker extends LitElement {
|
|||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _valueRenderer = (itemId: string) => {
|
||||||
|
const item = this._addons!.find((addon) => addon.id === itemId);
|
||||||
|
return html`${item?.icon
|
||||||
|
? html`<img
|
||||||
|
slot="start"
|
||||||
|
alt=${item.primary ?? "Unknown"}
|
||||||
|
.src=${item.icon}
|
||||||
|
/>`
|
||||||
|
: nothing}<span slot="headline">${item?.primary || "Unknown"}</span>`;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,9 +13,9 @@ import { createAreaRegistryEntry } from "../data/area_registry";
|
|||||||
import type {
|
import type {
|
||||||
DeviceEntityDisplayLookup,
|
DeviceEntityDisplayLookup,
|
||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
} from "../data/device_registry";
|
} from "../data/device/device_registry";
|
||||||
import { getDeviceEntityDisplayLookup } from "../data/device_registry";
|
import { getDeviceEntityDisplayLookup } from "../data/device/device_registry";
|
||||||
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
|
||||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
|
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
@@ -30,6 +30,12 @@ import "./ha-svg-icon";
|
|||||||
|
|
||||||
const ADD_NEW_ID = "___ADD_NEW___";
|
const ADD_NEW_ID = "___ADD_NEW___";
|
||||||
|
|
||||||
|
const SEARCH_KEYS = [
|
||||||
|
{ name: "search_labels.areaName", weight: 10 },
|
||||||
|
{ name: "search_labels.aliases", weight: 8 },
|
||||||
|
{ name: "search_labels.floorName", weight: 6 },
|
||||||
|
{ name: "search_labels.id", weight: 3 },
|
||||||
|
];
|
||||||
@customElement("ha-area-picker")
|
@customElement("ha-area-picker")
|
||||||
export class HaAreaPicker extends LitElement {
|
export class HaAreaPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -290,13 +296,12 @@ export class HaAreaPicker extends LitElement {
|
|||||||
secondary: floorName,
|
secondary: floorName,
|
||||||
icon: area.icon || undefined,
|
icon: area.icon || undefined,
|
||||||
icon_path: area.icon ? undefined : mdiTextureBox,
|
icon_path: area.icon ? undefined : mdiTextureBox,
|
||||||
sorting_label: areaName,
|
search_labels: {
|
||||||
search_labels: [
|
areaName: areaName || null,
|
||||||
areaName,
|
floorName: floorName || null,
|
||||||
floorName,
|
id: area.area_id,
|
||||||
area.area_id,
|
aliases: area.aliases.join(" "),
|
||||||
...area.aliases,
|
},
|
||||||
].filter((v): v is string => Boolean(v)),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -358,27 +363,41 @@ export class HaAreaPicker extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const placeholder =
|
const baseLabel =
|
||||||
this.placeholder ?? this.hass.localize("ui.components.area-picker.area");
|
this.label ?? this.hass.localize("ui.components.area-picker.area");
|
||||||
|
|
||||||
const valueRenderer = this._computeValueRenderer(this.hass.areas);
|
const valueRenderer = this._computeValueRenderer(this.hass.areas);
|
||||||
|
|
||||||
|
// Only show label if there's no floor
|
||||||
|
let label: string | undefined = baseLabel;
|
||||||
|
if (this.value && baseLabel) {
|
||||||
|
const area = this.hass.areas[this.value];
|
||||||
|
if (area) {
|
||||||
|
const { floor } = getAreaContext(area, this.hass.floors);
|
||||||
|
if (floor) {
|
||||||
|
label = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-generic-picker
|
<ha-generic-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.label=${this.label}
|
.label=${label}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.notFoundLabel=${this._notFoundLabel}
|
.notFoundLabel=${this._notFoundLabel}
|
||||||
.emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
|
.emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.placeholder=${placeholder}
|
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
.valueRenderer=${valueRenderer}
|
.valueRenderer=${valueRenderer}
|
||||||
.addButtonLabel=${this.addButtonLabel}
|
.addButtonLabel=${this.addButtonLabel}
|
||||||
|
.searchKeys=${SEARCH_KEYS}
|
||||||
|
.unknownItemText=${this.hass.localize(
|
||||||
|
"ui.components.area-picker.unknown"
|
||||||
|
)}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
</ha-generic-picker>
|
</ha-generic-picker>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { LitElement, html } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||||
import { areaCompare } from "../data/area_registry";
|
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-expansion-panel";
|
import "./ha-expansion-panel";
|
||||||
import "./ha-items-display-editor";
|
import "./ha-items-display-editor";
|
||||||
@@ -37,11 +36,7 @@ export class HaAreasDisplayEditor extends LitElement {
|
|||||||
public showNavigationButton = false;
|
public showNavigationButton = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const compare = areaCompare(this.hass.areas);
|
const areas = Object.values(this.hass.areas);
|
||||||
|
|
||||||
const areas = Object.values(this.hass.areas).sort((areaA, areaB) =>
|
|
||||||
compare(areaA.area_id, areaB.area_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
const items: DisplayItem[] = areas.map((area) => {
|
const items: DisplayItem[] = areas.map((area) => {
|
||||||
const { floor } = getAreaContext(area, this.hass.floors);
|
const { floor } = getAreaContext(area, this.hass.floors);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import memoizeOne from "memoize-one";
|
|||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||||
import { areaCompare } from "../data/area_registry";
|
|
||||||
import type { FloorRegistryEntry } from "../data/floor_registry";
|
import type { FloorRegistryEntry } from "../data/floor_registry";
|
||||||
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
|
import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
@@ -131,11 +130,8 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
|||||||
// update items if floors change
|
// update items if floors change
|
||||||
_hassFloors: HomeAssistant["floors"]
|
_hassFloors: HomeAssistant["floors"]
|
||||||
): Record<string, DisplayItem[]> => {
|
): 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(
|
const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
|
||||||
(acc, area) => {
|
(acc, area) => {
|
||||||
const { floor } = getAreaContext(area, this.hass.floors);
|
const { floor } = getAreaContext(area, this.hass.floors);
|
||||||
|
|||||||
@@ -659,6 +659,7 @@ export class HaAssistChat extends LitElement {
|
|||||||
--markdown-table-border-color: var(--divider-color);
|
--markdown-table-border-color: var(--divider-color);
|
||||||
--markdown-code-background-color: var(--primary-background-color);
|
--markdown-code-background-color: var(--primary-background-color);
|
||||||
--markdown-code-text-color: var(--primary-text-color);
|
--markdown-code-text-color: var(--primary-text-color);
|
||||||
|
--markdown-list-indent: 1.15em;
|
||||||
&:not(:has(ha-markdown-element)) {
|
&:not(:has(ha-markdown-element)) {
|
||||||
min-height: 1lh;
|
min-height: 1lh;
|
||||||
min-width: 1lh;
|
min-width: 1lh;
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
|
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
|
||||||
|
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||||
import {
|
import {
|
||||||
STATE_ATTRIBUTES,
|
STATE_ATTRIBUTES,
|
||||||
STATE_ATTRIBUTES_DOMAIN_CLASS,
|
STATE_ATTRIBUTES_DOMAIN_CLASS,
|
||||||
} from "../data/entity_attributes";
|
} from "../data/entity/entity_attributes";
|
||||||
import { haStyle } from "../resources/styles";
|
import { haStyle } from "../resources/styles";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-attribute-value";
|
import "./ha-attribute-value";
|
||||||
import "./ha-expansion-panel";
|
import "./ha-expansion-panel";
|
||||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
|
||||||
|
|
||||||
@customElement("ha-attributes")
|
@customElement("ha-attributes")
|
||||||
class HaAttributes extends LitElement {
|
class HaAttributes extends LitElement {
|
||||||
|
|||||||
@@ -52,8 +52,10 @@ export class HaAutomationRow extends LitElement {
|
|||||||
<slot name="leading-icon"></slot>
|
<slot name="leading-icon"></slot>
|
||||||
</div>
|
</div>
|
||||||
<slot class="header" name="header"></slot>
|
<slot class="header" name="header"></slot>
|
||||||
|
<div class="icons">
|
||||||
<slot name="icons"></slot>
|
<slot name="icons"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,12 +120,11 @@ export class HaAutomationRow extends LitElement {
|
|||||||
}
|
}
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: var(--ha-space-0) var(--ha-space-2);
|
padding: 0 var(--ha-space-3);
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-weight: var(--ha-font-weight-medium);
|
|
||||||
outline: none;
|
outline: none;
|
||||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||||
}
|
}
|
||||||
@@ -140,11 +141,15 @@ export class HaAutomationRow extends LitElement {
|
|||||||
background-color: var(--ha-color-fill-neutral-loud-resting);
|
background-color: var(--ha-color-fill-neutral-loud-resting);
|
||||||
border-radius: var(--ha-border-radius-md);
|
border-radius: var(--ha-border-radius-md);
|
||||||
padding: var(--ha-space-1);
|
padding: var(--ha-space-1);
|
||||||
|
margin-top: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
|
.leading-icon-wrapper {
|
||||||
|
padding-top: var(--ha-space-3);
|
||||||
|
}
|
||||||
::slotted([slot="leading-icon"]) {
|
::slotted([slot="leading-icon"]) {
|
||||||
color: var(--ha-color-on-neutral-quiet);
|
color: var(--ha-color-on-neutral-quiet);
|
||||||
}
|
}
|
||||||
@@ -170,7 +175,11 @@ export class HaAutomationRow extends LitElement {
|
|||||||
::slotted([slot="header"]) {
|
::slotted([slot="header"]) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
margin: var(--ha-space-0) var(--ha-space-3);
|
margin: 0 var(--ha-space-3);
|
||||||
|
}
|
||||||
|
.icons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
:host([sort-selected]) .row {
|
:host([sort-selected]) .row {
|
||||||
outline: solid;
|
outline: solid;
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import "@home-assistant/webawesome/dist/components/drawer/drawer";
|
|||||||
import { css, html, LitElement, type PropertyValues } from "lit";
|
import { css, html, LitElement, type PropertyValues } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
|
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
|
||||||
|
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
|
||||||
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
|
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
|
||||||
|
|
||||||
@customElement("ha-bottom-sheet")
|
@customElement("ha-bottom-sheet")
|
||||||
export class HaBottomSheet extends LitElement {
|
export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||||
@property({ type: Boolean }) public open = false;
|
@property({ type: Boolean }) public open = false;
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
|
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
|
||||||
@@ -17,11 +18,18 @@ export class HaBottomSheet extends LitElement {
|
|||||||
|
|
||||||
@query("#drawer") private _drawer!: HTMLElement;
|
@query("#drawer") private _drawer!: HTMLElement;
|
||||||
|
|
||||||
|
@query("#body") private _bodyElement!: HTMLDivElement;
|
||||||
|
|
||||||
|
protected get scrollableElement(): HTMLElement | null {
|
||||||
|
return this._bodyElement;
|
||||||
|
}
|
||||||
|
|
||||||
private _gestureRecognizer = new SwipeGestureRecognizer();
|
private _gestureRecognizer = new SwipeGestureRecognizer();
|
||||||
|
|
||||||
private _isDragging = false;
|
private _isDragging = false;
|
||||||
|
|
||||||
private _handleAfterHide() {
|
private _handleAfterHide(afterHideEvent: Event) {
|
||||||
|
afterHideEvent.stopPropagation();
|
||||||
this.open = false;
|
this.open = false;
|
||||||
const ev = new Event("closed", {
|
const ev = new Event("closed", {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
@@ -48,9 +56,13 @@ export class HaBottomSheet extends LitElement {
|
|||||||
@touchstart=${this._handleTouchStart}
|
@touchstart=${this._handleTouchStart}
|
||||||
>
|
>
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
|
<div class="content-wrapper">
|
||||||
<div id="body" class="body ha-scrollbar">
|
<div id="body" class="body ha-scrollbar">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
${this.renderScrollableFades()}
|
||||||
|
</div>
|
||||||
|
<slot name="footer"></slot>
|
||||||
</wa-drawer>
|
</wa-drawer>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -166,7 +178,9 @@ export class HaBottomSheet extends LitElement {
|
|||||||
this._isDragging = false;
|
this._isDragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = [
|
static get styles() {
|
||||||
|
return [
|
||||||
|
...super.styles,
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
css`
|
css`
|
||||||
wa-drawer {
|
wa-drawer {
|
||||||
@@ -207,6 +221,13 @@ export class HaBottomSheet extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
.content-wrapper {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
:host([flexcontent]) .body {
|
:host([flexcontent]) .body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -218,8 +239,26 @@ export class HaBottomSheet extends LitElement {
|
|||||||
var(--safe-area-inset-left)
|
var(--safe-area-inset-left)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
slot[name="footer"] {
|
||||||
|
display: block;
|
||||||
|
padding: 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 {
|
declare global {
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ export class HaCard extends LitElement {
|
|||||||
line-height: var(--ha-line-height-expanded);
|
line-height: var(--ha-line-height-expanded);
|
||||||
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4);
|
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4);
|
||||||
display: block;
|
display: block;
|
||||||
margin-block-start: var(--ha-space-0);
|
margin-block-start: 0;
|
||||||
margin-block-end: var(--ha-space-0);
|
margin-block-end: 0;
|
||||||
font-weight: var(--ha-font-weight-normal);
|
font-weight: var(--ha-font-weight-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
:host ::slotted(.card-content:not(:first-child)),
|
:host ::slotted(.card-content:not(:first-child)),
|
||||||
slot:not(:first-child)::slotted(.card-content) {
|
slot:not(:first-child)::slotted(.card-content) {
|
||||||
padding-top: var(--ha-space-0);
|
padding-top: 0;
|
||||||
margin-top: calc(var(--ha-space-2) * -1);
|
margin-top: calc(var(--ha-space-2) * -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import type { ClimateEntity } from "../data/climate";
|
import type { ClimateEntity } from "../data/climate";
|
||||||
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
||||||
import { isUnavailableState, OFF } from "../data/entity";
|
import { isUnavailableState, OFF } from "../data/entity/entity";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
|
||||||
@customElement("ha-climate-state")
|
@customElement("ha-climate-state")
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
|
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
|
||||||
import { css, html, LitElement, nothing } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
|
||||||
import type { LocalizeKeys } from "../common/translations/localize";
|
import type { LocalizeKeys } from "../common/translations/localize";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-list-item";
|
import "./ha-generic-picker";
|
||||||
import "./ha-md-divider";
|
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||||
import "./ha-select";
|
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||||
import type { HaSelect } from "./ha-select";
|
|
||||||
|
|
||||||
@customElement("ha-color-picker")
|
@customElement("ha-color-picker")
|
||||||
export class HaColorPicker extends LitElement {
|
export class HaColorPicker extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public value?: string;
|
@property() public value?: string;
|
||||||
|
|
||||||
@property({ type: String, attribute: "default_color" })
|
@property({ type: String, attribute: "default_color" })
|
||||||
@@ -33,137 +31,178 @@ export class HaColorPicker extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@query("ha-select") private _select?: HaSelect;
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
connectedCallback(): void {
|
render() {
|
||||||
super.connectedCallback();
|
const effectiveValue = this.value ?? this.defaultColor ?? "";
|
||||||
// Refresh layout options when the field is connected to the DOM to ensure current value displayed
|
|
||||||
this._select?.layoutOptions();
|
return html`
|
||||||
|
<ha-generic-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
.hideClearIcon=${!this.value && !!this.defaultColor}
|
||||||
|
.label=${this.label}
|
||||||
|
.helper=${this.helper}
|
||||||
|
.value=${effectiveValue}
|
||||||
|
.getItems=${this._getItems}
|
||||||
|
.rowRenderer=${this._rowRenderer}
|
||||||
|
.valueRenderer=${this._valueRenderer}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
>
|
||||||
|
</ha-generic-picker>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueSelected(ev) {
|
private _getItems = () =>
|
||||||
ev.stopPropagation();
|
this._getColors(
|
||||||
if (!this.isConnected) return;
|
this.includeNone,
|
||||||
const value = ev.target.value;
|
this.includeState,
|
||||||
this.value = value === this.defaultColor ? undefined : value;
|
this.defaultColor,
|
||||||
fireEvent(this, "value-changed", {
|
this.value
|
||||||
value: this.value,
|
);
|
||||||
|
|
||||||
|
private _getColors = (
|
||||||
|
includeNone: boolean,
|
||||||
|
includeState: boolean,
|
||||||
|
defaultColor: string | undefined,
|
||||||
|
currentValue: string | undefined
|
||||||
|
): PickerComboBoxItem[] => {
|
||||||
|
const items: PickerComboBoxItem[] = [];
|
||||||
|
|
||||||
|
const defaultSuffix = this.hass.localize(
|
||||||
|
"ui.components.color-picker.default"
|
||||||
|
);
|
||||||
|
|
||||||
|
const addDefaultSuffix = (label: string, isDefault: boolean) =>
|
||||||
|
isDefault && defaultSuffix ? `${label} (${defaultSuffix})` : label;
|
||||||
|
|
||||||
|
if (includeNone) {
|
||||||
|
const noneLabel =
|
||||||
|
this.hass.localize("ui.components.color-picker.none") || "None";
|
||||||
|
items.push({
|
||||||
|
id: "none",
|
||||||
|
primary: addDefaultSuffix(noneLabel, defaultColor === "none"),
|
||||||
|
icon_path: mdiInvertColorsOff,
|
||||||
|
sorting_label: noneLabel,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
if (includeState) {
|
||||||
const value = this.value || this.defaultColor || "";
|
const stateLabel =
|
||||||
|
this.hass.localize("ui.components.color-picker.state") || "State";
|
||||||
|
items.push({
|
||||||
|
id: "state",
|
||||||
|
primary: addDefaultSuffix(stateLabel, defaultColor === "state"),
|
||||||
|
icon_path: mdiPalette,
|
||||||
|
sorting_label: stateLabel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const isCustom = !(
|
Array.from(THEME_COLORS).forEach((color) => {
|
||||||
THEME_COLORS.has(value) ||
|
const themeLabel =
|
||||||
value === "none" ||
|
this.hass.localize(
|
||||||
value === "state"
|
|
||||||
);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-select
|
|
||||||
.icon=${Boolean(value)}
|
|
||||||
.label=${this.label}
|
|
||||||
.value=${value}
|
|
||||||
.helper=${this.helper}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
@closed=${stopPropagation}
|
|
||||||
@selected=${this._valueSelected}
|
|
||||||
fixedMenuPosition
|
|
||||||
naturalMenuWidth
|
|
||||||
.clearable=${!this.defaultColor}
|
|
||||||
>
|
|
||||||
${value
|
|
||||||
? html`
|
|
||||||
<span slot="icon">
|
|
||||||
${value === "none"
|
|
||||||
? html`
|
|
||||||
<ha-svg-icon path=${mdiInvertColorsOff}></ha-svg-icon>
|
|
||||||
`
|
|
||||||
: value === "state"
|
|
||||||
? html`<ha-svg-icon path=${mdiPalette}></ha-svg-icon>`
|
|
||||||
: this._renderColorCircle(value || "grey")}
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${this.includeNone
|
|
||||||
? html`
|
|
||||||
<ha-list-item value="none" graphic="icon">
|
|
||||||
${this.hass.localize("ui.components.color-picker.none")}
|
|
||||||
${this.defaultColor === "none"
|
|
||||||
? ` (${this.hass.localize("ui.components.color-picker.default")})`
|
|
||||||
: nothing}
|
|
||||||
<ha-svg-icon
|
|
||||||
slot="graphic"
|
|
||||||
path=${mdiInvertColorsOff}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</ha-list-item>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${this.includeState
|
|
||||||
? html`
|
|
||||||
<ha-list-item value="state" graphic="icon">
|
|
||||||
${this.hass.localize("ui.components.color-picker.state")}
|
|
||||||
${this.defaultColor === "state"
|
|
||||||
? ` (${this.hass.localize("ui.components.color-picker.default")})`
|
|
||||||
: nothing}
|
|
||||||
<ha-svg-icon slot="graphic" path=${mdiPalette}></ha-svg-icon>
|
|
||||||
</ha-list-item>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${this.includeState || this.includeNone
|
|
||||||
? html`<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
|
|
||||||
: nothing}
|
|
||||||
${Array.from(THEME_COLORS).map(
|
|
||||||
(color) => html`
|
|
||||||
<ha-list-item .value=${color} graphic="icon">
|
|
||||||
${this.hass.localize(
|
|
||||||
`ui.components.color-picker.colors.${color}` as LocalizeKeys
|
`ui.components.color-picker.colors.${color}` as LocalizeKeys
|
||||||
) || color}
|
) || color;
|
||||||
${this.defaultColor === color
|
items.push({
|
||||||
? ` (${this.hass.localize("ui.components.color-picker.default")})`
|
id: color,
|
||||||
: nothing}
|
primary: addDefaultSuffix(themeLabel, defaultColor === color),
|
||||||
<span slot="graphic">${this._renderColorCircle(color)}</span>
|
sorting_label: themeLabel,
|
||||||
</ha-list-item>
|
});
|
||||||
`
|
});
|
||||||
)}
|
|
||||||
${isCustom
|
const isSpecial =
|
||||||
? html`
|
currentValue === "none" ||
|
||||||
<ha-list-item .value=${value} graphic="icon">
|
currentValue === "state" ||
|
||||||
${value}
|
THEME_COLORS.has(currentValue || "");
|
||||||
<span slot="graphic">${this._renderColorCircle(value)}</span>
|
|
||||||
</ha-list-item>
|
const hasValue = currentValue && currentValue.length > 0;
|
||||||
`
|
|
||||||
: nothing}
|
if (hasValue && !isSpecial) {
|
||||||
</ha-select>
|
items.push({
|
||||||
|
id: currentValue!,
|
||||||
|
primary: currentValue!,
|
||||||
|
sorting_label: currentValue!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _rowRenderer: (
|
||||||
|
item: PickerComboBoxItem,
|
||||||
|
index?: number
|
||||||
|
) => ReturnType<typeof html> = (item) => html`
|
||||||
|
<ha-combo-box-item type="button" compact>
|
||||||
|
${item.id === "none"
|
||||||
|
? html`<ha-svg-icon
|
||||||
|
slot="start"
|
||||||
|
.path=${mdiInvertColorsOff}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: item.id === "state"
|
||||||
|
? html`<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>`
|
||||||
|
: html`<span slot="start">
|
||||||
|
${this._renderColorCircle(item.id)}
|
||||||
|
</span>`}
|
||||||
|
<span slot="headline">${item.primary}</span>
|
||||||
|
</ha-combo-box-item>
|
||||||
|
`;
|
||||||
|
|
||||||
|
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
||||||
|
if (value === "none") {
|
||||||
|
return html`
|
||||||
|
<ha-svg-icon slot="start" .path=${mdiInvertColorsOff}></ha-svg-icon>
|
||||||
|
<span slot="headline">
|
||||||
|
${this.hass.localize("ui.components.color-picker.none")}
|
||||||
|
</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
if (value === "state") {
|
||||||
|
return html`
|
||||||
|
<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>
|
||||||
|
<span slot="headline">
|
||||||
|
${this.hass.localize("ui.components.color-picker.state")}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<span slot="start">${this._renderColorCircle(value)}</span>
|
||||||
|
<span slot="headline">
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.components.color-picker.colors.${value}` as LocalizeKeys
|
||||||
|
) || value}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
private _renderColorCircle(color: string) {
|
private _renderColorCircle(color: string) {
|
||||||
return html`
|
return html`
|
||||||
<span
|
<span
|
||||||
class="circle-color"
|
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
"--circle-color": computeCssColor(color),
|
"--circle-color": computeCssColor(color),
|
||||||
|
display: "block",
|
||||||
|
"background-color": "var(--circle-color, var(--divider-color))",
|
||||||
|
border: "1px solid var(--outline-color)",
|
||||||
|
"border-radius": "var(--ha-border-radius-pill)",
|
||||||
|
width: "20px",
|
||||||
|
height: "20px",
|
||||||
|
"box-sizing": "border-box",
|
||||||
})}
|
})}
|
||||||
></span>
|
></span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
private _valueChanged(ev: CustomEvent<{ value?: string }>) {
|
||||||
.circle-color {
|
ev.stopPropagation();
|
||||||
display: block;
|
const selected = ev.detail.value;
|
||||||
background-color: var(--circle-color, var(--divider-color));
|
const normalized =
|
||||||
border: 1px solid var(--outline-color);
|
selected && selected === this.defaultColor
|
||||||
border-radius: var(--ha-border-radius-pill);
|
? undefined
|
||||||
width: 20px;
|
: (selected ?? undefined);
|
||||||
height: 20px;
|
this.value = normalized;
|
||||||
box-sizing: border-box;
|
fireEvent(this, "value-changed", { value: this.value });
|
||||||
}
|
}
|
||||||
ha-select {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -20,6 +20,17 @@ export class HaComboBoxItem extends HaMdListItem {
|
|||||||
[slot="start"] {
|
[slot="start"] {
|
||||||
--state-icon-color: var(--secondary-text-color);
|
--state-icon-color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
[slot="overline"] {
|
||||||
|
/* mimicing a floating label of mdc-select */
|
||||||
|
line-height: 1.15rem;
|
||||||
|
font-size: calc(var(--mdc-typography-subtitle1-font-size, 1rem) * 0.75);
|
||||||
|
font-weight: var(--mdc-typography-subtitle1-font-weight, 400);
|
||||||
|
font-family: var(
|
||||||
|
--mdc-typography-subtitle1-font-family,
|
||||||
|
var(--mdc-typography-font-family)
|
||||||
|
);
|
||||||
|
color: var(--mdc-select-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||||
|
}
|
||||||
[slot="headline"] {
|
[slot="headline"] {
|
||||||
line-height: var(--ha-line-height-normal);
|
line-height: var(--ha-line-height-normal);
|
||||||
font-size: var(--ha-font-size-m);
|
font-size: var(--ha-font-size-m);
|
||||||
|
|||||||
@@ -1,433 +0,0 @@
|
|||||||
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
|
||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
|
||||||
import { comboBoxRenderer } from "@vaadin/combo-box/lit";
|
|
||||||
import "@vaadin/combo-box/theme/material/vaadin-combo-box-light";
|
|
||||||
import type {
|
|
||||||
ComboBoxDataProvider,
|
|
||||||
ComboBoxLight,
|
|
||||||
ComboBoxLightFilterChangedEvent,
|
|
||||||
ComboBoxLightOpenedChangedEvent,
|
|
||||||
ComboBoxLightValueChangedEvent,
|
|
||||||
} from "@vaadin/combo-box/vaadin-combo-box-light";
|
|
||||||
import { registerStyles } from "@vaadin/vaadin-themable-mixin/register-styles";
|
|
||||||
import type { TemplateResult } from "lit";
|
|
||||||
import { css, html, LitElement } from "lit";
|
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import "./ha-combo-box-item";
|
|
||||||
import "./ha-combo-box-textfield";
|
|
||||||
import "./ha-icon-button";
|
|
||||||
import "./ha-input-helper-text";
|
|
||||||
import "./ha-textfield";
|
|
||||||
import type { HaTextField } from "./ha-textfield";
|
|
||||||
|
|
||||||
registerStyles(
|
|
||||||
"vaadin-combo-box-item",
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
:host([focused]:not([disabled])) {
|
|
||||||
background-color: rgba(var(--rgb-primary-text-color, 0, 0, 0), 0.12);
|
|
||||||
}
|
|
||||||
:host([selected]:not([disabled])) {
|
|
||||||
background-color: transparent;
|
|
||||||
color: var(--mdc-theme-primary);
|
|
||||||
--mdc-ripple-color: var(--mdc-theme-primary);
|
|
||||||
--mdc-theme-text-primary-on-background: var(--mdc-theme-primary);
|
|
||||||
}
|
|
||||||
:host([selected]:not([disabled])):before {
|
|
||||||
background-color: var(--mdc-theme-primary);
|
|
||||||
opacity: 0.12;
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
:host([selected][focused]:not([disabled])):before {
|
|
||||||
opacity: 0.24;
|
|
||||||
}
|
|
||||||
:host(:hover:not([disabled])) {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
[part="content"] {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
[part="checkmark"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
@customElement("ha-combo-box")
|
|
||||||
export class HaComboBox extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public label?: string;
|
|
||||||
|
|
||||||
@property() public value?: string;
|
|
||||||
|
|
||||||
@property() public placeholder?: string;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public validationMessage?: string;
|
|
||||||
|
|
||||||
@property() public helper?: string;
|
|
||||||
|
|
||||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public invalid = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public icon = false;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public items?: any[];
|
|
||||||
|
|
||||||
@property({ attribute: false }) public filteredItems?: any[];
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public dataProvider?: ComboBoxDataProvider<any>;
|
|
||||||
|
|
||||||
@property({ attribute: "allow-custom-value", type: Boolean })
|
|
||||||
public allowCustomValue = false;
|
|
||||||
|
|
||||||
@property({ attribute: "item-value-path" }) public itemValuePath = "value";
|
|
||||||
|
|
||||||
@property({ attribute: "item-label-path" }) public itemLabelPath = "label";
|
|
||||||
|
|
||||||
@property({ attribute: "item-id-path" }) public itemIdPath?: string;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public renderer?: ComboBoxLitRenderer<any>;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public opened = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "hide-clear-icon" })
|
|
||||||
public hideClearIcon = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "clear-initial-value" })
|
|
||||||
public clearInitialValue = false;
|
|
||||||
|
|
||||||
@query("vaadin-combo-box-light", true) private _comboBox!: ComboBoxLight;
|
|
||||||
|
|
||||||
@query("ha-combo-box-textfield", true) private _inputElement!: HaTextField;
|
|
||||||
|
|
||||||
@state({ type: Boolean }) private _forceBlankValue = false;
|
|
||||||
|
|
||||||
private _overlayMutationObserver?: MutationObserver;
|
|
||||||
|
|
||||||
private _bodyMutationObserver?: MutationObserver;
|
|
||||||
|
|
||||||
public async open() {
|
|
||||||
await this.updateComplete;
|
|
||||||
this._comboBox?.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async focus() {
|
|
||||||
await this.updateComplete;
|
|
||||||
await this._inputElement?.updateComplete;
|
|
||||||
this._inputElement?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
if (this._overlayMutationObserver) {
|
|
||||||
this._overlayMutationObserver.disconnect();
|
|
||||||
this._overlayMutationObserver = undefined;
|
|
||||||
}
|
|
||||||
if (this._bodyMutationObserver) {
|
|
||||||
this._bodyMutationObserver.disconnect();
|
|
||||||
this._bodyMutationObserver = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public get selectedItem() {
|
|
||||||
return this._comboBox.selectedItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setInputValue(value: string) {
|
|
||||||
this._comboBox.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setTextFieldValue(value: string) {
|
|
||||||
this._inputElement.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<!-- @ts-ignore Tag definition is not included in theme folder -->
|
|
||||||
<vaadin-combo-box-light
|
|
||||||
.itemValuePath=${this.itemValuePath}
|
|
||||||
.itemIdPath=${this.itemIdPath}
|
|
||||||
.itemLabelPath=${this.itemLabelPath}
|
|
||||||
.items=${this.items}
|
|
||||||
.value=${this.value || ""}
|
|
||||||
.filteredItems=${this.filteredItems}
|
|
||||||
.dataProvider=${this.dataProvider}
|
|
||||||
.allowCustomValue=${this.allowCustomValue}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
.required=${this.required}
|
|
||||||
${comboBoxRenderer(this.renderer || this._defaultRowRenderer)}
|
|
||||||
@opened-changed=${this._openedChanged}
|
|
||||||
@filter-changed=${this._filterChanged}
|
|
||||||
@value-changed=${this._valueChanged}
|
|
||||||
attr-for-value="value"
|
|
||||||
>
|
|
||||||
<ha-combo-box-textfield
|
|
||||||
label=${ifDefined(this.label)}
|
|
||||||
placeholder=${ifDefined(this.placeholder)}
|
|
||||||
?disabled=${this.disabled}
|
|
||||||
?required=${this.required}
|
|
||||||
validationMessage=${ifDefined(this.validationMessage)}
|
|
||||||
.errorMessage=${this.errorMessage}
|
|
||||||
class="input"
|
|
||||||
autocapitalize="none"
|
|
||||||
autocomplete="off"
|
|
||||||
.autocorrect=${false}
|
|
||||||
input-spellcheck="false"
|
|
||||||
.suffix=${html`<div
|
|
||||||
style="width: 28px;"
|
|
||||||
role="none presentation"
|
|
||||||
></div>`}
|
|
||||||
.icon=${this.icon}
|
|
||||||
.invalid=${this.invalid}
|
|
||||||
.forceBlankValue=${this._forceBlankValue}
|
|
||||||
>
|
|
||||||
<slot name="icon" slot="leadingIcon"></slot>
|
|
||||||
</ha-combo-box-textfield>
|
|
||||||
${this.value && !this.hideClearIcon
|
|
||||||
? html`<ha-svg-icon
|
|
||||||
role="button"
|
|
||||||
tabindex="-1"
|
|
||||||
aria-label=${ifDefined(this.hass?.localize("ui.common.clear"))}
|
|
||||||
class=${`clear-button ${this.label ? "" : "no-label"}`}
|
|
||||||
.path=${mdiClose}
|
|
||||||
?disabled=${this.disabled}
|
|
||||||
@click=${this._clearValue}
|
|
||||||
></ha-svg-icon>`
|
|
||||||
: ""}
|
|
||||||
<ha-svg-icon
|
|
||||||
role="button"
|
|
||||||
tabindex="-1"
|
|
||||||
aria-label=${ifDefined(this.label)}
|
|
||||||
aria-expanded=${this.opened ? "true" : "false"}
|
|
||||||
class=${`toggle-button ${this.label ? "" : "no-label"}`}
|
|
||||||
.path=${this.opened ? mdiMenuUp : mdiMenuDown}
|
|
||||||
?disabled=${this.disabled}
|
|
||||||
@click=${this._toggleOpen}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</vaadin-combo-box-light>
|
|
||||||
${this._renderHelper()}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _renderHelper() {
|
|
||||||
return this.helper
|
|
||||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
|
||||||
>${this.helper}</ha-input-helper-text
|
|
||||||
>`
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private _defaultRowRenderer: ComboBoxLitRenderer<
|
|
||||||
string | Record<string, any>
|
|
||||||
> = (item) => html`
|
|
||||||
<ha-combo-box-item type="button">
|
|
||||||
${this.itemLabelPath ? item[this.itemLabelPath] : item}
|
|
||||||
</ha-combo-box-item>
|
|
||||||
`;
|
|
||||||
|
|
||||||
private _clearValue(ev: Event) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
fireEvent(this, "value-changed", { value: undefined });
|
|
||||||
}
|
|
||||||
|
|
||||||
private _toggleOpen(ev: Event) {
|
|
||||||
if (this.opened) {
|
|
||||||
this._comboBox?.close();
|
|
||||||
ev.stopPropagation();
|
|
||||||
} else {
|
|
||||||
this._comboBox?.inputElement.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const opened = ev.detail.value;
|
|
||||||
// delay this so we can handle click event for toggle button before setting _opened
|
|
||||||
setTimeout(() => {
|
|
||||||
this.opened = opened;
|
|
||||||
fireEvent(this, "opened-changed", { value: ev.detail.value });
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
if (this.clearInitialValue) {
|
|
||||||
this.setTextFieldValue("");
|
|
||||||
if (opened) {
|
|
||||||
// Wait 100ms to be sure vaddin-combo-box-light already tried to set the value
|
|
||||||
setTimeout(() => {
|
|
||||||
this._forceBlankValue = false;
|
|
||||||
}, 100);
|
|
||||||
} else {
|
|
||||||
this._forceBlankValue = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opened) {
|
|
||||||
const overlay = document.querySelector<HTMLElement>(
|
|
||||||
"vaadin-combo-box-overlay"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (overlay) {
|
|
||||||
this._removeInert(overlay);
|
|
||||||
}
|
|
||||||
this._observeBody();
|
|
||||||
} else {
|
|
||||||
this._bodyMutationObserver?.disconnect();
|
|
||||||
this._bodyMutationObserver = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _observeBody() {
|
|
||||||
if ("MutationObserver" in window && !this._bodyMutationObserver) {
|
|
||||||
this._bodyMutationObserver = new MutationObserver((mutations) => {
|
|
||||||
mutations.forEach((mutation) => {
|
|
||||||
mutation.addedNodes.forEach((node) => {
|
|
||||||
if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") {
|
|
||||||
this._removeInert(node as HTMLElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
mutation.removedNodes.forEach((node) => {
|
|
||||||
if (node.nodeName === "VAADIN-COMBO-BOX-OVERLAY") {
|
|
||||||
this._overlayMutationObserver?.disconnect();
|
|
||||||
this._overlayMutationObserver = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this._bodyMutationObserver.observe(document.body, {
|
|
||||||
childList: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _removeInert(overlay: HTMLElement) {
|
|
||||||
if (overlay.inert) {
|
|
||||||
overlay.inert = false;
|
|
||||||
this._overlayMutationObserver?.disconnect();
|
|
||||||
this._overlayMutationObserver = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ("MutationObserver" in window && !this._overlayMutationObserver) {
|
|
||||||
this._overlayMutationObserver = new MutationObserver((mutations) => {
|
|
||||||
mutations.forEach((mutation) => {
|
|
||||||
if (mutation.attributeName === "inert") {
|
|
||||||
const target = mutation.target as HTMLElement;
|
|
||||||
if (target.inert) {
|
|
||||||
this._overlayMutationObserver?.disconnect();
|
|
||||||
this._overlayMutationObserver = undefined;
|
|
||||||
target.inert = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this._overlayMutationObserver.observe(overlay, {
|
|
||||||
attributes: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _filterChanged(ev: ComboBoxLightFilterChangedEvent) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
fireEvent(this, "filter-changed", { value: ev.detail.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
if (!this.allowCustomValue) {
|
|
||||||
// @ts-ignore
|
|
||||||
this._comboBox._closeOnBlurIsPrevented = true;
|
|
||||||
}
|
|
||||||
if (!this.opened) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newValue = ev.detail.value;
|
|
||||||
if (newValue !== this.value) {
|
|
||||||
fireEvent(this, "value-changed", { value: newValue || undefined });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static styles = css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
vaadin-combo-box-light {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
ha-combo-box-textfield {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
ha-combo-box-textfield > ha-icon-button {
|
|
||||||
--mdc-icon-button-size: 24px;
|
|
||||||
padding: 2px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
ha-svg-icon {
|
|
||||||
color: var(--input-dropdown-icon-color);
|
|
||||||
position: absolute;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.toggle-button {
|
|
||||||
right: 12px;
|
|
||||||
top: -10px;
|
|
||||||
inset-inline-start: initial;
|
|
||||||
inset-inline-end: 12px;
|
|
||||||
direction: var(--direction);
|
|
||||||
}
|
|
||||||
:host([opened]) .toggle-button {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
.toggle-button[disabled],
|
|
||||||
.clear-button[disabled] {
|
|
||||||
color: var(--disabled-text-color);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.toggle-button.no-label {
|
|
||||||
top: -3px;
|
|
||||||
}
|
|
||||||
.clear-button {
|
|
||||||
--mdc-icon-size: 20px;
|
|
||||||
top: -7px;
|
|
||||||
right: 36px;
|
|
||||||
inset-inline-start: initial;
|
|
||||||
inset-inline-end: 36px;
|
|
||||||
direction: var(--direction);
|
|
||||||
}
|
|
||||||
.clear-button.no-label {
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
ha-input-helper-text {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-combo-box": HaComboBox;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HASSDomEvents {
|
|
||||||
"filter-changed": { value: string };
|
|
||||||
"opened-changed": { value: boolean };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||||
import { html, LitElement, nothing } from "lit";
|
import { html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
|
||||||
import type { ConfigEntry } from "../data/config_entries";
|
import type { ConfigEntry } from "../data/config_entries";
|
||||||
import { getConfigEntries } from "../data/config_entries";
|
import { getConfigEntries } from "../data/config_entries";
|
||||||
import { domainToName } from "../data/integration";
|
import { domainToName } from "../data/integration";
|
||||||
import type { ValueChangedEvent, HomeAssistant } from "../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
import { brandsUrl } from "../util/brands-url";
|
|
||||||
import "./ha-combo-box";
|
|
||||||
import type { HaComboBox } from "./ha-combo-box";
|
|
||||||
import "./ha-combo-box-item";
|
import "./ha-combo-box-item";
|
||||||
|
import "./ha-domain-icon";
|
||||||
|
import "./ha-generic-picker";
|
||||||
|
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||||
|
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||||
|
|
||||||
export interface ConfigEntryExtended extends ConfigEntry {
|
const SEARCH_KEYS = [
|
||||||
localized_domain_name?: string;
|
{ name: "primary", weight: 10 },
|
||||||
}
|
{ name: "secondary", weight: 8 },
|
||||||
|
{ name: "icon", weight: 5 },
|
||||||
|
];
|
||||||
|
|
||||||
@customElement("ha-config-entry-picker")
|
@customElement("ha-config-entry-picker")
|
||||||
class HaConfigEntryPicker extends LitElement {
|
class HaConfigEntryPicker extends LitElement {
|
||||||
@@ -28,119 +30,106 @@ class HaConfigEntryPicker extends LitElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
@state() private _configEntries?: ConfigEntryExtended[];
|
@state() private _configEntries?: PickerComboBoxItem[];
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
@query("ha-combo-box") private _comboBox!: HaComboBox;
|
@query("ha-generic-picker") private _picker!: HaGenericPicker;
|
||||||
|
|
||||||
public open() {
|
public open() {
|
||||||
this._comboBox?.open();
|
this._picker?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
public focus() {
|
public focus() {
|
||||||
this._comboBox?.focus();
|
this._picker?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated() {
|
protected firstUpdated() {
|
||||||
this._getConfigEntries();
|
this._getConfigEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _rowRenderer: ComboBoxLitRenderer<ConfigEntryExtended> = (
|
|
||||||
item
|
|
||||||
) => html`
|
|
||||||
<ha-combo-box-item type="button">
|
|
||||||
<span slot="headline">
|
|
||||||
${item.title ||
|
|
||||||
this.hass.localize(
|
|
||||||
"ui.panel.config.integrations.config_entry.unnamed_entry"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span slot="supporting-text">${item.localized_domain_name}</span>
|
|
||||||
<img
|
|
||||||
alt=""
|
|
||||||
slot="start"
|
|
||||||
src=${brandsUrl({
|
|
||||||
domain: item.domain,
|
|
||||||
type: "icon",
|
|
||||||
darkOptimized: this.hass.themes?.darkMode,
|
|
||||||
})}
|
|
||||||
crossorigin="anonymous"
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
@error=${this._onImageError}
|
|
||||||
@load=${this._onImageLoad}
|
|
||||||
/>
|
|
||||||
</ha-combo-box-item>
|
|
||||||
`;
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._configEntries) {
|
if (!this._configEntries) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<ha-combo-box
|
<ha-generic-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.label=${this.label === undefined && this.hass
|
.label=${this.label === undefined && this.hass
|
||||||
? this.hass.localize("ui.components.config-entry-picker.config_entry")
|
? this.hass.localize("ui.components.config-entry-picker.config_entry")
|
||||||
: this.label}
|
: this.label}
|
||||||
.value=${this._value}
|
.value=${this.value}
|
||||||
.required=${this.required}
|
.required=${this.required}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.renderer=${this._rowRenderer}
|
.rowRenderer=${this._rowRenderer}
|
||||||
.items=${this._configEntries}
|
.getItems=${this._getItems}
|
||||||
item-value-path="entry_id"
|
.searchKeys=${SEARCH_KEYS}
|
||||||
item-id-path="entry_id"
|
.valueRenderer=${this._valueRenderer}
|
||||||
item-label-path="title"
|
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
></ha-combo-box>
|
></ha-generic-picker>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onImageLoad(ev) {
|
private _rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||||
ev.target.style.visibility = "initial";
|
<ha-combo-box-item type="button">
|
||||||
}
|
<span slot="headline">${item.primary}</span>
|
||||||
|
<span slot="supporting-text">${item.secondary}</span>
|
||||||
private _onImageError(ev) {
|
<ha-domain-icon
|
||||||
ev.target.style.visibility = "hidden";
|
slot="start"
|
||||||
}
|
.hass=${this.hass}
|
||||||
|
.domain=${item.icon!}
|
||||||
|
brand-fallback
|
||||||
|
></ha-domain-icon>
|
||||||
|
</ha-combo-box-item>
|
||||||
|
`;
|
||||||
|
|
||||||
private async _getConfigEntries() {
|
private async _getConfigEntries() {
|
||||||
getConfigEntries(this.hass, {
|
getConfigEntries(this.hass, {
|
||||||
type: ["device", "hub", "service"],
|
type: ["device", "hub", "service"],
|
||||||
domain: this.integration,
|
domain: this.integration,
|
||||||
}).then((configEntries) => {
|
}).then((configEntries) => {
|
||||||
this._configEntries = configEntries
|
this._configEntries = configEntries.map((entry: ConfigEntry) => {
|
||||||
.map(
|
const domainName = domainToName(this.hass.localize, entry.domain);
|
||||||
(entry: ConfigEntry): ConfigEntryExtended => ({
|
return {
|
||||||
...entry,
|
id: entry.entry_id,
|
||||||
localized_domain_name: domainToName(
|
icon: entry.domain,
|
||||||
this.hass.localize,
|
primary:
|
||||||
entry.domain
|
entry.title ||
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.config_entry.unnamed_entry"
|
||||||
),
|
),
|
||||||
})
|
secondary: domainName,
|
||||||
)
|
sorting_label: [entry.title, domainName].filter(Boolean).join("_"),
|
||||||
.sort((conf1, conf2) =>
|
};
|
||||||
caseInsensitiveStringCompare(
|
});
|
||||||
conf1.localized_domain_name + conf1.title,
|
|
||||||
conf2.localized_domain_name + conf2.title,
|
|
||||||
this.hass.locale.language
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _value() {
|
private _valueRenderer = (itemId: string) => {
|
||||||
return this.value || "";
|
const item = this._configEntries!.find((entry) => entry.id === itemId);
|
||||||
}
|
return html`<span
|
||||||
|
style="display: flex; align-items: center; gap: var(--ha-space-2);"
|
||||||
|
slot="headline"
|
||||||
|
>${item?.icon
|
||||||
|
? html`<ha-domain-icon
|
||||||
|
.hass=${this.hass}
|
||||||
|
.domain=${item.icon!}
|
||||||
|
brand-fallback
|
||||||
|
></ha-domain-icon>`
|
||||||
|
: nothing}${item?.primary || "Unknown"}</span
|
||||||
|
>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _getItems = () => this._configEntries!;
|
||||||
|
|
||||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const newValue = ev.detail.value;
|
const newValue = ev.detail.value;
|
||||||
|
|
||||||
if (newValue !== this._value) {
|
if (newValue !== this.value) {
|
||||||
this._setValue(newValue);
|
this._setValue(newValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ export class HaControlSelect extends LitElement {
|
|||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
border-radius: var(--control-select-border-radius);
|
||||||
}
|
}
|
||||||
:host([vertical]) {
|
:host([vertical]) {
|
||||||
width: var(--control-select-thickness);
|
width: var(--control-select-thickness);
|
||||||
@@ -211,7 +212,6 @@ export class HaControlSelect extends LitElement {
|
|||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: var(--control-select-border-radius);
|
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import type { ConfigEntry, SubEntry } from "../data/config_entries";
|
|||||||
import { getConfigEntry, getSubEntries } from "../data/config_entries";
|
import { getConfigEntry, getSubEntries } from "../data/config_entries";
|
||||||
import type { Agent } from "../data/conversation";
|
import type { Agent } from "../data/conversation";
|
||||||
import { listAgents } from "../data/conversation";
|
import { listAgents } from "../data/conversation";
|
||||||
|
import { getExtendedEntityRegistryEntry } from "../data/entity/entity_registry";
|
||||||
import { fetchIntegrationManifest } from "../data/integration";
|
import { fetchIntegrationManifest } from "../data/integration";
|
||||||
import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow";
|
import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow";
|
||||||
|
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-list-item";
|
import "./ha-list-item";
|
||||||
import "./ha-select";
|
import "./ha-select";
|
||||||
import type { HaSelect } from "./ha-select";
|
import type { HaSelect } from "./ha-select";
|
||||||
import { getExtendedEntityRegistryEntry } from "../data/entity_registry";
|
|
||||||
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
|
|
||||||
|
|
||||||
const NONE = "__NONE_OPTION__";
|
const NONE = "__NONE_OPTION__";
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user