Compare commits
	
		
			325 Commits
		
	
	
		
			20240903.1
			...
			partial_te
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 85b24afc10 | ||
|   | 42f2341e06 | ||
|   | 48dfa1163b | ||
|   | 28e12f7fd1 | ||
|   | 5f58ac4fb6 | ||
|   | 25e7c4f1b2 | ||
|   | 00934f2183 | ||
|   | d302eaffe6 | ||
|   | 901f736d5f | ||
|   | e55f32ae91 | ||
|   | 3e0c998e74 | ||
|   | 597866ff4e | ||
|   | 64e21e185c | ||
|   | 7a1838ee1a | ||
|   | 4debac60ae | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 4af231e62b | ||
|   | 432cf4a7ed | ||
|   | df064967ca | ||
|   | dc0cab9307 | ||
|   | 3180747a0a | ||
|   | 1542095138 | ||
|   | 6c1937f247 | ||
|   | 9db1e52a55 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f92c63135c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a3bf1a014b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 31fba48ad5 | ||
|   | 05dfa1bb1a | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f0f47aac3b | ||
|   | 9b42494667 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 42df951f89 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f4996424a2 | ||
|   | fd01302d9a | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 2daaa1cb9c | ||
|   | 9fde175c6b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f1b24e847e | ||
|   | 7a587de54e | ||
|   | eb69f95f83 | ||
|   | 359a3a4af9 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | a8b75e7814 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 2b898822d1 | ||
|   | fc9a0958d4 | ||
|   | 5843877cc8 | ||
|   | 913837f064 | ||
|   | 386ac5d779 | ||
|   | 3a1a4ade68 | ||
|   | 5d49f4007e | ||
|   | ca20c2d292 | ||
|   | f1ab24da99 | ||
|   | e16e851952 | ||
|   | 0b562a4b16 | ||
|   | 7734922059 | ||
|   | 54320c3dbf | ||
|   | 849cfed669 | ||
|   | 8932dfd504 | ||
|   | b9922b2f8e | ||
|   | 11fc5bc755 | ||
|   | 67ac4882f2 | ||
|   | 413171bb3c | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | b111eb2316 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 9bafabe3e9 | ||
|   | 202bc6440b | ||
|   | e2a89a55b7 | ||
|   | 1e05730ec7 | ||
|   | 885a63d3f6 | ||
|   | 206fbac618 | ||
|   | 4509661652 | ||
|   | 4669decfd0 | ||
|   | f05c204da3 | ||
|   | 338692d2c3 | ||
|   | 5415690585 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 418315d20b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 9bbffb6919 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | e338d63ec5 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 264aedbff3 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 4103ef362c | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | a04a449eb9 | ||
|   | 08633c197b | ||
|   | f8b3a429c6 | ||
|   | 946c8a59b4 | ||
|   | f93c7e1b6e | ||
|   | 6298534b9c | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 5175b42069 | ||
|   | e01e31341b | ||
|   | 203d900d16 | ||
|   | 4d9e9aaead | ||
|   | 82ec308be0 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | dcafbcb06c | ||
|   | aa5f8dc082 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 4dcae9c69c | ||
|   | 13a1af97da | ||
|   | e3c435fd78 | ||
|   | a32dee7071 | ||
|   | c098858b73 | ||
|   | 9e509e3bc9 | ||
|   | 79ac2a72fa | ||
|   | b063840f46 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 4366308b2b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 126826e52c | ||
|   | e31af5d31b | ||
|   | fca97cd734 | ||
|   | ca94267c44 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | df1f26cee7 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 24a4e075e6 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 43fcc6238e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | b40b96248b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c7ac4c7490 | ||
|   | 6cd8471b91 | ||
|   | 940eaa26e0 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 79e68ce125 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | e581d35432 | ||
|   | d3d578e0f4 | ||
|   | 79c71cbe48 | ||
|   | 82b50a1c5d | ||
|   | 6cfda78aa1 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f9ff938775 | ||
|   | 778fcab90d | ||
|   | 07e5aa30c6 | ||
|   | 3f0ec03a14 | ||
|   | 1bb871b9ac | ||
|   | 0e8783fb01 | ||
|   | 1d88c4465b | ||
|   | af2d575bf0 | ||
|   | 92165d776a | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | a8bbd8ab90 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 43ac9dbea7 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | bba9eca4e9 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 40f65b1980 | ||
|   | 23a33b10a1 | ||
|   | 67a93013c7 | ||
|   | 1f838d7529 | ||
|   | ffc0435144 | ||
|   | 5877d69c87 | ||
|   | 99035cea8f | ||
|   | 1b441a7eec | ||
|   | ad49e9f7b0 | ||
|   | e32b15ede2 | ||
|   | a35b4376ea | ||
|   | 619f9f76ee | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f771bc10db | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | b8889a1183 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | eb6b45eaed | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 31a748ed93 | ||
|   | 0110bdd24a | ||
|   | 365b712976 | ||
|   | 7d97dbe15b | ||
|   | 8bc0ea5a0b | ||
|   | 44948a3474 | ||
|   | bc51b53b4a | ||
|   | 67217b9dd0 | ||
|   | 487795b7c4 | ||
|   | a30e0d33f9 | ||
|   | 0c1b8abe03 | ||
|   | ce9c5149d5 | ||
|   | adbcdc62eb | ||
|   | faf872bfb8 | ||
|   | fe0fb2382a | ||
|   | cdd29295e5 | ||
|   | f7532f3476 | ||
|   | c8930cec87 | ||
|   | f9c336890d | ||
|   | c721de109f | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 1c95e8d6ec | ||
|   | 57cf2c1341 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f7d5c5f850 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 470f5127f4 | ||
|   | 34e361601a | ||
|   | 70d6cce8f8 | ||
|   | f9814f35d1 | ||
|   | f30603753e | ||
|   | ce9993fd36 | ||
|   | 4c2044e70a | ||
|   | 7f96c1fbe1 | ||
|   | 75e24780c1 | ||
|   | 95580bc4c0 | ||
|   | 175f68e0cf | ||
|   | b6efedfc8d | ||
|   | 23c21a35d8 | ||
|   | 9c7324298b | ||
|   | e92be566a0 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4e96ad5f28 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f64a1500af | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | c9e8619c04 | ||
|   | 7ab1133b45 | ||
|   | 77abfd3e61 | ||
|   | d7aaa41aa4 | ||
|   | 8223f6b155 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 435eae77fa | ||
|   | ead54e445f | ||
|   | 7ee5db2be5 | ||
|   | fef6f0ac94 | ||
|   | 7a60763786 | ||
|   | 94e321a364 | ||
|   | 1c12c2b714 | ||
|   | 442a8f11a7 | ||
|   | 4e8b58cd6c | ||
|   | a92dab46c2 | ||
|   | 468660d235 | ||
|   | c721afa137 | ||
|   | ac9654c1de | ||
|   | 570ad38bac | ||
|   | e778a9aa1d | ||
|   | 49576189af | ||
|   | 5d71d4c0a1 | ||
|   | d334b1ca7b | ||
|   | 5551e98388 | ||
|   | 59945cb2f8 | ||
|   | 500bc959f0 | ||
|   | deece20206 | ||
|   | fc8945be60 | ||
|   | 3fbd5f07a9 | ||
|   | ff9af2f980 | ||
|   | 62cba99491 | ||
|   | 5a5005c09c | ||
|   | dd179e1f4e | ||
|   | 27bdf80168 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f70ce7491a | ||
|   | 9f17f6a8cf | ||
|   | 4e51c7cf96 | ||
|   | 291c026da0 | ||
|   | dd88d8633f | ||
|   | 254ee8568b | ||
|   | cd631e8693 | ||
|   | 765812331b | ||
|   | 7462f8fbe3 | ||
|   | dc940f248c | ||
|   | 2793ca65cd | ||
|   | e687ddab21 | ||
|   | 4bd27e5055 | ||
|   | c6e2e07286 | ||
|   | e77508b8a8 | ||
|   | a5db44a167 | ||
|   | 265bbfc95d | ||
|   | 305cecb213 | ||
|   | 813feff12e | ||
|   | cbce6f633f | ||
|   | 1bbf45d35e | ||
|   | 76e53e9738 | ||
|   | c30e4a6935 | ||
|   | c4a700a55c | ||
|   | a759767d79 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 7f868c8140 | ||
|   | f7f37c24e2 | ||
|   | be02a8869f | ||
|   | 3a9f09cb47 | ||
|   | 0c2a9d85e0 | ||
|   | e72356033c | ||
|   | 3c48559df6 | ||
|   | f36d68c677 | ||
|   | af46b8221e | ||
|   | d25f72524b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0840d8a10e | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 597bf5def0 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 3478bd309b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 64b8b7658d | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | a1af8718a0 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | fd9e2b647d | ||
|   | caee4ba7bc | ||
|   | 915036006d | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 48887f2066 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 68d9ce7923 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | a36f3c8fb1 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 4dfadea9e9 | ||
|   | 71dc26edab | ||
|   | f260c95add | ||
|   | dc6f1efffb | ||
|   | b7763882f4 | ||
|   | 7de5c46f14 | ||
|   | 5920efa2b2 | ||
|   | d2194d55f9 | ||
|   | c0043af4c9 | ||
|   | dcf763438b | ||
|   | 858a00e28c | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | ab407e8274 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 14f96a6262 | ||
|   | 2b33c70e04 | ||
|   | 717443e2d6 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 2aba9099a0 | ||
|   | 3079f126a8 | ||
|   | 1cdfb746bf | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 39a1844991 | ||
|   | 9e4dc0d39e | ||
|   | ab91a4b814 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | ca66c02fb3 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 97bb052d71 | ||
|   | 137bb473c0 | ||
|   | 326b57f91b | ||
|   | 32feab6a70 | ||
|   | 68a0d04f04 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 9078ab4026 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 8605684906 | ||
|   | 9f17d17d6e | ||
|   | ba5f176d52 | ||
|   | 7115d14699 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 23e37daff3 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | ed6c2dfe39 | ||
|   | b48a28f2a6 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 3166fec7db | ||
|   | 1a67bd0414 | ||
|   | d34c43e292 | ||
|   | c7cfbb5b6c | ||
|   | bde2fd8202 | ||
|   | e5327c0903 | ||
|   | 1a0ca1b78f | ||
|   | ed141b1d12 | ||
|   | 5a7a71c551 | ||
|   | f09e0d187b | ||
|   | 7f6325fa5e | ||
|   | de292a8143 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 84b2005844 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 0d93432a2c | ||
|   | 8bc9927ee2 | ||
|   | 484bed4dab | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 3d7e243707 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f8a432c89e | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d484b2f63d | ||
|   | 30d9186031 | ||
|   | cd74367acc | ||
|   | 618cd9d9e5 | ||
|   | 0ff2f1bf75 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d28f1f07e7 | ||
|   | 6aa5bc2d8b | ||
|   | 76fc0c7ab1 | ||
|   | 7aa7019386 | ||
|   | b69f0964c9 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 2f9b6d000b | 
							
								
								
									
										8
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -21,12 +21,12 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         uses: actions/checkout@v4.2.2 | ||||
|         with: | ||||
|           ref: dev | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         uses: actions/setup-node@v4.1.0 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -57,12 +57,12 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         uses: actions/checkout@v4.2.2 | ||||
|         with: | ||||
|           ref: master | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         uses: actions/setup-node@v4.1.0 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
|   | ||||
							
								
								
									
										22
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -24,9 +24,9 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         uses: actions/checkout@v4.2.2 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         uses: actions/setup-node@v4.1.0 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -37,7 +37,7 @@ jobs: | ||||
|       - name: Build resources | ||||
|         run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages | ||||
|       - name: Setup lint cache | ||||
|         uses: actions/cache@v4.0.2 | ||||
|         uses: actions/cache@v4.1.2 | ||||
|         with: | ||||
|           path: | | ||||
|             node_modules/.cache/prettier | ||||
| @@ -58,9 +58,9 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         uses: actions/checkout@v4.2.2 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         uses: actions/setup-node@v4.1.0 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -76,9 +76,9 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         uses: actions/checkout@v4.2.2 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         uses: actions/setup-node@v4.1.0 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -89,7 +89,7 @@ jobs: | ||||
|         env: | ||||
|           IS_TEST: "true" | ||||
|       - name: Upload bundle stats | ||||
|         uses: actions/upload-artifact@v4.4.0 | ||||
|         uses: actions/upload-artifact@v4.4.3 | ||||
|         with: | ||||
|           name: frontend-bundle-stats | ||||
|           path: build/stats/*.json | ||||
| @@ -100,9 +100,9 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         uses: actions/checkout@v4.2.2 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         uses: actions/setup-node@v4.1.0 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -113,7 +113,7 @@ jobs: | ||||
|         env: | ||||
|           IS_TEST: "true" | ||||
|       - name: Upload bundle stats | ||||
|         uses: actions/upload-artifact@v4.4.0 | ||||
|         uses: actions/upload-artifact@v4.4.3 | ||||
|         with: | ||||
|           name: supervisor-bundle-stats | ||||
|           path: build/stats/*.json | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -23,7 +23,7 @@ jobs: | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         uses: actions/checkout@v4.2.2 | ||||
|         with: | ||||
|           # We must fetch at least the immediate parents so that if this is | ||||
|           # a pull request then we can checkout the head. | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -22,12 +22,12 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         uses: actions/checkout@v4.2.2 | ||||
|         with: | ||||
|           ref: dev | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         uses: actions/setup-node@v4.1.0 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -58,12 +58,12 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         uses: actions/checkout@v4.2.2 | ||||
|         with: | ||||
|           ref: master | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         uses: actions/setup-node@v4.1.0 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -16,10 +16,10 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         uses: actions/checkout@v4.2.2 | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         uses: actions/setup-node@v4.1.0 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -21,10 +21,10 @@ jobs: | ||||
|     if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         uses: actions/checkout@v4.2.2 | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         uses: actions/setup-node@v4.1.0 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -20,7 +20,7 @@ jobs: | ||||
|       contents: write | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         uses: actions/checkout@v4.2.2 | ||||
|  | ||||
|       - name: Set up Python ${{ env.PYTHON_VERSION }} | ||||
|         uses: actions/setup-python@v5 | ||||
| @@ -28,7 +28,7 @@ jobs: | ||||
|           python-version: ${{ env.PYTHON_VERSION }} | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         uses: actions/setup-node@v4.1.0 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -57,14 +57,14 @@ jobs: | ||||
|         run: tar -czvf translations.tar.gz translations | ||||
|  | ||||
|       - name: Upload build artifacts | ||||
|         uses: actions/upload-artifact@v4.4.0 | ||||
|         uses: actions/upload-artifact@v4.4.3 | ||||
|         with: | ||||
|           name: wheels | ||||
|           path: dist/home_assistant_frontend*.whl | ||||
|           if-no-files-found: error | ||||
|  | ||||
|       - name: Upload translations | ||||
|         uses: actions/upload-artifact@v4.4.0 | ||||
|         uses: actions/upload-artifact@v4.4.3 | ||||
|         with: | ||||
|           name: translations | ||||
|           path: translations.tar.gz | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -23,7 +23,7 @@ jobs: | ||||
|       contents: write # Required to upload release assets | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         uses: actions/checkout@v4.2.2 | ||||
|  | ||||
|       - name: Verify version | ||||
|         uses: home-assistant/actions/helpers/verify-version@master | ||||
| @@ -34,7 +34,7 @@ jobs: | ||||
|           python-version: ${{ env.PYTHON_VERSION }} | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         uses: actions/setup-node@v4.1.0 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/translations.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -13,7 +13,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         uses: actions/checkout@v4.2.2 | ||||
|  | ||||
|       - name: Upload Translations | ||||
|         run: | | ||||
|   | ||||
| @@ -1,16 +1,7 @@ | ||||
| diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
 | ||||
| index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b441f523f 100644
 | ||||
| index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa526090a00 100644
 | ||||
| --- a/modular/sortable.core.esm.js
 | ||||
| +++ b/modular/sortable.core.esm.js
 | ||||
| @@ -1461,7 +1461,7 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | ||||
|            } | ||||
|            target = parent; // store last element | ||||
|          } | ||||
| -        /* jshint boss:true */ while (parent = parent.parentNode);
 | ||||
| +        /* jshint boss:true */ while (parent = parent.parentNode || parent.getRootNode().host);
 | ||||
|        } | ||||
|        _unhideGhostForTarget(); | ||||
|      } | ||||
| @@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | ||||
|          } | ||||
|          if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) { | ||||
| @@ -33,7 +24,7 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b | ||||
|            } | ||||
|            parentEl = el; // actualization | ||||
|   | ||||
| @@ -1802,7 +1807,13 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | ||||
| @@ -1802,7 +1807,12 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | ||||
|          targetRect = getRect(target); | ||||
|          if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) { | ||||
|            capture(); | ||||
| @@ -44,11 +35,10 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b | ||||
| +          catch(err) {
 | ||||
| +            return completed(false);
 | ||||
| +          }
 | ||||
| +          
 | ||||
|            parentEl = el; // actualization | ||||
|   | ||||
|            changed(); | ||||
| @@ -1849,12 +1860,17 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | ||||
| @@ -1849,10 +1859,15 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | ||||
|            _silent = true; | ||||
|            setTimeout(_unsilent, 30); | ||||
|            capture(); | ||||
| @@ -56,8 +46,6 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b | ||||
| -            el.appendChild(dragEl);
 | ||||
| -          } else {
 | ||||
| -            target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
 | ||||
| -          }
 | ||||
|   | ||||
| +          try {
 | ||||
| +            if (after && !nextSibling) {
 | ||||
| +              el.appendChild(dragEl);
 | ||||
| @@ -67,7 +55,6 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b | ||||
| +          }
 | ||||
| +          catch(err) {
 | ||||
| +            return completed(false);
 | ||||
| +          }
 | ||||
|            } | ||||
|   | ||||
|            // Undo chrome's scroll adjustment (has no effect on other browsers) | ||||
|            if (scrolledPastTop) { | ||||
|              scrollBy(scrolledPastTop, 0, scrollBefore - scrolledPastTop.scrollTop); | ||||
| @@ -6,4 +6,4 @@ enableGlobalCache: false | ||||
|  | ||||
| nodeLinker: node-modules | ||||
|  | ||||
| yarnPath: .yarn/releases/yarn-4.4.1.cjs | ||||
| yarnPath: .yarn/releases/yarn-4.5.1.cjs | ||||
|   | ||||
| @@ -27,3 +27,5 @@ A complete guide can be found at the following [link](https://www.home-assistant | ||||
| Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects. | ||||
|  | ||||
| We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices. | ||||
|  | ||||
| [](https://www.openhomefoundation.org/) | ||||
|   | ||||
| @@ -15,23 +15,29 @@ const brotliOptions = { | ||||
| }; | ||||
| const zopfliOptions = { threshold: 150 }; | ||||
|  | ||||
| const compressDistBrotli = (rootDir, modernDir) => | ||||
| const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) => | ||||
|   gulp | ||||
|     .src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], { | ||||
|       base: rootDir, | ||||
|     }) | ||||
|     .src( | ||||
|       [ | ||||
|         `${modernDir}/**/${filesGlob}`, | ||||
|         compressServiceWorker ? `${rootDir}/sw-modern.js` : undefined, | ||||
|       ].filter(Boolean), | ||||
|       { | ||||
|         base: rootDir, | ||||
|       } | ||||
|     ) | ||||
|     .pipe(brotli(brotliOptions)) | ||||
|     .pipe(gulp.dest(rootDir)); | ||||
|  | ||||
| const compressDistZopfli = (rootDir, modernDir) => | ||||
| const compressDistZopfli = (rootDir, modernDir, compressModern = false) => | ||||
|   gulp | ||||
|     .src( | ||||
|       [ | ||||
|         `${rootDir}/**/${filesGlob}`, | ||||
|         `!${modernDir}/**/${filesGlob}`, | ||||
|         compressModern ? undefined : `!${modernDir}/**/${filesGlob}`, | ||||
|         `!${rootDir}/{sw-modern,service_worker}.js`, | ||||
|         `${rootDir}/{authorize,onboarding}.html`, | ||||
|       ], | ||||
|       ].filter(Boolean), | ||||
|       { base: rootDir } | ||||
|     ) | ||||
|     .pipe(zopfli(zopfliOptions)) | ||||
| @@ -40,12 +46,20 @@ const compressDistZopfli = (rootDir, modernDir) => | ||||
| const compressAppBrotli = () => | ||||
|   compressDistBrotli(paths.app_output_root, paths.app_output_latest); | ||||
| const compressHassioBrotli = () => | ||||
|   compressDistBrotli(paths.hassio_output_root, paths.hassio_output_latest); | ||||
|   compressDistBrotli( | ||||
|     paths.hassio_output_root, | ||||
|     paths.hassio_output_latest, | ||||
|     false | ||||
|   ); | ||||
|  | ||||
| const compressAppZopfli = () => | ||||
|   compressDistZopfli(paths.app_output_root, paths.app_output_latest); | ||||
| const compressHassioZopfli = () => | ||||
|   compressDistZopfli(paths.hassio_output_root, paths.hassio_output_latest); | ||||
|   compressDistZopfli( | ||||
|     paths.hassio_output_root, | ||||
|     paths.hassio_output_latest, | ||||
|     true | ||||
|   ); | ||||
|  | ||||
| gulp.task("compress-app", gulp.parallel(compressAppBrotli, compressAppZopfli)); | ||||
| gulp.task( | ||||
|   | ||||
| @@ -60,6 +60,12 @@ function copyPolyfills(staticDir) { | ||||
|     npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"), | ||||
|     staticPath("polyfills/") | ||||
|   ); | ||||
|  | ||||
|   // dialog-polyfill css | ||||
|   copyFileDir( | ||||
|     npmPath("dialog-polyfill/dialog-polyfill.css"), | ||||
|     staticPath("polyfills/") | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function copyLoaderJS(staticDir) { | ||||
|   | ||||
| @@ -24,8 +24,11 @@ const convertToJSON = async ( | ||||
| ) => { | ||||
|   let localeData; | ||||
|   try { | ||||
|     // use "pt" for "pt-BR", because "pt-BR" is unsupported by @formatjs | ||||
|     const language = lang === "pt-BR" ? "pt" : lang; | ||||
|  | ||||
|     localeData = await readFile( | ||||
|       join(formatjsDir, pkg, subDir, `${lang}.js`), | ||||
|       join(formatjsDir, pkg, subDir, `${language}.js`), | ||||
|       "utf-8" | ||||
|     ); | ||||
|   } catch (e) { | ||||
|   | ||||
| @@ -139,7 +139,7 @@ | ||||
|         </p> | ||||
|       </div> | ||||
|  | ||||
|       <div class="section-header">Wat does Home Assistant Cast do?</div> | ||||
|       <div class="section-header">What does Home Assistant Cast do?</div> | ||||
|       <div class="card-content"> | ||||
|         <p> | ||||
|           Home Assistant Cast is a receiver application for the Chromecast. When | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import "@material/mwc-button/mwc-button"; | ||||
| import { ActionDetail } from "@material/mwc-list/mwc-list"; | ||||
| import "@material/mwc-list/mwc-list"; | ||||
| import type { ActionDetail } from "@material/mwc-list/mwc-list"; | ||||
| import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js"; | ||||
| import { Auth, Connection } from "home-assistant-js-websocket"; | ||||
| import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; | ||||
| @@ -89,8 +90,8 @@ class HcCast extends LitElement { | ||||
|                       generateDefaultViewConfig({}, {}, {}, {}, () => ""), | ||||
|                     ] | ||||
|                   ).map( | ||||
|                     (view, idx) => | ||||
|                       html`<ha-list-item | ||||
|                     (view, idx) => html` | ||||
|                       <ha-list-item | ||||
|                         graphic="avatar" | ||||
|                         .activated=${this.castManager.status?.lovelacePath === | ||||
|                         (view.path ?? idx)} | ||||
| @@ -108,8 +109,9 @@ class HcCast extends LitElement { | ||||
|                           : html`<ha-svg-icon | ||||
|                               slot="item-icon" | ||||
|                               .path=${mdiViewDashboard} | ||||
|                             ></ha-svg-icon>`}</ha-list-item | ||||
|                       > ` | ||||
|                             ></ha-svg-icon>`} | ||||
|                       </ha-list-item> | ||||
|                     ` | ||||
|                   )}</mwc-list | ||||
|                 > | ||||
|               `} | ||||
|   | ||||
| @@ -88,7 +88,7 @@ class HcLayout extends LitElement { | ||||
|       } | ||||
|  | ||||
|       .card-header { | ||||
|         color: var(--ha-card-header-color, --primary-text-color); | ||||
|         color: var(--ha-card-header-color, var(--primary-text-color)); | ||||
|         font-family: var(--ha-card-header-font-family, inherit); | ||||
|         font-size: var(--ha-card-header-font-size, 24px); | ||||
|         letter-spacing: -0.012em; | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, query } from "lit/decorators"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { fireEvent } from "../../../../src/common/dom/fire_event"; | ||||
| import { LovelaceConfig } from "../../../../src/data/lovelace/config/types"; | ||||
| import { getPanelTitleFromUrlPath } from "../../../../src/data/panel"; | ||||
| import { Lovelace } from "../../../../src/panels/lovelace/types"; | ||||
| import "../../../../src/panels/lovelace/views/hui-view"; | ||||
| import "../../../../src/panels/lovelace/views/hui-view-container"; | ||||
| import { HomeAssistant } from "../../../../src/types"; | ||||
| import "./hc-launch-screen"; | ||||
|  | ||||
| @@ -22,8 +23,6 @@ class HcLovelace extends LitElement { | ||||
|  | ||||
|   @property() public urlPath: string | null = null; | ||||
|  | ||||
|   @query("hui-view") private _huiView?: HTMLElement; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const index = this._viewIndex; | ||||
|     if (index === undefined) { | ||||
| @@ -45,13 +44,24 @@ class HcLovelace extends LitElement { | ||||
|       saveConfig: async () => undefined, | ||||
|       deleteConfig: async () => undefined, | ||||
|       setEditMode: () => undefined, | ||||
|       showToast: () => undefined, | ||||
|     }; | ||||
|  | ||||
|     const viewConfig = this.lovelaceConfig.views[index]; | ||||
|     const background = viewConfig.background || this.lovelaceConfig.background; | ||||
|  | ||||
|     return html` | ||||
|       <hui-view | ||||
|       <hui-view-container | ||||
|         .hass=${this.hass} | ||||
|         .lovelace=${lovelace} | ||||
|         .index=${index} | ||||
|       ></hui-view> | ||||
|         .background=${background} | ||||
|         .theme=${viewConfig.theme} | ||||
|       > | ||||
|         <hui-view | ||||
|           .hass=${this.hass} | ||||
|           .lovelace=${lovelace} | ||||
|           .index=${index} | ||||
|         ></hui-view> | ||||
|       </hui-view-container> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
| @@ -81,26 +91,6 @@ class HcLovelace extends LitElement { | ||||
|                 }${viewTitle || ""}` | ||||
|               : undefined, | ||||
|         }); | ||||
|  | ||||
|         const configBackground = | ||||
|           this.lovelaceConfig.views[index].background || | ||||
|           this.lovelaceConfig.background; | ||||
|  | ||||
|         const backgroundStyle = | ||||
|           typeof configBackground === "string" | ||||
|             ? configBackground | ||||
|             : configBackground?.image | ||||
|               ? `center / cover no-repeat url('${configBackground.image}')` | ||||
|               : undefined; | ||||
|  | ||||
|         if (backgroundStyle) { | ||||
|           this._huiView!.style.setProperty( | ||||
|             "--lovelace-background", | ||||
|             backgroundStyle | ||||
|           ); | ||||
|         } else { | ||||
|           this._huiView!.style.removeProperty("--lovelace-background"); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @@ -124,19 +114,15 @@ class HcLovelace extends LitElement { | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       :host { | ||||
|         min-height: 100vh; | ||||
|         height: 0; | ||||
|       hui-view-container { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         position: relative; | ||||
|         min-height: 100vh; | ||||
|         box-sizing: border-box; | ||||
|         background: var(--primary-background-color); | ||||
|       } | ||||
|       :host > * { | ||||
|         flex: 1; | ||||
|       } | ||||
|       hui-view { | ||||
|         background: var(--lovelace-background, var(--primary-background-color)); | ||||
|         flex: 1 1 100%; | ||||
|         max-width: 100%; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -36,6 +36,7 @@ import { HassElement } from "../../../../src/state/hass-element"; | ||||
| import { castContext } from "../cast_context"; | ||||
| import "./hc-launch-screen"; | ||||
| import { getPanelTitleFromUrlPath } from "../../../../src/data/panel"; | ||||
| import { checkLovelaceConfig } from "../../../../src/panels/lovelace/common/check-lovelace-config"; | ||||
|  | ||||
| const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = { | ||||
|   strategy: { | ||||
| @@ -365,7 +366,9 @@ export class HcMain extends HassElement { | ||||
|       this._urlPath || "lovelace" | ||||
|     ); | ||||
|     castContext.setApplicationState(title || ""); | ||||
|     this._lovelaceConfig = lovelaceConfig; | ||||
|     this._lovelaceConfig = checkLovelaceConfig( | ||||
|       lovelaceConfig | ||||
|     ) as LovelaceConfig; | ||||
|   } | ||||
|  | ||||
|   private _handleShowDemo(_msg: ShowDemoMessage) { | ||||
|   | ||||
| @@ -111,9 +111,47 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) => | ||||
|         friendly_name: "Living room Temperature", | ||||
|       }, | ||||
|     }, | ||||
|     "sensor.living_room_humidity": { | ||||
|       entity_id: "sensor.living_room_humidity", | ||||
|       state: "57", | ||||
|       attributes: { | ||||
|         state_class: "measurement", | ||||
|         unit_of_measurement: "%", | ||||
|         device_class: "humidity", | ||||
|         friendly_name: "Living room Humidity", | ||||
|       }, | ||||
|     }, | ||||
|     "sensor.outdoor_temperature": { | ||||
|       entity_id: "sensor.outdoor_temperature", | ||||
|       state: "10.5", | ||||
|       attributes: { | ||||
|         state_class: "measurement", | ||||
|         unit_of_measurement: "°C", | ||||
|         device_class: "temperature", | ||||
|         friendly_name: "Outdoor temperature", | ||||
|       }, | ||||
|     }, | ||||
|     "sensor.outdoor_humidity": { | ||||
|       entity_id: "sensor.outdoor_humidity", | ||||
|       state: "70.4", | ||||
|       attributes: { | ||||
|         state_class: "measurement", | ||||
|         unit_of_measurement: "%", | ||||
|         device_class: "humidity", | ||||
|         friendly_name: "Outdoor humidity", | ||||
|       }, | ||||
|     }, | ||||
|     "device_tracker.car": { | ||||
|       entity_id: "sensor.outdoor_humidity", | ||||
|       state: "not_home", | ||||
|       attributes: { | ||||
|         friendly_name: "Car", | ||||
|         icon: "mdi:car", | ||||
|       }, | ||||
|     }, | ||||
|     "media_player.living_room_nest_mini": { | ||||
|       entity_id: "media_player.living_room_nest_mini", | ||||
|       state: "on", | ||||
|       state: "playing", | ||||
|       attributes: { | ||||
|         device_class: "speaker", | ||||
|         volume_level: 0.18, | ||||
| @@ -161,6 +199,14 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) => | ||||
|         supported_features: 32, | ||||
|       }, | ||||
|     }, | ||||
|     "binary_sensor.kitchen_motion": { | ||||
|       entity_id: "light.kitchen_motion", | ||||
|       state: "on", | ||||
|       attributes: { | ||||
|         device_class: "motion", | ||||
|         friendly_name: "Kitchen motion", | ||||
|       }, | ||||
|     }, | ||||
|     "light.worktop_spotlights": { | ||||
|       entity_id: "light.worktop_spotlights", | ||||
|       state: "off", | ||||
| @@ -395,6 +441,14 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) => | ||||
|         supported_features: 64063, | ||||
|       }, | ||||
|     }, | ||||
|     "switch.in_meeting": { | ||||
|       entity_id: "switch.in_meeting", | ||||
|       state: "on", | ||||
|       attributes: { | ||||
|         icon: "mdi:laptop-account", | ||||
|         friendly_name: "In a meeting", | ||||
|       }, | ||||
|     }, | ||||
|     "sensor.standing_desk_height": { | ||||
|       entity_id: "sensor.standing_desk_height", | ||||
|       state: "72", | ||||
|   | ||||
| @@ -9,17 +9,57 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({ | ||||
|       title: isFrontpageEmbed ? "Home Assistant" : "Demo", | ||||
|       path: "home", | ||||
|       icon: "mdi:home-assistant", | ||||
|       badges: [ | ||||
|         { | ||||
|           type: "entity", | ||||
|           entity: "sensor.outdoor_temperature", | ||||
|           color: "red", | ||||
|         }, | ||||
|         { | ||||
|           type: "entity", | ||||
|           entity: "sensor.outdoor_humidity", | ||||
|           color: "indigo", | ||||
|         }, | ||||
|         { | ||||
|           type: "entity", | ||||
|           entity: "device_tracker.car", | ||||
|         }, | ||||
|       ], | ||||
|       sections: [ | ||||
|         ...(isFrontpageEmbed | ||||
|           ? [] | ||||
|           : [ | ||||
|               { | ||||
|                 title: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`, | ||||
|                 cards: [{ type: "custom:ha-demo-card" }], | ||||
|                 cards: [ | ||||
|                   { | ||||
|                     type: "heading", | ||||
|                     heading: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`, | ||||
|                   }, | ||||
|                   { type: "custom:ha-demo-card" }, | ||||
|                 ], | ||||
|               }, | ||||
|             ]), | ||||
|         { | ||||
|           cards: [ | ||||
|             { | ||||
|               type: "heading", | ||||
|               heading: localize( | ||||
|                 "ui.panel.page-demo.config.sections.titles.living_room" | ||||
|               ), | ||||
|               icon: "mdi:sofa", | ||||
|               badges: [ | ||||
|                 { | ||||
|                   type: "entity", | ||||
|                   entity: "sensor.living_room_temperature", | ||||
|                   color: "red", | ||||
|                 }, | ||||
|                 { | ||||
|                   type: "entity", | ||||
|                   entity: "sensor.living_room_humidity", | ||||
|                   color: "indigo", | ||||
|                 }, | ||||
|               ], | ||||
|             }, | ||||
|             { | ||||
|               type: "tile", | ||||
|               entity: "light.floor_lamp", | ||||
| @@ -38,13 +78,6 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({ | ||||
|               type: "tile", | ||||
|               entity: "light.bar_lamp", | ||||
|             }, | ||||
|             { | ||||
|               graph: "line", | ||||
|               type: "sensor", | ||||
|               entity: "sensor.living_room_temperature", | ||||
|               detail: 1, | ||||
|               name: "Temperature", | ||||
|             }, | ||||
|             { | ||||
|               type: "tile", | ||||
|               entity: "cover.living_room_garden_shutter", | ||||
| @@ -55,11 +88,25 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({ | ||||
|               entity: "media_player.living_room_nest_mini", | ||||
|             }, | ||||
|           ], | ||||
|           title: `🛋️ ${localize("ui.panel.page-demo.config.sections.titles.living_room")} `, | ||||
|         }, | ||||
|         { | ||||
|           type: "grid", | ||||
|           cards: [ | ||||
|             { | ||||
|               type: "heading", | ||||
|               heading: localize( | ||||
|                 "ui.panel.page-demo.config.sections.titles.kitchen" | ||||
|               ), | ||||
|               icon: "mdi:fridge", | ||||
|               badges: [ | ||||
|                 { | ||||
|                   type: "entity", | ||||
|                   entity: "binary_sensor.kitchen_motion", | ||||
|                   show_state: false, | ||||
|                   color: "blue", | ||||
|                 }, | ||||
|               ], | ||||
|             }, | ||||
|             { | ||||
|               type: "tile", | ||||
|               entity: "cover.kitchen_shutter", | ||||
| @@ -90,11 +137,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({ | ||||
|               entity: "media_player.kitchen_nest_audio", | ||||
|             }, | ||||
|           ], | ||||
|           title: `👩🍳 ${localize("ui.panel.page-demo.config.sections.titles.kitchen")}`, | ||||
|         }, | ||||
|         { | ||||
|           type: "grid", | ||||
|           cards: [ | ||||
|             { | ||||
|               type: "heading", | ||||
|               heading: localize( | ||||
|                 "ui.panel.page-demo.config.sections.titles.energy" | ||||
|               ), | ||||
|               icon: "mdi:transmission-tower", | ||||
|             }, | ||||
|             { | ||||
|               type: "tile", | ||||
|               entity: "binary_sensor.tesla_wall_connector_vehicle_connected", | ||||
| @@ -132,11 +185,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({ | ||||
|               color: "dark-grey", | ||||
|             }, | ||||
|           ], | ||||
|           title: `⚡️ ${localize("ui.panel.page-demo.config.sections.titles.energy")}`, | ||||
|         }, | ||||
|         { | ||||
|           type: "grid", | ||||
|           cards: [ | ||||
|             { | ||||
|               type: "heading", | ||||
|               heading: localize( | ||||
|                 "ui.panel.page-demo.config.sections.titles.climate" | ||||
|               ), | ||||
|               icon: "mdi:thermometer", | ||||
|             }, | ||||
|             { | ||||
|               type: "tile", | ||||
|               entity: "sun.sun", | ||||
| @@ -169,16 +228,38 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({ | ||||
|               state_content: ["preset_mode", "current_temperature"], | ||||
|             }, | ||||
|           ], | ||||
|           title: `🌤️ ${localize("ui.panel.page-demo.config.sections.titles.climate")}`, | ||||
|         }, | ||||
|         { | ||||
|           type: "grid", | ||||
|           cards: [ | ||||
|             { | ||||
|               type: "heading", | ||||
|               heading: localize( | ||||
|                 "ui.panel.page-demo.config.sections.titles.study" | ||||
|               ), | ||||
|               icon: "mdi:desk-lamp", | ||||
|               badges: [ | ||||
|                 { | ||||
|                   type: "entity", | ||||
|                   entity: "switch.in_meeting", | ||||
|                   state: "on", | ||||
|                   state_content: "name", | ||||
|                   visibility: [ | ||||
|                     { | ||||
|                       condition: "state", | ||||
|                       state: "on", | ||||
|                       entity: "switch.in_meeting", | ||||
|                     }, | ||||
|                   ], | ||||
|                 }, | ||||
|               ], | ||||
|             }, | ||||
|             { | ||||
|               type: "tile", | ||||
|               entity: "cover.study_shutter", | ||||
|               name: "Shutter", | ||||
|             }, | ||||
|  | ||||
|             { | ||||
|               type: "tile", | ||||
|               entity: "light.study_spotlights", | ||||
| @@ -195,12 +276,23 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({ | ||||
|               color: "brown", | ||||
|               icon: "mdi:desk", | ||||
|             }, | ||||
|             { | ||||
|               type: "tile", | ||||
|               entity: "switch.in_meeting", | ||||
|               name: "Meeting mode", | ||||
|             }, | ||||
|           ], | ||||
|           title: `🧑💻 ${localize("ui.panel.page-demo.config.sections.titles.study")}`, | ||||
|         }, | ||||
|         { | ||||
|           type: "grid", | ||||
|           cards: [ | ||||
|             { | ||||
|               type: "heading", | ||||
|               heading: localize( | ||||
|                 "ui.panel.page-demo.config.sections.titles.outdoor" | ||||
|               ), | ||||
|               icon: "mdi:tree", | ||||
|             }, | ||||
|             { | ||||
|               type: "tile", | ||||
|               entity: "light.outdoor_light", | ||||
| @@ -230,11 +322,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({ | ||||
|               name: "Illuminance", | ||||
|             }, | ||||
|           ], | ||||
|           title: `🌳 ${localize("ui.panel.page-demo.config.sections.titles.outdoor")}`, | ||||
|         }, | ||||
|         { | ||||
|           type: "grid", | ||||
|           cards: [ | ||||
|             { | ||||
|               type: "heading", | ||||
|               heading: localize( | ||||
|                 "ui.panel.page-demo.config.sections.titles.updates" | ||||
|               ), | ||||
|               icon: "mdi:update", | ||||
|             }, | ||||
|             { | ||||
|               type: "tile", | ||||
|               entity: "automation.home_assistant_auto_update", | ||||
| @@ -260,7 +358,6 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({ | ||||
|               icon: "mdi:home-assistant", | ||||
|             }, | ||||
|           ], | ||||
|           title: `🎉 ${localize("ui.panel.page-demo.config.sections.titles.updates")}`, | ||||
|         }, | ||||
|       ], | ||||
|     }, | ||||
|   | ||||
							
								
								
									
										9
									
								
								demo/src/stubs/config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; | ||||
|  | ||||
| export const mockConfig = (hass: MockHomeAssistant) => { | ||||
|   hass.mockWS("validate_config", () => ({ | ||||
|     actions: { valid: true }, | ||||
|     conditions: { valid: true }, | ||||
|     triggers: { valid: true }, | ||||
|   })); | ||||
| }; | ||||
							
								
								
									
										6
									
								
								demo/src/stubs/tags.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | ||||
| import { Tag } from "../../../src/data/tag"; | ||||
| import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; | ||||
|  | ||||
| export const mockTags = (hass: MockHomeAssistant) => { | ||||
|   hass.mockWS("tag/list", () => [{ id: "my-tag", name: "My Tag" }] as Tag[]); | ||||
| }; | ||||
| @@ -217,22 +217,22 @@ export const basicTrace: DemoTrace = { | ||||
|       id: "1615419646544", | ||||
|       alias: "Ensure Party mode", | ||||
|       description: "", | ||||
|       trigger: [ | ||||
|       triggers: [ | ||||
|         { | ||||
|           platform: "state", | ||||
|           trigger: "state", | ||||
|           entity_id: "input_boolean.toggle_1", | ||||
|         }, | ||||
|       ], | ||||
|       condition: [ | ||||
|       conditions: [ | ||||
|         { | ||||
|           condition: "template", | ||||
|           alias: "Test if Paulus is home", | ||||
|           value_template: "{{ true }}", | ||||
|         }, | ||||
|       ], | ||||
|       action: [ | ||||
|       actions: [ | ||||
|         { | ||||
|           service: "input_boolean.toggle", | ||||
|           action: "input_boolean.toggle", | ||||
|           target: { | ||||
|             entity_id: "input_boolean.toggle_4", | ||||
|           }, | ||||
| @@ -268,7 +268,7 @@ export const basicTrace: DemoTrace = { | ||||
|           ], | ||||
|           default: [ | ||||
|             { | ||||
|               service: "input_boolean.toggle", | ||||
|               action: "input_boolean.toggle", | ||||
|               alias: "Toggle 2", | ||||
|               target: { | ||||
|                 entity_id: "input_boolean.toggle_2", | ||||
| @@ -277,7 +277,7 @@ export const basicTrace: DemoTrace = { | ||||
|           ], | ||||
|         }, | ||||
|         { | ||||
|           service: "input_boolean.toggle", | ||||
|           action: "input_boolean.toggle", | ||||
|           target: { | ||||
|             entity_id: "input_boolean.toggle_4", | ||||
|           }, | ||||
|   | ||||
| @@ -31,8 +31,8 @@ export const mockDemoTrace = ( | ||||
|       ], | ||||
|     }, | ||||
|     config: { | ||||
|       trigger: [], | ||||
|       action: [], | ||||
|       triggers: [], | ||||
|       actions: [], | ||||
|     }, | ||||
|     context: { | ||||
|       id: "abcd", | ||||
|   | ||||
| @@ -133,17 +133,17 @@ export const motionLightTrace: DemoTrace = { | ||||
|     config: { | ||||
|       mode: "restart", | ||||
|       max_exceeded: "silent", | ||||
|       trigger: [ | ||||
|       triggers: [ | ||||
|         { | ||||
|           platform: "state", | ||||
|           trigger: "state", | ||||
|           entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use", | ||||
|           from: "off", | ||||
|           to: "on", | ||||
|         }, | ||||
|       ], | ||||
|       action: [ | ||||
|       actions: [ | ||||
|         { | ||||
|           service: "light.turn_on", | ||||
|           action: "light.turn_on", | ||||
|           target: { | ||||
|             entity_id: "light.elgato_key_light_air", | ||||
|           }, | ||||
| @@ -162,7 +162,7 @@ export const motionLightTrace: DemoTrace = { | ||||
|           delay: 0, | ||||
|         }, | ||||
|         { | ||||
|           service: "light.turn_off", | ||||
|           action: "light.turn_off", | ||||
|           target: { | ||||
|             entity_id: "light.elgato_key_light_air", | ||||
|           }, | ||||
|   | ||||
| @@ -48,7 +48,7 @@ const ACTIONS = [ | ||||
|   { | ||||
|     wait_for_trigger: [ | ||||
|       { | ||||
|         platform: "state", | ||||
|         trigger: "state", | ||||
|         entity_id: "input_boolean.toggle_1", | ||||
|       }, | ||||
|     ], | ||||
| @@ -121,7 +121,7 @@ const ACTIONS = [ | ||||
| ]; | ||||
|  | ||||
| const initialAction: Action = { | ||||
|   service: "light.turn_on", | ||||
|   action: "light.turn_on", | ||||
|   target: { | ||||
|     entity_id: "light.kitchen", | ||||
|   }, | ||||
| @@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement { | ||||
|         <div class="action"> | ||||
|           <span> | ||||
|             ${this._action | ||||
|               ? describeAction(this.hass, [], [], [], this._action) | ||||
|               ? describeAction(this.hass, [], [], {}, this._action) | ||||
|               : "<invalid YAML>"} | ||||
|           </span> | ||||
|           <ha-yaml-editor | ||||
| @@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement { | ||||
|         ${ACTIONS.map( | ||||
|           (conf) => html` | ||||
|             <div class="action"> | ||||
|               <span>${describeAction(this.hass, [], [], [], conf as any)}</span> | ||||
|               <span>${describeAction(this.hass, [], [], {}, conf as any)}</span> | ||||
|               <pre>${dump(conf)}</pre> | ||||
|             </div> | ||||
|           ` | ||||
|   | ||||
| @@ -22,46 +22,52 @@ const ENTITIES = [ | ||||
| ]; | ||||
|  | ||||
| const triggers = [ | ||||
|   { platform: "state", entity_id: "light.kitchen", from: "off", to: "on" }, | ||||
|   { platform: "mqtt" }, | ||||
|   { trigger: "state", entity_id: "light.kitchen", from: "off", to: "on" }, | ||||
|   { trigger: "mqtt" }, | ||||
|   { | ||||
|     platform: "geo_location", | ||||
|     trigger: "geo_location", | ||||
|     source: "test_source", | ||||
|     zone: "zone.home", | ||||
|     event: "enter", | ||||
|   }, | ||||
|   { platform: "homeassistant", event: "start" }, | ||||
|   { trigger: "homeassistant", event: "start" }, | ||||
|   { | ||||
|     platform: "numeric_state", | ||||
|     trigger: "numeric_state", | ||||
|     entity_id: "light.kitchen", | ||||
|     attribute: "brightness", | ||||
|     below: 80, | ||||
|     above: 20, | ||||
|   }, | ||||
|   { platform: "sun", event: "sunset" }, | ||||
|   { platform: "time_pattern" }, | ||||
|   { platform: "time_pattern", hours: "*", minutes: "/5", seconds: "10" }, | ||||
|   { platform: "webhook" }, | ||||
|   { platform: "persistent_notification" }, | ||||
|   { trigger: "sun", event: "sunset" }, | ||||
|   { trigger: "time_pattern" }, | ||||
|   { trigger: "time_pattern", hours: "*", minutes: "/5", seconds: "10" }, | ||||
|   { trigger: "webhook" }, | ||||
|   { trigger: "persistent_notification" }, | ||||
|   { | ||||
|     platform: "zone", | ||||
|     trigger: "zone", | ||||
|     entity_id: "person.person", | ||||
|     zone: "zone.home", | ||||
|     event: "enter", | ||||
|   }, | ||||
|   { platform: "tag" }, | ||||
|   { platform: "time", at: "15:32" }, | ||||
|   { platform: "template" }, | ||||
|   { platform: "conversation", command: "Turn on the lights" }, | ||||
|   { trigger: "tag" }, | ||||
|   { trigger: "time", at: "15:32" }, | ||||
|   { trigger: "template" }, | ||||
|   { trigger: "conversation", command: "Turn on the lights" }, | ||||
|   { | ||||
|     platform: "conversation", | ||||
|     trigger: "conversation", | ||||
|     command: ["Turn on the lights", "Turn the lights on"], | ||||
|   }, | ||||
|   { platform: "event", event_type: "homeassistant_started" }, | ||||
|   { trigger: "event", event_type: "homeassistant_started" }, | ||||
|   { | ||||
|     triggers: [ | ||||
|       { trigger: "state", entity_id: "light.kitchen", to: "on" }, | ||||
|       { trigger: "state", entity_id: "light.kitchen", to: "off" }, | ||||
|     ], | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| const initialTrigger: Trigger = { | ||||
|   platform: "state", | ||||
|   trigger: "state", | ||||
|   entity_id: "light.kitchen", | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -8,6 +8,9 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry"; | ||||
| import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry"; | ||||
| import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry"; | ||||
| import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor"; | ||||
| import { mockConfig } from "../../../../demo/src/stubs/config"; | ||||
| import { mockTags } from "../../../../demo/src/stubs/tags"; | ||||
| import { mockAuth } from "../../../../demo/src/stubs/auth"; | ||||
| import type { Trigger } from "../../../../src/data/automation"; | ||||
| import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location"; | ||||
| import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event"; | ||||
| @@ -26,6 +29,7 @@ import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger | ||||
| import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt"; | ||||
| import "../../../../src/panels/config/automation/trigger/ha-automation-trigger"; | ||||
| import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation"; | ||||
| import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list"; | ||||
|  | ||||
| const SCHEMAS: { name: string; triggers: Trigger[] }[] = [ | ||||
|   { | ||||
| @@ -111,11 +115,15 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [ | ||||
|     triggers: [ | ||||
|       { ...HaConversationTrigger.defaultConfig }, | ||||
|       { | ||||
|         platform: "conversation", | ||||
|         trigger: "conversation", | ||||
|         command: ["Turn on the lights", "Turn the lights on"], | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     name: "Trigger list", | ||||
|     triggers: [{ ...HaTriggerList.defaultConfig }], | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| @customElement("demo-automation-editor-trigger") | ||||
| @@ -135,6 +143,9 @@ export class DemoAutomationEditorTrigger extends LitElement { | ||||
|     mockDeviceRegistry(hass); | ||||
|     mockAreaRegistry(hass); | ||||
|     mockHassioSupervisor(hass); | ||||
|     mockConfig(hass); | ||||
|     mockTags(hass); | ||||
|     mockAuth(hass); | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|   | ||||
| @@ -64,6 +64,7 @@ const DEVICES: DeviceRegistryEntry[] = [ | ||||
|     labels: [], | ||||
|     created_at: 0, | ||||
|     modified_at: 0, | ||||
|     primary_config_entry: null, | ||||
|   }, | ||||
|   { | ||||
|     area_id: "backyard", | ||||
| @@ -86,6 +87,7 @@ const DEVICES: DeviceRegistryEntry[] = [ | ||||
|     labels: [], | ||||
|     created_at: 0, | ||||
|     modified_at: 0, | ||||
|     primary_config_entry: null, | ||||
|   }, | ||||
|   { | ||||
|     area_id: null, | ||||
| @@ -108,6 +110,7 @@ const DEVICES: DeviceRegistryEntry[] = [ | ||||
|     labels: [], | ||||
|     created_at: 0, | ||||
|     modified_at: 0, | ||||
|     primary_config_entry: null, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
|   | ||||
| @@ -64,6 +64,7 @@ const DEVICES: DeviceRegistryEntry[] = [ | ||||
|     labels: [], | ||||
|     created_at: 0, | ||||
|     modified_at: 0, | ||||
|     primary_config_entry: null, | ||||
|   }, | ||||
|   { | ||||
|     area_id: "backyard", | ||||
| @@ -86,6 +87,7 @@ const DEVICES: DeviceRegistryEntry[] = [ | ||||
|     labels: [], | ||||
|     created_at: 0, | ||||
|     modified_at: 0, | ||||
|     primary_config_entry: null, | ||||
|   }, | ||||
|   { | ||||
|     area_id: null, | ||||
| @@ -108,6 +110,7 @@ const DEVICES: DeviceRegistryEntry[] = [ | ||||
|     labels: [], | ||||
|     created_at: 0, | ||||
|     modified_at: 0, | ||||
|     primary_config_entry: null, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
|   | ||||
| @@ -232,6 +232,7 @@ const createDeviceRegistryEntries = ( | ||||
|     labels: [], | ||||
|     created_at: 0, | ||||
|     modified_at: 0, | ||||
|     primary_config_entry: null, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
|   | ||||
| @@ -417,7 +417,7 @@ class HassioAddonConfig extends LitElement { | ||||
|           justify-content: space-between; | ||||
|         } | ||||
|         .header h2 { | ||||
|           color: var(--ha-card-header-color, --primary-text-color); | ||||
|           color: var(--ha-card-header-color, var(--primary-text-color)); | ||||
|           font-family: var(--ha-card-header-font-family, inherit); | ||||
|           font-size: var(--ha-card-header-font-size, 24px); | ||||
|           letter-spacing: -0.012em; | ||||
|   | ||||
| @@ -37,7 +37,6 @@ import "./config/hassio-addon-config"; | ||||
| import "./config/hassio-addon-network"; | ||||
| import "./hassio-addon-router"; | ||||
| import "./info/hassio-addon-info"; | ||||
| import "./log/hassio-addon-logs"; | ||||
|  | ||||
| @customElement("hassio-addon-dashboard") | ||||
| class HassioAddonDashboard extends LitElement { | ||||
| @@ -161,16 +160,11 @@ class HassioAddonDashboard extends LitElement { | ||||
|           margin-bottom: 24px; | ||||
|           width: 600px; | ||||
|         } | ||||
|         hassio-addon-logs { | ||||
|           max-width: calc(100% - 8px); | ||||
|           min-width: 600px; | ||||
|         } | ||||
|         @media only screen and (max-width: 600px) { | ||||
|           hassio-addon-info, | ||||
|           hassio-addon-network, | ||||
|           hassio-addon-audio, | ||||
|           hassio-addon-config, | ||||
|           hassio-addon-logs { | ||||
|           hassio-addon-config { | ||||
|             max-width: 100%; | ||||
|             min-width: 100%; | ||||
|           } | ||||
|   | ||||
| @@ -2,7 +2,8 @@ import "@material/mwc-button"; | ||||
| import { | ||||
|   mdiCheckCircle, | ||||
|   mdiChip, | ||||
|   mdiCircle, | ||||
|   mdiPlayCircle, | ||||
|   mdiCircleOffOutline, | ||||
|   mdiCursorDefaultClickOutline, | ||||
|   mdiDocker, | ||||
|   mdiExclamationThick, | ||||
| @@ -37,6 +38,7 @@ import "../../../../src/components/ha-markdown"; | ||||
| import "../../../../src/components/ha-settings-row"; | ||||
| import "../../../../src/components/ha-svg-icon"; | ||||
| import "../../../../src/components/ha-switch"; | ||||
| import type { HaSwitch } from "../../../../src/components/ha-switch"; | ||||
| import { | ||||
|   AddonCapability, | ||||
|   HassioAddonDetails, | ||||
| @@ -198,7 +200,7 @@ class HassioAddonInfo extends LitElement { | ||||
|                               "dashboard.addon_running" | ||||
|                             )} | ||||
|                             class="running" | ||||
|                             .path=${mdiCircle} | ||||
|                             .path=${mdiPlayCircle} | ||||
|                           ></ha-svg-icon> | ||||
|                         ` | ||||
|                       : html` | ||||
| @@ -207,7 +209,7 @@ class HassioAddonInfo extends LitElement { | ||||
|                               "dashboard.addon_stopped" | ||||
|                             )} | ||||
|                             class="stopped" | ||||
|                             .path=${mdiCircle} | ||||
|                             .path=${mdiCircleOffOutline} | ||||
|                           ></ha-svg-icon> | ||||
|                         `} | ||||
|                   ` | ||||
| @@ -1118,12 +1120,28 @@ class HassioAddonInfo extends LitElement { | ||||
|   private async _uninstallClicked(ev: CustomEvent): Promise<void> { | ||||
|     const button = ev.currentTarget as any; | ||||
|     button.progress = true; | ||||
|     let removeData = false; | ||||
|     const _removeDataToggled = (e: Event) => { | ||||
|       removeData = (e.target as HaSwitch).checked; | ||||
|     }; | ||||
|  | ||||
|     const confirmed = await showConfirmationDialog(this, { | ||||
|       title: this.supervisor.localize("dialog.uninstall_addon.title", { | ||||
|         name: this.addon.name, | ||||
|       }), | ||||
|       text: this.supervisor.localize("dialog.uninstall_addon.text"), | ||||
|       text: html` | ||||
|         <ha-formfield | ||||
|           .label=${html`<p> | ||||
|             ${this.supervisor.localize("dialog.uninstall_addon.remove_data")} | ||||
|           </p>`} | ||||
|         > | ||||
|           <ha-switch | ||||
|             @change=${_removeDataToggled} | ||||
|             .checked=${removeData} | ||||
|             haptic | ||||
|           ></ha-switch> | ||||
|         </ha-formfield> | ||||
|       `, | ||||
|       confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"), | ||||
|       dismissText: this.supervisor.localize("common.cancel"), | ||||
|       destructive: true, | ||||
| @@ -1136,7 +1154,7 @@ class HassioAddonInfo extends LitElement { | ||||
|  | ||||
|     this._error = undefined; | ||||
|     try { | ||||
|       await uninstallHassioAddon(this.hass, this.addon.slug); | ||||
|       await uninstallHassioAddon(this.hass, this.addon.slug, removeData); | ||||
|       const eventdata = { | ||||
|         success: true, | ||||
|         response: undefined, | ||||
| @@ -1191,7 +1209,7 @@ class HassioAddonInfo extends LitElement { | ||||
|           padding-inline-start: 8px; | ||||
|           padding-inline-end: initial; | ||||
|           font-size: 24px; | ||||
|           color: var(--ha-card-header-color, --primary-text-color); | ||||
|           color: var(--ha-card-header-color, var(--primary-text-color)); | ||||
|         } | ||||
|         .addon-version { | ||||
|           float: var(--float-end); | ||||
|   | ||||
| @@ -1,12 +1,14 @@ | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import "../../../../src/components/ha-circular-progress"; | ||||
| import { HassioAddonDetails } from "../../../../src/data/hassio/addon"; | ||||
| import { Supervisor } from "../../../../src/data/supervisor/supervisor"; | ||||
| import { haStyle } from "../../../../src/resources/styles"; | ||||
| import { HomeAssistant } from "../../../../src/types"; | ||||
| import { hassioStyle } from "../../resources/hassio-style"; | ||||
| import "./hassio-addon-logs"; | ||||
| import "../../../../src/panels/config/logs/error-log-card"; | ||||
| import "../../../../src/components/search-input"; | ||||
| import { extractSearchParam } from "../../../../src/common/url/search-params"; | ||||
|  | ||||
| @customElement("hassio-addon-log-tab") | ||||
| class HassioAddonLogDashboard extends LitElement { | ||||
| @@ -16,6 +18,8 @@ class HassioAddonLogDashboard extends LitElement { | ||||
|  | ||||
|   @property({ attribute: false }) public addon?: HassioAddonDetails; | ||||
|  | ||||
|   @state() private _filter = extractSearchParam("filter") || ""; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.addon) { | ||||
|       return html` | ||||
| @@ -23,16 +27,31 @@ class HassioAddonLogDashboard extends LitElement { | ||||
|       `; | ||||
|     } | ||||
|     return html` | ||||
|       <div class="content"> | ||||
|         <hassio-addon-logs | ||||
|       <div class="search"> | ||||
|         <search-input | ||||
|           @value-changed=${this._filterChanged} | ||||
|           .hass=${this.hass} | ||||
|           .supervisor=${this.supervisor} | ||||
|           .addon=${this.addon} | ||||
|         ></hassio-addon-logs> | ||||
|           .filter=${this._filter} | ||||
|           .label=${this.hass.localize("ui.panel.config.logs.search")} | ||||
|         ></search-input> | ||||
|       </div> | ||||
|       <div class="content"> | ||||
|         <error-log-card | ||||
|           .hass=${this.hass} | ||||
|           .header=${this.addon.name} | ||||
|           .provider=${this.addon.slug} | ||||
|           show | ||||
|           .filter=${this._filter} | ||||
|         > | ||||
|         </error-log-card> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private async _filterChanged(ev) { | ||||
|     this._filter = ev.detail.value; | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyle, | ||||
| @@ -41,7 +60,21 @@ class HassioAddonLogDashboard extends LitElement { | ||||
|         .content { | ||||
|           margin: auto; | ||||
|           padding: 8px; | ||||
|           max-width: 1024px; | ||||
|         } | ||||
|         .search { | ||||
|           position: sticky; | ||||
|           top: 0; | ||||
|           z-index: 2; | ||||
|         } | ||||
|         search-input { | ||||
|           display: block; | ||||
|           --mdc-text-field-fill-color: var(--sidebar-background-color); | ||||
|           --mdc-text-field-idle-line-color: var(--divider-color); | ||||
|         } | ||||
|         @media all and (max-width: 870px) { | ||||
|           :host { | ||||
|             --error-log-card-height: calc(100vh - 304px); | ||||
|           } | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   | ||||
| @@ -1,90 +0,0 @@ | ||||
| import "@material/mwc-button"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import "../../../../src/components/ha-alert"; | ||||
| import "../../../../src/components/ha-ansi-to-html"; | ||||
| import "../../../../src/components/ha-card"; | ||||
| import { | ||||
|   fetchHassioAddonLogs, | ||||
|   HassioAddonDetails, | ||||
| } from "../../../../src/data/hassio/addon"; | ||||
| import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; | ||||
| import { Supervisor } from "../../../../src/data/supervisor/supervisor"; | ||||
| import { haStyle } from "../../../../src/resources/styles"; | ||||
| import { HomeAssistant } from "../../../../src/types"; | ||||
| import { hassioStyle } from "../../resources/hassio-style"; | ||||
|  | ||||
| @customElement("hassio-addon-logs") | ||||
| class HassioAddonLogs extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|  | ||||
|   @property({ attribute: false }) public addon!: HassioAddonDetails; | ||||
|  | ||||
|   @state() private _error?: string; | ||||
|  | ||||
|   @state() private _content?: string; | ||||
|  | ||||
|   public async connectedCallback(): Promise<void> { | ||||
|     super.connectedCallback(); | ||||
|     await this._loadData(); | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <h1>${this.addon.name}</h1> | ||||
|       <ha-card outlined> | ||||
|         ${this._error | ||||
|           ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` | ||||
|           : ""} | ||||
|         <div class="card-content"> | ||||
|           ${this._content | ||||
|             ? html`<ha-ansi-to-html | ||||
|                 .content=${this._content} | ||||
|               ></ha-ansi-to-html>` | ||||
|             : ""} | ||||
|         </div> | ||||
|         <div class="card-actions"> | ||||
|           <mwc-button @click=${this._refresh}> | ||||
|             ${this.supervisor.localize("common.refresh")} | ||||
|           </mwc-button> | ||||
|         </div> | ||||
|       </ha-card> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyle, | ||||
|       hassioStyle, | ||||
|       css` | ||||
|         :host, | ||||
|         ha-card { | ||||
|           display: block; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   private async _loadData(): Promise<void> { | ||||
|     this._error = undefined; | ||||
|     try { | ||||
|       this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug); | ||||
|     } catch (err: any) { | ||||
|       this._error = this.supervisor.localize("addon.logs.get_logs", { | ||||
|         error: extractApiErrorMessage(err), | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async _refresh(): Promise<void> { | ||||
|     await this._loadData(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "hassio-addon-logs": HassioAddonLogs; | ||||
|   } | ||||
| } | ||||
| @@ -48,6 +48,7 @@ import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-bac | ||||
| import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup"; | ||||
| import { supervisorTabs } from "../hassio-tabs"; | ||||
| import { hassioStyle } from "../resources/hassio-style"; | ||||
| import "../../../src/layouts/hass-loading-screen"; | ||||
|  | ||||
| type BackupItem = HassioBackup & { | ||||
|   secondary: string; | ||||
| @@ -69,6 +70,8 @@ export class HassioBackups extends LitElement { | ||||
|  | ||||
|   @state() private _backups?: HassioBackup[] = []; | ||||
|  | ||||
|   @state() private _isLoading = false; | ||||
|  | ||||
|   @query("hass-tabs-subpage-data-table", true) | ||||
|   private _dataTable!: HaTabsSubpageDataTable; | ||||
|  | ||||
| @@ -77,15 +80,10 @@ export class HassioBackups extends LitElement { | ||||
|   public connectedCallback(): void { | ||||
|     super.connectedCallback(); | ||||
|     if (this.hass && this._firstUpdatedCalled) { | ||||
|       this.refreshData(); | ||||
|       this.fetchBackups(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public async refreshData() { | ||||
|     await reloadHassioBackups(this.hass); | ||||
|     await this.fetchBackups(); | ||||
|   } | ||||
|  | ||||
|   private _computeBackupContent = (backup: HassioBackup): string => { | ||||
|     if (backup.type === "full") { | ||||
|       return this.supervisor.localize("backup.full_backup"); | ||||
| @@ -115,7 +113,7 @@ export class HassioBackups extends LitElement { | ||||
|   protected firstUpdated(changedProperties: PropertyValues): void { | ||||
|     super.firstUpdated(changedProperties); | ||||
|     if (this.hass && this.isConnected) { | ||||
|       this.refreshData(); | ||||
|       this.fetchBackups(); | ||||
|     } | ||||
|     this._firstUpdatedCalled = true; | ||||
|   } | ||||
| @@ -175,6 +173,13 @@ export class HassioBackups extends LitElement { | ||||
|     if (!this.supervisor) { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     if (this._isLoading) { | ||||
|       return html`<hass-loading-screen | ||||
|         .message=${this.supervisor.localize("backup.loading_backups")} | ||||
|       ></hass-loading-screen>`; | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       <hass-tabs-subpage-data-table | ||||
|         .tabs=${atLeastVersion(this.hass.config.version, 2022, 5) | ||||
| @@ -281,7 +286,7 @@ export class HassioBackups extends LitElement { | ||||
|   private _handleAction(ev: CustomEvent<ActionDetail>) { | ||||
|     switch (ev.detail.index) { | ||||
|       case 0: | ||||
|         this.refreshData(); | ||||
|         this.fetchBackups(); | ||||
|         break; | ||||
|       case 1: | ||||
|         showHassioBackupLocationDialog(this, { supervisor: this.supervisor }); | ||||
| @@ -306,13 +311,15 @@ export class HassioBackups extends LitElement { | ||||
|           supervisor: this.supervisor, | ||||
|           onDelete: () => this.fetchBackups(), | ||||
|         }), | ||||
|       reloadBackup: () => this.refreshData(), | ||||
|       reloadBackup: () => this.fetchBackups(), | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private async fetchBackups() { | ||||
|     this._isLoading = true; | ||||
|     await reloadHassioBackups(this.hass); | ||||
|     this._backups = await fetchHassioBackups(this.hass); | ||||
|     this._isLoading = false; | ||||
|   } | ||||
|  | ||||
|   private async _deleteSelected() { | ||||
| @@ -339,8 +346,7 @@ export class HassioBackups extends LitElement { | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|     await reloadHassioBackups(this.hass); | ||||
|     this._backups = await fetchHassioBackups(this.hass); | ||||
|     await this.fetchBackups(); | ||||
|     this._dataTable.clearSelection(); | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import { LocalizeFunc } from "../../../src/common/translations/localize"; | ||||
| import "../../../src/components/ha-checkbox"; | ||||
| import "../../../src/components/ha-formfield"; | ||||
| import "../../../src/components/ha-textfield"; | ||||
| import "../../../src/components/ha-password-field"; | ||||
| import "../../../src/components/ha-radio"; | ||||
| import type { HaRadio } from "../../../src/components/ha-radio"; | ||||
| import { | ||||
| @@ -261,23 +262,21 @@ export class SupervisorBackupContent extends LitElement { | ||||
|         : ""} | ||||
|       ${this.backupHasPassword | ||||
|         ? html` | ||||
|             <ha-textfield | ||||
|             <ha-password-field | ||||
|               .label=${this._localize("password")} | ||||
|               type="password" | ||||
|               name="backupPassword" | ||||
|               .value=${this.backupPassword} | ||||
|               @change=${this._handleTextValueChanged} | ||||
|             > | ||||
|             </ha-textfield> | ||||
|             </ha-password-field> | ||||
|             ${!this.backup | ||||
|               ? html`<ha-textfield | ||||
|               ? html`<ha-password-field | ||||
|                   .label=${this._localize("confirm_password")} | ||||
|                   type="password" | ||||
|                   name="confirmBackupPassword" | ||||
|                   .value=${this.confirmBackupPassword} | ||||
|                   @change=${this._handleTextValueChanged} | ||||
|                 > | ||||
|                 </ha-textfield>` | ||||
|                 </ha-password-field>` | ||||
|               : ""} | ||||
|           ` | ||||
|         : ""} | ||||
|   | ||||
| @@ -13,10 +13,12 @@ import "../../../../src/components/ha-circular-progress"; | ||||
| import "../../../../src/components/ha-dialog"; | ||||
| import "../../../../src/components/ha-expansion-panel"; | ||||
| import "../../../../src/components/ha-formfield"; | ||||
| import "../../../../src/components/ha-textfield"; | ||||
| import "../../../../src/components/ha-header-bar"; | ||||
| import "../../../../src/components/ha-icon-button"; | ||||
| import "../../../../src/components/ha-password-field"; | ||||
| import "../../../../src/components/ha-radio"; | ||||
| import "../../../../src/components/ha-textfield"; | ||||
| import type { HaTextField } from "../../../../src/components/ha-textfield"; | ||||
| import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; | ||||
| import { | ||||
|   AccessPoints, | ||||
| @@ -34,7 +36,6 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager"; | ||||
| import { haStyleDialog } from "../../../../src/resources/styles"; | ||||
| import type { HomeAssistant } from "../../../../src/types"; | ||||
| import { HassioNetworkDialogParams } from "./show-dialog-network"; | ||||
| import type { HaTextField } from "../../../../src/components/ha-textfield"; | ||||
|  | ||||
| const IP_VERSIONS = ["ipv4", "ipv6"]; | ||||
|  | ||||
| @@ -246,9 +247,8 @@ export class DialogHassioNetwork | ||||
|                       ${this._wifiConfiguration.auth === "wpa-psk" || | ||||
|                       this._wifiConfiguration.auth === "wep" | ||||
|                         ? html` | ||||
|                             <ha-textfield | ||||
|                             <ha-password-field | ||||
|                               class="flex-auto" | ||||
|                               type="password" | ||||
|                               id="psk" | ||||
|                               .label=${this.supervisor.localize( | ||||
|                                 "dialog.network.wifi_password" | ||||
| @@ -256,7 +256,7 @@ export class DialogHassioNetwork | ||||
|                               version="wifi" | ||||
|                               @change=${this._handleInputValueChangedWifi} | ||||
|                             > | ||||
|                             </ha-textfield> | ||||
|                             </ha-password-field> | ||||
|                           ` | ||||
|                         : ""} | ||||
|                     ` | ||||
|   | ||||
| @@ -25,8 +25,8 @@ import type { HomeAssistant } from "../../../../src/types"; | ||||
| import { HassioRepositoryDialogParams } from "./show-dialog-repositories"; | ||||
| import type { HaTextField } from "../../../../src/components/ha-textfield"; | ||||
| import "../../../../src/components/ha-textfield"; | ||||
| import "../../../../src/components/ha-list-new"; | ||||
| import "../../../../src/components/ha-list-item-new"; | ||||
| import "../../../../src/components/ha-md-list"; | ||||
| import "../../../../src/components/ha-md-list-item"; | ||||
|  | ||||
| @customElement("dialog-hassio-repositories") | ||||
| class HassioRepositoriesDialog extends LitElement { | ||||
| @@ -107,11 +107,11 @@ class HassioRepositoriesDialog extends LitElement { | ||||
|           ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` | ||||
|           : ""} | ||||
|         <div class="form"> | ||||
|           <ha-list-new> | ||||
|           <ha-md-list> | ||||
|             ${repositories.length | ||||
|               ? repositories.map( | ||||
|                   (repo) => html` | ||||
|                     <ha-list-item-new class="option"> | ||||
|                     <ha-md-list-item class="option"> | ||||
|                       ${repo.name} | ||||
|                       <div slot="supporting-text"> | ||||
|                         <div>${repo.maintainer}</div> | ||||
| @@ -142,11 +142,11 @@ class HassioRepositoriesDialog extends LitElement { | ||||
|                           )} | ||||
|                         </simple-tooltip> | ||||
|                       </div> | ||||
|                     </ha-list-item-new> | ||||
|                     </ha-md-list-item> | ||||
|                   ` | ||||
|                 ) | ||||
|               : html`<ha-list-item-new> No repositories </ha-list-item-new>`} | ||||
|           </ha-list-new> | ||||
|               : html`<ha-md-list-item> No repositories </ha-md-list-item>`} | ||||
|           </ha-md-list> | ||||
|           <div class="layout horizontal bottom"> | ||||
|             <ha-textfield | ||||
|               class="flex-auto" | ||||
| @@ -209,7 +209,7 @@ class HassioRepositoriesDialog extends LitElement { | ||||
|         div.delete ha-icon-button { | ||||
|           color: var(--error-color); | ||||
|         } | ||||
|         ha-list-item-new { | ||||
|         ha-md-list-item { | ||||
|           position: relative; | ||||
|         } | ||||
|       `, | ||||
|   | ||||
| @@ -13,10 +13,11 @@ | ||||
|       <% for (const entry of es5EntryJS) { %> | ||||
|         loadES5("<%= entry %>"); | ||||
|       <% } %> | ||||
|     } | ||||
|   } else { | ||||
|     <% for (const entry of es5EntryJS) { %> | ||||
|       loadES5("<%= entry %>"); | ||||
|     <% } %> | ||||
|   } | ||||
|   } | ||||
| })(); | ||||
|   | ||||
| @@ -120,10 +120,12 @@ class HassioSupervisorLog extends LitElement { | ||||
|     this._error = undefined; | ||||
|  | ||||
|     try { | ||||
|       this._content = await fetchHassioLogs( | ||||
|       const response = await fetchHassioLogs( | ||||
|         this.hass, | ||||
|         this._selectedLogProvider | ||||
|       ); | ||||
|  | ||||
|       this._content = await response.text(); | ||||
|     } catch (err: any) { | ||||
|       this._error = this.supervisor.localize("system.log.get_logs", { | ||||
|         provider: this._selectedLogProvider, | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| import "@material/mwc-list/mwc-list-item"; | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   PropertyValues, | ||||
|   nothing, | ||||
|   PropertyValues, | ||||
| } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| @@ -16,12 +15,12 @@ import "../../../src/components/ha-button-menu"; | ||||
| import "../../../src/components/ha-card"; | ||||
| import "../../../src/components/ha-checkbox"; | ||||
| import "../../../src/components/ha-faded"; | ||||
| import "../../../src/components/ha-formfield"; | ||||
| import "../../../src/components/ha-icon-button"; | ||||
| import "../../../src/components/ha-markdown"; | ||||
| import "../../../src/components/ha-settings-row"; | ||||
| import "../../../src/components/ha-svg-icon"; | ||||
| import "../../../src/components/ha-switch"; | ||||
| import type { HaSwitch } from "../../../src/components/ha-switch"; | ||||
| import { | ||||
|   fetchHassioAddonChangelog, | ||||
|   fetchHassioAddonInfo, | ||||
| @@ -42,6 +41,7 @@ import { updateCore } from "../../../src/data/supervisor/core"; | ||||
| import { StoreAddon } from "../../../src/data/supervisor/store"; | ||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||
| import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; | ||||
| import { haStyle } from "../../../src/resources/styles"; | ||||
| import { HomeAssistant, Route } from "../../../src/types"; | ||||
| import { addonArchIsSupported, extractChangelog } from "../util/addon"; | ||||
|  | ||||
| @@ -149,7 +149,7 @@ class UpdateAvailableCard extends LitElement { | ||||
|                           </ha-markdown> | ||||
|                         </ha-faded> | ||||
|                       ` | ||||
|                     : ""} | ||||
|                     : nothing} | ||||
|                   <div class="versions"> | ||||
|                     <p> | ||||
|                       ${this.supervisor.localize( | ||||
| @@ -164,15 +164,17 @@ class UpdateAvailableCard extends LitElement { | ||||
|                   </div> | ||||
|                   ${["core", "addon"].includes(this._updateType) | ||||
|                     ? html` | ||||
|                         <ha-formfield | ||||
|                           .label=${this.supervisor.localize( | ||||
|                             "update_available.create_backup" | ||||
|                           )} | ||||
|                         > | ||||
|                           <ha-checkbox checked></ha-checkbox> | ||||
|                         </ha-formfield> | ||||
|                         <hr /> | ||||
|                         <ha-settings-row> | ||||
|                           <span slot="heading"> | ||||
|                             ${this.supervisor.localize( | ||||
|                               "update_available.create_backup" | ||||
|                             )} | ||||
|                           </span> | ||||
|                           <ha-switch id="create_backup" checked></ha-switch> | ||||
|                         </ha-settings-row> | ||||
|                       ` | ||||
|                     : ""} | ||||
|                     : nothing} | ||||
|                 ` | ||||
|               : html`<ha-circular-progress | ||||
|                     aria-label="Updating" | ||||
| @@ -191,22 +193,24 @@ class UpdateAvailableCard extends LitElement { | ||||
|           ? html` | ||||
|               <div class="card-actions"> | ||||
|                 ${changelog | ||||
|                   ? html`<a .href=${changelog} target="_blank" rel="noreferrer"> | ||||
|                       <mwc-button | ||||
|                         .label=${this.supervisor.localize( | ||||
|                           "update_available.open_release_notes" | ||||
|                         )} | ||||
|                       > | ||||
|                       </mwc-button> | ||||
|                     </a>` | ||||
|                   : ""} | ||||
|                   ? html` | ||||
|                       <a href=${changelog} target="_blank" rel="noreferrer"> | ||||
|                         <ha-button | ||||
|                           .label=${this.supervisor.localize( | ||||
|                             "update_available.open_release_notes" | ||||
|                           )} | ||||
|                         > | ||||
|                         </ha-button> | ||||
|                       </a> | ||||
|                     ` | ||||
|                   : nothing} | ||||
|                 <span></span> | ||||
|                 <ha-progress-button @click=${this._update} raised> | ||||
|                 <ha-progress-button @click=${this._update}> | ||||
|                   ${this.supervisor.localize("common.update")} | ||||
|                 </ha-progress-button> | ||||
|               </div> | ||||
|             ` | ||||
|           : ""} | ||||
|           : nothing} | ||||
|       </ha-card> | ||||
|     `; | ||||
|   } | ||||
| @@ -242,9 +246,11 @@ class UpdateAvailableCard extends LitElement { | ||||
|     if (this._updateType && !["core", "addon"].includes(this._updateType)) { | ||||
|       return false; | ||||
|     } | ||||
|     const checkbox = this.shadowRoot?.querySelector("ha-checkbox"); | ||||
|     if (checkbox) { | ||||
|       return checkbox.checked; | ||||
|     const createBackupSwitch = this.shadowRoot?.getElementById( | ||||
|       "create-backup" | ||||
|     ) as HaSwitch; | ||||
|     if (createBackupSwitch) { | ||||
|       return createBackupSwitch.checked; | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
| @@ -397,41 +403,50 @@ class UpdateAvailableCard extends LitElement { | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       :host { | ||||
|         display: block; | ||||
|       } | ||||
|       ha-card { | ||||
|         margin: auto; | ||||
|       } | ||||
|       a { | ||||
|         text-decoration: none; | ||||
|         color: var(--primary-text-color); | ||||
|       } | ||||
|       ha-settings-row { | ||||
|         padding: 0; | ||||
|       } | ||||
|       .card-actions { | ||||
|         display: flex; | ||||
|         justify-content: space-between; | ||||
|         border-top: none; | ||||
|         padding: 0 8px 8px; | ||||
|       } | ||||
|     return [ | ||||
|       haStyle, | ||||
|       css` | ||||
|         :host { | ||||
|           display: block; | ||||
|         } | ||||
|         ha-card { | ||||
|           margin: auto; | ||||
|         } | ||||
|         a { | ||||
|           text-decoration: none; | ||||
|           color: var(--primary-text-color); | ||||
|         } | ||||
|         .card-actions { | ||||
|           display: flex; | ||||
|           justify-content: space-between; | ||||
|         } | ||||
|  | ||||
|       ha-circular-progress { | ||||
|         display: block; | ||||
|         margin: 32px; | ||||
|         text-align: center; | ||||
|       } | ||||
|         ha-circular-progress { | ||||
|           display: block; | ||||
|           margin: 32px; | ||||
|           text-align: center; | ||||
|         } | ||||
|  | ||||
|       .progress-text { | ||||
|         text-align: center; | ||||
|       } | ||||
|         .progress-text { | ||||
|           text-align: center; | ||||
|         } | ||||
|  | ||||
|       ha-markdown { | ||||
|         padding-bottom: 8px; | ||||
|       } | ||||
|     `; | ||||
|         ha-markdown { | ||||
|           padding-bottom: 8px; | ||||
|         } | ||||
|  | ||||
|         ha-settings-row { | ||||
|           padding: 0; | ||||
|           margin-bottom: -16px; | ||||
|         } | ||||
|  | ||||
|         hr { | ||||
|           border-color: var(--divider-color); | ||||
|           border-bottom: none; | ||||
|           margin: 16px 0 0 0; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										124
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -25,24 +25,24 @@ | ||||
|   "license": "Apache-2.0", | ||||
|   "type": "module", | ||||
|   "dependencies": { | ||||
|     "@babel/runtime": "7.25.6", | ||||
|     "@babel/runtime": "7.26.0", | ||||
|     "@braintree/sanitize-url": "7.1.0", | ||||
|     "@codemirror/autocomplete": "6.18.0", | ||||
|     "@codemirror/commands": "6.6.0", | ||||
|     "@codemirror/language": "6.10.2", | ||||
|     "@codemirror/autocomplete": "6.18.1", | ||||
|     "@codemirror/commands": "6.7.1", | ||||
|     "@codemirror/language": "6.10.3", | ||||
|     "@codemirror/legacy-modes": "6.4.1", | ||||
|     "@codemirror/search": "6.5.6", | ||||
|     "@codemirror/state": "6.4.1", | ||||
|     "@codemirror/view": "6.33.0", | ||||
|     "@codemirror/view": "6.34.1", | ||||
|     "@egjs/hammerjs": "2.0.17", | ||||
|     "@formatjs/intl-datetimeformat": "6.12.5", | ||||
|     "@formatjs/intl-displaynames": "6.6.8", | ||||
|     "@formatjs/intl-getcanonicallocales": "2.3.0", | ||||
|     "@formatjs/intl-listformat": "7.5.7", | ||||
|     "@formatjs/intl-locale": "4.0.0", | ||||
|     "@formatjs/intl-numberformat": "8.10.3", | ||||
|     "@formatjs/intl-pluralrules": "5.2.14", | ||||
|     "@formatjs/intl-relativetimeformat": "11.2.14", | ||||
|     "@formatjs/intl-datetimeformat": "6.16.1", | ||||
|     "@formatjs/intl-displaynames": "6.8.1", | ||||
|     "@formatjs/intl-getcanonicallocales": "2.5.1", | ||||
|     "@formatjs/intl-listformat": "7.7.1", | ||||
|     "@formatjs/intl-locale": "4.2.1", | ||||
|     "@formatjs/intl-numberformat": "8.14.1", | ||||
|     "@formatjs/intl-pluralrules": "5.3.1", | ||||
|     "@formatjs/intl-relativetimeformat": "11.4.1", | ||||
|     "@fullcalendar/core": "6.1.15", | ||||
|     "@fullcalendar/daygrid": "6.1.15", | ||||
|     "@fullcalendar/interaction": "6.1.15", | ||||
| @@ -80,16 +80,17 @@ | ||||
|     "@material/mwc-top-app-bar": "0.27.0", | ||||
|     "@material/mwc-top-app-bar-fixed": "0.27.0", | ||||
|     "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0", | ||||
|     "@material/web": "2.1.0", | ||||
|     "@material/web": "2.2.0", | ||||
|     "@mdi/js": "7.4.47", | ||||
|     "@mdi/svg": "7.4.47", | ||||
|     "@polymer/paper-item": "3.0.1", | ||||
|     "@polymer/paper-listbox": "3.0.1", | ||||
|     "@polymer/paper-tabs": "3.1.0", | ||||
|     "@polymer/polymer": "3.5.1", | ||||
|     "@polymer/polymer": "3.5.2", | ||||
|     "@replit/codemirror-indentation-markers": "6.5.3", | ||||
|     "@thomasloven/round-slider": "0.6.0", | ||||
|     "@vaadin/combo-box": "24.4.7", | ||||
|     "@vaadin/vaadin-themable-mixin": "24.4.7", | ||||
|     "@vaadin/combo-box": "24.5.1", | ||||
|     "@vaadin/vaadin-themable-mixin": "24.5.1", | ||||
|     "@vibrant/color": "3.2.1-alpha.1", | ||||
|     "@vibrant/core": "3.2.1-alpha.1", | ||||
|     "@vibrant/quantizer-mmcq": "3.2.1-alpha.1", | ||||
| @@ -97,28 +98,29 @@ | ||||
|     "@webcomponents/scoped-custom-element-registry": "0.0.9", | ||||
|     "@webcomponents/webcomponentsjs": "2.8.0", | ||||
|     "app-datepicker": "5.1.1", | ||||
|     "chart.js": "4.4.4", | ||||
|     "chart.js": "4.4.5", | ||||
|     "color-name": "2.0.0", | ||||
|     "comlink": "4.4.1", | ||||
|     "core-js": "3.38.1", | ||||
|     "cropperjs": "1.6.2", | ||||
|     "date-fns": "3.6.0", | ||||
|     "date-fns-tz": "3.1.3", | ||||
|     "date-fns": "4.1.0", | ||||
|     "date-fns-tz": "3.2.0", | ||||
|     "deep-clone-simple": "1.1.1", | ||||
|     "deep-freeze": "0.0.1", | ||||
|     "dialog-polyfill": "0.5.6", | ||||
|     "element-internals-polyfill": "1.3.11", | ||||
|     "fuse.js": "7.0.0", | ||||
|     "google-timezones-json": "1.2.0", | ||||
|     "hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch", | ||||
|     "home-assistant-js-websocket": "9.4.0", | ||||
|     "idb-keyval": "6.2.1", | ||||
|     "intl-messageformat": "10.5.14", | ||||
|     "intl-messageformat": "10.7.3", | ||||
|     "js-yaml": "4.1.0", | ||||
|     "leaflet": "1.9.4", | ||||
|     "leaflet-draw": "1.0.4", | ||||
|     "leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch", | ||||
|     "lit": "2.8.0", | ||||
|     "luxon": "3.5.0", | ||||
|     "marked": "14.1.0", | ||||
|     "marked": "14.1.3", | ||||
|     "memoize-one": "6.0.0", | ||||
|     "node-vibrant": "3.2.1-alpha.1", | ||||
|     "proxy-polyfill": "0.3.2", | ||||
| @@ -127,13 +129,13 @@ | ||||
|     "qrcode": "1.5.4", | ||||
|     "roboto-fontface": "0.10.0", | ||||
|     "rrule": "2.8.1", | ||||
|     "sortablejs": "1.15.2", | ||||
|     "sortablejs": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch", | ||||
|     "stacktrace-js": "2.0.2", | ||||
|     "superstruct": "2.0.2", | ||||
|     "tinykeys": "3.0.0", | ||||
|     "tsparticles-engine": "2.12.0", | ||||
|     "tsparticles-preset-links": "2.12.0", | ||||
|     "ua-parser-js": "1.0.38", | ||||
|     "ua-parser-js": "1.0.39", | ||||
|     "unfetch": "5.0.0", | ||||
|     "vis-data": "7.1.9", | ||||
|     "vis-network": "9.1.9", | ||||
| @@ -149,36 +151,36 @@ | ||||
|     "xss": "1.0.15" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@babel/core": "7.25.2", | ||||
|     "@babel/core": "7.26.0", | ||||
|     "@babel/helper-define-polyfill-provider": "0.6.2", | ||||
|     "@babel/plugin-proposal-decorators": "7.24.7", | ||||
|     "@babel/plugin-transform-runtime": "7.25.4", | ||||
|     "@babel/preset-env": "7.25.4", | ||||
|     "@babel/preset-typescript": "7.24.7", | ||||
|     "@bundle-stats/plugin-webpack-filter": "4.15.0", | ||||
|     "@babel/plugin-proposal-decorators": "7.25.9", | ||||
|     "@babel/plugin-transform-runtime": "7.25.9", | ||||
|     "@babel/preset-env": "7.26.0", | ||||
|     "@babel/preset-typescript": "7.26.0", | ||||
|     "@bundle-stats/plugin-webpack-filter": "4.16.0", | ||||
|     "@koa/cors": "5.0.0", | ||||
|     "@lokalise/node-api": "12.7.0", | ||||
|     "@lokalise/node-api": "12.8.0", | ||||
|     "@octokit/auth-oauth-device": "7.1.1", | ||||
|     "@octokit/plugin-retry": "7.1.1", | ||||
|     "@octokit/plugin-retry": "7.1.2", | ||||
|     "@octokit/rest": "21.0.2", | ||||
|     "@open-wc/dev-server-hmr": "0.1.4", | ||||
|     "@rollup/plugin-babel": "6.0.4", | ||||
|     "@rollup/plugin-commonjs": "26.0.1", | ||||
|     "@rollup/plugin-json": "6.1.0", | ||||
|     "@rollup/plugin-node-resolve": "15.2.3", | ||||
|     "@rollup/plugin-node-resolve": "15.2.4", | ||||
|     "@rollup/plugin-replace": "5.0.7", | ||||
|     "@types/babel__plugin-transform-runtime": "7.9.5", | ||||
|     "@types/chromecast-caf-receiver": "6.0.17", | ||||
|     "@types/chromecast-caf-sender": "1.0.10", | ||||
|     "@types/color-name": "1.1.4", | ||||
|     "@types/color-name": "2.0.0", | ||||
|     "@types/glob": "8.1.0", | ||||
|     "@types/html-minifier-terser": "7.0.2", | ||||
|     "@types/js-yaml": "4.0.9", | ||||
|     "@types/leaflet": "1.9.12", | ||||
|     "@types/leaflet": "1.9.14", | ||||
|     "@types/leaflet-draw": "1.0.11", | ||||
|     "@types/lodash.merge": "4.6.9", | ||||
|     "@types/luxon": "3.4.2", | ||||
|     "@types/mocha": "10.0.7", | ||||
|     "@types/mocha": "10.0.9", | ||||
|     "@types/qrcode": "1.5.5", | ||||
|     "@types/serve-handler": "6.1.4", | ||||
|     "@types/sortablejs": "1.15.8", | ||||
| @@ -189,21 +191,21 @@ | ||||
|     "@typescript-eslint/parser": "7.18.0", | ||||
|     "@web/dev-server": "0.1.38", | ||||
|     "@web/dev-server-rollup": "0.4.1", | ||||
|     "babel-loader": "9.1.3", | ||||
|     "babel-loader": "9.2.1", | ||||
|     "babel-plugin-template-html-minifier": "4.1.0", | ||||
|     "browserslist-useragent-regexp": "4.1.3", | ||||
|     "chai": "5.1.1", | ||||
|     "del": "7.1.0", | ||||
|     "eslint": "8.57.0", | ||||
|     "chai": "5.1.2", | ||||
|     "del": "8.0.0", | ||||
|     "eslint": "8.57.1", | ||||
|     "eslint-config-airbnb-base": "15.0.0", | ||||
|     "eslint-config-airbnb-typescript": "18.0.0", | ||||
|     "eslint-config-prettier": "9.1.0", | ||||
|     "eslint-import-resolver-webpack": "0.13.8", | ||||
|     "eslint-plugin-import": "2.29.1", | ||||
|     "eslint-plugin-lit": "1.14.0", | ||||
|     "eslint-import-resolver-webpack": "0.13.9", | ||||
|     "eslint-plugin-import": "2.31.0", | ||||
|     "eslint-plugin-lit": "1.15.0", | ||||
|     "eslint-plugin-lit-a11y": "4.1.4", | ||||
|     "eslint-plugin-unused-imports": "4.1.3", | ||||
|     "eslint-plugin-wc": "2.1.1", | ||||
|     "eslint-plugin-unused-imports": "4.1.4", | ||||
|     "eslint-plugin-wc": "2.2.0", | ||||
|     "fancy-log": "2.0.0", | ||||
|     "fs-extra": "11.2.0", | ||||
|     "glob": "11.0.0", | ||||
| @@ -213,35 +215,35 @@ | ||||
|     "gulp-rename": "2.0.0", | ||||
|     "gulp-zopfli-green": "6.0.2", | ||||
|     "html-minifier-terser": "7.2.0", | ||||
|     "husky": "9.1.5", | ||||
|     "instant-mocha": "1.5.2", | ||||
|     "husky": "9.1.6", | ||||
|     "instant-mocha": "1.5.3", | ||||
|     "jszip": "3.10.1", | ||||
|     "lint-staged": "15.2.9", | ||||
|     "lint-staged": "15.2.10", | ||||
|     "lit-analyzer": "2.0.3", | ||||
|     "lodash.merge": "4.6.2", | ||||
|     "lodash.template": "4.5.0", | ||||
|     "magic-string": "0.30.11", | ||||
|     "magic-string": "0.30.12", | ||||
|     "map-stream": "0.0.7", | ||||
|     "mocha": "10.5.0", | ||||
|     "mocha": "10.7.3", | ||||
|     "object-hash": "3.0.0", | ||||
|     "open": "10.1.0", | ||||
|     "pinst": "3.0.0", | ||||
|     "prettier": "3.3.3", | ||||
|     "rollup": "2.79.1", | ||||
|     "rollup": "2.79.2", | ||||
|     "rollup-plugin-string": "3.0.0", | ||||
|     "rollup-plugin-terser": "7.0.2", | ||||
|     "rollup-plugin-visualizer": "5.12.0", | ||||
|     "serve-handler": "6.1.5", | ||||
|     "sinon": "18.0.0", | ||||
|     "serve-handler": "6.1.6", | ||||
|     "sinon": "19.0.2", | ||||
|     "systemjs": "6.15.1", | ||||
|     "tar": "7.4.3", | ||||
|     "terser-webpack-plugin": "5.3.10", | ||||
|     "transform-async-modules-webpack-plugin": "1.1.1", | ||||
|     "ts-lit-plugin": "2.0.2", | ||||
|     "typescript": "5.5.4", | ||||
|     "webpack": "5.94.0", | ||||
|     "typescript": "5.6.3", | ||||
|     "webpack": "5.95.0", | ||||
|     "webpack-cli": "5.1.4", | ||||
|     "webpack-dev-server": "5.0.4", | ||||
|     "webpack-dev-server": "5.1.0", | ||||
|     "webpack-manifest-plugin": "5.0.0", | ||||
|     "webpack-stats-plugin": "1.1.3", | ||||
|     "webpackbar": "6.0.1", | ||||
| @@ -249,14 +251,12 @@ | ||||
|   }, | ||||
|   "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch", | ||||
|   "resolutions": { | ||||
|     "@polymer/polymer": "patch:@polymer/polymer@3.5.1#./.yarn/patches/@polymer/polymer/pr-5569.patch", | ||||
|     "@polymer/polymer": "patch:@polymer/polymer@3.5.2#./.yarn/patches/@polymer/polymer/pr-5569.patch", | ||||
|     "@material/mwc-button@^0.25.3": "^0.27.0", | ||||
|     "lit": "2.8.0", | ||||
|     "clean-css": "5.3.3", | ||||
|     "@lit/reactive-element": "1.6.3", | ||||
|     "@fullcalendar/daygrid": "6.1.15", | ||||
|     "sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch", | ||||
|     "leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch" | ||||
|     "@fullcalendar/daygrid": "6.1.15" | ||||
|   }, | ||||
|   "packageManager": "yarn@4.4.1" | ||||
|   "packageManager": "yarn@4.5.1" | ||||
| } | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								public/static/images/logo_nabu_casa.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/static/images/logo_nabu_casa_dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/static/images/voice-assistant/area.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 372 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/static/images/voice-assistant/change-wake-word.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 383 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/static/images/voice-assistant/error.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 377 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/static/images/voice-assistant/heart.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 389 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/static/images/voice-assistant/hi.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 379 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/static/images/voice-assistant/ok-nabu.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 381 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/static/images/voice-assistant/sleep.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 374 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/static/images/voice-assistant/update.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 379 KiB | 
| @@ -1,10 +1,10 @@ | ||||
| [build-system] | ||||
| requires = ["setuptools~=68.0", "wheel~=0.40.0"] | ||||
| requires = ["setuptools~=75.1"] | ||||
| build-backend = "setuptools.build_meta" | ||||
|  | ||||
| [project] | ||||
| name         = "home-assistant-frontend" | ||||
| version      = "20240903.1" | ||||
| version      = "20241010.0" | ||||
| license      = {text = "Apache-2.0"} | ||||
| description  = "The Home Assistant frontend" | ||||
| readme       = "README.md" | ||||
|   | ||||
| @@ -18,5 +18,9 @@ if [[ -n "$DEVCONTAINER" ]]; then | ||||
|     fi | ||||
| fi | ||||
|  | ||||
| if ! command -v yarn &> /dev/null; then | ||||
|     echo "Error: yarn not found. Please install it following the official instructions: https://yarnpkg.com/getting-started/install" >&2 | ||||
|     exit 1 | ||||
| fi | ||||
| # Install node modules | ||||
|     yarn install | ||||
| yarn install | ||||
|   | ||||
| @@ -1,36 +1,36 @@ | ||||
| import { theme2hex } from "./convert-color"; | ||||
|  | ||||
| export const COLORS = [ | ||||
|   "#44739e", | ||||
|   "#984ea3", | ||||
|   "#00d2d5", | ||||
|   "#ff7f00", | ||||
|   "#af8d00", | ||||
|   "#7f80cd", | ||||
|   "#b3e900", | ||||
|   "#c42e60", | ||||
|   "#a65628", | ||||
|   "#f781bf", | ||||
|   "#8dd3c7", | ||||
|   "#bebada", | ||||
|   "#fb8072", | ||||
|   "#80b1d3", | ||||
|   "#fdb462", | ||||
|   "#fccde5", | ||||
|   "#bc80bd", | ||||
|   "#ffed6f", | ||||
|   "#c4eaff", | ||||
|   "#cf8c00", | ||||
|   "#1b9e77", | ||||
|   "#d95f02", | ||||
|   "#e7298a", | ||||
|   "#e6ab02", | ||||
|   "#a6761d", | ||||
|   "#0097ff", | ||||
|   "#00d067", | ||||
|   "#f43600", | ||||
|   "#4ba93b", | ||||
|   "#5779bb", | ||||
|   "#4269d0", | ||||
|   "#f4bd4a", | ||||
|   "#ff725c", | ||||
|   "#6cc5b0", | ||||
|   "#a463f2", | ||||
|   "#ff8ab7", | ||||
|   "#9c6b4e", | ||||
|   "#97bbf5", | ||||
|   "#01ab63", | ||||
|   "#9498a0", | ||||
|   "#094bad", | ||||
|   "#c99000", | ||||
|   "#d84f3e", | ||||
|   "#49a28f", | ||||
|   "#048732", | ||||
|   "#d96895", | ||||
|   "#8043ce", | ||||
|   "#7599d1", | ||||
|   "#7a4c31", | ||||
|   "#74787f", | ||||
|   "#6989f4", | ||||
|   "#ffd444", | ||||
|   "#ff957c", | ||||
|   "#8fe9d3", | ||||
|   "#62cc71", | ||||
|   "#ffadda", | ||||
|   "#c884ff", | ||||
|   "#badeff", | ||||
|   "#bf8b6d", | ||||
|   "#b6bac2", | ||||
|   "#927acc", | ||||
|   "#97ee3f", | ||||
|   "#bf3947", | ||||
|   | ||||
| @@ -234,7 +234,12 @@ export const SENSOR_ENTITIES = [ | ||||
|   "weather", | ||||
| ]; | ||||
|  | ||||
| export const ASSIST_ENTITIES = ["conversation", "stt", "tts"]; | ||||
| export const ASSIST_ENTITIES = [ | ||||
|   "assist_satellite", | ||||
|   "conversation", | ||||
|   "stt", | ||||
|   "tts", | ||||
| ]; | ||||
|  | ||||
| /** Domains that render an input element instead of a text value when displayed in a row. | ||||
|  *  Those rows should then not show a cursor pointer when hovered (which would normally | ||||
|   | ||||
| @@ -34,9 +34,11 @@ export const protocolIntegrationPicked = async ( | ||||
|   if (domain === "zwave_js") { | ||||
|     const entries = options?.config_entry | ||||
|       ? undefined | ||||
|       : await getConfigEntries(hass, { | ||||
|           domain, | ||||
|         }); | ||||
|       : ( | ||||
|           await getConfigEntries(hass, { | ||||
|             domain, | ||||
|           }) | ||||
|         ).filter((e) => !e.disabled_by); | ||||
|  | ||||
|     if ( | ||||
|       !isComponentLoaded(hass, "zwave_js") || | ||||
| @@ -81,9 +83,11 @@ export const protocolIntegrationPicked = async ( | ||||
|   } else if (domain === "zha") { | ||||
|     const entries = options?.config_entry | ||||
|       ? undefined | ||||
|       : await getConfigEntries(hass, { | ||||
|           domain, | ||||
|         }); | ||||
|       : ( | ||||
|           await getConfigEntries(hass, { | ||||
|             domain, | ||||
|           }) | ||||
|         ).filter((e) => !e.disabled_by); | ||||
|  | ||||
|     if ( | ||||
|       !isComponentLoaded(hass, "zha") || | ||||
| @@ -129,9 +133,11 @@ export const protocolIntegrationPicked = async ( | ||||
|   } else if (domain === "matter") { | ||||
|     const entries = options?.config_entry | ||||
|       ? undefined | ||||
|       : await getConfigEntries(hass, { | ||||
|           domain, | ||||
|         }); | ||||
|       : ( | ||||
|           await getConfigEntries(hass, { | ||||
|             domain, | ||||
|           }) | ||||
|         ).filter((e) => !e.disabled_by); | ||||
|     if ( | ||||
|       !isComponentLoaded(hass, domain) || | ||||
|       (!options?.config_entry && !entries?.length) | ||||
|   | ||||
| @@ -15,7 +15,6 @@ export type LocalizeKeys = | ||||
|   | `ui.card.weather.cardinal_direction.${string}` | ||||
|   | `ui.card.lawn_mower.actions.${string}` | ||||
|   | `ui.components.calendar.event.rrule.${string}` | ||||
|   | `ui.components.logbook.${string}` | ||||
|   | `ui.components.selectors.file.${string}` | ||||
|   | `ui.dialogs.entity_registry.editor.${string}` | ||||
|   | `ui.dialogs.more_info_control.lawn_mower.${string}` | ||||
|   | ||||
| @@ -20,6 +20,15 @@ function findNestedItem( | ||||
|   }, obj); | ||||
| } | ||||
|  | ||||
| function updateNestedItem(obj: any, path: ItemPath): any { | ||||
|   const lastKey = path.pop()!; | ||||
|   const parent = findNestedItem(obj, path); | ||||
|   parent[lastKey] = Array.isArray(parent[lastKey]) | ||||
|     ? [...parent[lastKey]] | ||||
|     : [parent[lastKey]]; | ||||
|   return obj; | ||||
| } | ||||
|  | ||||
| export function nestedArrayMove<A>( | ||||
|   obj: A, | ||||
|   oldIndex: number, | ||||
| @@ -27,14 +36,18 @@ export function nestedArrayMove<A>( | ||||
|   oldPath?: ItemPath, | ||||
|   newPath?: ItemPath | ||||
| ): A { | ||||
|   const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A; | ||||
|   let newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A; | ||||
|  | ||||
|   if (oldPath) { | ||||
|     newObj = updateNestedItem(newObj, [...oldPath]); | ||||
|   } | ||||
|   if (newPath) { | ||||
|     newObj = updateNestedItem(newObj, [...newPath]); | ||||
|   } | ||||
|  | ||||
|   const from = oldPath ? findNestedItem(newObj, oldPath) : newObj; | ||||
|   const to = newPath ? findNestedItem(newObj, newPath, true) : newObj; | ||||
|  | ||||
|   if (!Array.isArray(from) || !Array.isArray(to)) { | ||||
|     return obj; | ||||
|   } | ||||
|  | ||||
|   const item = from.splice(oldIndex, 1)[0]; | ||||
|   to.splice(newIndex, 0, item); | ||||
|  | ||||
|   | ||||
| @@ -108,6 +108,7 @@ class HaDataTableLabels extends LitElement { | ||||
|       ha-label { | ||||
|         --ha-label-background-color: var(--color, var(--grey-color)); | ||||
|         --ha-label-background-opacity: 0.5; | ||||
|         outline: 1px solid var(--outline-color); | ||||
|       } | ||||
|       ha-button-menu { | ||||
|         border-radius: 10px; | ||||
|   | ||||
| @@ -25,7 +25,6 @@ import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { stringCompare } from "../../common/string/compare"; | ||||
| import { debounce } from "../../common/util/debounce"; | ||||
| import { groupBy } from "../../common/util/group-by"; | ||||
| import { nextRender } from "../../common/util/render-status"; | ||||
| import { haStyleScrollbar } from "../../resources/styles"; | ||||
| import { loadVirtualizer } from "../../resources/virtualizer"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| @@ -35,6 +34,7 @@ import "../ha-svg-icon"; | ||||
| import "../search-input"; | ||||
| import { filterData, sortData } from "./sort-filter"; | ||||
| import { LocalizeFunc } from "../../common/translations/localize"; | ||||
| import { nextRender } from "../../common/util/render-status"; | ||||
|  | ||||
| export interface RowClickedEvent { | ||||
|   id: string; | ||||
| @@ -169,8 +169,6 @@ export class HaDataTable extends LitElement { | ||||
|  | ||||
|   @query("slot[name='header']") private _header!: HTMLSlotElement; | ||||
|  | ||||
|   @state() private _items: DataTableRowData[] = []; | ||||
|  | ||||
|   @state() private _collapsedGroups: string[] = []; | ||||
|  | ||||
|   private _checkableRowsCount?: number; | ||||
| @@ -179,7 +177,9 @@ export class HaDataTable extends LitElement { | ||||
|  | ||||
|   private _sortColumns: SortableColumnContainer = {}; | ||||
|  | ||||
|   private curRequest = 0; | ||||
|   private _curRequest = 0; | ||||
|  | ||||
|   private _lastUpdate = 0; | ||||
|  | ||||
|   // @ts-ignore | ||||
|   @restoreScroll(".scroller") private _savedScrollPos?: number; | ||||
| @@ -204,11 +204,34 @@ export class HaDataTable extends LitElement { | ||||
|     this._checkedRowsChanged(); | ||||
|   } | ||||
|  | ||||
|   public select(ids: string[], clear?: boolean): void { | ||||
|     if (clear) { | ||||
|       this._checkedRows = []; | ||||
|     } | ||||
|     ids.forEach((id) => { | ||||
|       const row = this._filteredData.find((data) => data[this.id] === id); | ||||
|       if (row?.selectable !== false && !this._checkedRows.includes(id)) { | ||||
|         this._checkedRows.push(id); | ||||
|       } | ||||
|     }); | ||||
|     this._checkedRowsChanged(); | ||||
|   } | ||||
|  | ||||
|   public unselect(ids: string[]): void { | ||||
|     ids.forEach((id) => { | ||||
|       const index = this._checkedRows.indexOf(id); | ||||
|       if (index > -1) { | ||||
|         this._checkedRows.splice(index, 1); | ||||
|       } | ||||
|     }); | ||||
|     this._checkedRowsChanged(); | ||||
|   } | ||||
|  | ||||
|   public connectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     if (this._items.length) { | ||||
|     if (this._filteredData.length) { | ||||
|       // Force update of location of rows | ||||
|       this._items = [...this._items]; | ||||
|       this._filteredData = [...this._filteredData]; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -291,16 +314,13 @@ export class HaDataTable extends LitElement { | ||||
|       properties.has("columns") || | ||||
|       properties.has("_filter") || | ||||
|       properties.has("sortColumn") || | ||||
|       properties.has("sortDirection") || | ||||
|       properties.has("groupColumn") || | ||||
|       properties.has("groupOrder") || | ||||
|       properties.has("_collapsedGroups") | ||||
|       properties.has("sortDirection") | ||||
|     ) { | ||||
|       this._sortFilterData(); | ||||
|     } | ||||
|  | ||||
|     if (properties.has("selectable") || properties.has("hiddenColumns")) { | ||||
|       this._items = [...this._items]; | ||||
|       this._filteredData = [...this._filteredData]; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -467,7 +487,15 @@ export class HaDataTable extends LitElement { | ||||
|                   scroller | ||||
|                   class="mdc-data-table__content scroller ha-scrollbar" | ||||
|                   @scroll=${this._saveScrollPos} | ||||
|                   .items=${this._items} | ||||
|                   .items=${this._groupData( | ||||
|                     this._filteredData, | ||||
|                     localize, | ||||
|                     this.appendRow, | ||||
|                     this.hasFab, | ||||
|                     this.groupColumn, | ||||
|                     this.groupOrder, | ||||
|                     this._collapsedGroups | ||||
|                   )} | ||||
|                   .keyFunction=${this._keyFunction} | ||||
|                   .renderItem=${renderRow} | ||||
|                 ></lit-virtualizer> | ||||
| @@ -602,8 +630,13 @@ export class HaDataTable extends LitElement { | ||||
|  | ||||
|   private async _sortFilterData() { | ||||
|     const startTime = new Date().getTime(); | ||||
|     this.curRequest++; | ||||
|     const curRequest = this.curRequest; | ||||
|     const timeBetweenUpdate = startTime - this._lastUpdate; | ||||
|     const timeBetweenRequest = startTime - this._curRequest; | ||||
|     this._curRequest = startTime; | ||||
|  | ||||
|     const forceUpdate = | ||||
|       !this._lastUpdate || | ||||
|       (timeBetweenUpdate > 500 && timeBetweenRequest < 500); | ||||
|  | ||||
|     let filteredData = this.data; | ||||
|     if (this._filter) { | ||||
| @@ -614,6 +647,10 @@ export class HaDataTable extends LitElement { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (!forceUpdate && this._curRequest !== startTime) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const prom = this.sortColumn | ||||
|       ? sortData( | ||||
|           filteredData, | ||||
| @@ -634,91 +671,103 @@ export class HaDataTable extends LitElement { | ||||
|         setTimeout(resolve, 100 - elapsed); | ||||
|       }); | ||||
|     } | ||||
|     if (this.curRequest !== curRequest) { | ||||
|  | ||||
|     if (!forceUpdate && this._curRequest !== startTime) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const localize = this.localizeFunc || this.hass.localize; | ||||
|  | ||||
|     if (this.appendRow || this.hasFab || this.groupColumn) { | ||||
|       let items = [...data]; | ||||
|  | ||||
|       if (this.groupColumn) { | ||||
|         const grouped = groupBy(items, (item) => item[this.groupColumn!]); | ||||
|         if (grouped.undefined) { | ||||
|           // make sure ungrouped items are at the bottom | ||||
|           grouped[UNDEFINED_GROUP_KEY] = grouped.undefined; | ||||
|           delete grouped.undefined; | ||||
|         } | ||||
|         const sorted: { | ||||
|           [key: string]: DataTableRowData[]; | ||||
|         } = Object.keys(grouped) | ||||
|           .sort((a, b) => { | ||||
|             const orderA = this.groupOrder?.indexOf(a) ?? -1; | ||||
|             const orderB = this.groupOrder?.indexOf(b) ?? -1; | ||||
|             if (orderA !== orderB) { | ||||
|               if (orderA === -1) { | ||||
|                 return 1; | ||||
|               } | ||||
|               if (orderB === -1) { | ||||
|                 return -1; | ||||
|               } | ||||
|               return orderA - orderB; | ||||
|             } | ||||
|             return stringCompare( | ||||
|               ["", "-", "—"].includes(a) ? "zzz" : a, | ||||
|               ["", "-", "—"].includes(b) ? "zzz" : b, | ||||
|               this.hass.locale.language | ||||
|             ); | ||||
|           }) | ||||
|           .reduce((obj, key) => { | ||||
|             obj[key] = grouped[key]; | ||||
|             return obj; | ||||
|           }, {}); | ||||
|         const groupedItems: DataTableRowData[] = []; | ||||
|         Object.entries(sorted).forEach(([groupName, rows]) => { | ||||
|           groupedItems.push({ | ||||
|             append: true, | ||||
|             content: html`<div | ||||
|               class="mdc-data-table__cell group-header" | ||||
|               role="cell" | ||||
|               .group=${groupName} | ||||
|               @click=${this._collapseGroup} | ||||
|             > | ||||
|               <ha-icon-button | ||||
|                 .path=${mdiChevronUp} | ||||
|                 class=${this._collapsedGroups.includes(groupName) | ||||
|                   ? "collapsed" | ||||
|                   : ""} | ||||
|               > | ||||
|               </ha-icon-button> | ||||
|               ${groupName === UNDEFINED_GROUP_KEY | ||||
|                 ? localize("ui.components.data-table.ungrouped") | ||||
|                 : groupName || ""} | ||||
|             </div>`, | ||||
|           }); | ||||
|           if (!this._collapsedGroups.includes(groupName)) { | ||||
|             groupedItems.push(...rows); | ||||
|           } | ||||
|         }); | ||||
|         items = groupedItems; | ||||
|       } | ||||
|  | ||||
|       if (this.appendRow) { | ||||
|         items.push({ append: true, content: this.appendRow }); | ||||
|       } | ||||
|  | ||||
|       if (this.hasFab) { | ||||
|         items.push({ empty: true }); | ||||
|       } | ||||
|  | ||||
|       this._items = items; | ||||
|     } else { | ||||
|       this._items = data; | ||||
|     } | ||||
|     this._lastUpdate = startTime; | ||||
|     this._filteredData = data; | ||||
|   } | ||||
|  | ||||
|   private _groupData = memoizeOne( | ||||
|     ( | ||||
|       data: DataTableRowData[], | ||||
|       localize: LocalizeFunc, | ||||
|       appendRow, | ||||
|       hasFab: boolean, | ||||
|       groupColumn: string | undefined, | ||||
|       groupOrder: string[] | undefined, | ||||
|       collapsedGroups: string[] | ||||
|     ) => { | ||||
|       if (appendRow || hasFab || groupColumn) { | ||||
|         let items = [...data]; | ||||
|  | ||||
|         if (groupColumn) { | ||||
|           const grouped = groupBy(items, (item) => item[groupColumn]); | ||||
|           if (grouped.undefined) { | ||||
|             // make sure ungrouped items are at the bottom | ||||
|             grouped[UNDEFINED_GROUP_KEY] = grouped.undefined; | ||||
|             delete grouped.undefined; | ||||
|           } | ||||
|           const sorted: { | ||||
|             [key: string]: DataTableRowData[]; | ||||
|           } = Object.keys(grouped) | ||||
|             .sort((a, b) => { | ||||
|               const orderA = groupOrder?.indexOf(a) ?? -1; | ||||
|               const orderB = groupOrder?.indexOf(b) ?? -1; | ||||
|               if (orderA !== orderB) { | ||||
|                 if (orderA === -1) { | ||||
|                   return 1; | ||||
|                 } | ||||
|                 if (orderB === -1) { | ||||
|                   return -1; | ||||
|                 } | ||||
|                 return orderA - orderB; | ||||
|               } | ||||
|               return stringCompare( | ||||
|                 ["", "-", "—"].includes(a) ? "zzz" : a, | ||||
|                 ["", "-", "—"].includes(b) ? "zzz" : b, | ||||
|                 this.hass.locale.language | ||||
|               ); | ||||
|             }) | ||||
|             .reduce((obj, key) => { | ||||
|               obj[key] = grouped[key]; | ||||
|               return obj; | ||||
|             }, {}); | ||||
|           const groupedItems: DataTableRowData[] = []; | ||||
|           Object.entries(sorted).forEach(([groupName, rows]) => { | ||||
|             groupedItems.push({ | ||||
|               append: true, | ||||
|               content: html`<div | ||||
|                 class="mdc-data-table__cell group-header" | ||||
|                 role="cell" | ||||
|                 .group=${groupName} | ||||
|                 @click=${this._collapseGroup} | ||||
|               > | ||||
|                 <ha-icon-button | ||||
|                   .path=${mdiChevronUp} | ||||
|                   class=${collapsedGroups.includes(groupName) | ||||
|                     ? "collapsed" | ||||
|                     : ""} | ||||
|                 > | ||||
|                 </ha-icon-button> | ||||
|                 ${groupName === UNDEFINED_GROUP_KEY | ||||
|                   ? localize("ui.components.data-table.ungrouped") | ||||
|                   : groupName || ""} | ||||
|               </div>`, | ||||
|             }); | ||||
|             if (!collapsedGroups.includes(groupName)) { | ||||
|               groupedItems.push(...rows); | ||||
|             } | ||||
|           }); | ||||
|           items = groupedItems; | ||||
|         } | ||||
|  | ||||
|         if (appendRow) { | ||||
|           items.push({ append: true, content: appendRow }); | ||||
|         } | ||||
|  | ||||
|         if (hasFab) { | ||||
|           items.push({ empty: true }); | ||||
|         } | ||||
|  | ||||
|         return items; | ||||
|       } | ||||
|       return data; | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   private _memFilterData = memoizeOne( | ||||
|     ( | ||||
|       data: DataTableRowData[], | ||||
| @@ -802,8 +851,8 @@ export class HaDataTable extends LitElement { | ||||
|  | ||||
|   private _checkedRowsChanged() { | ||||
|     // force scroller to update, change it's items | ||||
|     if (this._items.length) { | ||||
|       this._items = [...this._items]; | ||||
|     if (this._filteredData.length) { | ||||
|       this._filteredData = [...this._filteredData]; | ||||
|     } | ||||
|     fireEvent(this, "selection-changed", { | ||||
|       value: this._checkedRows, | ||||
| @@ -985,6 +1034,7 @@ export class HaDataTable extends LitElement { | ||||
|           /* @noflip */ | ||||
|           padding-inline-end: initial; | ||||
|           width: 60px; | ||||
|           min-width: 60px; | ||||
|         } | ||||
|  | ||||
|         .mdc-data-table__table { | ||||
| @@ -1150,6 +1200,7 @@ export class HaDataTable extends LitElement { | ||||
|           display: flex; | ||||
|           align-items: center; | ||||
|           cursor: pointer; | ||||
|           background-color: var(--primary-background-color); | ||||
|         } | ||||
|  | ||||
|         .group-header ha-icon-button { | ||||
|   | ||||
| @@ -254,7 +254,7 @@ class DateRangePickerElement extends WrappedElement { | ||||
|           .daterangepicker select.hourselect, | ||||
|           .daterangepicker select.minuteselect, | ||||
|           .daterangepicker select.secondselect { | ||||
|             background: transparent; | ||||
|             background: var(--card-background-color); | ||||
|             border: 1px solid var(--divider-color); | ||||
|             color: var(--primary-color); | ||||
|           } | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class HaDeviceTriggerPicker extends HaDeviceAutomationPicker<DeviceTrigger> { | ||||
|       fetchDeviceTriggers, | ||||
|       (deviceId?: string) => ({ | ||||
|         device_id: deviceId || "", | ||||
|         platform: "device", | ||||
|         trigger: "device", | ||||
|         domain: "", | ||||
|         entity_id: "", | ||||
|       }) | ||||
|   | ||||
| @@ -1,10 +1,9 @@ | ||||
| import type { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { css, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { isValidEntityId } from "../../common/entity/valid_entity_id"; | ||||
| import type { ValueChangedEvent, HomeAssistant } from "../../types"; | ||||
| import type { HomeAssistant, ValueChangedEvent } from "../../types"; | ||||
| import "./ha-entity-picker"; | ||||
| import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker"; | ||||
|  | ||||
| @@ -98,10 +97,7 @@ class HaEntitiesPickerLight extends LitElement { | ||||
|               .excludeEntities=${this.excludeEntities} | ||||
|               .includeDeviceClasses=${this.includeDeviceClasses} | ||||
|               .includeUnitOfMeasurement=${this.includeUnitOfMeasurement} | ||||
|               .entityFilter=${this._getEntityFilter( | ||||
|                 this.value, | ||||
|                 this.entityFilter | ||||
|               )} | ||||
|               .entityFilter=${this.entityFilter} | ||||
|               .value=${entityId} | ||||
|               .label=${this.pickedEntityLabel} | ||||
|               .disabled=${this.disabled} | ||||
| @@ -118,10 +114,13 @@ class HaEntitiesPickerLight extends LitElement { | ||||
|           .includeDomains=${this.includeDomains} | ||||
|           .excludeDomains=${this.excludeDomains} | ||||
|           .includeEntities=${this.includeEntities} | ||||
|           .excludeEntities=${this.excludeEntities} | ||||
|           .excludeEntities=${this._excludeEntities( | ||||
|             this.value, | ||||
|             this.excludeEntities | ||||
|           )} | ||||
|           .includeDeviceClasses=${this.includeDeviceClasses} | ||||
|           .includeUnitOfMeasurement=${this.includeUnitOfMeasurement} | ||||
|           .entityFilter=${this._getEntityFilter(this.value, this.entityFilter)} | ||||
|           .entityFilter=${this.entityFilter} | ||||
|           .label=${this.pickEntityLabel} | ||||
|           .helper=${this.helper} | ||||
|           .disabled=${this.disabled} | ||||
| @@ -133,14 +132,16 @@ class HaEntitiesPickerLight extends LitElement { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _getEntityFilter = memoizeOne( | ||||
|   private _excludeEntities = memoizeOne( | ||||
|     ( | ||||
|       value: string[] | undefined, | ||||
|       entityFilter: HaEntityPickerEntityFilterFunc | undefined | ||||
|     ): HaEntityPickerEntityFilterFunc => | ||||
|       (stateObj: HassEntity) => | ||||
|         (!value || !value.includes(stateObj.entity_id)) && | ||||
|         (!entityFilter || entityFilter(stateObj)) | ||||
|       excludeEntities: string[] | undefined | ||||
|     ): string[] | undefined => { | ||||
|       if (value === undefined) { | ||||
|         return excludeEntities; | ||||
|       } | ||||
|       return [...(excludeEntities || []), ...value]; | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   private get _currentEntities() { | ||||
|   | ||||
| @@ -87,7 +87,7 @@ export class HaEntityPicker extends LitElement { | ||||
|   public includeUnitOfMeasurement?: string[]; | ||||
|  | ||||
|   /** | ||||
|    * List of allowed entities to show. Will ignore all other filters. | ||||
|    * List of allowed entities to show. | ||||
|    * @type {Array} | ||||
|    * @attr include-entities | ||||
|    */ | ||||
| @@ -220,30 +220,13 @@ export class HaEntityPicker extends LitElement { | ||||
|  | ||||
|       if (includeEntities) { | ||||
|         entityIds = entityIds.filter((entityId) => | ||||
|           this.includeEntities!.includes(entityId) | ||||
|           includeEntities.includes(entityId) | ||||
|         ); | ||||
|  | ||||
|         return entityIds | ||||
|           .map((key) => { | ||||
|             const friendly_name = computeStateName(hass!.states[key]) || key; | ||||
|             return { | ||||
|               ...hass!.states[key], | ||||
|               friendly_name, | ||||
|               strings: [key, friendly_name], | ||||
|             }; | ||||
|           }) | ||||
|           .sort((entityA, entityB) => | ||||
|             caseInsensitiveStringCompare( | ||||
|               entityA.friendly_name, | ||||
|               entityB.friendly_name, | ||||
|               this.hass.locale.language | ||||
|             ) | ||||
|           ); | ||||
|       } | ||||
|  | ||||
|       if (excludeEntities) { | ||||
|         entityIds = entityIds.filter( | ||||
|           (entityId) => !excludeEntities!.includes(entityId) | ||||
|           (entityId) => !excludeEntities.includes(entityId) | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|   | ||||
| @@ -173,6 +173,7 @@ class HaEntityStatePicker extends LitElement { | ||||
|               no-style | ||||
|               @item-moved=${this._moveItem} | ||||
|               .disabled=${this.disabled} | ||||
|               filter="button.trailing.action" | ||||
|             > | ||||
|               <ha-chip-set> | ||||
|                 ${repeat( | ||||
|   | ||||
| @@ -1,5 +1,17 @@ | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   PropertyValues, | ||||
|   TemplateResult, | ||||
| } from "lit"; | ||||
| import { | ||||
|   customElement, | ||||
|   property, | ||||
|   query, | ||||
|   state as litState, | ||||
| } from "lit/decorators"; | ||||
|  | ||||
| interface State { | ||||
|   bold: boolean; | ||||
| @@ -11,11 +23,24 @@ interface State { | ||||
| } | ||||
|  | ||||
| @customElement("ha-ansi-to-html") | ||||
| class HaAnsiToHtml extends LitElement { | ||||
| export class HaAnsiToHtml extends LitElement { | ||||
|   @property() public content!: string; | ||||
|  | ||||
|   @query("pre") private _pre?: HTMLPreElement; | ||||
|  | ||||
|   @litState() private _filter = ""; | ||||
|  | ||||
|   protected render(): TemplateResult | void { | ||||
|     return html`${this._parseTextToColoredPre(this.content)}`; | ||||
|     return html`<pre></pre>`; | ||||
|   } | ||||
|  | ||||
|   protected firstUpdated(_changedProperties: PropertyValues): void { | ||||
|     super.firstUpdated(_changedProperties); | ||||
|  | ||||
|     // handle initial content | ||||
|     if (this.content) { | ||||
|       this.parseTextToColoredPre(this.content); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
| @@ -24,6 +49,7 @@ class HaAnsiToHtml extends LitElement { | ||||
|         overflow-x: auto; | ||||
|         white-space: pre-wrap; | ||||
|         overflow-wrap: break-word; | ||||
|         margin: 0; | ||||
|       } | ||||
|       .bold { | ||||
|         font-weight: bold; | ||||
| @@ -85,11 +111,33 @@ class HaAnsiToHtml extends LitElement { | ||||
|       .bg-white { | ||||
|         background-color: rgb(204, 204, 204); | ||||
|       } | ||||
|  | ||||
|       ::highlight(search-results) { | ||||
|         background-color: var(--primary-color); | ||||
|         color: var(--text-primary-color); | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _parseTextToColoredPre(text) { | ||||
|     const pre = document.createElement("pre"); | ||||
|   /** | ||||
|    * add new lines to the log | ||||
|    * @param lines log lines | ||||
|    * @param top should the new lines be added to the top of the log | ||||
|    */ | ||||
|   public parseLinesToColoredPre(lines: string[], top = false) { | ||||
|     for (const line of lines) { | ||||
|       this.parseLineToColoredPre(line, top); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Add a single line to the log | ||||
|    * @param line log line | ||||
|    * @param top should the new line be added to the top of the log | ||||
|    */ | ||||
|   public parseLineToColoredPre(line, top = false) { | ||||
|     const lineDiv = document.createElement("div"); | ||||
|  | ||||
|     // eslint-disable-next-line no-control-regex | ||||
|     const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g; | ||||
|     let i = 0; | ||||
| @@ -103,7 +151,7 @@ class HaAnsiToHtml extends LitElement { | ||||
|       backgroundColor: null, | ||||
|     }; | ||||
|  | ||||
|     const addSpan = (content) => { | ||||
|     const addPart = (content) => { | ||||
|       const span = document.createElement("span"); | ||||
|       if (state.bold) { | ||||
|         span.classList.add("bold"); | ||||
| @@ -124,15 +172,18 @@ class HaAnsiToHtml extends LitElement { | ||||
|         span.classList.add(`bg-${state.backgroundColor}`); | ||||
|       } | ||||
|       span.appendChild(document.createTextNode(content)); | ||||
|       pre.appendChild(span); | ||||
|       lineDiv.appendChild(span); | ||||
|     }; | ||||
|  | ||||
|     /* eslint-disable no-cond-assign */ | ||||
|     let match; | ||||
|     // eslint-disable-next-line | ||||
|     while ((match = re.exec(text)) !== null) { | ||||
|     while ((match = re.exec(line)) !== null) { | ||||
|       const j = match!.index; | ||||
|       addSpan(text.substring(i, j)); | ||||
|       const substring = line.substring(i, j); | ||||
|       if (substring) { | ||||
|         addPart(substring); | ||||
|       } | ||||
|       i = j + match[0].length; | ||||
|  | ||||
|       if (match[1] === undefined) { | ||||
| @@ -234,9 +285,93 @@ class HaAnsiToHtml extends LitElement { | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|     addSpan(text.substring(i)); | ||||
|  | ||||
|     return pre; | ||||
|     const substring = line.substring(i); | ||||
|     if (substring) { | ||||
|       addPart(substring); | ||||
|     } | ||||
|  | ||||
|     if (top) { | ||||
|       this._pre?.prepend(lineDiv); | ||||
|       lineDiv.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 500 }); | ||||
|     } else { | ||||
|       this._pre?.appendChild(lineDiv); | ||||
|     } | ||||
|  | ||||
|     // filter new lines if a filter is set | ||||
|     if (this._filter) { | ||||
|       this.filterLines(this._filter); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public parseTextToColoredPre(text) { | ||||
|     const lines = text.split("\n"); | ||||
|  | ||||
|     for (const line of lines) { | ||||
|       this.parseLineToColoredPre(line); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Filter lines based on a search string, lines and search string will be converted to lowercase | ||||
|    * @param filter the search string | ||||
|    * @returns true if there are lines to display | ||||
|    */ | ||||
|   filterLines(filter: string): boolean { | ||||
|     this._filter = filter; | ||||
|     const lines = this.shadowRoot?.querySelectorAll("div") || []; | ||||
|     let numberOfFoundLines = 0; | ||||
|     if (!filter) { | ||||
|       lines.forEach((line) => { | ||||
|         line.style.display = ""; | ||||
|       }); | ||||
|       numberOfFoundLines = lines.length; | ||||
|       if (CSS.highlights) { | ||||
|         CSS.highlights.delete("search-results"); | ||||
|       } | ||||
|     } else { | ||||
|       const highlightRanges: Range[] = []; | ||||
|       lines.forEach((line) => { | ||||
|         if (!line.textContent?.toLowerCase().includes(filter.toLowerCase())) { | ||||
|           line.style.display = "none"; | ||||
|         } else { | ||||
|           line.style.display = ""; | ||||
|           numberOfFoundLines++; | ||||
|           if (CSS.highlights && line.firstChild !== null && line.textContent) { | ||||
|             const spansOfLine = line.querySelectorAll("span"); | ||||
|             spansOfLine.forEach((span) => { | ||||
|               const text = span.textContent.toLowerCase(); | ||||
|               const indices: number[] = []; | ||||
|               let startPos = 0; | ||||
|               while (startPos < text.length) { | ||||
|                 const index = text.indexOf(filter.toLowerCase(), startPos); | ||||
|                 if (index === -1) break; | ||||
|                 indices.push(index); | ||||
|                 startPos = index + filter.length; | ||||
|               } | ||||
|  | ||||
|               indices.forEach((index) => { | ||||
|                 const range = new Range(); | ||||
|                 range.setStart(span.firstChild!, index); | ||||
|                 range.setEnd(span.firstChild!, index + filter.length); | ||||
|                 highlightRanges.push(range); | ||||
|               }); | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|       if (CSS.highlights) { | ||||
|         CSS.highlights.set("search-results", new Highlight(...highlightRanges)); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return !!numberOfFoundLines; | ||||
|   } | ||||
|  | ||||
|   public clear() { | ||||
|     if (this._pre) { | ||||
|       this._pre.innerHTML = ""; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { mdiTextureBox } from "@mdi/js"; | ||||
| import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; | ||||
| import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| @@ -20,12 +20,7 @@ import { | ||||
|   getDeviceEntityDisplayLookup, | ||||
| } from "../data/device_registry"; | ||||
| import { EntityRegistryDisplayEntry } from "../data/entity_registry"; | ||||
| import { | ||||
|   FloorRegistryEntry, | ||||
|   getFloorAreaLookup, | ||||
|   subscribeFloorRegistry, | ||||
| } from "../data/floor_registry"; | ||||
| import { SubscribeMixin } from "../mixins/subscribe-mixin"; | ||||
| import { FloorRegistryEntry, getFloorAreaLookup } from "../data/floor_registry"; | ||||
| import { HomeAssistant, ValueChangedEvent } from "../types"; | ||||
| import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||
| import "./ha-combo-box"; | ||||
| @@ -50,7 +45,7 @@ interface FloorAreaEntry { | ||||
| } | ||||
|  | ||||
| @customElement("ha-area-floor-picker") | ||||
| export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { | ||||
| export class HaAreaFloorPicker extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property() public label?: string; | ||||
| @@ -111,22 +106,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @property({ type: Boolean }) public required = false; | ||||
|  | ||||
|   @state() private _floors?: FloorRegistryEntry[]; | ||||
|  | ||||
|   @state() private _opened?: boolean; | ||||
|  | ||||
|   @query("ha-combo-box", true) public comboBox!: HaComboBox; | ||||
|  | ||||
|   private _init = false; | ||||
|  | ||||
|   protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] { | ||||
|     return [ | ||||
|       subscribeFloorRegistry(this.hass.connection, (floors) => { | ||||
|         this._floors = floors; | ||||
|       }), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   public async open() { | ||||
|     await this.updateComplete; | ||||
|     await this.comboBox?.open(); | ||||
| @@ -431,12 +416,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     if ( | ||||
|       (!this._init && this.hass && this._floors) || | ||||
|       (!this._init && this.hass) || | ||||
|       (this._init && changedProps.has("_opened") && this._opened) | ||||
|     ) { | ||||
|       this._init = true; | ||||
|       const areas = this._getAreas( | ||||
|         this._floors!, | ||||
|         Object.values(this.hass.floors), | ||||
|         Object.values(this.hass.areas), | ||||
|         Object.values(this.hass.devices), | ||||
|         Object.values(this.hass.entities), | ||||
|   | ||||
							
								
								
									
										155
									
								
								src/components/ha-badge.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,155 @@ | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { ifDefined } from "lit/directives/if-defined"; | ||||
| import "./ha-ripple"; | ||||
|  | ||||
| type BadgeType = "badge" | "button"; | ||||
|  | ||||
| @customElement("ha-badge") | ||||
| export class HaBadge extends LitElement { | ||||
|   @property() public type: BadgeType = "badge"; | ||||
|  | ||||
|   @property() public label?: string; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "icon-only" }) iconOnly = false; | ||||
|  | ||||
|   protected render() { | ||||
|     const label = this.label; | ||||
|  | ||||
|     return html` | ||||
|       <div | ||||
|         class="badge ${classMap({ | ||||
|           "icon-only": this.iconOnly, | ||||
|         })}" | ||||
|         role=${ifDefined(this.type === "button" ? "button" : undefined)} | ||||
|         tabindex=${ifDefined(this.type === "button" ? "0" : undefined)} | ||||
|       > | ||||
|         <ha-ripple .disabled=${this.type !== "button"}></ha-ripple> | ||||
|         <slot name="icon"></slot> | ||||
|         ${this.iconOnly | ||||
|           ? nothing | ||||
|           : html`<span class="info"> | ||||
|               ${label ? html`<span class="label">${label}</span>` : nothing} | ||||
|               <span class="content"><slot></slot></span> | ||||
|             </span>`} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       :host { | ||||
|         --badge-color: var(--secondary-text-color); | ||||
|         -webkit-tap-highlight-color: transparent; | ||||
|       } | ||||
|       .badge { | ||||
|         position: relative; | ||||
|         --ha-ripple-color: var(--badge-color); | ||||
|         --ha-ripple-hover-opacity: 0.04; | ||||
|         --ha-ripple-pressed-opacity: 0.12; | ||||
|         transition: | ||||
|           box-shadow 180ms ease-in-out, | ||||
|           border-color 180ms ease-in-out; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         align-items: center; | ||||
|         justify-content: center; | ||||
|         gap: 8px; | ||||
|         height: var(--ha-badge-size, 36px); | ||||
|         min-width: var(--ha-badge-size, 36px); | ||||
|         padding: 0px 12px; | ||||
|         box-sizing: border-box; | ||||
|         width: auto; | ||||
|         border-radius: var( | ||||
|           --ha-badge-border-radius, | ||||
|           calc(var(--ha-badge-size, 36px) / 2) | ||||
|         ); | ||||
|         background: var( | ||||
|           --ha-card-background, | ||||
|           var(--card-background-color, white) | ||||
|         ); | ||||
|         -webkit-backdrop-filter: var(--ha-card-backdrop-filter, none); | ||||
|         backdrop-filter: var(--ha-card-backdrop-filter, none); | ||||
|         border-width: var(--ha-card-border-width, 1px); | ||||
|         box-shadow: var(--ha-card-box-shadow, none); | ||||
|         border-style: solid; | ||||
|         border-color: var( | ||||
|           --ha-card-border-color, | ||||
|           var(--divider-color, #e0e0e0) | ||||
|         ); | ||||
|       } | ||||
|       .badge:focus-visible { | ||||
|         --shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent); | ||||
|         --shadow-focus: 0 0 0 1px var(--badge-color); | ||||
|         border-color: var(--badge-color); | ||||
|         box-shadow: var(--shadow-default), var(--shadow-focus); | ||||
|       } | ||||
|       [role="button"] { | ||||
|         cursor: pointer; | ||||
|       } | ||||
|       [role="button"]:focus { | ||||
|         outline: none; | ||||
|       } | ||||
|       .info { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         align-items: flex-start; | ||||
|         padding-inline-start: initial; | ||||
|         text-align: center; | ||||
|         font-family: Roboto; | ||||
|       } | ||||
|       .label { | ||||
|         font-size: 10px; | ||||
|         font-style: normal; | ||||
|         font-weight: 500; | ||||
|         line-height: 10px; | ||||
|         letter-spacing: 0.1px; | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|       .content { | ||||
|         font-size: 12px; | ||||
|         font-style: normal; | ||||
|         font-weight: 500; | ||||
|         line-height: 16px; | ||||
|         letter-spacing: 0.1px; | ||||
|         color: var(--primary-text-color); | ||||
|       } | ||||
|       ::slotted([slot="icon"]) { | ||||
|         --mdc-icon-size: 18px; | ||||
|         color: var(--badge-color); | ||||
|         line-height: 0; | ||||
|         margin-left: -4px; | ||||
|         margin-right: 0; | ||||
|         margin-inline-start: -4px; | ||||
|         margin-inline-end: 0; | ||||
|       } | ||||
|       ::slotted(img[slot="icon"]) { | ||||
|         width: 30px; | ||||
|         height: 30px; | ||||
|         border-radius: 50%; | ||||
|         object-fit: cover; | ||||
|         overflow: hidden; | ||||
|         margin-left: -10px; | ||||
|         margin-right: 0; | ||||
|         margin-inline-start: -10px; | ||||
|         margin-inline-end: 0; | ||||
|       } | ||||
|       .badge.icon-only { | ||||
|         padding: 0; | ||||
|       } | ||||
|       .badge.icon-only ::slotted([slot="icon"]) { | ||||
|         margin-left: 0; | ||||
|         margin-right: 0; | ||||
|         margin-inline-start: 0; | ||||
|         margin-inline-end: 0; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-badge": HaBadge; | ||||
|   } | ||||
| } | ||||
| @@ -43,7 +43,7 @@ export class HaCard extends LitElement { | ||||
|  | ||||
|       .card-header, | ||||
|       :host ::slotted(.card-header) { | ||||
|         color: var(--ha-card-header-color, --primary-text-color); | ||||
|         color: var(--ha-card-header-color, var(--primary-text-color)); | ||||
|         font-family: var(--ha-card-header-font-family, inherit); | ||||
|         font-size: var(--ha-card-header-font-size, 24px); | ||||
|         letter-spacing: -0.012em; | ||||
|   | ||||
| @@ -124,9 +124,12 @@ export class HaCodeEditor extends ReactiveElement { | ||||
|     const transactions: TransactionSpec[] = []; | ||||
|     if (changedProps.has("mode")) { | ||||
|       transactions.push({ | ||||
|         effects: this._loadedCodeMirror!.langCompartment!.reconfigure( | ||||
|           this._mode | ||||
|         ), | ||||
|         effects: [ | ||||
|           this._loadedCodeMirror!.langCompartment!.reconfigure(this._mode), | ||||
|           this._loadedCodeMirror!.foldingCompartment.reconfigure( | ||||
|             this._getFoldingExtensions() | ||||
|           ), | ||||
|         ], | ||||
|       }); | ||||
|     } | ||||
|     if (changedProps.has("readOnly")) { | ||||
| @@ -177,6 +180,14 @@ export class HaCodeEditor extends ReactiveElement { | ||||
|       this._loadedCodeMirror.crosshairCursor(), | ||||
|       this._loadedCodeMirror.highlightSelectionMatches(), | ||||
|       this._loadedCodeMirror.highlightActiveLine(), | ||||
|       this._loadedCodeMirror.indentationMarkers({ | ||||
|         thickness: 0, | ||||
|         activeThickness: 1, | ||||
|         colors: { | ||||
|           activeLight: "var(--secondary-text-color)", | ||||
|           activeDark: "var(--secondary-text-color)", | ||||
|         }, | ||||
|       }), | ||||
|       this._loadedCodeMirror.keymap.of([ | ||||
|         ...this._loadedCodeMirror.defaultKeymap, | ||||
|         ...this._loadedCodeMirror.searchKeymap, | ||||
| @@ -194,6 +205,9 @@ export class HaCodeEditor extends ReactiveElement { | ||||
|         this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : [] | ||||
|       ), | ||||
|       this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate), | ||||
|       this._loadedCodeMirror.foldingCompartment.of( | ||||
|         this._getFoldingExtensions() | ||||
|       ), | ||||
|     ]; | ||||
|  | ||||
|     if (!this.readOnly) { | ||||
| @@ -311,6 +325,17 @@ export class HaCodeEditor extends ReactiveElement { | ||||
|     fireEvent(this, "value-changed", { value: this._value }); | ||||
|   }; | ||||
|  | ||||
|   private _getFoldingExtensions = (): Extension => { | ||||
|     if (this.mode === "yaml") { | ||||
|       return [ | ||||
|         this._loadedCodeMirror!.foldGutter(), | ||||
|         this._loadedCodeMirror!.foldingOnIndent, | ||||
|       ]; | ||||
|     } | ||||
|  | ||||
|     return []; | ||||
|   }; | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       :host(.error-state) .cm-gutters { | ||||
|   | ||||
| @@ -1,14 +1,16 @@ | ||||
| import "@material/mwc-list/mwc-list-item"; | ||||
| import { mdiInvertColorsOff, mdiPalette } from "@mdi/js"; | ||||
| import { css, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { customElement, property, query } from "lit/decorators"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import { computeCssColor, THEME_COLORS } from "../common/color/compute-color"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { stopPropagation } from "../common/dom/stop_propagation"; | ||||
| import "./ha-select"; | ||||
| import "./ha-list-item"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import { LocalizeKeys } from "../common/translations/localize"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import "./ha-list-item"; | ||||
| import "./ha-md-divider"; | ||||
| import "./ha-select"; | ||||
| import type { HaSelect } from "./ha-select"; | ||||
|  | ||||
| @customElement("ha-color-picker") | ||||
| export class HaColorPicker extends LitElement { | ||||
| @@ -20,43 +22,97 @@ export class HaColorPicker extends LitElement { | ||||
|  | ||||
|   @property() public value?: string; | ||||
|  | ||||
|   @property({ type: Boolean }) public defaultColor = false; | ||||
|   @property({ type: String, attribute: "default_color" }) | ||||
|   public defaultColor?: string; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "include_state" }) | ||||
|   public includeState = false; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "include_none" }) | ||||
|   public includeNone = false; | ||||
|  | ||||
|   @property({ type: Boolean }) public disabled = false; | ||||
|  | ||||
|   _valueSelected(ev) { | ||||
|   @query("ha-select") private _select?: HaSelect; | ||||
|  | ||||
|   connectedCallback(): void { | ||||
|     super.connectedCallback(); | ||||
|     // Refresh layout options when the field is connected to the DOM to ensure current value displayed | ||||
|     this._select?.layoutOptions(); | ||||
|   } | ||||
|  | ||||
|   private _valueSelected(ev) { | ||||
|     ev.stopPropagation(); | ||||
|     if (!this.isConnected) return; | ||||
|     const value = ev.target.value; | ||||
|     if (value) { | ||||
|       fireEvent(this, "value-changed", { | ||||
|         value: value !== "default" ? value : undefined, | ||||
|       }); | ||||
|     } | ||||
|     this.value = value === this.defaultColor ? undefined : value; | ||||
|     fireEvent(this, "value-changed", { | ||||
|       value: this.value, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   render() { | ||||
|     const value = this.value || this.defaultColor || ""; | ||||
|  | ||||
|     const isCustom = !( | ||||
|       THEME_COLORS.has(value) || | ||||
|       value === "none" || | ||||
|       value === "state" | ||||
|     ); | ||||
|  | ||||
|     return html` | ||||
|       <ha-select | ||||
|         .icon=${Boolean(this.value)} | ||||
|         .icon=${Boolean(value)} | ||||
|         .label=${this.label} | ||||
|         .value=${this.value || "default"} | ||||
|         .value=${value} | ||||
|         .helper=${this.helper} | ||||
|         .disabled=${this.disabled} | ||||
|         @closed=${stopPropagation} | ||||
|         @selected=${this._valueSelected} | ||||
|         fixedMenuPosition | ||||
|         naturalMenuWidth | ||||
|         .clearable=${!this.defaultColor} | ||||
|       > | ||||
|         ${this.value | ||||
|         ${value | ||||
|           ? html` | ||||
|               <span slot="icon"> | ||||
|                 ${this.renderColorCircle(this.value || "grey")} | ||||
|                 ${value === "none" | ||||
|                   ? html` | ||||
|                       <ha-svg-icon path=${mdiInvertColorsOff}></ha-svg-icon> | ||||
|                     ` | ||||
|                   : value === "state" | ||||
|                     ? html`<ha-svg-icon path=${mdiPalette}></ha-svg-icon>` | ||||
|                     : this.renderColorCircle(value || "grey")} | ||||
|               </span> | ||||
|             ` | ||||
|           : nothing} | ||||
|         ${this.defaultColor | ||||
|           ? html` <ha-list-item value="default"> | ||||
|               ${this.hass.localize(`ui.components.color-picker.default_color`)} | ||||
|             </ha-list-item>` | ||||
|         ${this.includeNone | ||||
|           ? html` | ||||
|               <ha-list-item value="none" graphic="icon"> | ||||
|                 ${this.hass.localize("ui.components.color-picker.none")} | ||||
|                 ${this.defaultColor === "none" | ||||
|                   ? ` (${this.hass.localize("ui.components.color-picker.default")})` | ||||
|                   : nothing} | ||||
|                 <ha-svg-icon | ||||
|                   slot="graphic" | ||||
|                   path=${mdiInvertColorsOff} | ||||
|                 ></ha-svg-icon> | ||||
|               </ha-list-item> | ||||
|             ` | ||||
|           : nothing} | ||||
|         ${this.includeState | ||||
|           ? html` | ||||
|               <ha-list-item value="state" graphic="icon"> | ||||
|                 ${this.hass.localize("ui.components.color-picker.state")} | ||||
|                 ${this.defaultColor === "state" | ||||
|                   ? ` (${this.hass.localize("ui.components.color-picker.default")})` | ||||
|                   : nothing} | ||||
|                 <ha-svg-icon slot="graphic" path=${mdiPalette}></ha-svg-icon> | ||||
|               </ha-list-item> | ||||
|             ` | ||||
|           : nothing} | ||||
|         ${this.includeState || this.includeNone | ||||
|           ? html`<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>` | ||||
|           : nothing} | ||||
|         ${Array.from(THEME_COLORS).map( | ||||
|           (color) => html` | ||||
| @@ -64,10 +120,21 @@ export class HaColorPicker extends LitElement { | ||||
|               ${this.hass.localize( | ||||
|                 `ui.components.color-picker.colors.${color}` as LocalizeKeys | ||||
|               ) || color} | ||||
|               ${this.defaultColor === color | ||||
|                 ? ` (${this.hass.localize("ui.components.color-picker.default")})` | ||||
|                 : nothing} | ||||
|               <span slot="graphic">${this.renderColorCircle(color)}</span> | ||||
|             </ha-list-item> | ||||
|           ` | ||||
|         )} | ||||
|         ${isCustom | ||||
|           ? html` | ||||
|               <ha-list-item .value=${value} graphic="icon"> | ||||
|                 ${value} | ||||
|                 <span slot="graphic">${this.renderColorCircle(value)}</span> | ||||
|               </ha-list-item> | ||||
|             ` | ||||
|           : nothing} | ||||
|       </ha-select> | ||||
|     `; | ||||
|   } | ||||
| @@ -87,10 +154,11 @@ export class HaColorPicker extends LitElement { | ||||
|     return css` | ||||
|       .circle-color { | ||||
|         display: block; | ||||
|         background-color: var(--circle-color); | ||||
|         background-color: var(--circle-color, var(--divider-color)); | ||||
|         border-radius: 10px; | ||||
|         width: 20px; | ||||
|         height: 20px; | ||||
|         box-sizing: border-box; | ||||
|       } | ||||
|       ha-select { | ||||
|         width: 100%; | ||||
|   | ||||
| @@ -45,7 +45,7 @@ export class HaControlButton extends LitElement { | ||||
|         position: relative; | ||||
|         cursor: pointer; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         flex-direction: row; | ||||
|         align-items: center; | ||||
|         justify-content: center; | ||||
|         text-align: center; | ||||
|   | ||||
| @@ -10,8 +10,13 @@ export class HaDialogHeader extends LitElement { | ||||
|           <section class="header-navigation-icon"> | ||||
|             <slot name="navigationIcon"></slot> | ||||
|           </section> | ||||
|           <section class="header-title"> | ||||
|             <slot name="title"></slot> | ||||
|           <section class="header-content"> | ||||
|             <div class="header-title"> | ||||
|               <slot name="title"></slot> | ||||
|             </div> | ||||
|             <div class="header-subtitle"> | ||||
|               <slot name="subtitle"></slot> | ||||
|             </div> | ||||
|           </section> | ||||
|           <section class="header-action-items"> | ||||
|             <slot name="actionItems"></slot> | ||||
| @@ -39,17 +44,24 @@ export class HaDialogHeader extends LitElement { | ||||
|           padding: 4px; | ||||
|           box-sizing: border-box; | ||||
|         } | ||||
|         .header-title { | ||||
|         .header-content { | ||||
|           flex: 1; | ||||
|           font-size: 22px; | ||||
|           line-height: 28px; | ||||
|           font-weight: 400; | ||||
|           padding: 10px 4px; | ||||
|           min-width: 0; | ||||
|           overflow: hidden; | ||||
|           text-overflow: ellipsis; | ||||
|           white-space: nowrap; | ||||
|         } | ||||
|         .header-title { | ||||
|           font-size: 22px; | ||||
|           line-height: 28px; | ||||
|           font-weight: 400; | ||||
|         } | ||||
|         .header-subtitle { | ||||
|           font-size: 14px; | ||||
|           line-height: 20px; | ||||
|           color: var(--secondary-text-color); | ||||
|         } | ||||
|         @media all and (min-width: 450px) and (min-height: 500px) { | ||||
|           .header-bar { | ||||
|             padding: 12px; | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import "@material/mwc-menu/mwc-menu-surface"; | ||||
| import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js"; | ||||
| import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
|   LitElement, | ||||
| @@ -15,13 +14,8 @@ import { repeat } from "lit/directives/repeat"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { computeRTL } from "../common/util/compute_rtl"; | ||||
| import { | ||||
|   FloorRegistryEntry, | ||||
|   getFloorAreaLookup, | ||||
|   subscribeFloorRegistry, | ||||
| } from "../data/floor_registry"; | ||||
| import { getFloorAreaLookup } from "../data/floor_registry"; | ||||
| import { RelatedResult, findRelated } from "../data/search"; | ||||
| import { SubscribeMixin } from "../mixins/subscribe-mixin"; | ||||
| import { haStyleScrollbar } from "../resources/styles"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import "./ha-check-list-item"; | ||||
| @@ -31,7 +25,7 @@ import "./ha-svg-icon"; | ||||
| import "./ha-tree-indicator"; | ||||
|  | ||||
| @customElement("ha-filter-floor-areas") | ||||
| export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
| export class HaFilterFloorAreas extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public value?: { | ||||
| @@ -47,8 +41,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @state() private _shouldRender = false; | ||||
|  | ||||
|   @state() private _floors?: FloorRegistryEntry[]; | ||||
|  | ||||
|   public willUpdate(properties: PropertyValues) { | ||||
|     super.willUpdate(properties); | ||||
|  | ||||
| @@ -60,7 +52,7 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     const areas = this._areas(this.hass.areas, this._floors); | ||||
|     const areas = this._areas(this.hass.areas, this.hass.floors); | ||||
|  | ||||
|     return html` | ||||
|       <ha-expansion-panel | ||||
| @@ -189,14 +181,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|     this._findRelated(); | ||||
|   } | ||||
|  | ||||
|   protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] { | ||||
|     return [ | ||||
|       subscribeFloorRegistry(this.hass.connection, (floors) => { | ||||
|         this._floors = floors; | ||||
|       }), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   protected updated(changed) { | ||||
|     if (changed.has("expanded") && this.expanded) { | ||||
|       setTimeout(() => { | ||||
| @@ -220,9 +204,9 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|   } | ||||
|  | ||||
|   private _areas = memoizeOne( | ||||
|     (areaReg: HomeAssistant["areas"], floors?: FloorRegistryEntry[]) => { | ||||
|     (areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => { | ||||
|       const areas = Object.values(areaReg); | ||||
|  | ||||
|       const floors = Object.values(floorReg); | ||||
|       const floorAreaLookup = getFloorAreaLookup(areas); | ||||
|  | ||||
|       const unassisgnedAreas = areas.filter( | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; | ||||
| import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { LitElement, PropertyValues, TemplateResult, html } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| @@ -24,10 +24,8 @@ import { | ||||
|   FloorRegistryEntry, | ||||
|   createFloorRegistryEntry, | ||||
|   getFloorAreaLookup, | ||||
|   subscribeFloorRegistry, | ||||
| } from "../data/floor_registry"; | ||||
| import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; | ||||
| import { SubscribeMixin } from "../mixins/subscribe-mixin"; | ||||
| import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail"; | ||||
| import { HomeAssistant, ValueChangedEvent } from "../types"; | ||||
| import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||
| @@ -53,7 +51,7 @@ const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) => | ||||
|   </ha-list-item>`; | ||||
|  | ||||
| @customElement("ha-floor-picker") | ||||
| export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
| export class HaFloorPicker extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property() public label?: string; | ||||
| @@ -111,8 +109,6 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @state() private _opened?: boolean; | ||||
|  | ||||
|   @state() private _floors?: FloorRegistryEntry[]; | ||||
|  | ||||
|   @query("ha-combo-box", true) public comboBox!: HaComboBox; | ||||
|  | ||||
|   private _suggestion?: string; | ||||
| @@ -129,14 +125,6 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|     await this.comboBox?.focus(); | ||||
|   } | ||||
|  | ||||
|   protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] { | ||||
|     return [ | ||||
|       subscribeFloorRegistry(this.hass.connection, (floors) => { | ||||
|         this._floors = floors; | ||||
|       }), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   private _getFloors = memoizeOne( | ||||
|     ( | ||||
|       floors: FloorRegistryEntry[], | ||||
| @@ -320,12 +308,12 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     if ( | ||||
|       (!this._init && this.hass && this._floors) || | ||||
|       (!this._init && this.hass) || | ||||
|       (this._init && changedProps.has("_opened") && this._opened) | ||||
|     ) { | ||||
|       this._init = true; | ||||
|       const floors = this._getFloors( | ||||
|         this._floors!, | ||||
|         Object.values(this.hass.floors), | ||||
|         Object.values(this.hass.areas), | ||||
|         Object.values(this.hass.devices), | ||||
|         Object.values(this.hass.entities), | ||||
| @@ -360,8 +348,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|           ? this.hass.localize("ui.components.floor-picker.floor") | ||||
|           : this.label} | ||||
|         .placeholder=${this.placeholder | ||||
|           ? this._floors?.find((floor) => floor.floor_id === this.placeholder) | ||||
|               ?.name | ||||
|           ? this.hass.floors[this.placeholder]?.name | ||||
|           : undefined} | ||||
|         .renderer=${rowRenderer} | ||||
|         @filter-changed=${this._filterChanged} | ||||
| @@ -460,7 +447,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|               floor_id: floor.floor_id, | ||||
|             }); | ||||
|           }); | ||||
|           const floors = [...this._floors!, floor]; | ||||
|           const floors = [...Object.values(this.hass.floors), floor]; | ||||
|           this.comboBox.filteredItems = this._getFloors( | ||||
|             floors, | ||||
|             Object.values(this.hass.areas)!, | ||||
|   | ||||
| @@ -95,10 +95,10 @@ export const computeInitialHaFormData = ( | ||||
|       } else if ( | ||||
|         "action" in selector || | ||||
|         "trigger" in selector || | ||||
|         "condition" in selector || | ||||
|         "media" in selector || | ||||
|         "target" in selector | ||||
|         "condition" in selector | ||||
|       ) { | ||||
|         data[field.name] = []; | ||||
|       } else if ("media" in selector || "target" in selector) { | ||||
|         data[field.name] = {}; | ||||
|       } else { | ||||
|         throw new Error( | ||||
|   | ||||
| @@ -30,6 +30,10 @@ export class HaFormExpendable extends LitElement implements HaFormElement { | ||||
|     options?: { path?: string[] } | ||||
|   ) => string; | ||||
|  | ||||
|   @property({ attribute: false }) public localizeValue?: ( | ||||
|     key: string | ||||
|   ) => string; | ||||
|  | ||||
|   private _renderDescription() { | ||||
|     const description = this.computeHelper?.(this.schema); | ||||
|     return description ? html`<p>${description}</p>` : nothing; | ||||
| @@ -86,6 +90,7 @@ export class HaFormExpendable extends LitElement implements HaFormElement { | ||||
|             .disabled=${this.disabled} | ||||
|             .computeLabel=${this._computeLabel} | ||||
|             .computeHelper=${this._computeHelper} | ||||
|             .localizeValue=${this.localizeValue} | ||||
|           ></ha-form> | ||||
|         </div> | ||||
|       </ha-expansion-panel> | ||||
|   | ||||
| @@ -35,6 +35,10 @@ export class HaFormGrid extends LitElement implements HaFormElement { | ||||
|     schema: HaFormSchema | ||||
|   ) => string; | ||||
|  | ||||
|   @property({ attribute: false }) public localizeValue?: ( | ||||
|     key: string | ||||
|   ) => string; | ||||
|  | ||||
|   public async focus() { | ||||
|     await this.updateComplete; | ||||
|     this.renderRoot.querySelector("ha-form")?.focus(); | ||||
| @@ -65,6 +69,7 @@ export class HaFormGrid extends LitElement implements HaFormElement { | ||||
|             .disabled=${this.disabled} | ||||
|             .computeLabel=${this.computeLabel} | ||||
|             .computeHelper=${this.computeHelper} | ||||
|             .localizeValue=${this.localizeValue} | ||||
|           ></ha-form> | ||||
|         ` | ||||
|       )} | ||||
|   | ||||
| @@ -73,6 +73,10 @@ export class HaForm extends LitElement implements HaFormElement { | ||||
|     schema: any | ||||
|   ) => string | undefined; | ||||
|  | ||||
|   @property({ attribute: false }) public localizeValue?: ( | ||||
|     key: string | ||||
|   ) => string; | ||||
|  | ||||
|   protected getFormProperties(): Record<string, any> { | ||||
|     return {}; | ||||
|   } | ||||
| @@ -145,6 +149,7 @@ export class HaForm extends LitElement implements HaFormElement { | ||||
|                   .disabled=${item.disabled || this.disabled || false} | ||||
|                   .placeholder=${item.required ? "" : item.default} | ||||
|                   .helper=${this._computeHelper(item)} | ||||
|                   .localizeValue=${this.localizeValue} | ||||
|                   .required=${item.required || false} | ||||
|                   .context=${this._generateContext(item)} | ||||
|                 ></ha-selector>` | ||||
| @@ -158,6 +163,7 @@ export class HaForm extends LitElement implements HaFormElement { | ||||
|                   localize: this.hass?.localize, | ||||
|                   computeLabel: this.computeLabel, | ||||
|                   computeHelper: this.computeHelper, | ||||
|                   localizeValue: this.localizeValue, | ||||
|                   context: this._generateContext(item), | ||||
|                   ...this.getFormProperties(), | ||||
|                 })} | ||||
|   | ||||
| @@ -46,7 +46,7 @@ export class HaHeaderBar extends LitElement { | ||||
|           flex: none; | ||||
|         } | ||||
|         .mdc-top-app-bar__title { | ||||
|           padding-inline-start: 20px; | ||||
|           padding-inline-start: 24px; | ||||
|           padding-inline-end: initial; | ||||
|         } | ||||
|       `, | ||||
|   | ||||
							
								
								
									
										58
									
								
								src/components/ha-heading-badge.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,58 @@ | ||||
| import { css, CSSResultGroup, html, LitElement } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { ifDefined } from "lit/directives/if-defined"; | ||||
|  | ||||
| type HeadingBadgeType = "text" | "button"; | ||||
|  | ||||
| @customElement("ha-heading-badge") | ||||
| export class HaBadge extends LitElement { | ||||
|   @property() public type: HeadingBadgeType = "text"; | ||||
|  | ||||
|   protected render() { | ||||
|     return html` | ||||
|       <div | ||||
|         class="heading-badge" | ||||
|         role=${ifDefined(this.type === "button" ? "button" : undefined)} | ||||
|         tabindex=${ifDefined(this.type === "button" ? "0" : undefined)} | ||||
|       > | ||||
|         <slot name="icon"></slot> | ||||
|         <slot></slot> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       :host { | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|       [role="button"] { | ||||
|         cursor: pointer; | ||||
|       } | ||||
|       .heading-badge { | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         white-space: nowrap; | ||||
|         align-items: center; | ||||
|         gap: 3px; | ||||
|         font-family: Roboto; | ||||
|         font-size: 14px; | ||||
|         font-style: normal; | ||||
|         font-weight: 400; | ||||
|         line-height: 20px; | ||||
|         letter-spacing: 0.1px; | ||||
|         --mdc-icon-size: 14px; | ||||
|       } | ||||
|       ::slotted([slot="icon"]) { | ||||
|         --ha-icon-display: block; | ||||
|         color: var(--icon-color, inherit); | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-heading-badge": HaBadge; | ||||
|   } | ||||
| } | ||||
| @@ -216,6 +216,7 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) { | ||||
|     ha-input-chip { | ||||
|       --md-input-chip-selected-container-color: var(--color, var(--grey-color)); | ||||
|       --ha-input-chip-selected-container-opacity: 0.5; | ||||
|       --md-input-chip-selected-outline-width: 1px; | ||||
|     } | ||||
|   `; | ||||
| } | ||||
|   | ||||
| @@ -86,6 +86,11 @@ export class HaMarkdown extends LitElement { | ||||
|         font-size: 1.5em; | ||||
|         font-weight: bold; | ||||
|       } | ||||
|       hr { | ||||
|         border-color: var(--divider-color); | ||||
|         border-bottom: none; | ||||
|         margin: 16px 0; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -6,8 +6,8 @@ import type { HaIconButton } from "./ha-icon-button"; | ||||
| import "./ha-menu"; | ||||
| import type { HaMenu } from "./ha-menu"; | ||||
| 
 | ||||
| @customElement("ha-button-menu-new") | ||||
| export class HaButtonMenuNew extends LitElement { | ||||
| @customElement("ha-md-button-menu") | ||||
| export class HaMdButtonMenu extends LitElement { | ||||
|   protected readonly [FOCUS_TARGET]; | ||||
| 
 | ||||
|   @property({ type: Boolean }) public disabled = false; | ||||
| @@ -84,6 +84,6 @@ export class HaButtonMenuNew extends LitElement { | ||||
| 
 | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-button-menu-new": HaButtonMenuNew; | ||||
|     "ha-md-button-menu": HaMdButtonMenu; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										250
									
								
								src/components/ha-md-dialog.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,250 @@ | ||||
| import { MdDialog } from "@material/web/dialog/dialog"; | ||||
| import { | ||||
|   type DialogAnimation, | ||||
|   DIALOG_DEFAULT_CLOSE_ANIMATION, | ||||
|   DIALOG_DEFAULT_OPEN_ANIMATION, | ||||
| } from "@material/web/dialog/internal/animations"; | ||||
| import { css } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
|  | ||||
| // workaround to be able to overlay an dialog with another dialog | ||||
| MdDialog.addInitializer(async (instance) => { | ||||
|   await instance.updateComplete; | ||||
|  | ||||
|   const dialogInstance = instance as MdDialog; | ||||
|  | ||||
|   // @ts-expect-error dialog is private | ||||
|   dialogInstance.dialog.prepend(dialogInstance.scrim); | ||||
|   // @ts-expect-error scrim is private | ||||
|   dialogInstance.scrim.style.inset = 0; | ||||
|   // @ts-expect-error scrim is private | ||||
|   dialogInstance.scrim.style.zIndex = 0; | ||||
|  | ||||
|   const { getOpenAnimation, getCloseAnimation } = dialogInstance; | ||||
|   dialogInstance.getOpenAnimation = () => { | ||||
|     const animations = getOpenAnimation.call(this); | ||||
|     animations.container = [ | ||||
|       ...(animations.container ?? []), | ||||
|       ...(animations.dialog ?? []), | ||||
|     ]; | ||||
|     animations.dialog = []; | ||||
|     return animations; | ||||
|   }; | ||||
|   dialogInstance.getCloseAnimation = () => { | ||||
|     const animations = getCloseAnimation.call(this); | ||||
|     animations.container = [ | ||||
|       ...(animations.container ?? []), | ||||
|       ...(animations.dialog ?? []), | ||||
|     ]; | ||||
|     animations.dialog = []; | ||||
|     return animations; | ||||
|   }; | ||||
| }); | ||||
|  | ||||
| let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>; | ||||
|  | ||||
| /** | ||||
|  * Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs | ||||
|  * | ||||
|  */ | ||||
| @customElement("ha-md-dialog") | ||||
| export class HaMdDialog extends MdDialog { | ||||
|   /** | ||||
|    * When true the dialog will not close when the user presses the esc key or press out of the dialog. | ||||
|    */ | ||||
|   @property({ attribute: "disable-cancel-action", type: Boolean }) | ||||
|   public disableCancelAction = false; | ||||
|  | ||||
|   private _polyfillDialogRegistered = false; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     this.addEventListener("cancel", this._handleCancel); | ||||
|  | ||||
|     if (typeof HTMLDialogElement !== "function") { | ||||
|       this.addEventListener("open", this._handleOpen); | ||||
|  | ||||
|       if (!DIALOG_POLYFILL) { | ||||
|         DIALOG_POLYFILL = import("dialog-polyfill"); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // if browser doesn't support animate API disable open/close animations | ||||
|     if (this.animate === undefined) { | ||||
|       this.quick = true; | ||||
|     } | ||||
|  | ||||
|     // if browser doesn't support animate API disable open/close animations | ||||
|     if (this.animate === undefined) { | ||||
|       this.quick = true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // prevent open in older browsers and wait for polyfill to load | ||||
|   private async _handleOpen(openEvent: Event) { | ||||
|     openEvent.preventDefault(); | ||||
|  | ||||
|     if (this._polyfillDialogRegistered) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this._polyfillDialogRegistered = true; | ||||
|     this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css"); | ||||
|     const dialog = this.shadowRoot?.querySelector( | ||||
|       "dialog" | ||||
|     ) as HTMLDialogElement; | ||||
|  | ||||
|     const dialogPolyfill = await DIALOG_POLYFILL; | ||||
|     dialogPolyfill.default.registerDialog(dialog); | ||||
|     this.removeEventListener("open", this._handleOpen); | ||||
|  | ||||
|     this.show(); | ||||
|   } | ||||
|  | ||||
|   private async _loadPolyfillStylesheet(href) { | ||||
|     const link = document.createElement("link"); | ||||
|     link.rel = "stylesheet"; | ||||
|     link.href = href; | ||||
|  | ||||
|     return new Promise<void>((resolve, reject) => { | ||||
|       link.onload = () => resolve(); | ||||
|       link.onerror = () => | ||||
|         reject(new Error(`Stylesheet failed to load: ${href}`)); | ||||
|  | ||||
|       this.shadowRoot?.appendChild(link); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   _handleCancel(closeEvent: Event) { | ||||
|     if (this.disableCancelAction) { | ||||
|       closeEvent.preventDefault(); | ||||
|       const dialogElement = this.shadowRoot?.querySelector("dialog .container"); | ||||
|       if (this.animate !== undefined) { | ||||
|         dialogElement?.animate( | ||||
|           [ | ||||
|             { | ||||
|               transform: "rotate(-1deg)", | ||||
|               "animation-timing-function": "ease-in", | ||||
|             }, | ||||
|             { | ||||
|               transform: "rotate(1.5deg)", | ||||
|               "animation-timing-function": "ease-out", | ||||
|             }, | ||||
|             { | ||||
|               transform: "rotate(0deg)", | ||||
|               "animation-timing-function": "ease-in", | ||||
|             }, | ||||
|           ], | ||||
|           { | ||||
|             duration: 200, | ||||
|             iterations: 2, | ||||
|           } | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   static override styles = [ | ||||
|     ...super.styles, | ||||
|     css` | ||||
|       :host { | ||||
|         --md-dialog-container-color: var(--card-background-color); | ||||
|         --md-dialog-headline-color: var(--primary-text-color); | ||||
|         --md-dialog-supporting-text-color: var(--primary-text-color); | ||||
|         --md-sys-color-scrim: #000000; | ||||
|  | ||||
|         --md-dialog-headline-weight: 400; | ||||
|         --md-dialog-headline-size: 1.574rem; | ||||
|         --md-dialog-supporting-text-size: 1rem; | ||||
|         --md-dialog-supporting-text-line-height: 1.5rem; | ||||
|       } | ||||
|  | ||||
|       :host([type="alert"]) { | ||||
|         min-width: 320px; | ||||
|       } | ||||
|  | ||||
|       :host(:not([type="alert"])) { | ||||
|         @media all and (max-width: 450px), all and (max-height: 500px) { | ||||
|           min-width: calc( | ||||
|             100vw - env(safe-area-inset-right) - env(safe-area-inset-left) | ||||
|           ); | ||||
|           max-width: calc( | ||||
|             100vw - env(safe-area-inset-right) - env(safe-area-inset-left) | ||||
|           ); | ||||
|           min-height: 100%; | ||||
|           max-height: 100%; | ||||
|           --md-dialog-container-shape: 0; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       :host ::slotted(ha-dialog-header) { | ||||
|         display: contents; | ||||
|       } | ||||
|  | ||||
|       slot[name="content"]::slotted(*) { | ||||
|         padding: var(--dialog-content-padding, 24px); | ||||
|       } | ||||
|       .scrim { | ||||
|         z-index: 10; // overlay navigation | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| // by default the dialog open/close animation will be from/to the top | ||||
| // but if we have a special mobile dialog which is at the bottom of the screen, an from bottom animation can be used: | ||||
| const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = { | ||||
|   ...DIALOG_DEFAULT_OPEN_ANIMATION, | ||||
|   dialog: [ | ||||
|     [ | ||||
|       // Dialog slide up | ||||
|       [{ transform: "translateY(50px)" }, { transform: "translateY(0)" }], | ||||
|       { duration: 500, easing: "cubic-bezier(.3,0,0,1)" }, | ||||
|     ], | ||||
|   ], | ||||
|   container: [ | ||||
|     [ | ||||
|       // Container fade in | ||||
|       [{ opacity: 0 }, { opacity: 1 }], | ||||
|       { duration: 50, easing: "linear", pseudoElement: "::before" }, | ||||
|     ], | ||||
|   ], | ||||
| }; | ||||
|  | ||||
| const CLOSE_TO_BOTTOM_ANIMATION: DialogAnimation = { | ||||
|   ...DIALOG_DEFAULT_CLOSE_ANIMATION, | ||||
|   dialog: [ | ||||
|     [ | ||||
|       // Dialog slide down | ||||
|       [{ transform: "translateY(0)" }, { transform: "translateY(50px)" }], | ||||
|       { duration: 150, easing: "cubic-bezier(.3,0,0,1)" }, | ||||
|     ], | ||||
|   ], | ||||
|   container: [ | ||||
|     [ | ||||
|       // Container fade out | ||||
|       [{ opacity: "1" }, { opacity: "0" }], | ||||
|       { delay: 100, duration: 50, easing: "linear", pseudoElement: "::before" }, | ||||
|     ], | ||||
|   ], | ||||
| }; | ||||
|  | ||||
| export const getMobileOpenFromBottomAnimation = () => { | ||||
|   const matches = window.matchMedia( | ||||
|     "all and (max-width: 450px), all and (max-height: 500px)" | ||||
|   ).matches; | ||||
|   return matches ? OPEN_FROM_BOTTOM_ANIMATION : DIALOG_DEFAULT_OPEN_ANIMATION; | ||||
| }; | ||||
|  | ||||
| export const getMobileCloseToBottomAnimation = () => { | ||||
|   const matches = window.matchMedia( | ||||
|     "all and (max-width: 450px), all and (max-height: 500px)" | ||||
|   ).matches; | ||||
|   return matches ? CLOSE_TO_BOTTOM_ANIMATION : DIALOG_DEFAULT_CLOSE_ANIMATION; | ||||
| }; | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-md-dialog": HaMdDialog; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										21
									
								
								src/components/ha-md-divider.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| import { MdDivider } from "@material/web/divider/divider"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
|  | ||||
| @customElement("ha-md-divider") | ||||
| export class HaMdDivider extends MdDivider { | ||||
|   static override styles = [ | ||||
|     ...super.styles, | ||||
|     css` | ||||
|       :host { | ||||
|         --md-divider-color: var(--divider-color); | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-md-divider": HaMdDivider; | ||||
|   } | ||||
| } | ||||
| @@ -2,8 +2,8 @@ import { MdListItem } from "@material/web/list/list-item"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
| 
 | ||||
| @customElement("ha-list-item-new") | ||||
| export class HaListItemNew extends MdListItem { | ||||
| @customElement("ha-md-list-item") | ||||
| export class HaMdListItem extends MdListItem { | ||||
|   static override styles = [ | ||||
|     ...super.styles, | ||||
|     css` | ||||
| @@ -21,6 +21,6 @@ export class HaListItemNew extends MdListItem { | ||||
| 
 | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-list-item-new": HaListItemNew; | ||||
|     "ha-md-list-item": HaMdListItem; | ||||
|   } | ||||
| } | ||||
| @@ -2,8 +2,8 @@ import { MdList } from "@material/web/list/list"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
| 
 | ||||
| @customElement("ha-list-new") | ||||
| export class HaListNew extends MdList { | ||||
| @customElement("ha-md-list") | ||||
| export class HaMdList extends MdList { | ||||
|   static override styles = [ | ||||
|     ...super.styles, | ||||
|     css` | ||||
| @@ -16,6 +16,6 @@ export class HaListNew extends MdList { | ||||
| 
 | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-list-new": HaListNew; | ||||
|     "ha-md-list": HaMdList; | ||||
|   } | ||||
| } | ||||
| @@ -2,8 +2,8 @@ import { MdMenuItem } from "@material/web/menu/menu-item"; | ||||
| import { css } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| 
 | ||||
| @customElement("ha-menu-item") | ||||
| export class HaMenuItem extends MdMenuItem { | ||||
| @customElement("ha-md-menu-item") | ||||
| export class HaMdMenuItem extends MdMenuItem { | ||||
|   @property({ attribute: false }) clickAction?: (item?: HTMLElement) => void; | ||||
| 
 | ||||
|   static override styles = [ | ||||
| @@ -41,6 +41,6 @@ export class HaMenuItem extends MdMenuItem { | ||||
| 
 | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-menu-item": HaMenuItem; | ||||
|     "ha-md-menu-item": HaMdMenuItem; | ||||
|   } | ||||
| } | ||||
| @@ -6,7 +6,7 @@ import { | ||||
| } from "@material/web/menu/internal/controllers/shared"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
| import type { HaMenuItem } from "./ha-menu-item"; | ||||
| import type { HaMdMenuItem } from "./ha-md-menu-item"; | ||||
|  | ||||
| @customElement("ha-menu") | ||||
| export class HaMenu extends MdMenu { | ||||
| @@ -22,7 +22,7 @@ export class HaMenu extends MdMenu { | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
|     (ev.detail.initiator as HaMenuItem).clickAction?.(ev.detail.initiator); | ||||
|     (ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator); | ||||
|   } | ||||
|  | ||||
|   static override styles = [ | ||||
|   | ||||
| @@ -24,9 +24,11 @@ export class HaOutlinedField extends MdOutlinedField { | ||||
|       } | ||||
|       .with-start .start { | ||||
|         margin-inline-end: var(--ha-outlined-field-start-margin, 4px); | ||||
|         margin-inline-start: initial; | ||||
|       } | ||||
|       .with-end .end { | ||||
|         margin-inline-start: var(--ha-outlined-field-end-margin, 4px); | ||||
|         margin-inline-end: initial; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
|   | ||||
							
								
								
									
										192
									
								
								src/components/ha-password-field.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,192 @@ | ||||
| import { TextAreaCharCounter } from "@material/mwc-textfield/mwc-textfield-base"; | ||||
| import { mdiEye, mdiEyeOff } from "@mdi/js"; | ||||
| import { LitElement, css, html } from "lit"; | ||||
| import { | ||||
|   customElement, | ||||
|   eventOptions, | ||||
|   property, | ||||
|   query, | ||||
|   state, | ||||
| } from "lit/decorators"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import "./ha-icon-button"; | ||||
| import "./ha-textfield"; | ||||
| import type { HaTextField } from "./ha-textfield"; | ||||
|  | ||||
| @customElement("ha-password-field") | ||||
| export class HaPasswordField extends LitElement { | ||||
|   @property({ attribute: false }) public hass?: HomeAssistant; | ||||
|  | ||||
|   @property({ type: Boolean }) public invalid?: boolean; | ||||
|  | ||||
|   @property({ attribute: "error-message" }) public errorMessage?: string; | ||||
|  | ||||
|   @property({ type: Boolean }) public icon = false; | ||||
|  | ||||
|   @property({ type: Boolean }) public iconTrailing = false; | ||||
|  | ||||
|   @property() public autocomplete?: string; | ||||
|  | ||||
|   @property() public autocorrect?: string; | ||||
|  | ||||
|   @property({ attribute: "input-spellcheck" }) | ||||
|   public inputSpellcheck?: string; | ||||
|  | ||||
|   @property({ type: String }) value = ""; | ||||
|  | ||||
|   @property({ type: String }) placeholder = ""; | ||||
|  | ||||
|   @property({ type: String }) label = ""; | ||||
|  | ||||
|   @property({ type: Boolean, reflect: true }) disabled = false; | ||||
|  | ||||
|   @property({ type: Boolean }) required = false; | ||||
|  | ||||
|   @property({ type: Number }) minLength = -1; | ||||
|  | ||||
|   @property({ type: Number }) maxLength = -1; | ||||
|  | ||||
|   @property({ type: Boolean, reflect: true }) outlined = false; | ||||
|  | ||||
|   @property({ type: String }) helper = ""; | ||||
|  | ||||
|   @property({ type: Boolean }) validateOnInitialRender = false; | ||||
|  | ||||
|   @property({ type: String }) validationMessage = ""; | ||||
|  | ||||
|   @property({ type: Boolean }) autoValidate = false; | ||||
|  | ||||
|   @property({ type: String }) pattern = ""; | ||||
|  | ||||
|   @property({ type: Number }) size: number | null = null; | ||||
|  | ||||
|   @property({ type: Boolean }) helperPersistent = false; | ||||
|  | ||||
|   @property({ type: Boolean }) charCounter: boolean | TextAreaCharCounter = | ||||
|     false; | ||||
|  | ||||
|   @property({ type: Boolean }) endAligned = false; | ||||
|  | ||||
|   @property({ type: String }) prefix = ""; | ||||
|  | ||||
|   @property({ type: String }) suffix = ""; | ||||
|  | ||||
|   @property({ type: String }) name = ""; | ||||
|  | ||||
|   @property({ type: String, attribute: "input-mode" }) | ||||
|   inputMode!: string; | ||||
|  | ||||
|   @property({ type: Boolean }) readOnly = false; | ||||
|  | ||||
|   @property({ type: String }) autocapitalize = ""; | ||||
|  | ||||
|   @state() private _unmaskedPassword = false; | ||||
|  | ||||
|   @query("ha-textfield") private _textField!: HaTextField; | ||||
|  | ||||
|   protected render() { | ||||
|     return html`<ha-textfield | ||||
|         .invalid=${this.invalid} | ||||
|         .errorMessage=${this.errorMessage} | ||||
|         .icon=${this.icon} | ||||
|         .iconTrailing=${this.iconTrailing} | ||||
|         .autocomplete=${this.autocomplete} | ||||
|         .autocorrect=${this.autocorrect} | ||||
|         .inputSpellcheck=${this.inputSpellcheck} | ||||
|         .value=${this.value} | ||||
|         .placeholder=${this.placeholder} | ||||
|         .label=${this.label} | ||||
|         .disabled=${this.disabled} | ||||
|         .required=${this.required} | ||||
|         .minLength=${this.minLength} | ||||
|         .maxLength=${this.maxLength} | ||||
|         .outlined=${this.outlined} | ||||
|         .helper=${this.helper} | ||||
|         .validateOnInitialRender=${this.validateOnInitialRender} | ||||
|         .validationMessage=${this.validationMessage} | ||||
|         .autoValidate=${this.autoValidate} | ||||
|         .pattern=${this.pattern} | ||||
|         .size=${this.size} | ||||
|         .helperPersistent=${this.helperPersistent} | ||||
|         .charCounter=${this.charCounter} | ||||
|         .endAligned=${this.endAligned} | ||||
|         .prefix=${this.prefix} | ||||
|         .name=${this.name} | ||||
|         .inputMode=${this.inputMode} | ||||
|         .readOnly=${this.readOnly} | ||||
|         .autocapitalize=${this.autocapitalize} | ||||
|         .type=${this._unmaskedPassword ? "text" : "password"} | ||||
|         .suffix=${html`<div style="width: 24px"></div>`} | ||||
|         @input=${this._handleInputChange} | ||||
|         @change=${this._reDispatchEvent} | ||||
|       ></ha-textfield> | ||||
|       <ha-icon-button | ||||
|         toggles | ||||
|         .label=${this.hass?.localize( | ||||
|           this._unmaskedPassword | ||||
|             ? "ui.components.selectors.text.hide_password" | ||||
|             : "ui.components.selectors.text.show_password" | ||||
|         ) || (this._unmaskedPassword ? "Hide password" : "Show password")} | ||||
|         @click=${this._toggleUnmaskedPassword} | ||||
|         .path=${this._unmaskedPassword ? mdiEyeOff : mdiEye} | ||||
|       ></ha-icon-button>`; | ||||
|   } | ||||
|  | ||||
|   public checkValidity(): boolean { | ||||
|     return this._textField.checkValidity(); | ||||
|   } | ||||
|  | ||||
|   public reportValidity(): boolean { | ||||
|     return this._textField.reportValidity(); | ||||
|   } | ||||
|  | ||||
|   public setCustomValidity(message: string): void { | ||||
|     return this._textField.setCustomValidity(message); | ||||
|   } | ||||
|  | ||||
|   public layout(): Promise<void> { | ||||
|     return this._textField.layout(); | ||||
|   } | ||||
|  | ||||
|   private _toggleUnmaskedPassword(): void { | ||||
|     this._unmaskedPassword = !this._unmaskedPassword; | ||||
|   } | ||||
|  | ||||
|   @eventOptions({ passive: true }) | ||||
|   private _handleInputChange(ev) { | ||||
|     this.value = ev.target.value; | ||||
|   } | ||||
|  | ||||
|   @eventOptions({ passive: true }) | ||||
|   private _reDispatchEvent(oldEvent: Event) { | ||||
|     const newEvent = new Event(oldEvent.type, oldEvent); | ||||
|     this.dispatchEvent(newEvent); | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     :host { | ||||
|       display: block; | ||||
|       position: relative; | ||||
|     } | ||||
|     ha-textfield { | ||||
|       width: 100%; | ||||
|     } | ||||
|     ha-icon-button { | ||||
|       position: absolute; | ||||
|       top: 8px; | ||||
|       right: 8px; | ||||
|       inset-inline-start: initial; | ||||
|       inset-inline-end: 8px; | ||||
|       --mdc-icon-button-size: 40px; | ||||
|       --mdc-icon-size: 20px; | ||||
|       color: var(--secondary-text-color); | ||||
|       direction: var(--direction); | ||||
|     } | ||||
|   `; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-password-field": HaPasswordField; | ||||
|   } | ||||
| } | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { PropertyValues, ReactiveElement } from "lit"; | ||||
| import { parseISO } from "date-fns"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { relativeTime } from "../common/datetime/relative_time"; | ||||
| import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter"; | ||||
| @@ -58,7 +59,12 @@ class HaRelativeTime extends ReactiveElement { | ||||
|     if (!this.datetime) { | ||||
|       this.innerHTML = this.hass.localize("ui.components.relative_time.never"); | ||||
|     } else { | ||||
|       const relTime = relativeTime(new Date(this.datetime), this.hass.locale); | ||||
|       const date = | ||||
|         typeof this.datetime === "string" | ||||
|           ? parseISO(this.datetime) | ||||
|           : this.datetime; | ||||
|  | ||||
|       const relTime = relativeTime(date, this.hass.locale); | ||||
|       this.innerHTML = this.capitalize | ||||
|         ? capitalizeFirstLetter(relTime) | ||||
|         : relTime; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { Action } from "../../data/script"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { Action, migrateAutomationAction } from "../../data/script"; | ||||
| import { ActionSelector } from "../../data/selector"; | ||||
| import "../../panels/config/automation/action/ha-automation-action"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| @@ -17,12 +18,19 @@ export class HaActionSelector extends LitElement { | ||||
|  | ||||
|   @property({ type: Boolean, reflect: true }) public disabled = false; | ||||
|  | ||||
|   private _actions = memoizeOne((action: Action | undefined) => { | ||||
|     if (!action) { | ||||
|       return []; | ||||
|     } | ||||
|     return migrateAutomationAction(action); | ||||
|   }); | ||||
|  | ||||
|   protected render() { | ||||
|     return html` | ||||
|       ${this.label ? html`<label>${this.label}</label>` : nothing} | ||||
|       <ha-automation-action | ||||
|         .disabled=${this.disabled} | ||||
|         .actions=${this.value || []} | ||||
|         .actions=${this._actions(this.value)} | ||||
|         .hass=${this.hass} | ||||
|         .path=${this.selector.action?.path} | ||||
|       ></ha-automation-action> | ||||
|   | ||||