mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-27 12:39:54 +00:00 
			
		
		
		
	Compare commits
	
		
			289 Commits
		
	
	
		
			20240424.1
			...
			dashboard_
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | ebe5207b6e | ||
|   | bd1ede4145 | ||
|   | 321a085c0e | ||
|   | 6a3041988a | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 23fcdf876c | ||
|   | 00f325e961 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d00b3cfc61 | ||
|   | 4cc9e74ea8 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a56b9a96ce | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d4b5f4bc14 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | cf1523ee73 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f5d571ca84 | ||
|   | 362e92f313 | ||
|   | 5ddf72b973 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 6e78c28f51 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 772f0bb669 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 846c2a848f | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 8495757005 | ||
|   | 9960d38b91 | ||
|   | d3222f8bb0 | ||
|   | 2e5cce5409 | ||
|   | f78946447f | ||
|   | eb0579ddc5 | ||
|   | 686424fc70 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 039e9b40bd | ||
|   | 8272bef890 | ||
|   | 62528b2413 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | fa24f529e0 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 43a54f6cda | ||
|   | 9c153bbd58 | ||
|   | 27afe9ecb7 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 72f989e2bd | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | a6ef46565f | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | a35ac09688 | ||
|   | 27024135ea | ||
|   | 8759ed740a | ||
|   | bb3e8ae33d | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | b5b60c9bf0 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 3b6a2cf7d8 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 7e10e14102 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | a580abab4a | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 11523c08c4 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 7a8988528b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 2a6380f083 | ||
|   | 29881c8bb4 | ||
|   | 56254ddf03 | ||
|   | 007ba70641 | ||
|   | 3e1227b064 | ||
|   | 067e179f26 | ||
|   | 9a3f7df25e | ||
|   | c7b4e8f37c | ||
|   | bfa8b886ab | ||
|   | 433c00b73a | ||
|   | a497f42f73 | ||
|   | 165723cb5b | ||
|   | 42b5fa696a | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 59062d96a8 | ||
|   | d36bbfe07d | ||
|   | 0d489213a4 | ||
|   | c54acc9369 | ||
|   | 562bc084f0 | ||
|   | 6fce2f35a5 | ||
|   | f4e24bed2e | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 09969c0e2d | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 4b0181774b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 272db5e9e8 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 9ae3a824d9 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 9db55c9391 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 59697127c0 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 565600e945 | ||
|   | 721eebf367 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f5ae842167 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 3575734ed0 | ||
|   | 4e8de1f64d | ||
|   | cd73b8ac29 | ||
|   | 74eca6b1f5 | ||
|   | 9ef0bd6e46 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 53eb7f771f | ||
|   | cd62f064cb | ||
|   | db82b856e0 | ||
|   | ab340e13e9 | ||
|   | 22c54b3fea | ||
|   | 2dd7e598d5 | ||
|   | d1ce06e368 | ||
|   | 9717304b68 | ||
|   | c646f3c39a | ||
|   | 0297ec5a7b | ||
|   | e48286c2a0 | ||
|   | f2b2da9877 | ||
|   | 250f87cfd8 | ||
|   | 9f6afb162a | ||
|   | 0add65feff | ||
|   | c08d9a9166 | ||
|   | d9a9038cec | ||
|   | 6bee3ef45c | ||
|   | 3a855f95ad | ||
|   | e7f3393ec6 | ||
|   | d7cb4cb537 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | c55720c933 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f78e757485 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | fa03c58a93 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 49f1ad633f | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d449e10120 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 474c8c243e | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | e9e53e9451 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | cfa84f30be | ||
|   | 7416ae7dfd | ||
|   | 6f1fa139e7 | ||
|   | b38a348957 | ||
|   | f97971faf6 | ||
|   | c5ae9e8497 | ||
|   | c00287c401 | ||
|   | c0e048023d | ||
|   | 431f4937c1 | ||
|   | 0a55220837 | ||
|   | 13f01492b4 | ||
|   | ce5bcf61f9 | ||
|   | d31a777135 | ||
|   | 5cc08cfe0b | ||
|   | 3eea7dc6cd | ||
|   | a629f01300 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f1345af526 | ||
|   | 064c51f487 | ||
|   | d88670034a | ||
|   | 5fab1969a8 | ||
|   | b3e14d449e | ||
|   | 97206ee8fe | ||
|   | 7748315fc3 | ||
|   | e059ca146b | ||
|   | 56cabeb497 | ||
|   | 7a7bd87f50 | ||
|   | ff9c794659 | ||
|   | 2921161336 | ||
|   | 91e5fcacd5 | ||
|   | febbf34de6 | ||
|   | 5a2977f4d4 | ||
|   | 7a7a355765 | ||
|   | ccebae84a7 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f96f38ee82 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d4056e6a32 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 389a7a6ed9 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 05aecaaaf1 | ||
|   | 085131d546 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | c2737d5cec | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 29ae46d775 | ||
|   | dfee3ba089 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 1dbb70b964 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 1eecc5c0e2 | ||
|   | 81c0bcff0b | ||
|   | 6ccbeb8a75 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f59ed0a72b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | a9f00ded0f | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 74730ba201 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 95b2f7d821 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 661b14da54 | ||
|   | 41e34c0d61 | ||
|   | 5d044a06eb | ||
|   | f617426808 | ||
|   | 3c3d54243c | ||
|   | afc624bf4b | ||
|   | 0991628843 | ||
|   | 34b9c7b9d1 | ||
|   | d52641b495 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 80c7fd2bf2 | ||
|   | e0062cf190 | ||
|   | 7d2cee650d | ||
|   | 66560b1f1c | ||
|   | a500b582e3 | ||
|   | 19f94ff8cc | ||
|   | 0b6994d402 | ||
|   | 9fe8f507ec | ||
|   | 2113cf5280 | ||
|   | ae9e1b724f | ||
|   | 9b28c7cf69 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d9bac06806 | ||
|   | b1e37cb1e1 | ||
|   | a2a89502d8 | ||
|   | 4cc5d2d04b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 79abcca3b3 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 043f383a35 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | d4dd767941 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 174f1991b1 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | e15495a626 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | a8a9a797cb | ||
|   | 914dbc1e28 | ||
|   | 111816f08a | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 1b4534890c | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | ed6542469d | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 3774a3d6ba | ||
|   | bfa293ae3a | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 9264adb799 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 829ea4a9e4 | ||
|   | be26f8bc24 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | c864b34a9a | ||
|   | 099ea61a94 | ||
|   | 3ebe6027be | ||
|   | f5f2a5ad5b | ||
|   | d046700d06 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 2ad84b2832 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f9ccb9fc72 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 6d3940db1e | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 20d174431d | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 1900710e06 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | ed86a48e1c | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d2bdb52926 | ||
|   | 9c57c9f151 | ||
|   | 9e9cb15a42 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 6421a9443d | ||
|   | f2b43ddad8 | ||
|   | e55b59d9b7 | ||
|   | 4a77359a06 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 505d7b6ddb | ||
|   | 79cdc43699 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 8ff9823cd7 | ||
|   | 3488c60818 | ||
|   | b2af21ba5c | ||
|   | 12a61a0021 | ||
|   | 649917cdde | ||
|   | 3ed27ee853 | ||
|   | c1d3a76917 | ||
|   | 571ed6b9e9 | ||
|   | a347315fa7 | ||
|   | 57d1405115 | ||
|   | e5ff6bd2f5 | ||
|   | 43a422cdca | ||
|   | 11f2bef05c | ||
|   | ff9f331287 | ||
|   | cdf64ccdaa | ||
|   | 8b220acca2 | ||
|   | 8fdb7fa1d5 | ||
|   | 008c842431 | ||
|   | bc41de0d9c | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 7310c9cf6d | ||
|   | 84b436c08e | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 1925a47bdc | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 438a426458 | ||
|   | f923deb71d | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | e79bc71ab7 | ||
|   | 11b0990d2b | ||
|   | 870cb0c65f | ||
|   | deda2009f8 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | b2797ab8da | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 644dcb0381 | ||
|   | c65f4f7a6e | ||
|   | e2266aa671 | ||
|   | 68a79490dc | ||
|   | 6febe8552e | ||
|   | f611f23f6f | ||
|   | ef4f11fdf8 | ||
|   | 627e06663b | ||
|   | ab01633069 | ||
|   | 17dcc90638 | ||
|   | d0df029ff1 | ||
|   | 86a7e69812 | ||
|   | af9417f2a6 | ||
|   | 7120ad99b9 | ||
|   | 334c245b65 | ||
|   | bcb72d83b8 | ||
|   | c99e0e846b | ||
|   | ec3f63e8a3 | ||
|   | 1bc33a30ec | ||
|   | 8cca233b7c | ||
|   | a78608bfb4 | ||
|   | e7c1ac94af | ||
|   | 1a797b3415 | ||
|   | 2b27a4da2b | ||
|   | 1df92fa863 | ||
|   | cdde85315a | ||
|   | dc67f9faf4 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 3ad1be50a2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8aadfe7d28 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | cff54b73a4 | ||
|   | b54cfeb0c0 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | cefe612b11 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 4bc874b497 | ||
|   | f3abaa8e02 | ||
|   | 21a563fe98 | ||
|   | 1acbcccd62 | ||
|   | 35d6c638ab | ||
|   | 68f8239708 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 0db64cca0b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | accfda5f4b | ||
|   | c97c20f57d | ||
|   | 2725d0191d | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 852cc62398 | ||
|   | 654e3ce437 | ||
|   | 20a3a00aec | ||
|   | 22b927d666 | ||
|   | 709d6be2e3 | 
| @@ -115,6 +115,7 @@ | ||||
|       } | ||||
|     ], | ||||
|     "unused-imports/no-unused-imports": "error", | ||||
|     "lit/attribute-names": "warn", | ||||
|     "lit/attribute-value-entities": "off", | ||||
|     "lit/no-template-map": "off", | ||||
|     "lit/no-native-attributes": "warn", | ||||
| @@ -125,6 +126,5 @@ | ||||
|     "lit-a11y/anchor-is-valid": "warn", | ||||
|     "lit-a11y/role-has-required-aria-attrs": "warn" | ||||
|   }, | ||||
|   "plugins": ["disable", "unused-imports"], | ||||
|   "processor": "disable/disable" | ||||
|   "plugins": ["unused-imports"] | ||||
| } | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -21,7 +21,7 @@ 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.3 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         with: | ||||
|           ref: dev | ||||
|  | ||||
| @@ -57,7 +57,7 @@ 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.3 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         with: | ||||
|           ref: master | ||||
|  | ||||
|   | ||||
							
								
								
									
										12
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -24,7 +24,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.3 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|         with: | ||||
| @@ -58,7 +58,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.3 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|         with: | ||||
| @@ -76,7 +76,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.3 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|         with: | ||||
| @@ -89,7 +89,7 @@ jobs: | ||||
|         env: | ||||
|           IS_TEST: "true" | ||||
|       - name: Upload bundle stats | ||||
|         uses: actions/upload-artifact@v4.3.2 | ||||
|         uses: actions/upload-artifact@v4.3.3 | ||||
|         with: | ||||
|           name: frontend-bundle-stats | ||||
|           path: build/stats/*.json | ||||
| @@ -100,7 +100,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.3 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|         with: | ||||
| @@ -113,7 +113,7 @@ jobs: | ||||
|         env: | ||||
|           IS_TEST: "true" | ||||
|       - name: Upload bundle stats | ||||
|         uses: actions/upload-artifact@v4.3.2 | ||||
|         uses: actions/upload-artifact@v4.3.3 | ||||
|         with: | ||||
|           name: supervisor-bundle-stats | ||||
|           path: build/stats/*.json | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -23,7 +23,7 @@ jobs: | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v4.1.3 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         with: | ||||
|           # We must fetch at least the immediate parents so that if this is | ||||
|           # a pull request then we can checkout the head. | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -22,7 +22,7 @@ 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.3 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         with: | ||||
|           ref: dev | ||||
|  | ||||
| @@ -58,7 +58,7 @@ 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.3 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|         with: | ||||
|           ref: master | ||||
|  | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -16,7 +16,7 @@ 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.3 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -21,7 +21,7 @@ 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.3 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.2 | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -20,7 +20,7 @@ jobs: | ||||
|       contents: write | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v4.1.3 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|  | ||||
|       - name: Set up Python ${{ env.PYTHON_VERSION }} | ||||
|         uses: actions/setup-python@v5 | ||||
| @@ -57,14 +57,14 @@ jobs: | ||||
|         run: tar -czvf translations.tar.gz translations | ||||
|  | ||||
|       - name: Upload build artifacts | ||||
|         uses: actions/upload-artifact@v4.3.2 | ||||
|         uses: actions/upload-artifact@v4.3.3 | ||||
|         with: | ||||
|           name: wheels | ||||
|           path: dist/home_assistant_frontend*.whl | ||||
|           if-no-files-found: error | ||||
|  | ||||
|       - name: Upload translations | ||||
|         uses: actions/upload-artifact@v4.3.2 | ||||
|         uses: actions/upload-artifact@v4.3.3 | ||||
|         with: | ||||
|           name: translations | ||||
|           path: translations.tar.gz | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/relative-ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/relative-ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Send bundle stats and build information to RelativeCI | ||||
|         uses: relative-ci/agent-action@v2.1.10 | ||||
|         uses: relative-ci/agent-action@v2.1.11 | ||||
|         with: | ||||
|           key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }} | ||||
|           token: ${{ github.token }} | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										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.3 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|  | ||||
|       - name: Verify version | ||||
|         uses: home-assistant/actions/helpers/verify-version@master | ||||
| @@ -55,7 +55,7 @@ jobs: | ||||
|           script/release | ||||
|  | ||||
|       - name: Upload release assets | ||||
|         uses: softprops/action-gh-release@v2.0.4 | ||||
|         uses: softprops/action-gh-release@v2.0.6 | ||||
|         with: | ||||
|           files: | | ||||
|             dist/*.whl | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/translations.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										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.3 | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|  | ||||
|       - name: Upload Translations | ||||
|         run: | | ||||
|   | ||||
							
								
								
									
										893
									
								
								.yarn/releases/yarn-4.1.1.cjs
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										893
									
								
								.yarn/releases/yarn-4.1.1.cjs
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										894
									
								
								.yarn/releases/yarn-4.3.1.cjs
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										894
									
								
								.yarn/releases/yarn-4.3.1.cjs
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -6,4 +6,4 @@ enableGlobalCache: false | ||||
|  | ||||
| nodeLinker: node-modules | ||||
|  | ||||
| yarnPath: .yarn/releases/yarn-4.1.1.cjs | ||||
| yarnPath: .yarn/releases/yarn-4.3.1.cjs | ||||
|   | ||||
| @@ -1,7 +1,56 @@ | ||||
| import defineProvider from "@babel/helper-define-polyfill-provider"; | ||||
| import { join } from "node:path"; | ||||
| import paths from "../paths.cjs"; | ||||
|  | ||||
| const POLYFILL_DIR = join(paths.polymer_dir, "src/resources/polyfills"); | ||||
|  | ||||
| // List of polyfill keys with supported browser targets for the functionality | ||||
| const PolyfillSupport = { | ||||
|   // Note states and shadowRoot properties should be supported. | ||||
|   "element-internals": { | ||||
|     android: 90, | ||||
|     chrome: 90, | ||||
|     edge: 90, | ||||
|     firefox: 126, | ||||
|     ios: 17.4, | ||||
|     opera: 76, | ||||
|     opera_mobile: 64, | ||||
|     safari: 17.4, | ||||
|     samsung: 15.0, | ||||
|   }, | ||||
|   "element-append": { | ||||
|     android: 54, | ||||
|     chrome: 54, | ||||
|     edge: 17, | ||||
|     firefox: 49, | ||||
|     ios: 10.0, | ||||
|     opera: 41, | ||||
|     opera_mobile: 41, | ||||
|     safari: 10.0, | ||||
|     samsung: 6.0, | ||||
|   }, | ||||
|   "element-getattributenames": { | ||||
|     android: 61, | ||||
|     chrome: 61, | ||||
|     edge: 18, | ||||
|     firefox: 45, | ||||
|     ios: 10.3, | ||||
|     opera: 48, | ||||
|     opera_mobile: 45, | ||||
|     safari: 10.1, | ||||
|     samsung: 8.0, | ||||
|   }, | ||||
|   "element-toggleattribute": { | ||||
|     android: 69, | ||||
|     chrome: 69, | ||||
|     edge: 18, | ||||
|     firefox: 63, | ||||
|     ios: 12.0, | ||||
|     opera: 56, | ||||
|     opera_mobile: 48, | ||||
|     safari: 12.0, | ||||
|     samsung: 10.0, | ||||
|   }, | ||||
|   fetch: { | ||||
|     android: 42, | ||||
|     chrome: 42, | ||||
| @@ -13,6 +62,31 @@ const PolyfillSupport = { | ||||
|     safari: 10.1, | ||||
|     samsung: 4.0, | ||||
|   }, | ||||
|   "intl-getcanonicallocales": { | ||||
|     android: 54, | ||||
|     chrome: 54, | ||||
|     edge: 16, | ||||
|     firefox: 48, | ||||
|     ios: 10.3, | ||||
|     opera: 41, | ||||
|     opera_mobile: 41, | ||||
|     safari: 10.1, | ||||
|     samsung: 6.0, | ||||
|   }, | ||||
|   "intl-locale": { | ||||
|     android: 74, | ||||
|     chrome: 74, | ||||
|     edge: 79, | ||||
|     firefox: 75, | ||||
|     ios: 14.0, | ||||
|     opera: 62, | ||||
|     opera_mobile: 53, | ||||
|     safari: 14.0, | ||||
|     samsung: 11.0, | ||||
|   }, | ||||
|   "intl-other": { | ||||
|     // Not specified (i.e. always try polyfill) since compatibility depends on supported locales | ||||
|   }, | ||||
|   proxy: { | ||||
|     android: 49, | ||||
|     chrome: 49, | ||||
| @@ -24,17 +98,67 @@ const PolyfillSupport = { | ||||
|     safari: 10.0, | ||||
|     samsung: 5.0, | ||||
|   }, | ||||
|   "resize-observer": { | ||||
|     android: 64, | ||||
|     chrome: 64, | ||||
|     edge: 79, | ||||
|     firefox: 69, | ||||
|     ios: 13.4, | ||||
|     opera: 51, | ||||
|     opera_mobile: 47, | ||||
|     safari: 13.1, | ||||
|     samsung: 9.0, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| // Map of global variables and/or instance and static properties to the | ||||
| // corresponding polyfill key and actual module to import | ||||
| const polyfillMap = { | ||||
|   global: { | ||||
|     Proxy: { key: "proxy", module: "proxy-polyfill" }, | ||||
|     fetch: { key: "fetch", module: "unfetch/polyfill" }, | ||||
|     Proxy: { key: "proxy", module: "proxy-polyfill" }, | ||||
|     ResizeObserver: { | ||||
|       key: "resize-observer", | ||||
|       module: join(POLYFILL_DIR, "resize-observer.ts"), | ||||
|     }, | ||||
|   }, | ||||
|   instance: { | ||||
|     attachInternals: { | ||||
|       key: "element-internals", | ||||
|       module: "element-internals-polyfill", | ||||
|     }, | ||||
|     ...Object.fromEntries( | ||||
|       ["append", "getAttributeNames", "toggleAttribute"].map((prop) => { | ||||
|         const key = `element-${prop.toLowerCase()}`; | ||||
|         return [prop, { key, module: join(POLYFILL_DIR, `${key}.ts`) }]; | ||||
|       }) | ||||
|     ), | ||||
|   }, | ||||
|   static: { | ||||
|     Intl: { | ||||
|       getCanonicalLocales: { | ||||
|         key: "intl-getcanonicallocales", | ||||
|         module: join(POLYFILL_DIR, "intl-polyfill.ts"), | ||||
|       }, | ||||
|       Locale: { | ||||
|         key: "intl-locale", | ||||
|         module: join(POLYFILL_DIR, "intl-polyfill.ts"), | ||||
|       }, | ||||
|       ...Object.fromEntries( | ||||
|         [ | ||||
|           "DateTimeFormat", | ||||
|           "DisplayNames", | ||||
|           "ListFormat", | ||||
|           "NumberFormat", | ||||
|           "PluralRules", | ||||
|           "RelativeTimeFormat", | ||||
|         ].map((obj) => [ | ||||
|           obj, | ||||
|           { key: "intl-other", module: join(POLYFILL_DIR, "intl-polyfill.ts") }, | ||||
|         ]) | ||||
|       ), | ||||
|     }, | ||||
|   }, | ||||
|   instance: {}, | ||||
|   static: {}, | ||||
| }; | ||||
|  | ||||
| // Create plugin using the same factory as for CoreJS | ||||
| @@ -42,14 +166,16 @@ export default defineProvider( | ||||
|   ({ createMetaResolver, debug, shouldInjectPolyfill }) => { | ||||
|     const resolvePolyfill = createMetaResolver(polyfillMap); | ||||
|     return { | ||||
|       name: "HA Custom", | ||||
|       name: "custom-polyfill", | ||||
|       polyfills: PolyfillSupport, | ||||
|       usageGlobal(meta, utils) { | ||||
|         const polyfill = resolvePolyfill(meta); | ||||
|         if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) { | ||||
|           debug(polyfill.desc.key); | ||||
|           utils.injectGlobalImport(polyfill.desc.module); | ||||
|           return true; | ||||
|         } | ||||
|         return false; | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
|   | ||||
| @@ -3,6 +3,8 @@ const env = require("./env.cjs"); | ||||
| const paths = require("./paths.cjs"); | ||||
| const { dependencies } = require("../package.json"); | ||||
|  | ||||
| const BABEL_PLUGINS = path.join(__dirname, "babel-plugins"); | ||||
|  | ||||
| // GitHub base URL to use for production source maps | ||||
| // Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version | ||||
| module.exports.sourceMapURL = () => { | ||||
| @@ -90,8 +92,8 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({ | ||||
|     [ | ||||
|       "@babel/preset-env", | ||||
|       { | ||||
|         useBuiltIns: latestBuild ? false : "usage", | ||||
|         corejs: latestBuild ? false : dependencies["core-js"], | ||||
|         useBuiltIns: "usage", | ||||
|         corejs: dependencies["core-js"], | ||||
|         bugfixes: true, | ||||
|         shippedProposals: true, | ||||
|       }, | ||||
| @@ -100,22 +102,12 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({ | ||||
|   ], | ||||
|   plugins: [ | ||||
|     [ | ||||
|       path.resolve( | ||||
|         paths.polymer_dir, | ||||
|         "build-scripts/babel-plugins/inline-constants-plugin.cjs" | ||||
|       ), | ||||
|       path.join(BABEL_PLUGINS, "inline-constants-plugin.cjs"), | ||||
|       { | ||||
|         modules: ["@mdi/js"], | ||||
|         ignoreModuleNotFound: true, | ||||
|       }, | ||||
|     ], | ||||
|     [ | ||||
|       path.resolve( | ||||
|         paths.polymer_dir, | ||||
|         "build-scripts/babel-plugins/custom-polyfill-plugin.js" | ||||
|       ), | ||||
|       { method: "usage-global" }, | ||||
|     ], | ||||
|     // Minify template literals for production | ||||
|     isProdBuild && [ | ||||
|       "template-html-minifier", | ||||
| @@ -153,6 +145,27 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({ | ||||
|   ], | ||||
|   sourceMaps: !isTestBuild, | ||||
|   overrides: [ | ||||
|     { | ||||
|       // Add plugin to inject various polyfills, excluding the polyfills | ||||
|       // themselves to prevent self-injection. | ||||
|       plugins: [ | ||||
|         [ | ||||
|           path.join(BABEL_PLUGINS, "custom-polyfill-plugin.js"), | ||||
|           { method: "usage-global" }, | ||||
|         ], | ||||
|       ], | ||||
|       exclude: [ | ||||
|         path.join(paths.polymer_dir, "src/resources/polyfills"), | ||||
|         ...[ | ||||
|           "@formatjs/(?:ecma402-abstract|intl-\\w+)", | ||||
|           "@lit-labs/virtualizer/polyfills", | ||||
|           "@webcomponents/scoped-custom-element-registry", | ||||
|           "element-internals-polyfill", | ||||
|           "proxy-polyfill", | ||||
|           "unfetch", | ||||
|         ].map((p) => new RegExp(`/node_modules/${p}/`)), | ||||
|       ], | ||||
|     }, | ||||
|     { | ||||
|       // Use unambiguous for dependencies so that require() is correctly injected into CommonJS files | ||||
|       // Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills | ||||
|   | ||||
| @@ -1,12 +1,14 @@ | ||||
| /* eslint-disable max-classes-per-file */ | ||||
|  | ||||
| import { deleteAsync } from "del"; | ||||
| import { glob } from "glob"; | ||||
| import gulp from "gulp"; | ||||
| import merge from "gulp-merge-json"; | ||||
| import rename from "gulp-rename"; | ||||
| import merge from "lodash.merge"; | ||||
| import { createHash } from "node:crypto"; | ||||
| import { mkdir, readFile } from "node:fs/promises"; | ||||
| import { basename, join } from "node:path"; | ||||
| import { Transform } from "node:stream"; | ||||
| import { PassThrough, Transform } from "node:stream"; | ||||
| import { finished } from "node:stream/promises"; | ||||
| import env from "../env.cjs"; | ||||
| import paths from "../paths.cjs"; | ||||
| @@ -17,6 +19,7 @@ const inBackendDir = "translations/backend"; | ||||
| const workDir = "build/translations"; | ||||
| const outDir = join(workDir, "output"); | ||||
| const EN_SRC = join(paths.translations_src, "en.json"); | ||||
| const TEST_LOCALE = "en-x-test"; | ||||
|  | ||||
| let mergeBackend = false; | ||||
|  | ||||
| @@ -54,6 +57,39 @@ class CustomJSON extends Transform { | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Transform stream to merge Vinyl JSON files (buffer mode only). | ||||
| class MergeJSON extends Transform { | ||||
|   _objects = []; | ||||
|  | ||||
|   constructor(stem, startObj = {}, reviver = null) { | ||||
|     super({ objectMode: true, allowHalfOpen: false }); | ||||
|     this._stem = stem; | ||||
|     this._startObj = structuredClone(startObj); | ||||
|     this._reviver = reviver; | ||||
|   } | ||||
|  | ||||
|   async _transform(file, _, callback) { | ||||
|     try { | ||||
|       this._objects.push(JSON.parse(file.contents.toString(), this._reviver)); | ||||
|       if (!this._outFile) this._outFile = file.clone({ contents: false }); | ||||
|       callback(null); | ||||
|     } catch (err) { | ||||
|       callback(err); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async _flush(callback) { | ||||
|     try { | ||||
|       const mergedObj = merge(this._startObj, ...this._objects); | ||||
|       this._outFile.contents = Buffer.from(JSON.stringify(mergedObj)); | ||||
|       this._outFile.stem = this._stem; | ||||
|       callback(null, this._outFile); | ||||
|     } catch (err) { | ||||
|       callback(err); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Utility to flatten object keys to single level using separator | ||||
| const flatten = (data, prefix = "", sep = ".") => { | ||||
|   const output = {}; | ||||
| @@ -115,7 +151,7 @@ const createTestTranslation = () => | ||||
|     : gulp | ||||
|         .src(EN_SRC) | ||||
|         .pipe(new CustomJSON(null, testReviver)) | ||||
|         .pipe(rename("test.json")) | ||||
|         .pipe(rename(`${TEST_LOCALE}.json`)) | ||||
|         .pipe(gulp.dest(workDir)); | ||||
|  | ||||
| /** | ||||
| @@ -131,12 +167,7 @@ const createMasterTranslation = () => | ||||
|   gulp | ||||
|     .src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])]) | ||||
|     .pipe(new CustomJSON(lokaliseTransform)) | ||||
|     .pipe( | ||||
|       merge({ | ||||
|         fileName: "en.json", | ||||
|         jsonSpace: undefined, | ||||
|       }) | ||||
|     ) | ||||
|     .pipe(new MergeJSON("en")) | ||||
|     .pipe(gulp.dest(workDir)); | ||||
|  | ||||
| const FRAGMENTS = ["base"]; | ||||
| @@ -162,7 +193,7 @@ const createTranslations = async () => { | ||||
|   // each locale, then fragmentizes and flattens the data for final output. | ||||
|   const translationFiles = await glob([ | ||||
|     `${inFrontendDir}/!(en).json`, | ||||
|     ...(env.isProdBuild() ? [] : [`${workDir}/test.json`]), | ||||
|     ...(env.isProdBuild() ? [] : [`${workDir}/${TEST_LOCALE}.json`]), | ||||
|   ]); | ||||
|   const hashStream = new Transform({ | ||||
|     objectMode: true, | ||||
| @@ -213,7 +244,10 @@ const createTranslations = async () => { | ||||
|   // TODO: This is a naive interpretation of BCP47 that should be improved. | ||||
|   //       Will be OK for now as long as we don't have anything more complicated | ||||
|   // than a base translation + region. | ||||
|   gulp.src(`${workDir}/en.json`).pipe(hashStream, { end: false }); | ||||
|   gulp | ||||
|     .src(`${workDir}/en.json`) | ||||
|     .pipe(new PassThrough({ objectMode: true })) | ||||
|     .pipe(hashStream, { end: false }); | ||||
|   const mergesFinished = []; | ||||
|   for (const translationFile of translationFiles) { | ||||
|     const locale = basename(translationFile, ".json"); | ||||
| @@ -221,8 +255,8 @@ const createTranslations = async () => { | ||||
|     const mergeFiles = []; | ||||
|     for (let i = 1; i <= subtags.length; i++) { | ||||
|       const lang = subtags.slice(0, i).join("-"); | ||||
|       if (lang === "test") { | ||||
|         mergeFiles.push(`${workDir}/test.json`); | ||||
|       if (lang === TEST_LOCALE) { | ||||
|         mergeFiles.push(`${workDir}/${TEST_LOCALE}.json`); | ||||
|       } else if (lang !== "en") { | ||||
|         mergeFiles.push(`${inFrontendDir}/${lang}.json`); | ||||
|         if (mergeBackend) { | ||||
| @@ -230,14 +264,9 @@ const createTranslations = async () => { | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     const mergeStream = gulp.src(mergeFiles, { allowEmpty: true }).pipe( | ||||
|       merge({ | ||||
|         fileName: `${locale}.json`, | ||||
|         startObj: enMaster, | ||||
|         jsonReviver: emptyReviver, | ||||
|         jsonSpace: undefined, | ||||
|       }) | ||||
|     ); | ||||
|     const mergeStream = gulp | ||||
|       .src(mergeFiles, { allowEmpty: true }) | ||||
|       .pipe(new MergeJSON(locale, enMaster, emptyReviver)); | ||||
|     mergesFinished.push(finished(mergeStream)); | ||||
|     mergeStream.pipe(hashStream, { end: false }); | ||||
|   } | ||||
| @@ -256,7 +285,7 @@ const writeTranslationMetaData = () => | ||||
|       new CustomJSON((meta) => { | ||||
|         // Add the test translation in development. | ||||
|         if (!env.isProdBuild()) { | ||||
|           meta.test = { nativeName: "Test" }; | ||||
|           meta[TEST_LOCALE] = { nativeName: "Translation Test" }; | ||||
|         } | ||||
|         // Filter out locales without a native name, and add the hashes. | ||||
|         for (const locale of Object.keys(meta)) { | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import "@material/mwc-button/mwc-button"; | ||||
| import { mdiCast, mdiCastConnected } from "@mdi/js"; | ||||
| import "@polymer/paper-item/paper-icon-item"; | ||||
| import "@polymer/paper-listbox/paper-listbox"; | ||||
| import { 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"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| @@ -28,6 +27,7 @@ import { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view"; | ||||
| import "../../../../src/layouts/hass-loading-screen"; | ||||
| import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config"; | ||||
| import "./hc-layout"; | ||||
| import "../../../../src/components/ha-list-item"; | ||||
|  | ||||
| @customElement("hc-cast") | ||||
| class HcCast extends LitElement { | ||||
| @@ -83,34 +83,37 @@ class HcCast extends LitElement { | ||||
|               ` | ||||
|             : html` | ||||
|                 <div class="section-header">PICK A VIEW</div> | ||||
|                 <paper-listbox | ||||
|                   attr-for-selected="data-path" | ||||
|                   .selected=${this.castManager.status.lovelacePath || ""} | ||||
|                 > | ||||
|                 <mwc-list @action=${this._handlePickView} activatable> | ||||
|                   ${( | ||||
|                     this.lovelaceViews ?? [ | ||||
|                       generateDefaultViewConfig({}, {}, {}, {}, () => ""), | ||||
|                     ] | ||||
|                   ).map( | ||||
|                     (view, idx) => html` | ||||
|                       <paper-icon-item | ||||
|                         @click=${this._handlePickView} | ||||
|                         data-path=${view.path || idx} | ||||
|                     (view, idx) => | ||||
|                       html`<ha-list-item | ||||
|                         graphic="avatar" | ||||
|                         .activated=${this.castManager.status?.lovelacePath === | ||||
|                         (view.path ?? idx)} | ||||
|                         .selected=${this.castManager.status?.lovelacePath === | ||||
|                         (view.path ?? idx)} | ||||
|                       > | ||||
|                         ${view.title || view.path || "Unnamed view"} | ||||
|                         ${view.icon | ||||
|                           ? html` | ||||
|                               <ha-icon | ||||
|                                 .icon=${view.icon} | ||||
|                                 slot="item-icon" | ||||
|                                 slot="graphic" | ||||
|                               ></ha-icon> | ||||
|                             ` | ||||
|                           : ""} | ||||
|                         ${view.title || view.path} | ||||
|                       </paper-icon-item> | ||||
|                     ` | ||||
|                   )} | ||||
|                 </paper-listbox> | ||||
|                           : html`<ha-svg-icon | ||||
|                               slot="item-icon" | ||||
|                               .path=${mdiViewDashboard} | ||||
|                             ></ha-svg-icon>`}</ha-list-item | ||||
|                       > ` | ||||
|                   )}</mwc-list | ||||
|                 > | ||||
|               `} | ||||
|  | ||||
|         <div class="card-actions"> | ||||
|           ${this.castManager.status | ||||
|             ? html` | ||||
| @@ -182,8 +185,8 @@ class HcCast extends LitElement { | ||||
|     this.castManager.requestSession(); | ||||
|   } | ||||
|  | ||||
|   private async _handlePickView(ev: Event) { | ||||
|     const path = (ev.currentTarget as any).getAttribute("data-path"); | ||||
|   private async _handlePickView(ev: CustomEvent<ActionDetail>) { | ||||
|     const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index; | ||||
|     await ensureConnectedCastSession(this.castManager!, this.auth!); | ||||
|     castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path); | ||||
|   } | ||||
| @@ -246,25 +249,14 @@ class HcCast extends LitElement { | ||||
|         height: 18px; | ||||
|       } | ||||
|  | ||||
|       paper-listbox { | ||||
|         padding-top: 0; | ||||
|       } | ||||
|  | ||||
|       paper-listbox ha-icon { | ||||
|       ha-list-item ha-icon, | ||||
|       ha-list-item ha-svg-icon { | ||||
|         padding: 12px; | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|  | ||||
|       paper-icon-item { | ||||
|         cursor: pointer; | ||||
|       } | ||||
|  | ||||
|       paper-icon-item[disabled] { | ||||
|         cursor: initial; | ||||
|       } | ||||
|  | ||||
|       :host([hide-icons]) paper-icon-item { | ||||
|         --paper-item-icon-width: 0px; | ||||
|       :host([hide-icons]) ha-icon { | ||||
|         display: none; | ||||
|       } | ||||
|  | ||||
|       .spacer { | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, query } 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 { HomeAssistant } from "../../../../src/types"; | ||||
| @@ -61,7 +62,12 @@ class HcLovelace extends LitElement { | ||||
|       const index = this._viewIndex; | ||||
|  | ||||
|       if (index !== undefined) { | ||||
|         const dashboardTitle = this.lovelaceConfig.title || this.urlPath; | ||||
|         const title = getPanelTitleFromUrlPath( | ||||
|           this.hass, | ||||
|           this.urlPath || "lovelace" | ||||
|         ); | ||||
|  | ||||
|         const dashboardTitle = title || this.urlPath; | ||||
|  | ||||
|         const viewTitle = | ||||
|           this.lovelaceConfig.views[index].title || | ||||
| @@ -80,10 +86,17 @@ class HcLovelace extends LitElement { | ||||
|           this.lovelaceConfig.views[index].background || | ||||
|           this.lovelaceConfig.background; | ||||
|  | ||||
|         if (configBackground) { | ||||
|         const backgroundStyle = | ||||
|           typeof configBackground === "string" | ||||
|             ? configBackground | ||||
|             : configBackground?.image | ||||
|               ? `center / cover no-repeat url('${configBackground.image}')` | ||||
|               : undefined; | ||||
|  | ||||
|         if (backgroundStyle) { | ||||
|           this._huiView!.style.setProperty( | ||||
|             "--lovelace-background", | ||||
|             configBackground | ||||
|             backgroundStyle | ||||
|           ); | ||||
|         } else { | ||||
|           this._huiView!.style.removeProperty("--lovelace-background"); | ||||
|   | ||||
| @@ -35,6 +35,7 @@ import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/lo | ||||
| import { HassElement } from "../../../../src/state/hass-element"; | ||||
| import { castContext } from "../cast_context"; | ||||
| import "./hc-launch-screen"; | ||||
| import { getPanelTitleFromUrlPath } from "../../../../src/data/panel"; | ||||
|  | ||||
| const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = { | ||||
|   strategy: { | ||||
| @@ -359,7 +360,11 @@ export class HcMain extends HassElement { | ||||
|   } | ||||
|  | ||||
|   private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) { | ||||
|     castContext.setApplicationState(lovelaceConfig.title || ""); | ||||
|     const title = getPanelTitleFromUrlPath( | ||||
|       this.hass!, | ||||
|       this._urlPath || "lovelace" | ||||
|     ); | ||||
|     castContext.setApplicationState(title || ""); | ||||
|     this._lovelaceConfig = lovelaceConfig; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| import "@material/mwc-button"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { until } from "lit/directives/until"; | ||||
| import { fireEvent } from "../../../src/common/dom/fire_event"; | ||||
| import "../../../src/components/ha-card"; | ||||
| import "../../../src/components/ha-button"; | ||||
| import "../../../src/components/ha-circular-progress"; | ||||
| import { LovelaceCardConfig } from "../../../src/data/lovelace/config/card"; | ||||
| import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; | ||||
| @@ -11,7 +12,6 @@ import { | ||||
|   demoConfigs, | ||||
|   selectedDemoConfig, | ||||
|   selectedDemoConfigIndex, | ||||
|   setDemoConfig, | ||||
| } from "../configs/demo-configs"; | ||||
|  | ||||
| @customElement("ha-demo-card") | ||||
| @@ -64,9 +64,9 @@ export class HADemoCard extends LitElement implements LovelaceCard { | ||||
|                 )} | ||||
|           </div> | ||||
|  | ||||
|           <mwc-button @click=${this._nextConfig} .disabled=${this._switching}> | ||||
|           <ha-button @click=${this._nextConfig} .disabled=${this._switching}> | ||||
|             ${this.hass.localize("ui.panel.page-demo.cards.demo.next_demo")} | ||||
|           </mwc-button> | ||||
|           </ha-button> | ||||
|         </div> | ||||
|         <div class="content"> | ||||
|           <p class="small-hidden"> | ||||
| @@ -87,9 +87,9 @@ export class HADemoCard extends LitElement implements LovelaceCard { | ||||
|         </div> | ||||
|         <div class="actions small-hidden"> | ||||
|           <a href="https://www.home-assistant.io" target="_blank"> | ||||
|             <mwc-button> | ||||
|             <ha-button> | ||||
|               ${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")} | ||||
|             </mwc-button> | ||||
|             </ha-button> | ||||
|           </a> | ||||
|         </div> | ||||
|       </ha-card> | ||||
| @@ -113,13 +113,7 @@ export class HADemoCard extends LitElement implements LovelaceCard { | ||||
|  | ||||
|   private async _updateConfig(index: number) { | ||||
|     this._switching = true; | ||||
|     try { | ||||
|       await setDemoConfig(this.hass, this.lovelace!, index); | ||||
|     } catch (err: any) { | ||||
|       alert("Failed to switch config :-("); | ||||
|     } finally { | ||||
|       this._switching = false; | ||||
|     } | ||||
|     fireEvent(this, "set-demo-config" as any, { index }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
| @@ -149,7 +143,7 @@ export class HADemoCard extends LitElement implements LovelaceCard { | ||||
|           height: 60px; | ||||
|         } | ||||
|  | ||||
|         .picker mwc-button { | ||||
|         .picker ha-button { | ||||
|           margin-right: 8px; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import { | ||||
| import { HomeAssistantAppEl } from "../../src/layouts/home-assistant"; | ||||
| import { HomeAssistant } from "../../src/types"; | ||||
| import { selectedDemoConfig } from "./configs/demo-configs"; | ||||
| import { mockAreaRegistry } from "./stubs/area_registry"; | ||||
| import { mockAuth } from "./stubs/auth"; | ||||
| import { mockConfigEntries } from "./stubs/config_entries"; | ||||
| import { mockEnergy } from "./stubs/energy"; | ||||
| @@ -23,10 +24,10 @@ import { mockLovelace } from "./stubs/lovelace"; | ||||
| import { mockMediaPlayer } from "./stubs/media_player"; | ||||
| import { mockPersistentNotification } from "./stubs/persistent_notification"; | ||||
| import { mockRecorder } from "./stubs/recorder"; | ||||
| import { mockTodo } from "./stubs/todo"; | ||||
| import { mockSensor } from "./stubs/sensor"; | ||||
| import { mockSystemLog } from "./stubs/system_log"; | ||||
| import { mockTemplate } from "./stubs/template"; | ||||
| import { mockTodo } from "./stubs/todo"; | ||||
| import { mockTranslations } from "./stubs/translations"; | ||||
|  | ||||
| @customElement("ha-demo") | ||||
| @@ -62,6 +63,7 @@ export class HaDemo extends HomeAssistantAppEl { | ||||
|     mockEnergy(hass); | ||||
|     mockPersistentNotification(hass); | ||||
|     mockConfigEntries(hass); | ||||
|     mockAreaRegistry(hass); | ||||
|     mockEntityRegistry(hass, [ | ||||
|       { | ||||
|         config_entry_id: "co2signal", | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| import type { LocalizeFunc } from "../../../src/common/translations/localize"; | ||||
| import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; | ||||
| import { selectedDemoConfig } from "../configs/demo-configs"; | ||||
| import { | ||||
|   selectedDemoConfig, | ||||
|   selectedDemoConfigIndex, | ||||
|   setDemoConfig, | ||||
| } from "../configs/demo-configs"; | ||||
| import "../custom-cards/cast-demo-row"; | ||||
| import "../custom-cards/ha-demo-card"; | ||||
| import type { HADemoCard } from "../custom-cards/ha-demo-card"; | ||||
|  | ||||
| export const mockLovelace = ( | ||||
|   hass: MockHomeAssistant, | ||||
| @@ -19,17 +22,22 @@ export const mockLovelace = ( | ||||
|   hass.mockWS("lovelace/resources", () => Promise.resolve([])); | ||||
| }; | ||||
|  | ||||
| customElements.whenDefined("hui-view").then(() => { | ||||
| customElements.whenDefined("hui-root").then(() => { | ||||
|   // eslint-disable-next-line | ||||
|   const HUIView = customElements.get("hui-view"); | ||||
|   // Patch HUI-VIEW to make the lovelace object available to the demo card | ||||
|   const oldCreateCard = HUIView!.prototype.createCardElement; | ||||
|   const HUIRoot = customElements.get("hui-root")!; | ||||
|  | ||||
|   HUIView!.prototype.createCardElement = function (config) { | ||||
|     const el = oldCreateCard.call(this, config); | ||||
|     if (el.tagName === "HA-DEMO-CARD") { | ||||
|       (el as HADemoCard).lovelace = this.lovelace; | ||||
|     } | ||||
|     return el; | ||||
|   const oldFirstUpdated = HUIRoot.prototype.firstUpdated; | ||||
|  | ||||
|   HUIRoot.prototype.firstUpdated = function (changedProperties) { | ||||
|     oldFirstUpdated.call(this, changedProperties); | ||||
|     this.addEventListener("set-demo-config", async (ev) => { | ||||
|       const index = (ev as CustomEvent).detail.index; | ||||
|       try { | ||||
|         await setDemoConfig(this.hass, this.lovelace!, index); | ||||
|       } catch (err: any) { | ||||
|         setDemoConfig(this.hass, this.lovelace!, selectedDemoConfigIndex); | ||||
|         alert("Failed to switch config :-("); | ||||
|       } | ||||
|     }); | ||||
|   }; | ||||
| }); | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| import { load } from "js-yaml"; | ||||
| import { html, css, LitElement, PropertyValues } from "lit"; | ||||
| import { LitElement, PropertyValueMap, css, html, nothing } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import "../../../src/panels/lovelace/cards/hui-card"; | ||||
| import type { HuiCard } from "../../../src/panels/lovelace/cards/hui-card"; | ||||
| import { HomeAssistant } from "../../../src/types"; | ||||
|  | ||||
| export interface DemoCardConfig { | ||||
| @@ -19,7 +21,12 @@ class DemoCard extends LitElement { | ||||
|  | ||||
|   @state() private _size?: number; | ||||
|  | ||||
|   @query("#card") private _card!: HTMLElement; | ||||
|   @query("hui-card", false) private _card?: HuiCard; | ||||
|  | ||||
|   private _config = memoizeOne((config: string) => { | ||||
|     const c = (load(config) as any)[0]; | ||||
|     return c; | ||||
|   }); | ||||
|  | ||||
|   render() { | ||||
|     return html` | ||||
| @@ -30,63 +37,32 @@ class DemoCard extends LitElement { | ||||
|           : ""} | ||||
|       </h2> | ||||
|       <div class="root"> | ||||
|         <div id="card"></div> | ||||
|         ${this.showConfig ? html`<pre>${this.config.config.trim()}</pre>` : ""} | ||||
|         <hui-card | ||||
|           .config=${this._config(this.config.config)} | ||||
|           .hass=${this.hass} | ||||
|           @card-updated=${this._cardUpdated} | ||||
|         ></hui-card> | ||||
|         ${this.showConfig | ||||
|           ? html`<pre>${this.config.config.trim()}</pre>` | ||||
|           : nothing} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   updated(changedProps: PropertyValues) { | ||||
|     super.updated(changedProps); | ||||
|  | ||||
|     if (changedProps.has("config")) { | ||||
|       const card = this._card; | ||||
|       while (card.lastChild) { | ||||
|         card.removeChild(card.lastChild); | ||||
|       } | ||||
|  | ||||
|       const el = this._createCardElement((load(this.config.config) as any)[0]); | ||||
|       card.appendChild(el); | ||||
|       this._getSize(el); | ||||
|     } | ||||
|  | ||||
|     if (changedProps.has("hass")) { | ||||
|       const card = this._card.lastChild; | ||||
|       if (card) { | ||||
|         (card as any).hass = this.hass; | ||||
|       } | ||||
|     } | ||||
|   private async _cardUpdated(ev) { | ||||
|     ev.stopPropagation(); | ||||
|     this._updateSize(); | ||||
|   } | ||||
|  | ||||
|   async _getSize(el) { | ||||
|     await customElements.whenDefined(el.localName); | ||||
|  | ||||
|     if (!("getCardSize" in el)) { | ||||
|       this._size = undefined; | ||||
|       return; | ||||
|     } | ||||
|     this._size = await el.getCardSize(); | ||||
|   private async _updateSize() { | ||||
|     this._size = await this._card?.getCardSize(); | ||||
|   } | ||||
|  | ||||
|   _createCardElement(cardConfig) { | ||||
|     const element = createCardElement(cardConfig); | ||||
|     if (this.hass) { | ||||
|       element.hass = this.hass; | ||||
|     } | ||||
|     element.addEventListener( | ||||
|       "ll-rebuild", | ||||
|       (ev) => { | ||||
|         ev.stopPropagation(); | ||||
|         this._rebuildCard(element, cardConfig); | ||||
|       }, | ||||
|       { once: true } | ||||
|     ); | ||||
|     return element; | ||||
|   } | ||||
|  | ||||
|   _rebuildCard(cardElToReplace, config) { | ||||
|     const newCardEl = this._createCardElement(config); | ||||
|     cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace); | ||||
|   protected update( | ||||
|     _changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown> | ||||
|   ): void { | ||||
|     super.update(_changedProperties); | ||||
|     this._updateSize(); | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
| @@ -101,7 +77,7 @@ class DemoCard extends LitElement { | ||||
|       font-size: 0.5em; | ||||
|       color: var(--primary-text-color); | ||||
|     } | ||||
|     #card { | ||||
|     hui-card { | ||||
|       max-width: 400px; | ||||
|       width: 100vw; | ||||
|     } | ||||
|   | ||||
| @@ -64,6 +64,12 @@ const ACTIONS = [ | ||||
|       entity_id: "input_boolean.toggle_4", | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     sequence: [ | ||||
|       { scene: "scene.kitchen_morning" }, | ||||
|       { service: "light.turn_off", target: { entity_id: "light.kitchen" } }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     parallel: [ | ||||
|       { scene: "scene.kitchen_morning" }, | ||||
|   | ||||
| @@ -20,6 +20,7 @@ import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation | ||||
| import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template"; | ||||
| import { Action } from "../../../../src/data/script"; | ||||
| import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition"; | ||||
| import { HaSequenceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-sequence"; | ||||
| import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel"; | ||||
| import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if"; | ||||
| import { HaStopAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-stop"; | ||||
| @@ -39,6 +40,7 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [ | ||||
|   { name: "If-Then", actions: [HaIfAction.defaultConfig] }, | ||||
|   { name: "Choose", actions: [HaChooseAction.defaultConfig] }, | ||||
|   { name: "Variables", actions: [{ variables: { hello: "1" } }] }, | ||||
|   { name: "Sequence", actions: [HaSequenceAction.defaultConfig] }, | ||||
|   { name: "Parallel", actions: [HaParallelAction.defaultConfig] }, | ||||
|   { name: "Stop", actions: [HaStopAction.defaultConfig] }, | ||||
| ]; | ||||
|   | ||||
| @@ -56,48 +56,46 @@ export class DemoDateTimeDateTimeNumeric extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|                 <div class="center"> | ||||
|                   ${formatDateTimeNumeric( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.language, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatDateTimeNumeric( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.am_pm, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatDateTimeNumeric( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.twenty_four, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|           ([key, value]) => html` | ||||
|             <div class="container"> | ||||
|               <div>${value.nativeName}</div> | ||||
|               <div class="center"> | ||||
|                 ${formatDateTimeNumeric( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.language, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             ` | ||||
|           )} | ||||
|               <div class="center"> | ||||
|                 ${formatDateTimeNumeric( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.am_pm, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|               <div class="center"> | ||||
|                 ${formatDateTimeNumeric( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.twenty_four, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             </div> | ||||
|           ` | ||||
|         )} | ||||
|       </mwc-list> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -56,48 +56,46 @@ export class DemoDateTimeDateTimeSeconds extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|                 <div class="center"> | ||||
|                   ${formatDateTimeWithSeconds( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.language, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatDateTimeWithSeconds( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.am_pm, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatDateTimeWithSeconds( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.twenty_four, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|           ([key, value]) => html` | ||||
|             <div class="container"> | ||||
|               <div>${value.nativeName}</div> | ||||
|               <div class="center"> | ||||
|                 ${formatDateTimeWithSeconds( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.language, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             ` | ||||
|           )} | ||||
|               <div class="center"> | ||||
|                 ${formatDateTimeWithSeconds( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.am_pm, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|               <div class="center"> | ||||
|                 ${formatDateTimeWithSeconds( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.twenty_four, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             </div> | ||||
|           ` | ||||
|         )} | ||||
|       </mwc-list> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -56,48 +56,46 @@ export class DemoDateTimeDateTimeShortYear extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|                 <div class="center"> | ||||
|                   ${formatShortDateTimeWithYear( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.language, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatShortDateTimeWithYear( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.am_pm, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatShortDateTimeWithYear( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.twenty_four, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|           ([key, value]) => html` | ||||
|             <div class="container"> | ||||
|               <div>${value.nativeName}</div> | ||||
|               <div class="center"> | ||||
|                 ${formatShortDateTimeWithYear( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.language, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             ` | ||||
|           )} | ||||
|               <div class="center"> | ||||
|                 ${formatShortDateTimeWithYear( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.am_pm, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|               <div class="center"> | ||||
|                 ${formatShortDateTimeWithYear( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.twenty_four, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             </div> | ||||
|           ` | ||||
|         )} | ||||
|       </mwc-list> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -56,48 +56,46 @@ export class DemoDateTimeDateTimeShort extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|                 <div class="center"> | ||||
|                   ${formatShortDateTime( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.language, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatShortDateTime( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.am_pm, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatShortDateTime( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.twenty_four, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|           ([key, value]) => html` | ||||
|             <div class="container"> | ||||
|               <div>${value.nativeName}</div> | ||||
|               <div class="center"> | ||||
|                 ${formatShortDateTime( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.language, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             ` | ||||
|           )} | ||||
|               <div class="center"> | ||||
|                 ${formatShortDateTime( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.am_pm, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|               <div class="center"> | ||||
|                 ${formatShortDateTime( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.twenty_four, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             </div> | ||||
|           ` | ||||
|         )} | ||||
|       </mwc-list> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -56,48 +56,46 @@ export class DemoDateTimeDateTime extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|                 <div class="center"> | ||||
|                   ${formatDateTime( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.language, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatDateTime( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.am_pm, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatDateTime( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.twenty_four, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|           ([key, value]) => html` | ||||
|             <div class="container"> | ||||
|               <div>${value.nativeName}</div> | ||||
|               <div class="center"> | ||||
|                 ${formatDateTime( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.language, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             ` | ||||
|           )} | ||||
|               <div class="center"> | ||||
|                 ${formatDateTime( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.am_pm, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|               <div class="center"> | ||||
|                 ${formatDateTime( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.twenty_four, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             </div> | ||||
|           ` | ||||
|         )} | ||||
|       </mwc-list> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -35,59 +35,57 @@ export class DemoDateTimeDate extends LitElement { | ||||
|           <div class="center">Month-Day-Year</div> | ||||
|           <div class="center">Year-Month-Day</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|                 <div class="center"> | ||||
|                   ${formatDateNumeric( | ||||
|                     date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       date_format: DateFormat.language, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatDateNumeric( | ||||
|                     date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       date_format: DateFormat.DMY, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatDateNumeric( | ||||
|                     date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       date_format: DateFormat.MDY, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatDateNumeric( | ||||
|                     date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       date_format: DateFormat.YMD, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|           ([key, value]) => html` | ||||
|             <div class="container"> | ||||
|               <div>${value.nativeName}</div> | ||||
|               <div class="center"> | ||||
|                 ${formatDateNumeric( | ||||
|                   date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     date_format: DateFormat.language, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             ` | ||||
|           )} | ||||
|               <div class="center"> | ||||
|                 ${formatDateNumeric( | ||||
|                   date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     date_format: DateFormat.DMY, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|               <div class="center"> | ||||
|                 ${formatDateNumeric( | ||||
|                   date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     date_format: DateFormat.MDY, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|               <div class="center"> | ||||
|                 ${formatDateNumeric( | ||||
|                   date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     date_format: DateFormat.YMD, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             </div> | ||||
|           ` | ||||
|         )} | ||||
|       </mwc-list> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -56,48 +56,46 @@ export class DemoDateTimeTimeSeconds extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|                 <div class="center"> | ||||
|                   ${formatTimeWithSeconds( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.language, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatTimeWithSeconds( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.am_pm, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatTimeWithSeconds( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.twenty_four, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|           ([key, value]) => html` | ||||
|             <div class="container"> | ||||
|               <div>${value.nativeName}</div> | ||||
|               <div class="center"> | ||||
|                 ${formatTimeWithSeconds( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.language, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             ` | ||||
|           )} | ||||
|               <div class="center"> | ||||
|                 ${formatTimeWithSeconds( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.am_pm, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|               <div class="center"> | ||||
|                 ${formatTimeWithSeconds( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.twenty_four, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             </div> | ||||
|           ` | ||||
|         )} | ||||
|       </mwc-list> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -56,48 +56,46 @@ export class DemoDateTimeTimeWeekday extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|                 <div class="center"> | ||||
|                   ${formatTimeWeekday( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.language, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatTimeWeekday( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.am_pm, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatTimeWeekday( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.twenty_four, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|           ([key, value]) => html` | ||||
|             <div class="container"> | ||||
|               <div>${value.nativeName}</div> | ||||
|               <div class="center"> | ||||
|                 ${formatTimeWeekday( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.language, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             ` | ||||
|           )} | ||||
|               <div class="center"> | ||||
|                 ${formatTimeWeekday( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.am_pm, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|               <div class="center"> | ||||
|                 ${formatTimeWeekday( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.twenty_four, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             </div> | ||||
|           ` | ||||
|         )} | ||||
|       </mwc-list> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -56,48 +56,46 @@ export class DemoDateTimeTime extends LitElement { | ||||
|           <div class="center">12 Hours</div> | ||||
|           <div class="center">24 Hours</div> | ||||
|         </div> | ||||
|         ${Object.entries(translationMetadata.translations) | ||||
|           .filter(([key, _]) => key !== "test") | ||||
|           .map( | ||||
|             ([key, value]) => html` | ||||
|               <div class="container"> | ||||
|                 <div>${value.nativeName}</div> | ||||
|                 <div class="center"> | ||||
|                   ${formatTime( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.language, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatTime( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.am_pm, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <div class="center"> | ||||
|                   ${formatTime( | ||||
|                     this.date, | ||||
|                     { | ||||
|                       ...defaultLocale, | ||||
|                       language: key, | ||||
|                       time_format: TimeFormat.twenty_four, | ||||
|                     }, | ||||
|                     demoConfig | ||||
|                   )} | ||||
|                 </div> | ||||
|         ${Object.entries(translationMetadata.translations).map( | ||||
|           ([key, value]) => html` | ||||
|             <div class="container"> | ||||
|               <div>${value.nativeName}</div> | ||||
|               <div class="center"> | ||||
|                 ${formatTime( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.language, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             ` | ||||
|           )} | ||||
|               <div class="center"> | ||||
|                 ${formatTime( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.am_pm, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|               <div class="center"> | ||||
|                 ${formatTime( | ||||
|                   this.date, | ||||
|                   { | ||||
|                     ...defaultLocale, | ||||
|                     language: key, | ||||
|                     time_format: TimeFormat.twenty_four, | ||||
|                   }, | ||||
|                   demoConfig | ||||
|                 )} | ||||
|               </div> | ||||
|             </div> | ||||
|           ` | ||||
|         )} | ||||
|       </mwc-list> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -368,6 +368,7 @@ export class DemoEntityState extends LitElement { | ||||
|               hass.localize, | ||||
|               entry.stateObj, | ||||
|               hass.locale, | ||||
|               [], // numericDeviceClasses | ||||
|               hass.config, | ||||
|               hass.entities | ||||
|             )}`, | ||||
|   | ||||
| @@ -1,19 +1,19 @@ | ||||
| import { mdiStorePlus, mdiUpdate } from "@mdi/js"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { mdiRefresh, mdiStorePlus } from "@mdi/js"; | ||||
| import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { atLeastVersion } from "../../../src/common/config/version"; | ||||
| import { fireEvent } from "../../../src/common/dom/fire_event"; | ||||
| import "../../../src/components/ha-fab"; | ||||
| import { reloadHassioAddons } from "../../../src/data/hassio/addon"; | ||||
| import { extractApiErrorMessage } from "../../../src/data/hassio/common"; | ||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||
| import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; | ||||
| import "../../../src/layouts/hass-subpage"; | ||||
| import "../../../src/layouts/hass-tabs-subpage"; | ||||
| import { haStyle } from "../../../src/resources/styles"; | ||||
| import { HomeAssistant, Route } from "../../../src/types"; | ||||
| import { supervisorTabs } from "../hassio-tabs"; | ||||
| import "./hassio-addons"; | ||||
| import "../../../src/layouts/hass-subpage"; | ||||
| import { reloadHassioAddons } from "../../../src/data/hassio/addon"; | ||||
| import { extractApiErrorMessage } from "../../../src/data/hassio/common"; | ||||
| import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; | ||||
| import { fireEvent } from "../../../src/common/dom/fire_event"; | ||||
|  | ||||
| @customElement("hassio-dashboard") | ||||
| class HassioDashboard extends LitElement { | ||||
| @@ -43,7 +43,7 @@ class HassioDashboard extends LitElement { | ||||
|         <ha-icon-button | ||||
|           slot="toolbar-icon" | ||||
|           @click=${this._handleCheckUpdates} | ||||
|           .path=${mdiUpdate} | ||||
|           .path=${mdiRefresh} | ||||
|           .label=${this.supervisor.localize("store.check_updates")} | ||||
|         ></ha-icon-button> | ||||
|         <hassio-addons | ||||
|   | ||||
							
								
								
									
										124
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										124
									
								
								package.json
									
									
									
									
									
								
							| @@ -25,24 +25,24 @@ | ||||
|   "license": "Apache-2.0", | ||||
|   "type": "module", | ||||
|   "dependencies": { | ||||
|     "@babel/runtime": "7.24.4", | ||||
|     "@braintree/sanitize-url": "7.0.1", | ||||
|     "@codemirror/autocomplete": "6.16.0", | ||||
|     "@codemirror/commands": "6.5.0", | ||||
|     "@codemirror/language": "6.10.1", | ||||
|     "@babel/runtime": "7.24.7", | ||||
|     "@braintree/sanitize-url": "7.0.3", | ||||
|     "@codemirror/autocomplete": "6.16.3", | ||||
|     "@codemirror/commands": "6.6.0", | ||||
|     "@codemirror/language": "6.10.2", | ||||
|     "@codemirror/legacy-modes": "6.4.0", | ||||
|     "@codemirror/search": "6.5.6", | ||||
|     "@codemirror/state": "6.4.1", | ||||
|     "@codemirror/view": "6.26.3", | ||||
|     "@codemirror/view": "6.28.2", | ||||
|     "@egjs/hammerjs": "2.0.17", | ||||
|     "@formatjs/intl-datetimeformat": "6.12.3", | ||||
|     "@formatjs/intl-displaynames": "6.6.6", | ||||
|     "@formatjs/intl-datetimeformat": "6.12.5", | ||||
|     "@formatjs/intl-displaynames": "6.6.8", | ||||
|     "@formatjs/intl-getcanonicallocales": "2.3.0", | ||||
|     "@formatjs/intl-listformat": "7.5.5", | ||||
|     "@formatjs/intl-locale": "3.4.5", | ||||
|     "@formatjs/intl-numberformat": "8.10.1", | ||||
|     "@formatjs/intl-pluralrules": "5.2.12", | ||||
|     "@formatjs/intl-relativetimeformat": "11.2.12", | ||||
|     "@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", | ||||
|     "@fullcalendar/core": "6.1.11", | ||||
|     "@fullcalendar/daygrid": "6.1.11", | ||||
|     "@fullcalendar/interaction": "6.1.11", | ||||
| @@ -53,7 +53,7 @@ | ||||
|     "@lit-labs/context": "0.4.1", | ||||
|     "@lit-labs/motion": "1.0.7", | ||||
|     "@lit-labs/observers": "2.0.2", | ||||
|     "@lit-labs/virtualizer": "2.0.12", | ||||
|     "@lit-labs/virtualizer": "2.0.13", | ||||
|     "@lrnwebcomponents/simple-tooltip": "8.0.2", | ||||
|     "@material/chips": "=14.0.0-canary.53b3cad2f.0", | ||||
|     "@material/data-table": "=14.0.0-canary.53b3cad2f.0", | ||||
| @@ -70,7 +70,6 @@ | ||||
|     "@material/mwc-list": "0.27.0", | ||||
|     "@material/mwc-menu": "0.27.0", | ||||
|     "@material/mwc-radio": "0.27.0", | ||||
|     "@material/mwc-ripple": "0.27.0", | ||||
|     "@material/mwc-select": "0.27.0", | ||||
|     "@material/mwc-snackbar": "0.27.0", | ||||
|     "@material/mwc-switch": "0.27.0", | ||||
| @@ -81,7 +80,7 @@ | ||||
|     "@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": "1.4.1", | ||||
|     "@material/web": "1.5.0", | ||||
|     "@mdi/js": "7.4.47", | ||||
|     "@mdi/svg": "7.4.47", | ||||
|     "@polymer/paper-item": "3.0.1", | ||||
| @@ -89,8 +88,8 @@ | ||||
|     "@polymer/paper-tabs": "3.1.0", | ||||
|     "@polymer/polymer": "3.5.1", | ||||
|     "@thomasloven/round-slider": "0.6.0", | ||||
|     "@vaadin/combo-box": "24.3.11", | ||||
|     "@vaadin/vaadin-themable-mixin": "24.3.11", | ||||
|     "@vaadin/combo-box": "24.4.0", | ||||
|     "@vaadin/vaadin-themable-mixin": "24.4.0", | ||||
|     "@vibrant/color": "3.2.1-alpha.1", | ||||
|     "@vibrant/core": "3.2.1-alpha.1", | ||||
|     "@vibrant/quantizer-mmcq": "3.2.1-alpha.1", | ||||
| @@ -98,10 +97,10 @@ | ||||
|     "@webcomponents/scoped-custom-element-registry": "0.0.9", | ||||
|     "@webcomponents/webcomponentsjs": "2.8.0", | ||||
|     "app-datepicker": "5.1.1", | ||||
|     "chart.js": "4.4.2", | ||||
|     "chart.js": "4.4.3", | ||||
|     "color-name": "2.0.0", | ||||
|     "comlink": "4.4.1", | ||||
|     "core-js": "3.37.0", | ||||
|     "core-js": "3.37.1", | ||||
|     "cropperjs": "1.6.2", | ||||
|     "date-fns": "3.6.0", | ||||
|     "date-fns-tz": "3.1.3", | ||||
| @@ -111,9 +110,9 @@ | ||||
|     "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.3.0", | ||||
|     "home-assistant-js-websocket": "9.4.0", | ||||
|     "idb-keyval": "6.2.1", | ||||
|     "intl-messageformat": "10.5.11", | ||||
|     "intl-messageformat": "10.5.14", | ||||
|     "js-yaml": "4.1.0", | ||||
|     "leaflet": "1.9.4", | ||||
|     "leaflet-draw": "1.0.4", | ||||
| @@ -134,49 +133,50 @@ | ||||
|     "tinykeys": "2.1.0", | ||||
|     "tsparticles-engine": "2.12.0", | ||||
|     "tsparticles-preset-links": "2.12.0", | ||||
|     "ua-parser-js": "1.0.37", | ||||
|     "ua-parser-js": "1.0.38", | ||||
|     "unfetch": "5.0.0", | ||||
|     "vis-data": "7.1.9", | ||||
|     "vis-network": "9.1.9", | ||||
|     "vue": "2.7.16", | ||||
|     "vue2-daterange-picker": "0.6.8", | ||||
|     "weekstart": "2.0.0", | ||||
|     "workbox-cacheable-response": "7.0.0", | ||||
|     "workbox-core": "7.0.0", | ||||
|     "workbox-expiration": "7.0.0", | ||||
|     "workbox-precaching": "7.0.0", | ||||
|     "workbox-routing": "7.0.0", | ||||
|     "workbox-strategies": "7.0.0", | ||||
|     "workbox-cacheable-response": "7.1.0", | ||||
|     "workbox-core": "7.1.0", | ||||
|     "workbox-expiration": "7.1.0", | ||||
|     "workbox-precaching": "7.1.0", | ||||
|     "workbox-routing": "7.1.0", | ||||
|     "workbox-strategies": "7.1.0", | ||||
|     "xss": "1.0.15" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@babel/core": "7.24.4", | ||||
|     "@babel/helper-define-polyfill-provider": "0.6.1", | ||||
|     "@babel/plugin-proposal-decorators": "7.24.1", | ||||
|     "@babel/plugin-transform-runtime": "7.24.3", | ||||
|     "@babel/preset-env": "7.24.4", | ||||
|     "@babel/preset-typescript": "7.24.1", | ||||
|     "@bundle-stats/plugin-webpack-filter": "4.12.2", | ||||
|     "@babel/core": "7.24.7", | ||||
|     "@babel/helper-define-polyfill-provider": "0.6.2", | ||||
|     "@babel/plugin-proposal-decorators": "7.24.7", | ||||
|     "@babel/plugin-transform-runtime": "7.24.7", | ||||
|     "@babel/preset-env": "7.24.7", | ||||
|     "@babel/preset-typescript": "7.24.7", | ||||
|     "@bundle-stats/plugin-webpack-filter": "4.13.2", | ||||
|     "@koa/cors": "5.0.0", | ||||
|     "@lokalise/node-api": "12.4.0", | ||||
|     "@lokalise/node-api": "12.5.0", | ||||
|     "@octokit/auth-oauth-device": "7.1.1", | ||||
|     "@octokit/plugin-retry": "7.1.0", | ||||
|     "@octokit/rest": "20.1.0", | ||||
|     "@octokit/plugin-retry": "7.1.1", | ||||
|     "@octokit/rest": "21.0.0", | ||||
|     "@open-wc/dev-server-hmr": "0.1.4", | ||||
|     "@rollup/plugin-babel": "6.0.4", | ||||
|     "@rollup/plugin-commonjs": "25.0.7", | ||||
|     "@rollup/plugin-commonjs": "26.0.1", | ||||
|     "@rollup/plugin-json": "6.1.0", | ||||
|     "@rollup/plugin-node-resolve": "15.2.3", | ||||
|     "@rollup/plugin-replace": "5.0.5", | ||||
|     "@rollup/plugin-replace": "5.0.7", | ||||
|     "@types/babel__plugin-transform-runtime": "7.9.5", | ||||
|     "@types/chromecast-caf-receiver": "6.0.14", | ||||
|     "@types/chromecast-caf-sender": "1.0.9", | ||||
|     "@types/chromecast-caf-receiver": "6.0.15", | ||||
|     "@types/chromecast-caf-sender": "1.0.10", | ||||
|     "@types/color-name": "1.1.4", | ||||
|     "@types/glob": "8.1.0", | ||||
|     "@types/html-minifier-terser": "7.0.2", | ||||
|     "@types/js-yaml": "4.0.9", | ||||
|     "@types/leaflet": "1.9.11", | ||||
|     "@types/leaflet": "1.9.12", | ||||
|     "@types/leaflet-draw": "1.0.11", | ||||
|     "@types/lodash.merge": "4.6.9", | ||||
|     "@types/luxon": "3.4.2", | ||||
|     "@types/mocha": "10.0.6", | ||||
|     "@types/qrcode": "1.5.5", | ||||
| @@ -185,39 +185,38 @@ | ||||
|     "@types/tar": "6.1.13", | ||||
|     "@types/ua-parser-js": "0.7.39", | ||||
|     "@types/webspeechapi": "0.0.29", | ||||
|     "@typescript-eslint/eslint-plugin": "7.7.0", | ||||
|     "@typescript-eslint/parser": "7.7.0", | ||||
|     "@typescript-eslint/eslint-plugin": "7.13.1", | ||||
|     "@typescript-eslint/parser": "7.13.1", | ||||
|     "@web/dev-server": "0.1.38", | ||||
|     "@web/dev-server-rollup": "0.4.1", | ||||
|     "babel-loader": "9.1.3", | ||||
|     "babel-plugin-template-html-minifier": "4.1.0", | ||||
|     "chai": "5.1.0", | ||||
|     "chai": "5.1.1", | ||||
|     "del": "7.1.0", | ||||
|     "eslint": "8.57.0", | ||||
|     "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-disable": "2.0.3", | ||||
|     "eslint-plugin-import": "2.29.1", | ||||
|     "eslint-plugin-lit": "1.11.0", | ||||
|     "eslint-plugin-lit": "1.14.0", | ||||
|     "eslint-plugin-lit-a11y": "4.1.2", | ||||
|     "eslint-plugin-unused-imports": "3.1.0", | ||||
|     "eslint-plugin-unused-imports": "4.0.0", | ||||
|     "eslint-plugin-wc": "2.1.0", | ||||
|     "fancy-log": "2.0.0", | ||||
|     "fs-extra": "11.2.0", | ||||
|     "glob": "10.3.12", | ||||
|     "gulp": "4.0.2", | ||||
|     "glob": "10.4.2", | ||||
|     "gulp": "5.0.0", | ||||
|     "gulp-json-transform": "0.5.0", | ||||
|     "gulp-merge-json": "2.2.1", | ||||
|     "gulp-rename": "2.0.0", | ||||
|     "gulp-zopfli-green": "6.0.1", | ||||
|     "html-minifier-terser": "7.2.0", | ||||
|     "husky": "9.0.11", | ||||
|     "instant-mocha": "1.5.2", | ||||
|     "jszip": "3.10.1", | ||||
|     "lint-staged": "15.2.2", | ||||
|     "lint-staged": "15.2.7", | ||||
|     "lit-analyzer": "2.0.3", | ||||
|     "lodash.merge": "4.6.2", | ||||
|     "lodash.template": "4.5.0", | ||||
|     "magic-string": "0.30.10", | ||||
|     "map-stream": "0.0.7", | ||||
| @@ -225,27 +224,27 @@ | ||||
|     "object-hash": "3.0.0", | ||||
|     "open": "10.1.0", | ||||
|     "pinst": "3.0.0", | ||||
|     "prettier": "3.2.5", | ||||
|     "prettier": "3.3.2", | ||||
|     "rollup": "2.79.1", | ||||
|     "rollup-plugin-string": "3.0.0", | ||||
|     "rollup-plugin-terser": "7.0.2", | ||||
|     "rollup-plugin-visualizer": "5.12.0", | ||||
|     "serve-handler": "6.1.5", | ||||
|     "sinon": "17.0.1", | ||||
|     "sinon": "18.0.0", | ||||
|     "source-map-url": "0.4.1", | ||||
|     "systemjs": "6.14.3", | ||||
|     "tar": "7.0.1", | ||||
|     "systemjs": "6.15.1", | ||||
|     "tar": "7.4.0", | ||||
|     "terser-webpack-plugin": "5.3.10", | ||||
|     "transform-async-modules-webpack-plugin": "1.1.0", | ||||
|     "transform-async-modules-webpack-plugin": "1.1.1", | ||||
|     "ts-lit-plugin": "2.0.2", | ||||
|     "typescript": "5.4.5", | ||||
|     "webpack": "5.91.0", | ||||
|     "webpack": "5.92.1", | ||||
|     "webpack-cli": "5.1.4", | ||||
|     "webpack-dev-server": "5.0.4", | ||||
|     "webpack-manifest-plugin": "5.0.0", | ||||
|     "webpack-stats-plugin": "1.1.3", | ||||
|     "webpackbar": "6.0.1", | ||||
|     "workbox-build": "7.0.0" | ||||
|     "workbox-build": "7.1.1" | ||||
|   }, | ||||
|   "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch", | ||||
|   "resolutions": { | ||||
| @@ -254,8 +253,9 @@ | ||||
|     "lit": "2.8.0", | ||||
|     "clean-css": "5.3.3", | ||||
|     "@lit/reactive-element": "1.6.3", | ||||
|     "@fullcalendar/daygrid": "6.1.11", | ||||
|     "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" | ||||
|   }, | ||||
|   "packageManager": "yarn@4.1.1" | ||||
|   "packageManager": "yarn@4.3.1" | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" | ||||
|  | ||||
| [project] | ||||
| name         = "home-assistant-frontend" | ||||
| version      = "20240424.1" | ||||
| version      = "20240610.0" | ||||
| license      = {text = "Apache-2.0"} | ||||
| description  = "The Home Assistant frontend" | ||||
| readme       = "README.md" | ||||
|   | ||||
| @@ -31,6 +31,7 @@ import { | ||||
|   mdiFormatListBulleted, | ||||
|   mdiFormatListCheckbox, | ||||
|   mdiFormTextbox, | ||||
|   mdiForumOutline, | ||||
|   mdiGauge, | ||||
|   mdiGoogleAssistant, | ||||
|   mdiGoogleCirclesCommunities, | ||||
| @@ -98,7 +99,7 @@ export const FIXED_DOMAIN_ICONS = { | ||||
|   calendar: mdiCalendar, | ||||
|   climate: mdiThermostat, | ||||
|   configurator: mdiCog, | ||||
|   conversation: mdiMicrophoneMessage, | ||||
|   conversation: mdiForumOutline, | ||||
|   counter: mdiCounter, | ||||
|   date: mdiCalendar, | ||||
|   datetime: mdiCalendarClock, | ||||
| @@ -235,6 +236,8 @@ export const SENSOR_ENTITIES = [ | ||||
|   "weather", | ||||
| ]; | ||||
|  | ||||
| export const ASSIST_ENTITIES = ["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 | ||||
|  *  be the default) unless the element itself enforces it (e.g. a button). Also those elements | ||||
|   | ||||
| @@ -1,8 +1,6 @@ | ||||
| import { getWeekStartByLocale } from "weekstart"; | ||||
| import { FrontendLocaleData, FirstWeekday } from "../../data/translation"; | ||||
|  | ||||
| import "../../resources/intl-polyfill"; | ||||
|  | ||||
| export const weekdays = [ | ||||
|   "sunday", | ||||
|   "monday", | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { HassConfig } from "home-assistant-js-websocket"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { DateFormat, FrontendLocaleData } from "../../data/translation"; | ||||
| import "../../resources/intl-polyfill"; | ||||
| import { resolveTimeZone } from "./resolve-time-zone"; | ||||
|  | ||||
| // Tuesday, August 10 | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { HassConfig } from "home-assistant-js-websocket"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { FrontendLocaleData } from "../../data/translation"; | ||||
| import "../../resources/intl-polyfill"; | ||||
| import { formatDateNumeric } from "./format_date"; | ||||
| import { formatTime } from "./format_time"; | ||||
| import { resolveTimeZone } from "./resolve-time-zone"; | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import { HaDurationData } from "../../components/ha-duration-input"; | ||||
| import { FrontendLocaleData } from "../../data/translation"; | ||||
| import "../../resources/intl-polyfill"; | ||||
|  | ||||
| const leftPad = (num: number) => (num < 10 ? `0${num}` : num); | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { HassConfig } from "home-assistant-js-websocket"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { FrontendLocaleData } from "../../data/translation"; | ||||
| import "../../resources/intl-polyfill"; | ||||
| import { resolveTimeZone } from "./resolve-time-zone"; | ||||
| import { useAmPm } from "./use_am_pm"; | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import memoizeOne from "memoize-one"; | ||||
| import "../../resources/intl-polyfill"; | ||||
|  | ||||
| export const localizeWeekdays = memoizeOne( | ||||
|   (language: string, short: boolean): string[] => { | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { FrontendLocaleData } from "../../data/translation"; | ||||
| import "../../resources/intl-polyfill"; | ||||
| import { selectUnit } from "../util/select-unit"; | ||||
|  | ||||
| const formatRelTimeMem = memoizeOne( | ||||
|   | ||||
| @@ -108,6 +108,8 @@ export const storage = | ||||
|     subscribe?: boolean; | ||||
|     state?: boolean; | ||||
|     stateOptions?: InternalPropertyDeclaration; | ||||
|     serializer?: (value: any) => any; | ||||
|     deserializer?: (value: any) => any; | ||||
|   }): any => | ||||
|   (clsElement: ClassElement) => { | ||||
|     const storageName = options.storage || "localStorage"; | ||||
| @@ -141,7 +143,9 @@ export const storage = | ||||
|  | ||||
|     const getValue = (): any => | ||||
|       storageInstance.hasKey(storageKey!) | ||||
|         ? storageInstance.getValue(storageKey!) | ||||
|         ? options.deserializer | ||||
|           ? options.deserializer(storageInstance.getValue(storageKey!)) | ||||
|           : storageInstance.getValue(storageKey!) | ||||
|         : initVal; | ||||
|  | ||||
|     const setValue = (el: ReactiveElement, value: any) => { | ||||
| @@ -149,7 +153,10 @@ export const storage = | ||||
|       if (options.state) { | ||||
|         oldValue = getValue(); | ||||
|       } | ||||
|       storageInstance.setValue(storageKey!, value); | ||||
|       storageInstance.setValue( | ||||
|         storageKey!, | ||||
|         options.serializer ? options.serializer(value) : value | ||||
|       ); | ||||
|       if (options.state) { | ||||
|         el.requestUpdate(clsElement.key, oldValue); | ||||
|       } | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| export type MediaQueriesListener = () => void; | ||||
|  | ||||
| /** | ||||
|  * Attach a media query. Listener is called right away and when it matches. | ||||
|  * @param mediaQuery media query to match. | ||||
| @@ -7,7 +9,7 @@ | ||||
| export const listenMediaQuery = ( | ||||
|   mediaQuery: string, | ||||
|   matchesChanged: (matches: boolean) => void | ||||
| ) => { | ||||
| ): MediaQueriesListener => { | ||||
|   const mql = matchMedia(mediaQuery); | ||||
|   const listener = (e) => matchesChanged(e.matches); | ||||
|   mql.addListener(listener); | ||||
|   | ||||
							
								
								
									
										1
									
								
								src/common/dom/prevent_default.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/common/dom/prevent_default.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| export const preventDefault = (ev) => ev.preventDefault(); | ||||
| @@ -19,28 +19,11 @@ import { blankBeforeUnit } from "../translations/blank_before_unit"; | ||||
| import { LocalizeFunc } from "../translations/localize"; | ||||
| import { computeDomain } from "./compute_domain"; | ||||
|  | ||||
| export const computeStateDisplaySingleEntity = ( | ||||
|   localize: LocalizeFunc, | ||||
|   stateObj: HassEntity, | ||||
|   locale: FrontendLocaleData, | ||||
|   config: HassConfig, | ||||
|   entity: EntityRegistryDisplayEntry | undefined, | ||||
|   state?: string | ||||
| ): string => | ||||
|   computeStateDisplayFromEntityAttributes( | ||||
|     localize, | ||||
|     locale, | ||||
|     config, | ||||
|     entity, | ||||
|     stateObj.entity_id, | ||||
|     stateObj.attributes, | ||||
|     state !== undefined ? state : stateObj.state | ||||
|   ); | ||||
|  | ||||
| export const computeStateDisplay = ( | ||||
|   localize: LocalizeFunc, | ||||
|   stateObj: HassEntity, | ||||
|   locale: FrontendLocaleData, | ||||
|   sensorNumericDeviceClasses: string[], | ||||
|   config: HassConfig, | ||||
|   entities: HomeAssistant["entities"], | ||||
|   state?: string | ||||
| @@ -52,6 +35,7 @@ export const computeStateDisplay = ( | ||||
|   return computeStateDisplayFromEntityAttributes( | ||||
|     localize, | ||||
|     locale, | ||||
|     sensorNumericDeviceClasses, | ||||
|     config, | ||||
|     entity, | ||||
|     stateObj.entity_id, | ||||
| @@ -63,6 +47,7 @@ export const computeStateDisplay = ( | ||||
| export const computeStateDisplayFromEntityAttributes = ( | ||||
|   localize: LocalizeFunc, | ||||
|   locale: FrontendLocaleData, | ||||
|   sensorNumericDeviceClasses: string[], | ||||
|   config: HassConfig, | ||||
|   entity: EntityRegistryDisplayEntry | undefined, | ||||
|   entityId: string, | ||||
| @@ -73,8 +58,15 @@ export const computeStateDisplayFromEntityAttributes = ( | ||||
|     return localize(`state.default.${state}`); | ||||
|   } | ||||
|  | ||||
|   const domain = computeDomain(entityId); | ||||
|  | ||||
|   // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` | ||||
|   if (isNumericFromAttributes(attributes)) { | ||||
|   if ( | ||||
|     isNumericFromAttributes( | ||||
|       attributes, | ||||
|       domain === "sensor" ? sensorNumericDeviceClasses : [] | ||||
|     ) | ||||
|   ) { | ||||
|     // state is duration | ||||
|     if ( | ||||
|       attributes.device_class === "duration" && | ||||
| @@ -120,8 +112,6 @@ export const computeStateDisplayFromEntityAttributes = ( | ||||
|     return value; | ||||
|   } | ||||
|  | ||||
|   const domain = computeDomain(entityId); | ||||
|  | ||||
|   if (domain === "datetime") { | ||||
|     const time = new Date(state); | ||||
|     return formatDateTime(time, locale, config); | ||||
|   | ||||
| @@ -28,7 +28,15 @@ export const FIXED_DOMAIN_STATES = { | ||||
|   input_button: [], | ||||
|   lawn_mower: ["error", "paused", "mowing", "docked"], | ||||
|   light: ["on", "off"], | ||||
|   lock: ["jammed", "locked", "locking", "unlocked", "unlocking"], | ||||
|   lock: [ | ||||
|     "jammed", | ||||
|     "locked", | ||||
|     "locking", | ||||
|     "unlocked", | ||||
|     "unlocking", | ||||
|     "opening", | ||||
|     "open", | ||||
|   ], | ||||
|   media_player: [ | ||||
|     "off", | ||||
|     "on", | ||||
|   | ||||
| @@ -12,11 +12,10 @@ export const formatLanguageCode = ( | ||||
|   } | ||||
| }; | ||||
|  | ||||
| const formatLanguageCodeMem = memoizeOne((locale: FrontendLocaleData) => | ||||
|   Intl && "DisplayNames" in Intl | ||||
|     ? new Intl.DisplayNames(locale.language, { | ||||
|         type: "language", | ||||
|         fallback: "code", | ||||
|       }) | ||||
|     : undefined | ||||
| const formatLanguageCodeMem = memoizeOne( | ||||
|   (locale: FrontendLocaleData) => | ||||
|     new Intl.DisplayNames(locale.language, { | ||||
|       type: "language", | ||||
|       fallback: "code", | ||||
|     }) | ||||
| ); | ||||
|   | ||||
| @@ -11,6 +11,7 @@ declare global { | ||||
|  | ||||
| export interface NavigateOptions { | ||||
|   replace?: boolean; | ||||
|   data?: any; | ||||
| } | ||||
|  | ||||
| export const navigate = (path: string, options?: NavigateOptions) => { | ||||
| @@ -24,7 +25,7 @@ export const navigate = (path: string, options?: NavigateOptions) => { | ||||
|   if (__DEMO__) { | ||||
|     if (replace) { | ||||
|       mainWindow.history.replaceState( | ||||
|         mainWindow.history.state?.root ? { root: true } : null, | ||||
|         mainWindow.history.state?.root ? { root: true } : options?.data ?? null, | ||||
|         "", | ||||
|         `${mainWindow.location.pathname}#${path}` | ||||
|       ); | ||||
| @@ -33,12 +34,12 @@ export const navigate = (path: string, options?: NavigateOptions) => { | ||||
|     } | ||||
|   } else if (replace) { | ||||
|     mainWindow.history.replaceState( | ||||
|       mainWindow.history.state?.root ? { root: true } : null, | ||||
|       mainWindow.history.state?.root ? { root: true } : options?.data ?? null, | ||||
|       "", | ||||
|       path | ||||
|     ); | ||||
|   } else { | ||||
|     mainWindow.history.pushState(null, "", path); | ||||
|     mainWindow.history.pushState(options?.data ?? null, "", path); | ||||
|   } | ||||
|   fireEvent(mainWindow, "location-changed", { | ||||
|     replace, | ||||
|   | ||||
| @@ -14,8 +14,12 @@ export const isNumericState = (stateObj: HassEntity): boolean => | ||||
|   isNumericFromAttributes(stateObj.attributes); | ||||
|  | ||||
| export const isNumericFromAttributes = ( | ||||
|   attributes: HassEntityAttributeBase | ||||
| ): boolean => !!attributes.unit_of_measurement || !!attributes.state_class; | ||||
|   attributes: HassEntityAttributeBase, | ||||
|   numericDeviceClasses?: string[] | ||||
| ): boolean => | ||||
|   !!attributes.unit_of_measurement || | ||||
|   !!attributes.state_class || | ||||
|   (numericDeviceClasses || []).includes(attributes.device_class || ""); | ||||
|  | ||||
| export const numberFormatToLocale = ( | ||||
|   localeOptions: FrontendLocaleData | ||||
| @@ -59,30 +63,18 @@ export const formatNumber = ( | ||||
|  | ||||
|   if ( | ||||
|     localeOptions?.number_format !== NumberFormat.none && | ||||
|     !Number.isNaN(Number(num)) && | ||||
|     Intl | ||||
|     !Number.isNaN(Number(num)) | ||||
|   ) { | ||||
|     try { | ||||
|       return new Intl.NumberFormat( | ||||
|         locale, | ||||
|         getDefaultFormatOptions(num, options) | ||||
|       ).format(Number(num)); | ||||
|     } catch (err: any) { | ||||
|       // Don't fail when using "TEST" language | ||||
|       // eslint-disable-next-line no-console | ||||
|       console.error(err); | ||||
|       return new Intl.NumberFormat( | ||||
|         undefined, | ||||
|         getDefaultFormatOptions(num, options) | ||||
|       ).format(Number(num)); | ||||
|     } | ||||
|     return new Intl.NumberFormat( | ||||
|       locale, | ||||
|       getDefaultFormatOptions(num, options) | ||||
|     ).format(Number(num)); | ||||
|   } | ||||
|  | ||||
|   if ( | ||||
|     !Number.isNaN(Number(num)) && | ||||
|     num !== "" && | ||||
|     localeOptions?.number_format === NumberFormat.none && | ||||
|     Intl | ||||
|     localeOptions?.number_format === NumberFormat.none | ||||
|   ) { | ||||
|     // If NumberFormat is none, use en-US format without grouping. | ||||
|     return new Intl.NumberFormat( | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import memoizeOne from "memoize-one"; | ||||
| import "../../resources/intl-polyfill"; | ||||
| import { FrontendLocaleData } from "../../data/translation"; | ||||
|  | ||||
| export const formatListWithAnds = ( | ||||
|   | ||||
| @@ -21,7 +21,8 @@ export const computeFormatFunctions = async ( | ||||
|   localize: LocalizeFunc, | ||||
|   locale: FrontendLocaleData, | ||||
|   config: HassConfig, | ||||
|   entities: HomeAssistant["entities"] | ||||
|   entities: HomeAssistant["entities"], | ||||
|   sensorNumericDeviceClasses: string[] | ||||
| ): Promise<{ | ||||
|   formatEntityState: FormatEntityStateFunc; | ||||
|   formatEntityAttributeValue: FormatEntityAttributeValueFunc; | ||||
| @@ -35,7 +36,15 @@ export const computeFormatFunctions = async ( | ||||
|  | ||||
|   return { | ||||
|     formatEntityState: (stateObj, state) => | ||||
|       computeStateDisplay(localize, stateObj, locale, config, entities, state), | ||||
|       computeStateDisplay( | ||||
|         localize, | ||||
|         stateObj, | ||||
|         locale, | ||||
|         sensorNumericDeviceClasses, | ||||
|         config, | ||||
|         entities, | ||||
|         state | ||||
|       ), | ||||
|     formatEntityAttributeValue: (stateObj, attribute, value) => | ||||
|       computeAttributeValueDisplay( | ||||
|         localize, | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import IntlMessageFormat from "intl-messageformat"; | ||||
| import type { IntlMessageFormat } from "intl-messageformat"; | ||||
| import type { HTMLTemplateResult } from "lit"; | ||||
| import { polyfillLocaleData } from "../../resources/locale-data-polyfill"; | ||||
| import { polyfillLocaleData } from "../../resources/polyfills/locale-data-polyfill"; | ||||
| import { Resources, TranslationDict } from "../../types"; | ||||
| import { fireEvent } from "../dom/fire_event"; | ||||
|  | ||||
| @@ -89,9 +89,8 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>( | ||||
|   resources: Resources, | ||||
|   formats?: FormatsType | ||||
| ): Promise<LocalizeFunc<Keys>> => { | ||||
|   await import("../../resources/intl-polyfill").then(() => | ||||
|     polyfillLocaleData(language) | ||||
|   ); | ||||
|   const { IntlMessageFormat } = await import("intl-messageformat"); | ||||
|   await polyfillLocaleData(language); | ||||
|  | ||||
|   // Every time any of the parameters change, invalidate the strings cache. | ||||
|   cache._localizationCache = {}; | ||||
|   | ||||
| @@ -313,31 +313,38 @@ export class HaChartBase extends LitElement { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _loading = false; | ||||
|  | ||||
|   private async _setupChart() { | ||||
|     if (this._loading) return; | ||||
|     const ctx: CanvasRenderingContext2D = this.renderRoot | ||||
|       .querySelector("canvas")! | ||||
|       .getContext("2d")!; | ||||
|     this._loading = true; | ||||
|     try { | ||||
|       const ChartConstructor = (await import("../../resources/chartjs")).Chart; | ||||
|  | ||||
|     const ChartConstructor = (await import("../../resources/chartjs")).Chart; | ||||
|       const computedStyles = getComputedStyle(this); | ||||
|  | ||||
|     const computedStyles = getComputedStyle(this); | ||||
|       ChartConstructor.defaults.borderColor = | ||||
|         computedStyles.getPropertyValue("--divider-color"); | ||||
|       ChartConstructor.defaults.color = computedStyles.getPropertyValue( | ||||
|         "--secondary-text-color" | ||||
|       ); | ||||
|       ChartConstructor.defaults.font.family = | ||||
|         computedStyles.getPropertyValue("--mdc-typography-body1-font-family") || | ||||
|         computedStyles.getPropertyValue("--mdc-typography-font-family") || | ||||
|         "Roboto, Noto, sans-serif"; | ||||
|  | ||||
|     ChartConstructor.defaults.borderColor = | ||||
|       computedStyles.getPropertyValue("--divider-color"); | ||||
|     ChartConstructor.defaults.color = computedStyles.getPropertyValue( | ||||
|       "--secondary-text-color" | ||||
|     ); | ||||
|     ChartConstructor.defaults.font.family = | ||||
|       computedStyles.getPropertyValue("--mdc-typography-body1-font-family") || | ||||
|       computedStyles.getPropertyValue("--mdc-typography-font-family") || | ||||
|       "Roboto, Noto, sans-serif"; | ||||
|  | ||||
|     this.chart = new ChartConstructor(ctx, { | ||||
|       type: this.chartType, | ||||
|       data: this.data, | ||||
|       options: this._createOptions(), | ||||
|       plugins: this._createPlugins(), | ||||
|     }); | ||||
|       this.chart = new ChartConstructor(ctx, { | ||||
|         type: this.chartType, | ||||
|         data: this.data, | ||||
|         options: this._createOptions(), | ||||
|         plugins: this._createPlugins(), | ||||
|       }); | ||||
|     } finally { | ||||
|       this._loading = false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _createOptions() { | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import "element-internals-polyfill"; | ||||
| import { MdAssistChip } from "@material/web/chips/assist-chip"; | ||||
| import { css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import "element-internals-polyfill"; | ||||
| import { MdChipSet } from "@material/web/chips/chip-set"; | ||||
| import { customElement } from "lit/decorators"; | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import "element-internals-polyfill"; | ||||
| import { MdFilterChip } from "@material/web/chips/filter-chip"; | ||||
| import { css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import "element-internals-polyfill"; | ||||
| import { MdInputChip } from "@material/web/chips/input-chip"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
| @@ -20,6 +19,7 @@ export class HaInputChip extends MdInputChip { | ||||
|           0.15 | ||||
|         ); | ||||
|         --ha-input-chip-selected-container-opacity: 1; | ||||
|         --md-input-chip-label-text-font: Roboto, sans-serif; | ||||
|       } | ||||
|       /** Set the size of mdc icons **/ | ||||
|       ::slotted([slot="icon"]) { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { mdiArrowDown, mdiArrowUp, mdiChevronDown } from "@mdi/js"; | ||||
| import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js"; | ||||
| import deepClone from "deep-clone-simple"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
| @@ -565,36 +565,30 @@ export class HaDataTable extends LitElement { | ||||
|           }, {}); | ||||
|         const groupedItems: DataTableRowData[] = []; | ||||
|         Object.entries(sorted).forEach(([groupName, rows]) => { | ||||
|           if ( | ||||
|             groupName !== UNDEFINED_GROUP_KEY || | ||||
|             Object.keys(sorted).length > 1 | ||||
|           ) { | ||||
|             groupedItems.push({ | ||||
|               append: true, | ||||
|               content: html`<div | ||||
|                 class="mdc-data-table__cell group-header" | ||||
|                 role="cell" | ||||
|                 .group=${groupName} | ||||
|                 @click=${this._collapseGroup} | ||||
|           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 | ||||
|                   .path=${mdiChevronDown} | ||||
|                   class=${this._collapsedGroups.includes(groupName) | ||||
|                     ? "collapsed" | ||||
|                     : ""} | ||||
|                 > | ||||
|                 </ha-icon-button> | ||||
|                 ${groupName === UNDEFINED_GROUP_KEY | ||||
|                   ? this.hass.localize("ui.components.data-table.ungrouped") | ||||
|                   : groupName || ""} | ||||
|               </div>`, | ||||
|             }); | ||||
|           } | ||||
|               </ha-icon-button> | ||||
|               ${groupName === UNDEFINED_GROUP_KEY | ||||
|                 ? this.hass.localize("ui.components.data-table.ungrouped") | ||||
|                 : groupName || ""} | ||||
|             </div>`, | ||||
|           }); | ||||
|           if (!this._collapsedGroups.includes(groupName)) { | ||||
|             groupedItems.push(...rows); | ||||
|           } | ||||
|         }); | ||||
|  | ||||
|         items = groupedItems; | ||||
|       } | ||||
|  | ||||
| @@ -736,6 +730,28 @@ export class HaDataTable extends LitElement { | ||||
|     fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); | ||||
|   }; | ||||
|  | ||||
|   public expandAllGroups() { | ||||
|     this._collapsedGroups = []; | ||||
|     fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); | ||||
|   } | ||||
|  | ||||
|   public collapseAllGroups() { | ||||
|     if ( | ||||
|       !this.groupColumn || | ||||
|       !this.data.some((item) => item[this.groupColumn!]) | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
|     const grouped = groupBy(this.data, (item) => item[this.groupColumn!]); | ||||
|     if (grouped.undefined) { | ||||
|       // undefined is a reserved group name | ||||
|       grouped[UNDEFINED_GROUP_KEY] = grouped.undefined; | ||||
|       delete grouped.undefined; | ||||
|     } | ||||
|     this._collapsedGroups = Object.keys(grouped); | ||||
|     fireEvent(this, "collapsed-changed", { value: this._collapsedGroups }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyleScrollbar, | ||||
| @@ -990,6 +1006,7 @@ export class HaDataTable extends LitElement { | ||||
|           padding-top: 12px; | ||||
|           padding-left: 12px; | ||||
|           padding-inline-start: 12px; | ||||
|           padding-inline-end: initial; | ||||
|           width: 100%; | ||||
|           font-weight: 500; | ||||
|           display: flex; | ||||
|   | ||||
| @@ -76,6 +76,8 @@ class HaEntitiesPickerLight extends LitElement { | ||||
|   @property({ attribute: false }) | ||||
|   public entityFilter?: HaEntityPickerEntityFilterFunc; | ||||
|  | ||||
|   @property({ type: Array }) public createDomains?: string[]; | ||||
|  | ||||
|   protected render() { | ||||
|     if (!this.hass) { | ||||
|       return nothing; | ||||
| @@ -103,6 +105,7 @@ class HaEntitiesPickerLight extends LitElement { | ||||
|               .value=${entityId} | ||||
|               .label=${this.pickedEntityLabel} | ||||
|               .disabled=${this.disabled} | ||||
|               .createDomains=${this.createDomains} | ||||
|               @value-changed=${this._entityChanged} | ||||
|             ></ha-entity-picker> | ||||
|           </div> | ||||
| @@ -122,6 +125,7 @@ class HaEntitiesPickerLight extends LitElement { | ||||
|           .label=${this.pickEntityLabel} | ||||
|           .helper=${this.helper} | ||||
|           .disabled=${this.disabled} | ||||
|           .createDomains=${this.createDomains} | ||||
|           .required=${this.required && !currentEntities.length} | ||||
|           @value-changed=${this._addEntity} | ||||
|         ></ha-entity-picker> | ||||
|   | ||||
| @@ -405,9 +405,9 @@ export class HaEntityPicker extends LitElement { | ||||
|     this._opened = ev.detail.value; | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: ValueChangedEvent<string>) { | ||||
|   private _valueChanged(ev: ValueChangedEvent<string | undefined>) { | ||||
|     ev.stopPropagation(); | ||||
|     const newValue = ev.detail.value; | ||||
|     const newValue = ev.detail.value?.trim(); | ||||
|  | ||||
|     if (newValue && newValue.startsWith(CREATE_ID)) { | ||||
|       const domain = newValue.substring(CREATE_ID.length); | ||||
| @@ -427,13 +427,13 @@ export class HaEntityPicker extends LitElement { | ||||
|  | ||||
|   private _filterChanged(ev: CustomEvent): void { | ||||
|     const target = ev.target as HaComboBox; | ||||
|     const filterString = ev.detail.value.toLowerCase(); | ||||
|     const filterString = ev.detail.value.trim().toLowerCase(); | ||||
|     target.filteredItems = filterString.length | ||||
|       ? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states) | ||||
|       : this._states; | ||||
|   } | ||||
|  | ||||
|   private _setValue(value: string) { | ||||
|   private _setValue(value: string | undefined) { | ||||
|     this.value = value; | ||||
|     setTimeout(() => { | ||||
|       fireEvent(this, "value-changed", { value }); | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import "element-internals-polyfill"; | ||||
| import { MdCircularProgress } from "@material/web/progress/circular-progress"; | ||||
| import { PropertyValues, css } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
|   | ||||
| @@ -47,6 +47,8 @@ export class HaCodeEditor extends ReactiveElement { | ||||
|  | ||||
|   @property({ type: Boolean }) public readOnly = false; | ||||
|  | ||||
|   @property({ type: Boolean }) public linewrap = false; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "autocomplete-entities" }) | ||||
|   public autocompleteEntities = false; | ||||
|  | ||||
| @@ -134,6 +136,13 @@ export class HaCodeEditor extends ReactiveElement { | ||||
|         ), | ||||
|       }); | ||||
|     } | ||||
|     if (changedProps.has("linewrap")) { | ||||
|       transactions.push({ | ||||
|         effects: this._loadedCodeMirror!.linewrapCompartment!.reconfigure( | ||||
|           this.linewrap ? this._loadedCodeMirror!.EditorView.lineWrapping : [] | ||||
|         ), | ||||
|       }); | ||||
|     } | ||||
|     if (changedProps.has("_value") && this._value !== this.value) { | ||||
|       transactions.push({ | ||||
|         changes: { | ||||
| @@ -181,6 +190,9 @@ export class HaCodeEditor extends ReactiveElement { | ||||
|       this._loadedCodeMirror.readonlyCompartment.of( | ||||
|         this._loadedCodeMirror.EditorView.editable.of(!this.readOnly) | ||||
|       ), | ||||
|       this._loadedCodeMirror.linewrapCompartment.of( | ||||
|         this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : [] | ||||
|       ), | ||||
|       this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate), | ||||
|     ]; | ||||
|  | ||||
|   | ||||
| @@ -1,14 +1,7 @@ | ||||
| import { Ripple } from "@material/mwc-ripple"; | ||||
| import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { | ||||
|   customElement, | ||||
|   eventOptions, | ||||
|   property, | ||||
|   queryAsync, | ||||
|   state, | ||||
| } from "lit/decorators"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { ifDefined } from "lit/directives/if-defined"; | ||||
| import "./ha-ripple"; | ||||
|  | ||||
| @customElement("ha-control-button") | ||||
| export class HaControlButton extends LitElement { | ||||
| @@ -16,10 +9,6 @@ export class HaControlButton extends LitElement { | ||||
|  | ||||
|   @property() public label?: string; | ||||
|  | ||||
|   @queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>; | ||||
|  | ||||
|   @state() private _shouldRenderRipple = false; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <button | ||||
| @@ -28,54 +17,13 @@ export class HaControlButton extends LitElement { | ||||
|         aria-label=${ifDefined(this.label)} | ||||
|         title=${ifDefined(this.label)} | ||||
|         .disabled=${Boolean(this.disabled)} | ||||
|         @focus=${this.handleRippleFocus} | ||||
|         @blur=${this.handleRippleBlur} | ||||
|         @mousedown=${this.handleRippleActivate} | ||||
|         @mouseup=${this.handleRippleDeactivate} | ||||
|         @mouseenter=${this.handleRippleMouseEnter} | ||||
|         @mouseleave=${this.handleRippleMouseLeave} | ||||
|         @touchstart=${this.handleRippleActivate} | ||||
|         @touchend=${this.handleRippleDeactivate} | ||||
|         @touchcancel=${this.handleRippleDeactivate} | ||||
|       > | ||||
|         <slot></slot> | ||||
|         ${this._shouldRenderRipple && !this.disabled | ||||
|           ? html`<mwc-ripple></mwc-ripple>` | ||||
|           : ""} | ||||
|         <ha-ripple .disabled=${this.disabled}></ha-ripple> | ||||
|       </button> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _rippleHandlers: RippleHandlers = new RippleHandlers(() => { | ||||
|     this._shouldRenderRipple = true; | ||||
|     return this._ripple; | ||||
|   }); | ||||
|  | ||||
|   @eventOptions({ passive: true }) | ||||
|   private handleRippleActivate(evt?: Event) { | ||||
|     this._rippleHandlers.startPress(evt); | ||||
|   } | ||||
|  | ||||
|   private handleRippleDeactivate() { | ||||
|     this._rippleHandlers.endPress(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleMouseEnter() { | ||||
|     this._rippleHandlers.startHover(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleMouseLeave() { | ||||
|     this._rippleHandlers.endHover(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleFocus() { | ||||
|     this._rippleHandlers.startFocus(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleBlur() { | ||||
|     this._rippleHandlers.endFocus(); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       :host { | ||||
| @@ -86,6 +34,7 @@ export class HaControlButton extends LitElement { | ||||
|         --control-button-border-radius: 10px; | ||||
|         --control-button-padding: 8px; | ||||
|         --mdc-icon-size: 20px; | ||||
|         --ha-ripple-color: var(--secondary-text-color); | ||||
|         color: var(--primary-text-color); | ||||
|         width: 40px; | ||||
|         height: 40px; | ||||
| @@ -113,12 +62,14 @@ export class HaControlButton extends LitElement { | ||||
|         outline: none; | ||||
|         overflow: hidden; | ||||
|         background: none; | ||||
|         --mdc-ripple-color: var(--control-button-background-color); | ||||
|         /* For safari border-radius overflow */ | ||||
|         z-index: 0; | ||||
|         font-size: inherit; | ||||
|         color: inherit; | ||||
|       } | ||||
|       .button:focus-visible { | ||||
|         --control-button-background-opacity: 0.4; | ||||
|       } | ||||
|       .button::before { | ||||
|         content: ""; | ||||
|         position: absolute; | ||||
|   | ||||
| @@ -1,22 +1,14 @@ | ||||
| import { Ripple } from "@material/mwc-ripple"; | ||||
| import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers"; | ||||
| import { SelectBase } from "@material/mwc-select/mwc-select-base"; | ||||
| import { mdiMenuDown } from "@mdi/js"; | ||||
| import { css, html, nothing } from "lit"; | ||||
| import { | ||||
|   customElement, | ||||
|   eventOptions, | ||||
|   property, | ||||
|   query, | ||||
|   queryAsync, | ||||
|   state, | ||||
| } from "lit/decorators"; | ||||
| import { customElement, property, query } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { ifDefined } from "lit/directives/if-defined"; | ||||
| import { debounce } from "../common/util/debounce"; | ||||
| import { nextRender } from "../common/util/render-status"; | ||||
| import "./ha-icon"; | ||||
| import type { HaIcon } from "./ha-icon"; | ||||
| import "./ha-ripple"; | ||||
| import "./ha-svg-icon"; | ||||
| import type { HaSvgIcon } from "./ha-svg-icon"; | ||||
|  | ||||
| @@ -32,10 +24,6 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|   @property({ type: Boolean, attribute: "hide-label" }) | ||||
|   public hideLabel = false; | ||||
|  | ||||
|   @queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>; | ||||
|  | ||||
|   @state() private _shouldRenderRipple = false; | ||||
|  | ||||
|   public override render() { | ||||
|     const classes = { | ||||
|       "select-disabled": this.disabled, | ||||
| @@ -69,17 +57,10 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|           aria-labelledby=${ifDefined(labelledby)} | ||||
|           aria-label=${ifDefined(labelAttribute)} | ||||
|           aria-required=${this.required} | ||||
|           @click=${this.onClick} | ||||
|           @focus=${this.onFocus} | ||||
|           @blur=${this.onBlur} | ||||
|           @click=${this.onClick} | ||||
|           @keydown=${this.onKeydown} | ||||
|           @mousedown=${this.handleRippleActivate} | ||||
|           @mouseup=${this.handleRippleDeactivate} | ||||
|           @mouseenter=${this.handleRippleMouseEnter} | ||||
|           @mouseleave=${this.handleRippleMouseLeave} | ||||
|           @touchstart=${this.handleRippleActivate} | ||||
|           @touchend=${this.handleRippleDeactivate} | ||||
|           @touchcancel=${this.handleRippleDeactivate} | ||||
|         > | ||||
|           ${this.renderIcon()} | ||||
|           <div class="content"> | ||||
| @@ -91,9 +72,7 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|               : nothing} | ||||
|           </div> | ||||
|           ${this.renderArrow()} | ||||
|           ${this._shouldRenderRipple && !this.disabled | ||||
|             ? html` <mwc-ripple></mwc-ripple> ` | ||||
|             : nothing} | ||||
|           <ha-ripple .disabled=${this.disabled}></ha-ripple> | ||||
|         </div> | ||||
|         ${this.renderMenu()} | ||||
|       </div> | ||||
| @@ -135,46 +114,6 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   protected onFocus() { | ||||
|     this.handleRippleFocus(); | ||||
|     super.onFocus(); | ||||
|   } | ||||
|  | ||||
|   protected onBlur() { | ||||
|     this.handleRippleBlur(); | ||||
|     super.onBlur(); | ||||
|   } | ||||
|  | ||||
|   private _rippleHandlers: RippleHandlers = new RippleHandlers(() => { | ||||
|     this._shouldRenderRipple = true; | ||||
|     return this._ripple; | ||||
|   }); | ||||
|  | ||||
|   @eventOptions({ passive: true }) | ||||
|   private handleRippleActivate(evt?: Event) { | ||||
|     this._rippleHandlers.startPress(evt); | ||||
|   } | ||||
|  | ||||
|   private handleRippleDeactivate() { | ||||
|     this._rippleHandlers.endPress(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleMouseEnter() { | ||||
|     this._rippleHandlers.startHover(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleMouseLeave() { | ||||
|     this._rippleHandlers.endHover(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleFocus() { | ||||
|     this._rippleHandlers.startFocus(); | ||||
|   } | ||||
|  | ||||
|   private handleRippleBlur() { | ||||
|     this._rippleHandlers.endFocus(); | ||||
|   } | ||||
|  | ||||
|   connectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     window.addEventListener("translations-updated", this._translationsUpdated); | ||||
| @@ -204,6 +143,7 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|         --control-select-menu-height: 48px; | ||||
|         --control-select-menu-padding: 6px 10px; | ||||
|         --mdc-icon-size: 20px; | ||||
|         --ha-ripple-color: var(--secondary-text-color); | ||||
|         font-size: 14px; | ||||
|         line-height: 1.4; | ||||
|         width: auto; | ||||
| @@ -224,7 +164,6 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|         outline: none; | ||||
|         overflow: hidden; | ||||
|         background: none; | ||||
|         --mdc-ripple-color: var(--control-select-menu-background-color); | ||||
|         /* For safari border-radius overflow */ | ||||
|         z-index: 0; | ||||
|         transition: color 180ms ease-in-out; | ||||
| @@ -264,6 +203,10 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|         letter-spacing: inherit; | ||||
|       } | ||||
|  | ||||
|       .select-anchor:focus-visible { | ||||
|         --control-select-menu-background-opacity: 0.4; | ||||
|       } | ||||
|  | ||||
|       .select-anchor::before { | ||||
|         content: ""; | ||||
|         position: absolute; | ||||
|   | ||||
| @@ -19,6 +19,7 @@ import { HomeAssistant } from "../types"; | ||||
| import "./ha-list-item"; | ||||
| import "./ha-select"; | ||||
| import type { HaSelect } from "./ha-select"; | ||||
| import { getExtendedEntityRegistryEntry } from "../data/entity_registry"; | ||||
|  | ||||
| const NONE = "__NONE_OPTION__"; | ||||
|  | ||||
| @@ -107,13 +108,23 @@ export class HaConversationAgentPicker extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private async _maybeFetchConfigEntry() { | ||||
|     if (!this.value || this.value === "homeassistant") { | ||||
|     if (!this.value || !(this.value in this.hass.entities)) { | ||||
|       this._configEntry = undefined; | ||||
|       return; | ||||
|     } | ||||
|     try { | ||||
|       const regEntry = await getExtendedEntityRegistryEntry( | ||||
|         this.hass, | ||||
|         this.value | ||||
|       ); | ||||
|  | ||||
|       if (!regEntry.config_entry_id) { | ||||
|         this._configEntry = undefined; | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       this._configEntry = ( | ||||
|         await getConfigEntry(this.hass, this.value) | ||||
|         await getConfigEntry(this.hass, regEntry.config_entry_id) | ||||
|       ).config_entry; | ||||
|     } catch (err) { | ||||
|       this._configEntry = undefined; | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { stopPropagation } from "../common/dom/stop_propagation"; | ||||
| import { caseInsensitiveStringCompare } from "../common/string/compare"; | ||||
| import "../resources/intl-polyfill"; | ||||
| import "./ha-list-item"; | ||||
| import "./ha-select"; | ||||
| import type { HaSelect } from "./ha-select"; | ||||
| @@ -282,14 +281,10 @@ export class HaCountryPicker extends LitElement { | ||||
|   private _getOptions = memoizeOne( | ||||
|     (language?: string, countries?: string[]) => { | ||||
|       let options: { label: string; value: string }[] = []; | ||||
|       const countryDisplayNames = | ||||
|         Intl && "DisplayNames" in Intl | ||||
|           ? new Intl.DisplayNames(language, { | ||||
|               type: "region", | ||||
|               fallback: "code", | ||||
|             }) | ||||
|           : undefined; | ||||
|  | ||||
|       const countryDisplayNames = new Intl.DisplayNames(language, { | ||||
|         type: "region", | ||||
|         fallback: "code", | ||||
|       }); | ||||
|       if (countries) { | ||||
|         options = countries.map((country) => ({ | ||||
|           value: country, | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { stopPropagation } from "../common/dom/stop_propagation"; | ||||
| import { caseInsensitiveStringCompare } from "../common/string/compare"; | ||||
| import "../resources/intl-polyfill"; | ||||
| import "./ha-list-item"; | ||||
| import "./ha-select"; | ||||
| import type { HaSelect } from "./ha-select"; | ||||
| @@ -170,12 +169,9 @@ const CURRENCIES = [ | ||||
| ]; | ||||
|  | ||||
| const curSymbol = (currency: string, locale?: string) => | ||||
|   Intl && "NumberFormat" in Intl | ||||
|     ? new Intl.NumberFormat(locale, { style: "currency", currency }) | ||||
|         .formatToParts(1) | ||||
|         .find((x) => x.type === "currency")?.value | ||||
|     : currency; | ||||
|  | ||||
|   new Intl.NumberFormat(locale, { style: "currency", currency }) | ||||
|     .formatToParts(1) | ||||
|     .find((x) => x.type === "currency")?.value; | ||||
| @customElement("ha-currency-picker") | ||||
| export class HaCurrencyPicker extends LitElement { | ||||
|   @property() public language = "en"; | ||||
| @@ -189,13 +185,10 @@ export class HaCurrencyPicker extends LitElement { | ||||
|   @property({ type: Boolean, reflect: true }) public disabled = false; | ||||
|  | ||||
|   private _getOptions = memoizeOne((language?: string) => { | ||||
|     const currencyDisplayNames = | ||||
|       Intl && "DisplayNames" in Intl | ||||
|         ? new Intl.DisplayNames(language, { | ||||
|             type: "currency", | ||||
|             fallback: "code", | ||||
|           }) | ||||
|         : undefined; | ||||
|     const currencyDisplayNames = new Intl.DisplayNames(language, { | ||||
|       type: "currency", | ||||
|       fallback: "code", | ||||
|     }); | ||||
|     const options = CURRENCIES.map((currency) => ({ | ||||
|       value: currency, | ||||
|       label: `${ | ||||
|   | ||||
| @@ -127,6 +127,10 @@ export class HaDialog extends DialogBase { | ||||
|         border-radius: var(--ha-dialog-border-radius, 28px); | ||||
|         -webkit-backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none); | ||||
|         backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none); | ||||
|         background: var( | ||||
|           --ha-dialog-surface-background, | ||||
|           var(--mdc-theme-surface, #fff) | ||||
|         ); | ||||
|       } | ||||
|       :host([flexContent]) .mdc-dialog .mdc-dialog__content { | ||||
|         display: flex; | ||||
|   | ||||
| @@ -21,6 +21,8 @@ export class HaExpansionPanel extends LitElement { | ||||
|  | ||||
|   @property({ type: Boolean, reflect: true }) leftChevron = false; | ||||
|  | ||||
|   @property({ type: Boolean, reflect: true }) noCollapse = false; | ||||
|  | ||||
|   @property() header?: string; | ||||
|  | ||||
|   @property() secondary?: string; | ||||
| @@ -34,16 +36,17 @@ export class HaExpansionPanel extends LitElement { | ||||
|       <div class="top ${classMap({ expanded: this.expanded })}"> | ||||
|         <div | ||||
|           id="summary" | ||||
|           class=${classMap({ noCollapse: this.noCollapse })} | ||||
|           @click=${this._toggleContainer} | ||||
|           @keydown=${this._toggleContainer} | ||||
|           @focus=${this._focusChanged} | ||||
|           @blur=${this._focusChanged} | ||||
|           role="button" | ||||
|           tabindex="0" | ||||
|           tabindex=${this.noCollapse ? -1 : 0} | ||||
|           aria-expanded=${this.expanded} | ||||
|           aria-controls="sect1" | ||||
|         > | ||||
|           ${this.leftChevron | ||||
|           ${this.leftChevron && !this.noCollapse | ||||
|             ? html` | ||||
|                 <ha-svg-icon | ||||
|                   .path=${mdiChevronDown} | ||||
| @@ -57,7 +60,7 @@ export class HaExpansionPanel extends LitElement { | ||||
|               <slot class="secondary" name="secondary">${this.secondary}</slot> | ||||
|             </div> | ||||
|           </slot> | ||||
|           ${!this.leftChevron | ||||
|           ${!this.leftChevron && !this.noCollapse | ||||
|             ? html` | ||||
|                 <ha-svg-icon | ||||
|                   .path=${mdiChevronDown} | ||||
| @@ -106,6 +109,9 @@ export class HaExpansionPanel extends LitElement { | ||||
|       return; | ||||
|     } | ||||
|     ev.preventDefault(); | ||||
|     if (this.noCollapse) { | ||||
|       return; | ||||
|     } | ||||
|     const newExpanded = !this.expanded; | ||||
|     fireEvent(this, "expanded-will-change", { expanded: newExpanded }); | ||||
|     this._container.style.overflow = "hidden"; | ||||
| @@ -130,6 +136,9 @@ export class HaExpansionPanel extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _focusChanged(ev) { | ||||
|     if (this.noCollapse) { | ||||
|       return; | ||||
|     } | ||||
|     this.shadowRoot!.querySelector(".top")!.classList.toggle( | ||||
|       "focused", | ||||
|       ev.type === "focus" | ||||
| @@ -191,6 +200,9 @@ export class HaExpansionPanel extends LitElement { | ||||
|         font-weight: 500; | ||||
|         outline: none; | ||||
|       } | ||||
|       #summary.noCollapse { | ||||
|         cursor: default; | ||||
|       } | ||||
|  | ||||
|       .summary-icon.expanded { | ||||
|         transform: rotate(180deg); | ||||
|   | ||||
| @@ -1,7 +1,14 @@ | ||||
| import { SelectedDetail } from "@material/mwc-list"; | ||||
| import "@material/mwc-menu/mwc-menu-surface"; | ||||
| import { mdiFilterVariantRemove } from "@mdi/js"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   nothing, | ||||
|   PropertyValues, | ||||
| } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { Blueprints, fetchBlueprints } from "../data/blueprint"; | ||||
| @@ -25,6 +32,16 @@ export class HaFilterBlueprints extends LitElement { | ||||
|  | ||||
|   @state() private _blueprints?: Blueprints; | ||||
|  | ||||
|   public willUpdate(properties: PropertyValues) { | ||||
|     super.willUpdate(properties); | ||||
|  | ||||
|     if (!this.hasUpdated) { | ||||
|       if (this.value?.length) { | ||||
|         this._findRelated(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     return html` | ||||
|       <ha-expansion-panel | ||||
| @@ -96,7 +113,6 @@ export class HaFilterBlueprints extends LitElement { | ||||
|     ev: CustomEvent<SelectedDetail<Set<number>>> | ||||
|   ) { | ||||
|     const blueprints = this._blueprints!; | ||||
|     const relatedPromises: Promise<RelatedResult>[] = []; | ||||
|  | ||||
|     if (!ev.detail.index.size) { | ||||
|       fireEvent(this, "data-table-filter-changed", { | ||||
| @@ -112,13 +128,33 @@ export class HaFilterBlueprints extends LitElement { | ||||
|     for (const index of ev.detail.index) { | ||||
|       const blueprintId = Object.keys(blueprints)[index]; | ||||
|       value.push(blueprintId); | ||||
|     } | ||||
|  | ||||
|     this.value = value; | ||||
|  | ||||
|     this._findRelated(); | ||||
|   } | ||||
|  | ||||
|   private async _findRelated() { | ||||
|     if (!this.value?.length) { | ||||
|       fireEvent(this, "data-table-filter-changed", { | ||||
|         value: [], | ||||
|         items: undefined, | ||||
|       }); | ||||
|       this.value = []; | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const relatedPromises: Promise<RelatedResult>[] = []; | ||||
|  | ||||
|     for (const blueprintId of this.value) { | ||||
|       if (this.type) { | ||||
|         relatedPromises.push( | ||||
|           findRelated(this.hass, `${this.type}_blueprint`, blueprintId) | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|     this.value = value; | ||||
|  | ||||
|     const results = await Promise.all(relatedPromises); | ||||
|     const items: Set<string> = new Set(); | ||||
|     for (const result of results) { | ||||
| @@ -128,7 +164,7 @@ export class HaFilterBlueprints extends LitElement { | ||||
|     } | ||||
|  | ||||
|     fireEvent(this, "data-table-filter-changed", { | ||||
|       value, | ||||
|       value: this.value, | ||||
|       items: this.type ? items : undefined, | ||||
|     }); | ||||
|   } | ||||
|   | ||||
| @@ -41,6 +41,9 @@ export class HaFilterDevices extends LitElement { | ||||
|  | ||||
|     if (!this.hasUpdated) { | ||||
|       loadVirtualizer(); | ||||
|       if (this.value?.length) { | ||||
|         this._findRelated(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -87,9 +87,20 @@ export class HaFilterDomains extends LitElement { | ||||
|     Object.keys(states).forEach((entityId) => { | ||||
|       domains.add(computeDomain(entityId)); | ||||
|     }); | ||||
|     return Array.from(domains) | ||||
|       .filter((domain) => !filter || domain.toLowerCase().includes(filter)) | ||||
|       .sort((a, b) => stringCompare(a, b, this.hass.locale.language)); | ||||
|  | ||||
|     return Array.from(domains.values()) | ||||
|       .map((domain) => ({ | ||||
|         domain, | ||||
|         name: domainToName(this.hass.localize, domain), | ||||
|       })) | ||||
|       .filter( | ||||
|         (entry) => | ||||
|           !filter || | ||||
|           entry.domain.toLowerCase().includes(filter) || | ||||
|           entry.name.toLowerCase().includes(filter) | ||||
|       ) | ||||
|       .sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language)) | ||||
|       .map((entry) => entry.domain); | ||||
|   }); | ||||
|  | ||||
|   protected updated(changed) { | ||||
| @@ -163,14 +174,14 @@ export class HaFilterDomains extends LitElement { | ||||
|           align-items: center; | ||||
|         } | ||||
|         .header ha-icon-button { | ||||
|           margin-inline-start: auto; | ||||
|           margin-inline-start: initial; | ||||
|           margin-inline-end: 8px; | ||||
|         } | ||||
|         .badge { | ||||
|           display: inline-block; | ||||
|           margin-left: 8px; | ||||
|           margin-inline-start: 8px; | ||||
|           margin-inline-end: 0; | ||||
|           margin-inline-end: initial; | ||||
|           min-width: 16px; | ||||
|           box-sizing: border-box; | ||||
|           border-radius: 50%; | ||||
|   | ||||
| @@ -42,6 +42,9 @@ export class HaFilterEntities extends LitElement { | ||||
|  | ||||
|     if (!this.hasUpdated) { | ||||
|       loadVirtualizer(); | ||||
|       if (this.value?.length) { | ||||
|         this._findRelated(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -186,15 +189,12 @@ export class HaFilterEntities extends LitElement { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const value: string[] = []; | ||||
|  | ||||
|     for (const entityId of this.value) { | ||||
|       value.push(entityId); | ||||
|       if (this.type) { | ||||
|         relatedPromises.push(findRelated(this.hass, "entity", entityId)); | ||||
|       } | ||||
|     } | ||||
|     this.value = value; | ||||
|  | ||||
|     const results = await Promise.all(relatedPromises); | ||||
|     const items: Set<string> = new Set(); | ||||
|     for (const result of results) { | ||||
| @@ -204,7 +204,7 @@ export class HaFilterEntities extends LitElement { | ||||
|     } | ||||
|  | ||||
|     fireEvent(this, "data-table-filter-changed", { | ||||
|       value, | ||||
|       value: this.value, | ||||
|       items: this.type ? items : undefined, | ||||
|     }); | ||||
|   } | ||||
|   | ||||
| @@ -1,7 +1,14 @@ | ||||
| import "@material/mwc-menu/mwc-menu-surface"; | ||||
| import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js"; | ||||
| import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
|   LitElement, | ||||
|   PropertyValues, | ||||
|   css, | ||||
|   html, | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { repeat } from "lit/directives/repeat"; | ||||
| @@ -42,6 +49,16 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @state() private _floors?: FloorRegistryEntry[]; | ||||
|  | ||||
|   public willUpdate(properties: PropertyValues) { | ||||
|     super.willUpdate(properties); | ||||
|  | ||||
|     if (!this.hasUpdated) { | ||||
|       if (this.value?.floors?.length || this.value?.areas?.length) { | ||||
|         this._findRelated(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     const areas = this._areas(this.hass.areas, this._floors); | ||||
|  | ||||
| @@ -190,6 +207,10 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected firstUpdated() { | ||||
|     this._findRelated(); | ||||
|   } | ||||
|  | ||||
|   private _expandedWillChange(ev) { | ||||
|     this._shouldRender = ev.detail.expanded; | ||||
|   } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import type { Selector } from "../../data/selector"; | ||||
| import type { HaFormSchema } from "./types"; | ||||
|  | ||||
| export const computeInitialHaFormData = ( | ||||
|   schema: HaFormSchema[] | ||||
|   schema: HaFormSchema[] | readonly HaFormSchema[] | ||||
| ): Record<string, any> => { | ||||
|   const data = {}; | ||||
|   schema.forEach((field) => { | ||||
| @@ -36,6 +36,8 @@ export const computeInitialHaFormData = ( | ||||
|         minutes: 0, | ||||
|         seconds: 0, | ||||
|       }; | ||||
|     } else if (field.type === "expandable") { | ||||
|       data[field.name] = computeInitialHaFormData(field.schema); | ||||
|     } else if ("selector" in field) { | ||||
|       const selector: Selector = field.selector; | ||||
|  | ||||
| @@ -71,6 +73,10 @@ export const computeInitialHaFormData = ( | ||||
|         if (selector.country?.countries?.length) { | ||||
|           data[field.name] = selector.country.countries[0]; | ||||
|         } | ||||
|       } else if ("language" in selector) { | ||||
|         if (selector.language?.languages?.length) { | ||||
|           data[field.name] = selector.language.languages[0]; | ||||
|         } | ||||
|       } else if ("duration" in selector) { | ||||
|         data[field.name] = { | ||||
|           hours: 0, | ||||
| @@ -93,7 +99,9 @@ export const computeInitialHaFormData = ( | ||||
|       ) { | ||||
|         data[field.name] = {}; | ||||
|       } else { | ||||
|         throw new Error("Selector not supported in initial form data"); | ||||
|         throw new Error( | ||||
|           `Selector ${Object.keys(selector)[0]} not supported in initial form data` | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
|   | ||||
| @@ -1,13 +1,29 @@ | ||||
| import { FormfieldBase } from "@material/mwc-formfield/mwc-formfield-base"; | ||||
| import { styles } from "@material/mwc-formfield/mwc-formfield.css"; | ||||
| import { css } from "lit"; | ||||
| import { css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
|  | ||||
| @customElement("ha-formfield") | ||||
| export class HaFormfield extends FormfieldBase { | ||||
|   @property({ type: Boolean, reflect: true }) public disabled = false; | ||||
|  | ||||
|   protected override render() { | ||||
|     const classes = { | ||||
|       "mdc-form-field--align-end": this.alignEnd, | ||||
|       "mdc-form-field--space-between": this.spaceBetween, | ||||
|       "mdc-form-field--nowrap": this.nowrap, | ||||
|     }; | ||||
|  | ||||
|     return html` <div class="mdc-form-field ${classMap(classes)}"> | ||||
|       <slot></slot> | ||||
|       <label class="mdc-label" @click=${this._labelClick} | ||||
|         ><slot name="label">${this.label}</slot></label | ||||
|       > | ||||
|     </div>`; | ||||
|   } | ||||
|  | ||||
|   protected _labelClick() { | ||||
|     const input = this.input as HTMLInputElement | undefined; | ||||
|     if (!input) return; | ||||
| @@ -39,6 +55,9 @@ export class HaFormfield extends FormfieldBase { | ||||
|         margin-inline-end: 10px; | ||||
|         margin-inline-start: inline; | ||||
|       } | ||||
|       .mdc-form-field { | ||||
|         align-items: var(--ha-formfield-align-items, center); | ||||
|       } | ||||
|       .mdc-form-field > label { | ||||
|         direction: var(--direction); | ||||
|         margin-inline-start: 0; | ||||
|   | ||||
							
								
								
									
										233
									
								
								src/components/ha-grid-size-picker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								src/components/ha-grid-size-picker.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,233 @@ | ||||
| import { LitElement, css, html, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import "./ha-icon-button"; | ||||
| import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider"; | ||||
|  | ||||
| import { mdiRestore } from "@mdi/js"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { HomeAssistant } from "../types"; | ||||
|  | ||||
| type GridSizeValue = { | ||||
|   rows?: number; | ||||
|   columns?: number; | ||||
| }; | ||||
|  | ||||
| @customElement("ha-grid-size-picker") | ||||
| export class HaGridSizeEditor extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public value?: GridSizeValue; | ||||
|  | ||||
|   @property({ attribute: false }) public rows = 6; | ||||
|  | ||||
|   @property({ attribute: false }) public columns = 4; | ||||
|  | ||||
|   @property({ attribute: false }) public rowMin?: number; | ||||
|  | ||||
|   @property({ attribute: false }) public rowMax?: number; | ||||
|  | ||||
|   @property({ attribute: false }) public columnMin?: number; | ||||
|  | ||||
|   @property({ attribute: false }) public columnMax?: number; | ||||
|  | ||||
|   @property({ attribute: false }) public isDefault?: boolean; | ||||
|  | ||||
|   @state() public _localValue?: GridSizeValue = undefined; | ||||
|  | ||||
|   protected willUpdate(changedProperties) { | ||||
|     if (changedProperties.has("value")) { | ||||
|       this._localValue = this.value; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     return html` | ||||
|       <div class="grid"> | ||||
|         <ha-grid-layout-slider | ||||
|           aria-label=${this.hass.localize( | ||||
|             "ui.components.grid-size-picker.columns" | ||||
|           )} | ||||
|           id="columns" | ||||
|           .min=${this.columnMin ?? 1} | ||||
|           .max=${this.columnMax ?? this.columns} | ||||
|           .range=${this.columns} | ||||
|           .value=${this.value?.columns} | ||||
|           @value-changed=${this._valueChanged} | ||||
|           @slider-moved=${this._sliderMoved} | ||||
|         ></ha-grid-layout-slider> | ||||
|         <ha-grid-layout-slider | ||||
|           aria-label=${this.hass.localize( | ||||
|             "ui.components.grid-size-picker.rows" | ||||
|           )} | ||||
|           id="rows" | ||||
|           .min=${this.rowMin ?? 1} | ||||
|           .max=${this.rowMax ?? this.rows} | ||||
|           .range=${this.rows} | ||||
|           vertical | ||||
|           .value=${this.value?.rows} | ||||
|           @value-changed=${this._valueChanged} | ||||
|           @slider-moved=${this._sliderMoved} | ||||
|         ></ha-grid-layout-slider> | ||||
|         ${!this.isDefault | ||||
|           ? html` | ||||
|               <ha-icon-button | ||||
|                 @click=${this._reset} | ||||
|                 class="reset" | ||||
|                 .path=${mdiRestore} | ||||
|                 label=${this.hass.localize( | ||||
|                   "ui.components.grid-size-picker.reset_default" | ||||
|                 )} | ||||
|                 title=${this.hass.localize( | ||||
|                   "ui.components.grid-size-picker.reset_default" | ||||
|                 )} | ||||
|               > | ||||
|               </ha-icon-button> | ||||
|             ` | ||||
|           : nothing} | ||||
|         <div | ||||
|           class="preview" | ||||
|           style=${styleMap({ | ||||
|             "--total-rows": this.rows, | ||||
|             "--total-columns": this.columns, | ||||
|             "--rows": this._localValue?.rows, | ||||
|             "--columns": this._localValue?.columns, | ||||
|           })} | ||||
|         > | ||||
|           <div> | ||||
|             ${Array(this.rows * this.columns) | ||||
|               .fill(0) | ||||
|               .map((_, index) => { | ||||
|                 const row = Math.floor(index / this.columns) + 1; | ||||
|                 const column = (index % this.columns) + 1; | ||||
|                 const disabled = | ||||
|                   (this.rowMin !== undefined && row < this.rowMin) || | ||||
|                   (this.rowMax !== undefined && row > this.rowMax) || | ||||
|                   (this.columnMin !== undefined && column < this.columnMin) || | ||||
|                   (this.columnMax !== undefined && column > this.columnMax); | ||||
|                 return html` | ||||
|                   <div | ||||
|                     class="cell" | ||||
|                     data-row=${row} | ||||
|                     data-column=${column} | ||||
|                     ?disabled=${disabled} | ||||
|                     @click=${this._cellClick} | ||||
|                   ></div> | ||||
|                 `; | ||||
|               })} | ||||
|           </div> | ||||
|           <div class="selected"> | ||||
|             <div class="cell"></div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   _cellClick(ev) { | ||||
|     const cell = ev.currentTarget as HTMLElement; | ||||
|     if (cell.getAttribute("disabled") !== null) return; | ||||
|     const rows = Number(cell.getAttribute("data-row")); | ||||
|     const columns = Number(cell.getAttribute("data-column")); | ||||
|     fireEvent(this, "value-changed", { | ||||
|       value: { rows, columns }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev) { | ||||
|     ev.stopPropagation(); | ||||
|     const key = ev.currentTarget.id; | ||||
|     const newValue = { | ||||
|       ...this.value, | ||||
|       [key]: ev.detail.value, | ||||
|     }; | ||||
|     fireEvent(this, "value-changed", { value: newValue }); | ||||
|   } | ||||
|  | ||||
|   private _reset(ev) { | ||||
|     ev.stopPropagation(); | ||||
|     fireEvent(this, "value-changed", { | ||||
|       value: { | ||||
|         rows: undefined, | ||||
|         columns: undefined, | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _sliderMoved(ev) { | ||||
|     ev.stopPropagation(); | ||||
|     const key = ev.currentTarget.id; | ||||
|     const value = ev.detail.value; | ||||
|     if (value === undefined) return; | ||||
|     this._localValue = { | ||||
|       ...this.value, | ||||
|       [key]: ev.detail.value, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   static styles = [ | ||||
|     css` | ||||
|       .grid { | ||||
|         display: grid; | ||||
|         grid-template-areas: | ||||
|           "reset column-slider" | ||||
|           "row-slider preview"; | ||||
|         grid-template-rows: auto 1fr; | ||||
|         grid-template-columns: auto 1fr; | ||||
|         gap: 8px; | ||||
|       } | ||||
|       #columns { | ||||
|         grid-area: column-slider; | ||||
|       } | ||||
|       #rows { | ||||
|         grid-area: row-slider; | ||||
|       } | ||||
|       .reset { | ||||
|         grid-area: reset; | ||||
|       } | ||||
|       .preview { | ||||
|         position: relative; | ||||
|         grid-area: preview; | ||||
|         aspect-ratio: 1 / 1; | ||||
|       } | ||||
|       .preview > div { | ||||
|         position: absolute; | ||||
|         width: 100%; | ||||
|         height: 100%; | ||||
|         top: 0; | ||||
|         left: 0; | ||||
|         display: grid; | ||||
|         grid-template-columns: repeat(var(--total-columns), 1fr); | ||||
|         grid-template-rows: repeat(var(--total-rows), 1fr); | ||||
|         gap: 4px; | ||||
|       } | ||||
|       .preview .cell { | ||||
|         background-color: var(--disabled-color); | ||||
|         grid-column: span 1; | ||||
|         grid-row: span 1; | ||||
|         border-radius: 4px; | ||||
|         opacity: 0.2; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|       .preview .cell[disabled] { | ||||
|         opacity: 0.05; | ||||
|         cursor: initial; | ||||
|       } | ||||
|       .selected { | ||||
|         pointer-events: none; | ||||
|       } | ||||
|       .selected .cell { | ||||
|         background-color: var(--primary-color); | ||||
|         grid-column: 1 / span var(--columns, 0); | ||||
|         grid-row: 1 / span var(--rows, 0); | ||||
|         opacity: 0.5; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-grid-size-picker": HaGridSizeEditor; | ||||
|   } | ||||
| } | ||||
| @@ -1,4 +1,3 @@ | ||||
| import "@material/mwc-list/mwc-list-item"; | ||||
| import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; | ||||
| import { | ||||
|   ComboBoxDataProviderCallback, | ||||
| @@ -11,6 +10,7 @@ import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { customIcons } from "../data/custom_icons"; | ||||
| import { HomeAssistant, ValueChangedEvent } from "../types"; | ||||
| import "./ha-combo-box"; | ||||
| import "./ha-list-item"; | ||||
| import "./ha-icon"; | ||||
|  | ||||
| type IconItem = { | ||||
| @@ -67,10 +67,10 @@ const loadCustomIconItems = async (iconsetPrefix: string) => { | ||||
| }; | ||||
|  | ||||
| const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) => | ||||
|   html`<mwc-list-item graphic="avatar"> | ||||
|   html`<ha-list-item graphic="avatar"> | ||||
|     <ha-icon .icon=${item.icon} slot="graphic"></ha-icon> | ||||
|     ${item.icon} | ||||
|   </mwc-list-item>`; | ||||
|   </ha-list-item>`; | ||||
|  | ||||
| @customElement("ha-icon-picker") | ||||
| export class HaIconPicker extends LitElement { | ||||
| @@ -198,8 +198,7 @@ export class HaIconPicker extends LitElement { | ||||
|  | ||||
|   static get styles() { | ||||
|     return css` | ||||
|       ha-icon, | ||||
|       ha-svg-icon { | ||||
|       *[slot="icon"] { | ||||
|         color: var(--primary-text-color); | ||||
|         position: relative; | ||||
|         bottom: 2px; | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import "@material/web/ripple/ripple"; | ||||
|  | ||||
| @customElement("ha-label") | ||||
| class HaLabel extends LitElement { | ||||
| @@ -11,7 +10,6 @@ class HaLabel extends LitElement { | ||||
|       <span class="content"> | ||||
|         <slot name="icon"></slot> | ||||
|         <slot></slot> | ||||
|         <md-ripple></md-ripple> | ||||
|       </span> | ||||
|     `; | ||||
|   } | ||||
| @@ -27,7 +25,6 @@ class HaLabel extends LitElement { | ||||
|             0.15 | ||||
|           ); | ||||
|           --ha-label-background-opacity: 1; | ||||
|  | ||||
|           position: relative; | ||||
|           box-sizing: border-box; | ||||
|           display: inline-flex; | ||||
|   | ||||
| @@ -6,7 +6,6 @@ import { stopPropagation } from "../common/dom/stop_propagation"; | ||||
| import { formatLanguageCode } from "../common/language/format_language"; | ||||
| import { caseInsensitiveStringCompare } from "../common/string/compare"; | ||||
| import { FrontendLocaleData } from "../data/translation"; | ||||
| import "../resources/intl-polyfill"; | ||||
| import { translationMetadata } from "../resources/translations-metadata"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import "./ha-list-item"; | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { customElement } from "lit/decorators"; | ||||
| import "element-internals-polyfill"; | ||||
| 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 { | ||||
|   | ||||
| @@ -100,6 +100,7 @@ export class HaListItem extends ListItemBase { | ||||
|             span.material-icons:first-of-type, | ||||
|             span.material-icons:last-of-type { | ||||
|               direction: rtl !important; | ||||
|               --direction: rtl; | ||||
|             } | ||||
|           ` | ||||
|         : css``, | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { customElement } from "lit/decorators"; | ||||
| import "element-internals-polyfill"; | ||||
| 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 { | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import { MdMenuItem } from "@material/web/menu/menu-item"; | ||||
| import "element-internals-polyfill"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
|  | ||||
| @@ -25,6 +24,7 @@ export class HaMenuItem extends MdMenuItem { | ||||
|  | ||||
|         --md-sys-color-on-primary-container: var(--primary-text-color); | ||||
|         --md-sys-color-on-secondary-container: var(--primary-text-color); | ||||
|         --md-menu-item-label-text-font: Roboto, sans-serif; | ||||
|       } | ||||
|       :host(.warning) { | ||||
|         --md-menu-item-label-text-color: var(--error-color); | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { customElement } from "lit/decorators"; | ||||
| import "element-internals-polyfill"; | ||||
| import { css } from "lit"; | ||||
| import { MdMenu } from "@material/web/menu/menu"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
|  | ||||
| @customElement("ha-menu") | ||||
| export class HaMenu extends MdMenu { | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { MdOutlinedButton } from "@material/web/button/outlined-button"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
| import "element-internals-polyfill"; | ||||
| import { MdOutlinedButton } from "@material/web/button/outlined-button"; | ||||
|  | ||||
| @customElement("ha-outlined-button") | ||||
| export class HaOutlinedButton extends MdOutlinedButton { | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import { MdOutlinedField } from "@material/web/field/outlined-field"; | ||||
| import "element-internals-polyfill"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
| import { literal } from "lit/static-html"; | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { MdOutlinedIconButton } from "@material/web/iconbutton/outlined-icon-button"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
| import "element-internals-polyfill"; | ||||
| import { MdOutlinedIconButton } from "@material/web/iconbutton/outlined-icon-button"; | ||||
|  | ||||
| @customElement("ha-outlined-icon-button") | ||||
| export class HaOutlinedIconButton extends MdOutlinedIconButton { | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field"; | ||||
| import "element-internals-polyfill"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
| import { literal } from "lit/static-html"; | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { mdiImagePlus } from "@mdi/js"; | ||||
| import { LitElement, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { haStyle } from "../resources/styles"; | ||||
| import { createImage, generateImageThumbnailUrl } from "../data/image_upload"; | ||||
| import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; | ||||
| import { | ||||
| @@ -31,6 +32,8 @@ export class HaPictureUpload extends LitElement { | ||||
|  | ||||
|   @property({ attribute: false }) public cropOptions?: CropOptions; | ||||
|  | ||||
|   @property({ type: Boolean }) public original = false; | ||||
|  | ||||
|   @property({ type: Number }) public size = 512; | ||||
|  | ||||
|   @state() private _uploading = false; | ||||
| @@ -60,13 +63,15 @@ export class HaPictureUpload extends LitElement { | ||||
|           alt=${this.currentImageAltText || | ||||
|           this.hass.localize("ui.components.picture-upload.current_image_alt")} | ||||
|         /> | ||||
|         <ha-button | ||||
|           @click=${this._handleChangeClick} | ||||
|           .label=${this.hass.localize( | ||||
|             "ui.components.picture-upload.change_picture" | ||||
|           )} | ||||
|         > | ||||
|         </ha-button> | ||||
|         <div> | ||||
|           <ha-button | ||||
|             @click=${this._handleChangeClick} | ||||
|             .label=${this.hass.localize( | ||||
|               "ui.components.picture-upload.change_picture" | ||||
|             )} | ||||
|           > | ||||
|           </ha-button> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div>`; | ||||
|   } | ||||
| @@ -122,7 +127,11 @@ export class HaPictureUpload extends LitElement { | ||||
|     this._uploading = true; | ||||
|     try { | ||||
|       const media = await createImage(this.hass, file); | ||||
|       this.value = generateImageThumbnailUrl(media.id, this.size); | ||||
|       this.value = generateImageThumbnailUrl( | ||||
|         media.id, | ||||
|         this.size, | ||||
|         this.original | ||||
|       ); | ||||
|       fireEvent(this, "change"); | ||||
|     } catch (err: any) { | ||||
|       showAlertDialog(this, { | ||||
| @@ -134,32 +143,35 @@ export class HaPictureUpload extends LitElement { | ||||
|   } | ||||
|  | ||||
|   static get styles() { | ||||
|     return css` | ||||
|       :host { | ||||
|         display: block; | ||||
|         height: 240px; | ||||
|       } | ||||
|       ha-file-upload { | ||||
|         height: 100%; | ||||
|       } | ||||
|       .center-vertical { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         height: 100%; | ||||
|       } | ||||
|       .value { | ||||
|         width: 100%; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         align-items: center; | ||||
|       } | ||||
|       img { | ||||
|         max-width: 100%; | ||||
|         max-height: 200px; | ||||
|         margin-bottom: 4px; | ||||
|         border-radius: var(--file-upload-image-border-radius); | ||||
|       } | ||||
|     `; | ||||
|     return [ | ||||
|       haStyle, | ||||
|       css` | ||||
|         :host { | ||||
|           display: block; | ||||
|           height: 240px; | ||||
|         } | ||||
|         ha-file-upload { | ||||
|           height: 100%; | ||||
|         } | ||||
|         .center-vertical { | ||||
|           display: flex; | ||||
|           align-items: center; | ||||
|           height: 100%; | ||||
|         } | ||||
|         .value { | ||||
|           width: 100%; | ||||
|           display: flex; | ||||
|           flex-direction: column; | ||||
|           align-items: center; | ||||
|         } | ||||
|         img { | ||||
|           max-width: 100%; | ||||
|           max-height: 200px; | ||||
|           margin-bottom: 4px; | ||||
|           border-radius: var(--file-upload-image-border-radius); | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										62
									
								
								src/components/ha-ripple.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/components/ha-ripple.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| import { AttachableController } from "@material/web/internal/controller/attachable-controller"; | ||||
| import { MdRipple } from "@material/web/ripple/ripple"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
|  | ||||
| @customElement("ha-ripple") | ||||
| export class HaRipple extends MdRipple { | ||||
|   private readonly attachableTouchController = new AttachableController( | ||||
|     this, | ||||
|     this.onTouchControlChange.bind(this) | ||||
|   ); | ||||
|  | ||||
|   attach(control: HTMLElement) { | ||||
|     super.attach(control); | ||||
|     this.attachableTouchController.attach(control); | ||||
|   } | ||||
|  | ||||
|   detach() { | ||||
|     super.detach(); | ||||
|     this.attachableTouchController.detach(); | ||||
|   } | ||||
|  | ||||
|   private _handleTouchEnd = () => { | ||||
|     if (!this.disabled) { | ||||
|       // @ts-ignore | ||||
|       super.endPressAnimation(); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   private onTouchControlChange( | ||||
|     prev: HTMLElement | null, | ||||
|     next: HTMLElement | null | ||||
|   ) { | ||||
|     // Add touchend event to clean ripple on touch devices using action handler | ||||
|     prev?.removeEventListener("touchend", this._handleTouchEnd); | ||||
|     next?.addEventListener("touchend", this._handleTouchEnd); | ||||
|   } | ||||
|  | ||||
|   static override styles = [ | ||||
|     ...super.styles, | ||||
|     css` | ||||
|       :host { | ||||
|         --md-ripple-hover-opacity: var(--ha-ripple-hover-opacity, 0.08); | ||||
|         --md-ripple-pressed-opacity: var(--ha-ripple-pressed-opacity, 0.12); | ||||
|         --md-ripple-hover-color: var( | ||||
|           --ha-ripple-hover-color, | ||||
|           var(--ha-ripple-color, var(--secondary-text-color)) | ||||
|         ); | ||||
|         --md-ripple-pressed-color: var( | ||||
|           --ha-ripple-pressed-color, | ||||
|           var(--ha-ripple-color, var(--secondary-text-color)) | ||||
|         ); | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-ripple": HaRipple; | ||||
|   } | ||||
| } | ||||
| @@ -8,7 +8,10 @@ import { | ||||
|   fetchEntitySourcesWithCache, | ||||
| } from "../../data/entity_sources"; | ||||
| import type { EntitySelector } from "../../data/selector"; | ||||
| import { filterSelectorEntities } from "../../data/selector"; | ||||
| import { | ||||
|   filterSelectorEntities, | ||||
|   computeCreateDomains, | ||||
| } from "../../data/selector"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| import "../entity/ha-entities-picker"; | ||||
| import "../entity/ha-entity-picker"; | ||||
| @@ -31,6 +34,8 @@ export class HaEntitySelector extends LitElement { | ||||
|  | ||||
|   @property({ type: Boolean }) public required = true; | ||||
|  | ||||
|   @state() private _createDomains: string[] | undefined; | ||||
|  | ||||
|   private _hasIntegration(selector: EntitySelector) { | ||||
|     return ( | ||||
|       selector.entity?.filter && | ||||
| @@ -64,6 +69,7 @@ export class HaEntitySelector extends LitElement { | ||||
|         .includeEntities=${this.selector.entity?.include_entities} | ||||
|         .excludeEntities=${this.selector.entity?.exclude_entities} | ||||
|         .entityFilter=${this._filterEntities} | ||||
|         .createDomains=${this._createDomains} | ||||
|         .disabled=${this.disabled} | ||||
|         .required=${this.required} | ||||
|         allow-custom-entity | ||||
| @@ -79,6 +85,7 @@ export class HaEntitySelector extends LitElement { | ||||
|         .includeEntities=${this.selector.entity.include_entities} | ||||
|         .excludeEntities=${this.selector.entity.exclude_entities} | ||||
|         .entityFilter=${this._filterEntities} | ||||
|         .createDomains=${this._createDomains} | ||||
|         .disabled=${this.disabled} | ||||
|         .required=${this.required} | ||||
|       ></ha-entities-picker> | ||||
| @@ -96,6 +103,9 @@ export class HaEntitySelector extends LitElement { | ||||
|         this._entitySources = sources; | ||||
|       }); | ||||
|     } | ||||
|     if (changedProps.has("selector")) { | ||||
|       this._createDomains = computeCreateDomains(this.selector); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _filterEntities = (entity: HassEntity): boolean => { | ||||
|   | ||||
							
								
								
									
										145
									
								
								src/components/ha-selector/ha-selector-image.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/components/ha-selector/ha-selector-image.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | ||||
| import { css, CSSResultGroup, html, LitElement } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { ImageSelector } from "../../data/selector"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| import "../ha-icon-button"; | ||||
| import "../ha-textarea"; | ||||
| import "../ha-textfield"; | ||||
| import "../ha-picture-upload"; | ||||
| import "../ha-radio"; | ||||
| import type { HaPictureUpload } from "../ha-picture-upload"; | ||||
| import { URL_PREFIX } from "../../data/image_upload"; | ||||
|  | ||||
| @customElement("ha-selector-image") | ||||
| export class HaImageSelector extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property() public value?: any; | ||||
|  | ||||
|   @property() public name?: string; | ||||
|  | ||||
|   @property() public label?: string; | ||||
|  | ||||
|   @property() public placeholder?: string; | ||||
|  | ||||
|   @property() public helper?: string; | ||||
|  | ||||
|   @property({ attribute: false }) public selector!: ImageSelector; | ||||
|  | ||||
|   @property({ type: Boolean }) public disabled = false; | ||||
|  | ||||
|   @property({ type: Boolean }) public required = true; | ||||
|  | ||||
|   @state() private showUpload = false; | ||||
|  | ||||
|   protected firstUpdated(changedProps): void { | ||||
|     super.firstUpdated(changedProps); | ||||
|  | ||||
|     if (!this.value || this.value.startsWith(URL_PREFIX)) { | ||||
|       this.showUpload = true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     return html` | ||||
|       <div> | ||||
|         <label> | ||||
|           ${this.hass.localize("ui.components.selectors.image.select_image")} | ||||
|           <ha-formfield | ||||
|             .label=${this.hass.localize("ui.components.selectors.image.upload")} | ||||
|           > | ||||
|             <ha-radio | ||||
|               name="mode" | ||||
|               value="upload" | ||||
|               .checked=${this.showUpload} | ||||
|               @change=${this._radioGroupPicked} | ||||
|             ></ha-radio> | ||||
|           </ha-formfield> | ||||
|           <ha-formfield | ||||
|             .label=${this.hass.localize("ui.components.selectors.image.url")} | ||||
|           > | ||||
|             <ha-radio | ||||
|               name="mode" | ||||
|               value="url" | ||||
|               .checked=${!this.showUpload} | ||||
|               @change=${this._radioGroupPicked} | ||||
|             ></ha-radio> | ||||
|           </ha-formfield> | ||||
|         </label> | ||||
|         ${!this.showUpload | ||||
|           ? html` | ||||
|               <ha-textfield | ||||
|                 .name=${this.name} | ||||
|                 .value=${this.value || ""} | ||||
|                 .placeholder=${this.placeholder || ""} | ||||
|                 .helper=${this.helper} | ||||
|                 helperPersistent | ||||
|                 .disabled=${this.disabled} | ||||
|                 @input=${this._handleChange} | ||||
|                 .label=${this.label || ""} | ||||
|                 .required=${this.required} | ||||
|               ></ha-textfield> | ||||
|             ` | ||||
|           : html` | ||||
|               <ha-picture-upload | ||||
|                 .hass=${this.hass} | ||||
|                 .value=${this.value?.startsWith(URL_PREFIX) ? this.value : null} | ||||
|                 .original=${this.selector.image?.original} | ||||
|                 .cropOptions=${this.selector.image?.crop} | ||||
|                 @change=${this._pictureChanged} | ||||
|               ></ha-picture-upload> | ||||
|             `} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _radioGroupPicked(ev): void { | ||||
|     this.showUpload = ev.target.value === "upload"; | ||||
|   } | ||||
|  | ||||
|   private _pictureChanged(ev) { | ||||
|     const value = (ev.target as HaPictureUpload).value; | ||||
|  | ||||
|     fireEvent(this, "value-changed", { value: value ?? undefined }); | ||||
|   } | ||||
|  | ||||
|   private _handleChange(ev) { | ||||
|     let value = ev.target.value; | ||||
|     if (this.value === value) { | ||||
|       return; | ||||
|     } | ||||
|     if (value === "" && !this.required) { | ||||
|       value = undefined; | ||||
|     } | ||||
|  | ||||
|     fireEvent(this, "value-changed", { value }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       :host { | ||||
|         display: block; | ||||
|         position: relative; | ||||
|       } | ||||
|       div { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|       } | ||||
|       label { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|       } | ||||
|       ha-textarea, | ||||
|       ha-textfield { | ||||
|         width: 100%; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-selector-image": HaImageSelector; | ||||
|   } | ||||
| } | ||||
| @@ -278,6 +278,14 @@ export class HaSelectSelector extends LitElement { | ||||
|  | ||||
|   private _valueChanged(ev) { | ||||
|     ev.stopPropagation(); | ||||
|  | ||||
|     if (ev.detail?.index === -1 && this.value !== undefined) { | ||||
|       fireEvent(this, "value-changed", { | ||||
|         value: undefined, | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const value = ev.detail?.value || ev.target.value; | ||||
|     if (this.disabled || value === undefined || value === (this.value ?? "")) { | ||||
|       return; | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user