mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-31 22:49:37 +00:00 
			
		
		
		
	Compare commits
	
		
			299 Commits
		
	
	
		
			20250924.0
			...
			ha-wa-dial
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 1aa55b3071 | ||
|   | aa21eff508 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 75af4f939e | ||
|   | 453a2ac7f3 | ||
|   | 8fbd0226fc | ||
|   | 2a8d935601 | ||
|   | a6328fb6d7 | ||
|   | a78b61006f | ||
|   | d506aa23b6 | ||
|   | 48b4df43ab | ||
|   | 8cdcd9cb55 | ||
|   | a1e2ac1d99 | ||
|   | 8ecddbc42c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6f70ef52a5 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 7dff02d7c8 | ||
|   | 8bbd7a6a06 | ||
|   | 5c73a06f76 | ||
|   | 9943dae82c | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 70bf049df0 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f9d9fbb7f0 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9cb84d3f37 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | c1bcf27cf8 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 164ec2a9b5 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 20001a551c | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | b7f85bf733 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | b303e9441b | ||
|   | 8f4bd0f620 | ||
|   | 596346bf59 | ||
|   | 769cea92aa | ||
|   | f825016514 | ||
|   | c6fd45bd6a | ||
|   | 6c4f4af75c | ||
|   | cd5c3ef2f6 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 636a6fa02e | ||
|   | 21b83426d6 | ||
|   | c139ec22f9 | ||
|   | a6ef3a26da | ||
|   | 221ca56121 | ||
|   | 4e6e3629a8 | ||
|   | fe94ae0243 | ||
|   | 8a1a22d4bd | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 153a578986 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 04bb10d0a2 | ||
|   | 35e52de2c1 | ||
|   | b0862fddaa | ||
|   | 77735f5310 | ||
|   | e388756533 | ||
|   | e9ca9bb781 | ||
|   | e48918442c | ||
|   | 52f37f41f0 | ||
|   | 4687006fec | ||
|   | aca4ca3066 | ||
|   | 3a2c00622a | ||
|   | 699c25a6c3 | ||
|   | 1ad226d608 | ||
|   | 992a4cd98a | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | fd217f8ea5 | ||
|   | dede14e578 | ||
|   | fa7aca67e5 | ||
|   | 6abdfa6d5c | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 0a70e2abda | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 1ec589e9b6 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 2d2b5633c4 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 76df75c306 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 027ded61c2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a718589ba0 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 5b5dc9d853 | ||
|   | 2a49b5e15a | ||
|   | fa4dd1c5ea | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 37a3af2e8b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | fbfcef1573 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 4eecd37aaf | ||
|   | c798521ab8 | ||
|   | e432f0a8ee | ||
|   | e3a1d0abe2 | ||
|   | 8080ba696c | ||
|   | 7bd8f321a4 | ||
|   | 4e958302b4 | ||
|   | 8a42d15bde | ||
|   | ef0da0a7ee | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | ae053c20b0 | ||
|   | 5f71938d60 | ||
|   | 82ac26b326 | ||
|   | 80b92b9813 | ||
|   | 904a083f61 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d75ee09d55 | ||
|   | a8e0d506b6 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 01dd731622 | ||
|   | dc20702d36 | ||
|   | f32ca9be29 | ||
|   | 8c4c4157a8 | ||
|   | c8419d4c3d | ||
|   | 089316b8ae | ||
|   | 8d03ac5f64 | ||
|   | e0e1f6f920 | ||
|   | d4c98cae3a | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 46d0eb4f44 | ||
|   | 07812f8d84 | ||
|   | 96f54d348f | ||
|   | 6084ab116f | ||
|   | 6b7acd8d3b | ||
|   | e35b155c66 | ||
|   | 437d02c12f | ||
|   | 9cd74fbff8 | ||
|   | 33a7aacd83 | ||
|   | 39546615bb | ||
|   | be51cbc944 | ||
|   | 77874aa2d7 | ||
|   | 4808463d5f | ||
|   | 5fb3cab247 | ||
|   | d1093b187f | ||
|   | fd7f0d3841 | ||
|   | 36aa74e4a5 | ||
|   | 938128d1c3 | ||
|   | 2a5d4ac578 | ||
|   | be63ff7702 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 132c68bf20 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 16499bbd6b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | c7eddfed8f | ||
|   | 150842e431 | ||
|   | 9eb5360a68 | ||
|   | e9e32c7d91 | ||
|   | c83d760e82 | ||
|   | 489b7f9227 | ||
|   | ad2ba63155 | ||
|   | 29bc894dbd | ||
|   | faf6cb6333 | ||
|   | a2e1e6362b | ||
|   | 3212ab6f3b | ||
|   | 3d27daad80 | ||
|   | b679f1ce60 | ||
|   | 6b0a5d783b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 23e2f94d11 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c250777858 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | c35d0da9bd | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 794aa45a2b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d0b85d0c0b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 23b6a3a1a9 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 43a23e6cdd | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | aa4dd1cf29 | ||
|   | 0ae55c39cc | ||
|   | 0bfacacc9e | ||
|   | c2f21c19af | ||
|   | 6653333c38 | ||
|   | 8c19e080be | ||
|   | c649b1015a | ||
|   | 1b6c33efd4 | ||
|   | 5cfc34b020 | ||
|   | 1e7647b214 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | cef3a7ef99 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 14d0028426 | ||
|   | 28032d9d0d | ||
|   | 6c1995ba1b | ||
|   | b68464c5d5 | ||
|   | 31ccf114a6 | ||
|   | 1b932ae4a2 | ||
|   | 0df6019b95 | ||
|   | 94fb03d2e2 | ||
|   | 6dc165ebf8 | ||
|   | f2c5b91def | ||
|   | b312cca050 | ||
|   | ac14733bff | ||
|   | a2d4165511 | ||
|   | b87ffbd4f7 | ||
|   | a8f8d197f8 | ||
|   | 4fcac79047 | ||
|   | 42ddacd41a | ||
|   | ebc9981289 | ||
|   | 23deab253b | ||
|   | ab172abe02 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 10d5d8b15d | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | c9e472dab7 | ||
|   | 1e13b2b812 | ||
|   | e04a04632a | ||
|   | 04bc5fba63 | ||
|   | e66724ca9e | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | bcfe5add33 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7cc116dd07 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ee93f31220 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | b7cc19f12e | ||
|   | f70edf9311 | ||
|   | 0fa7c2face | ||
|   | 7b3a265a70 | ||
|   | 5d9aae3ad5 | ||
|   | 5de84ac0d8 | ||
|   | 98c4ec91d6 | ||
|   | 972b9cb758 | ||
|   | ac621af811 | ||
|   | 7eb97bb58f | ||
|   | d37af0f488 | ||
|   | 0d3b340228 | ||
|   | 288e03775b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | df36e9d205 | ||
|   | 15a0b35866 | ||
|   | aa7522f681 | ||
|   | c09e97a561 | ||
|   | 733be8e5a3 | ||
|   | d107ac7d4c | ||
|   | efc5bacb97 | ||
|   | 430e52efe3 | ||
|   | 6b4c4a9cf8 | ||
|   | e5b1acc2c3 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | c89f476d67 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | e68afead17 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | c4651c0bc0 | ||
|   | 6d95b7af11 | ||
|   | 3e74cf3ada | ||
|   | 859ee98abb | ||
|   | dd3e5e3724 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 2e3ab4d64f | ||
|   | 63cbeca820 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 1057ff314c | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 5b946f1048 | ||
|   | fdd66b5cec | ||
|   | 76c9723c71 | ||
|   | b02368b9c6 | ||
|   | 0bcb7897c9 | ||
|   | 786bbb3850 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | e8ead84fe5 | ||
|   | 428e7fb332 | ||
|   | ad9e8d5a52 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | e3cf04b3d1 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 10c3042db1 | ||
|   | 25f6b7de2f | ||
|   | ca1cda4824 | ||
|   | 8c4a67315b | ||
|   | c18de97b32 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 23a3ca3ed7 | ||
|   | 69457b4e85 | ||
|   | 2e096c23e0 | ||
|   | 552691e200 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 91258c86c1 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 3750a378cd | ||
|   | 12d3304c72 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 246100809d | ||
|   | 6efca93186 | ||
|   | 6280647b9a | ||
|   | 2ff52c6c29 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d038e11170 | ||
|   | 8925b39fe5 | ||
|   | beeef65506 | ||
|   | 994c1b5751 | ||
|   | 6823c647b6 | ||
|   | 866b478dc0 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d746dc5752 | ||
|   | 5f53e1e71c | ||
|   | 3da82df093 | ||
|   | 4cedfffb71 | ||
|   | 1e1514e7da | ||
|   | 60e07075bc | ||
|   | c998086474 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 53be0a3fa2 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d69c46c80c | ||
|   | 0c2a7bfed0 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | afdd232e38 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 179751a135 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 52f6024306 | ||
|   | 7c7a4e61f2 | ||
|   | facce7b016 | ||
|   | e546cb3374 | ||
|   | a0d2e7312b | ||
|   | c0a9dadcbe | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | e1edf7fb98 | ||
|   | 6d5c165bd2 | ||
|   | 54177a16e9 | ||
|   | c814b8e888 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 33a0b32cc5 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 7dae13bf57 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 0a3fe6e0fb | ||
|   | e0348e4da7 | ||
|   | d53f3ec898 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | e422547d93 | ||
|   | d91a3fbe85 | ||
|   | 01d7130f22 | ||
|   | c57851e4df | ||
|   | 6f1f13acb0 | ||
|   | a8abd00809 | ||
|   | e053978dbe | ||
|   | 6e57f726a3 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | b7cabadbe1 | ||
|   | d920217374 | ||
|   | 1630263276 | ||
|   | 5680c742be | ||
|   | 2aeb9cf0ef | ||
|   | c9931b3a3c | ||
|   | fbf7ebdfe4 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 52ccb03de5 | ||
|   | 900236ac07 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 28940c930d | ||
|   | e278b463fd | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | db2acd4e39 | ||
|   | 6dcc52cd44 | ||
|   | 981db50826 | ||
|   | 09683863a7 | ||
|   | 8c78f931dc | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 40ce3c1e31 | ||
|   | e430a1b1be | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | a2c6116417 | ||
|   | 3239273f3e | 
							
								
								
									
										8
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -26,7 +26,7 @@ jobs: | |||||||
|           ref: dev |           ref: dev | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -42,7 +42,7 @@ jobs: | |||||||
|       - name: Deploy to Netlify |       - name: Deploy to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         run: | | ||||||
|           npx -y netlify-cli deploy --dir=cast/dist --alias dev |           npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --alias dev | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }} |           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }} | ||||||
| @@ -61,7 +61,7 @@ jobs: | |||||||
|           ref: master |           ref: master | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -77,7 +77,7 @@ jobs: | |||||||
|       - name: Deploy to Netlify |       - name: Deploy to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         run: | | ||||||
|           npx -y netlify-cli deploy --dir=cast/dist --prod |           npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --prod | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }} |           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }} | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -26,7 +26,7 @@ jobs: | |||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 |         uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | ||||||
|         with: |         with: | ||||||
|           path: | |           path: | | ||||||
|             node_modules/.cache/prettier |             node_modules/.cache/prettier | ||||||
| @@ -60,7 +60,7 @@ jobs: | |||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -78,7 +78,7 @@ jobs: | |||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 |         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||||
|         with: |         with: | ||||||
|           name: frontend-bundle-stats |           name: frontend-bundle-stats | ||||||
|           path: build/stats/*.json |           path: build/stats/*.json | ||||||
| @@ -102,7 +102,7 @@ jobs: | |||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 |         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||||
|         with: |         with: | ||||||
|           name: supervisor-bundle-stats |           name: supervisor-bundle-stats | ||||||
|           path: build/stats/*.json |           path: build/stats/*.json | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 |         uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 | ||||||
|         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@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 |         uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 | ||||||
|  |  | ||||||
|       # ℹ️ 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@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 |         uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -27,7 +27,7 @@ jobs: | |||||||
|           ref: dev |           ref: dev | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -43,7 +43,7 @@ jobs: | |||||||
|       - name: Deploy to Netlify |       - name: Deploy to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         run: | | ||||||
|           npx -y netlify-cli deploy --dir=demo/dist --prod |           npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }} |           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }} | ||||||
| @@ -62,7 +62,7 @@ jobs: | |||||||
|           ref: master |           ref: master | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -78,7 +78,7 @@ jobs: | |||||||
|       - name: Deploy to Netlify |       - name: Deploy to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         run: | | ||||||
|           npx -y netlify-cli deploy --dir=demo/dist --prod |           npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }} |           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }} | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -19,7 +19,7 @@ jobs: | |||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -35,7 +35,7 @@ jobs: | |||||||
|       - name: Deploy to Netlify |       - name: Deploy to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         run: | | ||||||
|           npx -y netlify-cli deploy --dir=gallery/dist --prod |           npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --prod | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }} |           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }} | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -24,7 +24,7 @@ jobs: | |||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -40,7 +40,7 @@ jobs: | |||||||
|       - name: Deploy preview to Netlify |       - name: Deploy preview to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         run: | | ||||||
|           npx -y netlify-cli deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \ |           npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \ | ||||||
|             --json > deploy_output.json |             --json > deploy_output.json | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -28,7 +28,7 @@ jobs: | |||||||
|           python-version: ${{ env.PYTHON_VERSION }} |           python-version: ${{ env.PYTHON_VERSION }} | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 |         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 |         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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@1707825cbfcc7452b2913d273414705415ae64d4 # v3.0.1 |         uses: relative-ci/agent-action@8504826a02078b05756e4c07e380023cc2c4274a # v3.1.0 | ||||||
|         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 }} | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -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@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 |         uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 | ||||||
|         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.07.0 |         uses: home-assistant/wheels@2025.10.0 | ||||||
|         with: |         with: | ||||||
|           abi: cp313 |           abi: cp313 | ||||||
|           tag: musllinux_1_2 |           tag: musllinux_1_2 | ||||||
| @@ -93,7 +93,7 @@ jobs: | |||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 |         uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 | ||||||
|         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 | ||||||
|  |  | ||||||
| @@ -122,7 +122,7 @@ jobs: | |||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.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@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 |         uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 | ||||||
|         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@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 |         uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 | ||||||
|         with: |         with: | ||||||
|           repo-token: ${{ secrets.GITHUB_TOKEN }} |           repo-token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|           days-before-stale: 90 |           days-before-stale: 90 | ||||||
|   | |||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -6,4 +6,4 @@ enableGlobalCache: false | |||||||
|  |  | ||||||
| nodeLinker: node-modules | nodeLinker: node-modules | ||||||
|  |  | ||||||
| yarnPath: .yarn/releases/yarn-4.10.2.cjs | yarnPath: .yarn/releases/yarn-4.10.3.cjs | ||||||
|   | |||||||
| @@ -183,7 +183,6 @@ module.exports.babelOptions = ({ | |||||||
|       include: /\/node_modules\//, |       include: /\/node_modules\//, | ||||||
|       exclude: [ |       exclude: [ | ||||||
|         "element-internals-polyfill", |         "element-internals-polyfill", | ||||||
|         "@shoelace-style", |  | ||||||
|         "@?lit(?:-labs|-element|-html)?", |         "@?lit(?:-labs|-element|-html)?", | ||||||
|       ].map((p) => new RegExp(`/node_modules/${p}/`)), |       ].map((p) => new RegExp(`/node_modules/${p}/`)), | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -242,7 +242,7 @@ class HcCast extends LitElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     .question:before { |     .question:before { | ||||||
|       border-radius: 4px; |       border-radius: var(--ha-border-radius-sm); | ||||||
|       position: absolute; |       position: absolute; | ||||||
|       top: 0; |       top: 0; | ||||||
|       right: 0; |       right: 0; | ||||||
|   | |||||||
| @@ -95,7 +95,8 @@ class HcLayout extends LitElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     .hero { |     .hero { | ||||||
|       border-radius: 4px 4px 0 0; |       border-radius: var(--ha-border-radius-sm) var(--ha-border-radius-sm) | ||||||
|  |         var(--ha-border-radius-square) var(--ha-border-radius-square); | ||||||
|     } |     } | ||||||
|     .subtitle { |     .subtitle { | ||||||
|       font-size: var(--ha-font-size-m); |       font-size: var(--ha-font-size-m); | ||||||
|   | |||||||
| @@ -5,17 +5,17 @@ const castContext = framework.CastReceiverContext.getInstance(); | |||||||
| const playerManager = castContext.getPlayerManager(); | const playerManager = castContext.getPlayerManager(); | ||||||
|  |  | ||||||
| playerManager.setMessageInterceptor( | playerManager.setMessageInterceptor( | ||||||
|   "LOAD" as framework.messages.MessageType.LOAD, |   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 = "LIVE" as framework.messages.StreamType.LIVE; |       media.streamType = framework.messages.StreamType.LIVE; | ||||||
|       media.contentType = "application/vnd.apple.mpegurl"; |       media.contentType = "application/vnd.apple.mpegurl"; | ||||||
|       // @ts-ignore |       // @ts-ignore | ||||||
|       media.hlsVideoSegmentFormat = |       media.hlsVideoSegmentFormat = | ||||||
|         "fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4; |         framework.messages.HlsVideoSegmentFormat.FMP4; | ||||||
|     } |     } | ||||||
|     return loadRequestData; |     return loadRequestData; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -75,7 +75,7 @@ export const castDemoEntities: () => Entity[] = () => | |||||||
|         longitude: 4.8903147, |         longitude: 4.8903147, | ||||||
|         radius: 100, |         radius: 100, | ||||||
|         friendly_name: "Home", |         friendly_name: "Home", | ||||||
|         icon: "hass:home", |         icon: "mdi:home", | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     "input_number.harmonyvolume": { |     "input_number.harmonyvolume": { | ||||||
| @@ -88,7 +88,7 @@ export const castDemoEntities: () => Entity[] = () => | |||||||
|         step: 1, |         step: 1, | ||||||
|         mode: "slider", |         mode: "slider", | ||||||
|         friendly_name: "Volume", |         friendly_name: "Volume", | ||||||
|         icon: "hass:volume-high", |         icon: "mdi:volume-high", | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     "climate.upstairs": { |     "climate.upstairs": { | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => { | |||||||
|                 type: "weblink", |                 type: "weblink", | ||||||
|                 url: "/lovelace/climate", |                 url: "/lovelace/climate", | ||||||
|                 name: "Climate controls", |                 name: "Climate controls", | ||||||
|                 icon: "hass:arrow-right", |                 icon: "mdi:arrow-right", | ||||||
|               }, |               }, | ||||||
|             ], |             ], | ||||||
|           }, |           }, | ||||||
| @@ -76,7 +76,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => { | |||||||
|                 type: "weblink", |                 type: "weblink", | ||||||
|                 url: "/lovelace/overview", |                 url: "/lovelace/overview", | ||||||
|                 name: "Back", |                 name: "Back", | ||||||
|                 icon: "hass:arrow-left", |                 icon: "mdi:arrow-left", | ||||||
|               }, |               }, | ||||||
|             ], |             ], | ||||||
|           }, |           }, | ||||||
|   | |||||||
| @@ -40,8 +40,7 @@ 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 = |   loadRequestData.media.streamType = framework.messages.StreamType.NONE; | ||||||
|     "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; | ||||||
| @@ -90,7 +89,7 @@ const showMediaPlayer = () => { | |||||||
| const options = new framework.CastReceiverOptions(); | const options = new framework.CastReceiverOptions(); | ||||||
| options.disableIdleTimeout = true; | options.disableIdleTimeout = true; | ||||||
| options.customNamespaces = { | options.customNamespaces = { | ||||||
|   [CAST_NS]: "json" as framework.system.MessageType.JSON, |   [CAST_NS]: framework.system.MessageType.JSON, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| castContext.addCustomMessageListener( | castContext.addCustomMessageListener( | ||||||
| @@ -98,7 +97,9 @@ castContext.addCustomMessageListener( | |||||||
|   // @ts-ignore |   // @ts-ignore | ||||||
|   (ev: ReceivedMessage<HassMessage>) => { |   (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 (playerManager.getPlayerState() !== "IDLE") { |     if ( | ||||||
|  |       playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE | ||||||
|  |     ) { | ||||||
|       playerManager.stop(); |       playerManager.stop(); | ||||||
|     } else { |     } else { | ||||||
|       showLovelaceController(); |       showLovelaceController(); | ||||||
| @@ -112,7 +113,7 @@ castContext.addCustomMessageListener( | |||||||
| const playerManager = castContext.getPlayerManager(); | const playerManager = castContext.getPlayerManager(); | ||||||
|  |  | ||||||
| playerManager.setMessageInterceptor( | playerManager.setMessageInterceptor( | ||||||
|   "LOAD" as framework.messages.MessageType.LOAD, |   framework.messages.MessageType.LOAD, | ||||||
|   (loadRequestData) => { |   (loadRequestData) => { | ||||||
|     if ( |     if ( | ||||||
|       loadRequestData.media.contentId === |       loadRequestData.media.contentId === | ||||||
| @@ -126,23 +127,24 @@ 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 = "LIVE" as framework.messages.StreamType.LIVE; |       media.streamType = framework.messages.StreamType.LIVE; | ||||||
|       media.contentType = "application/vnd.apple.mpegurl"; |       media.contentType = "application/vnd.apple.mpegurl"; | ||||||
|       // @ts-ignore |       // @ts-ignore | ||||||
|       media.hlsVideoSegmentFormat = |       media.hlsVideoSegmentFormat = | ||||||
|         "fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4; |         framework.messages.HlsVideoSegmentFormat.FMP4; | ||||||
|     } |     } | ||||||
|     return loadRequestData; |     return loadRequestData; | ||||||
|   } |   } | ||||||
| ); | ); | ||||||
|  |  | ||||||
| playerManager.addEventListener( | playerManager.addEventListener( | ||||||
|   "MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS, |   framework.events.EventType.MEDIA_STATUS, | ||||||
|   (event) => { |   (event) => { | ||||||
|     if ( |     if ( | ||||||
|       event.mediaStatus?.playerState === "IDLE" && |       event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE && | ||||||
|       event.mediaStatus?.idleReason && |       event.mediaStatus?.idleReason && | ||||||
|       event.mediaStatus?.idleReason !== "INTERRUPTED" |       event.mediaStatus?.idleReason !== | ||||||
|  |         framework.messages.IdleReason.INTERRUPTED | ||||||
|     ) { |     ) { | ||||||
|       // media finished or stopped, return to default Lovelace |       // media finished or stopped, return to default Lovelace | ||||||
|       showLovelaceController(); |       showLovelaceController(); | ||||||
|   | |||||||
| @@ -143,7 +143,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) => | |||||||
|       state: "on", |       state: "on", | ||||||
|       attributes: { |       attributes: { | ||||||
|         friendly_name: "Home Automation", |         friendly_name: "Home Automation", | ||||||
|         icon: "hass:home-automation", |         icon: "mdi:home-automation", | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     "input_boolean.tvtime": { |     "input_boolean.tvtime": { | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({ | |||||||
|   title: "Home Assistant", |   title: "Home Assistant", | ||||||
|   views: [ |   views: [ | ||||||
|     { |     { | ||||||
|       icon: "hass:home-assistant", |       icon: "mdi:home-assistant", | ||||||
|       id: "home", |       id: "home", | ||||||
|       title: "Home", |       title: "Home", | ||||||
|       cards: [ |       cards: [ | ||||||
|   | |||||||
| @@ -1236,7 +1236,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({ | |||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
|       path: "security", |       path: "security", | ||||||
|       icon: "hass:shield-home", |       icon: "mdi:shield-home", | ||||||
|       name: "Security", |       name: "Security", | ||||||
|       background: |       background: | ||||||
|         'center / cover no-repeat url("/assets/jimpower/background-15.jpg") fixed', |         'center / cover no-repeat url("/assets/jimpower/background-15.jpg") fixed', | ||||||
|   | |||||||
| @@ -208,7 +208,7 @@ class HaGallery extends LitElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       .sidebar a[active]::before { |       .sidebar a[active]::before { | ||||||
|         border-radius: 12px; |         border-radius: var(--ha-border-radius-lg); | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         top: 0; |         top: 0; | ||||||
|         right: 2px; |         right: 2px; | ||||||
| @@ -241,7 +241,7 @@ class HaGallery extends LitElement { | |||||||
|         text-align: center; |         text-align: center; | ||||||
|         margin: 16px; |         margin: 16px; | ||||||
|         padding: 16px; |         padding: 16px; | ||||||
|         border-radius: 12px; |         border-radius: var(--ha-border-radius-lg); | ||||||
|         background-color: var(--primary-background-color); |         background-color: var(--primary-background-color); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -117,7 +117,7 @@ export class DemoHaBadge extends LitElement { | |||||||
|     } |     } | ||||||
|     .card-content { |     .card-content { | ||||||
|       display: flex; |       display: flex; | ||||||
|       gap: 24px; |       gap: var(--ha-space-6); | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -155,11 +155,11 @@ export class DemoHaButton extends LitElement { | |||||||
|     .card-content { |     .card-content { | ||||||
|       display: flex; |       display: flex; | ||||||
|       flex-direction: column; |       flex-direction: column; | ||||||
|       gap: 24px; |       gap: var(--ha-space-6); | ||||||
|     } |     } | ||||||
|     .card-content div { |     .card-content div { | ||||||
|       display: flex; |       display: flex; | ||||||
|       gap: 8px; |       gap: var(--ha-space-2); | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,10 +9,10 @@ import { css, html, LitElement } from "lit"; | |||||||
| import { customElement } from "lit/decorators"; | import { customElement } from "lit/decorators"; | ||||||
| import { ifDefined } from "lit/directives/if-defined"; | import { ifDefined } from "lit/directives/if-defined"; | ||||||
| import { repeat } from "lit/directives/repeat"; | import { repeat } from "lit/directives/repeat"; | ||||||
| import "../../../../src/components/ha-control-button"; |  | ||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
| import "../../../../src/components/ha-svg-icon"; | import "../../../../src/components/ha-control-button"; | ||||||
| import "../../../../src/components/ha-control-button-group"; | import "../../../../src/components/ha-control-button-group"; | ||||||
|  | import "../../../../src/components/ha-svg-icon"; | ||||||
|  |  | ||||||
| interface Button { | interface Button { | ||||||
|   label: string; |   label: string; | ||||||
| @@ -156,17 +156,17 @@ export class DemoHaBarButton extends LitElement { | |||||||
|       --control-button-icon-color: var(--primary-color); |       --control-button-icon-color: var(--primary-color); | ||||||
|       --control-button-background-color: var(--primary-color); |       --control-button-background-color: var(--primary-color); | ||||||
|       --control-button-background-opacity: 0.2; |       --control-button-background-opacity: 0.2; | ||||||
|       --control-button-border-radius: 18px; |       --control-button-border-radius: var(--ha-border-radius-xl); | ||||||
|       height: 100px; |       height: 100px; | ||||||
|       width: 100px; |       width: 100px; | ||||||
|     } |     } | ||||||
|     .custom-group { |     .custom-group { | ||||||
|       --control-button-group-thickness: 100px; |       --control-button-group-thickness: 100px; | ||||||
|       --control-button-group-border-radius: 36px; |       --control-button-group-border-radius: var(--ha-border-radius-6xl); | ||||||
|       --control-button-group-spacing: 20px; |       --control-button-group-spacing: 20px; | ||||||
|     } |     } | ||||||
|     .custom-group ha-control-button { |     .custom-group ha-control-button { | ||||||
|       --control-button-border-radius: 18px; |       --control-button-border-radius: var(--ha-border-radius-xl); | ||||||
|       --mdc-icon-size: 32px; |       --mdc-icon-size: 32px; | ||||||
|     } |     } | ||||||
|     .vertical-buttons { |     .vertical-buttons { | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| import type { TemplateResult } from "lit"; | import type { TemplateResult } from "lit"; | ||||||
| import { LitElement, css, html } from "lit"; | import { LitElement, css, html } from "lit"; | ||||||
| import { customElement, state } from "lit/decorators"; | import { customElement, state } from "lit/decorators"; | ||||||
|  | import { ifDefined } from "lit/directives/if-defined"; | ||||||
|  | import { repeat } from "lit/directives/repeat"; | ||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
| import "../../../../src/components/ha-control-number-buttons"; | import "../../../../src/components/ha-control-number-buttons"; | ||||||
| import { repeat } from "lit/directives/repeat"; |  | ||||||
| import { ifDefined } from "lit/directives/if-defined"; |  | ||||||
|  |  | ||||||
| const buttons: { | const buttons: { | ||||||
|   id: string; |   id: string; | ||||||
| @@ -94,7 +94,7 @@ export class DemoHarControlNumberButtons extends LitElement { | |||||||
|       --control-number-buttons-background-color: #2196f3; |       --control-number-buttons-background-color: #2196f3; | ||||||
|       --control-number-buttons-background-opacity: 0.1; |       --control-number-buttons-background-opacity: 0.1; | ||||||
|       --control-number-buttons-thickness: 100px; |       --control-number-buttons-thickness: 100px; | ||||||
|       --control-number-buttons-border-radius: 36px; |       --control-number-buttons-border-radius: var(--ha-border-radius-6xl); | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -131,7 +131,7 @@ export class DemoHaControlSelectMenu extends LitElement { | |||||||
|       --control-button-icon-color: var(--primary-color); |       --control-button-icon-color: var(--primary-color); | ||||||
|       --control-button-background-color: var(--primary-color); |       --control-button-background-color: var(--primary-color); | ||||||
|       --control-button-background-opacity: 0.2; |       --control-button-background-opacity: 0.2; | ||||||
|       --control-button-border-radius: 18px; |       --control-button-border-radius: var(--ha-border-radius-xl); | ||||||
|       height: 100px; |       height: 100px; | ||||||
|       width: 100px; |       width: 100px; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -187,7 +187,7 @@ export class DemoHaControlSelect extends LitElement { | |||||||
|       --mdc-icon-size: 24px; |       --mdc-icon-size: 24px; | ||||||
|       --control-select-color: var(--state-fan-active-color); |       --control-select-color: var(--state-fan-active-color); | ||||||
|       --control-select-thickness: 130px; |       --control-select-thickness: 130px; | ||||||
|       --control-select-border-radius: 36px; |       --control-select-border-radius: var(--ha-border-radius-6xl); | ||||||
|     } |     } | ||||||
|     .vertical-selects { |     .vertical-selects { | ||||||
|       height: 300px; |       height: 300px; | ||||||
|   | |||||||
| @@ -3,8 +3,8 @@ import { css, html, LitElement } from "lit"; | |||||||
| import { customElement, state } from "lit/decorators"; | import { customElement, state } from "lit/decorators"; | ||||||
| import { ifDefined } from "lit/directives/if-defined"; | import { ifDefined } from "lit/directives/if-defined"; | ||||||
| import { repeat } from "lit/directives/repeat"; | import { repeat } from "lit/directives/repeat"; | ||||||
| import "../../../../src/components/ha-control-slider"; |  | ||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
|  | import "../../../../src/components/ha-control-slider"; | ||||||
|  |  | ||||||
| const sliders: { | const sliders: { | ||||||
|   id: string; |   id: string; | ||||||
| @@ -151,7 +151,7 @@ export class DemoHaBarSlider extends LitElement { | |||||||
|       --control-slider-background: #ffcf4c; |       --control-slider-background: #ffcf4c; | ||||||
|       --control-slider-background-opacity: 0.2; |       --control-slider-background-opacity: 0.2; | ||||||
|       --control-slider-thickness: 130px; |       --control-slider-thickness: 130px; | ||||||
|       --control-slider-border-radius: 36px; |       --control-slider-border-radius: var(--ha-border-radius-6xl); | ||||||
|     } |     } | ||||||
|     .vertical-sliders { |     .vertical-sliders { | ||||||
|       height: 300px; |       height: 300px; | ||||||
|   | |||||||
| @@ -9,8 +9,8 @@ import { css, html, LitElement } from "lit"; | |||||||
| import { customElement, state } from "lit/decorators"; | import { customElement, state } from "lit/decorators"; | ||||||
| import { ifDefined } from "lit/directives/if-defined"; | import { ifDefined } from "lit/directives/if-defined"; | ||||||
| import { repeat } from "lit/directives/repeat"; | import { repeat } from "lit/directives/repeat"; | ||||||
| import "../../../../src/components/ha-control-switch"; |  | ||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
|  | import "../../../../src/components/ha-control-switch"; | ||||||
|  |  | ||||||
| const switches: { | const switches: { | ||||||
|   id: string; |   id: string; | ||||||
| @@ -118,7 +118,7 @@ export class DemoHaControlSwitch extends LitElement { | |||||||
|       --control-switch-on-color: var(--green-color); |       --control-switch-on-color: var(--green-color); | ||||||
|       --control-switch-off-color: var(--red-color); |       --control-switch-off-color: var(--red-color); | ||||||
|       --control-switch-thickness: 130px; |       --control-switch-thickness: 130px; | ||||||
|       --control-switch-border-radius: 36px; |       --control-switch-border-radius: var(--ha-border-radius-6xl); | ||||||
|       --control-switch-padding: 6px; |       --control-switch-padding: 6px; | ||||||
|       --mdc-icon-size: 24px; |       --mdc-icon-size: 24px; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -123,11 +123,11 @@ export class DemoHaProgressButton extends LitElement { | |||||||
|     .card-content { |     .card-content { | ||||||
|       display: flex; |       display: flex; | ||||||
|       flex-direction: column; |       flex-direction: column; | ||||||
|       gap: 24px; |       gap: var(--ha-space-6); | ||||||
|     } |     } | ||||||
|     .card-content div { |     .card-content div { | ||||||
|       display: flex; |       display: flex; | ||||||
|       gap: 8px; |       gap: var(--ha-space-2); | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -131,7 +131,7 @@ export class DemoHaSelectBox extends LitElement { | |||||||
|       --mdc-icon-size: 24px; |       --mdc-icon-size: 24px; | ||||||
|       --control-select-color: var(--state-fan-active-color); |       --control-select-color: var(--state-fan-active-color); | ||||||
|       --control-select-thickness: 130px; |       --control-select-thickness: 130px; | ||||||
|       --control-select-border-radius: 36px; |       --control-select-border-radius: var(--ha-border-radius-6xl); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     p.title { |     p.title { | ||||||
|   | |||||||
| @@ -34,3 +34,5 @@ Check the [webawesome documentation](https://webawesome.com/docs/components/slid | |||||||
| **CSS Custom Properties** | **CSS Custom Properties** | ||||||
|  |  | ||||||
| - `--ha-slider-track-size` - Height of the slider track. Defaults to `4px`. | - `--ha-slider-track-size` - Height of the slider track. Defaults to `4px`. | ||||||
|  | - `--ha-slider-thumb-color` - Color of the slider thumb. Defaults to `var(--primary-color)`. | ||||||
|  | - `--ha-slider-indicator-color` - Color of the filled portion of the slider track. Defaults to `var(--primary-color)`. | ||||||
|   | |||||||
| @@ -79,7 +79,7 @@ export class DemoHaSlider extends LitElement { | |||||||
|       background-color: var(--primary-background-color); |       background-color: var(--primary-background-color); | ||||||
|       padding: 0 50px; |       padding: 0 50px; | ||||||
|       margin: 16px; |       margin: 16px; | ||||||
|       border-radius: 8px; |       border-radius: var(--ha-border-radius-md); | ||||||
|     } |     } | ||||||
|     ha-card { |     ha-card { | ||||||
|       margin: 24px auto; |       margin: 24px auto; | ||||||
| @@ -88,7 +88,7 @@ export class DemoHaSlider extends LitElement { | |||||||
|       display: flex; |       display: flex; | ||||||
|       flex-direction: column; |       flex-direction: column; | ||||||
|       align-items: center; |       align-items: center; | ||||||
|       gap: 24px; |       gap: var(--ha-space-6); | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -61,7 +61,7 @@ export class DemoHaSpinner extends LitElement { | |||||||
|       background-color: var(--primary-background-color); |       background-color: var(--primary-background-color); | ||||||
|       padding: 0 50px; |       padding: 0 50px; | ||||||
|       margin: 16px; |       margin: 16px; | ||||||
|       border-radius: 8px; |       border-radius: var(--ha-border-radius-md); | ||||||
|     } |     } | ||||||
|     ha-card { |     ha-card { | ||||||
|       margin: 24px auto; |       margin: 24px auto; | ||||||
| @@ -70,7 +70,7 @@ export class DemoHaSpinner extends LitElement { | |||||||
|       display: flex; |       display: flex; | ||||||
|       flex-direction: column; |       flex-direction: column; | ||||||
|       align-items: center; |       align-items: center; | ||||||
|       gap: 24px; |       gap: var(--ha-space-6); | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								gallery/src/pages/components/ha-wa-dialog.markdown
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								gallery/src/pages/components/ha-wa-dialog.markdown
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | --- | ||||||
|  | title: Dialog (ha-wa-dialog) | ||||||
|  | --- | ||||||
							
								
								
									
										523
									
								
								gallery/src/pages/components/ha-wa-dialog.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										523
									
								
								gallery/src/pages/components/ha-wa-dialog.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,523 @@ | |||||||
|  | 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-form/ha-form"; | ||||||
|  | import "../../../../src/components/ha-icon-button"; | ||||||
|  | import "../../../../src/components/ha-wa-dialog"; | ||||||
|  | import type { HaFormSchema } from "../../../../src/components/ha-form/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" | ||||||
|  |   | "actions"; | ||||||
|  |  | ||||||
|  | @customElement("demo-components-ha-wa-dialog") | ||||||
|  | export class DemoHaWaDialog extends LitElement { | ||||||
|  |   @state() private _openDialog: DialogType = false; | ||||||
|  |  | ||||||
|  |   protected render() { | ||||||
|  |     return html` | ||||||
|  |       <div class="content"> | ||||||
|  |         <h1>Dialog <code><ha-wa-dialog></code></h1> | ||||||
|  |  | ||||||
|  |         <p class="subtitle">Dialog component built with WebAwesome.</p> | ||||||
|  |  | ||||||
|  |         <h2>Demos</h2> | ||||||
|  |  | ||||||
|  |         <div class="buttons"> | ||||||
|  |           <ha-button @click=${this._handleOpenDialog("basic")} | ||||||
|  |             >Basic dialog</ha-button | ||||||
|  |           > | ||||||
|  |           <ha-button @click=${this._handleOpenDialog("basic-subtitle-below")} | ||||||
|  |             >Basic dialog with subtitle below</ha-button | ||||||
|  |           > | ||||||
|  |           <ha-button @click=${this._handleOpenDialog("basic-subtitle-above")} | ||||||
|  |             >Basic dialog with subtitle above</ha-button | ||||||
|  |           > | ||||||
|  |           <ha-button @click=${this._handleOpenDialog("form")} | ||||||
|  |             >Dialog with form</ha-button | ||||||
|  |           > | ||||||
|  |           <ha-button @click=${this._handleOpenDialog("actions")} | ||||||
|  |             >Dialog with actions</ha-button | ||||||
|  |           > | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <ha-wa-dialog | ||||||
|  |           .open=${this._openDialog === "basic"} | ||||||
|  |           header-title="Basic dialog" | ||||||
|  |           @closed=${this._handleClosed} | ||||||
|  |         > | ||||||
|  |           <div>Dialog content</div> | ||||||
|  |         </ha-wa-dialog> | ||||||
|  |  | ||||||
|  |         <ha-wa-dialog | ||||||
|  |           .open=${this._openDialog === "basic-subtitle-below"} | ||||||
|  |           header-title="Basic dialog with subtitle" | ||||||
|  |           header-subtitle="This is a basic dialog with a subtitle below" | ||||||
|  |           @closed=${this._handleClosed} | ||||||
|  |         > | ||||||
|  |           <div>Dialog content</div> | ||||||
|  |         </ha-wa-dialog> | ||||||
|  |  | ||||||
|  |         <ha-wa-dialog | ||||||
|  |           .open=${this._openDialog === "basic-subtitle-above"} | ||||||
|  |           header-title="Dialog with subtitle above" | ||||||
|  |           header-subtitle="This is a basic dialog with a subtitle above" | ||||||
|  |           header-subtitle-position="above" | ||||||
|  |           @closed=${this._handleClosed} | ||||||
|  |         > | ||||||
|  |           <div>Dialog content</div> | ||||||
|  |         </ha-wa-dialog> | ||||||
|  |  | ||||||
|  |         <ha-wa-dialog | ||||||
|  |           .open=${this._openDialog === "form"} | ||||||
|  |           header-title="Dialog with form" | ||||||
|  |           header-subtitle="This is a dialog with a form and a footer" | ||||||
|  |           prevent-scrim-close | ||||||
|  |           @closed=${this._handleClosed} | ||||||
|  |         > | ||||||
|  |           <ha-form autofocus .schema=${SCHEMA}></ha-form> | ||||||
|  |           <ha-dialog-footer slot="footer"> | ||||||
|  |             <ha-button | ||||||
|  |               data-dialog="close" | ||||||
|  |               slot="secondaryAction" | ||||||
|  |               variant="plain" | ||||||
|  |               >Cancel</ha-button | ||||||
|  |             > | ||||||
|  |             <ha-button data-dialog="close" slot="primaryAction" variant="accent" | ||||||
|  |               >Submit</ha-button | ||||||
|  |             > | ||||||
|  |           </ha-dialog-footer> | ||||||
|  |         </ha-wa-dialog> | ||||||
|  |  | ||||||
|  |         <ha-wa-dialog | ||||||
|  |           .open=${this._openDialog === "actions"} | ||||||
|  |           header-title="Dialog with actions" | ||||||
|  |           header-subtitle="This is a 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>Dialog content</div> | ||||||
|  |         </ha-wa-dialog> | ||||||
|  |  | ||||||
|  |         <h2>Design</h2> | ||||||
|  |  | ||||||
|  |         <h3>Width</h3> | ||||||
|  |  | ||||||
|  |         <p>There are multiple widths available for the dialog.</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(720px, var(--full-width))</code></td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <td><code>full</code></td> | ||||||
|  |               <td><code>var(--full-width)</code></td> | ||||||
|  |             </tr> | ||||||
|  |           </tbody> | ||||||
|  |         </table> | ||||||
|  |  | ||||||
|  |         <p> | ||||||
|  |           <code>--full-width</code> is calculated based on the available width | ||||||
|  |           of the screen. 95vw is the maximum width of the dialog on a large | ||||||
|  |           screen, while on a small screen it is 100vw minus the safe area | ||||||
|  |           insets. | ||||||
|  |         </p> | ||||||
|  |  | ||||||
|  |         <p>Dialogs have a default width of <code>medium</code>.</p> | ||||||
|  |  | ||||||
|  |         <h3>Prevent scrim close</h3> | ||||||
|  |  | ||||||
|  |         <p> | ||||||
|  |           You can prevent the dialog from being closed by clicking the | ||||||
|  |           scrim/overlay. This is allowed by default. | ||||||
|  |         </p> | ||||||
|  |  | ||||||
|  |         <h3>Header</h3> | ||||||
|  |  | ||||||
|  |         <p>The header contains a title, a subtitle and action items.</p> | ||||||
|  |  | ||||||
|  |         <table> | ||||||
|  |           <thead> | ||||||
|  |             <tr> | ||||||
|  |               <th>Slot</th> | ||||||
|  |               <th>Description</th> | ||||||
|  |             </tr> | ||||||
|  |           </thead> | ||||||
|  |           <tbody> | ||||||
|  |             <tr> | ||||||
|  |               <td><code>header</code></td> | ||||||
|  |               <td>The entire header area.</td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <td><code>headerTitle</code></td> | ||||||
|  |               <td>The header title text.</td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <td><code>headerSubtitle</code></td> | ||||||
|  |               <td>The header subtitle text.</td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <td><code>headerActionItems</code></td> | ||||||
|  |               <td>The header action items.</td> | ||||||
|  |             </tr> | ||||||
|  |           </tbody> | ||||||
|  |         </table> | ||||||
|  |  | ||||||
|  |         <h4>Header title</h4> | ||||||
|  |  | ||||||
|  |         <p>The header title is a text string.</p> | ||||||
|  |  | ||||||
|  |         <h4>Header subtitle</h4> | ||||||
|  |  | ||||||
|  |         <p>The header subtitle is a text string.</p> | ||||||
|  |  | ||||||
|  |         <h4>Header action items</h4> | ||||||
|  |  | ||||||
|  |         <p> | ||||||
|  |           The header action items usually containing icon buttons and/or menu | ||||||
|  |           buttons. | ||||||
|  |         </p> | ||||||
|  |  | ||||||
|  |         <h3>Body</h3> | ||||||
|  |  | ||||||
|  |         <p>The body is the content of the dialog.</p> | ||||||
|  |  | ||||||
|  |         <h3>Footer</h3> | ||||||
|  |  | ||||||
|  |         <p>The footer is the footer of the 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 so: | ||||||
|  |         </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>Example Usage</h3> | ||||||
|  |  | ||||||
|  |         <pre><code><ha-wa-dialog | ||||||
|  |   open | ||||||
|  |   header-title="Dialog title" | ||||||
|  |   header-subtitle="Dialog subtitle" | ||||||
|  |   prevent-scrim-close | ||||||
|  | > | ||||||
|  |   <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 data-dialog="close" slot="secondaryAction" variant="plain" | ||||||
|  |       >Cancel</ha-button | ||||||
|  |     > | ||||||
|  |     <ha-button slot="primaryAction" variant="accent">Submit</ha-button> | ||||||
|  |   </ha-dialog-footer> | ||||||
|  | </ha-wa-dialog></code></pre> | ||||||
|  |  | ||||||
|  |         <h3>API</h3> | ||||||
|  |  | ||||||
|  |         <p> | ||||||
|  |           This component is based on the webawesome dialog component. Check the | ||||||
|  |           <a | ||||||
|  |             href="https://webawesome.com/docs/components/dialog/" | ||||||
|  |             target="_blank" | ||||||
|  |             rel="noopener noreferrer" | ||||||
|  |             >webawesome documentation</a | ||||||
|  |           > | ||||||
|  |           for more details. | ||||||
|  |         </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 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.</td> | ||||||
|  |               <td><code>medium</code></td> | ||||||
|  |               <td> | ||||||
|  |                 <code>small</code>, <code>medium</code>, <code>large</code>, | ||||||
|  |                 <code>full</code> | ||||||
|  |               </td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <td><code>prevent-scrim-close</code></td> | ||||||
|  |               <td> | ||||||
|  |                 Prevents closing the dialog by clicking the scrim/overlay. | ||||||
|  |               </td> | ||||||
|  |               <td><code>false</code></td> | ||||||
|  |               <td><code>true</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>flexcontent</code></td> | ||||||
|  |               <td> | ||||||
|  |                 Makes the dialog body a flex container for flexible layouts. | ||||||
|  |               </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>--dialog-content-padding</code></td> | ||||||
|  |               <td>Padding for dialog content sections.</td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <td><code>--ha-dialog-show-duration</code></td> | ||||||
|  |               <td>Show animation duration.</td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <td><code>--ha-dialog-hide-duration</code></td> | ||||||
|  |               <td>Hide animation duration.</td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <td><code>--ha-dialog-surface-background</code></td> | ||||||
|  |               <td>Dialog background color.</td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <td><code>--ha-dialog-border-radius</code></td> | ||||||
|  |               <td>Border radius of the dialog surface.</td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <td><code>--dialog-z-index</code></td> | ||||||
|  |               <td>Z-index for the dialog.</td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <td><code>--dialog-surface-position</code></td> | ||||||
|  |               <td>CSS position of the dialog surface.</td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <td><code>--dialog-surface-margin-top</code></td> | ||||||
|  |               <td>Top margin for the dialog surface.</td> | ||||||
|  |             </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 dialog is shown.</td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <td><code>closed</code></td> | ||||||
|  |               <td>Fired after the dialog is hidden.</td> | ||||||
|  |             </tr> | ||||||
|  |           </tbody> | ||||||
|  |         </table> | ||||||
|  |       </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; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .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; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       a { | ||||||
|  |         color: var(--primary-color); | ||||||
|  |       } | ||||||
|  |     `, | ||||||
|  |   ]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "demo-components-ha-wa-dialog": DemoHaWaDialog; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -5,13 +5,13 @@ import type { | |||||||
| 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 memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
|  | import { mockIcons } from "../../../../demo/src/stubs/icons"; | ||||||
| import { computeDomain } from "../../../../src/common/entity/compute_domain"; | import { computeDomain } from "../../../../src/common/entity/compute_domain"; | ||||||
| import { computeStateDisplay } from "../../../../src/common/entity/compute_state_display"; | import { computeStateDisplay } from "../../../../src/common/entity/compute_state_display"; | ||||||
| import "../../../../src/components/data-table/ha-data-table"; | import "../../../../src/components/data-table/ha-data-table"; | ||||||
| import type { DataTableColumnContainer } from "../../../../src/components/data-table/ha-data-table"; | import type { DataTableColumnContainer } from "../../../../src/components/data-table/ha-data-table"; | ||||||
| import "../../../../src/components/entity/state-badge"; | import "../../../../src/components/entity/state-badge"; | ||||||
| import { provideHass } from "../../../../src/fake_data/provide_hass"; | import { provideHass } from "../../../../src/fake_data/provide_hass"; | ||||||
| import { mockIcons } from "../../../../demo/src/stubs/icons"; |  | ||||||
| import type { HomeAssistant } from "../../../../src/types"; | import type { HomeAssistant } from "../../../../src/types"; | ||||||
|  |  | ||||||
| const SENSOR_DEVICE_CLASSES = [ | const SENSOR_DEVICE_CLASSES = [ | ||||||
| @@ -434,7 +434,7 @@ export class DemoEntityState extends LitElement { | |||||||
|       display: block; |       display: block; | ||||||
|       height: 20px; |       height: 20px; | ||||||
|       width: 20px; |       width: 20px; | ||||||
|       border-radius: 10px; |       border-radius: var(--ha-border-radius-md); | ||||||
|       background-color: rgb(--color); |       background-color: rgb(--color); | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
|   | |||||||
| @@ -121,7 +121,7 @@ class HassioCardContent extends LitElement { | |||||||
|       height: 12px; |       height: 12px; | ||||||
|       top: 8px; |       top: 8px; | ||||||
|       right: 8px; |       right: 8px; | ||||||
|       border-radius: 50%; |       border-radius: var(--ha-border-radius-circle); | ||||||
|     } |     } | ||||||
|     .topbar { |     .topbar { | ||||||
|       position: absolute; |       position: absolute; | ||||||
|   | |||||||
| @@ -164,7 +164,7 @@ class HassioHardwareDialog extends LitElement { | |||||||
|         pre, |         pre, | ||||||
|         code { |         code { | ||||||
|           background-color: var(--markdown-code-background-color, none); |           background-color: var(--markdown-code-background-color, none); | ||||||
|           border-radius: 3px; |           border-radius: var(--ha-border-radius-sm); | ||||||
|         } |         } | ||||||
|         pre { |         pre { | ||||||
|           padding: 16px; |           padding: 16px; | ||||||
|   | |||||||
| @@ -228,7 +228,7 @@ class HassioRegistriesDialog extends LitElement { | |||||||
|       css` |       css` | ||||||
|         .registry { |         .registry { | ||||||
|           border: 1px solid var(--divider-color); |           border: 1px solid var(--divider-color); | ||||||
|           border-radius: 4px; |           border-radius: var(--ha-border-radius-sm); | ||||||
|           margin-top: 4px; |           margin-top: 4px; | ||||||
|         } |         } | ||||||
|         .action { |         .action { | ||||||
|   | |||||||
| @@ -193,7 +193,7 @@ class HassioRepositoriesDialog extends LitElement { | |||||||
|         } |         } | ||||||
|         .option { |         .option { | ||||||
|           border: 1px solid var(--divider-color); |           border: 1px solid var(--divider-color); | ||||||
|           border-radius: 4px; |           border-radius: var(--ha-border-radius-sm); | ||||||
|           margin-top: 4px; |           margin-top: 4px; | ||||||
|         } |         } | ||||||
|         ha-button { |         ha-button { | ||||||
|   | |||||||
| @@ -159,7 +159,7 @@ class HassioSystemManagedDialog extends LitElement { | |||||||
|           display: flex; |           display: flex; | ||||||
|           justify-content: center; |           justify-content: center; | ||||||
|           align-items: center; |           align-items: center; | ||||||
|           gap: 16px; |           gap: var(--ha-space-4); | ||||||
|           --mdc-icon-size: 48px; |           --mdc-icon-size: 48px; | ||||||
|           margin-bottom: 32px; |           margin-bottom: 32px; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ export const hassioStyle = css` | |||||||
|   .card-group { |   .card-group { | ||||||
|     display: grid; |     display: grid; | ||||||
|     grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); |     grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | ||||||
|     grid-gap: 8px; |     grid-gap: var(--ha-space-2); | ||||||
|   } |   } | ||||||
|   @media screen and (min-width: 640px) { |   @media screen and (min-width: 640px) { | ||||||
|     .card-group { |     .card-group { | ||||||
|   | |||||||
| @@ -302,7 +302,7 @@ class LandingPageLogs extends LitElement { | |||||||
|         max-height: 300px; |         max-height: 300px; | ||||||
|         overflow: auto; |         overflow: auto; | ||||||
|         border: 1px solid var(--divider-color); |         border: 1px solid var(--divider-color); | ||||||
|         border-radius: 4px; |         border-radius: var(--ha-border-radius-sm); | ||||||
|         padding: 4px; |         padding: 4px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -213,7 +213,7 @@ class HaLandingPage extends LandingPageBaseElement { | |||||||
|       ha-card .card-content { |       ha-card .card-content { | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|         gap: 16px; |         gap: var(--ha-space-4); | ||||||
|       } |       } | ||||||
|       ha-alert p { |       ha-alert p { | ||||||
|         text-align: unset; |         text-align: unset; | ||||||
| @@ -221,7 +221,7 @@ class HaLandingPage extends LandingPageBaseElement { | |||||||
|       ha-language-picker { |       ha-language-picker { | ||||||
|         display: block; |         display: block; | ||||||
|         width: 200px; |         width: 200px; | ||||||
|         border-radius: 4px; |         border-radius: var(--ha-border-radius-sm); | ||||||
|         overflow: hidden; |         overflow: hidden; | ||||||
|         --ha-select-height: 40px; |         --ha-select-height: 40px; | ||||||
|         --mdc-select-fill-color: none; |         --mdc-select-fill-color: none; | ||||||
|   | |||||||
							
								
								
									
										94
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										94
									
								
								package.json
									
									
									
									
									
								
							| @@ -28,32 +28,32 @@ | |||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@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.18.7", |     "@codemirror/autocomplete": "6.19.1", | ||||||
|     "@codemirror/commands": "6.8.1", |     "@codemirror/commands": "6.10.0", | ||||||
|     "@codemirror/language": "6.11.3", |     "@codemirror/language": "6.11.3", | ||||||
|     "@codemirror/legacy-modes": "6.5.1", |     "@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.2", |     "@codemirror/view": "6.38.6", | ||||||
|     "@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.0", |     "@formatjs/intl-datetimeformat": "6.18.2", | ||||||
|     "@formatjs/intl-displaynames": "6.8.11", |     "@formatjs/intl-displaynames": "6.8.13", | ||||||
|     "@formatjs/intl-durationformat": "0.7.4", |     "@formatjs/intl-durationformat": "0.7.6", | ||||||
|     "@formatjs/intl-getcanonicallocales": "2.5.5", |     "@formatjs/intl-getcanonicallocales": "2.5.6", | ||||||
|     "@formatjs/intl-listformat": "7.7.11", |     "@formatjs/intl-listformat": "7.7.13", | ||||||
|     "@formatjs/intl-locale": "4.2.11", |     "@formatjs/intl-locale": "4.2.13", | ||||||
|     "@formatjs/intl-numberformat": "8.15.4", |     "@formatjs/intl-numberformat": "8.15.6", | ||||||
|     "@formatjs/intl-pluralrules": "5.4.4", |     "@formatjs/intl-pluralrules": "5.4.6", | ||||||
|     "@formatjs/intl-relativetimeformat": "11.4.11", |     "@formatjs/intl-relativetimeformat": "11.4.13", | ||||||
|     "@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-beta.4.ha.3", |     "@home-assistant/webawesome": "3.0.0-beta.6.ha.6", | ||||||
|     "@lezer/highlight": "1.2.1", |     "@lezer/highlight": "1.2.2", | ||||||
|     "@lit-labs/motion": "1.0.9", |     "@lit-labs/motion": "1.0.9", | ||||||
|     "@lit-labs/observers": "2.0.6", |     "@lit-labs/observers": "2.0.6", | ||||||
|     "@lit-labs/virtualizer": "2.1.1", |     "@lit-labs/virtualizer": "2.1.1", | ||||||
| @@ -89,17 +89,17 @@ | |||||||
|     "@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.0", |     "@vaadin/combo-box": "24.9.2", | ||||||
|     "@vaadin/vaadin-themable-mixin": "24.9.0", |     "@vaadin/vaadin-themable-mixin": "24.9.2", | ||||||
|     "@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", | ||||||
|     "@webcomponents/webcomponentsjs": "2.8.0", |     "@webcomponents/webcomponentsjs": "2.8.0", | ||||||
|     "app-datepicker": "5.1.1", |     "app-datepicker": "5.1.1", | ||||||
|     "barcode-detector": "3.0.5", |     "barcode-detector": "3.0.6", | ||||||
|     "color-name": "2.0.2", |     "color-name": "2.0.2", | ||||||
|     "comlink": "4.4.2", |     "comlink": "4.4.2", | ||||||
|     "core-js": "3.45.1", |     "core-js": "3.46.0", | ||||||
|     "cropperjs": "1.6.2", |     "cropperjs": "1.6.2", | ||||||
|     "culori": "4.0.2", |     "culori": "4.0.2", | ||||||
|     "date-fns": "4.1.0", |     "date-fns": "4.1.0", | ||||||
| @@ -111,10 +111,10 @@ | |||||||
|     "fuse.js": "7.1.0", |     "fuse.js": "7.1.0", | ||||||
|     "google-timezones-json": "1.2.0", |     "google-timezones-json": "1.2.0", | ||||||
|     "gulp-zopfli-green": "6.0.2", |     "gulp-zopfli-green": "6.0.2", | ||||||
|     "hls.js": "1.6.12", |     "hls.js": "1.6.13", | ||||||
|     "home-assistant-js-websocket": "9.5.0", |     "home-assistant-js-websocket": "9.5.0", | ||||||
|     "idb-keyval": "6.2.2", |     "idb-keyval": "6.2.2", | ||||||
|     "intl-messageformat": "10.7.16", |     "intl-messageformat": "10.7.18", | ||||||
|     "js-yaml": "4.1.0", |     "js-yaml": "4.1.0", | ||||||
|     "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", | ||||||
| @@ -122,7 +122,7 @@ | |||||||
|     "lit": "3.3.1", |     "lit": "3.3.1", | ||||||
|     "lit-html": "3.3.1", |     "lit-html": "3.3.1", | ||||||
|     "luxon": "3.7.2", |     "luxon": "3.7.2", | ||||||
|     "marked": "16.3.0", |     "marked": "16.4.1", | ||||||
|     "memoize-one": "6.0.0", |     "memoize-one": "6.0.0", | ||||||
|     "node-vibrant": "4.0.3", |     "node-vibrant": "4.0.3", | ||||||
|     "object-hash": "3.0.0", |     "object-hash": "3.0.0", | ||||||
| @@ -135,7 +135,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.5", |     "ua-parser-js": "2.0.6", | ||||||
|     "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", | ||||||
| @@ -148,50 +148,50 @@ | |||||||
|     "xss": "1.0.15" |     "xss": "1.0.15" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@babel/core": "7.28.4", |     "@babel/core": "7.28.5", | ||||||
|     "@babel/helper-define-polyfill-provider": "0.6.5", |     "@babel/helper-define-polyfill-provider": "0.6.5", | ||||||
|     "@babel/plugin-transform-runtime": "7.28.3", |     "@babel/plugin-transform-runtime": "7.28.5", | ||||||
|     "@babel/preset-env": "7.28.3", |     "@babel/preset-env": "7.28.5", | ||||||
|     "@bundle-stats/plugin-webpack-filter": "4.21.3", |     "@bundle-stats/plugin-webpack-filter": "4.21.5", | ||||||
|     "@lokalise/node-api": "15.2.1", |     "@lokalise/node-api": "15.3.1", | ||||||
|     "@octokit/auth-oauth-device": "8.0.1", |     "@octokit/auth-oauth-device": "8.0.2", | ||||||
|     "@octokit/plugin-retry": "8.0.1", |     "@octokit/plugin-retry": "8.0.2", | ||||||
|     "@octokit/rest": "22.0.0", |     "@octokit/rest": "22.0.0", | ||||||
|     "@rsdoctor/rspack-plugin": "1.2.3", |     "@rsdoctor/rspack-plugin": "1.3.4", | ||||||
|     "@rspack/core": "1.5.5", |     "@rspack/core": "1.5.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.24", |     "@types/chromecast-caf-receiver": "6.0.22", | ||||||
|     "@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", | ||||||
|     "@types/html-minifier-terser": "7.0.2", |     "@types/html-minifier-terser": "7.0.2", | ||||||
|     "@types/js-yaml": "4.0.9", |     "@types/js-yaml": "4.0.9", | ||||||
|     "@types/leaflet": "1.9.20", |     "@types/leaflet": "1.9.21", | ||||||
|     "@types/leaflet-draw": "1.0.13", |     "@types/leaflet-draw": "1.0.13", | ||||||
|     "@types/leaflet.markercluster": "1.5.6", |     "@types/leaflet.markercluster": "1.5.6", | ||||||
|     "@types/lodash.merge": "4.6.9", |     "@types/lodash.merge": "4.6.9", | ||||||
|     "@types/luxon": "3.7.1", |     "@types/luxon": "3.7.1", | ||||||
|     "@types/mocha": "10.0.10", |     "@types/mocha": "10.0.10", | ||||||
|     "@types/qrcode": "1.5.5", |     "@types/qrcode": "1.5.6", | ||||||
|     "@types/sortablejs": "1.15.8", |     "@types/sortablejs": "1.15.9", | ||||||
|     "@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": "3.2.4", |     "@vitest/coverage-v8": "4.0.3", | ||||||
|     "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.36.0", |     "eslint": "9.38.0", | ||||||
|     "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", | ||||||
|     "eslint-plugin-import": "2.32.0", |     "eslint-plugin-import": "2.32.0", | ||||||
|     "eslint-plugin-lit": "2.1.1", |     "eslint-plugin-lit": "2.1.1", | ||||||
|     "eslint-plugin-lit-a11y": "5.1.1", |     "eslint-plugin-lit-a11y": "5.1.1", | ||||||
|     "eslint-plugin-unused-imports": "4.2.0", |     "eslint-plugin-unused-imports": "4.3.0", | ||||||
|     "eslint-plugin-wc": "3.0.1", |     "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": "11.0.3", |     "glob": "11.0.3", | ||||||
| @@ -201,9 +201,9 @@ | |||||||
|     "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.0.0", |     "jsdom": "27.0.1", | ||||||
|     "jszip": "3.10.1", |     "jszip": "3.10.1", | ||||||
|     "lint-staged": "16.1.6", |     "lint-staged": "16.2.6", | ||||||
|     "lit-analyzer": "2.0.3", |     "lit-analyzer": "2.0.3", | ||||||
|     "lodash.merge": "4.6.2", |     "lodash.merge": "4.6.2", | ||||||
|     "lodash.template": "4.5.0", |     "lodash.template": "4.5.0", | ||||||
| @@ -213,13 +213,13 @@ | |||||||
|     "rspack-manifest-plugin": "5.1.0", |     "rspack-manifest-plugin": "5.1.0", | ||||||
|     "serve": "14.2.5", |     "serve": "14.2.5", | ||||||
|     "sinon": "21.0.0", |     "sinon": "21.0.0", | ||||||
|     "tar": "7.4.3", |     "tar": "7.5.1", | ||||||
|     "terser-webpack-plugin": "5.3.14", |     "terser-webpack-plugin": "5.3.14", | ||||||
|     "ts-lit-plugin": "2.0.2", |     "ts-lit-plugin": "2.0.2", | ||||||
|     "typescript": "5.9.2", |     "typescript": "5.9.3", | ||||||
|     "typescript-eslint": "8.44.0", |     "typescript-eslint": "8.46.2", | ||||||
|     "vite-tsconfig-paths": "5.1.4", |     "vite-tsconfig-paths": "5.1.4", | ||||||
|     "vitest": "3.2.4", |     "vitest": "4.0.3", | ||||||
|     "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" | ||||||
| @@ -235,5 +235,5 @@ | |||||||
|     "tslib": "2.8.1", |     "tslib": "2.8.1", | ||||||
|     "@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch" |     "@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch" | ||||||
|   }, |   }, | ||||||
|   "packageManager": "yarn@4.10.2" |   "packageManager": "yarn@4.10.3" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ | |||||||
|     ":semanticCommitsDisabled", |     ":semanticCommitsDisabled", | ||||||
|     "group:monorepos", |     "group:monorepos", | ||||||
|     "group:recommended", |     "group:recommended", | ||||||
|     "npm:unpublishSafe" |     "security:minimumReleaseAgeNpm" | ||||||
|   ], |   ], | ||||||
|   "enabledManagers": ["npm", "nvm"], |   "enabledManagers": ["npm", "nvm"], | ||||||
|   "postUpdateOptions": ["yarnDedupeHighest"], |   "postUpdateOptions": ["yarnDedupeHighest"], | ||||||
|   | |||||||
| @@ -103,7 +103,10 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) { | |||||||
|           ); |           ); | ||||||
|           box-shadow: var(--ha-card-box-shadow, none); |           box-shadow: var(--ha-card-box-shadow, none); | ||||||
|           box-sizing: border-box; |           box-sizing: border-box; | ||||||
|           border-radius: var(--ha-card-border-radius, 12px); |           border-radius: var( | ||||||
|  |             --ha-card-border-radius, | ||||||
|  |             var(--ha-border-radius-lg) | ||||||
|  |           ); | ||||||
|           border-width: var(--ha-card-border-width, 1px); |           border-width: var(--ha-card-border-width, 1px); | ||||||
|           border-style: solid; |           border-style: solid; | ||||||
|           border-color: var( |           border-color: var( | ||||||
| @@ -132,7 +135,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) { | |||||||
|         } |         } | ||||||
|         ha-language-picker { |         ha-language-picker { | ||||||
|           width: 200px; |           width: 200px; | ||||||
|           border-radius: 4px; |           border-radius: var(--ha-border-radius-sm); | ||||||
|           overflow: hidden; |           overflow: hidden; | ||||||
|           --ha-select-height: 40px; |           --ha-select-height: 40px; | ||||||
|           --mdc-select-fill-color: none; |           --mdc-select-fill-color: none; | ||||||
|   | |||||||
| @@ -1,23 +1,40 @@ | |||||||
|  | import { formatHex, parse } from "culori"; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Expands a 3-digit hex color to a 6-digit hex color. | ||||||
|  |  * @param hex - The hex color to expand. | ||||||
|  |  * @returns The expanded hex color. | ||||||
|  |  * @throws If the hex color is invalid. | ||||||
|  |  */ | ||||||
| export const expandHex = (hex: string): string => { | export const expandHex = (hex: string): string => { | ||||||
|   hex = hex.replace("#", ""); |   const color = parse(hex); | ||||||
|   if (hex.length === 6) return hex; |   if (!color) { | ||||||
|   let result = ""; |     throw new Error(`Invalid hex color: ${hex}`); | ||||||
|   for (const val of hex) { |  | ||||||
|     result += val + val; |  | ||||||
|   } |   } | ||||||
|   return result; |   const formattedColor = formatHex(color); | ||||||
|  |   if (!formattedColor) { | ||||||
|  |     throw new Error(`Could not format hex color: ${hex}`); | ||||||
|  |   } | ||||||
|  |   return formattedColor.replace("#", ""); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // Blend 2 hex colors: c1 is placed over c2, blend is c1's opacity. | /** | ||||||
|  |  * Blends two hex colors. c1 is placed over c2, blend is c1's opacity. | ||||||
|  |  * @param c1 - The first hex color. | ||||||
|  |  * @param c2 - The second hex color. | ||||||
|  |  * @param blend - The blend percentage (0-100). | ||||||
|  |  * @returns The blended hex color. | ||||||
|  |  */ | ||||||
| export const hexBlend = (c1: string, c2: string, blend = 50): string => { | export const hexBlend = (c1: string, c2: string, blend = 50): string => { | ||||||
|   let color = ""; |  | ||||||
|   c1 = expandHex(c1); |   c1 = expandHex(c1); | ||||||
|   c2 = expandHex(c2); |   c2 = expandHex(c2); | ||||||
|  |   let color = ""; | ||||||
|   for (let i = 0; i <= 5; i += 2) { |   for (let i = 0; i <= 5; i += 2) { | ||||||
|     const h1 = parseInt(c1.substring(i, i + 2), 16); |     const h1 = parseInt(c1.substring(i, i + 2), 16); | ||||||
|     const h2 = parseInt(c2.substring(i, i + 2), 16); |     const h2 = parseInt(c2.substring(i, i + 2), 16); | ||||||
|     let hex = Math.floor(h2 + (h1 - h2) * (blend / 100)).toString(16); |     const hex = Math.floor(h2 + (h1 - h2) * (blend / 100)) | ||||||
|     while (hex.length < 2) hex = "0" + hex; |       .toString(16) | ||||||
|  |       .padStart(2, "0"); | ||||||
|     color += hex; |     color += hex; | ||||||
|   } |   } | ||||||
|   return `#${color}`; |   return `#${color}`; | ||||||
|   | |||||||
| @@ -1,28 +1,49 @@ | |||||||
| export const luminosity = (rgb: [number, number, number]): number => { | import { wcagLuminance, wcagContrast } from "culori"; | ||||||
|   // http://www.w3.org/TR/WCAG20/#relativeluminancedef |  | ||||||
|   const lum: [number, number, number] = [0, 0, 0]; |  | ||||||
|   for (let i = 0; i < rgb.length; i++) { |  | ||||||
|     const chan = rgb[i] / 255; |  | ||||||
|     lum[i] = chan <= 0.03928 ? chan / 12.92 : ((chan + 0.055) / 1.055) ** 2.4; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2]; | /** | ||||||
| }; |  * Calculates the luminosity of an RGB color. | ||||||
|  |  * @param rgb - The RGB color to calculate the luminosity of. | ||||||
|  |  * @returns The luminosity of the color. | ||||||
|  |  */ | ||||||
|  | export const luminosity = (rgb: [number, number, number]): number => | ||||||
|  |   wcagLuminance({ | ||||||
|  |     mode: "rgb", | ||||||
|  |     r: rgb[0] / 255, | ||||||
|  |     g: rgb[1] / 255, | ||||||
|  |     b: rgb[2] / 255, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Calculates the contrast ratio between two RGB colors. | ||||||
|  |  * @param color1 - The first color to calculate the contrast ratio of. | ||||||
|  |  * @param color2 - The second color to calculate the contrast ratio of. | ||||||
|  |  * @returns The contrast ratio between the two colors. | ||||||
|  |  */ | ||||||
| export const rgbContrast = ( | export const rgbContrast = ( | ||||||
|   color1: [number, number, number], |   color1: [number, number, number], | ||||||
|   color2: [number, number, number] |   color2: [number, number, number] | ||||||
| ) => { | ) => | ||||||
|   const lum1 = luminosity(color1); |   wcagContrast( | ||||||
|   const lum2 = luminosity(color2); |     { | ||||||
|  |       mode: "rgb", | ||||||
|   if (lum1 > lum2) { |       r: color1[0] / 255, | ||||||
|     return (lum1 + 0.05) / (lum2 + 0.05); |       g: color1[1] / 255, | ||||||
|   } |       b: color1[2] / 255, | ||||||
|  |     }, | ||||||
|   return (lum2 + 0.05) / (lum1 + 0.05); |     { | ||||||
| }; |       mode: "rgb", | ||||||
|  |       r: color2[0] / 255, | ||||||
|  |       g: color2[1] / 255, | ||||||
|  |       b: color2[2] / 255, | ||||||
|  |     } | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Calculates the contrast ratio between two RGB colors. | ||||||
|  |  * @param rgb1 - The first color to calculate the contrast ratio of. | ||||||
|  |  * @param rgb2 - The second color to calculate the contrast ratio of. | ||||||
|  |  * @returns The contrast ratio between the two colors. | ||||||
|  |  */ | ||||||
| export const getRGBContrastRatio = ( | export const getRGBContrastRatio = ( | ||||||
|   rgb1: [number, number, number], |   rgb1: [number, number, number], | ||||||
|   rgb2: [number, number, number] |   rgb2: [number, number, number] | ||||||
|   | |||||||
							
								
								
									
										141
									
								
								src/common/controllers/undo-redo-controller.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/common/controllers/undo-redo-controller.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,141 @@ | |||||||
|  | import type { | ||||||
|  |   ReactiveController, | ||||||
|  |   ReactiveControllerHost, | ||||||
|  | } from "@lit/reactive-element/reactive-controller"; | ||||||
|  |  | ||||||
|  | const UNDO_REDO_STACK_LIMIT = 75; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Configuration options for the UndoRedoController. | ||||||
|  |  * | ||||||
|  |  * @template ConfigType The type of configuration to manage. | ||||||
|  |  */ | ||||||
|  | export interface UndoRedoControllerConfig<ConfigType> { | ||||||
|  |   stackLimit?: number; | ||||||
|  |   currentConfig: () => ConfigType; | ||||||
|  |   apply: (config: ConfigType) => void; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A controller to manage undo and redo operations for a given configuration type. | ||||||
|  |  * | ||||||
|  |  * @template ConfigType The type of configuration to manage. | ||||||
|  |  */ | ||||||
|  | export class UndoRedoController<ConfigType> implements ReactiveController { | ||||||
|  |   private _host: ReactiveControllerHost; | ||||||
|  |  | ||||||
|  |   private _undoStack: ConfigType[] = []; | ||||||
|  |  | ||||||
|  |   private _redoStack: ConfigType[] = []; | ||||||
|  |  | ||||||
|  |   private readonly _stackLimit: number = UNDO_REDO_STACK_LIMIT; | ||||||
|  |  | ||||||
|  |   private readonly _apply: (config: ConfigType) => void = () => { | ||||||
|  |     throw new Error("No apply function provided"); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private readonly _currentConfig: () => ConfigType = () => { | ||||||
|  |     throw new Error("No currentConfig function provided"); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   constructor( | ||||||
|  |     host: ReactiveControllerHost, | ||||||
|  |     options: UndoRedoControllerConfig<ConfigType> | ||||||
|  |   ) { | ||||||
|  |     if (options.stackLimit !== undefined) { | ||||||
|  |       this._stackLimit = options.stackLimit; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this._apply = options.apply; | ||||||
|  |     this._currentConfig = options.currentConfig; | ||||||
|  |     this._host = host; | ||||||
|  |     host.addController(this); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   hostConnected() { | ||||||
|  |     window.addEventListener("undo-change", this._onUndoChange); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   hostDisconnected() { | ||||||
|  |     window.removeEventListener("undo-change", this._onUndoChange); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _onUndoChange = (ev: Event) => { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     this.undo(); | ||||||
|  |     this._host.requestUpdate(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Indicates whether there are actions available to undo. | ||||||
|  |    * | ||||||
|  |    * @returns `true` if there are actions to undo, `false` otherwise. | ||||||
|  |    */ | ||||||
|  |   public get canUndo(): boolean { | ||||||
|  |     return this._undoStack.length > 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Indicates whether there are actions available to redo. | ||||||
|  |    * | ||||||
|  |    * @returns `true` if there are actions to redo, `false` otherwise. | ||||||
|  |    */ | ||||||
|  |   public get canRedo(): boolean { | ||||||
|  |     return this._redoStack.length > 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Commits the current configuration to the undo stack and clears the redo stack. | ||||||
|  |    * | ||||||
|  |    * @param config The current configuration to commit. | ||||||
|  |    */ | ||||||
|  |   public commit(config: ConfigType) { | ||||||
|  |     if (this._undoStack.length >= this._stackLimit) { | ||||||
|  |       this._undoStack.shift(); | ||||||
|  |     } | ||||||
|  |     this._undoStack.push({ ...config }); | ||||||
|  |     this._redoStack = []; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Undoes the last action and applies the previous configuration | ||||||
|  |    * while saving the current configuration to the redo stack. | ||||||
|  |    */ | ||||||
|  |   public undo() { | ||||||
|  |     if (this._undoStack.length === 0) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this._redoStack.push({ ...this._currentConfig() }); | ||||||
|  |     const config = this._undoStack.pop()!; | ||||||
|  |     this._apply(config); | ||||||
|  |     this._host.requestUpdate(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Redoes the last undone action and reapplies the configuration | ||||||
|  |    * while saving the current configuration to the undo stack. | ||||||
|  |    */ | ||||||
|  |   public redo() { | ||||||
|  |     if (this._redoStack.length === 0) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this._undoStack.push({ ...this._currentConfig() }); | ||||||
|  |     const config = this._redoStack.pop()!; | ||||||
|  |     this._apply(config); | ||||||
|  |     this._host.requestUpdate(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Resets the undo and redo stacks, clearing all history. | ||||||
|  |    */ | ||||||
|  |   public reset() { | ||||||
|  |     this._undoStack = []; | ||||||
|  |     this._redoStack = []; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HASSDomEvents { | ||||||
|  |     "undo-change": undefined; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -31,10 +31,10 @@ export const isNavigationClick = (e: MouseEvent, preventDefault = true) => { | |||||||
|  |  | ||||||
|   const location = window.location; |   const location = window.location; | ||||||
|   const origin = location.origin || location.protocol + "//" + location.host; |   const origin = location.origin || location.protocol + "//" + location.host; | ||||||
|   if (href.indexOf(origin) !== 0) { |   if (!href.startsWith(origin)) { | ||||||
|     return undefined; |     return undefined; | ||||||
|   } |   } | ||||||
|   href = href.substr(origin.length); |   href = href.slice(origin.length); | ||||||
|  |  | ||||||
|   if (href === "#") { |   if (href === "#") { | ||||||
|     return undefined; |     return undefined; | ||||||
|   | |||||||
| @@ -61,3 +61,9 @@ export const computeEntityEntryName = ( | |||||||
|  |  | ||||||
|   return name; |   return name; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const entityUseDeviceName = ( | ||||||
|  |   stateObj: HassEntity, | ||||||
|  |   entities: HomeAssistant["entities"], | ||||||
|  |   devices: HomeAssistant["devices"] | ||||||
|  | ): boolean => !computeEntityName(stateObj, entities, devices); | ||||||
|   | |||||||
							
								
								
									
										109
									
								
								src/common/entity/compute_entity_name_display.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/common/entity/compute_entity_name_display.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | |||||||
|  | import type { HassEntity } from "home-assistant-js-websocket"; | ||||||
|  | import type { HomeAssistant } from "../../types"; | ||||||
|  | import { ensureArray } from "../array/ensure-array"; | ||||||
|  | import { computeAreaName } from "./compute_area_name"; | ||||||
|  | import { computeDeviceName } from "./compute_device_name"; | ||||||
|  | import { computeEntityName, entityUseDeviceName } from "./compute_entity_name"; | ||||||
|  | import { computeFloorName } from "./compute_floor_name"; | ||||||
|  | import { getEntityContext } from "./context/get_entity_context"; | ||||||
|  |  | ||||||
|  | const DEFAULT_SEPARATOR = " "; | ||||||
|  |  | ||||||
|  | export const DEFAULT_ENTITY_NAME = [ | ||||||
|  |   { type: "device" }, | ||||||
|  |   { type: "entity" }, | ||||||
|  | ] satisfies EntityNameItem[]; | ||||||
|  |  | ||||||
|  | export type EntityNameItem = | ||||||
|  |   | { | ||||||
|  |       type: "entity" | "device" | "area" | "floor"; | ||||||
|  |     } | ||||||
|  |   | { | ||||||
|  |       type: "text"; | ||||||
|  |       text: string; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  | export interface EntityNameOptions { | ||||||
|  |   separator?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const computeEntityNameDisplay = ( | ||||||
|  |   stateObj: HassEntity, | ||||||
|  |   name: EntityNameItem | EntityNameItem[] | undefined, | ||||||
|  |   entities: HomeAssistant["entities"], | ||||||
|  |   devices: HomeAssistant["devices"], | ||||||
|  |   areas: HomeAssistant["areas"], | ||||||
|  |   floors: HomeAssistant["floors"], | ||||||
|  |   options?: EntityNameOptions | ||||||
|  | ) => { | ||||||
|  |   let items = ensureArray(name || DEFAULT_ENTITY_NAME); | ||||||
|  |  | ||||||
|  |   const separator = options?.separator ?? DEFAULT_SEPARATOR; | ||||||
|  |  | ||||||
|  |   // If all items are text, just join them | ||||||
|  |   if (items.every((n) => n.type === "text")) { | ||||||
|  |     return items.map((item) => item.text).join(separator); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const useDeviceName = entityUseDeviceName(stateObj, entities, devices); | ||||||
|  |  | ||||||
|  |   // If entity uses device name, and device is not already included, replace it with device name | ||||||
|  |   if (useDeviceName) { | ||||||
|  |     const hasDevice = items.some((n) => n.type === "device"); | ||||||
|  |     if (!hasDevice) { | ||||||
|  |       items = items.map((n) => (n.type === "entity" ? { type: "device" } : n)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const names = computeEntityNameList( | ||||||
|  |     stateObj, | ||||||
|  |     items, | ||||||
|  |     entities, | ||||||
|  |     devices, | ||||||
|  |     areas, | ||||||
|  |     floors | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   // If after processing there is only one name, return that | ||||||
|  |   if (names.length === 1) { | ||||||
|  |     return names[0] || ""; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return names.filter((n) => n).join(separator); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const computeEntityNameList = ( | ||||||
|  |   stateObj: HassEntity, | ||||||
|  |   name: EntityNameItem[], | ||||||
|  |   entities: HomeAssistant["entities"], | ||||||
|  |   devices: HomeAssistant["devices"], | ||||||
|  |   areas: HomeAssistant["areas"], | ||||||
|  |   floors: HomeAssistant["floors"] | ||||||
|  | ): (string | undefined)[] => { | ||||||
|  |   const { device, area, floor } = getEntityContext( | ||||||
|  |     stateObj, | ||||||
|  |     entities, | ||||||
|  |     devices, | ||||||
|  |     areas, | ||||||
|  |     floors | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   const names = name.map((item) => { | ||||||
|  |     switch (item.type) { | ||||||
|  |       case "entity": | ||||||
|  |         return computeEntityName(stateObj, entities, devices); | ||||||
|  |       case "device": | ||||||
|  |         return device ? computeDeviceName(device) : undefined; | ||||||
|  |       case "area": | ||||||
|  |         return area ? computeAreaName(area) : undefined; | ||||||
|  |       case "floor": | ||||||
|  |         return floor ? computeFloorName(floor) : undefined; | ||||||
|  |       case "text": | ||||||
|  |         return item.text; | ||||||
|  |       default: | ||||||
|  |         return ""; | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return names; | ||||||
|  | }; | ||||||
| @@ -1,3 +1,3 @@ | |||||||
| /** Compute the object ID of a state. */ | /** Compute the object ID of a state. */ | ||||||
| export const computeObjectId = (entityId: string): string => | export const computeObjectId = (entityId: string): string => | ||||||
|   entityId.substr(entityId.indexOf(".") + 1); |   entityId.slice(entityId.indexOf(".") + 1); | ||||||
|   | |||||||
| @@ -8,10 +8,10 @@ interface AreaContext { | |||||||
| } | } | ||||||
| export const getAreaContext = ( | export const getAreaContext = ( | ||||||
|   area: AreaRegistryEntry, |   area: AreaRegistryEntry, | ||||||
|   hass: HomeAssistant |   hassFloors: HomeAssistant["floors"] | ||||||
| ): AreaContext => { | ): AreaContext => { | ||||||
|   const floorId = area.floor_id; |   const floorId = area.floor_id; | ||||||
|   const floor = floorId ? hass.floors[floorId] : undefined; |   const floor = floorId ? hassFloors[floorId] : undefined; | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|     area: area, |     area: area, | ||||||
|   | |||||||
| @@ -122,3 +122,22 @@ export const generateEntityFilter = ( | |||||||
|     return true; |     return true; | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const findEntities = ( | ||||||
|  |   entities: string[], | ||||||
|  |   filters: EntityFilterFunc[] | ||||||
|  | ): string[] => { | ||||||
|  |   const seen = new Set<string>(); | ||||||
|  |   const results: string[] = []; | ||||||
|  |  | ||||||
|  |   for (const filter of filters) { | ||||||
|  |     for (const entity of entities) { | ||||||
|  |       if (filter(entity) && !seen.has(entity)) { | ||||||
|  |         seen.add(entity); | ||||||
|  |         results.push(entity); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return results; | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ export const FIXED_DOMAIN_STATES = { | |||||||
|     "pending", |     "pending", | ||||||
|     "triggered", |     "triggered", | ||||||
|   ], |   ], | ||||||
|  |   alert: ["on", "off", "idle"], | ||||||
|   assist_satellite: ["idle", "listening", "responding", "processing"], |   assist_satellite: ["idle", "listening", "responding", "processing"], | ||||||
|   automation: ["on", "off"], |   automation: ["on", "off"], | ||||||
|   binary_sensor: ["on", "off"], |   binary_sensor: ["on", "off"], | ||||||
|   | |||||||
| @@ -40,6 +40,7 @@ const STATE_COLORED_DOMAIN = new Set([ | |||||||
|   "vacuum", |   "vacuum", | ||||||
|   "valve", |   "valve", | ||||||
|   "water_heater", |   "water_heater", | ||||||
|  |   "weather", | ||||||
| ]); | ]); | ||||||
|  |  | ||||||
| export const stateColorCss = (stateObj: HassEntity, state?: string) => { | export const stateColorCss = (stateObj: HassEntity, state?: string) => { | ||||||
|   | |||||||
| @@ -32,6 +32,8 @@ export const numberFormatToLocale = ( | |||||||
|       return ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89 |       return ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89 | ||||||
|     case NumberFormat.space_comma: |     case NumberFormat.space_comma: | ||||||
|       return ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89 |       return ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89 | ||||||
|  |     case NumberFormat.quote_decimal: | ||||||
|  |       return ["de-CH"]; // Use German (Switzerland) formatting 1'234'567.89 | ||||||
|     case NumberFormat.system: |     case NumberFormat.system: | ||||||
|       return undefined; |       return undefined; | ||||||
|     default: |     default: | ||||||
|   | |||||||
| @@ -67,10 +67,7 @@ function isSeparatorAtPos(value: string, index: number): boolean { | |||||||
|     case undefined: |     case undefined: | ||||||
|       return false; |       return false; | ||||||
|     default: |     default: | ||||||
|       if (isEmojiImprecise(code)) { |       return isEmojiImprecise(code); | ||||||
|         return true; |  | ||||||
|       } |  | ||||||
|       return false; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,13 +1,12 @@ | |||||||
| import type { HassConfig, HassEntity } from "home-assistant-js-websocket"; | import type { HassConfig, HassEntity } from "home-assistant-js-websocket"; | ||||||
| import type { FrontendLocaleData } from "../../data/translation"; | import type { FrontendLocaleData } from "../../data/translation"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
|  | import { | ||||||
|  |   computeEntityNameDisplay, | ||||||
|  |   type EntityNameItem, | ||||||
|  |   type EntityNameOptions, | ||||||
|  | } from "../entity/compute_entity_name_display"; | ||||||
| import type { LocalizeFunc } from "./localize"; | import type { LocalizeFunc } from "./localize"; | ||||||
| import { computeEntityName } from "../entity/compute_entity_name"; |  | ||||||
| import { computeDeviceName } from "../entity/compute_device_name"; |  | ||||||
| import { getEntityContext } from "../entity/context/get_entity_context"; |  | ||||||
| import { computeAreaName } from "../entity/compute_area_name"; |  | ||||||
| import { computeFloorName } from "../entity/compute_floor_name"; |  | ||||||
| import { ensureArray } from "../array/ensure-array"; |  | ||||||
|  |  | ||||||
| export type FormatEntityStateFunc = ( | export type FormatEntityStateFunc = ( | ||||||
|   stateObj: HassEntity, |   stateObj: HassEntity, | ||||||
| @@ -27,8 +26,8 @@ export type EntityNameType = "entity" | "device" | "area" | "floor"; | |||||||
|  |  | ||||||
| export type FormatEntityNameFunc = ( | export type FormatEntityNameFunc = ( | ||||||
|   stateObj: HassEntity, |   stateObj: HassEntity, | ||||||
|   type: EntityNameType | EntityNameType[], |   name: EntityNameItem | EntityNameItem[], | ||||||
|   separator?: string |   options?: EntityNameOptions | ||||||
| ) => string; | ) => string; | ||||||
|  |  | ||||||
| export const computeFormatFunctions = async ( | export const computeFormatFunctions = async ( | ||||||
| @@ -75,45 +74,15 @@ export const computeFormatFunctions = async ( | |||||||
|       ), |       ), | ||||||
|     formatEntityAttributeName: (stateObj, attribute) => |     formatEntityAttributeName: (stateObj, attribute) => | ||||||
|       computeAttributeNameDisplay(localize, stateObj, entities, attribute), |       computeAttributeNameDisplay(localize, stateObj, entities, attribute), | ||||||
|     formatEntityName: (stateObj, type, separator = " ") => { |     formatEntityName: (stateObj, name, options) => | ||||||
|       const types = ensureArray(type); |       computeEntityNameDisplay( | ||||||
|       const namesList: (string | undefined)[] = []; |  | ||||||
|  |  | ||||||
|       const { device, area, floor } = getEntityContext( |  | ||||||
|         stateObj, |         stateObj, | ||||||
|  |         name, | ||||||
|         entities, |         entities, | ||||||
|         devices, |         devices, | ||||||
|         areas, |         areas, | ||||||
|         floors |         floors, | ||||||
|       ); |         options | ||||||
|  |       ), | ||||||
|       for (const t of types) { |  | ||||||
|         switch (t) { |  | ||||||
|           case "entity": { |  | ||||||
|             namesList.push(computeEntityName(stateObj, entities, devices)); |  | ||||||
|             break; |  | ||||||
|           } |  | ||||||
|           case "device": { |  | ||||||
|             if (device) { |  | ||||||
|               namesList.push(computeDeviceName(device)); |  | ||||||
|             } |  | ||||||
|             break; |  | ||||||
|           } |  | ||||||
|           case "area": { |  | ||||||
|             if (area) { |  | ||||||
|               namesList.push(computeAreaName(area)); |  | ||||||
|             } |  | ||||||
|             break; |  | ||||||
|           } |  | ||||||
|           case "floor": { |  | ||||||
|             if (floor) { |  | ||||||
|               namesList.push(computeFloorName(floor)); |  | ||||||
|             } |  | ||||||
|             break; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       return namesList.filter((name) => name !== undefined).join(separator); |  | ||||||
|     }, |  | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ export default function parseAspectRatio(input: string) { | |||||||
|   } |   } | ||||||
|   try { |   try { | ||||||
|     if (input.endsWith("%")) { |     if (input.endsWith("%")) { | ||||||
|       return { w: 100, h: parseOrThrow(input.substr(0, input.length - 1)) }; |       return { w: 100, h: parseOrThrow(input.slice(0, -1)) }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const arr = input.replace(":", "x").split("x"); |     const arr = input.replace(":", "x").split("x"); | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								src/common/util/xss.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/common/util/xss.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | import xss from "xss"; | ||||||
|  |  | ||||||
|  | export const filterXSS = (html: string) => | ||||||
|  |   xss(html, { | ||||||
|  |     whiteList: {}, | ||||||
|  |     stripIgnoreTag: true, | ||||||
|  |     stripIgnoreTagBody: true, | ||||||
|  |   }); | ||||||
| @@ -6,6 +6,7 @@ import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; | |||||||
| import "./ha-progress-button"; | import "./ha-progress-button"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
|  | import type { Appearance } from "../ha-button"; | ||||||
|  |  | ||||||
| @customElement("ha-call-service-button") | @customElement("ha-call-service-button") | ||||||
| class HaCallServiceButton extends LitElement { | class HaCallServiceButton extends LitElement { | ||||||
| @@ -25,12 +26,14 @@ class HaCallServiceButton extends LitElement { | |||||||
|  |  | ||||||
|   @property() public confirmation?; |   @property() public confirmation?; | ||||||
|  |  | ||||||
|  |   @property() public appearance: Appearance = "plain"; | ||||||
|  |  | ||||||
|   public render(): TemplateResult { |   public render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <ha-progress-button |       <ha-progress-button | ||||||
|         .progress=${this.progress} |         .progress=${this.progress} | ||||||
|         .disabled=${this.disabled} |         .disabled=${this.disabled} | ||||||
|         appearance="plain" |         .appearance=${this.appearance} | ||||||
|         @click=${this._buttonTapped} |         @click=${this._buttonTapped} | ||||||
|         tabindex="0" |         tabindex="0" | ||||||
|       > |       > | ||||||
|   | |||||||
| @@ -1,21 +1,22 @@ | |||||||
| import type { LineSeriesOption } from "echarts"; | import type { LineSeriesOption } from "echarts"; | ||||||
|  |  | ||||||
| export function downSampleLineData( | export function downSampleLineData< | ||||||
|   data: LineSeriesOption["data"], |   T extends [number, number] | NonNullable<LineSeriesOption["data"]>[number], | ||||||
|   chartWidth: number, | >( | ||||||
|  |   data: T[] | undefined, | ||||||
|  |   maxDetails: number, | ||||||
|   minX?: number, |   minX?: number, | ||||||
|   maxX?: number |   maxX?: number | ||||||
| ) { | ): T[] { | ||||||
|   if (!data || data.length < 10) { |   if (!data) { | ||||||
|     return data; |     return []; | ||||||
|   } |   } | ||||||
|   const width = chartWidth * window.devicePixelRatio; |   if (data.length <= maxDetails) { | ||||||
|   if (data.length <= width) { |  | ||||||
|     return data; |     return data; | ||||||
|   } |   } | ||||||
|   const min = minX ?? getPointData(data[0]!)[0]; |   const min = minX ?? getPointData(data[0]!)[0]; | ||||||
|   const max = maxX ?? getPointData(data[data.length - 1]!)[0]; |   const max = maxX ?? getPointData(data[data.length - 1]!)[0]; | ||||||
|   const step = Math.floor((max - min) / width); |   const step = Math.ceil((max - min) / Math.floor(maxDetails)); | ||||||
|   const frames = new Map< |   const frames = new Map< | ||||||
|     number, |     number, | ||||||
|     { |     { | ||||||
| @@ -47,7 +48,7 @@ export function downSampleLineData( | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Convert frames back to points |   // Convert frames back to points | ||||||
|   const result: typeof data = []; |   const result: T[] = []; | ||||||
|   for (const [_i, frame] of frames) { |   for (const [_i, frame] of frames) { | ||||||
|     // Use min/max points to preserve visual accuracy |     // Use min/max points to preserve visual accuracy | ||||||
|     // The order of the data must be preserved so max may be before min |     // The order of the data must be preserved so max may be before min | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { consume } from "@lit/context"; |  | ||||||
| import { ResizeController } from "@lit-labs/observers/resize-controller"; | import { ResizeController } from "@lit-labs/observers/resize-controller"; | ||||||
|  | import { consume } from "@lit/context"; | ||||||
| import { mdiChevronDown, mdiChevronUp, mdiRestart } from "@mdi/js"; | import { mdiChevronDown, mdiChevronUp, mdiRestart } from "@mdi/js"; | ||||||
| import { differenceInMinutes } from "date-fns"; | import { differenceInMinutes } from "date-fns"; | ||||||
| import type { DataZoomComponentOption } from "echarts/components"; | import type { DataZoomComponentOption } from "echarts/components"; | ||||||
| @@ -7,27 +7,28 @@ import type { EChartsType } from "echarts/core"; | |||||||
| import type { | import type { | ||||||
|   ECElementEvent, |   ECElementEvent, | ||||||
|   LegendComponentOption, |   LegendComponentOption, | ||||||
|  |   LineSeriesOption, | ||||||
|   XAXisOption, |   XAXisOption, | ||||||
|   YAXisOption, |   YAXisOption, | ||||||
|   LineSeriesOption, |  | ||||||
| } from "echarts/types/dist/shared"; | } from "echarts/types/dist/shared"; | ||||||
| import type { PropertyValues } from "lit"; | import type { 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 { classMap } from "lit/directives/class-map"; | import { classMap } from "lit/directives/class-map"; | ||||||
| import { styleMap } from "lit/directives/style-map"; | import { styleMap } from "lit/directives/style-map"; | ||||||
|  | import { ensureArray } from "../../common/array/ensure-array"; | ||||||
| import { getAllGraphColors } from "../../common/color/colors"; | import { getAllGraphColors } from "../../common/color/colors"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import { listenMediaQuery } from "../../common/dom/media_query"; | import { listenMediaQuery } from "../../common/dom/media_query"; | ||||||
| import { themesContext } from "../../data/context"; | import { themesContext } from "../../data/context"; | ||||||
| import type { Themes } from "../../data/ws-themes"; | import type { Themes } from "../../data/ws-themes"; | ||||||
| import type { ECOption } from "../../resources/echarts"; | import type { ECOption } from "../../resources/echarts/echarts"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import { isMac } from "../../util/is_mac"; | import { isMac } from "../../util/is_mac"; | ||||||
| import "../ha-icon-button"; |  | ||||||
| import { formatTimeLabel } from "./axis-label"; |  | ||||||
| import { ensureArray } from "../../common/array/ensure-array"; |  | ||||||
| import "../chips/ha-assist-chip"; | import "../chips/ha-assist-chip"; | ||||||
|  | import "../ha-icon-button"; | ||||||
|  | import { filterXSS } from "../../common/util/xss"; | ||||||
|  | import { formatTimeLabel } from "./axis-label"; | ||||||
| import { downSampleLineData } from "./down-sample"; | import { downSampleLineData } from "./down-sample"; | ||||||
|  |  | ||||||
| export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; | export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; | ||||||
| @@ -87,9 +88,19 @@ export class HaChartBase extends LitElement { | |||||||
|  |  | ||||||
|   private _lastTapTime?: number; |   private _lastTapTime?: number; | ||||||
|  |  | ||||||
|  |   private _shouldResizeChart = false; | ||||||
|  |  | ||||||
|   // @ts-ignore |   // @ts-ignore | ||||||
|   private _resizeController = new ResizeController(this, { |   private _resizeController = new ResizeController(this, { | ||||||
|     callback: () => this.chart?.resize(), |     callback: () => { | ||||||
|  |       if (this.chart) { | ||||||
|  |         if (!this.chart.getZr().animation.isFinished()) { | ||||||
|  |           this._shouldResizeChart = true; | ||||||
|  |         } else { | ||||||
|  |           this.chart.resize(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   private _loading = false; |   private _loading = false; | ||||||
| @@ -345,7 +356,7 @@ export class HaChartBase extends LitElement { | |||||||
|       if (this.chart) { |       if (this.chart) { | ||||||
|         this.chart.dispose(); |         this.chart.dispose(); | ||||||
|       } |       } | ||||||
|       const echarts = (await import("../../resources/echarts")).default; |       const echarts = (await import("../../resources/echarts/echarts")).default; | ||||||
|  |  | ||||||
|       if (this.extraComponents?.length) { |       if (this.extraComponents?.length) { | ||||||
|         echarts.use(this.extraComponents); |         echarts.use(this.extraComponents); | ||||||
| @@ -365,6 +376,7 @@ export class HaChartBase extends LitElement { | |||||||
|       if (!this.options?.dataZoom) { |       if (!this.options?.dataZoom) { | ||||||
|         this.chart.getZr().on("dblclick", this._handleClickZoom); |         this.chart.getZr().on("dblclick", this._handleClickZoom); | ||||||
|       } |       } | ||||||
|  |       this.chart.on("finished", this._handleChartRenderFinished); | ||||||
|       if (this._isTouchDevice) { |       if (this._isTouchDevice) { | ||||||
|         this.chart.getZr().on("click", (e: ECElementEvent) => { |         this.chart.getZr().on("click", (e: ECElementEvent) => { | ||||||
|           if (!e.zrByTouch) { |           if (!e.zrByTouch) { | ||||||
| @@ -804,14 +816,15 @@ export class HaChartBase extends LitElement { | |||||||
|             sampling: undefined, |             sampling: undefined, | ||||||
|             data: downSampleLineData( |             data: downSampleLineData( | ||||||
|               data as LineSeriesOption["data"], |               data as LineSeriesOption["data"], | ||||||
|               this.clientWidth, |               this.clientWidth * window.devicePixelRatio, | ||||||
|               minX, |               minX, | ||||||
|               maxX |               maxX | ||||||
|             ), |             ), | ||||||
|           }; |           }; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       return { ...s, data }; |       const name = filterXSS(String(s.name ?? s.id ?? "")); | ||||||
|  |       return { ...s, name, data }; | ||||||
|     }); |     }); | ||||||
|     return series as ECOption["series"]; |     return series as ECOption["series"]; | ||||||
|   } |   } | ||||||
| @@ -943,6 +956,13 @@ export class HaChartBase extends LitElement { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _handleChartRenderFinished = () => { | ||||||
|  |     if (this._shouldResizeChart) { | ||||||
|  |       this.chart?.resize(); | ||||||
|  |       this._shouldResizeChart = false; | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   static styles = css` |   static styles = css` | ||||||
|     :host { |     :host { | ||||||
|       display: block; |       display: block; | ||||||
| @@ -974,7 +994,7 @@ export class HaChartBase extends LitElement { | |||||||
|       right: 4px; |       right: 4px; | ||||||
|       display: flex; |       display: flex; | ||||||
|       flex-direction: column; |       flex-direction: column; | ||||||
|       gap: 4px; |       gap: var(--ha-space-1); | ||||||
|     } |     } | ||||||
|     .chart-controls.small { |     .chart-controls.small { | ||||||
|       top: 0; |       top: 0; | ||||||
| @@ -983,7 +1003,7 @@ export class HaChartBase extends LitElement { | |||||||
|     .chart-controls ha-icon-button, |     .chart-controls ha-icon-button, | ||||||
|     .chart-controls ::slotted(ha-icon-button) { |     .chart-controls ::slotted(ha-icon-button) { | ||||||
|       background: var(--card-background-color); |       background: var(--card-background-color); | ||||||
|       border-radius: 4px; |       border-radius: var(--ha-border-radius-sm); | ||||||
|       --mdc-icon-button-size: 32px; |       --mdc-icon-button-size: 32px; | ||||||
|       color: var(--primary-color); |       color: var(--primary-color); | ||||||
|       border: 1px solid var(--divider-color); |       border: 1px solid var(--divider-color); | ||||||
| @@ -1011,7 +1031,7 @@ export class HaChartBase extends LitElement { | |||||||
|       flex-wrap: wrap; |       flex-wrap: wrap; | ||||||
|       justify-content: center; |       justify-content: center; | ||||||
|       align-items: center; |       align-items: center; | ||||||
|       gap: 8px; |       gap: var(--ha-space-2); | ||||||
|     } |     } | ||||||
|     .chart-legend li { |     .chart-legend li { | ||||||
|       height: 24px; |       height: 24px; | ||||||
| @@ -1036,7 +1056,7 @@ export class HaChartBase extends LitElement { | |||||||
|     .chart-legend .bullet { |     .chart-legend .bullet { | ||||||
|       border-width: 1px; |       border-width: 1px; | ||||||
|       border-style: solid; |       border-style: solid; | ||||||
|       border-radius: 50%; |       border-radius: var(--ha-border-radius-circle); | ||||||
|       display: block; |       display: block; | ||||||
|       height: 16px; |       height: 16px; | ||||||
|       width: 16px; |       width: 16px; | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import type { TopLevelFormatterParams } from "echarts/types/dist/shared"; | |||||||
| import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js"; | import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { listenMediaQuery } from "../../common/dom/media_query"; | import { listenMediaQuery } from "../../common/dom/media_query"; | ||||||
| import type { ECOption } from "../../resources/echarts"; | import type { ECOption } from "../../resources/echarts/echarts"; | ||||||
| import "./ha-chart-base"; | import "./ha-chart-base"; | ||||||
| import type { HaChartBase } from "./ha-chart-base"; | import type { HaChartBase } from "./ha-chart-base"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
|   | |||||||
| @@ -1,14 +1,15 @@ | |||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { LitElement, html, css } from "lit"; | import { LitElement, html, css } from "lit"; | ||||||
| import type { EChartsType } from "echarts/core"; | import type { EChartsType } from "echarts/core"; | ||||||
| import type { CallbackDataParams } from "echarts/types/dist/shared"; |  | ||||||
| import type { SankeySeriesOption } from "echarts/types/dist/echarts"; | import type { SankeySeriesOption } from "echarts/types/dist/echarts"; | ||||||
| import { SankeyChart } from "echarts/charts"; | import type { CallbackDataParams } from "echarts/types/src/util/types"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { ResizeController } from "@lit-labs/observers/resize-controller"; | import { ResizeController } from "@lit-labs/observers/resize-controller"; | ||||||
|  | import SankeyChart from "../../resources/echarts/components/sankey/install"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import type { ECOption } from "../../resources/echarts"; | import type { ECOption } from "../../resources/echarts/echarts"; | ||||||
| import { measureTextWidth } from "../../util/text"; | import { measureTextWidth } from "../../util/text"; | ||||||
|  | import { filterXSS } from "../../common/util/xss"; | ||||||
| import "./ha-chart-base"; | import "./ha-chart-base"; | ||||||
| import { NODE_SIZE } from "../trace/hat-graph-const"; | import { NODE_SIZE } from "../trace/hat-graph-const"; | ||||||
| import "../ha-alert"; | import "../ha-alert"; | ||||||
| @@ -38,7 +39,7 @@ type ProcessedLink = Link & { | |||||||
|  |  | ||||||
| const OVERFLOW_MARGIN = 5; | const OVERFLOW_MARGIN = 5; | ||||||
| const FONT_SIZE = 12; | const FONT_SIZE = 12; | ||||||
| const NODE_GAP = 8; | const NODE_GAP = 6; | ||||||
| const LABEL_DISTANCE = 5; | const LABEL_DISTANCE = 5; | ||||||
|  |  | ||||||
| @customElement("ha-sankey-chart") | @customElement("ha-sankey-chart") | ||||||
| @@ -92,12 +93,12 @@ export class HaSankeyChart extends LitElement { | |||||||
|       : data.value; |       : data.value; | ||||||
|     if (data.id) { |     if (data.id) { | ||||||
|       const node = this.data.nodes.find((n) => n.id === data.id); |       const node = this.data.nodes.find((n) => n.id === data.id); | ||||||
|       return `${params.marker} ${node?.label ?? data.id}<br>${value}`; |       return `${params.marker} ${filterXSS(node?.label ?? data.id)}<br>${value}`; | ||||||
|     } |     } | ||||||
|     if (data.source && data.target) { |     if (data.source && data.target) { | ||||||
|       const source = this.data.nodes.find((n) => n.id === data.source); |       const source = this.data.nodes.find((n) => n.id === data.source); | ||||||
|       const target = this.data.nodes.find((n) => n.id === data.target); |       const target = this.data.nodes.find((n) => n.id === data.target); | ||||||
|       return `${source?.label ?? data.source} → ${target?.label ?? data.target}<br>${value}`; |       return `${filterXSS(source?.label ?? data.source)} → ${filterXSS(target?.label ?? data.target)}<br>${value}`; | ||||||
|     } |     } | ||||||
|     return null; |     return null; | ||||||
|   }; |   }; | ||||||
| @@ -163,6 +164,7 @@ export class HaSankeyChart extends LitElement { | |||||||
|       lineStyle: { |       lineStyle: { | ||||||
|         color: "gradient", |         color: "gradient", | ||||||
|         opacity: 0.4, |         opacity: 0.4, | ||||||
|  |         curveness: 0.5, | ||||||
|       }, |       }, | ||||||
|       layoutIterations: 0, |       layoutIterations: 0, | ||||||
|       label: { |       label: { | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ import { computeRTL } from "../../common/util/compute_rtl"; | |||||||
| import type { LineChartEntity, LineChartState } from "../../data/history"; | import type { LineChartEntity, LineChartState } from "../../data/history"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; | import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; | ||||||
| import type { ECOption } from "../../resources/echarts"; | import type { ECOption } from "../../resources/echarts/echarts"; | ||||||
| import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; | import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; | ||||||
| import { | import { | ||||||
|   getNumberFormatOptions, |   getNumberFormatOptions, | ||||||
|   | |||||||
| @@ -15,8 +15,8 @@ import type { TimelineEntity } from "../../data/history"; | |||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; | import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; | ||||||
| import { computeTimelineColor } from "./timeline-color"; | import { computeTimelineColor } from "./timeline-color"; | ||||||
| import type { ECOption } from "../../resources/echarts"; | import type { ECOption } from "../../resources/echarts/echarts"; | ||||||
| import echarts from "../../resources/echarts"; | import echarts from "../../resources/echarts/echarts"; | ||||||
| import { luminosity } from "../../common/color/rgb"; | import { luminosity } from "../../common/color/rgb"; | ||||||
| import { hex2rgb } from "../../common/color/convert-color"; | import { hex2rgb } from "../../common/color/convert-color"; | ||||||
| import { measureTextWidth } from "../../util/text"; | import { measureTextWidth } from "../../util/text"; | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ import { | |||||||
|   getStatisticMetadata, |   getStatisticMetadata, | ||||||
|   statisticsHaveType, |   statisticsHaveType, | ||||||
| } from "../../data/recorder"; | } from "../../data/recorder"; | ||||||
| import type { ECOption } from "../../resources/echarts"; | import type { ECOption } from "../../resources/echarts/echarts"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import type { CustomLegendOption } from "./ha-chart-base"; | import type { CustomLegendOption } from "./ha-chart-base"; | ||||||
| import "./ha-chart-base"; | import "./ha-chart-base"; | ||||||
|   | |||||||
| @@ -6,6 +6,8 @@ import { computeDomain } from "../../common/entity/compute_domain"; | |||||||
| import { stateColorProperties } from "../../common/entity/state_color"; | import { stateColorProperties } from "../../common/entity/state_color"; | ||||||
| import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; | import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; | ||||||
| import { computeCssValue } from "../../resources/css-variables"; | import { computeCssValue } from "../../resources/css-variables"; | ||||||
|  | import { computeStateDomain } from "../../common/entity/compute_state_domain"; | ||||||
|  | import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states"; | ||||||
|  |  | ||||||
| const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = { | const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = { | ||||||
|   media_player: { |   media_player: { | ||||||
| @@ -51,6 +53,28 @@ function computeTimelineStateColor( | |||||||
| let colorIndex = 0; | let colorIndex = 0; | ||||||
| const stateColorMap = new Map<string, string>(); | const stateColorMap = new Map<string, string>(); | ||||||
|  |  | ||||||
|  | function computeTimelineEnumColor( | ||||||
|  |   state: string, | ||||||
|  |   computedStyles: CSSStyleDeclaration, | ||||||
|  |   stateObj?: HassEntity | ||||||
|  | ): string | undefined { | ||||||
|  |   if (!stateObj) { | ||||||
|  |     return undefined; | ||||||
|  |   } | ||||||
|  |   const domain = computeStateDomain(stateObj); | ||||||
|  |   const states = | ||||||
|  |     FIXED_DOMAIN_STATES[domain] || | ||||||
|  |     (domain === "sensor" && | ||||||
|  |       stateObj.attributes.device_class === "enum" && | ||||||
|  |       stateObj.attributes.options) || | ||||||
|  |     []; | ||||||
|  |   const idx = states.indexOf(state); | ||||||
|  |   if (idx === -1) { | ||||||
|  |     return undefined; | ||||||
|  |   } | ||||||
|  |   return getGraphColorByIndex(idx, computedStyles); | ||||||
|  | } | ||||||
|  |  | ||||||
| function computeTimeLineGenericColor( | function computeTimeLineGenericColor( | ||||||
|   state: string, |   state: string, | ||||||
|   computedStyles: CSSStyleDeclaration |   computedStyles: CSSStyleDeclaration | ||||||
| @@ -71,6 +95,7 @@ export function computeTimelineColor( | |||||||
| ): string { | ): string { | ||||||
|   return ( |   return ( | ||||||
|     computeTimelineStateColor(state, computedStyles, stateObj) || |     computeTimelineStateColor(state, computedStyles, stateObj) || | ||||||
|  |     computeTimelineEnumColor(state, computedStyles, stateObj) || | ||||||
|     computeTimeLineGenericColor(state, computedStyles) |     computeTimeLineGenericColor(state, computedStyles) | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
|  | import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles"; | ||||||
| import { FilterChip } from "@material/web/chips/internal/filter-chip"; | import { FilterChip } from "@material/web/chips/internal/filter-chip"; | ||||||
| import { styles } from "@material/web/chips/internal/filter-styles"; | import { styles } from "@material/web/chips/internal/filter-styles"; | ||||||
| import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles"; | import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles"; | ||||||
| import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles"; | import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles"; | ||||||
| import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles"; | import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles"; | ||||||
| import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles"; |  | ||||||
| import { css, html } from "lit"; | import { css, html } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
|  |  | ||||||
| @@ -30,6 +30,7 @@ export class HaFilterChip extends FilterChip { | |||||||
|           var(--rgb-primary-text-color), |           var(--rgb-primary-text-color), | ||||||
|           0.15 |           0.15 | ||||||
|         ); |         ); | ||||||
|  |         border-radius: var(--ha-border-radius-md); | ||||||
|       } |       } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js"; | import { mdiDragHorizontalVariant, mdiEye, mdiEyeOff } from "@mdi/js"; | ||||||
| import type { CSSResultGroup } from "lit"; | import type { CSSResultGroup } from "lit"; | ||||||
| import { LitElement, css, html, nothing } from "lit"; | import { LitElement, css, html, nothing } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| @@ -129,7 +129,7 @@ export class DialogDataTableSettings extends LitElement { | |||||||
|                   ${canMove && isVisible |                   ${canMove && isVisible | ||||||
|                     ? html`<ha-svg-icon |                     ? html`<ha-svg-icon | ||||||
|                         class="handle" |                         class="handle" | ||||||
|                         .path=${mdiDrag} |                         .path=${mdiDragHorizontalVariant} | ||||||
|                         slot="graphic" |                         slot="graphic" | ||||||
|                       ></ha-svg-icon>` |                       ></ha-svg-icon>` | ||||||
|                     : nothing} |                     : nothing} | ||||||
| @@ -290,7 +290,9 @@ export class DialogDataTableSettings extends LitElement { | |||||||
|           ha-dialog { |           ha-dialog { | ||||||
|             --vertical-align-dialog: flex-start; |             --vertical-align-dialog: flex-start; | ||||||
|             --dialog-surface-margin-top: 250px; |             --dialog-surface-margin-top: 250px; | ||||||
|             --ha-dialog-border-radius: 28px 28px 0 0; |             --ha-dialog-border-radius: var(--ha-border-radius-4xl) | ||||||
|  |               var(--ha-border-radius-4xl) var(--ha-border-radius-square) | ||||||
|  |               var(--ha-border-radius-square); | ||||||
|             --mdc-dialog-min-height: calc(100% - 250px); |             --mdc-dialog-min-height: calc(100% - 250px); | ||||||
|             --mdc-dialog-max-height: calc(100% - 250px); |             --mdc-dialog-max-height: calc(100% - 250px); | ||||||
|           } |           } | ||||||
|   | |||||||
| @@ -1053,7 +1053,7 @@ export class HaDataTable extends LitElement { | |||||||
|  |  | ||||||
|         .mdc-data-table { |         .mdc-data-table { | ||||||
|           background-color: var(--data-table-background-color); |           background-color: var(--data-table-background-color); | ||||||
|           border-radius: 4px; |           border-radius: var(--ha-border-radius-sm); | ||||||
|           border-width: 1px; |           border-width: 1px; | ||||||
|           border-style: solid; |           border-style: solid; | ||||||
|           border-color: var(--divider-color); |           border-color: var(--divider-color); | ||||||
|   | |||||||
| @@ -1,6 +1,9 @@ | |||||||
| import { expose } from "comlink"; | import { expose } from "comlink"; | ||||||
| import { stringCompare, ipCompare } from "../../common/string/compare"; | import Fuse from "fuse.js"; | ||||||
|  | import memoizeOne from "memoize-one"; | ||||||
|  | import { ipCompare, stringCompare } from "../../common/string/compare"; | ||||||
| import { stripDiacritics } from "../../common/string/strip-diacritics"; | import { stripDiacritics } from "../../common/string/strip-diacritics"; | ||||||
|  | import { HaFuse } from "../../resources/fuse"; | ||||||
| import type { | import type { | ||||||
|   ClonedDataTableColumnData, |   ClonedDataTableColumnData, | ||||||
|   DataTableRowData, |   DataTableRowData, | ||||||
| @@ -8,29 +11,48 @@ import type { | |||||||
|   SortingDirection, |   SortingDirection, | ||||||
| } from "./ha-data-table"; | } from "./ha-data-table"; | ||||||
|  |  | ||||||
|  | const fuseIndex = memoizeOne( | ||||||
|  |   (data: DataTableRowData[], columns: SortableColumnContainer) => { | ||||||
|  |     const searchKeys = new Set<string>(); | ||||||
|  |     Object.entries(columns).forEach(([key, column]) => { | ||||||
|  |       if (column.filterable) { | ||||||
|  |         searchKeys.add( | ||||||
|  |           column.filterKey | ||||||
|  |             ? `${column.valueColumn || key}.${column.filterKey}` | ||||||
|  |             : key | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |     return Fuse.createIndex([...searchKeys], data); | ||||||
|  |   } | ||||||
|  | ); | ||||||
|  |  | ||||||
| const filterData = ( | const filterData = ( | ||||||
|   data: DataTableRowData[], |   data: DataTableRowData[], | ||||||
|   columns: SortableColumnContainer, |   columns: SortableColumnContainer, | ||||||
|   filter: string |   filter: string | ||||||
| ) => { | ) => { | ||||||
|   filter = stripDiacritics(filter.toLowerCase()); |   filter = stripDiacritics(filter.toLowerCase()); | ||||||
|   return data.filter((row) => |  | ||||||
|     Object.entries(columns).some((columnEntry) => { |  | ||||||
|       const [key, column] = columnEntry; |  | ||||||
|       if (column.filterable) { |  | ||||||
|         const value = String( |  | ||||||
|           column.filterKey |  | ||||||
|             ? row[column.valueColumn || key][column.filterKey] |  | ||||||
|             : row[column.valueColumn || key] |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         if (stripDiacritics(value).toLowerCase().includes(filter)) { |   if (filter === "") { | ||||||
|           return true; |     return data; | ||||||
|         } |   } | ||||||
|       } |  | ||||||
|       return false; |   const index = fuseIndex(data, columns); | ||||||
|     }) |  | ||||||
|  |   const fuse = new HaFuse( | ||||||
|  |     data, | ||||||
|  |     { shouldSort: false, minMatchCharLength: 1 }, | ||||||
|  |     index | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  |   const searchResults = fuse.multiTermsSearch(filter); | ||||||
|  |  | ||||||
|  |   if (searchResults) { | ||||||
|  |     return searchResults.map((result) => result.item); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return data; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const sortData = ( | const sortData = ( | ||||||
|   | |||||||
| @@ -4,11 +4,11 @@ import Vue from "vue"; | |||||||
| import DateRangePicker from "vue2-daterange-picker"; | import DateRangePicker from "vue2-daterange-picker"; | ||||||
| // @ts-ignore | // @ts-ignore | ||||||
| import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css"; | import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css"; | ||||||
| import { fireEvent } from "../common/dom/fire_event"; |  | ||||||
| import { | import { | ||||||
|   localizeWeekdays, |  | ||||||
|   localizeMonths, |   localizeMonths, | ||||||
|  |   localizeWeekdays, | ||||||
| } from "../common/datetime/localize_date"; | } from "../common/datetime/localize_date"; | ||||||
|  | import { fireEvent } from "../common/dom/fire_event"; | ||||||
| import { mainWindow } from "../common/dom/get_main_window"; | import { mainWindow } from "../common/dom/get_main_window"; | ||||||
|  |  | ||||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | // eslint-disable-next-line @typescript-eslint/naming-convention | ||||||
| @@ -177,7 +177,7 @@ class DateRangePickerElement extends WrappedElement { | |||||||
|             top: auto; |             top: auto; | ||||||
|             box-shadow: var(--ha-card-box-shadow, none); |             box-shadow: var(--ha-card-box-shadow, none); | ||||||
|             background-color: var(--card-background-color); |             background-color: var(--card-background-color); | ||||||
|             border-radius: var(--ha-card-border-radius, 12px); |             border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg)); | ||||||
|             border-width: var(--ha-card-border-width, 1px); |             border-width: var(--ha-card-border-width, 1px); | ||||||
|             border-style: solid; |             border-style: solid; | ||||||
|             border-color: var( |             border-color: var( | ||||||
| @@ -203,7 +203,7 @@ class DateRangePickerElement extends WrappedElement { | |||||||
|           .daterangepicker .calendar-table th { |           .daterangepicker .calendar-table th { | ||||||
|             background-color: transparent; |             background-color: transparent; | ||||||
|             color: var(--secondary-text-color); |             color: var(--secondary-text-color); | ||||||
|             border-radius: 0; |             border-radius: var(--ha-border-radius-square); | ||||||
|             outline: none; |             outline: none; | ||||||
|             min-width: 32px; |             min-width: 32px; | ||||||
|             height: 32px; |             height: 32px; | ||||||
| @@ -225,13 +225,13 @@ class DateRangePickerElement extends WrappedElement { | |||||||
|             color: var(--text-primary-color); |             color: var(--text-primary-color); | ||||||
|           } |           } | ||||||
|           .daterangepicker td.start-date.end-date { |           .daterangepicker td.start-date.end-date { | ||||||
|             border-radius: 50%; |             border-radius: var(--ha-border-radius-circle); | ||||||
|           } |           } | ||||||
|           .daterangepicker td.start-date { |           .daterangepicker td.start-date { | ||||||
|             border-radius: 50% 0 0 50%; |             border-radius: var(--ha-border-radius-circle) var(--ha-border-radius-square) var(--ha-border-radius-square) var(--ha-border-radius-circle); | ||||||
|           } |           } | ||||||
|           .daterangepicker td.end-date { |           .daterangepicker td.end-date { | ||||||
|             border-radius: 0 50% 50% 0; |             border-radius: var(--ha-border-radius-square) var(--ha-border-radius-circle) var(--ha-border-radius-circle) var(--ha-border-radius-square); | ||||||
|           } |           } | ||||||
|           .reportrange-text { |           .reportrange-text { | ||||||
|             background: none !important; |             background: none !important; | ||||||
| @@ -265,7 +265,7 @@ class DateRangePickerElement extends WrappedElement { | |||||||
|             border: 1px solid var(--primary-color); |             border: 1px solid var(--primary-color); | ||||||
|             background-color: transparent; |             background-color: transparent; | ||||||
|             color: var(--primary-color); |             color: var(--primary-color); | ||||||
|             border-radius: 4px; |             border-radius: var(--ha-border-radius-sm); | ||||||
|             padding: 8px; |             padding: 8px; | ||||||
|             cursor: pointer; |             cursor: pointer; | ||||||
|           } |           } | ||||||
| @@ -321,10 +321,10 @@ class DateRangePickerElement extends WrappedElement { | |||||||
|               -webkit-transform: rotate(-45deg); |               -webkit-transform: rotate(-45deg); | ||||||
|             } |             } | ||||||
|             .daterangepicker td.start-date { |             .daterangepicker td.start-date { | ||||||
|               border-radius: 0 50% 50% 0; |               border-radius: var(--ha-border-radius-square) var(--ha-border-radius-circle) var(--ha-border-radius-circle) var(--ha-border-radius-square); | ||||||
|             } |             } | ||||||
|             .daterangepicker td.end-date { |             .daterangepicker td.end-date { | ||||||
|               border-radius: 50% 0 0 50%; |               border-radius: var(--ha-border-radius-circle) var(--ha-border-radius-square) var(--ha-border-radius-square) var(--ha-border-radius-circle); | ||||||
|             } |             } | ||||||
|             `; |             `; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -5,24 +5,18 @@ import { customElement, property, query, state } 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 { computeAreaName } from "../../common/entity/compute_area_name"; | import { computeAreaName } from "../../common/entity/compute_area_name"; | ||||||
| import { | import { computeDeviceName } from "../../common/entity/compute_device_name"; | ||||||
|   computeDeviceName, |  | ||||||
|   computeDeviceNameDisplay, |  | ||||||
| } from "../../common/entity/compute_device_name"; |  | ||||||
| import { computeDomain } from "../../common/entity/compute_domain"; |  | ||||||
| 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 { | ||||||
|   getDeviceEntityDisplayLookup, |   getDevices, | ||||||
|   type DeviceEntityDisplayLookup, |   type DevicePickerItem, | ||||||
|   type DeviceRegistryEntry, |   type DeviceRegistryEntry, | ||||||
| } from "../../data/device_registry"; | } from "../../data/device_registry"; | ||||||
| import { domainToName } from "../../data/integration"; |  | ||||||
| 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"; | ||||||
| import type { HaGenericPicker } from "../ha-generic-picker"; | import type { HaGenericPicker } from "../ha-generic-picker"; | ||||||
| import type { PickerComboBoxItem } from "../ha-picker-combo-box"; |  | ||||||
|  |  | ||||||
| export type HaDevicePickerDeviceFilterFunc = ( | export type HaDevicePickerDeviceFilterFunc = ( | ||||||
|   device: DeviceRegistryEntry |   device: DeviceRegistryEntry | ||||||
| @@ -30,11 +24,6 @@ export type HaDevicePickerDeviceFilterFunc = ( | |||||||
|  |  | ||||||
| export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean; | export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean; | ||||||
|  |  | ||||||
| interface DevicePickerItem extends PickerComboBoxItem { |  | ||||||
|   domain?: string; |  | ||||||
|   domain_name?: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @customElement("ha-device-picker") | @customElement("ha-device-picker") | ||||||
| export class HaDevicePicker extends LitElement { | export class HaDevicePicker extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -104,6 +93,8 @@ export class HaDevicePicker extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _configEntryLookup: Record<string, ConfigEntry> = {}; |   @state() private _configEntryLookup: Record<string, ConfigEntry> = {}; | ||||||
|  |  | ||||||
|  |   private _getDevicesMemoized = memoizeOne(getDevices); | ||||||
|  |  | ||||||
|   protected firstUpdated(_changedProperties: PropertyValues): void { |   protected firstUpdated(_changedProperties: PropertyValues): void { | ||||||
|     super.firstUpdated(_changedProperties); |     super.firstUpdated(_changedProperties); | ||||||
|     this._loadConfigEntries(); |     this._loadConfigEntries(); | ||||||
| @@ -117,162 +108,18 @@ export class HaDevicePicker extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _getItems = () => |   private _getItems = () => | ||||||
|     this._getDevices( |     this._getDevicesMemoized( | ||||||
|       this.hass.devices, |       this.hass, | ||||||
|       this.hass.entities, |  | ||||||
|       this._configEntryLookup, |       this._configEntryLookup, | ||||||
|       this.includeDomains, |       this.includeDomains, | ||||||
|       this.excludeDomains, |       this.excludeDomains, | ||||||
|       this.includeDeviceClasses, |       this.includeDeviceClasses, | ||||||
|       this.deviceFilter, |       this.deviceFilter, | ||||||
|       this.entityFilter, |       this.entityFilter, | ||||||
|       this.excludeDevices |       this.excludeDevices, | ||||||
|  |       this.value | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|   private _getDevices = memoizeOne( |  | ||||||
|     ( |  | ||||||
|       haDevices: HomeAssistant["devices"], |  | ||||||
|       haEntities: HomeAssistant["entities"], |  | ||||||
|       configEntryLookup: Record<string, ConfigEntry>, |  | ||||||
|       includeDomains: this["includeDomains"], |  | ||||||
|       excludeDomains: this["excludeDomains"], |  | ||||||
|       includeDeviceClasses: this["includeDeviceClasses"], |  | ||||||
|       deviceFilter: this["deviceFilter"], |  | ||||||
|       entityFilter: this["entityFilter"], |  | ||||||
|       excludeDevices: this["excludeDevices"] |  | ||||||
|     ): DevicePickerItem[] => { |  | ||||||
|       const devices = Object.values(haDevices); |  | ||||||
|       const entities = Object.values(haEntities); |  | ||||||
|  |  | ||||||
|       let deviceEntityLookup: DeviceEntityDisplayLookup = {}; |  | ||||||
|  |  | ||||||
|       if ( |  | ||||||
|         includeDomains || |  | ||||||
|         excludeDomains || |  | ||||||
|         includeDeviceClasses || |  | ||||||
|         entityFilter |  | ||||||
|       ) { |  | ||||||
|         deviceEntityLookup = getDeviceEntityDisplayLookup(entities); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       let inputDevices = devices.filter( |  | ||||||
|         (device) => device.id === this.value || !device.disabled_by |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       if (includeDomains) { |  | ||||||
|         inputDevices = inputDevices.filter((device) => { |  | ||||||
|           const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|           if (!devEntities || !devEntities.length) { |  | ||||||
|             return false; |  | ||||||
|           } |  | ||||||
|           return deviceEntityLookup[device.id].some((entity) => |  | ||||||
|             includeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|           ); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (excludeDomains) { |  | ||||||
|         inputDevices = inputDevices.filter((device) => { |  | ||||||
|           const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|           if (!devEntities || !devEntities.length) { |  | ||||||
|             return true; |  | ||||||
|           } |  | ||||||
|           return entities.every( |  | ||||||
|             (entity) => |  | ||||||
|               !excludeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|           ); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (excludeDevices) { |  | ||||||
|         inputDevices = inputDevices.filter( |  | ||||||
|           (device) => !excludeDevices!.includes(device.id) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (includeDeviceClasses) { |  | ||||||
|         inputDevices = inputDevices.filter((device) => { |  | ||||||
|           const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|           if (!devEntities || !devEntities.length) { |  | ||||||
|             return false; |  | ||||||
|           } |  | ||||||
|           return deviceEntityLookup[device.id].some((entity) => { |  | ||||||
|             const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|             if (!stateObj) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return ( |  | ||||||
|               stateObj.attributes.device_class && |  | ||||||
|               includeDeviceClasses.includes(stateObj.attributes.device_class) |  | ||||||
|             ); |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (entityFilter) { |  | ||||||
|         inputDevices = inputDevices.filter((device) => { |  | ||||||
|           const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|           if (!devEntities || !devEntities.length) { |  | ||||||
|             return false; |  | ||||||
|           } |  | ||||||
|           return devEntities.some((entity) => { |  | ||||||
|             const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|             if (!stateObj) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return entityFilter(stateObj); |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (deviceFilter) { |  | ||||||
|         inputDevices = inputDevices.filter( |  | ||||||
|           (device) => |  | ||||||
|             // We always want to include the device of the current value |  | ||||||
|             device.id === this.value || deviceFilter!(device) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const outputDevices = inputDevices.map<DevicePickerItem>((device) => { |  | ||||||
|         const deviceName = computeDeviceNameDisplay( |  | ||||||
|           device, |  | ||||||
|           this.hass, |  | ||||||
|           deviceEntityLookup[device.id] |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         const { area } = getDeviceContext(device, this.hass); |  | ||||||
|  |  | ||||||
|         const areaName = area ? computeAreaName(area) : undefined; |  | ||||||
|  |  | ||||||
|         const configEntry = device.primary_config_entry |  | ||||||
|           ? configEntryLookup?.[device.primary_config_entry] |  | ||||||
|           : undefined; |  | ||||||
|  |  | ||||||
|         const domain = configEntry?.domain; |  | ||||||
|         const domainName = domain |  | ||||||
|           ? domainToName(this.hass.localize, domain) |  | ||||||
|           : undefined; |  | ||||||
|  |  | ||||||
|         return { |  | ||||||
|           id: device.id, |  | ||||||
|           label: "", |  | ||||||
|           primary: |  | ||||||
|             deviceName || |  | ||||||
|             this.hass.localize("ui.components.device-picker.unnamed_device"), |  | ||||||
|           secondary: areaName, |  | ||||||
|           domain: configEntry?.domain, |  | ||||||
|           domain_name: domainName, |  | ||||||
|           search_labels: [deviceName, areaName, domain, domainName].filter( |  | ||||||
|             Boolean |  | ||||||
|           ) as string[], |  | ||||||
|           sorting_label: deviceName || "zzz", |  | ||||||
|         }; |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       return outputDevices; |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   private _valueRenderer = memoizeOne( |   private _valueRenderer = memoizeOne( | ||||||
|     (configEntriesLookup: Record<string, ConfigEntry>) => (value: string) => { |     (configEntriesLookup: Record<string, ConfigEntry>) => (value: string) => { | ||||||
|       const deviceId = value; |       const deviceId = value; | ||||||
|   | |||||||
| @@ -1,13 +1,13 @@ | |||||||
| import { mdiDrag } from "@mdi/js"; | import { mdiDragHorizontalVariant } from "@mdi/js"; | ||||||
| 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 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 { HomeAssistant, ValueChangedEvent } from "../../types"; | import type { HomeAssistant, ValueChangedEvent } from "../../types"; | ||||||
| import "../ha-sortable"; | import "../ha-sortable"; | ||||||
| import "./ha-entity-picker"; | import "./ha-entity-picker"; | ||||||
| import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker"; |  | ||||||
|  |  | ||||||
| @customElement("ha-entities-picker") | @customElement("ha-entities-picker") | ||||||
| class HaEntitiesPicker extends LitElement { | class HaEntitiesPicker extends LitElement { | ||||||
| @@ -118,7 +118,7 @@ class HaEntitiesPicker extends LitElement { | |||||||
|                   ? html` |                   ? html` | ||||||
|                       <ha-svg-icon |                       <ha-svg-icon | ||||||
|                         class="entity-handle" |                         class="entity-handle" | ||||||
|                         .path=${mdiDrag} |                         .path=${mdiDragHorizontalVariant} | ||||||
|                       ></ha-svg-icon> |                       ></ha-svg-icon> | ||||||
|                     ` |                     ` | ||||||
|                   : nothing} |                   : nothing} | ||||||
| @@ -147,6 +147,7 @@ class HaEntitiesPicker extends LitElement { | |||||||
|           .createDomains=${this.createDomains} |           .createDomains=${this.createDomains} | ||||||
|           .required=${this.required && !currentEntities.length} |           .required=${this.required && !currentEntities.length} | ||||||
|           @value-changed=${this._addEntity} |           @value-changed=${this._addEntity} | ||||||
|  |           add-button | ||||||
|         ></ha-entity-picker> |         ></ha-entity-picker> | ||||||
|       </div> |       </div> | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import type { HassEntity } from "home-assistant-js-websocket"; |  | ||||||
| import type { PropertyValues } from "lit"; | 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, query, state } from "lit/decorators"; | ||||||
| @@ -8,8 +7,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types"; | |||||||
| import "../ha-combo-box"; | import "../ha-combo-box"; | ||||||
| import type { HaComboBox } from "../ha-combo-box"; | import type { HaComboBox } from "../ha-combo-box"; | ||||||
|  |  | ||||||
| export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; |  | ||||||
|  |  | ||||||
| interface AttributeOption { | interface AttributeOption { | ||||||
|   value: string; |   value: string; | ||||||
|   label: string; |   label: string; | ||||||
|   | |||||||
							
								
								
									
										536
									
								
								src/components/entity/ha-entity-name-picker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										536
									
								
								src/components/entity/ha-entity-name-picker.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,536 @@ | |||||||
|  | import "@material/mwc-menu/mwc-menu-surface"; | ||||||
|  | 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 { css, html, LitElement, nothing } from "lit"; | ||||||
|  | import { customElement, property, query, state } from "lit/decorators"; | ||||||
|  | import { repeat } from "lit/directives/repeat"; | ||||||
|  | import memoizeOne from "memoize-one"; | ||||||
|  | import { ensureArray } from "../../common/array/ensure-array"; | ||||||
|  | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
|  | import { stopPropagation } from "../../common/dom/stop_propagation"; | ||||||
|  | import type { EntityNameItem } from "../../common/entity/compute_entity_name_display"; | ||||||
|  | import { getEntityContext } from "../../common/entity/context/get_entity_context"; | ||||||
|  | import type { EntityNameType } from "../../common/translations/entity-state"; | ||||||
|  | import type { LocalizeKeys } from "../../common/translations/localize"; | ||||||
|  | import type { HomeAssistant, ValueChangedEvent } from "../../types"; | ||||||
|  | import "../chips/ha-assist-chip"; | ||||||
|  | import "../chips/ha-chip-set"; | ||||||
|  | import "../chips/ha-input-chip"; | ||||||
|  | import "../ha-combo-box"; | ||||||
|  | import type { HaComboBox } from "../ha-combo-box"; | ||||||
|  | import "../ha-input-helper-text"; | ||||||
|  | import "../ha-sortable"; | ||||||
|  |  | ||||||
|  | interface EntityNameOption { | ||||||
|  |   primary: string; | ||||||
|  |   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> | ||||||
|  |     ${item.secondary | ||||||
|  |       ? html`<span slot="supporting-text">${item.secondary}</span>` | ||||||
|  |       : nothing} | ||||||
|  |   </ha-combo-box-item> | ||||||
|  | `; | ||||||
|  |  | ||||||
|  | const KNOWN_TYPES = new Set(["entity", "device", "area", "floor"]); | ||||||
|  |  | ||||||
|  | const UNIQUE_TYPES = new Set(["entity", "device", "area", "floor"]); | ||||||
|  |  | ||||||
|  | const formatOptionValue = (item: EntityNameItem) => { | ||||||
|  |   if (item.type === "text" && item.text) { | ||||||
|  |     return item.text; | ||||||
|  |   } | ||||||
|  |   return `___${item.type}___`; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const parseOptionValue = (value: string): EntityNameItem => { | ||||||
|  |   if (value.startsWith("___") && value.endsWith("___")) { | ||||||
|  |     const type = value.slice(3, -3); | ||||||
|  |     if (KNOWN_TYPES.has(type)) { | ||||||
|  |       return { type: type as EntityNameType }; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return { type: "text", text: value }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @customElement("ha-entity-name-picker") | ||||||
|  | export class HaEntityNamePicker extends LitElement { | ||||||
|  |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) public entityId?: string; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) public value?: | ||||||
|  |     | string | ||||||
|  |     | EntityNameItem | ||||||
|  |     | EntityNameItem[]; | ||||||
|  |  | ||||||
|  |   @property() public label?: string; | ||||||
|  |  | ||||||
|  |   @property() public helper?: string; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public required = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, reflect: true }) public disabled = false; | ||||||
|  |  | ||||||
|  |   @query(".container", true) private _container?: HTMLDivElement; | ||||||
|  |  | ||||||
|  |   @query("ha-combo-box", true) private _comboBox!: HaComboBox; | ||||||
|  |  | ||||||
|  |   @state() private _opened = false; | ||||||
|  |  | ||||||
|  |   private _editIndex?: number; | ||||||
|  |  | ||||||
|  |   private _validTypes = memoizeOne((entityId?: string) => { | ||||||
|  |     const options = new Set<string>(["text"]); | ||||||
|  |     if (!entityId) { | ||||||
|  |       return options; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const stateObj = this.hass.states[entityId]; | ||||||
|  |  | ||||||
|  |     if (!stateObj) { | ||||||
|  |       return options; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     options.add("entity"); | ||||||
|  |  | ||||||
|  |     const context = getEntityContext( | ||||||
|  |       stateObj, | ||||||
|  |       this.hass.entities, | ||||||
|  |       this.hass.devices, | ||||||
|  |       this.hass.areas, | ||||||
|  |       this.hass.floors | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (context.device) options.add("device"); | ||||||
|  |     if (context.area) options.add("area"); | ||||||
|  |     if (context.floor) options.add("floor"); | ||||||
|  |     return options; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   private _getOptions = memoizeOne((entityId?: string) => { | ||||||
|  |     if (!entityId) { | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const types = this._validTypes(entityId); | ||||||
|  |  | ||||||
|  |     const items = ( | ||||||
|  |       ["entity", "device", "area", "floor"] as const | ||||||
|  |     ).map<EntityNameOption>((name) => { | ||||||
|  |       const stateObj = this.hass.states[entityId]; | ||||||
|  |       const isValid = types.has(name); | ||||||
|  |       const primary = this.hass.localize( | ||||||
|  |         `ui.components.entity.entity-name-picker.types.${name}` | ||||||
|  |       ); | ||||||
|  |       const secondary = | ||||||
|  |         (stateObj && isValid | ||||||
|  |           ? this.hass.formatEntityName(stateObj, { type: name }) | ||||||
|  |           : this.hass.localize( | ||||||
|  |               `ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys | ||||||
|  |             )) || "-"; | ||||||
|  |  | ||||||
|  |       return { | ||||||
|  |         primary, | ||||||
|  |         secondary, | ||||||
|  |         field_label: primary, | ||||||
|  |         value: formatOptionValue({ type: name }), | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return items; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   private _customNameOption = memoizeOne((text: string) => ({ | ||||||
|  |     primary: this.hass.localize( | ||||||
|  |       "ui.components.entity.entity-name-picker.custom_name" | ||||||
|  |     ), | ||||||
|  |     secondary: `"${text}"`, | ||||||
|  |     field_label: text, | ||||||
|  |     value: formatOptionValue({ type: "text", text }), | ||||||
|  |   })); | ||||||
|  |  | ||||||
|  |   private _formatItem = (item: EntityNameItem) => { | ||||||
|  |     if (item.type === "text") { | ||||||
|  |       return `"${item.text}"`; | ||||||
|  |     } | ||||||
|  |     if (KNOWN_TYPES.has(item.type)) { | ||||||
|  |       return this.hass.localize( | ||||||
|  |         `ui.components.entity.entity-name-picker.types.${item.type as EntityNameType}` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return item.type; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   protected render() { | ||||||
|  |     const value = this._items; | ||||||
|  |     const options = this._getOptions(this.entityId); | ||||||
|  |     const validTypes = this._validTypes(this.entityId); | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       ${this.label ? html`<label>${this.label}</label>` : nothing} | ||||||
|  |       <div class="container"> | ||||||
|  |         <ha-sortable | ||||||
|  |           no-style | ||||||
|  |           @item-moved=${this._moveItem} | ||||||
|  |           .disabled=${this.disabled} | ||||||
|  |           handle-selector="button.primary.action" | ||||||
|  |           filter=".add" | ||||||
|  |         > | ||||||
|  |           <ha-chip-set> | ||||||
|  |             ${repeat( | ||||||
|  |               this._items, | ||||||
|  |               (item) => item, | ||||||
|  |               (item: EntityNameItem, idx) => { | ||||||
|  |                 const label = this._formatItem(item); | ||||||
|  |                 const isValid = validTypes.has(item.type); | ||||||
|  |                 return html` | ||||||
|  |                   <ha-input-chip | ||||||
|  |                     data-idx=${idx} | ||||||
|  |                     @remove=${this._removeItem} | ||||||
|  |                     @click=${this._editItem} | ||||||
|  |                     .label=${label} | ||||||
|  |                     .selected=${!this.disabled} | ||||||
|  |                     .disabled=${this.disabled} | ||||||
|  |                     class=${!isValid ? "invalid" : ""} | ||||||
|  |                   > | ||||||
|  |                     <ha-svg-icon | ||||||
|  |                       slot="icon" | ||||||
|  |                       .path=${mdiDragHorizontalVariant} | ||||||
|  |                     ></ha-svg-icon> | ||||||
|  |                     <span>${label}</span> | ||||||
|  |                   </ha-input-chip> | ||||||
|  |                 `; | ||||||
|  |               } | ||||||
|  |             )} | ||||||
|  |             ${this.disabled | ||||||
|  |               ? nothing | ||||||
|  |               : html` | ||||||
|  |                   <ha-assist-chip | ||||||
|  |                     @click=${this._addItem} | ||||||
|  |                     .disabled=${this.disabled} | ||||||
|  |                     label=${this.hass.localize( | ||||||
|  |                       "ui.components.entity.entity-name-picker.add" | ||||||
|  |                     )} | ||||||
|  |                     class="add" | ||||||
|  |                   > | ||||||
|  |                     <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|  |                   </ha-assist-chip> | ||||||
|  |                 `} | ||||||
|  |           </ha-chip-set> | ||||||
|  |         </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> | ||||||
|  |       ${this._renderHelper()} | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _renderHelper() { | ||||||
|  |     return this.helper | ||||||
|  |       ? html` | ||||||
|  |           <ha-input-helper-text .disabled=${this.disabled}> | ||||||
|  |             ${this.helper} | ||||||
|  |           </ha-input-helper-text> | ||||||
|  |         ` | ||||||
|  |       : nothing; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _onClosed(ev) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     this._opened = false; | ||||||
|  |     this._editIndex = undefined; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _onOpened(ev) { | ||||||
|  |     if (!this._opened) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     this._opened = true; | ||||||
|  |     await this._comboBox?.focus(); | ||||||
|  |     await this._comboBox?.open(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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._opened = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private get _items(): EntityNameItem[] { | ||||||
|  |     return this._toItems(this.value); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _toItems = memoizeOne((value?: typeof this.value) => { | ||||||
|  |     if (typeof value === "string") { | ||||||
|  |       if (value === "") { | ||||||
|  |         return []; | ||||||
|  |       } | ||||||
|  |       return [{ type: "text", text: value } satisfies EntityNameItem]; | ||||||
|  |     } | ||||||
|  |     return value ? ensureArray(value) : []; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   private _toValue = memoizeOne( | ||||||
|  |     (items: EntityNameItem[]): typeof this.value => { | ||||||
|  |       if (items.length === 0) { | ||||||
|  |         return ""; | ||||||
|  |       } | ||||||
|  |       if (items.length === 1) { | ||||||
|  |         const item = items[0]; | ||||||
|  |         return item.type === "text" ? item.text : item; | ||||||
|  |       } | ||||||
|  |       return items; | ||||||
|  |     } | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   private _openedChanged(ev: ValueChangedEvent<boolean>) { | ||||||
|  |     const open = ev.detail.value; | ||||||
|  |     if (open) { | ||||||
|  |       const options = this._comboBox.items || []; | ||||||
|  |  | ||||||
|  |       const initialItem = | ||||||
|  |         this._editIndex != null ? this._items[this._editIndex] : undefined; | ||||||
|  |  | ||||||
|  |       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( | ||||||
|  |       items | ||||||
|  |         .filter((item) => UNIQUE_TYPES.has(item.type)) | ||||||
|  |         .map((item) => formatOptionValue(item)) | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const filteredOptions = options.filter( | ||||||
|  |       (option) => !excludedValues.has(option.value) || option.value === current | ||||||
|  |     ); | ||||||
|  |     return filteredOptions; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private _filterChanged(ev: ValueChangedEvent<string>) { | ||||||
|  |     const input = ev.detail.value; | ||||||
|  |     const filter = input?.toLowerCase() || ""; | ||||||
|  |     const options = this._comboBox.items || []; | ||||||
|  |  | ||||||
|  |     const currentItem = | ||||||
|  |       this._editIndex != null ? this._items[this._editIndex] : undefined; | ||||||
|  |  | ||||||
|  |     const currentValue = currentItem ? formatOptionValue(currentItem) : ""; | ||||||
|  |  | ||||||
|  |     let filteredItems = this._filterSelectedOptions(options, currentValue); | ||||||
|  |  | ||||||
|  |     if (!filter) { | ||||||
|  |       this._comboBox.filteredItems = filteredItems; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const fuseOptions: IFuseOptions<EntityNameOption> = { | ||||||
|  |       keys: ["primary", "secondary", "value"], | ||||||
|  |       isCaseSensitive: false, | ||||||
|  |       minMatchCharLength: Math.min(filter.length, 2), | ||||||
|  |       threshold: 0.2, | ||||||
|  |       ignoreDiacritics: true, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const fuse = new Fuse(filteredItems, fuseOptions); | ||||||
|  |     filteredItems = fuse.search(filter).map((result) => result.item); | ||||||
|  |     filteredItems.push(this._customNameOption(input)); | ||||||
|  |     this._comboBox.filteredItems = filteredItems; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _moveItem(ev: CustomEvent) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     const { oldIndex, newIndex } = ev.detail; | ||||||
|  |     const value = this._items; | ||||||
|  |     const newValue = value.concat(); | ||||||
|  |     const element = newValue.splice(oldIndex, 1)[0]; | ||||||
|  |     newValue.splice(newIndex, 0, element); | ||||||
|  |     this._setValue(newValue); | ||||||
|  |     await this.updateComplete; | ||||||
|  |     this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _removeItem(ev) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     const value = [...this._items]; | ||||||
|  |     const idx = parseInt(ev.target.dataset.idx, 10); | ||||||
|  |     value.splice(idx, 1); | ||||||
|  |     this._setValue(value); | ||||||
|  |     await this.updateComplete; | ||||||
|  |     this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     const value = ev.detail.value; | ||||||
|  |  | ||||||
|  |     if (this.disabled || value === "") { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const item: EntityNameItem = parseOptionValue(value); | ||||||
|  |  | ||||||
|  |     const newValue = [...this._items]; | ||||||
|  |  | ||||||
|  |     if (this._editIndex != null) { | ||||||
|  |       newValue[this._editIndex] = item; | ||||||
|  |     } else { | ||||||
|  |       newValue.push(item); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this._setValue(newValue); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _setValue(value: EntityNameItem[]) { | ||||||
|  |     const newValue = this._toValue(value); | ||||||
|  |     this.value = newValue; | ||||||
|  |     fireEvent(this, "value-changed", { | ||||||
|  |       value: newValue, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static styles = css` | ||||||
|  |     :host { | ||||||
|  |       position: relative; | ||||||
|  |       width: 100%; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .container { | ||||||
|  |       position: relative; | ||||||
|  |       background-color: var(--mdc-text-field-fill-color, whitesmoke); | ||||||
|  |       border-radius: var(--ha-border-radius-sm); | ||||||
|  |       border-end-end-radius: var(--ha-border-radius-square); | ||||||
|  |       border-end-start-radius: var(--ha-border-radius-square); | ||||||
|  |     } | ||||||
|  |     .container:after { | ||||||
|  |       display: block; | ||||||
|  |       content: ""; | ||||||
|  |       position: absolute; | ||||||
|  |       pointer-events: none; | ||||||
|  |       bottom: 0; | ||||||
|  |       left: 0; | ||||||
|  |       right: 0; | ||||||
|  |       height: 1px; | ||||||
|  |       width: 100%; | ||||||
|  |       background-color: var( | ||||||
|  |         --mdc-text-field-idle-line-color, | ||||||
|  |         rgba(0, 0, 0, 0.42) | ||||||
|  |       ); | ||||||
|  |       transform: | ||||||
|  |         height 180ms ease-in-out, | ||||||
|  |         background-color 180ms ease-in-out; | ||||||
|  |     } | ||||||
|  |     :host([disabled]) .container:after { | ||||||
|  |       background-color: var( | ||||||
|  |         --mdc-text-field-disabled-line-color, | ||||||
|  |         rgba(0, 0, 0, 0.42) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     .container:focus-within:after { | ||||||
|  |       height: 2px; | ||||||
|  |       background-color: var(--mdc-theme-primary); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     label { | ||||||
|  |       display: block; | ||||||
|  |       margin: 0 0 var(--ha-space-2); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .add { | ||||||
|  |       order: 1; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     mwc-menu-surface { | ||||||
|  |       --mdc-menu-min-width: 100%; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     ha-chip-set { | ||||||
|  |       padding: var(--ha-space-2) var(--ha-space-2); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .invalid { | ||||||
|  |       text-decoration: line-through; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .sortable-fallback { | ||||||
|  |       display: none; | ||||||
|  |       opacity: 0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .sortable-ghost { | ||||||
|  |       opacity: 0.4; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .sortable-drag { | ||||||
|  |       cursor: grabbing; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     ha-input-helper-text { | ||||||
|  |       display: block; | ||||||
|  |       margin: var(--ha-space-2) 0 0; | ||||||
|  |     } | ||||||
|  |   `; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-entity-name-picker": HaEntityNamePicker; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,14 +1,17 @@ | |||||||
| import { mdiPlus, mdiShape } from "@mdi/js"; | import { mdiPlus, mdiShape } from "@mdi/js"; | ||||||
| import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; | import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; | ||||||
| import type { HassEntity } from "home-assistant-js-websocket"; |  | ||||||
| import { html, LitElement, nothing, type PropertyValues } from "lit"; | import { 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"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import { computeDomain } from "../../common/entity/compute_domain"; | import { computeEntityNameList } from "../../common/entity/compute_entity_name_display"; | ||||||
| import { computeStateName } from "../../common/entity/compute_state_name"; |  | ||||||
| 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 { | ||||||
|  |   getEntities, | ||||||
|  |   type EntityComboBoxItem, | ||||||
|  | } from "../../data/entity_registry"; | ||||||
| import { domainToName } from "../../data/integration"; | import { domainToName } from "../../data/integration"; | ||||||
| import { | import { | ||||||
|   isHelperDomain, |   isHelperDomain, | ||||||
| @@ -19,21 +22,11 @@ import type { HomeAssistant } from "../../types"; | |||||||
| import "../ha-combo-box-item"; | import "../ha-combo-box-item"; | ||||||
| import "../ha-generic-picker"; | import "../ha-generic-picker"; | ||||||
| import type { HaGenericPicker } from "../ha-generic-picker"; | import type { HaGenericPicker } from "../ha-generic-picker"; | ||||||
| import type { | import type { PickerComboBoxSearchFn } from "../ha-picker-combo-box"; | ||||||
|   PickerComboBoxItem, |  | ||||||
|   PickerComboBoxSearchFn, |  | ||||||
| } from "../ha-picker-combo-box"; |  | ||||||
| import type { PickerValueRenderer } from "../ha-picker-field"; | import type { PickerValueRenderer } from "../ha-picker-field"; | ||||||
| import "../ha-svg-icon"; | import "../ha-svg-icon"; | ||||||
| import "./state-badge"; | import "./state-badge"; | ||||||
|  |  | ||||||
| interface EntityComboBoxItem extends PickerComboBoxItem { |  | ||||||
|   domain_name?: string; |  | ||||||
|   stateObj?: HassEntity; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean; |  | ||||||
|  |  | ||||||
| const CREATE_ID = "___create-new-entity___"; | const CREATE_ID = "___create-new-entity___"; | ||||||
|  |  | ||||||
| @customElement("ha-entity-picker") | @customElement("ha-entity-picker") | ||||||
| @@ -120,6 +113,9 @@ export class HaEntityPicker extends LitElement { | |||||||
|   @property({ attribute: "hide-clear-icon", type: Boolean }) |   @property({ attribute: "hide-clear-icon", type: Boolean }) | ||||||
|   public hideClearIcon = false; |   public hideClearIcon = false; | ||||||
|  |  | ||||||
|  |   @property({ attribute: "add-button", type: Boolean }) | ||||||
|  |   public addButton = false; | ||||||
|  |  | ||||||
|   @query("ha-generic-picker") private _picker?: HaGenericPicker; |   @query("ha-generic-picker") private _picker?: HaGenericPicker; | ||||||
|  |  | ||||||
|   protected firstUpdated(changedProperties: PropertyValues): void { |   protected firstUpdated(changedProperties: PropertyValues): void { | ||||||
| @@ -144,9 +140,14 @@ export class HaEntityPicker extends LitElement { | |||||||
|       `; |       `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const entityName = this.hass.formatEntityName(stateObj, "entity"); |     const [entityName, deviceName, areaName] = computeEntityNameList( | ||||||
|     const deviceName = this.hass.formatEntityName(stateObj, "device"); |       stateObj, | ||||||
|     const areaName = this.hass.formatEntityName(stateObj, "area"); |       [{ type: "entity" }, { type: "device" }, { type: "area" }], | ||||||
|  |       this.hass.entities, | ||||||
|  |       this.hass.devices, | ||||||
|  |       this.hass.areas, | ||||||
|  |       this.hass.floors | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     const isRTL = computeRTL(this.hass); |     const isRTL = computeRTL(this.hass); | ||||||
|  |  | ||||||
| @@ -249,8 +250,10 @@ export class HaEntityPicker extends LitElement { | |||||||
|     } |     } | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  |   private _getEntitiesMemoized = memoizeOne(getEntities); | ||||||
|  |  | ||||||
|   private _getItems = () => |   private _getItems = () => | ||||||
|     this._getEntities( |     this._getEntitiesMemoized( | ||||||
|       this.hass, |       this.hass, | ||||||
|       this.includeDomains, |       this.includeDomains, | ||||||
|       this.excludeDomains, |       this.excludeDomains, | ||||||
| @@ -258,125 +261,10 @@ export class HaEntityPicker extends LitElement { | |||||||
|       this.includeDeviceClasses, |       this.includeDeviceClasses, | ||||||
|       this.includeUnitOfMeasurement, |       this.includeUnitOfMeasurement, | ||||||
|       this.includeEntities, |       this.includeEntities, | ||||||
|       this.excludeEntities |       this.excludeEntities, | ||||||
|  |       this.value | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|   private _getEntities = memoizeOne( |  | ||||||
|     ( |  | ||||||
|       hass: this["hass"], |  | ||||||
|       includeDomains: this["includeDomains"], |  | ||||||
|       excludeDomains: this["excludeDomains"], |  | ||||||
|       entityFilter: this["entityFilter"], |  | ||||||
|       includeDeviceClasses: this["includeDeviceClasses"], |  | ||||||
|       includeUnitOfMeasurement: this["includeUnitOfMeasurement"], |  | ||||||
|       includeEntities: this["includeEntities"], |  | ||||||
|       excludeEntities: this["excludeEntities"] |  | ||||||
|     ): EntityComboBoxItem[] => { |  | ||||||
|       let items: EntityComboBoxItem[] = []; |  | ||||||
|  |  | ||||||
|       let entityIds = Object.keys(hass.states); |  | ||||||
|  |  | ||||||
|       if (includeEntities) { |  | ||||||
|         entityIds = entityIds.filter((entityId) => |  | ||||||
|           includeEntities.includes(entityId) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (excludeEntities) { |  | ||||||
|         entityIds = entityIds.filter( |  | ||||||
|           (entityId) => !excludeEntities.includes(entityId) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (includeDomains) { |  | ||||||
|         entityIds = entityIds.filter((eid) => |  | ||||||
|           includeDomains.includes(computeDomain(eid)) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (excludeDomains) { |  | ||||||
|         entityIds = entityIds.filter( |  | ||||||
|           (eid) => !excludeDomains.includes(computeDomain(eid)) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const isRTL = computeRTL(this.hass); |  | ||||||
|  |  | ||||||
|       items = entityIds.map<EntityComboBoxItem>((entityId) => { |  | ||||||
|         const stateObj = hass!.states[entityId]; |  | ||||||
|  |  | ||||||
|         const friendlyName = computeStateName(stateObj); // Keep this for search |  | ||||||
|         const entityName = this.hass.formatEntityName(stateObj, "entity"); |  | ||||||
|         const deviceName = this.hass.formatEntityName(stateObj, "device"); |  | ||||||
|         const areaName = this.hass.formatEntityName(stateObj, "area"); |  | ||||||
|  |  | ||||||
|         const domainName = domainToName( |  | ||||||
|           this.hass.localize, |  | ||||||
|           computeDomain(entityId) |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         const primary = entityName || deviceName || entityId; |  | ||||||
|         const secondary = [areaName, entityName ? deviceName : undefined] |  | ||||||
|           .filter(Boolean) |  | ||||||
|           .join(isRTL ? " ◂ " : " ▸ "); |  | ||||||
|         const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - "); |  | ||||||
|  |  | ||||||
|         return { |  | ||||||
|           id: entityId, |  | ||||||
|           primary: primary, |  | ||||||
|           secondary: secondary, |  | ||||||
|           domain_name: domainName, |  | ||||||
|           sorting_label: [deviceName, entityName].filter(Boolean).join("_"), |  | ||||||
|           search_labels: [ |  | ||||||
|             entityName, |  | ||||||
|             deviceName, |  | ||||||
|             areaName, |  | ||||||
|             domainName, |  | ||||||
|             friendlyName, |  | ||||||
|             entityId, |  | ||||||
|           ].filter(Boolean) as string[], |  | ||||||
|           a11y_label: a11yLabel, |  | ||||||
|           stateObj: stateObj, |  | ||||||
|         }; |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       if (includeDeviceClasses) { |  | ||||||
|         items = items.filter( |  | ||||||
|           (item) => |  | ||||||
|             // We always want to include the entity of the current value |  | ||||||
|             item.id === this.value || |  | ||||||
|             (item.stateObj?.attributes.device_class && |  | ||||||
|               includeDeviceClasses.includes( |  | ||||||
|                 item.stateObj.attributes.device_class |  | ||||||
|               )) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (includeUnitOfMeasurement) { |  | ||||||
|         items = items.filter( |  | ||||||
|           (item) => |  | ||||||
|             // We always want to include the entity of the current value |  | ||||||
|             item.id === this.value || |  | ||||||
|             (item.stateObj?.attributes.unit_of_measurement && |  | ||||||
|               includeUnitOfMeasurement.includes( |  | ||||||
|                 item.stateObj.attributes.unit_of_measurement |  | ||||||
|               )) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (entityFilter) { |  | ||||||
|         items = items.filter( |  | ||||||
|           (item) => |  | ||||||
|             // We always want to include the entity of the current value |  | ||||||
|             item.id === this.value || |  | ||||||
|             (item.stateObj && entityFilter!(item.stateObj)) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return items; |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
|     const placeholder = |     const placeholder = | ||||||
|       this.placeholder ?? |       this.placeholder ?? | ||||||
| @@ -396,7 +284,7 @@ export class HaEntityPicker extends LitElement { | |||||||
|         .searchLabel=${this.searchLabel} |         .searchLabel=${this.searchLabel} | ||||||
|         .notFoundLabel=${notFoundLabel} |         .notFoundLabel=${notFoundLabel} | ||||||
|         .placeholder=${placeholder} |         .placeholder=${placeholder} | ||||||
|         .value=${this.value} |         .value=${this.addButton ? undefined : this.value} | ||||||
|         .rowRenderer=${this._rowRenderer} |         .rowRenderer=${this._rowRenderer} | ||||||
|         .getItems=${this._getItems} |         .getItems=${this._getItems} | ||||||
|         .getAdditionalItems=${this._getAdditionalItems} |         .getAdditionalItems=${this._getAdditionalItems} | ||||||
| @@ -404,6 +292,9 @@ export class HaEntityPicker extends LitElement { | |||||||
|         .searchFn=${this._searchFn} |         .searchFn=${this._searchFn} | ||||||
|         .valueRenderer=${this._valueRenderer} |         .valueRenderer=${this._valueRenderer} | ||||||
|         @value-changed=${this._valueChanged} |         @value-changed=${this._valueChanged} | ||||||
|  |         .addButtonLabel=${this.addButton | ||||||
|  |           ? this.hass.localize("ui.components.entity.entity-picker.add") | ||||||
|  |           : undefined} | ||||||
|       > |       > | ||||||
|       </ha-generic-picker> |       </ha-generic-picker> | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -1,23 +1,39 @@ | |||||||
| import { mdiDrag } from "@mdi/js"; | import "@material/mwc-menu/mwc-menu-surface"; | ||||||
|  | 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 type { PropertyValues } from "lit"; | import { css, html, LitElement, nothing } from "lit"; | ||||||
| import { LitElement, css, html, nothing } from "lit"; |  | ||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, query, state } 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, | ||||||
|   STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS, |   STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS, | ||||||
| } from "../../state-display/state-display"; | } from "../../state-display/state-display"; | ||||||
| import type { HomeAssistant, ValueChangedEvent } from "../../types"; | import type { HomeAssistant, ValueChangedEvent } from "../../types"; | ||||||
| import "../ha-combo-box"; | import "../chips/ha-assist-chip"; | ||||||
| import "../ha-sortable"; |  | ||||||
| import "../chips/ha-input-chip"; |  | ||||||
| import "../chips/ha-chip-set"; | import "../chips/ha-chip-set"; | ||||||
|  | import "../chips/ha-input-chip"; | ||||||
|  | import "../ha-combo-box"; | ||||||
| import type { HaComboBox } from "../ha-combo-box"; | import type { HaComboBox } from "../ha-combo-box"; | ||||||
|  | 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", | ||||||
| @@ -74,7 +90,7 @@ const HIDDEN_ATTRIBUTES = [ | |||||||
| ]; | ]; | ||||||
|  |  | ||||||
| @customElement("ha-entity-state-content-picker") | @customElement("ha-entity-state-content-picker") | ||||||
| class HaEntityStatePicker extends LitElement { | export class HaStateContentPicker extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|   @property({ attribute: false }) public entityId?: string; |   @property({ attribute: false }) public entityId?: string; | ||||||
| @@ -95,26 +111,28 @@ class HaEntityStatePicker extends LitElement { | |||||||
|  |  | ||||||
|   @property() public helper?: string; |   @property() public helper?: string; | ||||||
|  |  | ||||||
|   @state() private _opened = false; |   @query(".container", true) private _container?: HTMLDivElement; | ||||||
|  |  | ||||||
|   @query("ha-combo-box", true) private _comboBox!: HaComboBox; |   @query("ha-combo-box", true) private _comboBox!: HaComboBox; | ||||||
|  |  | ||||||
|   protected shouldUpdate(changedProps: PropertyValues) { |   @state() private _opened = false; | ||||||
|     return !(!changedProps.has("_opened") && this._opened); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private options = memoizeOne( |   private _editIndex?: number; | ||||||
|  |  | ||||||
|  |   private _options = 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 [ |       return [ | ||||||
|         { |         { | ||||||
|           label: this.hass.localize("ui.components.state-content-picker.state"), |           primary: this.hass.localize( | ||||||
|  |             "ui.components.state-content-picker.state" | ||||||
|  |           ), | ||||||
|           value: "state", |           value: "state", | ||||||
|         }, |         }, | ||||||
|         ...(allowName |         ...(allowName | ||||||
|           ? [ |           ? [ | ||||||
|               { |               { | ||||||
|                 label: this.hass.localize( |                 primary: this.hass.localize( | ||||||
|                   "ui.components.state-content-picker.name" |                   "ui.components.state-content-picker.name" | ||||||
|                 ), |                 ), | ||||||
|                 value: "name", |                 value: "name", | ||||||
| @@ -122,13 +140,13 @@ class HaEntityStatePicker extends LitElement { | |||||||
|             ] |             ] | ||||||
|           : []), |           : []), | ||||||
|         { |         { | ||||||
|           label: this.hass.localize( |           primary: this.hass.localize( | ||||||
|             "ui.components.state-content-picker.last_changed" |             "ui.components.state-content-picker.last_changed" | ||||||
|           ), |           ), | ||||||
|           value: "last_changed", |           value: "last_changed", | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           label: this.hass.localize( |           primary: this.hass.localize( | ||||||
|             "ui.components.state-content-picker.last_updated" |             "ui.components.state-content-picker.last_updated" | ||||||
|           ), |           ), | ||||||
|           value: "last_updated", |           value: "last_updated", | ||||||
| @@ -137,7 +155,7 @@ class HaEntityStatePicker extends LitElement { | |||||||
|           ? 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) => ({ | ||||||
|               label: this.hass.localize( |               primary: this.hass.localize( | ||||||
|                 `ui.components.state-content-picker.${content}` |                 `ui.components.state-content-picker.${content}` | ||||||
|               ), |               ), | ||||||
|               value: content, |               value: content, | ||||||
| @@ -146,105 +164,201 @@ class HaEntityStatePicker extends LitElement { | |||||||
|         ...Object.keys(stateObj?.attributes ?? {}) |         ...Object.keys(stateObj?.attributes ?? {}) | ||||||
|           .filter((a) => !HIDDEN_ATTRIBUTES.includes(a)) |           .filter((a) => !HIDDEN_ATTRIBUTES.includes(a)) | ||||||
|           .map((attribute) => ({ |           .map((attribute) => ({ | ||||||
|  |             primary: this.hass.formatEntityAttributeName(stateObj!, attribute), | ||||||
|             value: attribute, |             value: attribute, | ||||||
|             label: this.hass.formatEntityAttributeName(stateObj!, attribute), |  | ||||||
|           })), |           })), | ||||||
|       ]; |       ] satisfies StateContentOption[]; | ||||||
|     } |     } | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   private _filter = ""; |  | ||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
|     if (!this.hass) { |  | ||||||
|       return nothing; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const value = this._value; |     const value = this._value; | ||||||
|  |  | ||||||
|     const stateObj = this.entityId |     const stateObj = this.entityId | ||||||
|       ? this.hass.states[this.entityId] |       ? this.hass.states[this.entityId] | ||||||
|       : undefined; |       : undefined; | ||||||
|  |  | ||||||
|     const options = this.options(this.entityId, stateObj, this.allowName); |     const options = this._options(this.entityId, stateObj, this.allowName); | ||||||
|     const optionItems = options.filter( |  | ||||||
|       (option) => !this._value.includes(option.value) |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       ${value?.length |       ${this.label ? html`<label>${this.label}</label>` : nothing} | ||||||
|         ? html` |       <div class="container ${this.disabled ? "disabled" : ""}"> | ||||||
|             <ha-sortable |         <ha-sortable | ||||||
|               no-style |           no-style | ||||||
|               @item-moved=${this._moveItem} |           @item-moved=${this._moveItem} | ||||||
|               .disabled=${this.disabled} |           .disabled=${this.disabled} | ||||||
|               handle-selector="button.primary.action" |           handle-selector="button.primary.action" | ||||||
|             > |           filter=".add" | ||||||
|               <ha-chip-set> |         > | ||||||
|                 ${repeat( |           <ha-chip-set> | ||||||
|                   this._value, |             ${repeat( | ||||||
|                   (item) => item, |               this._value, | ||||||
|                   (item, idx) => { |               (item) => item, | ||||||
|                     const label = |               (item: string, idx) => { | ||||||
|                       options.find((option) => option.value === item)?.label || |                 const label = options.find((o) => o.value === item)?.primary; | ||||||
|                       item; |                 const isValid = !!label; | ||||||
|                     return html` |                 return html` | ||||||
|                       <ha-input-chip |                   <ha-input-chip | ||||||
|                         .idx=${idx} |                     data-idx=${idx} | ||||||
|                         @remove=${this._removeItem} |                     @remove=${this._removeItem} | ||||||
|                         .label=${label} |                     @click=${this._editItem} | ||||||
|                         selected |                     .label=${label || item} | ||||||
|                       > |                     .selected=${!this.disabled} | ||||||
|                         <ha-svg-icon slot="icon" .path=${mdiDrag}></ha-svg-icon> |                     .disabled=${this.disabled} | ||||||
|                         ${label} |                     class=${!isValid ? "invalid" : ""} | ||||||
|                       </ha-input-chip> |                   > | ||||||
|                     `; |                     <ha-svg-icon | ||||||
|                   } |                       slot="icon" | ||||||
|                 )} |                       .path=${mdiDragHorizontalVariant} | ||||||
|               </ha-chip-set> |                     ></ha-svg-icon> | ||||||
|             </ha-sortable> |                   </ha-input-chip> | ||||||
|           ` |                 `; | ||||||
|         : nothing} |               } | ||||||
|  |             )} | ||||||
|  |             ${this.disabled | ||||||
|  |               ? nothing | ||||||
|  |               : html` | ||||||
|  |                   <ha-assist-chip | ||||||
|  |                     @click=${this._addItem} | ||||||
|  |                     .disabled=${this.disabled} | ||||||
|  |                     label=${this.hass.localize( | ||||||
|  |                       "ui.components.entity.entity-state-content-picker.add" | ||||||
|  |                     )} | ||||||
|  |                     class="add" | ||||||
|  |                   > | ||||||
|  |                     <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|  |                   </ha-assist-chip> | ||||||
|  |                 `} | ||||||
|  |           </ha-chip-set> | ||||||
|  |         </ha-sortable> | ||||||
|  |  | ||||||
|       <ha-combo-box |         <mwc-menu-surface | ||||||
|         item-value-path="value" |           .open=${this._opened} | ||||||
|         item-label-path="label" |           @closed=${this._onClosed} | ||||||
|         .hass=${this.hass} |           @opened=${this._onOpened} | ||||||
|         .label=${this.label} |           @input=${stopPropagation} | ||||||
|         .helper=${this.helper} |           .anchor=${this._container} | ||||||
|         .disabled=${this.disabled} |         > | ||||||
|         .required=${this.required && !value.length} |           <ha-combo-box | ||||||
|         .value=${""} |             .hass=${this.hass} | ||||||
|         .items=${optionItems} |             .value=${""} | ||||||
|         allow-custom-value |             .autofocus=${this.autofocus} | ||||||
|         @filter-changed=${this._filterChanged} |             .disabled=${this.disabled || !this.entityId} | ||||||
|         @value-changed=${this._comboBoxValueChanged} |             .required=${this.required && !value.length} | ||||||
|         @opened-changed=${this._openedChanged} |             .helper=${this.helper} | ||||||
|       ></ha-combo-box> |             .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> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _onClosed(ev) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     this._opened = false; | ||||||
|  |     this._editIndex = undefined; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _onOpened(ev) { | ||||||
|  |     if (!this._opened) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     this._opened = true; | ||||||
|  |     await this._comboBox?.focus(); | ||||||
|  |     await this._comboBox?.open(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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._opened = true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private get _value() { |   private get _value() { | ||||||
|     return !this.value ? [] : ensureArray(this.value); |     return !this.value ? [] : ensureArray(this.value); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _toValue = memoizeOne((value: string[]): typeof this.value => { | ||||||
|  |     if (value.length === 0) { | ||||||
|  |       return undefined; | ||||||
|  |     } | ||||||
|  |     if (value.length === 1) { | ||||||
|  |       return value[0]; | ||||||
|  |     } | ||||||
|  |     return value; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   private _openedChanged(ev: ValueChangedEvent<boolean>) { |   private _openedChanged(ev: ValueChangedEvent<boolean>) { | ||||||
|     this._opened = ev.detail.value; |     const open = ev.detail.value; | ||||||
|     this._comboBox.filteredItems = this._comboBox.items; |     if (open) { | ||||||
|  |       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; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _filterChanged(ev?: CustomEvent): void { |   private _filterSelectedOptions = ( | ||||||
|     this._filter = ev?.detail.value || ""; |     options: StateContentOption[], | ||||||
|  |     current?: string | ||||||
|  |   ) => { | ||||||
|  |     const value = this._value; | ||||||
|  |  | ||||||
|     const filteredItems = this._comboBox.items?.filter((item) => { |     return options.filter( | ||||||
|       const label = item.label || item.value; |       (option) => !value.includes(option.value) || option.value === current | ||||||
|       return label.toLowerCase().includes(this._filter?.toLowerCase()); |     ); | ||||||
|     }); |   }; | ||||||
|  |  | ||||||
|     if (this._filter) { |   private _filterChanged(ev: ValueChangedEvent<string>) { | ||||||
|       filteredItems?.unshift({ label: this._filter, value: this._filter }); |     const input = ev.detail.value; | ||||||
|  |     const filter = input?.toLowerCase() || ""; | ||||||
|  |     const options = this._comboBox.items || []; | ||||||
|  |  | ||||||
|  |     const currentValue = | ||||||
|  |       this._editIndex != null ? this._value[this._editIndex] : ""; | ||||||
|  |  | ||||||
|  |     this._comboBox.filteredItems = this._filterSelectedOptions( | ||||||
|  |       options, | ||||||
|  |       currentValue | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (!filter) { | ||||||
|  |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const fuseOptions: IFuseOptions<StateContentOption> = { | ||||||
|  |       keys: ["primary", "secondary", "value"], | ||||||
|  |       isCaseSensitive: false, | ||||||
|  |       minMatchCharLength: Math.min(filter.length, 2), | ||||||
|  |       threshold: 0.2, | ||||||
|  |       ignoreDiacritics: true, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions); | ||||||
|  |     const filteredItems = fuse.search(filter).map((result) => result.item); | ||||||
|  |  | ||||||
|     this._comboBox.filteredItems = filteredItems; |     this._comboBox.filteredItems = filteredItems; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -257,43 +371,40 @@ class HaEntityStatePicker extends LitElement { | |||||||
|     newValue.splice(newIndex, 0, element); |     newValue.splice(newIndex, 0, element); | ||||||
|     this._setValue(newValue); |     this._setValue(newValue); | ||||||
|     await this.updateComplete; |     await this.updateComplete; | ||||||
|     this._filterChanged(); |     this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _removeItem(ev) { |   private async _removeItem(ev) { | ||||||
|     ev.stopPropagation(); |     ev.stopPropagation(); | ||||||
|     const value: string[] = [...this._value]; |     const value = [...this._value]; | ||||||
|     value.splice(ev.target.idx, 1); |     const idx = parseInt(ev.target.dataset.idx, 10); | ||||||
|  |     value.splice(idx, 1); | ||||||
|     this._setValue(value); |     this._setValue(value); | ||||||
|     await this.updateComplete; |     await this.updateComplete; | ||||||
|     this._filterChanged(); |     this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _comboBoxValueChanged(ev: CustomEvent): void { |   private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void { | ||||||
|     ev.stopPropagation(); |     ev.stopPropagation(); | ||||||
|     const newValue = ev.detail.value; |     const value = ev.detail.value; | ||||||
|  |  | ||||||
|     if (this.disabled || newValue === "") { |     if (this.disabled || value === "") { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const currentValue = this._value; |     const newValue = [...this._value]; | ||||||
|  |  | ||||||
|     if (currentValue.includes(newValue)) { |     if (this._editIndex != null) { | ||||||
|       return; |       newValue[this._editIndex] = value; | ||||||
|  |     } else { | ||||||
|  |       newValue.push(value); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     setTimeout(() => { |     this._setValue(newValue); | ||||||
|       this._filterChanged(); |  | ||||||
|       this._comboBox.setInputValue(""); |  | ||||||
|     }, 0); |  | ||||||
|  |  | ||||||
|     this._setValue([...currentValue, newValue]); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _setValue(value: string[]) { |   private _setValue(value: string[]) { | ||||||
|     const newValue = |     const newValue = this._toValue(value); | ||||||
|       value.length === 0 ? undefined : value.length === 1 ? value[0] : value; |  | ||||||
|     this.value = newValue; |     this.value = newValue; | ||||||
|     fireEvent(this, "value-changed", { |     fireEvent(this, "value-changed", { | ||||||
|       value: newValue, |       value: newValue, | ||||||
| @@ -303,10 +414,64 @@ class HaEntityStatePicker extends LitElement { | |||||||
|   static styles = css` |   static styles = css` | ||||||
|     :host { |     :host { | ||||||
|       position: relative; |       position: relative; | ||||||
|  |       width: 100%; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .container { | ||||||
|  |       position: relative; | ||||||
|  |       background-color: var(--mdc-text-field-fill-color, whitesmoke); | ||||||
|  |       border-radius: var(--ha-border-radius-sm); | ||||||
|  |       border-end-end-radius: var(--ha-border-radius-square); | ||||||
|  |       border-end-start-radius: var(--ha-border-radius-square); | ||||||
|  |     } | ||||||
|  |     .container:after { | ||||||
|  |       display: block; | ||||||
|  |       content: ""; | ||||||
|  |       position: absolute; | ||||||
|  |       pointer-events: none; | ||||||
|  |       bottom: 0; | ||||||
|  |       left: 0; | ||||||
|  |       right: 0; | ||||||
|  |       height: 1px; | ||||||
|  |       width: 100%; | ||||||
|  |       background-color: var( | ||||||
|  |         --mdc-text-field-idle-line-color, | ||||||
|  |         rgba(0, 0, 0, 0.42) | ||||||
|  |       ); | ||||||
|  |       transform: | ||||||
|  |         height 180ms ease-in-out, | ||||||
|  |         background-color 180ms ease-in-out; | ||||||
|  |     } | ||||||
|  |     .container.disabled:after { | ||||||
|  |       background-color: var( | ||||||
|  |         --mdc-text-field-disabled-line-color, | ||||||
|  |         rgba(0, 0, 0, 0.42) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     .container:focus-within:after { | ||||||
|  |       height: 2px; | ||||||
|  |       background-color: var(--mdc-theme-primary); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     label { | ||||||
|  |       display: block; | ||||||
|  |       margin: 0 0 var(--ha-space-2); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .add { | ||||||
|  |       order: 1; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     mwc-menu-surface { | ||||||
|  |       --mdc-menu-min-width: 100%; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     ha-chip-set { |     ha-chip-set { | ||||||
|       padding: 8px 0; |       padding: var(--ha-space-2) var(--ha-space-2); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     .invalid { | ||||||
|  |       text-decoration: line-through; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     .sortable-fallback { |     .sortable-fallback { | ||||||
| @@ -326,6 +491,6 @@ class HaEntityStatePicker extends LitElement { | |||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
|     "ha-entity-state-content-picker": HaEntityStatePicker; |     "ha-entity-state-content-picker": HaStateContentPicker; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import type { HassEntity } from "home-assistant-js-websocket"; |  | ||||||
| import type { PropertyValues } from "lit"; | 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, query, state } from "lit/decorators"; | ||||||
| @@ -9,8 +8,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types"; | |||||||
| import "../ha-combo-box"; | import "../ha-combo-box"; | ||||||
| import type { HaComboBox } from "../ha-combo-box"; | import type { HaComboBox } from "../ha-combo-box"; | ||||||
|  |  | ||||||
| export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; |  | ||||||
|  |  | ||||||
| interface StateOption { | interface StateOption { | ||||||
|   value: string; |   value: string; | ||||||
|   label: string; |   label: string; | ||||||
|   | |||||||
| @@ -112,7 +112,7 @@ export class HaEntityToggle extends LitElement { | |||||||
|     if (!this.hass || !this.stateObj) { |     if (!this.hass || !this.stateObj) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     forwardHaptic("light"); |     forwardHaptic(this, "light"); | ||||||
|     const stateDomain = computeStateDomain(this.stateObj); |     const stateDomain = computeStateDomain(this.stateObj); | ||||||
|     let serviceDomain; |     let serviceDomain; | ||||||
|     let service; |     let service; | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import { customElement, property, query } from "lit/decorators"; | |||||||
| 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 { computeEntityNameList } from "../../common/entity/compute_entity_name_display"; | ||||||
| import { computeStateName } from "../../common/entity/compute_state_name"; | import { computeStateName } from "../../common/entity/compute_state_name"; | ||||||
| import { computeRTL } from "../../common/util/compute_rtl"; | import { computeRTL } from "../../common/util/compute_rtl"; | ||||||
| import { domainToName } from "../../data/integration"; | import { domainToName } from "../../data/integration"; | ||||||
| @@ -199,7 +200,7 @@ export class HaStatisticPicker extends LitElement { | |||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       const isRTL = computeRTL(this.hass); |       const isRTL = computeRTL(hass); | ||||||
|  |  | ||||||
|       const output: StatisticComboBoxItem[] = []; |       const output: StatisticComboBoxItem[] = []; | ||||||
|  |  | ||||||
| @@ -256,9 +257,15 @@ export class HaStatisticPicker extends LitElement { | |||||||
|         const id = meta.statistic_id; |         const id = meta.statistic_id; | ||||||
|  |  | ||||||
|         const friendlyName = computeStateName(stateObj); // Keep this for search |         const friendlyName = computeStateName(stateObj); // Keep this for search | ||||||
|         const entityName = hass.formatEntityName(stateObj, "entity"); |  | ||||||
|         const deviceName = hass.formatEntityName(stateObj, "device"); |         const [entityName, deviceName, areaName] = computeEntityNameList( | ||||||
|         const areaName = hass.formatEntityName(stateObj, "area"); |           stateObj, | ||||||
|  |           [{ type: "entity" }, { type: "device" }, { type: "area" }], | ||||||
|  |           hass.entities, | ||||||
|  |           hass.devices, | ||||||
|  |           hass.areas, | ||||||
|  |           hass.floors | ||||||
|  |         ); | ||||||
|  |  | ||||||
|         const primary = entityName || deviceName || id; |         const primary = entityName || deviceName || id; | ||||||
|         const secondary = [areaName, entityName ? deviceName : undefined] |         const secondary = [areaName, entityName ? deviceName : undefined] | ||||||
| @@ -331,9 +338,14 @@ export class HaStatisticPicker extends LitElement { | |||||||
|     const stateObj = this.hass.states[statisticId]; |     const stateObj = this.hass.states[statisticId]; | ||||||
|  |  | ||||||
|     if (stateObj) { |     if (stateObj) { | ||||||
|       const entityName = this.hass.formatEntityName(stateObj, "entity"); |       const [entityName, deviceName, areaName] = computeEntityNameList( | ||||||
|       const deviceName = this.hass.formatEntityName(stateObj, "device"); |         stateObj, | ||||||
|       const areaName = this.hass.formatEntityName(stateObj, "area"); |         [{ type: "entity" }, { type: "device" }, { type: "area" }], | ||||||
|  |         this.hass.entities, | ||||||
|  |         this.hass.devices, | ||||||
|  |         this.hass.areas, | ||||||
|  |         this.hass.floors | ||||||
|  |       ); | ||||||
|  |  | ||||||
|       const isRTL = computeRTL(this.hass); |       const isRTL = computeRTL(this.hass); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -236,7 +236,7 @@ export class StateBadge extends LitElement { | |||||||
|           border-radius: var(--state-badge-with-media-image-border-radius, 8%); |           border-radius: var(--state-badge-with-media-image-border-radius, 8%); | ||||||
|         } |         } | ||||||
|         :host(.has-no-radius) { |         :host(.has-no-radius) { | ||||||
|           border-radius: 0; |           border-radius: var(--ha-border-radius-square); | ||||||
|         } |         } | ||||||
|         :host(:focus) { |         :host(:focus) { | ||||||
|           outline: none; |           outline: none; | ||||||
|   | |||||||
| @@ -99,7 +99,7 @@ class HaAlert extends LitElement { | |||||||
|       opacity: 0.12; |       opacity: 0.12; | ||||||
|       pointer-events: none; |       pointer-events: none; | ||||||
|       content: ""; |       content: ""; | ||||||
|       border-radius: 4px; |       border-radius: var(--ha-border-radius-sm); | ||||||
|     } |     } | ||||||
|     .icon.no-title { |     .icon.no-title { | ||||||
|       align-self: center; |       align-self: center; | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import type { CSSResultGroup, TemplateResult } from "lit"; | import type { CSSResultGroup, TemplateResult } from "lit"; | ||||||
| import { css, html, LitElement } from "lit"; | import { css, html, LitElement, nothing } 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 type { LocalizeFunc } from "../common/translations/localize"; | import type { LocalizeFunc } from "../common/translations/localize"; | ||||||
| @@ -46,7 +46,7 @@ export class HaAnalytics extends LitElement { | |||||||
|         </span> |         </span> | ||||||
|         <ha-switch |         <ha-switch | ||||||
|           @change=${this._handleRowClick} |           @change=${this._handleRowClick} | ||||||
|           .checked=${baseEnabled} |           .checked=${!!baseEnabled} | ||||||
|           .preference=${"base"} |           .preference=${"base"} | ||||||
|           .disabled=${loading} |           .disabled=${loading} | ||||||
|           name="base" |           name="base" | ||||||
| @@ -70,17 +70,21 @@ export class HaAnalytics extends LitElement { | |||||||
|               <ha-switch |               <ha-switch | ||||||
|                 .id="switch-${preference}" |                 .id="switch-${preference}" | ||||||
|                 @change=${this._handleRowClick} |                 @change=${this._handleRowClick} | ||||||
|                 .checked=${this.analytics?.preferences[preference]} |                 .checked=${!!this.analytics?.preferences[preference]} | ||||||
|                 .preference=${preference} |                 .preference=${preference} | ||||||
|                 name=${preference} |                 name=${preference} | ||||||
|                 ?disabled=${baseEnabled} |  | ||||||
|               > |               > | ||||||
|               </ha-switch> |               </ha-switch> | ||||||
|               <ha-tooltip .for="switch-${preference}" placement="right"> |               ${baseEnabled | ||||||
|                 ${this.localize( |                 ? nothing | ||||||
|                   `ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled` |                 : html`<ha-tooltip | ||||||
|                 )} |                     .for="switch-${preference}" | ||||||
|               </ha-tooltip> |                     placement="right" | ||||||
|  |                   > | ||||||
|  |                     ${this.localize( | ||||||
|  |                       `ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled` | ||||||
|  |                     )} | ||||||
|  |                   </ha-tooltip>`} | ||||||
|             </span> |             </span> | ||||||
|           </ha-settings-row> |           </ha-settings-row> | ||||||
|         ` |         ` | ||||||
| @@ -98,7 +102,7 @@ export class HaAnalytics extends LitElement { | |||||||
|         </span> |         </span> | ||||||
|         <ha-switch |         <ha-switch | ||||||
|           @change=${this._handleRowClick} |           @change=${this._handleRowClick} | ||||||
|           .checked=${this.analytics?.preferences.diagnostics} |           .checked=${!!this.analytics?.preferences.diagnostics} | ||||||
|           .preference=${"diagnostics"} |           .preference=${"diagnostics"} | ||||||
|           .disabled=${loading} |           .disabled=${loading} | ||||||
|           name="diagnostics" |           name="diagnostics" | ||||||
|   | |||||||
| @@ -8,21 +8,13 @@ import { styleMap } from "lit/directives/style-map"; | |||||||
| 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 { computeAreaName } from "../common/entity/compute_area_name"; | import { computeAreaName } from "../common/entity/compute_area_name"; | ||||||
| import { computeDomain } from "../common/entity/compute_domain"; |  | ||||||
| import { computeFloorName } from "../common/entity/compute_floor_name"; | import { computeFloorName } from "../common/entity/compute_floor_name"; | ||||||
| import { stringCompare } from "../common/string/compare"; |  | ||||||
| import { computeRTL } from "../common/util/compute_rtl"; | import { computeRTL } from "../common/util/compute_rtl"; | ||||||
| import type { AreaRegistryEntry } from "../data/area_registry"; |  | ||||||
| import type { |  | ||||||
|   DeviceEntityDisplayLookup, |  | ||||||
|   DeviceRegistryEntry, |  | ||||||
| } from "../data/device_registry"; |  | ||||||
| import { getDeviceEntityDisplayLookup } from "../data/device_registry"; |  | ||||||
| import type { EntityRegistryDisplayEntry } from "../data/entity_registry"; |  | ||||||
| import { | import { | ||||||
|   getFloorAreaLookup, |   getAreasAndFloors, | ||||||
|   type FloorRegistryEntry, |   type AreaFloorValue, | ||||||
| } from "../data/floor_registry"; |   type FloorComboBoxItem, | ||||||
|  | } from "../data/area_floor"; | ||||||
| import type { HomeAssistant, ValueChangedEvent } from "../types"; | import type { HomeAssistant, ValueChangedEvent } from "../types"; | ||||||
| import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||||
| import "./ha-combo-box-item"; | import "./ha-combo-box-item"; | ||||||
| @@ -30,24 +22,12 @@ import "./ha-floor-icon"; | |||||||
| import "./ha-generic-picker"; | import "./ha-generic-picker"; | ||||||
| import type { HaGenericPicker } from "./ha-generic-picker"; | import type { HaGenericPicker } from "./ha-generic-picker"; | ||||||
| import "./ha-icon-button"; | import "./ha-icon-button"; | ||||||
| import type { PickerComboBoxItem } from "./ha-picker-combo-box"; |  | ||||||
| import type { PickerValueRenderer } from "./ha-picker-field"; | import type { PickerValueRenderer } from "./ha-picker-field"; | ||||||
| import "./ha-svg-icon"; | import "./ha-svg-icon"; | ||||||
| import "./ha-tree-indicator"; | import "./ha-tree-indicator"; | ||||||
|  |  | ||||||
| const SEPARATOR = "________"; | const SEPARATOR = "________"; | ||||||
|  |  | ||||||
| interface FloorComboBoxItem extends PickerComboBoxItem { |  | ||||||
|   type: "floor" | "area"; |  | ||||||
|   floor?: FloorRegistryEntry; |  | ||||||
|   area?: AreaRegistryEntry; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| interface AreaFloorValue { |  | ||||||
|   id: string; |  | ||||||
|   type: "floor" | "area"; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @customElement("ha-area-floor-picker") | @customElement("ha-area-floor-picker") | ||||||
| export class HaAreaFloorPicker extends LitElement { | export class HaAreaFloorPicker extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -154,243 +134,6 @@ export class HaAreaFloorPicker extends LitElement { | |||||||
|     `; |     `; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   private _getAreasAndFloors = memoizeOne( |  | ||||||
|     ( |  | ||||||
|       haFloors: HomeAssistant["floors"], |  | ||||||
|       haAreas: HomeAssistant["areas"], |  | ||||||
|       haDevices: HomeAssistant["devices"], |  | ||||||
|       haEntities: HomeAssistant["entities"], |  | ||||||
|       includeDomains: this["includeDomains"], |  | ||||||
|       excludeDomains: this["excludeDomains"], |  | ||||||
|       includeDeviceClasses: this["includeDeviceClasses"], |  | ||||||
|       deviceFilter: this["deviceFilter"], |  | ||||||
|       entityFilter: this["entityFilter"], |  | ||||||
|       excludeAreas: this["excludeAreas"], |  | ||||||
|       excludeFloors: this["excludeFloors"] |  | ||||||
|     ): FloorComboBoxItem[] => { |  | ||||||
|       const floors = Object.values(haFloors); |  | ||||||
|       const areas = Object.values(haAreas); |  | ||||||
|       const devices = Object.values(haDevices); |  | ||||||
|       const entities = Object.values(haEntities); |  | ||||||
|  |  | ||||||
|       let deviceEntityLookup: DeviceEntityDisplayLookup = {}; |  | ||||||
|       let inputDevices: DeviceRegistryEntry[] | undefined; |  | ||||||
|       let inputEntities: EntityRegistryDisplayEntry[] | undefined; |  | ||||||
|  |  | ||||||
|       if ( |  | ||||||
|         includeDomains || |  | ||||||
|         excludeDomains || |  | ||||||
|         includeDeviceClasses || |  | ||||||
|         deviceFilter || |  | ||||||
|         entityFilter |  | ||||||
|       ) { |  | ||||||
|         deviceEntityLookup = getDeviceEntityDisplayLookup(entities); |  | ||||||
|         inputDevices = devices; |  | ||||||
|         inputEntities = entities.filter((entity) => entity.area_id); |  | ||||||
|  |  | ||||||
|         if (includeDomains) { |  | ||||||
|           inputDevices = inputDevices!.filter((device) => { |  | ||||||
|             const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|             if (!devEntities || !devEntities.length) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return deviceEntityLookup[device.id].some((entity) => |  | ||||||
|               includeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|             ); |  | ||||||
|           }); |  | ||||||
|           inputEntities = inputEntities!.filter((entity) => |  | ||||||
|             includeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (excludeDomains) { |  | ||||||
|           inputDevices = inputDevices!.filter((device) => { |  | ||||||
|             const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|             if (!devEntities || !devEntities.length) { |  | ||||||
|               return true; |  | ||||||
|             } |  | ||||||
|             return entities.every( |  | ||||||
|               (entity) => |  | ||||||
|                 !excludeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|             ); |  | ||||||
|           }); |  | ||||||
|           inputEntities = inputEntities!.filter( |  | ||||||
|             (entity) => |  | ||||||
|               !excludeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (includeDeviceClasses) { |  | ||||||
|           inputDevices = inputDevices!.filter((device) => { |  | ||||||
|             const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|             if (!devEntities || !devEntities.length) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return deviceEntityLookup[device.id].some((entity) => { |  | ||||||
|               const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|               if (!stateObj) { |  | ||||||
|                 return false; |  | ||||||
|               } |  | ||||||
|               return ( |  | ||||||
|                 stateObj.attributes.device_class && |  | ||||||
|                 includeDeviceClasses.includes(stateObj.attributes.device_class) |  | ||||||
|               ); |  | ||||||
|             }); |  | ||||||
|           }); |  | ||||||
|           inputEntities = inputEntities!.filter((entity) => { |  | ||||||
|             const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|             return ( |  | ||||||
|               stateObj.attributes.device_class && |  | ||||||
|               includeDeviceClasses.includes(stateObj.attributes.device_class) |  | ||||||
|             ); |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (deviceFilter) { |  | ||||||
|           inputDevices = inputDevices!.filter((device) => |  | ||||||
|             deviceFilter!(device) |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (entityFilter) { |  | ||||||
|           inputDevices = inputDevices!.filter((device) => { |  | ||||||
|             const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|             if (!devEntities || !devEntities.length) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return deviceEntityLookup[device.id].some((entity) => { |  | ||||||
|               const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|               if (!stateObj) { |  | ||||||
|                 return false; |  | ||||||
|               } |  | ||||||
|               return entityFilter(stateObj); |  | ||||||
|             }); |  | ||||||
|           }); |  | ||||||
|           inputEntities = inputEntities!.filter((entity) => { |  | ||||||
|             const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|             if (!stateObj) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return entityFilter!(stateObj); |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       let outputAreas = areas; |  | ||||||
|  |  | ||||||
|       let areaIds: string[] | undefined; |  | ||||||
|  |  | ||||||
|       if (inputDevices) { |  | ||||||
|         areaIds = inputDevices |  | ||||||
|           .filter((device) => device.area_id) |  | ||||||
|           .map((device) => device.area_id!); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (inputEntities) { |  | ||||||
|         areaIds = (areaIds ?? []).concat( |  | ||||||
|           inputEntities |  | ||||||
|             .filter((entity) => entity.area_id) |  | ||||||
|             .map((entity) => entity.area_id!) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (areaIds) { |  | ||||||
|         outputAreas = outputAreas.filter((area) => |  | ||||||
|           areaIds!.includes(area.area_id) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (excludeAreas) { |  | ||||||
|         outputAreas = outputAreas.filter( |  | ||||||
|           (area) => !excludeAreas!.includes(area.area_id) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (excludeFloors) { |  | ||||||
|         outputAreas = outputAreas.filter( |  | ||||||
|           (area) => !area.floor_id || !excludeFloors!.includes(area.floor_id) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const floorAreaLookup = getFloorAreaLookup(outputAreas); |  | ||||||
|       const unassisgnedAreas = Object.values(outputAreas).filter( |  | ||||||
|         (area) => !area.floor_id || !floorAreaLookup[area.floor_id] |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       // @ts-ignore |  | ||||||
|       const floorAreaEntries: [ |  | ||||||
|         FloorRegistryEntry | undefined, |  | ||||||
|         AreaRegistryEntry[], |  | ||||||
|       ][] = Object.entries(floorAreaLookup) |  | ||||||
|         .map(([floorId, floorAreas]) => { |  | ||||||
|           const floor = floors.find((fl) => fl.floor_id === floorId)!; |  | ||||||
|           return [floor, floorAreas] as const; |  | ||||||
|         }) |  | ||||||
|         .sort(([floorA], [floorB]) => { |  | ||||||
|           if (floorA.level !== floorB.level) { |  | ||||||
|             return (floorA.level ?? 0) - (floorB.level ?? 0); |  | ||||||
|           } |  | ||||||
|           return stringCompare(floorA.name, floorB.name); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|       const items: FloorComboBoxItem[] = []; |  | ||||||
|  |  | ||||||
|       floorAreaEntries.forEach(([floor, floorAreas]) => { |  | ||||||
|         if (floor) { |  | ||||||
|           const floorName = computeFloorName(floor); |  | ||||||
|  |  | ||||||
|           const areaSearchLabels = floorAreas |  | ||||||
|             .map((area) => { |  | ||||||
|               const areaName = computeAreaName(area) || area.area_id; |  | ||||||
|               return [area.area_id, areaName, ...area.aliases]; |  | ||||||
|             }) |  | ||||||
|             .flat(); |  | ||||||
|  |  | ||||||
|           items.push({ |  | ||||||
|             id: this._formatValue({ id: floor.floor_id, type: "floor" }), |  | ||||||
|             type: "floor", |  | ||||||
|             primary: floorName, |  | ||||||
|             floor: floor, |  | ||||||
|             search_labels: [ |  | ||||||
|               floor.floor_id, |  | ||||||
|               floorName, |  | ||||||
|               ...floor.aliases, |  | ||||||
|               ...areaSearchLabels, |  | ||||||
|             ], |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|         items.push( |  | ||||||
|           ...floorAreas.map((area) => { |  | ||||||
|             const areaName = computeAreaName(area) || area.area_id; |  | ||||||
|             return { |  | ||||||
|               id: this._formatValue({ id: area.area_id, type: "area" }), |  | ||||||
|               type: "area" as const, |  | ||||||
|               primary: areaName, |  | ||||||
|               area: area, |  | ||||||
|               icon: area.icon || undefined, |  | ||||||
|               search_labels: [area.area_id, areaName, ...area.aliases], |  | ||||||
|             }; |  | ||||||
|           }) |  | ||||||
|         ); |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       items.push( |  | ||||||
|         ...unassisgnedAreas.map((area) => { |  | ||||||
|           const areaName = computeAreaName(area) || area.area_id; |  | ||||||
|           return { |  | ||||||
|             id: this._formatValue({ id: area.area_id, type: "area" }), |  | ||||||
|             type: "area" as const, |  | ||||||
|             primary: areaName, |  | ||||||
|             icon: area.icon || undefined, |  | ||||||
|             search_labels: [area.area_id, areaName, ...area.aliases], |  | ||||||
|           }; |  | ||||||
|         }) |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       return items; |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = ( |   private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = ( | ||||||
|     item, |     item, | ||||||
|     { index }, |     { index }, | ||||||
| @@ -445,12 +188,16 @@ export class HaAreaFloorPicker extends LitElement { | |||||||
|     `; |     `; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors); | ||||||
|  |  | ||||||
|   private _getItems = () => |   private _getItems = () => | ||||||
|     this._getAreasAndFloors( |     this._getAreasAndFloorsMemoized( | ||||||
|  |       this.hass.states, | ||||||
|       this.hass.floors, |       this.hass.floors, | ||||||
|       this.hass.areas, |       this.hass.areas, | ||||||
|       this.hass.devices, |       this.hass.devices, | ||||||
|       this.hass.entities, |       this.hass.entities, | ||||||
|  |       this._formatValue, | ||||||
|       this.includeDomains, |       this.includeDomains, | ||||||
|       this.excludeDomains, |       this.excludeDomains, | ||||||
|       this.includeDeviceClasses, |       this.includeDeviceClasses, | ||||||
|   | |||||||
| @@ -107,7 +107,7 @@ export class HaAreaPicker extends LitElement { | |||||||
|           `; |           `; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const { floor } = getAreaContext(area, this.hass); |         const { floor } = getAreaContext(area, this.hass.floors); | ||||||
|  |  | ||||||
|         const areaName = area ? computeAreaName(area) : undefined; |         const areaName = area ? computeAreaName(area) : undefined; | ||||||
|         const floorName = floor ? computeFloorName(floor) : undefined; |         const floorName = floor ? computeFloorName(floor) : undefined; | ||||||
| @@ -279,7 +279,7 @@ export class HaAreaPicker extends LitElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       const items = outputAreas.map<PickerComboBoxItem>((area) => { |       const items = outputAreas.map<PickerComboBoxItem>((area) => { | ||||||
|         const { floor } = getAreaContext(area, this.hass); |         const { floor } = getAreaContext(area, this.hass.floors); | ||||||
|         const floorName = floor ? computeFloorName(floor) : undefined; |         const floorName = floor ? computeFloorName(floor) : undefined; | ||||||
|         const areaName = computeAreaName(area); |         const areaName = computeAreaName(area); | ||||||
|         return { |         return { | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ export class HaAreasDisplayEditor extends LitElement { | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const items: DisplayItem[] = areas.map((area) => { |     const items: DisplayItem[] = areas.map((area) => { | ||||||
|       const { floor } = getAreaContext(area, this.hass!); |       const { floor } = getAreaContext(area, this.hass.floors); | ||||||
|       return { |       return { | ||||||
|         value: area.area_id, |         value: area.area_id, | ||||||
|         label: area.name, |         label: area.name, | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { mdiDrag, mdiTextureBox } from "@mdi/js"; | import { mdiDragHorizontalVariant, mdiTextureBox } from "@mdi/js"; | ||||||
| import type { TemplateResult } from "lit"; | import type { TemplateResult } from "lit"; | ||||||
| import { LitElement, css, html, nothing } from "lit"; | import { LitElement, css, html, nothing } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| @@ -105,7 +105,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement { | |||||||
|                       <ha-svg-icon |                       <ha-svg-icon | ||||||
|                         class="handle" |                         class="handle" | ||||||
|                         slot="icons" |                         slot="icons" | ||||||
|                         .path=${mdiDrag} |                         .path=${mdiDragHorizontalVariant} | ||||||
|                       ></ha-svg-icon> |                       ></ha-svg-icon> | ||||||
|                     `} |                     `} | ||||||
|                 <ha-items-display-editor |                 <ha-items-display-editor | ||||||
| @@ -138,7 +138,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement { | |||||||
|       ); |       ); | ||||||
|       const groupedItems: Record<string, DisplayItem[]> = areas.reduce( |       const groupedItems: Record<string, DisplayItem[]> = areas.reduce( | ||||||
|         (acc, area) => { |         (acc, area) => { | ||||||
|           const { floor } = getAreaContext(area, this.hass!); |           const { floor } = getAreaContext(area, this.hass.floors); | ||||||
|           const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR; |           const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR; | ||||||
|  |  | ||||||
|           if (!acc[floorId]) { |           if (!acc[floorId]) { | ||||||
|   | |||||||
| @@ -1,24 +1,24 @@ | |||||||
| import type { PropertyValues, TemplateResult } from "lit"; |  | ||||||
| import { css, LitElement, html, nothing } from "lit"; |  | ||||||
| import { mdiAlertCircle, mdiMicrophone, mdiSend } from "@mdi/js"; | import { mdiAlertCircle, mdiMicrophone, mdiSend } from "@mdi/js"; | ||||||
|  | import type { PropertyValues, TemplateResult } from "lit"; | ||||||
|  | import { css, html, LitElement, nothing } from "lit"; | ||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, query, state } from "lit/decorators"; | ||||||
| import { classMap } from "lit/directives/class-map"; | import { classMap } from "lit/directives/class-map"; | ||||||
| import type { HomeAssistant } from "../types"; | import { supportsFeature } from "../common/entity/supports-feature"; | ||||||
| import { | import { | ||||||
|   type PipelineRunEvent, |  | ||||||
|   runAssistPipeline, |   runAssistPipeline, | ||||||
|   type AssistPipeline, |   type AssistPipeline, | ||||||
|   type ConversationChatLogAssistantDelta, |   type ConversationChatLogAssistantDelta, | ||||||
|   type ConversationChatLogToolResultDelta, |   type ConversationChatLogToolResultDelta, | ||||||
|  |   type PipelineRunEvent, | ||||||
| } from "../data/assist_pipeline"; | } from "../data/assist_pipeline"; | ||||||
| import { supportsFeature } from "../common/entity/supports-feature"; |  | ||||||
| import { ConversationEntityFeature } from "../data/conversation"; | import { ConversationEntityFeature } from "../data/conversation"; | ||||||
|  | import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; | ||||||
|  | import type { HomeAssistant } from "../types"; | ||||||
| import { AudioRecorder } from "../util/audio-recorder"; | import { AudioRecorder } from "../util/audio-recorder"; | ||||||
|  | import { documentationUrl } from "../util/documentation-url"; | ||||||
| import "./ha-alert"; | import "./ha-alert"; | ||||||
| import "./ha-textfield"; | import "./ha-textfield"; | ||||||
| import type { HaTextField } from "./ha-textfield"; | import type { HaTextField } from "./ha-textfield"; | ||||||
| import { documentationUrl } from "../util/documentation-url"; |  | ||||||
| import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; |  | ||||||
|  |  | ||||||
| interface AssistMessage { | interface AssistMessage { | ||||||
|   who: string; |   who: string; | ||||||
| @@ -591,7 +591,7 @@ export class HaAssistChat extends LitElement { | |||||||
|       clear: both; |       clear: both; | ||||||
|       margin: 8px 0; |       margin: 8px 0; | ||||||
|       padding: 8px; |       padding: 8px; | ||||||
|       border-radius: 15px; |       border-radius: var(--ha-border-radius-xl); | ||||||
|     } |     } | ||||||
|     .message:last-child { |     .message:last-child { | ||||||
|       margin-bottom: 0; |       margin-bottom: 0; | ||||||
| @@ -659,7 +659,7 @@ export class HaAssistChat extends LitElement { | |||||||
|     .double-bounce2 { |     .double-bounce2 { | ||||||
|       width: 48px; |       width: 48px; | ||||||
|       height: 48px; |       height: 48px; | ||||||
|       border-radius: 50%; |       border-radius: var(--ha-border-radius-circle); | ||||||
|       background-color: var(--primary-color); |       background-color: var(--primary-color); | ||||||
|       opacity: 0.2; |       opacity: 0.2; | ||||||
|       position: absolute; |       position: absolute; | ||||||
|   | |||||||
| @@ -118,7 +118,7 @@ export class HaAutomationRow extends LitElement { | |||||||
|     } |     } | ||||||
|     .row { |     .row { | ||||||
|       display: flex; |       display: flex; | ||||||
|       padding: 0 8px; |       padding: var(--ha-space-0) var(--ha-space-2); | ||||||
|       min-height: 48px; |       min-height: 48px; | ||||||
|       align-items: center; |       align-items: center; | ||||||
|       cursor: pointer; |       cursor: pointer; | ||||||
| @@ -134,12 +134,12 @@ export class HaAutomationRow extends LitElement { | |||||||
|     .expand-button { |     .expand-button { | ||||||
|       transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); |       transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); | ||||||
|       color: var(--ha-color-on-neutral-quiet); |       color: var(--ha-color-on-neutral-quiet); | ||||||
|       margin-left: -8px; |       margin-left: calc(var(--ha-space-2) * -1); | ||||||
|     } |     } | ||||||
|     :host([building-block]) .leading-icon-wrapper { |     :host([building-block]) .leading-icon-wrapper { | ||||||
|       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: 4px; |       padding: var(--ha-space-1); | ||||||
|       display: flex; |       display: flex; | ||||||
|       justify-content: center; |       justify-content: center; | ||||||
|       align-items: center; |       align-items: center; | ||||||
| @@ -149,7 +149,7 @@ export class HaAutomationRow extends LitElement { | |||||||
|       color: var(--ha-color-on-neutral-quiet); |       color: var(--ha-color-on-neutral-quiet); | ||||||
|     } |     } | ||||||
|     :host([building-block]) ::slotted([slot="leading-icon"]) { |     :host([building-block]) ::slotted([slot="leading-icon"]) { | ||||||
|       --mdc-icon-size: 20px; |       --mdc-icon-size: var(--ha-space-5); | ||||||
|       color: var(--white-color); |       color: var(--white-color); | ||||||
|       transform: rotate(-45deg); |       transform: rotate(-45deg); | ||||||
|     } |     } | ||||||
| @@ -170,7 +170,7 @@ export class HaAutomationRow extends LitElement { | |||||||
|     ::slotted([slot="header"]) { |     ::slotted([slot="header"]) { | ||||||
|       flex: 1; |       flex: 1; | ||||||
|       overflow-wrap: anywhere; |       overflow-wrap: anywhere; | ||||||
|       margin: 0 12px; |       margin: var(--ha-space-0) var(--ha-space-3); | ||||||
|     } |     } | ||||||
|     :host([sort-selected]) .row { |     :host([sort-selected]) .row { | ||||||
|       outline: solid; |       outline: solid; | ||||||
|   | |||||||
| @@ -54,7 +54,7 @@ export class HaBadge extends LitElement { | |||||||
|       flex-direction: row; |       flex-direction: row; | ||||||
|       align-items: center; |       align-items: center; | ||||||
|       justify-content: center; |       justify-content: center; | ||||||
|       gap: 8px; |       gap: var(--ha-space-2); | ||||||
|       height: var(--ha-badge-size, 36px); |       height: var(--ha-badge-size, 36px); | ||||||
|       min-width: var(--ha-badge-size, 36px); |       min-width: var(--ha-badge-size, 36px); | ||||||
|       padding: 0px 12px; |       padding: 0px 12px; | ||||||
| @@ -122,7 +122,7 @@ export class HaBadge extends LitElement { | |||||||
|     ::slotted(img[slot="icon"]) { |     ::slotted(img[slot="icon"]) { | ||||||
|       width: 30px; |       width: 30px; | ||||||
|       height: 30px; |       height: 30px; | ||||||
|       border-radius: 50%; |       border-radius: var(--ha-border-radius-circle); | ||||||
|       object-fit: cover; |       object-fit: cover; | ||||||
|       overflow: hidden; |       overflow: hidden; | ||||||
|       margin-left: -10px; |       margin-left: -10px; | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user