mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-30 22:19:55 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			copilot/fi
			...
			feature/sa
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | d1b71ac701 | 
							
								
								
									
										12
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -21,12 +21,12 @@ jobs: | |||||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} |       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|         with: |         with: | ||||||
|           ref: dev |           ref: dev | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v4.4.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -42,7 +42,7 @@ jobs: | |||||||
|       - name: Deploy to Netlify |       - name: Deploy to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         run: | | ||||||
|           npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --alias dev |           npx -y netlify-cli deploy --dir=cast/dist --alias dev | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }} |           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }} | ||||||
| @@ -56,12 +56,12 @@ jobs: | |||||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} |       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|         with: |         with: | ||||||
|           ref: master |           ref: master | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v4.4.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -77,7 +77,7 @@ jobs: | |||||||
|       - name: Deploy to Netlify |       - name: Deploy to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         run: | | ||||||
|           npx -y netlify-cli@23.7.3 deploy --dir=cast/dist --prod |           npx -y netlify-cli deploy --dir=cast/dist --prod | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }} |           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_CAST_SITE_ID }} | ||||||
|   | |||||||
							
								
								
									
										22
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -24,9 +24,9 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v4.4.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -37,7 +37,7 @@ jobs: | |||||||
|       - name: Build resources |       - name: Build resources | ||||||
|         run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages |         run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages | ||||||
|       - name: Setup lint cache |       - name: Setup lint cache | ||||||
|         uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 |         uses: actions/cache@v4.2.4 | ||||||
|         with: |         with: | ||||||
|           path: | |           path: | | ||||||
|             node_modules/.cache/prettier |             node_modules/.cache/prettier | ||||||
| @@ -58,9 +58,9 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v4.4.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -76,9 +76,9 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v4.4.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -89,7 +89,7 @@ jobs: | |||||||
|         env: |         env: | ||||||
|           IS_TEST: "true" |           IS_TEST: "true" | ||||||
|       - name: Upload bundle stats |       - name: Upload bundle stats | ||||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 |         uses: actions/upload-artifact@v4.6.2 | ||||||
|         with: |         with: | ||||||
|           name: frontend-bundle-stats |           name: frontend-bundle-stats | ||||||
|           path: build/stats/*.json |           path: build/stats/*.json | ||||||
| @@ -100,9 +100,9 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v4.4.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -113,7 +113,7 @@ jobs: | |||||||
|         env: |         env: | ||||||
|           IS_TEST: "true" |           IS_TEST: "true" | ||||||
|       - name: Upload bundle stats |       - name: Upload bundle stats | ||||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 |         uses: actions/upload-artifact@v4.6.2 | ||||||
|         with: |         with: | ||||||
|           name: supervisor-bundle-stats |           name: supervisor-bundle-stats | ||||||
|           path: build/stats/*.json |           path: build/stats/*.json | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -23,7 +23,7 @@ jobs: | |||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout repository |       - name: Checkout repository | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|         with: |         with: | ||||||
|           # We must fetch at least the immediate parents so that if this is |           # We must fetch at least the immediate parents so that if this is | ||||||
|           # a pull request then we can checkout the head. |           # a pull request then we can checkout the head. | ||||||
| @@ -36,14 +36,14 @@ jobs: | |||||||
|  |  | ||||||
|       # Initializes the CodeQL tools for scanning. |       # Initializes the CodeQL tools for scanning. | ||||||
|       - name: Initialize CodeQL |       - name: Initialize CodeQL | ||||||
|         uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 |         uses: github/codeql-action/init@v3 | ||||||
|         with: |         with: | ||||||
|           languages: ${{ matrix.language }} |           languages: ${{ matrix.language }} | ||||||
|  |  | ||||||
|       # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java). |       # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java). | ||||||
|       # If this step fails, then you should remove it and run the build manually (see below) |       # If this step fails, then you should remove it and run the build manually (see below) | ||||||
|       - name: Autobuild |       - name: Autobuild | ||||||
|         uses: github/codeql-action/autobuild@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 |         uses: github/codeql-action/autobuild@v3 | ||||||
|  |  | ||||||
|       # ℹ️ Command-line programs to run using the OS shell. |       # ℹ️ Command-line programs to run using the OS shell. | ||||||
|       # 📚 https://git.io/JvXDl |       # 📚 https://git.io/JvXDl | ||||||
| @@ -57,4 +57,4 @@ jobs: | |||||||
|       #   make release |       #   make release | ||||||
|  |  | ||||||
|       - name: Perform CodeQL Analysis |       - name: Perform CodeQL Analysis | ||||||
|         uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 |         uses: github/codeql-action/analyze@v3 | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -22,12 +22,12 @@ jobs: | |||||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} |       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|         with: |         with: | ||||||
|           ref: dev |           ref: dev | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v4.4.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -43,7 +43,7 @@ jobs: | |||||||
|       - name: Deploy to Netlify |       - name: Deploy to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         run: | | ||||||
|           npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod |           npx -y netlify-cli deploy --dir=demo/dist --prod | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }} |           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_DEV_SITE_ID }} | ||||||
| @@ -57,12 +57,12 @@ jobs: | |||||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} |       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|         with: |         with: | ||||||
|           ref: master |           ref: master | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v4.4.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -78,7 +78,7 @@ jobs: | |||||||
|       - name: Deploy to Netlify |       - name: Deploy to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         run: | | ||||||
|           npx -y netlify-cli@23.7.3 deploy --dir=demo/dist --prod |           npx -y netlify-cli deploy --dir=demo/dist --prod | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }} |           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEMO_SITE_ID }} | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -16,10 +16,10 @@ jobs: | |||||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} |       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v4.4.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -35,7 +35,7 @@ jobs: | |||||||
|       - name: Deploy to Netlify |       - name: Deploy to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         run: | | ||||||
|           npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --prod |           npx -y netlify-cli deploy --dir=gallery/dist --prod | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }} |           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_GALLERY_SITE_ID }} | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -21,10 +21,10 @@ jobs: | |||||||
|     if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') |     if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out files from GitHub |       - name: Check out files from GitHub | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v4.4.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -40,7 +40,7 @@ jobs: | |||||||
|       - name: Deploy preview to Netlify |       - name: Deploy preview to Netlify | ||||||
|         id: deploy |         id: deploy | ||||||
|         run: | |         run: | | ||||||
|           npx -y netlify-cli@23.7.3 deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \ |           npx -y netlify-cli deploy --dir=gallery/dist --alias "deploy-preview-${{ github.event.number }}" \ | ||||||
|             --json > deploy_output.json |             --json > deploy_output.json | ||||||
|         env: |         env: | ||||||
|           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} |           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/labeler.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/labeler.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -10,6 +10,6 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Apply labels |       - name: Apply labels | ||||||
|         uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 |         uses: actions/labeler@v5.0.0 | ||||||
|         with: |         with: | ||||||
|           sync-labels: true |           sync-labels: true | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							| @@ -9,7 +9,7 @@ jobs: | |||||||
|   lock: |   lock: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 |       - uses: dessant/lock-threads@v5.0.1 | ||||||
|         with: |         with: | ||||||
|           github-token: ${{ github.token }} |           github-token: ${{ github.token }} | ||||||
|           process-only: "issues, prs" |           process-only: "issues, prs" | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -20,15 +20,15 @@ jobs: | |||||||
|       contents: write |       contents: write | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|  |  | ||||||
|       - name: Set up Python ${{ env.PYTHON_VERSION }} |       - name: Set up Python ${{ env.PYTHON_VERSION }} | ||||||
|         uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 |         uses: actions/setup-python@v5 | ||||||
|         with: |         with: | ||||||
|           python-version: ${{ env.PYTHON_VERSION }} |           python-version: ${{ env.PYTHON_VERSION }} | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v4.4.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -57,14 +57,14 @@ jobs: | |||||||
|         run: tar -czvf translations.tar.gz translations |         run: tar -czvf translations.tar.gz translations | ||||||
|  |  | ||||||
|       - name: Upload build artifacts |       - name: Upload build artifacts | ||||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 |         uses: actions/upload-artifact@v4.6.2 | ||||||
|         with: |         with: | ||||||
|           name: wheels |           name: wheels | ||||||
|           path: dist/home_assistant_frontend*.whl |           path: dist/home_assistant_frontend*.whl | ||||||
|           if-no-files-found: error |           if-no-files-found: error | ||||||
|  |  | ||||||
|       - name: Upload translations |       - name: Upload translations | ||||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 |         uses: actions/upload-artifact@v4.6.2 | ||||||
|         with: |         with: | ||||||
|           name: translations |           name: translations | ||||||
|           path: translations.tar.gz |           path: translations.tar.gz | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/relative-ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/relative-ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Send bundle stats and build information to RelativeCI |       - name: Send bundle stats and build information to RelativeCI | ||||||
|         uses: relative-ci/agent-action@1707825cbfcc7452b2913d273414705415ae64d4 # v3.0.1 |         uses: relative-ci/agent-action@v3.0.1 | ||||||
|         with: |         with: | ||||||
|           key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }} |           key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }} | ||||||
|           token: ${{ github.token }} |           token: ${{ github.token }} | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/release-drafter.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release-drafter.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -18,6 +18,6 @@ jobs: | |||||||
|       pull-requests: read |       pull-requests: read | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0 |       - uses: release-drafter/release-drafter@v6.1.0 | ||||||
|         env: |         env: | ||||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -23,10 +23,10 @@ jobs: | |||||||
|       contents: write # Required to upload release assets |       contents: write # Required to upload release assets | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|  |  | ||||||
|       - name: Set up Python ${{ env.PYTHON_VERSION }} |       - name: Set up Python ${{ env.PYTHON_VERSION }} | ||||||
|         uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 |         uses: actions/setup-python@v5 | ||||||
|         with: |         with: | ||||||
|           python-version: ${{ env.PYTHON_VERSION }} |           python-version: ${{ env.PYTHON_VERSION }} | ||||||
|  |  | ||||||
| @@ -34,7 +34,7 @@ jobs: | |||||||
|         uses: home-assistant/actions/helpers/verify-version@master |         uses: home-assistant/actions/helpers/verify-version@master | ||||||
|  |  | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v4.4.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -55,7 +55,7 @@ jobs: | |||||||
|           script/release |           script/release | ||||||
|  |  | ||||||
|       - name: Upload release assets |       - name: Upload release assets | ||||||
|         uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 |         uses: softprops/action-gh-release@v2.3.2 | ||||||
|         with: |         with: | ||||||
|           files: | |           files: | | ||||||
|             dist/*.whl |             dist/*.whl | ||||||
| @@ -73,9 +73,8 @@ jobs: | |||||||
|           version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' ) |           version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' ) | ||||||
|           echo "home-assistant-frontend==$version" > ./requirements.txt |           echo "home-assistant-frontend==$version" > ./requirements.txt | ||||||
|  |  | ||||||
|       # home-assistant/wheels doesn't support SHA pinning |  | ||||||
|       - name: Build wheels |       - name: Build wheels | ||||||
|         uses: home-assistant/wheels@2025.09.1 |         uses: home-assistant/wheels@2025.07.0 | ||||||
|         with: |         with: | ||||||
|           abi: cp313 |           abi: cp313 | ||||||
|           tag: musllinux_1_2 |           tag: musllinux_1_2 | ||||||
| @@ -91,9 +90,9 @@ jobs: | |||||||
|       contents: write # Required to upload release assets |       contents: write # Required to upload release assets | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v4.4.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -108,7 +107,7 @@ jobs: | |||||||
|       - name: Tar folder |       - name: Tar folder | ||||||
|         run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist . |         run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist . | ||||||
|       - name: Upload release asset |       - name: Upload release asset | ||||||
|         uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 |         uses: softprops/action-gh-release@v2.3.2 | ||||||
|         with: |         with: | ||||||
|           files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz |           files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz | ||||||
|  |  | ||||||
| @@ -120,9 +119,9 @@ jobs: | |||||||
|       contents: write # Required to upload release assets |       contents: write # Required to upload release assets | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|       - name: Setup Node |       - name: Setup Node | ||||||
|         uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 |         uses: actions/setup-node@v4.4.0 | ||||||
|         with: |         with: | ||||||
|           node-version-file: ".nvmrc" |           node-version-file: ".nvmrc" | ||||||
|           cache: yarn |           cache: yarn | ||||||
| @@ -137,6 +136,6 @@ jobs: | |||||||
|       - name: Tar folder |       - name: Tar folder | ||||||
|         run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build . |         run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build . | ||||||
|       - name: Upload release asset |       - name: Upload release asset | ||||||
|         uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 |         uses: softprops/action-gh-release@v2.3.2 | ||||||
|         with: |         with: | ||||||
|           files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz |           files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/restrict-task-creation.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/restrict-task-creation.yml
									
									
									
									
										vendored
									
									
								
							| @@ -12,7 +12,7 @@ jobs: | |||||||
|     if: github.event.issue.type.name == 'Task' |     if: github.event.issue.type.name == 'Task' | ||||||
|     steps: |     steps: | ||||||
|       - name: Check if user is authorized |       - name: Check if user is authorized | ||||||
|         uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 |         uses: actions/github-script@v7 | ||||||
|         with: |         with: | ||||||
|           script: | |           script: | | ||||||
|             const issueAuthor = context.payload.issue.user.login; |             const issueAuthor = context.payload.issue.user.login; | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -10,7 +10,7 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: 90 days stale policy |       - name: 90 days stale policy | ||||||
|         uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 |         uses: actions/stale@v9.1.0 | ||||||
|         with: |         with: | ||||||
|           repo-token: ${{ secrets.GITHUB_TOKEN }} |           repo-token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|           days-before-stale: 90 |           days-before-stale: 90 | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/translations.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/translations.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -14,7 +14,7 @@ jobs: | |||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout the repository |       - name: Checkout the repository | ||||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 |         uses: actions/checkout@v5.0.0 | ||||||
|  |  | ||||||
|       - name: Upload Translations |       - name: Upload Translations | ||||||
|         run: | |         run: | | ||||||
|   | |||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -6,4 +6,4 @@ enableGlobalCache: false | |||||||
|  |  | ||||||
| nodeLinker: node-modules | nodeLinker: node-modules | ||||||
|  |  | ||||||
| yarnPath: .yarn/releases/yarn-4.10.3.cjs | yarnPath: .yarn/releases/yarn-4.9.4.cjs | ||||||
|   | |||||||
| @@ -183,6 +183,7 @@ module.exports.babelOptions = ({ | |||||||
|       include: /\/node_modules\//, |       include: /\/node_modules\//, | ||||||
|       exclude: [ |       exclude: [ | ||||||
|         "element-internals-polyfill", |         "element-internals-polyfill", | ||||||
|  |         "@shoelace-style", | ||||||
|         "@?lit(?:-labs|-element|-html)?", |         "@?lit(?:-labs|-element|-html)?", | ||||||
|       ].map((p) => new RegExp(`/node_modules/${p}/`)), |       ].map((p) => new RegExp(`/node_modules/${p}/`)), | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -242,7 +242,7 @@ class HcCast extends LitElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     .question:before { |     .question:before { | ||||||
|       border-radius: var(--ha-border-radius-sm); |       border-radius: 4px; | ||||||
|       position: absolute; |       position: absolute; | ||||||
|       top: 0; |       top: 0; | ||||||
|       right: 0; |       right: 0; | ||||||
|   | |||||||
| @@ -95,8 +95,7 @@ class HcLayout extends LitElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     .hero { |     .hero { | ||||||
|       border-radius: var(--ha-border-radius-sm) var(--ha-border-radius-sm) |       border-radius: 4px 4px 0 0; | ||||||
|         var(--ha-border-radius-square) var(--ha-border-radius-square); |  | ||||||
|     } |     } | ||||||
|     .subtitle { |     .subtitle { | ||||||
|       font-size: var(--ha-font-size-m); |       font-size: var(--ha-font-size-m); | ||||||
|   | |||||||
| @@ -5,17 +5,17 @@ const castContext = framework.CastReceiverContext.getInstance(); | |||||||
| const playerManager = castContext.getPlayerManager(); | const playerManager = castContext.getPlayerManager(); | ||||||
|  |  | ||||||
| playerManager.setMessageInterceptor( | playerManager.setMessageInterceptor( | ||||||
|   framework.messages.MessageType.LOAD, |   "LOAD" as framework.messages.MessageType.LOAD, | ||||||
|   (loadRequestData) => { |   (loadRequestData) => { | ||||||
|     const media = loadRequestData.media; |     const media = loadRequestData.media; | ||||||
|     // Special handling if it came from Google Assistant |     // Special handling if it came from Google Assistant | ||||||
|     if (media.entity) { |     if (media.entity) { | ||||||
|       media.contentId = media.entity; |       media.contentId = media.entity; | ||||||
|       media.streamType = framework.messages.StreamType.LIVE; |       media.streamType = "LIVE" as framework.messages.StreamType.LIVE; | ||||||
|       media.contentType = "application/vnd.apple.mpegurl"; |       media.contentType = "application/vnd.apple.mpegurl"; | ||||||
|       // @ts-ignore |       // @ts-ignore | ||||||
|       media.hlsVideoSegmentFormat = |       media.hlsVideoSegmentFormat = | ||||||
|         framework.messages.HlsVideoSegmentFormat.FMP4; |         "fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4; | ||||||
|     } |     } | ||||||
|     return loadRequestData; |     return loadRequestData; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -75,7 +75,7 @@ export const castDemoEntities: () => Entity[] = () => | |||||||
|         longitude: 4.8903147, |         longitude: 4.8903147, | ||||||
|         radius: 100, |         radius: 100, | ||||||
|         friendly_name: "Home", |         friendly_name: "Home", | ||||||
|         icon: "mdi:home", |         icon: "hass:home", | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     "input_number.harmonyvolume": { |     "input_number.harmonyvolume": { | ||||||
| @@ -88,7 +88,7 @@ export const castDemoEntities: () => Entity[] = () => | |||||||
|         step: 1, |         step: 1, | ||||||
|         mode: "slider", |         mode: "slider", | ||||||
|         friendly_name: "Volume", |         friendly_name: "Volume", | ||||||
|         icon: "mdi:volume-high", |         icon: "hass:volume-high", | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     "climate.upstairs": { |     "climate.upstairs": { | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => { | |||||||
|                 type: "weblink", |                 type: "weblink", | ||||||
|                 url: "/lovelace/climate", |                 url: "/lovelace/climate", | ||||||
|                 name: "Climate controls", |                 name: "Climate controls", | ||||||
|                 icon: "mdi:arrow-right", |                 icon: "hass:arrow-right", | ||||||
|               }, |               }, | ||||||
|             ], |             ], | ||||||
|           }, |           }, | ||||||
| @@ -76,7 +76,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => { | |||||||
|                 type: "weblink", |                 type: "weblink", | ||||||
|                 url: "/lovelace/overview", |                 url: "/lovelace/overview", | ||||||
|                 name: "Back", |                 name: "Back", | ||||||
|                 icon: "mdi:arrow-left", |                 icon: "hass:arrow-left", | ||||||
|               }, |               }, | ||||||
|             ], |             ], | ||||||
|           }, |           }, | ||||||
|   | |||||||
| @@ -40,7 +40,8 @@ const playDummyMedia = (viewTitle?: string) => { | |||||||
|   loadRequestData.media.contentId = |   loadRequestData.media.contentId = | ||||||
|     "https://cast.home-assistant.io/images/google-nest-hub.png"; |     "https://cast.home-assistant.io/images/google-nest-hub.png"; | ||||||
|   loadRequestData.media.contentType = "image/jpeg"; |   loadRequestData.media.contentType = "image/jpeg"; | ||||||
|   loadRequestData.media.streamType = framework.messages.StreamType.NONE; |   loadRequestData.media.streamType = | ||||||
|  |     "NONE" as framework.messages.StreamType.NONE; | ||||||
|   const metadata = new framework.messages.GenericMediaMetadata(); |   const metadata = new framework.messages.GenericMediaMetadata(); | ||||||
|   metadata.title = viewTitle; |   metadata.title = viewTitle; | ||||||
|   loadRequestData.media.metadata = metadata; |   loadRequestData.media.metadata = metadata; | ||||||
| @@ -89,7 +90,7 @@ const showMediaPlayer = () => { | |||||||
| const options = new framework.CastReceiverOptions(); | const options = new framework.CastReceiverOptions(); | ||||||
| options.disableIdleTimeout = true; | options.disableIdleTimeout = true; | ||||||
| options.customNamespaces = { | options.customNamespaces = { | ||||||
|   [CAST_NS]: framework.system.MessageType.JSON, |   [CAST_NS]: "json" as framework.system.MessageType.JSON, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| castContext.addCustomMessageListener( | castContext.addCustomMessageListener( | ||||||
| @@ -97,9 +98,7 @@ castContext.addCustomMessageListener( | |||||||
|   // @ts-ignore |   // @ts-ignore | ||||||
|   (ev: ReceivedMessage<HassMessage>) => { |   (ev: ReceivedMessage<HassMessage>) => { | ||||||
|     // We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller |     // We received a show Lovelace command, stop media from playing, hide media player and show Lovelace controller | ||||||
|     if ( |     if (playerManager.getPlayerState() !== "IDLE") { | ||||||
|       playerManager.getPlayerState() !== framework.messages.PlayerState.IDLE |  | ||||||
|     ) { |  | ||||||
|       playerManager.stop(); |       playerManager.stop(); | ||||||
|     } else { |     } else { | ||||||
|       showLovelaceController(); |       showLovelaceController(); | ||||||
| @@ -113,7 +112,7 @@ castContext.addCustomMessageListener( | |||||||
| const playerManager = castContext.getPlayerManager(); | const playerManager = castContext.getPlayerManager(); | ||||||
|  |  | ||||||
| playerManager.setMessageInterceptor( | playerManager.setMessageInterceptor( | ||||||
|   framework.messages.MessageType.LOAD, |   "LOAD" as framework.messages.MessageType.LOAD, | ||||||
|   (loadRequestData) => { |   (loadRequestData) => { | ||||||
|     if ( |     if ( | ||||||
|       loadRequestData.media.contentId === |       loadRequestData.media.contentId === | ||||||
| @@ -127,24 +126,23 @@ playerManager.setMessageInterceptor( | |||||||
|     // Special handling if it came from Google Assistant |     // Special handling if it came from Google Assistant | ||||||
|     if (media.entity) { |     if (media.entity) { | ||||||
|       media.contentId = media.entity; |       media.contentId = media.entity; | ||||||
|       media.streamType = framework.messages.StreamType.LIVE; |       media.streamType = "LIVE" as framework.messages.StreamType.LIVE; | ||||||
|       media.contentType = "application/vnd.apple.mpegurl"; |       media.contentType = "application/vnd.apple.mpegurl"; | ||||||
|       // @ts-ignore |       // @ts-ignore | ||||||
|       media.hlsVideoSegmentFormat = |       media.hlsVideoSegmentFormat = | ||||||
|         framework.messages.HlsVideoSegmentFormat.FMP4; |         "fmp4" as framework.messages.HlsVideoSegmentFormat.FMP4; | ||||||
|     } |     } | ||||||
|     return loadRequestData; |     return loadRequestData; | ||||||
|   } |   } | ||||||
| ); | ); | ||||||
|  |  | ||||||
| playerManager.addEventListener( | playerManager.addEventListener( | ||||||
|   framework.events.EventType.MEDIA_STATUS, |   "MEDIA_STATUS" as framework.events.EventType.MEDIA_STATUS, | ||||||
|   (event) => { |   (event) => { | ||||||
|     if ( |     if ( | ||||||
|       event.mediaStatus?.playerState === framework.messages.PlayerState.IDLE && |       event.mediaStatus?.playerState === "IDLE" && | ||||||
|       event.mediaStatus?.idleReason && |       event.mediaStatus?.idleReason && | ||||||
|       event.mediaStatus?.idleReason !== |       event.mediaStatus?.idleReason !== "INTERRUPTED" | ||||||
|         framework.messages.IdleReason.INTERRUPTED |  | ||||||
|     ) { |     ) { | ||||||
|       // media finished or stopped, return to default Lovelace |       // media finished or stopped, return to default Lovelace | ||||||
|       showLovelaceController(); |       showLovelaceController(); | ||||||
|   | |||||||
| @@ -143,7 +143,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) => | |||||||
|       state: "on", |       state: "on", | ||||||
|       attributes: { |       attributes: { | ||||||
|         friendly_name: "Home Automation", |         friendly_name: "Home Automation", | ||||||
|         icon: "mdi:home-automation", |         icon: "hass:home-automation", | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     "input_boolean.tvtime": { |     "input_boolean.tvtime": { | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({ | |||||||
|   title: "Home Assistant", |   title: "Home Assistant", | ||||||
|   views: [ |   views: [ | ||||||
|     { |     { | ||||||
|       icon: "mdi:home-assistant", |       icon: "hass:home-assistant", | ||||||
|       id: "home", |       id: "home", | ||||||
|       title: "Home", |       title: "Home", | ||||||
|       cards: [ |       cards: [ | ||||||
|   | |||||||
| @@ -1236,7 +1236,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({ | |||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
|       path: "security", |       path: "security", | ||||||
|       icon: "mdi:shield-home", |       icon: "hass:shield-home", | ||||||
|       name: "Security", |       name: "Security", | ||||||
|       background: |       background: | ||||||
|         'center / cover no-repeat url("/assets/jimpower/background-15.jpg") fixed', |         'center / cover no-repeat url("/assets/jimpower/background-15.jpg") fixed', | ||||||
|   | |||||||
| @@ -68,7 +68,7 @@ | |||||||
|       } |       } | ||||||
|       #ha-launch-screen .ha-launch-screen-spacer-top { |       #ha-launch-screen .ha-launch-screen-spacer-top { | ||||||
|         flex: 1; |         flex: 1; | ||||||
|         margin-top: calc( 2 * max(var(--safe-area-inset-top, 0px), 48px) + 46px ); |         margin-top: calc( 2 * max(var(--safe-area-inset-bottom, 0px), 48px) + 46px ); | ||||||
|         padding-top: 48px; |         padding-top: 48px; | ||||||
|       } |       } | ||||||
|       #ha-launch-screen .ha-launch-screen-spacer-bottom { |       #ha-launch-screen .ha-launch-screen-spacer-bottom { | ||||||
|   | |||||||
| @@ -1,11 +1,10 @@ | |||||||
| import { LitElement, css, html, nothing } from "lit"; | import { LitElement, css, html } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| import "../../../src/components/ha-card"; | import "../../../src/components/ha-card"; | ||||||
| import "../../../src/dialogs/more-info/more-info-content"; | import "../../../src/dialogs/more-info/more-info-content"; | ||||||
| import "../../../src/state-summary/state-card-content"; | import "../../../src/state-summary/state-card-content"; | ||||||
| import "../ha-demo-options"; | import "../ha-demo-options"; | ||||||
| import type { HomeAssistant } from "../../../src/types"; | import type { HomeAssistant } from "../../../src/types"; | ||||||
| import { computeShowNewMoreInfo } from "../../../src/dialogs/more-info/const"; |  | ||||||
|  |  | ||||||
| @customElement("demo-more-info") | @customElement("demo-more-info") | ||||||
| class DemoMoreInfo extends LitElement { | class DemoMoreInfo extends LitElement { | ||||||
| @@ -22,13 +21,11 @@ class DemoMoreInfo extends LitElement { | |||||||
|       <div class="root"> |       <div class="root"> | ||||||
|         <div id="card"> |         <div id="card"> | ||||||
|           <ha-card> |           <ha-card> | ||||||
|             ${!computeShowNewMoreInfo(state) |             <state-card-content | ||||||
|               ? html`<state-card-content |  | ||||||
|               .stateObj=${state} |               .stateObj=${state} | ||||||
|               .hass=${this.hass} |               .hass=${this.hass} | ||||||
|               in-dialog |               in-dialog | ||||||
|                 ></state-card-content>` |             ></state-card-content> | ||||||
|               : nothing} |  | ||||||
|  |  | ||||||
|             <more-info-content |             <more-info-content | ||||||
|               .hass=${this.hass} |               .hass=${this.hass} | ||||||
|   | |||||||
| @@ -17,10 +17,6 @@ export const createMediaPlayerEntities = () => [ | |||||||
|       new Date().getTime() - 23000 |       new Date().getTime() - 23000 | ||||||
|     ).toISOString(), |     ).toISOString(), | ||||||
|     volume_level: 0.5, |     volume_level: 0.5, | ||||||
|     source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"], |  | ||||||
|     source: "AirPlay", |  | ||||||
|     sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"], |  | ||||||
|     sound_mode: "Music", |  | ||||||
|   }), |   }), | ||||||
|   getEntity("media_player", "music_playing", "playing", { |   getEntity("media_player", "music_playing", "playing", { | ||||||
|     friendly_name: "Playing The Music", |     friendly_name: "Playing The Music", | ||||||
| @@ -28,8 +24,8 @@ export const createMediaPlayerEntities = () => [ | |||||||
|     media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", |     media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", | ||||||
|     media_artist: "Technohead", |     media_artist: "Technohead", | ||||||
|     // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + |     // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + | ||||||
|     // Select Source + Stop + Clear + Play + Shuffle Set + Browse Media + Grouping |     // Select Source + Stop + Clear + Play + Shuffle Set + Browse Media | ||||||
|     supported_features: 784959, |     supported_features: 195135, | ||||||
|     entity_picture: "/images/album_cover.jpg", |     entity_picture: "/images/album_cover.jpg", | ||||||
|     media_duration: 300, |     media_duration: 300, | ||||||
|     media_position: 0, |     media_position: 0, | ||||||
| @@ -38,9 +34,6 @@ export const createMediaPlayerEntities = () => [ | |||||||
|       new Date().getTime() - 23000 |       new Date().getTime() - 23000 | ||||||
|     ).toISOString(), |     ).toISOString(), | ||||||
|     volume_level: 0.5, |     volume_level: 0.5, | ||||||
|     sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"], |  | ||||||
|     sound_mode: "Music", |  | ||||||
|     group_members: ["media_player.playing", "media_player.stream_playing"], |  | ||||||
|   }), |   }), | ||||||
|   getEntity("media_player", "stream_playing", "playing", { |   getEntity("media_player", "stream_playing", "playing", { | ||||||
|     friendly_name: "Playing the Stream", |     friendly_name: "Playing the Stream", | ||||||
| @@ -156,18 +149,15 @@ export const createMediaPlayerEntities = () => [ | |||||||
|   }), |   }), | ||||||
|   getEntity("media_player", "receiver_on", "on", { |   getEntity("media_player", "receiver_on", "on", { | ||||||
|     source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"], |     source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"], | ||||||
|     sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"], |  | ||||||
|     volume_level: 0.63, |     volume_level: 0.63, | ||||||
|     is_volume_muted: false, |     is_volume_muted: false, | ||||||
|     source: "TV", |     source: "TV", | ||||||
|     sound_mode: "Movie", |  | ||||||
|     friendly_name: "Receiver (selectable sources)", |     friendly_name: "Receiver (selectable sources)", | ||||||
|     // Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode |     // Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode | ||||||
|     supported_features: 84364, |     supported_features: 84364, | ||||||
|   }), |   }), | ||||||
|   getEntity("media_player", "receiver_off", "off", { |   getEntity("media_player", "receiver_off", "off", { | ||||||
|     source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"], |     source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"], | ||||||
|     sound_mode_list: ["Movie", "Music", "Game", "Pure Audio"], |  | ||||||
|     friendly_name: "Receiver (selectable sources)", |     friendly_name: "Receiver (selectable sources)", | ||||||
|     // Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode |     // Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode | ||||||
|     supported_features: 84364, |     supported_features: 84364, | ||||||
|   | |||||||
| @@ -208,7 +208,7 @@ class HaGallery extends LitElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       .sidebar a[active]::before { |       .sidebar a[active]::before { | ||||||
|         border-radius: var(--ha-border-radius-lg); |         border-radius: 12px; | ||||||
|         position: absolute; |         position: absolute; | ||||||
|         top: 0; |         top: 0; | ||||||
|         right: 2px; |         right: 2px; | ||||||
| @@ -241,7 +241,7 @@ class HaGallery extends LitElement { | |||||||
|         text-align: center; |         text-align: center; | ||||||
|         margin: 16px; |         margin: 16px; | ||||||
|         padding: 16px; |         padding: 16px; | ||||||
|         border-radius: var(--ha-border-radius-lg); |         border-radius: 12px; | ||||||
|         background-color: var(--primary-background-color); |         background-color: var(--primary-background-color); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -117,7 +117,7 @@ export class DemoHaBadge extends LitElement { | |||||||
|     } |     } | ||||||
|     .card-content { |     .card-content { | ||||||
|       display: flex; |       display: flex; | ||||||
|       gap: var(--ha-space-6); |       gap: 24px; | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -155,11 +155,11 @@ export class DemoHaButton extends LitElement { | |||||||
|     .card-content { |     .card-content { | ||||||
|       display: flex; |       display: flex; | ||||||
|       flex-direction: column; |       flex-direction: column; | ||||||
|       gap: var(--ha-space-6); |       gap: 24px; | ||||||
|     } |     } | ||||||
|     .card-content div { |     .card-content div { | ||||||
|       display: flex; |       display: flex; | ||||||
|       gap: var(--ha-space-2); |       gap: 8px; | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,10 +9,10 @@ import { css, html, LitElement } from "lit"; | |||||||
| import { customElement } from "lit/decorators"; | import { customElement } from "lit/decorators"; | ||||||
| import { ifDefined } from "lit/directives/if-defined"; | import { ifDefined } from "lit/directives/if-defined"; | ||||||
| import { repeat } from "lit/directives/repeat"; | import { repeat } from "lit/directives/repeat"; | ||||||
| import "../../../../src/components/ha-card"; |  | ||||||
| import "../../../../src/components/ha-control-button"; | import "../../../../src/components/ha-control-button"; | ||||||
| import "../../../../src/components/ha-control-button-group"; | import "../../../../src/components/ha-card"; | ||||||
| import "../../../../src/components/ha-svg-icon"; | import "../../../../src/components/ha-svg-icon"; | ||||||
|  | import "../../../../src/components/ha-control-button-group"; | ||||||
|  |  | ||||||
| interface Button { | interface Button { | ||||||
|   label: string; |   label: string; | ||||||
| @@ -156,17 +156,17 @@ export class DemoHaBarButton extends LitElement { | |||||||
|       --control-button-icon-color: var(--primary-color); |       --control-button-icon-color: var(--primary-color); | ||||||
|       --control-button-background-color: var(--primary-color); |       --control-button-background-color: var(--primary-color); | ||||||
|       --control-button-background-opacity: 0.2; |       --control-button-background-opacity: 0.2; | ||||||
|       --control-button-border-radius: var(--ha-border-radius-xl); |       --control-button-border-radius: 18px; | ||||||
|       height: 100px; |       height: 100px; | ||||||
|       width: 100px; |       width: 100px; | ||||||
|     } |     } | ||||||
|     .custom-group { |     .custom-group { | ||||||
|       --control-button-group-thickness: 100px; |       --control-button-group-thickness: 100px; | ||||||
|       --control-button-group-border-radius: var(--ha-border-radius-6xl); |       --control-button-group-border-radius: 36px; | ||||||
|       --control-button-group-spacing: 20px; |       --control-button-group-spacing: 20px; | ||||||
|     } |     } | ||||||
|     .custom-group ha-control-button { |     .custom-group ha-control-button { | ||||||
|       --control-button-border-radius: var(--ha-border-radius-xl); |       --control-button-border-radius: 18px; | ||||||
|       --mdc-icon-size: 32px; |       --mdc-icon-size: 32px; | ||||||
|     } |     } | ||||||
|     .vertical-buttons { |     .vertical-buttons { | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| import type { TemplateResult } from "lit"; | import type { TemplateResult } from "lit"; | ||||||
| import { LitElement, css, html } from "lit"; | import { LitElement, css, html } from "lit"; | ||||||
| import { customElement, state } from "lit/decorators"; | import { customElement, state } from "lit/decorators"; | ||||||
| import { ifDefined } from "lit/directives/if-defined"; |  | ||||||
| import { repeat } from "lit/directives/repeat"; |  | ||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
| import "../../../../src/components/ha-control-number-buttons"; | import "../../../../src/components/ha-control-number-buttons"; | ||||||
|  | import { repeat } from "lit/directives/repeat"; | ||||||
|  | import { ifDefined } from "lit/directives/if-defined"; | ||||||
|  |  | ||||||
| const buttons: { | const buttons: { | ||||||
|   id: string; |   id: string; | ||||||
| @@ -94,7 +94,7 @@ export class DemoHarControlNumberButtons extends LitElement { | |||||||
|       --control-number-buttons-background-color: #2196f3; |       --control-number-buttons-background-color: #2196f3; | ||||||
|       --control-number-buttons-background-opacity: 0.1; |       --control-number-buttons-background-opacity: 0.1; | ||||||
|       --control-number-buttons-thickness: 100px; |       --control-number-buttons-thickness: 100px; | ||||||
|       --control-number-buttons-border-radius: var(--ha-border-radius-6xl); |       --control-number-buttons-border-radius: 36px; | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -131,7 +131,7 @@ export class DemoHaControlSelectMenu extends LitElement { | |||||||
|       --control-button-icon-color: var(--primary-color); |       --control-button-icon-color: var(--primary-color); | ||||||
|       --control-button-background-color: var(--primary-color); |       --control-button-background-color: var(--primary-color); | ||||||
|       --control-button-background-opacity: 0.2; |       --control-button-background-opacity: 0.2; | ||||||
|       --control-button-border-radius: var(--ha-border-radius-xl); |       --control-button-border-radius: 18px; | ||||||
|       height: 100px; |       height: 100px; | ||||||
|       width: 100px; |       width: 100px; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -187,7 +187,7 @@ export class DemoHaControlSelect extends LitElement { | |||||||
|       --mdc-icon-size: 24px; |       --mdc-icon-size: 24px; | ||||||
|       --control-select-color: var(--state-fan-active-color); |       --control-select-color: var(--state-fan-active-color); | ||||||
|       --control-select-thickness: 130px; |       --control-select-thickness: 130px; | ||||||
|       --control-select-border-radius: var(--ha-border-radius-6xl); |       --control-select-border-radius: 36px; | ||||||
|     } |     } | ||||||
|     .vertical-selects { |     .vertical-selects { | ||||||
|       height: 300px; |       height: 300px; | ||||||
|   | |||||||
| @@ -3,8 +3,8 @@ import { css, html, LitElement } from "lit"; | |||||||
| import { customElement, state } from "lit/decorators"; | import { customElement, state } from "lit/decorators"; | ||||||
| import { ifDefined } from "lit/directives/if-defined"; | import { ifDefined } from "lit/directives/if-defined"; | ||||||
| import { repeat } from "lit/directives/repeat"; | import { repeat } from "lit/directives/repeat"; | ||||||
| import "../../../../src/components/ha-card"; |  | ||||||
| import "../../../../src/components/ha-control-slider"; | import "../../../../src/components/ha-control-slider"; | ||||||
|  | import "../../../../src/components/ha-card"; | ||||||
|  |  | ||||||
| const sliders: { | const sliders: { | ||||||
|   id: string; |   id: string; | ||||||
| @@ -151,7 +151,7 @@ export class DemoHaBarSlider extends LitElement { | |||||||
|       --control-slider-background: #ffcf4c; |       --control-slider-background: #ffcf4c; | ||||||
|       --control-slider-background-opacity: 0.2; |       --control-slider-background-opacity: 0.2; | ||||||
|       --control-slider-thickness: 130px; |       --control-slider-thickness: 130px; | ||||||
|       --control-slider-border-radius: var(--ha-border-radius-6xl); |       --control-slider-border-radius: 36px; | ||||||
|     } |     } | ||||||
|     .vertical-sliders { |     .vertical-sliders { | ||||||
|       height: 300px; |       height: 300px; | ||||||
|   | |||||||
| @@ -9,8 +9,8 @@ import { css, html, LitElement } from "lit"; | |||||||
| import { customElement, state } from "lit/decorators"; | import { customElement, state } from "lit/decorators"; | ||||||
| import { ifDefined } from "lit/directives/if-defined"; | import { ifDefined } from "lit/directives/if-defined"; | ||||||
| import { repeat } from "lit/directives/repeat"; | import { repeat } from "lit/directives/repeat"; | ||||||
| import "../../../../src/components/ha-card"; |  | ||||||
| import "../../../../src/components/ha-control-switch"; | import "../../../../src/components/ha-control-switch"; | ||||||
|  | import "../../../../src/components/ha-card"; | ||||||
|  |  | ||||||
| const switches: { | const switches: { | ||||||
|   id: string; |   id: string; | ||||||
| @@ -118,7 +118,7 @@ export class DemoHaControlSwitch extends LitElement { | |||||||
|       --control-switch-on-color: var(--green-color); |       --control-switch-on-color: var(--green-color); | ||||||
|       --control-switch-off-color: var(--red-color); |       --control-switch-off-color: var(--red-color); | ||||||
|       --control-switch-thickness: 130px; |       --control-switch-thickness: 130px; | ||||||
|       --control-switch-border-radius: var(--ha-border-radius-6xl); |       --control-switch-border-radius: 36px; | ||||||
|       --control-switch-padding: 6px; |       --control-switch-padding: 6px; | ||||||
|       --mdc-icon-size: 24px; |       --mdc-icon-size: 24px; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,37 +0,0 @@ | |||||||
| --- |  | ||||||
| title: Marquee Text |  | ||||||
| --- |  | ||||||
|  |  | ||||||
| # Marquee Text `<ha-marquee-text>` |  | ||||||
|  |  | ||||||
| Marquee text component scrolls text horizontally if it overflows its container. It supports pausing on hover and customizable speed and pause duration. |  | ||||||
|  |  | ||||||
| ## Implementation |  | ||||||
|  |  | ||||||
| ### Example Usage |  | ||||||
|  |  | ||||||
| <ha-marquee-text style="width: 200px;"> |  | ||||||
|     This is a long text that will scroll horizontally if it overflows the container. |  | ||||||
| </ha-marquee-text> |  | ||||||
|  |  | ||||||
| ```html |  | ||||||
| <ha-marquee-text style="width: 200px;"> |  | ||||||
|   This is a long text that will scroll horizontally if it overflows the |  | ||||||
|   container. |  | ||||||
| </ha-marquee-text> |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### API |  | ||||||
|  |  | ||||||
| **Slots** |  | ||||||
|  |  | ||||||
| - default slot: The text content to be displayed and scrolled. |  | ||||||
|   - no default |  | ||||||
|  |  | ||||||
| **Properties/Attributes** |  | ||||||
|  |  | ||||||
| | Name           | Type    | Default | Description                                                                  | |  | ||||||
| | -------------- | ------- | ------- | ---------------------------------------------------------------------------- | |  | ||||||
| | speed          | number  | `15`    | The speed of the scrolling animation. Higher values result in faster scroll. | |  | ||||||
| | pause-on-hover | boolean | `true`  | Whether to pause the scrolling animation when                                | |  | ||||||
| | pause-duration | number  | `1000`  | The delay in milliseconds before the scrolling animation starts/restarts.    | |  | ||||||
| @@ -1,25 +0,0 @@ | |||||||
| import { css, LitElement } from "lit"; |  | ||||||
| import { customElement } from "lit/decorators"; |  | ||||||
| import "../../../../src/components/ha-card"; |  | ||||||
| import "../../../../src/components/ha-marquee-text"; |  | ||||||
|  |  | ||||||
| @customElement("demo-components-ha-marquee-text") |  | ||||||
| export class DemoHaMarqueeText extends LitElement { |  | ||||||
|   static styles = css` |  | ||||||
|     ha-card { |  | ||||||
|       max-width: 600px; |  | ||||||
|       margin: 24px auto; |  | ||||||
|     } |  | ||||||
|     .card-content { |  | ||||||
|       display: flex; |  | ||||||
|       flex-direction: column; |  | ||||||
|       align-items: flex-start; |  | ||||||
|     } |  | ||||||
|   `; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "demo-components-ha-marquee-text": DemoHaMarqueeText; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -123,11 +123,11 @@ export class DemoHaProgressButton extends LitElement { | |||||||
|     .card-content { |     .card-content { | ||||||
|       display: flex; |       display: flex; | ||||||
|       flex-direction: column; |       flex-direction: column; | ||||||
|       gap: var(--ha-space-6); |       gap: 24px; | ||||||
|     } |     } | ||||||
|     .card-content div { |     .card-content div { | ||||||
|       display: flex; |       display: flex; | ||||||
|       gap: var(--ha-space-2); |       gap: 8px; | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -131,7 +131,7 @@ export class DemoHaSelectBox extends LitElement { | |||||||
|       --mdc-icon-size: 24px; |       --mdc-icon-size: 24px; | ||||||
|       --control-select-color: var(--state-fan-active-color); |       --control-select-color: var(--state-fan-active-color); | ||||||
|       --control-select-thickness: 130px; |       --control-select-thickness: 130px; | ||||||
|       --control-select-border-radius: var(--ha-border-radius-6xl); |       --control-select-border-radius: 36px; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     p.title { |     p.title { | ||||||
|   | |||||||
| @@ -1,38 +0,0 @@ | |||||||
| --- |  | ||||||
| title: Slider |  | ||||||
| subtitle: A slider component for selecting a value from a range. |  | ||||||
| --- |  | ||||||
|  |  | ||||||
| <style> |  | ||||||
|   .wrapper { |  | ||||||
|     display: flex; |  | ||||||
|     gap: 24px; |  | ||||||
|   } |  | ||||||
| </style> |  | ||||||
|  |  | ||||||
| # Slider `<ha-slider>` |  | ||||||
|  |  | ||||||
| ## Implementation |  | ||||||
|  |  | ||||||
| ### Example Usage |  | ||||||
|  |  | ||||||
| <div class="wrapper"> |  | ||||||
|   <ha-slider size="small" with-markers min="0" max="8" value="4"></ha-slider> |  | ||||||
|   <ha-slider size="medium"></ha-slider> |  | ||||||
| </div> |  | ||||||
|  |  | ||||||
| ```html |  | ||||||
| <ha-slider size="small" with-markers min="0" max="8" value="4"></ha-slider> |  | ||||||
| <ha-slider size="medium"></ha-slider> |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### API |  | ||||||
|  |  | ||||||
| This component is based on the webawesome slider component. |  | ||||||
| Check the [webawesome documentation](https://webawesome.com/docs/components/slider/) for more details. |  | ||||||
|  |  | ||||||
| **CSS Custom Properties** |  | ||||||
|  |  | ||||||
| - `--ha-slider-track-size` - Height of the slider track. Defaults to `4px`. |  | ||||||
| - `--ha-slider-thumb-color` - Color of the slider thumb. Defaults to `var(--primary-color)`. |  | ||||||
| - `--ha-slider-indicator-color` - Color of the filled portion of the slider track. Defaults to `var(--primary-color)`. |  | ||||||
| @@ -1,100 +0,0 @@ | |||||||
| import type { TemplateResult } from "lit"; |  | ||||||
| import { css, html, LitElement } from "lit"; |  | ||||||
| import { customElement, property } from "lit/decorators"; |  | ||||||
| import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element"; |  | ||||||
| import "../../../../src/components/ha-bar"; |  | ||||||
| import "../../../../src/components/ha-card"; |  | ||||||
| import "../../../../src/components/ha-spinner"; |  | ||||||
| import "../../../../src/components/ha-slider"; |  | ||||||
| import type { HomeAssistant } from "../../../../src/types"; |  | ||||||
|  |  | ||||||
| @customElement("demo-components-ha-slider") |  | ||||||
| export class DemoHaSlider extends LitElement { |  | ||||||
|   @property({ attribute: false }) hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |  | ||||||
|     return html` |  | ||||||
|       ${["light", "dark"].map( |  | ||||||
|         (mode) => html` |  | ||||||
|           <div class=${mode}> |  | ||||||
|             <ha-card header="ha-slider ${mode} demo"> |  | ||||||
|               <div class="card-content"> |  | ||||||
|                 <span>Default (disabled)</span> |  | ||||||
|                 <ha-slider |  | ||||||
|                   disabled |  | ||||||
|                   min="0" |  | ||||||
|                   max="8" |  | ||||||
|                   value="4" |  | ||||||
|                   with-markers |  | ||||||
|                 ></ha-slider> |  | ||||||
|                 <span>Small</span> |  | ||||||
|                 <ha-slider |  | ||||||
|                   size="small" |  | ||||||
|                   min="0" |  | ||||||
|                   max="8" |  | ||||||
|                   value="4" |  | ||||||
|                   with-markers |  | ||||||
|                 ></ha-slider> |  | ||||||
|                 <span>Medium</span> |  | ||||||
|                 <ha-slider |  | ||||||
|                   size="medium" |  | ||||||
|                   min="0" |  | ||||||
|                   max="8" |  | ||||||
|                   value="4" |  | ||||||
|                   with-markers |  | ||||||
|                 ></ha-slider> |  | ||||||
|               </div> |  | ||||||
|             </ha-card> |  | ||||||
|           </div> |  | ||||||
|         ` |  | ||||||
|       )} |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   firstUpdated(changedProps) { |  | ||||||
|     super.firstUpdated(changedProps); |  | ||||||
|     applyThemesOnElement( |  | ||||||
|       this.shadowRoot!.querySelector(".dark"), |  | ||||||
|       { |  | ||||||
|         default_theme: "default", |  | ||||||
|         default_dark_theme: "default", |  | ||||||
|         themes: {}, |  | ||||||
|         darkMode: true, |  | ||||||
|         theme: "default", |  | ||||||
|       }, |  | ||||||
|       undefined, |  | ||||||
|       undefined, |  | ||||||
|       true |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static styles = css` |  | ||||||
|     :host { |  | ||||||
|       display: flex; |  | ||||||
|       justify-content: center; |  | ||||||
|     } |  | ||||||
|     .dark, |  | ||||||
|     .light { |  | ||||||
|       display: block; |  | ||||||
|       background-color: var(--primary-background-color); |  | ||||||
|       padding: 0 50px; |  | ||||||
|       margin: 16px; |  | ||||||
|       border-radius: var(--ha-border-radius-md); |  | ||||||
|     } |  | ||||||
|     ha-card { |  | ||||||
|       margin: 24px auto; |  | ||||||
|     } |  | ||||||
|     .card-content { |  | ||||||
|       display: flex; |  | ||||||
|       flex-direction: column; |  | ||||||
|       align-items: center; |  | ||||||
|       gap: var(--ha-space-6); |  | ||||||
|     } |  | ||||||
|   `; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "demo-components-ha-slider": DemoHaSlider; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -61,7 +61,7 @@ export class DemoHaSpinner extends LitElement { | |||||||
|       background-color: var(--primary-background-color); |       background-color: var(--primary-background-color); | ||||||
|       padding: 0 50px; |       padding: 0 50px; | ||||||
|       margin: 16px; |       margin: 16px; | ||||||
|       border-radius: var(--ha-border-radius-md); |       border-radius: 8px; | ||||||
|     } |     } | ||||||
|     ha-card { |     ha-card { | ||||||
|       margin: 24px auto; |       margin: 24px auto; | ||||||
| @@ -70,7 +70,7 @@ export class DemoHaSpinner extends LitElement { | |||||||
|       display: flex; |       display: flex; | ||||||
|       flex-direction: column; |       flex-direction: column; | ||||||
|       align-items: center; |       align-items: center; | ||||||
|       gap: var(--ha-space-6); |       gap: 24px; | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,23 +6,21 @@ A tooltip's target is its _first child element_, so you should only wrap one ele | |||||||
|  |  | ||||||
| Tooltips use `display: contents` so they won't interfere with how elements are positioned in a flex or grid layout. | Tooltips use `display: contents` so they won't interfere with how elements are positioned in a flex or grid layout. | ||||||
|  |  | ||||||
| <ha-button id="hover">Hover Me</ha-button> | <ha-tooltip content="This is a tooltip"> | ||||||
| <ha-tooltip for="hover"> |   <ha-button>Hover Me</ha-button> | ||||||
| This is a tooltip |  | ||||||
| </ha-tooltip> | </ha-tooltip> | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| <ha-button id="hover">Hover Me</ha-button> | <ha-tooltip content="This is a tooltip"> | ||||||
| <ha-tooltip for="hover"> |   <ha-button>Hover Me</ha-button> | ||||||
| This is a tooltip |  | ||||||
| </ha-tooltip> | </ha-tooltip> | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## Documentation | ## Documentation | ||||||
|  |  | ||||||
| This element is based on webawesome `wa-tooltip` it only sets some css tokens and has a custom show/hide animation. | This element is based on shoelace `sl-tooltip` it only sets some css tokens and has a custom show/hide animation. | ||||||
|  |  | ||||||
| <a href="https://webawesome.com/docs/components/tooltip/" target="_blank" rel="noopener noreferrer">Webawesome documentation</a> | <a href="https://shoelace.style/components/tooltip" target="_blank" rel="noopener noreferrer">Shoelace documentation</a> | ||||||
|  |  | ||||||
| ### HA style tokens | ### HA style tokens | ||||||
|  |  | ||||||
| @@ -30,7 +28,7 @@ In your theme settings use this without the prefixed `--`. | |||||||
|  |  | ||||||
| - `--ha-tooltip-border-radius` (Default: 4px) | - `--ha-tooltip-border-radius` (Default: 4px) | ||||||
| - `--ha-tooltip-arrow-size` (Default: 8px) | - `--ha-tooltip-arrow-size` (Default: 8px) | ||||||
| - `--wa-tooltip-font-family` (Default: `var(--ha-font-family-body)`) | - `--sl-tooltip-font-family` (Default: `var(--ha-font-family-body)`) | ||||||
| - `--ha-tooltip-font-size` (Default: `var(--ha-font-size-s)`) | - `--ha-tooltip-font-size` (Default: `var(--ha-font-size-s)`) | ||||||
| - `--wa-tooltip-font-weight` (Default: `var(--ha-font-weight-normal)`) | - `--sl-tooltip-font-weight` (Default: `var(--ha-font-weight-normal)`) | ||||||
| - `--wa-tooltip-line-height` (Default: `var(--ha-line-height-condensed)`) | - `--sl-tooltip-line-height` (Default: `var(--ha-line-height-condensed)`) | ||||||
|   | |||||||
| @@ -1,3 +0,0 @@ | |||||||
| --- |  | ||||||
| title: Dialog (ha-wa-dialog) |  | ||||||
| --- |  | ||||||
| @@ -1,523 +0,0 @@ | |||||||
| import { css, html, LitElement } from "lit"; |  | ||||||
| import { customElement, state } from "lit/decorators"; |  | ||||||
| import { mdiCog, mdiHelp } from "@mdi/js"; |  | ||||||
| import "../../../../src/components/ha-button"; |  | ||||||
| import "../../../../src/components/ha-card"; |  | ||||||
| import "../../../../src/components/ha-dialog-footer"; |  | ||||||
| import "../../../../src/components/ha-form/ha-form"; |  | ||||||
| import "../../../../src/components/ha-icon-button"; |  | ||||||
| import "../../../../src/components/ha-wa-dialog"; |  | ||||||
| import type { HaFormSchema } from "../../../../src/components/ha-form/types"; |  | ||||||
|  |  | ||||||
| const SCHEMA: HaFormSchema[] = [ |  | ||||||
|   { type: "string", name: "Name", default: "", autofocus: true }, |  | ||||||
|   { type: "string", name: "Email", default: "" }, |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| type DialogType = |  | ||||||
|   | false |  | ||||||
|   | "basic" |  | ||||||
|   | "basic-subtitle-below" |  | ||||||
|   | "basic-subtitle-above" |  | ||||||
|   | "form" |  | ||||||
|   | "actions"; |  | ||||||
|  |  | ||||||
| @customElement("demo-components-ha-wa-dialog") |  | ||||||
| export class DemoHaWaDialog extends LitElement { |  | ||||||
|   @state() private _openDialog: DialogType = false; |  | ||||||
|  |  | ||||||
|   protected render() { |  | ||||||
|     return html` |  | ||||||
|       <div class="content"> |  | ||||||
|         <h1>Dialog <code><ha-wa-dialog></code></h1> |  | ||||||
|  |  | ||||||
|         <p class="subtitle">Dialog component built with WebAwesome.</p> |  | ||||||
|  |  | ||||||
|         <h2>Demos</h2> |  | ||||||
|  |  | ||||||
|         <div class="buttons"> |  | ||||||
|           <ha-button @click=${this._handleOpenDialog("basic")} |  | ||||||
|             >Basic dialog</ha-button |  | ||||||
|           > |  | ||||||
|           <ha-button @click=${this._handleOpenDialog("basic-subtitle-below")} |  | ||||||
|             >Basic dialog with subtitle below</ha-button |  | ||||||
|           > |  | ||||||
|           <ha-button @click=${this._handleOpenDialog("basic-subtitle-above")} |  | ||||||
|             >Basic dialog with subtitle above</ha-button |  | ||||||
|           > |  | ||||||
|           <ha-button @click=${this._handleOpenDialog("form")} |  | ||||||
|             >Dialog with form</ha-button |  | ||||||
|           > |  | ||||||
|           <ha-button @click=${this._handleOpenDialog("actions")} |  | ||||||
|             >Dialog with actions</ha-button |  | ||||||
|           > |  | ||||||
|         </div> |  | ||||||
|  |  | ||||||
|         <ha-wa-dialog |  | ||||||
|           .open=${this._openDialog === "basic"} |  | ||||||
|           header-title="Basic dialog" |  | ||||||
|           @closed=${this._handleClosed} |  | ||||||
|         > |  | ||||||
|           <div>Dialog content</div> |  | ||||||
|         </ha-wa-dialog> |  | ||||||
|  |  | ||||||
|         <ha-wa-dialog |  | ||||||
|           .open=${this._openDialog === "basic-subtitle-below"} |  | ||||||
|           header-title="Basic dialog with subtitle" |  | ||||||
|           header-subtitle="This is a basic dialog with a subtitle below" |  | ||||||
|           @closed=${this._handleClosed} |  | ||||||
|         > |  | ||||||
|           <div>Dialog content</div> |  | ||||||
|         </ha-wa-dialog> |  | ||||||
|  |  | ||||||
|         <ha-wa-dialog |  | ||||||
|           .open=${this._openDialog === "basic-subtitle-above"} |  | ||||||
|           header-title="Dialog with subtitle above" |  | ||||||
|           header-subtitle="This is a basic dialog with a subtitle above" |  | ||||||
|           header-subtitle-position="above" |  | ||||||
|           @closed=${this._handleClosed} |  | ||||||
|         > |  | ||||||
|           <div>Dialog content</div> |  | ||||||
|         </ha-wa-dialog> |  | ||||||
|  |  | ||||||
|         <ha-wa-dialog |  | ||||||
|           .open=${this._openDialog === "form"} |  | ||||||
|           header-title="Dialog with form" |  | ||||||
|           header-subtitle="This is a dialog with a form and a footer" |  | ||||||
|           prevent-scrim-close |  | ||||||
|           @closed=${this._handleClosed} |  | ||||||
|         > |  | ||||||
|           <ha-form autofocus .schema=${SCHEMA}></ha-form> |  | ||||||
|           <ha-dialog-footer slot="footer"> |  | ||||||
|             <ha-button |  | ||||||
|               data-dialog="close" |  | ||||||
|               slot="secondaryAction" |  | ||||||
|               variant="plain" |  | ||||||
|               >Cancel</ha-button |  | ||||||
|             > |  | ||||||
|             <ha-button data-dialog="close" slot="primaryAction" variant="accent" |  | ||||||
|               >Submit</ha-button |  | ||||||
|             > |  | ||||||
|           </ha-dialog-footer> |  | ||||||
|         </ha-wa-dialog> |  | ||||||
|  |  | ||||||
|         <ha-wa-dialog |  | ||||||
|           .open=${this._openDialog === "actions"} |  | ||||||
|           header-title="Dialog with actions" |  | ||||||
|           header-subtitle="This is a dialog with header actions" |  | ||||||
|           @closed=${this._handleClosed} |  | ||||||
|         > |  | ||||||
|           <div slot="headerActionItems"> |  | ||||||
|             <ha-icon-button label="Settings" path=${mdiCog}></ha-icon-button> |  | ||||||
|             <ha-icon-button label="Help" path=${mdiHelp}></ha-icon-button> |  | ||||||
|           </div> |  | ||||||
|  |  | ||||||
|           <div>Dialog content</div> |  | ||||||
|         </ha-wa-dialog> |  | ||||||
|  |  | ||||||
|         <h2>Design</h2> |  | ||||||
|  |  | ||||||
|         <h3>Width</h3> |  | ||||||
|  |  | ||||||
|         <p>There are multiple widths available for the dialog.</p> |  | ||||||
|  |  | ||||||
|         <table> |  | ||||||
|           <thead> |  | ||||||
|             <tr> |  | ||||||
|               <th>Name</th> |  | ||||||
|               <th>Value</th> |  | ||||||
|             </tr> |  | ||||||
|           </thead> |  | ||||||
|           <tbody> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>small</code></td> |  | ||||||
|               <td><code>min(320px, var(--full-width))</code></td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>medium</code></td> |  | ||||||
|               <td><code>min(580px, var(--full-width))</code></td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>large</code></td> |  | ||||||
|               <td><code>min(720px, var(--full-width))</code></td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>full</code></td> |  | ||||||
|               <td><code>var(--full-width)</code></td> |  | ||||||
|             </tr> |  | ||||||
|           </tbody> |  | ||||||
|         </table> |  | ||||||
|  |  | ||||||
|         <p> |  | ||||||
|           <code>--full-width</code> is calculated based on the available width |  | ||||||
|           of the screen. 95vw is the maximum width of the dialog on a large |  | ||||||
|           screen, while on a small screen it is 100vw minus the safe area |  | ||||||
|           insets. |  | ||||||
|         </p> |  | ||||||
|  |  | ||||||
|         <p>Dialogs have a default width of <code>medium</code>.</p> |  | ||||||
|  |  | ||||||
|         <h3>Prevent scrim close</h3> |  | ||||||
|  |  | ||||||
|         <p> |  | ||||||
|           You can prevent the dialog from being closed by clicking the |  | ||||||
|           scrim/overlay. This is allowed by default. |  | ||||||
|         </p> |  | ||||||
|  |  | ||||||
|         <h3>Header</h3> |  | ||||||
|  |  | ||||||
|         <p>The header contains a title, a subtitle and action items.</p> |  | ||||||
|  |  | ||||||
|         <table> |  | ||||||
|           <thead> |  | ||||||
|             <tr> |  | ||||||
|               <th>Slot</th> |  | ||||||
|               <th>Description</th> |  | ||||||
|             </tr> |  | ||||||
|           </thead> |  | ||||||
|           <tbody> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>header</code></td> |  | ||||||
|               <td>The entire header area.</td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>headerTitle</code></td> |  | ||||||
|               <td>The header title text.</td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>headerSubtitle</code></td> |  | ||||||
|               <td>The header subtitle text.</td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>headerActionItems</code></td> |  | ||||||
|               <td>The header action items.</td> |  | ||||||
|             </tr> |  | ||||||
|           </tbody> |  | ||||||
|         </table> |  | ||||||
|  |  | ||||||
|         <h4>Header title</h4> |  | ||||||
|  |  | ||||||
|         <p>The header title is a text string.</p> |  | ||||||
|  |  | ||||||
|         <h4>Header subtitle</h4> |  | ||||||
|  |  | ||||||
|         <p>The header subtitle is a text string.</p> |  | ||||||
|  |  | ||||||
|         <h4>Header action items</h4> |  | ||||||
|  |  | ||||||
|         <p> |  | ||||||
|           The header action items usually containing icon buttons and/or menu |  | ||||||
|           buttons. |  | ||||||
|         </p> |  | ||||||
|  |  | ||||||
|         <h3>Body</h3> |  | ||||||
|  |  | ||||||
|         <p>The body is the content of the dialog.</p> |  | ||||||
|  |  | ||||||
|         <h3>Footer</h3> |  | ||||||
|  |  | ||||||
|         <p>The footer is the footer of the dialog.</p> |  | ||||||
|  |  | ||||||
|         <p> |  | ||||||
|           It is recommended to use the <code>ha-dialog-footer</code> component |  | ||||||
|           for the footer and to style the buttons inside the footer as so: |  | ||||||
|         </p> |  | ||||||
|  |  | ||||||
|         <table> |  | ||||||
|           <thead> |  | ||||||
|             <tr> |  | ||||||
|               <th>Slot</th> |  | ||||||
|               <th>Description</th> |  | ||||||
|               <th>Variant to use</th> |  | ||||||
|             </tr> |  | ||||||
|           </thead> |  | ||||||
|           <tbody> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>secondaryAction</code></td> |  | ||||||
|               <td>The secondary action button(s).</td> |  | ||||||
|               <td><code>plain</code></td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>primaryAction</code></td> |  | ||||||
|               <td>The primary action button(s).</td> |  | ||||||
|               <td><code>accent</code></td> |  | ||||||
|             </tr> |  | ||||||
|           </tbody> |  | ||||||
|         </table> |  | ||||||
|  |  | ||||||
|         <h2>Implementation</h2> |  | ||||||
|  |  | ||||||
|         <h3>Example Usage</h3> |  | ||||||
|  |  | ||||||
|         <pre><code><ha-wa-dialog |  | ||||||
|   open |  | ||||||
|   header-title="Dialog title" |  | ||||||
|   header-subtitle="Dialog subtitle" |  | ||||||
|   prevent-scrim-close |  | ||||||
| > |  | ||||||
|   <div slot="headerActionItems"> |  | ||||||
|     <ha-icon-button label="Settings" path="mdiCog"></ha-icon-button> |  | ||||||
|     <ha-icon-button label="Help" path="mdiHelp"></ha-icon-button> |  | ||||||
|   </div> |  | ||||||
|   <div>Dialog content</div> |  | ||||||
|   <ha-dialog-footer slot="footer"> |  | ||||||
|     <ha-button data-dialog="close" slot="secondaryAction" variant="plain" |  | ||||||
|       >Cancel</ha-button |  | ||||||
|     > |  | ||||||
|     <ha-button slot="primaryAction" variant="accent">Submit</ha-button> |  | ||||||
|   </ha-dialog-footer> |  | ||||||
| </ha-wa-dialog></code></pre> |  | ||||||
|  |  | ||||||
|         <h3>API</h3> |  | ||||||
|  |  | ||||||
|         <p> |  | ||||||
|           This component is based on the webawesome dialog component. Check the |  | ||||||
|           <a |  | ||||||
|             href="https://webawesome.com/docs/components/dialog/" |  | ||||||
|             target="_blank" |  | ||||||
|             rel="noopener noreferrer" |  | ||||||
|             >webawesome documentation</a |  | ||||||
|           > |  | ||||||
|           for more details. |  | ||||||
|         </p> |  | ||||||
|  |  | ||||||
|         <h4>Attributes</h4> |  | ||||||
|  |  | ||||||
|         <table> |  | ||||||
|           <thead> |  | ||||||
|             <tr> |  | ||||||
|               <th>Attribute</th> |  | ||||||
|               <th>Description</th> |  | ||||||
|               <th>Default</th> |  | ||||||
|               <th>Options</th> |  | ||||||
|             </tr> |  | ||||||
|           </thead> |  | ||||||
|           <tbody> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>open</code></td> |  | ||||||
|               <td>Controls the dialog open state.</td> |  | ||||||
|               <td><code>false</code></td> |  | ||||||
|               <td><code>false</code>, <code>true</code></td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>width</code></td> |  | ||||||
|               <td>Preferred dialog width preset.</td> |  | ||||||
|               <td><code>medium</code></td> |  | ||||||
|               <td> |  | ||||||
|                 <code>small</code>, <code>medium</code>, <code>large</code>, |  | ||||||
|                 <code>full</code> |  | ||||||
|               </td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>prevent-scrim-close</code></td> |  | ||||||
|               <td> |  | ||||||
|                 Prevents closing the dialog by clicking the scrim/overlay. |  | ||||||
|               </td> |  | ||||||
|               <td><code>false</code></td> |  | ||||||
|               <td><code>true</code></td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>header-title</code></td> |  | ||||||
|               <td>Header title text when no custom title slot is provided.</td> |  | ||||||
|               <td></td> |  | ||||||
|               <td></td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>header-subtitle</code></td> |  | ||||||
|               <td> |  | ||||||
|                 Header subtitle text when no custom subtitle slot is provided. |  | ||||||
|               </td> |  | ||||||
|               <td></td> |  | ||||||
|               <td></td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>header-subtitle-position</code></td> |  | ||||||
|               <td>Position of the subtitle relative to the title.</td> |  | ||||||
|               <td><code>below</code></td> |  | ||||||
|               <td><code>above</code>, <code>below</code></td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>flexcontent</code></td> |  | ||||||
|               <td> |  | ||||||
|                 Makes the dialog body a flex container for flexible layouts. |  | ||||||
|               </td> |  | ||||||
|               <td><code>false</code></td> |  | ||||||
|               <td><code>false</code>, <code>true</code></td> |  | ||||||
|             </tr> |  | ||||||
|           </tbody> |  | ||||||
|         </table> |  | ||||||
|  |  | ||||||
|         <h4>CSS Custom Properties</h4> |  | ||||||
|  |  | ||||||
|         <table> |  | ||||||
|           <thead> |  | ||||||
|             <tr> |  | ||||||
|               <th>CSS Property</th> |  | ||||||
|               <th>Description</th> |  | ||||||
|             </tr> |  | ||||||
|           </thead> |  | ||||||
|           <tbody> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>--dialog-content-padding</code></td> |  | ||||||
|               <td>Padding for dialog content sections.</td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>--ha-dialog-show-duration</code></td> |  | ||||||
|               <td>Show animation duration.</td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>--ha-dialog-hide-duration</code></td> |  | ||||||
|               <td>Hide animation duration.</td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>--ha-dialog-surface-background</code></td> |  | ||||||
|               <td>Dialog background color.</td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>--ha-dialog-border-radius</code></td> |  | ||||||
|               <td>Border radius of the dialog surface.</td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>--dialog-z-index</code></td> |  | ||||||
|               <td>Z-index for the dialog.</td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>--dialog-surface-position</code></td> |  | ||||||
|               <td>CSS position of the dialog surface.</td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>--dialog-surface-margin-top</code></td> |  | ||||||
|               <td>Top margin for the dialog surface.</td> |  | ||||||
|             </tr> |  | ||||||
|           </tbody> |  | ||||||
|         </table> |  | ||||||
|  |  | ||||||
|         <h4>Events</h4> |  | ||||||
|  |  | ||||||
|         <table> |  | ||||||
|           <thead> |  | ||||||
|             <tr> |  | ||||||
|               <th>Event</th> |  | ||||||
|               <th>Description</th> |  | ||||||
|             </tr> |  | ||||||
|           </thead> |  | ||||||
|           <tbody> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>opened</code></td> |  | ||||||
|               <td>Fired when the dialog is shown.</td> |  | ||||||
|             </tr> |  | ||||||
|             <tr> |  | ||||||
|               <td><code>closed</code></td> |  | ||||||
|               <td>Fired after the dialog is hidden.</td> |  | ||||||
|             </tr> |  | ||||||
|           </tbody> |  | ||||||
|         </table> |  | ||||||
|       </div> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _handleOpenDialog = (dialog: DialogType) => () => { |  | ||||||
|     this._openDialog = dialog; |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   private _handleClosed = () => { |  | ||||||
|     this._openDialog = false; |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   static styles = [ |  | ||||||
|     css` |  | ||||||
|       :host { |  | ||||||
|         display: block; |  | ||||||
|         padding: var(--ha-space-4); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .content { |  | ||||||
|         max-width: 1000px; |  | ||||||
|         margin: 0 auto; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       h1 { |  | ||||||
|         margin-top: 0; |  | ||||||
|         margin-bottom: var(--ha-space-2); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       h2 { |  | ||||||
|         margin-top: var(--ha-space-6); |  | ||||||
|         margin-bottom: var(--ha-space-3); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       h3, |  | ||||||
|       h4 { |  | ||||||
|         margin-top: var(--ha-space-4); |  | ||||||
|         margin-bottom: var(--ha-space-2); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       p { |  | ||||||
|         margin: var(--ha-space-2) 0; |  | ||||||
|         line-height: 1.6; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .subtitle { |  | ||||||
|         color: var(--secondary-text-color); |  | ||||||
|         font-size: 1.1em; |  | ||||||
|         margin-bottom: var(--ha-space-4); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       table { |  | ||||||
|         width: 100%; |  | ||||||
|         border-collapse: collapse; |  | ||||||
|         margin: var(--ha-space-3) 0; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       th, |  | ||||||
|       td { |  | ||||||
|         text-align: left; |  | ||||||
|         padding: var(--ha-space-2); |  | ||||||
|         border-bottom: 1px solid var(--divider-color); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       th { |  | ||||||
|         font-weight: 500; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       code { |  | ||||||
|         background-color: var(--secondary-background-color); |  | ||||||
|         padding: 2px 6px; |  | ||||||
|         border-radius: 4px; |  | ||||||
|         font-family: monospace; |  | ||||||
|         font-size: 0.9em; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       pre { |  | ||||||
|         background-color: var(--secondary-background-color); |  | ||||||
|         padding: var(--ha-space-3); |  | ||||||
|         border-radius: 8px; |  | ||||||
|         overflow-x: auto; |  | ||||||
|         margin: var(--ha-space-3) 0; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       pre code { |  | ||||||
|         background-color: transparent; |  | ||||||
|         padding: 0; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       .buttons { |  | ||||||
|         display: flex; |  | ||||||
|         flex-direction: row; |  | ||||||
|         flex-wrap: wrap; |  | ||||||
|         gap: var(--ha-space-2); |  | ||||||
|         margin: var(--ha-space-4) 0; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       a { |  | ||||||
|         color: var(--primary-color); |  | ||||||
|       } |  | ||||||
|     `, |  | ||||||
|   ]; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "demo-components-ha-wa-dialog": DemoHaWaDialog; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -5,13 +5,13 @@ import type { | |||||||
| import { css, html, LitElement, nothing } from "lit"; | import { css, html, LitElement, nothing } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { mockIcons } from "../../../../demo/src/stubs/icons"; |  | ||||||
| import { computeDomain } from "../../../../src/common/entity/compute_domain"; | import { computeDomain } from "../../../../src/common/entity/compute_domain"; | ||||||
| import { computeStateDisplay } from "../../../../src/common/entity/compute_state_display"; | import { computeStateDisplay } from "../../../../src/common/entity/compute_state_display"; | ||||||
| import "../../../../src/components/data-table/ha-data-table"; | import "../../../../src/components/data-table/ha-data-table"; | ||||||
| import type { DataTableColumnContainer } from "../../../../src/components/data-table/ha-data-table"; | import type { DataTableColumnContainer } from "../../../../src/components/data-table/ha-data-table"; | ||||||
| import "../../../../src/components/entity/state-badge"; | import "../../../../src/components/entity/state-badge"; | ||||||
| import { provideHass } from "../../../../src/fake_data/provide_hass"; | import { provideHass } from "../../../../src/fake_data/provide_hass"; | ||||||
|  | import { mockIcons } from "../../../../demo/src/stubs/icons"; | ||||||
| import type { HomeAssistant } from "../../../../src/types"; | import type { HomeAssistant } from "../../../../src/types"; | ||||||
|  |  | ||||||
| const SENSOR_DEVICE_CLASSES = [ | const SENSOR_DEVICE_CLASSES = [ | ||||||
| @@ -434,7 +434,7 @@ export class DemoEntityState extends LitElement { | |||||||
|       display: block; |       display: block; | ||||||
|       height: 20px; |       height: 20px; | ||||||
|       width: 20px; |       width: 20px; | ||||||
|       border-radius: var(--ha-border-radius-md); |       border-radius: 10px; | ||||||
|       background-color: rgb(--color); |       background-color: rgb(--color); | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
|   | |||||||
| @@ -11,10 +11,7 @@ import "../../../../src/components/ha-alert"; | |||||||
| import "../../../../src/components/ha-button-menu"; | import "../../../../src/components/ha-button-menu"; | ||||||
| import "../../../../src/components/ha-card"; | import "../../../../src/components/ha-card"; | ||||||
| import "../../../../src/components/ha-form/ha-form"; | import "../../../../src/components/ha-form/ha-form"; | ||||||
| import type { | import type { HaFormSchema } from "../../../../src/components/ha-form/types"; | ||||||
|   HaFormSchema, |  | ||||||
|   HaFormDataContainer, |  | ||||||
| } from "../../../../src/components/ha-form/types"; |  | ||||||
| import "../../../../src/components/ha-formfield"; | import "../../../../src/components/ha-formfield"; | ||||||
| import "../../../../src/components/ha-icon-button"; | import "../../../../src/components/ha-icon-button"; | ||||||
| import "../../../../src/components/ha-list-item"; | import "../../../../src/components/ha-list-item"; | ||||||
| @@ -36,7 +33,6 @@ import { haStyle } from "../../../../src/resources/styles"; | |||||||
| import type { HomeAssistant } from "../../../../src/types"; | import type { HomeAssistant } from "../../../../src/types"; | ||||||
| import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart"; | import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart"; | ||||||
| import { hassioStyle } from "../../resources/hassio-style"; | import { hassioStyle } from "../../resources/hassio-style"; | ||||||
| import type { ObjectSelector, Selector } from "../../../../src/data/selector"; |  | ||||||
|  |  | ||||||
| const SUPPORTED_UI_TYPES = [ | const SUPPORTED_UI_TYPES = [ | ||||||
|   "string", |   "string", | ||||||
| @@ -82,83 +78,41 @@ class HassioAddonConfig extends LitElement { | |||||||
|  |  | ||||||
|   @query("ha-yaml-editor") private _editor?: HaYamlEditor; |   @query("ha-yaml-editor") private _editor?: HaYamlEditor; | ||||||
|  |  | ||||||
|   private _getTranslationEntry( |   public computeLabel = (entry: HaFormSchema): string => | ||||||
|     language: string, |     this.addon.translations[this.hass.language]?.configuration?.[entry.name] | ||||||
|     entry: HaFormSchema, |       ?.name || | ||||||
|     options?: { path?: string[] } |     this.addon.translations.en?.configuration?.[entry.name]?.name || | ||||||
|   ) { |  | ||||||
|     let parent = this.addon.translations[language]?.configuration; |  | ||||||
|     if (!parent) return undefined; |  | ||||||
|     if (options?.path) { |  | ||||||
|       for (const key of options.path) { |  | ||||||
|         parent = parent[key]?.fields; |  | ||||||
|         if (!parent) return undefined; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return parent[entry.name]; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public computeLabel = ( |  | ||||||
|     entry: HaFormSchema, |  | ||||||
|     _data: HaFormDataContainer, |  | ||||||
|     options?: { path?: string[] } |  | ||||||
|   ): string => |  | ||||||
|     this._getTranslationEntry(this.hass.language, entry, options)?.name || |  | ||||||
|     this._getTranslationEntry("en", entry, options)?.name || |  | ||||||
|     entry.name; |     entry.name; | ||||||
|  |  | ||||||
|   public computeHelper = ( |   public computeHelper = (entry: HaFormSchema): string => | ||||||
|     entry: HaFormSchema, |     this.addon.translations[this.hass.language]?.configuration?.[entry.name] | ||||||
|     options?: { path?: string[] } |  | ||||||
|   ): string => |  | ||||||
|     this._getTranslationEntry(this.hass.language, entry, options) |  | ||||||
|       ?.description || |       ?.description || | ||||||
|     this._getTranslationEntry("en", entry, options)?.description || |     this.addon.translations.en?.configuration?.[entry.name]?.description || | ||||||
|     ""; |     ""; | ||||||
|  |  | ||||||
|   private _convertSchema = memoizeOne( |   private _convertSchema = memoizeOne( | ||||||
|     // Convert supervisor schema to selectors |     // Convert supervisor schema to selectors | ||||||
|     (schema: readonly HaFormSchema[]): HaFormSchema[] => |     (schema: Record<string, any>): HaFormSchema[] => | ||||||
|       this._convertSchemaElements(schema) |       schema.map((entry) => | ||||||
|   ); |         entry.type === "select" | ||||||
|  |           ? { | ||||||
|   private _convertSchemaElements( |  | ||||||
|     schema: readonly HaFormSchema[] |  | ||||||
|   ): HaFormSchema[] { |  | ||||||
|     return schema.map((entry) => this._convertSchemaElement(entry)); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _convertSchemaElement(entry: any): HaFormSchema { |  | ||||||
|     if (entry.type === "schema" && !entry.multiple) { |  | ||||||
|       return { |  | ||||||
|         name: entry.name, |  | ||||||
|         type: "expandable", |  | ||||||
|         required: entry.required, |  | ||||||
|         schema: this._convertSchemaElements(entry.schema), |  | ||||||
|       }; |  | ||||||
|     } |  | ||||||
|     const selector = this._convertSchemaElementToSelector(entry, false); |  | ||||||
|     if (selector) { |  | ||||||
|       return { |  | ||||||
|               name: entry.name, |               name: entry.name, | ||||||
|               required: entry.required, |               required: entry.required, | ||||||
|         selector, |               selector: { select: { options: entry.options } }, | ||||||
|       }; |  | ||||||
|             } |             } | ||||||
|     return entry; |           : entry.type === "string" | ||||||
|  |             ? entry.multiple | ||||||
|  |               ? { | ||||||
|  |                   name: entry.name, | ||||||
|  |                   required: entry.required, | ||||||
|  |                   selector: { | ||||||
|  |                     select: { options: [], multiple: true, custom_value: true }, | ||||||
|  |                   }, | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|   private _convertSchemaElementToSelector( |  | ||||||
|     entry: any, |  | ||||||
|     force: boolean |  | ||||||
|   ): Selector | null { |  | ||||||
|     if (entry.type === "select") { |  | ||||||
|       return { select: { options: entry.options } }; |  | ||||||
|     } |  | ||||||
|     if (entry.type === "string") { |  | ||||||
|       return entry.multiple |  | ||||||
|         ? { select: { options: [], multiple: true, custom_value: true } } |  | ||||||
|               : { |               : { | ||||||
|  |                   name: entry.name, | ||||||
|  |                   required: entry.required, | ||||||
|  |                   selector: { | ||||||
|                     text: { |                     text: { | ||||||
|                       type: entry.format |                       type: entry.format | ||||||
|                         ? entry.format |                         ? entry.format | ||||||
| @@ -166,41 +120,36 @@ class HassioAddonConfig extends LitElement { | |||||||
|                           ? "password" |                           ? "password" | ||||||
|                           : "text", |                           : "text", | ||||||
|                     }, |                     }, | ||||||
|           }; |  | ||||||
|     } |  | ||||||
|     if (entry.type === "boolean") { |  | ||||||
|       return { boolean: {} }; |  | ||||||
|     } |  | ||||||
|     if (entry.type === "schema") { |  | ||||||
|       const fields: NonNullable<ObjectSelector["object"]>["fields"] = {}; |  | ||||||
|       for (const child_entry of entry.schema) { |  | ||||||
|         fields[child_entry.name] = { |  | ||||||
|           required: child_entry.required, |  | ||||||
|           selector: this._convertSchemaElementToSelector(child_entry, true)!, |  | ||||||
|         }; |  | ||||||
|       } |  | ||||||
|       return { |  | ||||||
|         object: { |  | ||||||
|           multiple: entry.multiple, |  | ||||||
|           fields, |  | ||||||
|                   }, |                   }, | ||||||
|       }; |  | ||||||
|                 } |                 } | ||||||
|     if (entry.type === "float" || entry.type === "integer") { |             : entry.type === "boolean" | ||||||
|       return { |               ? { | ||||||
|  |                   name: entry.name, | ||||||
|  |                   required: entry.required, | ||||||
|  |                   selector: { boolean: {} }, | ||||||
|  |                 } | ||||||
|  |               : entry.type === "schema" | ||||||
|  |                 ? { | ||||||
|  |                     name: entry.name, | ||||||
|  |                     required: entry.required, | ||||||
|  |                     selector: { object: {} }, | ||||||
|  |                   } | ||||||
|  |                 : entry.type === "float" || entry.type === "integer" | ||||||
|  |                   ? { | ||||||
|  |                       name: entry.name, | ||||||
|  |                       required: entry.required, | ||||||
|  |                       selector: { | ||||||
|                         number: { |                         number: { | ||||||
|                           mode: "box", |                           mode: "box", | ||||||
|                           step: entry.type === "float" ? "any" : undefined, |                           step: entry.type === "float" ? "any" : undefined, | ||||||
|                         }, |                         }, | ||||||
|       }; |                       }, | ||||||
|     } |  | ||||||
|     if (force) { |  | ||||||
|       return { object: {} }; |  | ||||||
|     } |  | ||||||
|     return null; |  | ||||||
|                     } |                     } | ||||||
|  |                   : entry | ||||||
|  |       ) | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   private _filteredSchema = memoizeOne( |   private _filteredShchema = memoizeOne( | ||||||
|     (options: Record<string, unknown>, schema: HaFormSchema[]) => |     (options: Record<string, unknown>, schema: HaFormSchema[]) => | ||||||
|       schema.filter((entry) => entry.name in options || entry.required) |       schema.filter((entry) => entry.name in options || entry.required) | ||||||
|   ); |   ); | ||||||
| @@ -212,7 +161,7 @@ class HassioAddonConfig extends LitElement { | |||||||
|       showForm && |       showForm && | ||||||
|       JSON.stringify(this.addon.schema) !== |       JSON.stringify(this.addon.schema) !== | ||||||
|         JSON.stringify( |         JSON.stringify( | ||||||
|           this._filteredSchema(this.addon.options, this.addon.schema!) |           this._filteredShchema(this.addon.options, this.addon.schema!) | ||||||
|         ); |         ); | ||||||
|     return html` |     return html` | ||||||
|       <h1>${this.addon.name}</h1> |       <h1>${this.addon.name}</h1> | ||||||
| @@ -250,7 +199,6 @@ class HassioAddonConfig extends LitElement { | |||||||
|         <div class="card-content"> |         <div class="card-content"> | ||||||
|           ${showForm |           ${showForm | ||||||
|             ? html`<ha-form |             ? html`<ha-form | ||||||
|                 .hass=${this.hass} |  | ||||||
|                 .disabled=${this.disabled} |                 .disabled=${this.disabled} | ||||||
|                 .data=${this._options!} |                 .data=${this._options!} | ||||||
|                 @value-changed=${this._configChanged} |                 @value-changed=${this._configChanged} | ||||||
| @@ -259,7 +207,7 @@ class HassioAddonConfig extends LitElement { | |||||||
|                 .schema=${this._convertSchema( |                 .schema=${this._convertSchema( | ||||||
|                   this._showOptional |                   this._showOptional | ||||||
|                     ? this.addon.schema! |                     ? this.addon.schema! | ||||||
|                     : this._filteredSchema( |                     : this._filteredShchema( | ||||||
|                         this.addon.options, |                         this.addon.options, | ||||||
|                         this.addon.schema! |                         this.addon.schema! | ||||||
|                       ) |                       ) | ||||||
|   | |||||||
| @@ -781,7 +781,7 @@ class HassioAddonInfo extends LitElement { | |||||||
|  |  | ||||||
|       ${this.addon.long_description |       ${this.addon.long_description | ||||||
|         ? html` |         ? html` | ||||||
|             <ha-card class="long-description" outlined> |             <ha-card outlined> | ||||||
|               <div class="card-content"> |               <div class="card-content"> | ||||||
|                 <ha-markdown |                 <ha-markdown | ||||||
|                   .content=${this.addon.long_description} |                   .content=${this.addon.long_description} | ||||||
| @@ -1333,9 +1333,6 @@ class HassioAddonInfo extends LitElement { | |||||||
|         .description a { |         .description a { | ||||||
|           color: var(--primary-color); |           color: var(--primary-color); | ||||||
|         } |         } | ||||||
|         .long-description { |  | ||||||
|           direction: ltr; |  | ||||||
|         } |  | ||||||
|         ha-assist-chip { |         ha-assist-chip { | ||||||
|           --md-sys-color-primary: var(--text-primary-color); |           --md-sys-color-primary: var(--text-primary-color); | ||||||
|           --md-sys-color-on-surface: var(--text-primary-color); |           --md-sys-color-on-surface: var(--text-primary-color); | ||||||
|   | |||||||
| @@ -121,7 +121,7 @@ class HassioCardContent extends LitElement { | |||||||
|       height: 12px; |       height: 12px; | ||||||
|       top: 8px; |       top: 8px; | ||||||
|       right: 8px; |       right: 8px; | ||||||
|       border-radius: var(--ha-border-radius-circle); |       border-radius: 50%; | ||||||
|     } |     } | ||||||
|     .topbar { |     .topbar { | ||||||
|       position: absolute; |       position: absolute; | ||||||
|   | |||||||
| @@ -164,7 +164,7 @@ class HassioHardwareDialog extends LitElement { | |||||||
|         pre, |         pre, | ||||||
|         code { |         code { | ||||||
|           background-color: var(--markdown-code-background-color, none); |           background-color: var(--markdown-code-background-color, none); | ||||||
|           border-radius: var(--ha-border-radius-sm); |           border-radius: 3px; | ||||||
|         } |         } | ||||||
|         pre { |         pre { | ||||||
|           padding: 16px; |           padding: 16px; | ||||||
|   | |||||||
| @@ -15,8 +15,6 @@ import "../../../../src/components/ha-list"; | |||||||
| import "../../../../src/components/ha-list-item"; | import "../../../../src/components/ha-list-item"; | ||||||
| import "../../../../src/components/ha-password-field"; | import "../../../../src/components/ha-password-field"; | ||||||
| import "../../../../src/components/ha-radio"; | import "../../../../src/components/ha-radio"; | ||||||
| import "../../../../src/components/ha-tab-group"; |  | ||||||
| import "../../../../src/components/ha-tab-group-tab"; |  | ||||||
| import "../../../../src/components/ha-textfield"; | import "../../../../src/components/ha-textfield"; | ||||||
| import type { HaTextField } from "../../../../src/components/ha-textfield"; | import type { HaTextField } from "../../../../src/components/ha-textfield"; | ||||||
| import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; | import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; | ||||||
| @@ -38,6 +36,7 @@ import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager"; | |||||||
| import { haStyleDialog } from "../../../../src/resources/styles"; | import { haStyleDialog } from "../../../../src/resources/styles"; | ||||||
| import type { HomeAssistant } from "../../../../src/types"; | import type { HomeAssistant } from "../../../../src/types"; | ||||||
| import type { HassioNetworkDialogParams } from "./show-dialog-network"; | import type { HassioNetworkDialogParams } from "./show-dialog-network"; | ||||||
|  | import "../../../../src/components/sl-tab-group"; | ||||||
|  |  | ||||||
| const IP_VERSIONS = ["ipv4", "ipv6"]; | const IP_VERSIONS = ["ipv4", "ipv6"]; | ||||||
|  |  | ||||||
| @@ -115,19 +114,19 @@ export class DialogHassioNetwork | |||||||
|             ></ha-icon-button> |             ></ha-icon-button> | ||||||
|           </ha-header-bar> |           </ha-header-bar> | ||||||
|           ${this._interfaces.length > 1 |           ${this._interfaces.length > 1 | ||||||
|             ? html`<ha-tab-group @wa-tab-show=${this._handleTabActivated} |             ? html`<sl-tab-group @sl-tab-show=${this._handleTabActivated} | ||||||
|                 >${this._interfaces.map( |                 >${this._interfaces.map( | ||||||
|                   (device, index) => |                   (device, index) => | ||||||
|                     html`<ha-tab-group-tab |                     html`<sl-tab | ||||||
|                       slot="nav" |                       slot="nav" | ||||||
|                       .id=${device.interface} |                       .id=${device.interface} | ||||||
|                       .panel=${index.toString()} |                       .panel=${index.toString()} | ||||||
|                       .active=${this._curTabIndex === index} |                       .active=${this._curTabIndex === index} | ||||||
|                     > |                     > | ||||||
|                       ${device.interface} |                       ${device.interface} | ||||||
|                     </ha-tab-group-tab>` |                     </sl-tab>` | ||||||
|                 )} |                 )} | ||||||
|               </ha-tab-group>` |               </sl-tab-group>` | ||||||
|             : ""} |             : ""} | ||||||
|         </div> |         </div> | ||||||
|         ${cache(this._renderTab())} |         ${cache(this._renderTab())} | ||||||
| @@ -628,10 +627,10 @@ export class DialogHassioNetwork | |||||||
|           --mdc-list-side-padding: 10px; |           --mdc-list-side-padding: 10px; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         ha-tab-group-tab { |         sl-tab { | ||||||
|           flex: 1; |           flex: 1; | ||||||
|         } |         } | ||||||
|         ha-tab-group-tab::part(base) { |         sl-tab::part(base) { | ||||||
|           width: 100%; |           width: 100%; | ||||||
|           justify-content: center; |           justify-content: center; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -228,7 +228,7 @@ class HassioRegistriesDialog extends LitElement { | |||||||
|       css` |       css` | ||||||
|         .registry { |         .registry { | ||||||
|           border: 1px solid var(--divider-color); |           border: 1px solid var(--divider-color); | ||||||
|           border-radius: var(--ha-border-radius-sm); |           border-radius: 4px; | ||||||
|           margin-top: 4px; |           margin-top: 4px; | ||||||
|         } |         } | ||||||
|         .action { |         .action { | ||||||
|   | |||||||
| @@ -119,17 +119,15 @@ class HassioRepositoriesDialog extends LitElement { | |||||||
|                         <div>${repo.url}</div> |                         <div>${repo.url}</div> | ||||||
|                       </div> |                       </div> | ||||||
|                       <ha-tooltip |                       <ha-tooltip | ||||||
|                         .for="icon-button-${repo.slug}" |  | ||||||
|                         class="delete" |                         class="delete" | ||||||
|                         slot="end" |                         slot="end" | ||||||
|                       > |                         .content=${this._dialogParams!.supervisor.localize( | ||||||
|                         ${this._dialogParams!.supervisor.localize( |  | ||||||
|                           usedRepositories.includes(repo.slug) |                           usedRepositories.includes(repo.slug) | ||||||
|                             ? "dialog.repositories.used" |                             ? "dialog.repositories.used" | ||||||
|                             : "dialog.repositories.remove" |                             : "dialog.repositories.remove" | ||||||
|                         )} |                         )} | ||||||
|                       </ha-tooltip> |                       > | ||||||
|                       <div .id="icon-button-${repo.slug}"> |                         <div> | ||||||
|                           <ha-icon-button |                           <ha-icon-button | ||||||
|                             .disabled=${usedRepositories.includes(repo.slug)} |                             .disabled=${usedRepositories.includes(repo.slug)} | ||||||
|                             .slug=${repo.slug} |                             .slug=${repo.slug} | ||||||
| @@ -140,6 +138,7 @@ class HassioRepositoriesDialog extends LitElement { | |||||||
|                           > |                           > | ||||||
|                           </ha-icon-button> |                           </ha-icon-button> | ||||||
|                         </div> |                         </div> | ||||||
|  |                       </ha-tooltip> | ||||||
|                     </ha-md-list-item> |                     </ha-md-list-item> | ||||||
|                   ` |                   ` | ||||||
|                 ) |                 ) | ||||||
| @@ -193,7 +192,7 @@ class HassioRepositoriesDialog extends LitElement { | |||||||
|         } |         } | ||||||
|         .option { |         .option { | ||||||
|           border: 1px solid var(--divider-color); |           border: 1px solid var(--divider-color); | ||||||
|           border-radius: var(--ha-border-radius-sm); |           border-radius: 4px; | ||||||
|           margin-top: 4px; |           margin-top: 4px; | ||||||
|         } |         } | ||||||
|         ha-button { |         ha-button { | ||||||
|   | |||||||
| @@ -159,7 +159,7 @@ class HassioSystemManagedDialog extends LitElement { | |||||||
|           display: flex; |           display: flex; | ||||||
|           justify-content: center; |           justify-content: center; | ||||||
|           align-items: center; |           align-items: center; | ||||||
|           gap: var(--ha-space-4); |           gap: 16px; | ||||||
|           --mdc-icon-size: 48px; |           --mdc-icon-size: 48px; | ||||||
|           margin-bottom: 32px; |           margin-bottom: 32px; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import type { PropertyValues, TemplateResult } from "lit"; | |||||||
| import { css, html, LitElement } from "lit"; | import { css, html, LitElement } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { fireEvent } from "../../../src/common/dom/fire_event"; | import { fireEvent } from "../../../src/common/dom/fire_event"; | ||||||
| import { goBack, navigate } from "../../../src/common/navigate"; | import { navigate } from "../../../src/common/navigate"; | ||||||
| import { extractSearchParam } from "../../../src/common/url/search-params"; | import { extractSearchParam } from "../../../src/common/url/search-params"; | ||||||
| import { nextRender } from "../../../src/common/util/render-status"; | import { nextRender } from "../../../src/common/util/render-status"; | ||||||
| import "../../../src/components/ha-icon-button"; | import "../../../src/components/ha-icon-button"; | ||||||
| @@ -193,7 +193,7 @@ class HassioIngressView extends LitElement { | |||||||
|         title: addon.name, |         title: addon.name, | ||||||
|       }); |       }); | ||||||
|       await nextRender(); |       await nextRender(); | ||||||
|       goBack(); |       history.back(); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -275,7 +275,7 @@ class HassioIngressView extends LitElement { | |||||||
|         title: addon.name, |         title: addon.name, | ||||||
|       }); |       }); | ||||||
|       await nextRender(); |       await nextRender(); | ||||||
|       goBack(); |       history.back(); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ export const hassioStyle = css` | |||||||
|   .card-group { |   .card-group { | ||||||
|     display: grid; |     display: grid; | ||||||
|     grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); |     grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | ||||||
|     grid-gap: var(--ha-space-2); |     grid-gap: 8px; | ||||||
|   } |   } | ||||||
|   @media screen and (min-width: 640px) { |   @media screen and (min-width: 640px) { | ||||||
|     .card-group { |     .card-group { | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ import type { TemplateResult } from "lit"; | |||||||
| import { css, html, LitElement } from "lit"; | import { css, html, LitElement } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| import type { Supervisor } from "../../../src/data/supervisor/supervisor"; | import type { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||||
| import { goBack } from "../../../src/common/navigate"; |  | ||||||
| import "../../../src/layouts/hass-subpage"; | import "../../../src/layouts/hass-subpage"; | ||||||
| import type { HomeAssistant, Route } from "../../../src/types"; | import type { HomeAssistant, Route } from "../../../src/types"; | ||||||
| import "./update-available-card"; | import "./update-available-card"; | ||||||
| @@ -36,7 +35,7 @@ class UpdateAvailableDashboard extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _updateComplete() { |   private _updateComplete() { | ||||||
|     goBack(); |     history.back(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   static styles = css` |   static styles = css` | ||||||
|   | |||||||
| @@ -302,7 +302,7 @@ class LandingPageLogs extends LitElement { | |||||||
|         max-height: 300px; |         max-height: 300px; | ||||||
|         overflow: auto; |         overflow: auto; | ||||||
|         border: 1px solid var(--divider-color); |         border: 1px solid var(--divider-color); | ||||||
|         border-radius: var(--ha-border-radius-sm); |         border-radius: 4px; | ||||||
|         padding: 4px; |         padding: 4px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -213,7 +213,7 @@ class HaLandingPage extends LandingPageBaseElement { | |||||||
|       ha-card .card-content { |       ha-card .card-content { | ||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|         gap: var(--ha-space-4); |         gap: 16px; | ||||||
|       } |       } | ||||||
|       ha-alert p { |       ha-alert p { | ||||||
|         text-align: unset; |         text-align: unset; | ||||||
| @@ -221,7 +221,7 @@ class HaLandingPage extends LandingPageBaseElement { | |||||||
|       ha-language-picker { |       ha-language-picker { | ||||||
|         display: block; |         display: block; | ||||||
|         width: 200px; |         width: 200px; | ||||||
|         border-radius: var(--ha-border-radius-sm); |         border-radius: 4px; | ||||||
|         overflow: hidden; |         overflow: hidden; | ||||||
|         --ha-select-height: 40px; |         --ha-select-height: 40px; | ||||||
|         --mdc-select-fill-color: none; |         --mdc-select-fill-color: none; | ||||||
|   | |||||||
| @@ -19,9 +19,8 @@ | |||||||
|         height: auto; |         height: auto; | ||||||
|         padding: 32px 0; |         padding: 32px 0; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .content { |       .content { | ||||||
|         max-width: min(560px, calc(100vw - var(--safe-area-inset-right, 0px) - var(--safe-area-inset-left, 0px))); |         max-width: 560px; | ||||||
|         margin: 0 auto; |         margin: 0 auto; | ||||||
|         padding: 0 16px; |         padding: 0 16px; | ||||||
|         box-sizing: content-box; |         box-sizing: content-box; | ||||||
|   | |||||||
							
								
								
									
										106
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										106
									
								
								package.json
									
									
									
									
									
								
							| @@ -26,33 +26,32 @@ | |||||||
|   "license": "Apache-2.0", |   "license": "Apache-2.0", | ||||||
|   "type": "module", |   "type": "module", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@babel/runtime": "7.28.4", |     "@awesome.me/webawesome": "3.0.0-beta.4", | ||||||
|  |     "@babel/runtime": "7.28.3", | ||||||
|     "@braintree/sanitize-url": "7.1.1", |     "@braintree/sanitize-url": "7.1.1", | ||||||
|     "@codemirror/autocomplete": "6.19.0", |     "@codemirror/autocomplete": "6.18.6", | ||||||
|     "@codemirror/commands": "6.9.0", |     "@codemirror/commands": "6.8.1", | ||||||
|     "@codemirror/language": "6.11.3", |     "@codemirror/language": "6.11.3", | ||||||
|     "@codemirror/legacy-modes": "6.5.2", |     "@codemirror/legacy-modes": "6.5.1", | ||||||
|     "@codemirror/search": "6.5.11", |     "@codemirror/search": "6.5.11", | ||||||
|     "@codemirror/state": "6.5.2", |     "@codemirror/state": "6.5.2", | ||||||
|     "@codemirror/view": "6.38.5", |     "@codemirror/view": "6.38.1", | ||||||
|     "@date-fns/tz": "1.4.1", |  | ||||||
|     "@egjs/hammerjs": "2.0.17", |     "@egjs/hammerjs": "2.0.17", | ||||||
|     "@formatjs/intl-datetimeformat": "6.18.2", |     "@formatjs/intl-datetimeformat": "6.18.0", | ||||||
|     "@formatjs/intl-displaynames": "6.8.13", |     "@formatjs/intl-displaynames": "6.8.11", | ||||||
|     "@formatjs/intl-durationformat": "0.7.6", |     "@formatjs/intl-durationformat": "0.7.4", | ||||||
|     "@formatjs/intl-getcanonicallocales": "2.5.6", |     "@formatjs/intl-getcanonicallocales": "2.5.5", | ||||||
|     "@formatjs/intl-listformat": "7.7.13", |     "@formatjs/intl-listformat": "7.7.11", | ||||||
|     "@formatjs/intl-locale": "4.2.13", |     "@formatjs/intl-locale": "4.2.11", | ||||||
|     "@formatjs/intl-numberformat": "8.15.6", |     "@formatjs/intl-numberformat": "8.15.4", | ||||||
|     "@formatjs/intl-pluralrules": "5.4.6", |     "@formatjs/intl-pluralrules": "5.4.4", | ||||||
|     "@formatjs/intl-relativetimeformat": "11.4.13", |     "@formatjs/intl-relativetimeformat": "11.4.11", | ||||||
|     "@fullcalendar/core": "6.1.19", |     "@fullcalendar/core": "6.1.19", | ||||||
|     "@fullcalendar/daygrid": "6.1.19", |     "@fullcalendar/daygrid": "6.1.19", | ||||||
|     "@fullcalendar/interaction": "6.1.19", |     "@fullcalendar/interaction": "6.1.19", | ||||||
|     "@fullcalendar/list": "6.1.19", |     "@fullcalendar/list": "6.1.19", | ||||||
|     "@fullcalendar/luxon3": "6.1.19", |     "@fullcalendar/luxon3": "6.1.19", | ||||||
|     "@fullcalendar/timegrid": "6.1.19", |     "@fullcalendar/timegrid": "6.1.19", | ||||||
|     "@home-assistant/webawesome": "3.0.0-beta.6.ha.4", |  | ||||||
|     "@lezer/highlight": "1.2.1", |     "@lezer/highlight": "1.2.1", | ||||||
|     "@lit-labs/motion": "1.0.9", |     "@lit-labs/motion": "1.0.9", | ||||||
|     "@lit-labs/observers": "2.0.6", |     "@lit-labs/observers": "2.0.6", | ||||||
| @@ -85,24 +84,26 @@ | |||||||
|     "@mdi/js": "7.4.47", |     "@mdi/js": "7.4.47", | ||||||
|     "@mdi/svg": "7.4.47", |     "@mdi/svg": "7.4.47", | ||||||
|     "@replit/codemirror-indentation-markers": "6.5.3", |     "@replit/codemirror-indentation-markers": "6.5.3", | ||||||
|  |     "@shoelace-style/shoelace": "2.20.1", | ||||||
|     "@swc/helpers": "0.5.17", |     "@swc/helpers": "0.5.17", | ||||||
|     "@thomasloven/round-slider": "0.6.0", |     "@thomasloven/round-slider": "0.6.0", | ||||||
|     "@tsparticles/engine": "3.9.1", |     "@tsparticles/engine": "3.9.1", | ||||||
|     "@tsparticles/preset-links": "3.2.0", |     "@tsparticles/preset-links": "3.2.0", | ||||||
|     "@vaadin/combo-box": "24.9.2", |     "@vaadin/combo-box": "24.8.6", | ||||||
|     "@vaadin/vaadin-themable-mixin": "24.9.2", |     "@vaadin/vaadin-themable-mixin": "24.8.6", | ||||||
|     "@vibrant/color": "4.0.0", |     "@vibrant/color": "4.0.0", | ||||||
|     "@vue/web-component-wrapper": "1.3.0", |     "@vue/web-component-wrapper": "1.3.0", | ||||||
|     "@webcomponents/scoped-custom-element-registry": "0.0.10", |     "@webcomponents/scoped-custom-element-registry": "0.0.10", | ||||||
|     "@webcomponents/webcomponentsjs": "2.8.0", |     "@webcomponents/webcomponentsjs": "2.8.0", | ||||||
|     "app-datepicker": "5.1.1", |     "app-datepicker": "5.1.1", | ||||||
|     "barcode-detector": "3.0.6", |     "barcode-detector": "3.0.5", | ||||||
|     "color-name": "2.0.2", |     "color-name": "2.0.0", | ||||||
|     "comlink": "4.4.2", |     "comlink": "4.4.2", | ||||||
|     "core-js": "3.46.0", |     "core-js": "3.45.1", | ||||||
|     "cropperjs": "1.6.2", |     "cropperjs": "1.6.2", | ||||||
|     "culori": "4.0.2", |     "culori": "4.0.2", | ||||||
|     "date-fns": "4.1.0", |     "date-fns": "4.1.0", | ||||||
|  |     "date-fns-tz": "3.2.0", | ||||||
|     "deep-clone-simple": "1.1.1", |     "deep-clone-simple": "1.1.1", | ||||||
|     "deep-freeze": "0.0.1", |     "deep-freeze": "0.0.1", | ||||||
|     "dialog-polyfill": "0.5.6", |     "dialog-polyfill": "0.5.6", | ||||||
| @@ -111,18 +112,18 @@ | |||||||
|     "fuse.js": "7.1.0", |     "fuse.js": "7.1.0", | ||||||
|     "google-timezones-json": "1.2.0", |     "google-timezones-json": "1.2.0", | ||||||
|     "gulp-zopfli-green": "6.0.2", |     "gulp-zopfli-green": "6.0.2", | ||||||
|     "hls.js": "1.6.13", |     "hls.js": "1.6.11", | ||||||
|     "home-assistant-js-websocket": "9.5.0", |     "home-assistant-js-websocket": "9.5.0", | ||||||
|     "idb-keyval": "6.2.2", |     "idb-keyval": "6.2.2", | ||||||
|     "intl-messageformat": "10.7.18", |     "intl-messageformat": "10.7.16", | ||||||
|     "js-yaml": "4.1.0", |     "js-yaml": "4.1.0", | ||||||
|     "leaflet": "1.9.4", |     "leaflet": "1.9.4", | ||||||
|     "leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch", |     "leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch", | ||||||
|     "leaflet.markercluster": "1.5.3", |     "leaflet.markercluster": "1.5.3", | ||||||
|     "lit": "3.3.1", |     "lit": "3.3.1", | ||||||
|     "lit-html": "3.3.1", |     "lit-html": "3.3.1", | ||||||
|     "luxon": "3.7.2", |     "luxon": "3.7.1", | ||||||
|     "marked": "16.4.0", |     "marked": "16.2.1", | ||||||
|     "memoize-one": "6.0.0", |     "memoize-one": "6.0.0", | ||||||
|     "node-vibrant": "4.0.3", |     "node-vibrant": "4.0.3", | ||||||
|     "object-hash": "3.0.0", |     "object-hash": "3.0.0", | ||||||
| @@ -135,7 +136,7 @@ | |||||||
|     "stacktrace-js": "2.0.2", |     "stacktrace-js": "2.0.2", | ||||||
|     "superstruct": "2.0.2", |     "superstruct": "2.0.2", | ||||||
|     "tinykeys": "3.0.0", |     "tinykeys": "3.0.0", | ||||||
|     "ua-parser-js": "2.0.6", |     "ua-parser-js": "2.0.4", | ||||||
|     "vue": "2.7.16", |     "vue": "2.7.16", | ||||||
|     "vue2-daterange-picker": "0.6.8", |     "vue2-daterange-picker": "0.6.8", | ||||||
|     "weekstart": "2.0.0", |     "weekstart": "2.0.0", | ||||||
| @@ -148,28 +149,28 @@ | |||||||
|     "xss": "1.0.15" |     "xss": "1.0.15" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@babel/core": "7.28.4", |     "@babel/core": "7.28.3", | ||||||
|     "@babel/helper-define-polyfill-provider": "0.6.5", |     "@babel/helper-define-polyfill-provider": "0.6.5", | ||||||
|     "@babel/plugin-transform-runtime": "7.28.3", |     "@babel/plugin-transform-runtime": "7.28.3", | ||||||
|     "@babel/preset-env": "7.28.3", |     "@babel/preset-env": "7.28.3", | ||||||
|     "@bundle-stats/plugin-webpack-filter": "4.21.5", |     "@bundle-stats/plugin-webpack-filter": "4.21.3", | ||||||
|     "@lokalise/node-api": "15.3.0", |     "@lokalise/node-api": "15.2.1", | ||||||
|     "@octokit/auth-oauth-device": "8.0.2", |     "@octokit/auth-oauth-device": "8.0.1", | ||||||
|     "@octokit/plugin-retry": "8.0.2", |     "@octokit/plugin-retry": "8.0.1", | ||||||
|     "@octokit/rest": "22.0.0", |     "@octokit/rest": "22.0.0", | ||||||
|     "@rsdoctor/rspack-plugin": "1.3.2", |     "@rsdoctor/rspack-plugin": "1.2.3", | ||||||
|     "@rspack/core": "1.5.8", |     "@rspack/core": "1.5.1", | ||||||
|     "@rspack/dev-server": "1.1.4", |     "@rspack/dev-server": "1.1.4", | ||||||
|     "@types/babel__plugin-transform-runtime": "7.9.5", |     "@types/babel__plugin-transform-runtime": "7.9.5", | ||||||
|     "@types/chromecast-caf-receiver": "6.0.22", |     "@types/chromecast-caf-receiver": "6.0.24", | ||||||
|     "@types/chromecast-caf-sender": "1.0.11", |     "@types/chromecast-caf-sender": "1.0.11", | ||||||
|     "@types/color-name": "2.0.0", |     "@types/color-name": "2.0.0", | ||||||
|     "@types/culori": "4.0.1", |     "@types/culori": "4.0.0", | ||||||
|     "@types/html-minifier-terser": "7.0.2", |     "@types/html-minifier-terser": "7.0.2", | ||||||
|     "@types/js-yaml": "4.0.9", |     "@types/js-yaml": "4.0.9", | ||||||
|     "@types/leaflet": "1.9.21", |     "@types/leaflet": "1.9.20", | ||||||
|     "@types/leaflet-draw": "1.0.13", |     "@types/leaflet-draw": "1.0.12", | ||||||
|     "@types/leaflet.markercluster": "1.5.6", |     "@types/leaflet.markercluster": "1.5.5", | ||||||
|     "@types/lodash.merge": "4.6.9", |     "@types/lodash.merge": "4.6.9", | ||||||
|     "@types/luxon": "3.7.1", |     "@types/luxon": "3.7.1", | ||||||
|     "@types/mocha": "10.0.10", |     "@types/mocha": "10.0.10", | ||||||
| @@ -182,8 +183,8 @@ | |||||||
|     "babel-loader": "10.0.0", |     "babel-loader": "10.0.0", | ||||||
|     "babel-plugin-template-html-minifier": "4.1.0", |     "babel-plugin-template-html-minifier": "4.1.0", | ||||||
|     "browserslist-useragent-regexp": "4.1.3", |     "browserslist-useragent-regexp": "4.1.3", | ||||||
|     "del": "8.0.1", |     "del": "8.0.0", | ||||||
|     "eslint": "9.37.0", |     "eslint": "9.34.0", | ||||||
|     "eslint-config-airbnb-base": "15.0.0", |     "eslint-config-airbnb-base": "15.0.0", | ||||||
|     "eslint-config-prettier": "10.1.8", |     "eslint-config-prettier": "10.1.8", | ||||||
|     "eslint-import-resolver-webpack": "0.13.10", |     "eslint-import-resolver-webpack": "0.13.10", | ||||||
| @@ -191,9 +192,9 @@ | |||||||
|     "eslint-plugin-lit": "2.1.1", |     "eslint-plugin-lit": "2.1.1", | ||||||
|     "eslint-plugin-lit-a11y": "5.1.1", |     "eslint-plugin-lit-a11y": "5.1.1", | ||||||
|     "eslint-plugin-unused-imports": "4.2.0", |     "eslint-plugin-unused-imports": "4.2.0", | ||||||
|     "eslint-plugin-wc": "3.0.2", |     "eslint-plugin-wc": "3.0.1", | ||||||
|     "fancy-log": "2.0.0", |     "fancy-log": "2.0.0", | ||||||
|     "fs-extra": "11.3.2", |     "fs-extra": "11.3.1", | ||||||
|     "glob": "11.0.3", |     "glob": "11.0.3", | ||||||
|     "gulp": "5.0.1", |     "gulp": "5.0.1", | ||||||
|     "gulp-brotli": "3.0.0", |     "gulp-brotli": "3.0.0", | ||||||
| @@ -201,23 +202,23 @@ | |||||||
|     "gulp-rename": "2.1.0", |     "gulp-rename": "2.1.0", | ||||||
|     "html-minifier-terser": "7.2.0", |     "html-minifier-terser": "7.2.0", | ||||||
|     "husky": "9.1.7", |     "husky": "9.1.7", | ||||||
|     "jsdom": "27.0.0", |     "jsdom": "26.1.0", | ||||||
|     "jszip": "3.10.1", |     "jszip": "3.10.1", | ||||||
|     "lint-staged": "16.2.4", |     "lint-staged": "16.1.5", | ||||||
|     "lit-analyzer": "2.0.3", |     "lit-analyzer": "2.0.3", | ||||||
|     "lodash.merge": "4.6.2", |     "lodash.merge": "4.6.2", | ||||||
|     "lodash.template": "4.5.0", |     "lodash.template": "4.5.0", | ||||||
|     "map-stream": "0.0.7", |     "map-stream": "0.0.7", | ||||||
|     "pinst": "3.0.0", |     "pinst": "3.0.0", | ||||||
|     "prettier": "3.6.2", |     "prettier": "3.6.2", | ||||||
|     "rspack-manifest-plugin": "5.1.0", |     "rspack-manifest-plugin": "5.0.3", | ||||||
|     "serve": "14.2.5", |     "serve": "14.2.4", | ||||||
|     "sinon": "21.0.0", |     "sinon": "21.0.0", | ||||||
|     "tar": "7.5.1", |     "tar": "7.4.3", | ||||||
|     "terser-webpack-plugin": "5.3.14", |     "terser-webpack-plugin": "5.3.14", | ||||||
|     "ts-lit-plugin": "2.0.2", |     "ts-lit-plugin": "2.0.2", | ||||||
|     "typescript": "5.9.3", |     "typescript": "5.9.2", | ||||||
|     "typescript-eslint": "8.46.0", |     "typescript-eslint": "8.41.0", | ||||||
|     "vite-tsconfig-paths": "5.1.4", |     "vite-tsconfig-paths": "5.1.4", | ||||||
|     "vitest": "3.2.4", |     "vitest": "3.2.4", | ||||||
|     "webpack-stats-plugin": "1.1.3", |     "webpack-stats-plugin": "1.1.3", | ||||||
| @@ -231,9 +232,10 @@ | |||||||
|     "clean-css": "5.3.3", |     "clean-css": "5.3.3", | ||||||
|     "@lit/reactive-element": "2.1.1", |     "@lit/reactive-element": "2.1.1", | ||||||
|     "@fullcalendar/daygrid": "6.1.19", |     "@fullcalendar/daygrid": "6.1.19", | ||||||
|     "globals": "16.4.0", |     "globals": "16.3.0", | ||||||
|     "tslib": "2.8.1", |     "tslib": "2.8.1", | ||||||
|     "@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch" |     "@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch", | ||||||
|  |     "@vaadin/vaadin-themable-mixin": "24.8.6" | ||||||
|   }, |   }, | ||||||
|   "packageManager": "yarn@4.10.3" |   "packageManager": "yarn@4.9.4" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,11 +1,3 @@ | |||||||
| export default { | export default { | ||||||
|   trailingComma: "es5", |   trailingComma: "es5", | ||||||
|   overrides: [ |  | ||||||
|     { |  | ||||||
|       files: "*.globals.ts", |  | ||||||
|       options: { |  | ||||||
|         printWidth: 9999, // Effectively disables line wrapping for these files |  | ||||||
|       }, |  | ||||||
|     }, |  | ||||||
|   ], |  | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" | |||||||
|  |  | ||||||
| [project] | [project] | ||||||
| name         = "home-assistant-frontend" | name         = "home-assistant-frontend" | ||||||
| version      = "20250924.0" | version      = "20250827.0" | ||||||
| license      = "Apache-2.0" | license      = "Apache-2.0" | ||||||
| license-files = ["LICENSE*"] | license-files = ["LICENSE*"] | ||||||
| description  = "The Home Assistant frontend" | description  = "The Home Assistant frontend" | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ | |||||||
|     ":semanticCommitsDisabled", |     ":semanticCommitsDisabled", | ||||||
|     "group:monorepos", |     "group:monorepos", | ||||||
|     "group:recommended", |     "group:recommended", | ||||||
|     "security:minimumReleaseAgeNpm" |     "npm:unpublishSafe" | ||||||
|   ], |   ], | ||||||
|   "enabledManagers": ["npm", "nvm"], |   "enabledManagers": ["npm", "nvm"], | ||||||
|   "postUpdateOptions": ["yarnDedupeHighest"], |   "postUpdateOptions": ["yarnDedupeHighest"], | ||||||
|   | |||||||
| @@ -103,10 +103,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) { | |||||||
|           ); |           ); | ||||||
|           box-shadow: var(--ha-card-box-shadow, none); |           box-shadow: var(--ha-card-box-shadow, none); | ||||||
|           box-sizing: border-box; |           box-sizing: border-box; | ||||||
|           border-radius: var( |           border-radius: var(--ha-card-border-radius, 12px); | ||||||
|             --ha-card-border-radius, |  | ||||||
|             var(--ha-border-radius-lg) |  | ||||||
|           ); |  | ||||||
|           border-width: var(--ha-card-border-width, 1px); |           border-width: var(--ha-card-border-width, 1px); | ||||||
|           border-style: solid; |           border-style: solid; | ||||||
|           border-color: var( |           border-color: var( | ||||||
| @@ -135,7 +132,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) { | |||||||
|         } |         } | ||||||
|         ha-language-picker { |         ha-language-picker { | ||||||
|           width: 200px; |           width: 200px; | ||||||
|           border-radius: var(--ha-border-radius-sm); |           border-radius: 4px; | ||||||
|           overflow: hidden; |           overflow: hidden; | ||||||
|           --ha-select-height: 40px; |           --ha-select-height: 40px; | ||||||
|           --mdc-select-fill-color: none; |           --mdc-select-fill-color: none; | ||||||
|   | |||||||
| @@ -1,40 +1,23 @@ | |||||||
| import { formatHex, parse } from "culori"; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Expands a 3-digit hex color to a 6-digit hex color. |  | ||||||
|  * @param hex - The hex color to expand. |  | ||||||
|  * @returns The expanded hex color. |  | ||||||
|  * @throws If the hex color is invalid. |  | ||||||
|  */ |  | ||||||
| export const expandHex = (hex: string): string => { | export const expandHex = (hex: string): string => { | ||||||
|   const color = parse(hex); |   hex = hex.replace("#", ""); | ||||||
|   if (!color) { |   if (hex.length === 6) return hex; | ||||||
|     throw new Error(`Invalid hex color: ${hex}`); |   let result = ""; | ||||||
|  |   for (const val of hex) { | ||||||
|  |     result += val + val; | ||||||
|   } |   } | ||||||
|   const formattedColor = formatHex(color); |   return result; | ||||||
|   if (!formattedColor) { |  | ||||||
|     throw new Error(`Could not format hex color: ${hex}`); |  | ||||||
|   } |  | ||||||
|   return formattedColor.replace("#", ""); |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** | // Blend 2 hex colors: c1 is placed over c2, blend is c1's opacity. | ||||||
|  * Blends two hex colors. c1 is placed over c2, blend is c1's opacity. |  | ||||||
|  * @param c1 - The first hex color. |  | ||||||
|  * @param c2 - The second hex color. |  | ||||||
|  * @param blend - The blend percentage (0-100). |  | ||||||
|  * @returns The blended hex color. |  | ||||||
|  */ |  | ||||||
| export const hexBlend = (c1: string, c2: string, blend = 50): string => { | export const hexBlend = (c1: string, c2: string, blend = 50): string => { | ||||||
|  |   let color = ""; | ||||||
|   c1 = expandHex(c1); |   c1 = expandHex(c1); | ||||||
|   c2 = expandHex(c2); |   c2 = expandHex(c2); | ||||||
|   let color = ""; |  | ||||||
|   for (let i = 0; i <= 5; i += 2) { |   for (let i = 0; i <= 5; i += 2) { | ||||||
|     const h1 = parseInt(c1.substring(i, i + 2), 16); |     const h1 = parseInt(c1.substring(i, i + 2), 16); | ||||||
|     const h2 = parseInt(c2.substring(i, i + 2), 16); |     const h2 = parseInt(c2.substring(i, i + 2), 16); | ||||||
|     const hex = Math.floor(h2 + (h1 - h2) * (blend / 100)) |     let hex = Math.floor(h2 + (h1 - h2) * (blend / 100)).toString(16); | ||||||
|       .toString(16) |     while (hex.length < 2) hex = "0" + hex; | ||||||
|       .padStart(2, "0"); |  | ||||||
|     color += hex; |     color += hex; | ||||||
|   } |   } | ||||||
|   return `#${color}`; |   return `#${color}`; | ||||||
|   | |||||||
| @@ -1,49 +1,28 @@ | |||||||
| import { wcagLuminance, wcagContrast } from "culori"; | export const luminosity = (rgb: [number, number, number]): number => { | ||||||
|  |   // http://www.w3.org/TR/WCAG20/#relativeluminancedef | ||||||
|  |   const lum: [number, number, number] = [0, 0, 0]; | ||||||
|  |   for (let i = 0; i < rgb.length; i++) { | ||||||
|  |     const chan = rgb[i] / 255; | ||||||
|  |     lum[i] = chan <= 0.03928 ? chan / 12.92 : ((chan + 0.055) / 1.055) ** 2.4; | ||||||
|  |   } | ||||||
|  |  | ||||||
| /** |   return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2]; | ||||||
|  * Calculates the luminosity of an RGB color. | }; | ||||||
|  * @param rgb - The RGB color to calculate the luminosity of. |  | ||||||
|  * @returns The luminosity of the color. |  | ||||||
|  */ |  | ||||||
| export const luminosity = (rgb: [number, number, number]): number => |  | ||||||
|   wcagLuminance({ |  | ||||||
|     mode: "rgb", |  | ||||||
|     r: rgb[0] / 255, |  | ||||||
|     g: rgb[1] / 255, |  | ||||||
|     b: rgb[2] / 255, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Calculates the contrast ratio between two RGB colors. |  | ||||||
|  * @param color1 - The first color to calculate the contrast ratio of. |  | ||||||
|  * @param color2 - The second color to calculate the contrast ratio of. |  | ||||||
|  * @returns The contrast ratio between the two colors. |  | ||||||
|  */ |  | ||||||
| export const rgbContrast = ( | export const rgbContrast = ( | ||||||
|   color1: [number, number, number], |   color1: [number, number, number], | ||||||
|   color2: [number, number, number] |   color2: [number, number, number] | ||||||
| ) => | ) => { | ||||||
|   wcagContrast( |   const lum1 = luminosity(color1); | ||||||
|     { |   const lum2 = luminosity(color2); | ||||||
|       mode: "rgb", |  | ||||||
|       r: color1[0] / 255, |   if (lum1 > lum2) { | ||||||
|       g: color1[1] / 255, |     return (lum1 + 0.05) / (lum2 + 0.05); | ||||||
|       b: color1[2] / 255, |   } | ||||||
|     }, |  | ||||||
|     { |   return (lum2 + 0.05) / (lum1 + 0.05); | ||||||
|       mode: "rgb", | }; | ||||||
|       r: color2[0] / 255, |  | ||||||
|       g: color2[1] / 255, |  | ||||||
|       b: color2[2] / 255, |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Calculates the contrast ratio between two RGB colors. |  | ||||||
|  * @param rgb1 - The first color to calculate the contrast ratio of. |  | ||||||
|  * @param rgb2 - The second color to calculate the contrast ratio of. |  | ||||||
|  * @returns The contrast ratio between the two colors. |  | ||||||
|  */ |  | ||||||
| export const getRGBContrastRatio = ( | export const getRGBContrastRatio = ( | ||||||
|   rgb1: [number, number, number], |   rgb1: [number, number, number], | ||||||
|   rgb2: [number, number, number] |   rgb2: [number, number, number] | ||||||
|   | |||||||
| @@ -1,141 +0,0 @@ | |||||||
| import type { |  | ||||||
|   ReactiveController, |  | ||||||
|   ReactiveControllerHost, |  | ||||||
| } from "@lit/reactive-element/reactive-controller"; |  | ||||||
|  |  | ||||||
| const UNDO_REDO_STACK_LIMIT = 75; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Configuration options for the UndoRedoController. |  | ||||||
|  * |  | ||||||
|  * @template ConfigType The type of configuration to manage. |  | ||||||
|  */ |  | ||||||
| export interface UndoRedoControllerConfig<ConfigType> { |  | ||||||
|   stackLimit?: number; |  | ||||||
|   currentConfig: () => ConfigType; |  | ||||||
|   apply: (config: ConfigType) => void; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * A controller to manage undo and redo operations for a given configuration type. |  | ||||||
|  * |  | ||||||
|  * @template ConfigType The type of configuration to manage. |  | ||||||
|  */ |  | ||||||
| export class UndoRedoController<ConfigType> implements ReactiveController { |  | ||||||
|   private _host: ReactiveControllerHost; |  | ||||||
|  |  | ||||||
|   private _undoStack: ConfigType[] = []; |  | ||||||
|  |  | ||||||
|   private _redoStack: ConfigType[] = []; |  | ||||||
|  |  | ||||||
|   private readonly _stackLimit: number = UNDO_REDO_STACK_LIMIT; |  | ||||||
|  |  | ||||||
|   private readonly _apply: (config: ConfigType) => void = () => { |  | ||||||
|     throw new Error("No apply function provided"); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   private readonly _currentConfig: () => ConfigType = () => { |  | ||||||
|     throw new Error("No currentConfig function provided"); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   constructor( |  | ||||||
|     host: ReactiveControllerHost, |  | ||||||
|     options: UndoRedoControllerConfig<ConfigType> |  | ||||||
|   ) { |  | ||||||
|     if (options.stackLimit !== undefined) { |  | ||||||
|       this._stackLimit = options.stackLimit; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this._apply = options.apply; |  | ||||||
|     this._currentConfig = options.currentConfig; |  | ||||||
|     this._host = host; |  | ||||||
|     host.addController(this); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   hostConnected() { |  | ||||||
|     window.addEventListener("undo-change", this._onUndoChange); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   hostDisconnected() { |  | ||||||
|     window.removeEventListener("undo-change", this._onUndoChange); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _onUndoChange = (ev: Event) => { |  | ||||||
|     ev.stopPropagation(); |  | ||||||
|     this.undo(); |  | ||||||
|     this._host.requestUpdate(); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Indicates whether there are actions available to undo. |  | ||||||
|    * |  | ||||||
|    * @returns `true` if there are actions to undo, `false` otherwise. |  | ||||||
|    */ |  | ||||||
|   public get canUndo(): boolean { |  | ||||||
|     return this._undoStack.length > 0; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Indicates whether there are actions available to redo. |  | ||||||
|    * |  | ||||||
|    * @returns `true` if there are actions to redo, `false` otherwise. |  | ||||||
|    */ |  | ||||||
|   public get canRedo(): boolean { |  | ||||||
|     return this._redoStack.length > 0; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Commits the current configuration to the undo stack and clears the redo stack. |  | ||||||
|    * |  | ||||||
|    * @param config The current configuration to commit. |  | ||||||
|    */ |  | ||||||
|   public commit(config: ConfigType) { |  | ||||||
|     if (this._undoStack.length >= this._stackLimit) { |  | ||||||
|       this._undoStack.shift(); |  | ||||||
|     } |  | ||||||
|     this._undoStack.push({ ...config }); |  | ||||||
|     this._redoStack = []; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Undoes the last action and applies the previous configuration |  | ||||||
|    * while saving the current configuration to the redo stack. |  | ||||||
|    */ |  | ||||||
|   public undo() { |  | ||||||
|     if (this._undoStack.length === 0) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     this._redoStack.push({ ...this._currentConfig() }); |  | ||||||
|     const config = this._undoStack.pop()!; |  | ||||||
|     this._apply(config); |  | ||||||
|     this._host.requestUpdate(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Redoes the last undone action and reapplies the configuration |  | ||||||
|    * while saving the current configuration to the undo stack. |  | ||||||
|    */ |  | ||||||
|   public redo() { |  | ||||||
|     if (this._redoStack.length === 0) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     this._undoStack.push({ ...this._currentConfig() }); |  | ||||||
|     const config = this._redoStack.pop()!; |  | ||||||
|     this._apply(config); |  | ||||||
|     this._host.requestUpdate(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Resets the undo and redo stacks, clearing all history. |  | ||||||
|    */ |  | ||||||
|   public reset() { |  | ||||||
|     this._undoStack = []; |  | ||||||
|     this._redoStack = []; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HASSDomEvents { |  | ||||||
|     "undo-change": undefined; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -11,7 +11,7 @@ import { | |||||||
|   differenceInDays, |   differenceInDays, | ||||||
|   addDays, |   addDays, | ||||||
| } from "date-fns"; | } from "date-fns"; | ||||||
| import { TZDate } from "@date-fns/tz"; | import { toZonedTime, fromZonedTime } from "date-fns-tz"; | ||||||
| import type { HassConfig } from "home-assistant-js-websocket"; | import type { HassConfig } from "home-assistant-js-websocket"; | ||||||
| import type { FrontendLocaleData } from "../../data/translation"; | import type { FrontendLocaleData } from "../../data/translation"; | ||||||
| import { TimeZone } from "../../data/translation"; | import { TimeZone } from "../../data/translation"; | ||||||
| @@ -22,13 +22,12 @@ const calcZonedDate = ( | |||||||
|   fn: (date: Date, options?: any) => Date | number | boolean, |   fn: (date: Date, options?: any) => Date | number | boolean, | ||||||
|   options? |   options? | ||||||
| ) => { | ) => { | ||||||
|   const tzDate = new TZDate(date, tz); |   const inputZoned = toZonedTime(date, tz); | ||||||
|   const fnResult = fn(tzDate, options); |   const fnZoned = fn(inputZoned, options); | ||||||
|   if (fnResult instanceof Date) { |   if (fnZoned instanceof Date) { | ||||||
|     // Convert back to regular Date in the specified timezone |     return fromZonedTime(fnZoned, tz) as Date; | ||||||
|     return new Date(fnResult.getTime()); |  | ||||||
|   } |   } | ||||||
|   return fnResult; |   return fnZoned; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const calcDate = ( | export const calcDate = ( | ||||||
| @@ -66,7 +65,7 @@ export const calcDateDifferenceProperty = ( | |||||||
|     locale, |     locale, | ||||||
|     config, |     config, | ||||||
|     locale.time_zone === TimeZone.server |     locale.time_zone === TimeZone.server | ||||||
|       ? new TZDate(startDate, config.time_zone) |       ? toZonedTime(startDate, config.time_zone) | ||||||
|       : startDate |       : startDate | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
| @@ -145,36 +144,3 @@ export const shiftDateRange = ( | |||||||
|   } |   } | ||||||
|   return { start, end }; |   return { start, end }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @description Parses a date in browser display timezone |  | ||||||
|  * @param date - The date to parse |  | ||||||
|  * @param timezone - The timezone to parse the date in |  | ||||||
|  * @returns The parsed date as a Date object |  | ||||||
|  */ |  | ||||||
| export const parseDate = (date: string, timezone: string): Date => { |  | ||||||
|   const tzDate = new TZDate(date, timezone); |  | ||||||
|   return new Date(tzDate.getTime()); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @description Formats a date in browser display timezone |  | ||||||
|  * @param date - The date to format |  | ||||||
|  * @param timezone - The timezone to format the date in |  | ||||||
|  * @returns The formatted date in YYYY-MM-DD format |  | ||||||
|  */ |  | ||||||
| export const formatDate = (date: Date, timezone: string): string => { |  | ||||||
|   const tzDate = new TZDate(date, timezone); |  | ||||||
|   return tzDate.toISOString().split("T")[0]; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @description Formats a time in browser display timezone |  | ||||||
|  * @param date - The date to format |  | ||||||
|  * @param timezone - The timezone to format the time in |  | ||||||
|  * @returns The formatted time in HH:mm:ss format |  | ||||||
|  */ |  | ||||||
| export const formatTime = (date: Date, timezone: string): string => { |  | ||||||
|   const tzDate = new TZDate(date, timezone); |  | ||||||
|   return tzDate.toISOString().split("T")[1].split(".")[0]; |  | ||||||
| }; |  | ||||||
|   | |||||||
| @@ -31,10 +31,10 @@ export const isNavigationClick = (e: MouseEvent, preventDefault = true) => { | |||||||
|  |  | ||||||
|   const location = window.location; |   const location = window.location; | ||||||
|   const origin = location.origin || location.protocol + "//" + location.host; |   const origin = location.origin || location.protocol + "//" + location.host; | ||||||
|   if (!href.startsWith(origin)) { |   if (href.indexOf(origin) !== 0) { | ||||||
|     return undefined; |     return undefined; | ||||||
|   } |   } | ||||||
|   href = href.slice(origin.length); |   href = href.substr(origin.length); | ||||||
|  |  | ||||||
|   if (href === "#") { |   if (href === "#") { | ||||||
|     return undefined; |     return undefined; | ||||||
|   | |||||||
| @@ -10,10 +10,9 @@ import { stripPrefixFromEntityName } from "./strip_prefix_from_entity_name"; | |||||||
|  |  | ||||||
| export const computeEntityName = ( | export const computeEntityName = ( | ||||||
|   stateObj: HassEntity, |   stateObj: HassEntity, | ||||||
|   entities: HomeAssistant["entities"], |   hass: HomeAssistant | ||||||
|   devices: HomeAssistant["devices"] |  | ||||||
| ): string | undefined => { | ): string | undefined => { | ||||||
|   const entry = entities[stateObj.entity_id] as |   const entry = hass.entities[stateObj.entity_id] as | ||||||
|     | EntityRegistryDisplayEntry |     | EntityRegistryDisplayEntry | ||||||
|     | undefined; |     | undefined; | ||||||
|  |  | ||||||
| @@ -21,13 +20,12 @@ export const computeEntityName = ( | |||||||
|     // Fall back to state name if not in the entity registry (friendly name) |     // Fall back to state name if not in the entity registry (friendly name) | ||||||
|     return computeStateName(stateObj); |     return computeStateName(stateObj); | ||||||
|   } |   } | ||||||
|   return computeEntityEntryName(entry, devices); |   return computeEntityEntryName(entry, hass); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const computeEntityEntryName = ( | export const computeEntityEntryName = ( | ||||||
|   entry: EntityRegistryDisplayEntry | EntityRegistryEntry, |   entry: EntityRegistryDisplayEntry | EntityRegistryEntry, | ||||||
|   devices: HomeAssistant["devices"], |   hass: HomeAssistant | ||||||
|   fallbackStateObj?: HassEntity |  | ||||||
| ): string | undefined => { | ): string | undefined => { | ||||||
|   const name = |   const name = | ||||||
|     entry.name || |     entry.name || | ||||||
| @@ -35,14 +33,15 @@ export const computeEntityEntryName = ( | |||||||
|       ? String(entry.original_name) |       ? String(entry.original_name) | ||||||
|       : undefined); |       : undefined); | ||||||
|  |  | ||||||
|   const device = entry.device_id ? devices[entry.device_id] : undefined; |   const device = entry.device_id ? hass.devices[entry.device_id] : undefined; | ||||||
|  |  | ||||||
|   if (!device) { |   if (!device) { | ||||||
|     if (name) { |     if (name) { | ||||||
|       return name; |       return name; | ||||||
|     } |     } | ||||||
|     if (fallbackStateObj) { |     const stateObj = hass.states[entry.entity_id] as HassEntity | undefined; | ||||||
|       return computeStateName(fallbackStateObj); |     if (stateObj) { | ||||||
|  |       return computeStateName(stateObj); | ||||||
|     } |     } | ||||||
|     return undefined; |     return undefined; | ||||||
|   } |   } | ||||||
| @@ -61,9 +60,3 @@ export const computeEntityEntryName = ( | |||||||
|  |  | ||||||
|   return name; |   return name; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const entityUseDeviceName = ( |  | ||||||
|   stateObj: HassEntity, |  | ||||||
|   entities: HomeAssistant["entities"], |  | ||||||
|   devices: HomeAssistant["devices"] |  | ||||||
| ): boolean => !computeEntityName(stateObj, entities, devices); |  | ||||||
|   | |||||||
| @@ -1,109 +0,0 @@ | |||||||
| import type { HassEntity } from "home-assistant-js-websocket"; |  | ||||||
| import type { HomeAssistant } from "../../types"; |  | ||||||
| import { ensureArray } from "../array/ensure-array"; |  | ||||||
| import { computeAreaName } from "./compute_area_name"; |  | ||||||
| import { computeDeviceName } from "./compute_device_name"; |  | ||||||
| import { computeEntityName, entityUseDeviceName } from "./compute_entity_name"; |  | ||||||
| import { computeFloorName } from "./compute_floor_name"; |  | ||||||
| import { getEntityContext } from "./context/get_entity_context"; |  | ||||||
|  |  | ||||||
| const DEFAULT_SEPARATOR = " "; |  | ||||||
|  |  | ||||||
| export const DEFAULT_ENTITY_NAME = [ |  | ||||||
|   { type: "device" }, |  | ||||||
|   { type: "entity" }, |  | ||||||
| ] satisfies EntityNameItem[]; |  | ||||||
|  |  | ||||||
| export type EntityNameItem = |  | ||||||
|   | { |  | ||||||
|       type: "entity" | "device" | "area" | "floor"; |  | ||||||
|     } |  | ||||||
|   | { |  | ||||||
|       type: "text"; |  | ||||||
|       text: string; |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
| export interface EntityNameOptions { |  | ||||||
|   separator?: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const computeEntityNameDisplay = ( |  | ||||||
|   stateObj: HassEntity, |  | ||||||
|   name: EntityNameItem | EntityNameItem[] | undefined, |  | ||||||
|   entities: HomeAssistant["entities"], |  | ||||||
|   devices: HomeAssistant["devices"], |  | ||||||
|   areas: HomeAssistant["areas"], |  | ||||||
|   floors: HomeAssistant["floors"], |  | ||||||
|   options?: EntityNameOptions |  | ||||||
| ) => { |  | ||||||
|   let items = ensureArray(name || DEFAULT_ENTITY_NAME); |  | ||||||
|  |  | ||||||
|   const separator = options?.separator ?? DEFAULT_SEPARATOR; |  | ||||||
|  |  | ||||||
|   // If all items are text, just join them |  | ||||||
|   if (items.every((n) => n.type === "text")) { |  | ||||||
|     return items.map((item) => item.text).join(separator); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const useDeviceName = entityUseDeviceName(stateObj, entities, devices); |  | ||||||
|  |  | ||||||
|   // If entity uses device name, and device is not already included, replace it with device name |  | ||||||
|   if (useDeviceName) { |  | ||||||
|     const hasDevice = items.some((n) => n.type === "device"); |  | ||||||
|     if (!hasDevice) { |  | ||||||
|       items = items.map((n) => (n.type === "entity" ? { type: "device" } : n)); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const names = computeEntityNameList( |  | ||||||
|     stateObj, |  | ||||||
|     items, |  | ||||||
|     entities, |  | ||||||
|     devices, |  | ||||||
|     areas, |  | ||||||
|     floors |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   // If after processing there is only one name, return that |  | ||||||
|   if (names.length === 1) { |  | ||||||
|     return names[0] || ""; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return names.filter((n) => n).join(separator); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const computeEntityNameList = ( |  | ||||||
|   stateObj: HassEntity, |  | ||||||
|   name: EntityNameItem[], |  | ||||||
|   entities: HomeAssistant["entities"], |  | ||||||
|   devices: HomeAssistant["devices"], |  | ||||||
|   areas: HomeAssistant["areas"], |  | ||||||
|   floors: HomeAssistant["floors"] |  | ||||||
| ): (string | undefined)[] => { |  | ||||||
|   const { device, area, floor } = getEntityContext( |  | ||||||
|     stateObj, |  | ||||||
|     entities, |  | ||||||
|     devices, |  | ||||||
|     areas, |  | ||||||
|     floors |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   const names = name.map((item) => { |  | ||||||
|     switch (item.type) { |  | ||||||
|       case "entity": |  | ||||||
|         return computeEntityName(stateObj, entities, devices); |  | ||||||
|       case "device": |  | ||||||
|         return device ? computeDeviceName(device) : undefined; |  | ||||||
|       case "area": |  | ||||||
|         return area ? computeAreaName(area) : undefined; |  | ||||||
|       case "floor": |  | ||||||
|         return floor ? computeFloorName(floor) : undefined; |  | ||||||
|       case "text": |  | ||||||
|         return item.text; |  | ||||||
|       default: |  | ||||||
|         return ""; |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   return names; |  | ||||||
| }; |  | ||||||
| @@ -1,3 +1,3 @@ | |||||||
| /** Compute the object ID of a state. */ | /** Compute the object ID of a state. */ | ||||||
| export const computeObjectId = (entityId: string): string => | export const computeObjectId = (entityId: string): string => | ||||||
|   entityId.slice(entityId.indexOf(".") + 1); |   entityId.substr(entityId.indexOf(".") + 1); | ||||||
|   | |||||||
| @@ -8,10 +8,10 @@ interface AreaContext { | |||||||
| } | } | ||||||
| export const getAreaContext = ( | export const getAreaContext = ( | ||||||
|   area: AreaRegistryEntry, |   area: AreaRegistryEntry, | ||||||
|   hassFloors: HomeAssistant["floors"] |   hass: HomeAssistant | ||||||
| ): AreaContext => { | ): AreaContext => { | ||||||
|   const floorId = area.floor_id; |   const floorId = area.floor_id; | ||||||
|   const floor = floorId ? hassFloors[floorId] : undefined; |   const floor = floorId ? hass.floors[floorId] : undefined; | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|     area: area, |     area: area, | ||||||
|   | |||||||
| @@ -18,12 +18,9 @@ interface EntityContext { | |||||||
|  |  | ||||||
| export const getEntityContext = ( | export const getEntityContext = ( | ||||||
|   stateObj: HassEntity, |   stateObj: HassEntity, | ||||||
|   entities: HomeAssistant["entities"], |   hass: HomeAssistant | ||||||
|   devices: HomeAssistant["devices"], |  | ||||||
|   areas: HomeAssistant["areas"], |  | ||||||
|   floors: HomeAssistant["floors"] |  | ||||||
| ): EntityContext => { | ): EntityContext => { | ||||||
|   const entry = entities[stateObj.entity_id] as |   const entry = hass.entities[stateObj.entity_id] as | ||||||
|     | EntityRegistryDisplayEntry |     | EntityRegistryDisplayEntry | ||||||
|     | undefined; |     | undefined; | ||||||
|  |  | ||||||
| @@ -35,7 +32,7 @@ export const getEntityContext = ( | |||||||
|       floor: null, |       floor: null, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|   return getEntityEntryContext(entry, entities, devices, areas, floors); |   return getEntityEntryContext(entry, hass); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const getEntityEntryContext = ( | export const getEntityEntryContext = ( | ||||||
| @@ -43,18 +40,15 @@ export const getEntityEntryContext = ( | |||||||
|     | EntityRegistryDisplayEntry |     | EntityRegistryDisplayEntry | ||||||
|     | EntityRegistryEntry |     | EntityRegistryEntry | ||||||
|     | ExtEntityRegistryEntry, |     | ExtEntityRegistryEntry, | ||||||
|   entities: HomeAssistant["entities"], |   hass: HomeAssistant | ||||||
|   devices: HomeAssistant["devices"], |  | ||||||
|   areas: HomeAssistant["areas"], |  | ||||||
|   floors: HomeAssistant["floors"] |  | ||||||
| ): EntityContext => { | ): EntityContext => { | ||||||
|   const entity = entities[entry.entity_id]; |   const entity = hass.entities[entry.entity_id]; | ||||||
|   const deviceId = entry?.device_id; |   const deviceId = entry?.device_id; | ||||||
|   const device = deviceId ? devices[deviceId] : undefined; |   const device = deviceId ? hass.devices[deviceId] : undefined; | ||||||
|   const areaId = entry?.area_id || device?.area_id; |   const areaId = entry?.area_id || device?.area_id; | ||||||
|   const area = areaId ? areas[areaId] : undefined; |   const area = areaId ? hass.areas[areaId] : undefined; | ||||||
|   const floorId = area?.floor_id; |   const floorId = area?.floor_id; | ||||||
|   const floor = floorId ? floors[floorId] : undefined; |   const floor = floorId ? hass.floors[floorId] : undefined; | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|     entity: entity, |     entity: entity, | ||||||
|   | |||||||
| @@ -60,20 +60,17 @@ export const generateEntityFilter = ( | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const { area, floor, device, entity } = getEntityContext( |     const { area, floor, device, entity } = getEntityContext(stateObj, hass); | ||||||
|       stateObj, |  | ||||||
|       hass.entities, |  | ||||||
|       hass.devices, |  | ||||||
|       hass.areas, |  | ||||||
|       hass.floors |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     if (entity && entity.hidden) { |     if (entity && entity.hidden) { | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (floors) { |     if (floors) { | ||||||
|       if (!floor || !floors.has(floor.floor_id)) { |       if (!floor) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       if (!floors) { | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| @@ -122,22 +119,3 @@ export const generateEntityFilter = ( | |||||||
|     return true; |     return true; | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const findEntities = ( |  | ||||||
|   entities: string[], |  | ||||||
|   filters: EntityFilterFunc[] |  | ||||||
| ): string[] => { |  | ||||||
|   const seen = new Set<string>(); |  | ||||||
|   const results: string[] = []; |  | ||||||
|  |  | ||||||
|   for (const filter of filters) { |  | ||||||
|     for (const entity of entities) { |  | ||||||
|       if (filter(entity) && !seen.has(entity)) { |  | ||||||
|         seen.add(entity); |  | ||||||
|         results.push(entity); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return results; |  | ||||||
| }; |  | ||||||
|   | |||||||
| @@ -18,7 +18,6 @@ export const FIXED_DOMAIN_STATES = { | |||||||
|     "pending", |     "pending", | ||||||
|     "triggered", |     "triggered", | ||||||
|   ], |   ], | ||||||
|   alert: ["on", "off", "idle"], |  | ||||||
|   assist_satellite: ["idle", "listening", "responding", "processing"], |   assist_satellite: ["idle", "listening", "responding", "processing"], | ||||||
|   automation: ["on", "off"], |   automation: ["on", "off"], | ||||||
|   binary_sensor: ["on", "off"], |   binary_sensor: ["on", "off"], | ||||||
|   | |||||||
| @@ -40,7 +40,6 @@ const STATE_COLORED_DOMAIN = new Set([ | |||||||
|   "vacuum", |   "vacuum", | ||||||
|   "valve", |   "valve", | ||||||
|   "water_heater", |   "water_heater", | ||||||
|   "weather", |  | ||||||
| ]); | ]); | ||||||
|  |  | ||||||
| export const stateColorCss = (stateObj: HassEntity, state?: string) => { | export const stateColorCss = (stateObj: HassEntity, state?: string) => { | ||||||
|   | |||||||
| @@ -63,21 +63,3 @@ export const navigate = async ( | |||||||
|   }); |   }); | ||||||
|   return true; |   return true; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Navigate back in history, with fallback to a default path if no history exists. |  | ||||||
|  * This prevents a user from getting stuck when they navigate directly to a page with no history. |  | ||||||
|  */ |  | ||||||
| export const goBack = (fallbackPath?: string) => { |  | ||||||
|   const { history } = mainWindow; |  | ||||||
|  |  | ||||||
|   // Check if we have history to go back to |  | ||||||
|   if (history.length > 1) { |  | ||||||
|     history.back(); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // No history available, navigate to fallback path |  | ||||||
|   const fallback = fallbackPath || "/"; |  | ||||||
|   navigate(fallback, { replace: true }); |  | ||||||
| }; |  | ||||||
|   | |||||||
| @@ -32,8 +32,6 @@ export const numberFormatToLocale = ( | |||||||
|       return ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89 |       return ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89 | ||||||
|     case NumberFormat.space_comma: |     case NumberFormat.space_comma: | ||||||
|       return ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89 |       return ["fr", "sv", "cs"]; // Use French with fallback to Swedish and Czech formatting 1 234 567,89 | ||||||
|     case NumberFormat.quote_decimal: |  | ||||||
|       return ["de-CH"]; // Use German (Switzerland) formatting 1'234'567.89 |  | ||||||
|     case NumberFormat.system: |     case NumberFormat.system: | ||||||
|       return undefined; |       return undefined; | ||||||
|     default: |     default: | ||||||
|   | |||||||
| @@ -67,7 +67,10 @@ function isSeparatorAtPos(value: string, index: number): boolean { | |||||||
|     case undefined: |     case undefined: | ||||||
|       return false; |       return false; | ||||||
|     default: |     default: | ||||||
|       return isEmojiImprecise(code); |       if (isEmojiImprecise(code)) { | ||||||
|  |         return true; | ||||||
|  |       } | ||||||
|  |       return false; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,11 +1,6 @@ | |||||||
| import type { HassConfig, HassEntity } from "home-assistant-js-websocket"; | import type { HassConfig, HassEntity } from "home-assistant-js-websocket"; | ||||||
| import type { FrontendLocaleData } from "../../data/translation"; | import type { FrontendLocaleData } from "../../data/translation"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import { |  | ||||||
|   computeEntityNameDisplay, |  | ||||||
|   type EntityNameItem, |  | ||||||
|   type EntityNameOptions, |  | ||||||
| } from "../entity/compute_entity_name_display"; |  | ||||||
| import type { LocalizeFunc } from "./localize"; | import type { LocalizeFunc } from "./localize"; | ||||||
|  |  | ||||||
| export type FormatEntityStateFunc = ( | export type FormatEntityStateFunc = ( | ||||||
| @@ -22,28 +17,16 @@ export type FormatEntityAttributeNameFunc = ( | |||||||
|   attribute: string |   attribute: string | ||||||
| ) => string; | ) => string; | ||||||
|  |  | ||||||
| export type EntityNameType = "entity" | "device" | "area" | "floor"; |  | ||||||
|  |  | ||||||
| export type FormatEntityNameFunc = ( |  | ||||||
|   stateObj: HassEntity, |  | ||||||
|   name: EntityNameItem | EntityNameItem[], |  | ||||||
|   options?: EntityNameOptions |  | ||||||
| ) => string; |  | ||||||
|  |  | ||||||
| export const computeFormatFunctions = async ( | export const computeFormatFunctions = async ( | ||||||
|   localize: LocalizeFunc, |   localize: LocalizeFunc, | ||||||
|   locale: FrontendLocaleData, |   locale: FrontendLocaleData, | ||||||
|   config: HassConfig, |   config: HassConfig, | ||||||
|   entities: HomeAssistant["entities"], |   entities: HomeAssistant["entities"], | ||||||
|   devices: HomeAssistant["devices"], |  | ||||||
|   areas: HomeAssistant["areas"], |  | ||||||
|   floors: HomeAssistant["floors"], |  | ||||||
|   sensorNumericDeviceClasses: string[] |   sensorNumericDeviceClasses: string[] | ||||||
| ): Promise<{ | ): Promise<{ | ||||||
|   formatEntityState: FormatEntityStateFunc; |   formatEntityState: FormatEntityStateFunc; | ||||||
|   formatEntityAttributeValue: FormatEntityAttributeValueFunc; |   formatEntityAttributeValue: FormatEntityAttributeValueFunc; | ||||||
|   formatEntityAttributeName: FormatEntityAttributeNameFunc; |   formatEntityAttributeName: FormatEntityAttributeNameFunc; | ||||||
|   formatEntityName: FormatEntityNameFunc; |  | ||||||
| }> => { | }> => { | ||||||
|   const { computeStateDisplay } = await import( |   const { computeStateDisplay } = await import( | ||||||
|     "../entity/compute_state_display" |     "../entity/compute_state_display" | ||||||
| @@ -74,15 +57,5 @@ export const computeFormatFunctions = async ( | |||||||
|       ), |       ), | ||||||
|     formatEntityAttributeName: (stateObj, attribute) => |     formatEntityAttributeName: (stateObj, attribute) => | ||||||
|       computeAttributeNameDisplay(localize, stateObj, entities, attribute), |       computeAttributeNameDisplay(localize, stateObj, entities, attribute), | ||||||
|     formatEntityName: (stateObj, name, options) => |  | ||||||
|       computeEntityNameDisplay( |  | ||||||
|         stateObj, |  | ||||||
|         name, |  | ||||||
|         entities, |  | ||||||
|         devices, |  | ||||||
|         areas, |  | ||||||
|         floors, |  | ||||||
|         options |  | ||||||
|       ), |  | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,18 +0,0 @@ | |||||||
| /** |  | ||||||
|  * Orders object properties according to a specified key order. |  | ||||||
|  * Properties not in the order array will be placed at the end. |  | ||||||
|  */ |  | ||||||
| export function orderProperties<T extends Record<string, any>>( |  | ||||||
|   obj: T, |  | ||||||
|   keys: readonly string[] |  | ||||||
| ): T { |  | ||||||
|   const orderedEntries = keys |  | ||||||
|     .filter((key) => key in obj) |  | ||||||
|     .map((key) => [key, obj[key]] as const); |  | ||||||
|  |  | ||||||
|   const extraEntries = Object.entries(obj).filter( |  | ||||||
|     ([key]) => !keys.includes(key) |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   return Object.fromEntries([...orderedEntries, ...extraEntries]) as T; |  | ||||||
| } |  | ||||||
| @@ -14,7 +14,7 @@ export default function parseAspectRatio(input: string) { | |||||||
|   } |   } | ||||||
|   try { |   try { | ||||||
|     if (input.endsWith("%")) { |     if (input.endsWith("%")) { | ||||||
|       return { w: 100, h: parseOrThrow(input.slice(0, -1)) }; |       return { w: 100, h: parseOrThrow(input.substr(0, input.length - 1)) }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const arr = input.replace(":", "x").split("x"); |     const arr = input.replace(":", "x").split("x"); | ||||||
|   | |||||||
| @@ -1,8 +0,0 @@ | |||||||
| import xss from "xss"; |  | ||||||
|  |  | ||||||
| export const filterXSS = (html: string) => |  | ||||||
|   xss(html, { |  | ||||||
|     whiteList: {}, |  | ||||||
|     stripIgnoreTag: true, |  | ||||||
|     stripIgnoreTagBody: true, |  | ||||||
|   }); |  | ||||||
| @@ -1,22 +1,21 @@ | |||||||
| import type { LineSeriesOption } from "echarts"; | import type { LineSeriesOption } from "echarts"; | ||||||
|  |  | ||||||
| export function downSampleLineData< | export function downSampleLineData( | ||||||
|   T extends [number, number] | NonNullable<LineSeriesOption["data"]>[number], |   data: LineSeriesOption["data"], | ||||||
| >( |   chartWidth: number, | ||||||
|   data: T[] | undefined, |  | ||||||
|   maxDetails: number, |  | ||||||
|   minX?: number, |   minX?: number, | ||||||
|   maxX?: number |   maxX?: number | ||||||
| ): T[] { | ) { | ||||||
|   if (!data) { |   if (!data || data.length < 10) { | ||||||
|     return []; |     return data; | ||||||
|   } |   } | ||||||
|   if (data.length <= maxDetails) { |   const width = chartWidth * window.devicePixelRatio; | ||||||
|  |   if (data.length <= width) { | ||||||
|     return data; |     return data; | ||||||
|   } |   } | ||||||
|   const min = minX ?? getPointData(data[0]!)[0]; |   const min = minX ?? getPointData(data[0]!)[0]; | ||||||
|   const max = maxX ?? getPointData(data[data.length - 1]!)[0]; |   const max = maxX ?? getPointData(data[data.length - 1]!)[0]; | ||||||
|   const step = Math.ceil((max - min) / Math.floor(maxDetails)); |   const step = Math.floor((max - min) / width); | ||||||
|   const frames = new Map< |   const frames = new Map< | ||||||
|     number, |     number, | ||||||
|     { |     { | ||||||
| @@ -48,7 +47,7 @@ export function downSampleLineData< | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Convert frames back to points |   // Convert frames back to points | ||||||
|   const result: T[] = []; |   const result: typeof data = []; | ||||||
|   for (const [_i, frame] of frames) { |   for (const [_i, frame] of frames) { | ||||||
|     // Use min/max points to preserve visual accuracy |     // Use min/max points to preserve visual accuracy | ||||||
|     // The order of the data must be preserved so max may be before min |     // The order of the data must be preserved so max may be before min | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { ResizeController } from "@lit-labs/observers/resize-controller"; |  | ||||||
| import { consume } from "@lit/context"; | import { consume } from "@lit/context"; | ||||||
|  | import { ResizeController } from "@lit-labs/observers/resize-controller"; | ||||||
| import { mdiChevronDown, mdiChevronUp, mdiRestart } from "@mdi/js"; | import { mdiChevronDown, mdiChevronUp, mdiRestart } from "@mdi/js"; | ||||||
| import { differenceInMinutes } from "date-fns"; | import { differenceInMinutes } from "date-fns"; | ||||||
| import type { DataZoomComponentOption } from "echarts/components"; | import type { DataZoomComponentOption } from "echarts/components"; | ||||||
| @@ -7,28 +7,27 @@ import type { EChartsType } from "echarts/core"; | |||||||
| import type { | import type { | ||||||
|   ECElementEvent, |   ECElementEvent, | ||||||
|   LegendComponentOption, |   LegendComponentOption, | ||||||
|   LineSeriesOption, |  | ||||||
|   XAXisOption, |   XAXisOption, | ||||||
|   YAXisOption, |   YAXisOption, | ||||||
|  |   LineSeriesOption, | ||||||
| } from "echarts/types/dist/shared"; | } from "echarts/types/dist/shared"; | ||||||
| import type { PropertyValues } from "lit"; | import type { PropertyValues } from "lit"; | ||||||
| import { css, html, LitElement, nothing } from "lit"; | import { css, html, LitElement, nothing } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { classMap } from "lit/directives/class-map"; | import { classMap } from "lit/directives/class-map"; | ||||||
| import { styleMap } from "lit/directives/style-map"; | import { styleMap } from "lit/directives/style-map"; | ||||||
| import { ensureArray } from "../../common/array/ensure-array"; |  | ||||||
| import { getAllGraphColors } from "../../common/color/colors"; | import { getAllGraphColors } from "../../common/color/colors"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import { listenMediaQuery } from "../../common/dom/media_query"; | import { listenMediaQuery } from "../../common/dom/media_query"; | ||||||
| import { themesContext } from "../../data/context"; | import { themesContext } from "../../data/context"; | ||||||
| import type { Themes } from "../../data/ws-themes"; | import type { Themes } from "../../data/ws-themes"; | ||||||
| import type { ECOption } from "../../resources/echarts/echarts"; | import type { ECOption } from "../../resources/echarts"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import { isMac } from "../../util/is_mac"; | import { isMac } from "../../util/is_mac"; | ||||||
| import "../chips/ha-assist-chip"; |  | ||||||
| import "../ha-icon-button"; | import "../ha-icon-button"; | ||||||
| import { filterXSS } from "../../common/util/xss"; |  | ||||||
| import { formatTimeLabel } from "./axis-label"; | import { formatTimeLabel } from "./axis-label"; | ||||||
|  | import { ensureArray } from "../../common/array/ensure-array"; | ||||||
|  | import "../chips/ha-assist-chip"; | ||||||
| import { downSampleLineData } from "./down-sample"; | import { downSampleLineData } from "./down-sample"; | ||||||
|  |  | ||||||
| export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; | export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; | ||||||
| @@ -64,9 +63,6 @@ export class HaChartBase extends LitElement { | |||||||
|   @property({ attribute: "small-controls", type: Boolean }) |   @property({ attribute: "small-controls", type: Boolean }) | ||||||
|   public smallControls?: boolean; |   public smallControls?: boolean; | ||||||
|  |  | ||||||
|   @property({ attribute: "hide-reset-button", type: Boolean }) |  | ||||||
|   public hideResetButton?: boolean; |  | ||||||
|  |  | ||||||
|   // extraComponents is not reactive and should not trigger updates |   // extraComponents is not reactive and should not trigger updates | ||||||
|   public extraComponents?: any[]; |   public extraComponents?: any[]; | ||||||
|  |  | ||||||
| @@ -219,7 +215,7 @@ export class HaChartBase extends LitElement { | |||||||
|         </div> |         </div> | ||||||
|         ${this._renderLegend()} |         ${this._renderLegend()} | ||||||
|         <div class="chart-controls ${classMap({ small: this.smallControls })}"> |         <div class="chart-controls ${classMap({ small: this.smallControls })}"> | ||||||
|           ${this._isZoomed && !this.hideResetButton |           ${this._isZoomed | ||||||
|             ? html`<ha-icon-button |             ? html`<ha-icon-button | ||||||
|                 class="zoom-reset" |                 class="zoom-reset" | ||||||
|                 .path=${mdiRestart} |                 .path=${mdiRestart} | ||||||
| @@ -346,7 +342,7 @@ export class HaChartBase extends LitElement { | |||||||
|       if (this.chart) { |       if (this.chart) { | ||||||
|         this.chart.dispose(); |         this.chart.dispose(); | ||||||
|       } |       } | ||||||
|       const echarts = (await import("../../resources/echarts/echarts")).default; |       const echarts = (await import("../../resources/echarts")).default; | ||||||
|  |  | ||||||
|       if (this.extraComponents?.length) { |       if (this.extraComponents?.length) { | ||||||
|         echarts.use(this.extraComponents); |         echarts.use(this.extraComponents); | ||||||
| @@ -357,12 +353,20 @@ export class HaChartBase extends LitElement { | |||||||
|  |  | ||||||
|       this.chart = echarts.init(container, "custom"); |       this.chart = echarts.init(container, "custom"); | ||||||
|       this.chart.on("datazoom", (e: any) => { |       this.chart.on("datazoom", (e: any) => { | ||||||
|         this._handleDataZoomEvent(e); |         const { start, end } = e.batch?.[0] ?? e; | ||||||
|  |         this._isZoomed = start !== 0 || end !== 100; | ||||||
|  |         this._zoomRatio = (end - start) / 100; | ||||||
|  |         if (this._isTouchDevice) { | ||||||
|  |           // zooming changes the axis pointer so we need to hide it | ||||||
|  |           this.chart?.dispatchAction({ | ||||||
|  |             type: "hideTip", | ||||||
|  |             from: "datazoom", | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|       }); |       }); | ||||||
|       this.chart.on("click", (e: ECElementEvent) => { |       this.chart.on("click", (e: ECElementEvent) => { | ||||||
|         fireEvent(this, "chart-click", e); |         fireEvent(this, "chart-click", e); | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       if (!this.options?.dataZoom) { |       if (!this.options?.dataZoom) { | ||||||
|         this.chart.getZr().on("dblclick", this._handleClickZoom); |         this.chart.getZr().on("dblclick", this._handleClickZoom); | ||||||
|       } |       } | ||||||
| @@ -805,15 +809,14 @@ export class HaChartBase extends LitElement { | |||||||
|             sampling: undefined, |             sampling: undefined, | ||||||
|             data: downSampleLineData( |             data: downSampleLineData( | ||||||
|               data as LineSeriesOption["data"], |               data as LineSeriesOption["data"], | ||||||
|               this.clientWidth * window.devicePixelRatio, |               this.clientWidth, | ||||||
|               minX, |               minX, | ||||||
|               maxX |               maxX | ||||||
|             ), |             ), | ||||||
|           }; |           }; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       const name = filterXSS(String(s.name ?? s.id ?? "")); |       return { ...s, data }; | ||||||
|       return { ...s, name, data }; |  | ||||||
|     }); |     }); | ||||||
|     return series as ECOption["series"]; |     return series as ECOption["series"]; | ||||||
|   } |   } | ||||||
| @@ -865,60 +868,10 @@ export class HaChartBase extends LitElement { | |||||||
|     }); |     }); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   public zoom(start: number, end: number, silent = false) { |  | ||||||
|     this.chart?.dispatchAction({ |  | ||||||
|       type: "dataZoom", |  | ||||||
|       start, |  | ||||||
|       end, |  | ||||||
|       silent, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _handleZoomReset() { |   private _handleZoomReset() { | ||||||
|     this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 }); |     this.chart?.dispatchAction({ type: "dataZoom", start: 0, end: 100 }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _handleDataZoomEvent(e: any) { |  | ||||||
|     const zoomData = e.batch?.[0] ?? e; |  | ||||||
|     let start = typeof zoomData.start === "number" ? zoomData.start : 0; |  | ||||||
|     let end = typeof zoomData.end === "number" ? zoomData.end : 100; |  | ||||||
|  |  | ||||||
|     if ( |  | ||||||
|       start === 0 && |  | ||||||
|       end === 100 && |  | ||||||
|       zoomData.startValue !== undefined && |  | ||||||
|       zoomData.endValue !== undefined |  | ||||||
|     ) { |  | ||||||
|       const option = this.chart!.getOption(); |  | ||||||
|       const xAxis = option.xAxis?.[0] ?? option.xAxis; |  | ||||||
|  |  | ||||||
|       if (xAxis?.min && xAxis?.max) { |  | ||||||
|         const axisMin = new Date(xAxis.min).getTime(); |  | ||||||
|         const axisMax = new Date(xAxis.max).getTime(); |  | ||||||
|         const axisRange = axisMax - axisMin; |  | ||||||
|  |  | ||||||
|         start = Math.max( |  | ||||||
|           0, |  | ||||||
|           Math.min(100, ((zoomData.startValue - axisMin) / axisRange) * 100) |  | ||||||
|         ); |  | ||||||
|         end = Math.max( |  | ||||||
|           0, |  | ||||||
|           Math.min(100, ((zoomData.endValue - axisMin) / axisRange) * 100) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this._isZoomed = start !== 0 || end !== 100; |  | ||||||
|     this._zoomRatio = (end - start) / 100; |  | ||||||
|     if (this._isTouchDevice) { |  | ||||||
|       this.chart?.dispatchAction({ |  | ||||||
|         type: "hideTip", |  | ||||||
|         from: "datazoom", |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|     fireEvent(this, "chart-zoom", { start, end }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _legendClick(ev: any) { |   private _legendClick(ev: any) { | ||||||
|     if (!this.chart) { |     if (!this.chart) { | ||||||
|       return; |       return; | ||||||
| @@ -976,7 +929,7 @@ export class HaChartBase extends LitElement { | |||||||
|       right: 4px; |       right: 4px; | ||||||
|       display: flex; |       display: flex; | ||||||
|       flex-direction: column; |       flex-direction: column; | ||||||
|       gap: var(--ha-space-1); |       gap: 4px; | ||||||
|     } |     } | ||||||
|     .chart-controls.small { |     .chart-controls.small { | ||||||
|       top: 0; |       top: 0; | ||||||
| @@ -985,7 +938,7 @@ export class HaChartBase extends LitElement { | |||||||
|     .chart-controls ha-icon-button, |     .chart-controls ha-icon-button, | ||||||
|     .chart-controls ::slotted(ha-icon-button) { |     .chart-controls ::slotted(ha-icon-button) { | ||||||
|       background: var(--card-background-color); |       background: var(--card-background-color); | ||||||
|       border-radius: var(--ha-border-radius-sm); |       border-radius: 4px; | ||||||
|       --mdc-icon-button-size: 32px; |       --mdc-icon-button-size: 32px; | ||||||
|       color: var(--primary-color); |       color: var(--primary-color); | ||||||
|       border: 1px solid var(--divider-color); |       border: 1px solid var(--divider-color); | ||||||
| @@ -1013,7 +966,7 @@ export class HaChartBase extends LitElement { | |||||||
|       flex-wrap: wrap; |       flex-wrap: wrap; | ||||||
|       justify-content: center; |       justify-content: center; | ||||||
|       align-items: center; |       align-items: center; | ||||||
|       gap: var(--ha-space-2); |       gap: 8px; | ||||||
|     } |     } | ||||||
|     .chart-legend li { |     .chart-legend li { | ||||||
|       height: 24px; |       height: 24px; | ||||||
| @@ -1038,7 +991,7 @@ export class HaChartBase extends LitElement { | |||||||
|     .chart-legend .bullet { |     .chart-legend .bullet { | ||||||
|       border-width: 1px; |       border-width: 1px; | ||||||
|       border-style: solid; |       border-style: solid; | ||||||
|       border-radius: var(--ha-border-radius-circle); |       border-radius: 50%; | ||||||
|       display: block; |       display: block; | ||||||
|       height: 16px; |       height: 16px; | ||||||
|       width: 16px; |       width: 16px; | ||||||
| @@ -1071,9 +1024,5 @@ declare global { | |||||||
|     "dataset-hidden": { id: string }; |     "dataset-hidden": { id: string }; | ||||||
|     "dataset-unhidden": { id: string }; |     "dataset-unhidden": { id: string }; | ||||||
|     "chart-click": ECElementEvent; |     "chart-click": ECElementEvent; | ||||||
|     "chart-zoom": { |  | ||||||
|       start: number; |  | ||||||
|       end: number; |  | ||||||
|     }; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import type { TopLevelFormatterParams } from "echarts/types/dist/shared"; | |||||||
| import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js"; | import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { listenMediaQuery } from "../../common/dom/media_query"; | import { listenMediaQuery } from "../../common/dom/media_query"; | ||||||
| import type { ECOption } from "../../resources/echarts/echarts"; | import type { ECOption } from "../../resources/echarts"; | ||||||
| import "./ha-chart-base"; | import "./ha-chart-base"; | ||||||
| import type { HaChartBase } from "./ha-chart-base"; | import type { HaChartBase } from "./ha-chart-base"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
|   | |||||||
| @@ -1,15 +1,14 @@ | |||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { LitElement, html, css } from "lit"; | import { LitElement, html, css } from "lit"; | ||||||
| import type { EChartsType } from "echarts/core"; | import type { EChartsType } from "echarts/core"; | ||||||
|  | import type { CallbackDataParams } from "echarts/types/dist/shared"; | ||||||
| import type { SankeySeriesOption } from "echarts/types/dist/echarts"; | import type { SankeySeriesOption } from "echarts/types/dist/echarts"; | ||||||
| import type { CallbackDataParams } from "echarts/types/src/util/types"; | import { SankeyChart } from "echarts/charts"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { ResizeController } from "@lit-labs/observers/resize-controller"; | import { ResizeController } from "@lit-labs/observers/resize-controller"; | ||||||
| import SankeyChart from "../../resources/echarts/components/sankey/install"; |  | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import type { ECOption } from "../../resources/echarts/echarts"; | import type { ECOption } from "../../resources/echarts"; | ||||||
| import { measureTextWidth } from "../../util/text"; | import { measureTextWidth } from "../../util/text"; | ||||||
| import { filterXSS } from "../../common/util/xss"; |  | ||||||
| import "./ha-chart-base"; | import "./ha-chart-base"; | ||||||
| import { NODE_SIZE } from "../trace/hat-graph-const"; | import { NODE_SIZE } from "../trace/hat-graph-const"; | ||||||
| import "../ha-alert"; | import "../ha-alert"; | ||||||
| @@ -39,7 +38,7 @@ type ProcessedLink = Link & { | |||||||
|  |  | ||||||
| const OVERFLOW_MARGIN = 5; | const OVERFLOW_MARGIN = 5; | ||||||
| const FONT_SIZE = 12; | const FONT_SIZE = 12; | ||||||
| const NODE_GAP = 6; | const NODE_GAP = 8; | ||||||
| const LABEL_DISTANCE = 5; | const LABEL_DISTANCE = 5; | ||||||
|  |  | ||||||
| @customElement("ha-sankey-chart") | @customElement("ha-sankey-chart") | ||||||
| @@ -93,12 +92,12 @@ export class HaSankeyChart extends LitElement { | |||||||
|       : data.value; |       : data.value; | ||||||
|     if (data.id) { |     if (data.id) { | ||||||
|       const node = this.data.nodes.find((n) => n.id === data.id); |       const node = this.data.nodes.find((n) => n.id === data.id); | ||||||
|       return `${params.marker} ${filterXSS(node?.label ?? data.id)}<br>${value}`; |       return `${params.marker} ${node?.label ?? data.id}<br>${value}`; | ||||||
|     } |     } | ||||||
|     if (data.source && data.target) { |     if (data.source && data.target) { | ||||||
|       const source = this.data.nodes.find((n) => n.id === data.source); |       const source = this.data.nodes.find((n) => n.id === data.source); | ||||||
|       const target = this.data.nodes.find((n) => n.id === data.target); |       const target = this.data.nodes.find((n) => n.id === data.target); | ||||||
|       return `${filterXSS(source?.label ?? data.source)} → ${filterXSS(target?.label ?? data.target)}<br>${value}`; |       return `${source?.label ?? data.source} → ${target?.label ?? data.target}<br>${value}`; | ||||||
|     } |     } | ||||||
|     return null; |     return null; | ||||||
|   }; |   }; | ||||||
| @@ -164,7 +163,6 @@ export class HaSankeyChart extends LitElement { | |||||||
|       lineStyle: { |       lineStyle: { | ||||||
|         color: "gradient", |         color: "gradient", | ||||||
|         opacity: 0.4, |         opacity: 0.4, | ||||||
|         curveness: 0.5, |  | ||||||
|       }, |       }, | ||||||
|       layoutIterations: 0, |       layoutIterations: 0, | ||||||
|       label: { |       label: { | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ import { computeRTL } from "../../common/util/compute_rtl"; | |||||||
| import type { LineChartEntity, LineChartState } from "../../data/history"; | import type { LineChartEntity, LineChartState } from "../../data/history"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; | import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; | ||||||
| import type { ECOption } from "../../resources/echarts/echarts"; | import type { ECOption } from "../../resources/echarts"; | ||||||
| import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; | import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; | ||||||
| import { | import { | ||||||
|   getNumberFormatOptions, |   getNumberFormatOptions, | ||||||
| @@ -66,9 +66,6 @@ export class StateHistoryChartLine extends LitElement { | |||||||
|   @property({ attribute: "expand-legend", type: Boolean }) |   @property({ attribute: "expand-legend", type: Boolean }) | ||||||
|   public expandLegend?: boolean; |   public expandLegend?: boolean; | ||||||
|  |  | ||||||
|   @property({ attribute: "hide-reset-button", type: Boolean }) |  | ||||||
|   public hideResetButton?: boolean; |  | ||||||
|  |  | ||||||
|   @state() private _chartData: LineSeriesOption[] = []; |   @state() private _chartData: LineSeriesOption[] = []; | ||||||
|  |  | ||||||
|   @state() private _entityIds: string[] = []; |   @state() private _entityIds: string[] = []; | ||||||
| @@ -97,9 +94,7 @@ export class StateHistoryChartLine extends LitElement { | |||||||
|         style=${styleMap({ height: this.height })} |         style=${styleMap({ height: this.height })} | ||||||
|         @dataset-hidden=${this._datasetHidden} |         @dataset-hidden=${this._datasetHidden} | ||||||
|         @dataset-unhidden=${this._datasetUnhidden} |         @dataset-unhidden=${this._datasetUnhidden} | ||||||
|         @chart-zoom=${this._handleDataZoom} |  | ||||||
|         .expandLegend=${this.expandLegend} |         .expandLegend=${this.expandLegend} | ||||||
|         .hideResetButton=${this.hideResetButton} |  | ||||||
|       ></ha-chart-base> |       ></ha-chart-base> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| @@ -197,19 +192,6 @@ export class StateHistoryChartLine extends LitElement { | |||||||
|     this._hiddenStats.delete(ev.detail.id); |     this._hiddenStats.delete(ev.detail.id); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public zoom(start: number, end: number) { |  | ||||||
|     const chartBase = this.shadowRoot!.querySelector("ha-chart-base")!; |  | ||||||
|     chartBase.zoom(start, end, true); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _handleDataZoom(ev: CustomEvent) { |  | ||||||
|     fireEvent(this, "chart-zoom-with-index", { |  | ||||||
|       start: ev.detail.start ?? 0, |  | ||||||
|       end: ev.detail.end ?? 100, |  | ||||||
|       chartIndex: this.chartIndex, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public willUpdate(changedProps: PropertyValues) { |   public willUpdate(changedProps: PropertyValues) { | ||||||
|     if ( |     if ( | ||||||
|       changedProps.has("data") || |       changedProps.has("data") || | ||||||
|   | |||||||
| @@ -15,8 +15,8 @@ import type { TimelineEntity } from "../../data/history"; | |||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; | import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; | ||||||
| import { computeTimelineColor } from "./timeline-color"; | import { computeTimelineColor } from "./timeline-color"; | ||||||
| import type { ECOption } from "../../resources/echarts/echarts"; | import type { ECOption } from "../../resources/echarts"; | ||||||
| import echarts from "../../resources/echarts/echarts"; | import echarts from "../../resources/echarts"; | ||||||
| import { luminosity } from "../../common/color/rgb"; | import { luminosity } from "../../common/color/rgb"; | ||||||
| import { hex2rgb } from "../../common/color/convert-color"; | import { hex2rgb } from "../../common/color/convert-color"; | ||||||
| import { measureTextWidth } from "../../util/text"; | import { measureTextWidth } from "../../util/text"; | ||||||
| @@ -51,9 +51,6 @@ export class StateHistoryChartTimeline extends LitElement { | |||||||
|  |  | ||||||
|   @property({ attribute: false, type: Number }) public chartIndex?; |   @property({ attribute: false, type: Number }) public chartIndex?; | ||||||
|  |  | ||||||
|   @property({ attribute: "hide-reset-button", type: Boolean }) |  | ||||||
|   public hideResetButton?: boolean; |  | ||||||
|  |  | ||||||
|   @state() private _chartData: CustomSeriesOption[] = []; |   @state() private _chartData: CustomSeriesOption[] = []; | ||||||
|  |  | ||||||
|   @state() private _chartOptions?: ECOption; |   @state() private _chartOptions?: ECOption; | ||||||
| @@ -71,8 +68,6 @@ export class StateHistoryChartTimeline extends LitElement { | |||||||
|         .data=${this._chartData as ECOption["series"]} |         .data=${this._chartData as ECOption["series"]} | ||||||
|         small-controls |         small-controls | ||||||
|         @chart-click=${this._handleChartClick} |         @chart-click=${this._handleChartClick} | ||||||
|         @chart-zoom=${this._handleDataZoom} |  | ||||||
|         .hideResetButton=${this.hideResetButton} |  | ||||||
|       ></ha-chart-base> |       ></ha-chart-base> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
| @@ -261,19 +256,6 @@ export class StateHistoryChartTimeline extends LitElement { | |||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public zoom(start: number, end: number) { |  | ||||||
|     const chartBase = this.shadowRoot!.querySelector("ha-chart-base")!; |  | ||||||
|     chartBase.zoom(start, end, true); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _handleDataZoom(ev: CustomEvent) { |  | ||||||
|     fireEvent(this, "chart-zoom-with-index", { |  | ||||||
|       start: ev.detail.start ?? 0, |  | ||||||
|       end: ev.detail.end ?? 100, |  | ||||||
|       chartIndex: this.chartIndex, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _generateData() { |   private _generateData() { | ||||||
|     const computedStyles = getComputedStyle(this); |     const computedStyles = getComputedStyle(this); | ||||||
|     let stateHistory = this.data; |     let stateHistory = this.data; | ||||||
|   | |||||||
| @@ -1,8 +1,7 @@ | |||||||
| import type { PropertyValues } from "lit"; | import type { PropertyValues } from "lit"; | ||||||
| import { css, html, LitElement, nothing } from "lit"; | import { css, html, LitElement } from "lit"; | ||||||
| import { customElement, eventOptions, property, state } from "lit/decorators"; | import { customElement, eventOptions, property, state } from "lit/decorators"; | ||||||
| import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize"; | import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize"; | ||||||
| import { mdiRestart } from "@mdi/js"; |  | ||||||
| import { isComponentLoaded } from "../../common/config/is_component_loaded"; | import { isComponentLoaded } from "../../common/config/is_component_loaded"; | ||||||
| import { restoreScroll } from "../../common/decorators/restore-scroll"; | import { restoreScroll } from "../../common/decorators/restore-scroll"; | ||||||
| import type { | import type { | ||||||
| @@ -12,10 +11,6 @@ import type { | |||||||
| } from "../../data/history"; | } from "../../data/history"; | ||||||
| import { loadVirtualizer } from "../../resources/virtualizer"; | import { loadVirtualizer } from "../../resources/virtualizer"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import type { StateHistoryChartLine } from "./state-history-chart-line"; |  | ||||||
| import type { StateHistoryChartTimeline } from "./state-history-chart-timeline"; |  | ||||||
| import "../ha-fab"; |  | ||||||
| import "../ha-svg-icon"; |  | ||||||
| import "./state-history-chart-line"; | import "./state-history-chart-line"; | ||||||
| import "./state-history-chart-timeline"; | import "./state-history-chart-timeline"; | ||||||
|  |  | ||||||
| @@ -34,11 +29,6 @@ const chunkData = (inputArray: any[], chunks: number) => | |||||||
| declare global { | declare global { | ||||||
|   interface HASSDomEvents { |   interface HASSDomEvents { | ||||||
|     "y-width-changed": { value: number; chartIndex: number }; |     "y-width-changed": { value: number; chartIndex: number }; | ||||||
|     "chart-zoom-with-index": { |  | ||||||
|       start: number; |  | ||||||
|       end: number; |  | ||||||
|       chartIndex: number; |  | ||||||
|     }; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -84,9 +74,6 @@ export class StateHistoryCharts extends LitElement { | |||||||
|   @property({ attribute: "expand-legend", type: Boolean }) |   @property({ attribute: "expand-legend", type: Boolean }) | ||||||
|   public expandLegend?: boolean; |   public expandLegend?: boolean; | ||||||
|  |  | ||||||
|   @property({ attribute: "sync-charts", type: Boolean }) |  | ||||||
|   public syncCharts = false; |  | ||||||
|  |  | ||||||
|   private _computedStartTime!: Date; |   private _computedStartTime!: Date; | ||||||
|  |  | ||||||
|   private _computedEndTime!: Date; |   private _computedEndTime!: Date; | ||||||
| @@ -97,10 +84,6 @@ export class StateHistoryCharts extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _chartCount = 0; |   @state() private _chartCount = 0; | ||||||
|  |  | ||||||
|   @state() private _hasZoomedCharts = false; |  | ||||||
|  |  | ||||||
|   private _isSyncing = false; |  | ||||||
|  |  | ||||||
|   // @ts-ignore |   // @ts-ignore | ||||||
|   @restoreScroll(".container") private _savedScrollPos?: number; |   @restoreScroll(".container") private _savedScrollPos?: number; | ||||||
|  |  | ||||||
| @@ -132,12 +115,8 @@ export class StateHistoryCharts extends LitElement { | |||||||
|     // eslint-disable-next-line lit/no-this-assign-in-render |     // eslint-disable-next-line lit/no-this-assign-in-render | ||||||
|     this._chartCount = combinedItems.length; |     this._chartCount = combinedItems.length; | ||||||
|  |  | ||||||
|     return html` |     return this.virtualize | ||||||
|       ${this.virtualize |       ? html`<div class="container ha-scrollbar" @scroll=${this._saveScrollPos}> | ||||||
|         ? html`<div |  | ||||||
|             class="container ha-scrollbar" |  | ||||||
|             @scroll=${this._saveScrollPos} |  | ||||||
|           > |  | ||||||
|           <lit-virtualizer |           <lit-virtualizer | ||||||
|             scroller |             scroller | ||||||
|             class="ha-scrollbar" |             class="ha-scrollbar" | ||||||
| @@ -148,20 +127,7 @@ export class StateHistoryCharts extends LitElement { | |||||||
|         </div>` |         </div>` | ||||||
|       : html`${combinedItems.map((item, index) => |       : html`${combinedItems.map((item, index) => | ||||||
|           this._renderHistoryItem(item, index) |           this._renderHistoryItem(item, index) | ||||||
|           )}`} |         )}`; | ||||||
|       ${this.syncCharts && this._hasZoomedCharts |  | ||||||
|         ? html`<ha-fab |  | ||||||
|             slot="fab" |  | ||||||
|             class="reset-button" |  | ||||||
|             .label=${this.hass.localize( |  | ||||||
|               "ui.components.history_charts.zoom_reset" |  | ||||||
|             )} |  | ||||||
|             @click=${this._handleGlobalZoomReset} |  | ||||||
|           > |  | ||||||
|             <ha-svg-icon slot="icon" .path=${mdiRestart}></ha-svg-icon> |  | ||||||
|           </ha-fab>` |  | ||||||
|         : nothing} |  | ||||||
|     `; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _renderHistoryItem: RenderItemFunction< |   private _renderHistoryItem: RenderItemFunction< | ||||||
| @@ -190,10 +156,8 @@ export class StateHistoryCharts extends LitElement { | |||||||
|           .maxYAxis=${this.maxYAxis} |           .maxYAxis=${this.maxYAxis} | ||||||
|           .fitYData=${this.fitYData} |           .fitYData=${this.fitYData} | ||||||
|           @y-width-changed=${this._yWidthChanged} |           @y-width-changed=${this._yWidthChanged} | ||||||
|           @chart-zoom-with-index=${this._handleTimelineSync} |  | ||||||
|           .height=${this.virtualize ? undefined : this.height} |           .height=${this.virtualize ? undefined : this.height} | ||||||
|           .expandLegend=${this.expandLegend} |           .expandLegend=${this.expandLegend} | ||||||
|           ?hide-reset-button=${this.syncCharts} |  | ||||||
|         ></state-history-chart-line> |         ></state-history-chart-line> | ||||||
|       </div> `; |       </div> `; | ||||||
|     } |     } | ||||||
| @@ -211,8 +175,6 @@ export class StateHistoryCharts extends LitElement { | |||||||
|         .chartIndex=${index} |         .chartIndex=${index} | ||||||
|         .clickForMoreInfo=${this.clickForMoreInfo} |         .clickForMoreInfo=${this.clickForMoreInfo} | ||||||
|         @y-width-changed=${this._yWidthChanged} |         @y-width-changed=${this._yWidthChanged} | ||||||
|         @chart-zoom-with-index=${this._handleTimelineSync} |  | ||||||
|         ?hide-reset-button=${this.syncCharts} |  | ||||||
|       ></state-history-chart-timeline> |       ></state-history-chart-timeline> | ||||||
|     </div> `; |     </div> `; | ||||||
|   }; |   }; | ||||||
| @@ -302,66 +264,6 @@ export class StateHistoryCharts extends LitElement { | |||||||
|     this._maxYWidth = Math.max(...Object.values(this._childYWidths), 0); |     this._maxYWidth = Math.max(...Object.values(this._childYWidths), 0); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _handleTimelineSync( |  | ||||||
|     e: CustomEvent<HASSDomEvents["chart-zoom-with-index"]> |  | ||||||
|   ) { |  | ||||||
|     if (!this.syncCharts || this._isSyncing) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const { start, end, chartIndex } = e.detail; |  | ||||||
|  |  | ||||||
|     this._hasZoomedCharts = start !== 0 || end !== 100; |  | ||||||
|     this._syncZoomToAllCharts(start, end, chartIndex); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _syncZoomToAllCharts( |  | ||||||
|     start: number, |  | ||||||
|     end: number, |  | ||||||
|     sourceChartIndex?: number |  | ||||||
|   ) { |  | ||||||
|     this._isSyncing = true; |  | ||||||
|  |  | ||||||
|     requestAnimationFrame(() => { |  | ||||||
|       const chartComponents = this.renderRoot.querySelectorAll( |  | ||||||
|         "state-history-chart-line, state-history-chart-timeline" |  | ||||||
|       ) as unknown as (StateHistoryChartLine | StateHistoryChartTimeline)[]; |  | ||||||
|  |  | ||||||
|       chartComponents.forEach((chartComponent, index) => { |  | ||||||
|         if (index === sourceChartIndex) { |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if ("zoom" in chartComponent) { |  | ||||||
|           chartComponent.zoom(start, end); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       this._isSyncing = false; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _handleGlobalZoomReset() { |  | ||||||
|     this._hasZoomedCharts = false; |  | ||||||
|     this._isSyncing = true; |  | ||||||
|  |  | ||||||
|     requestAnimationFrame(() => { |  | ||||||
|       const chartComponents = this.renderRoot.querySelectorAll( |  | ||||||
|         "state-history-chart-line, state-history-chart-timeline" |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       chartComponents.forEach((chartComponent: any) => { |  | ||||||
|         const chartBase = |  | ||||||
|           chartComponent.renderRoot?.querySelector("ha-chart-base"); |  | ||||||
|  |  | ||||||
|         if (chartBase && chartBase.chart) { |  | ||||||
|           chartBase.zoom(0, 100); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|       this._isSyncing = false; |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _isHistoryEmpty(): boolean { |   private _isHistoryEmpty(): boolean { | ||||||
|     const historyDataEmpty = |     const historyDataEmpty = | ||||||
|       !this.historyData || |       !this.historyData || | ||||||
| @@ -443,12 +345,6 @@ export class StateHistoryCharts extends LitElement { | |||||||
|     state-history-chart-line { |     state-history-chart-line { | ||||||
|       width: 100%; |       width: 100%; | ||||||
|     } |     } | ||||||
|     .reset-button { |  | ||||||
|       position: fixed; |  | ||||||
|       bottom: calc(24px + var(--safe-area-inset-bottom)); |  | ||||||
|       right: calc(24px + var(--safe-area-inset-bottom)); |  | ||||||
|       z-index: 1; |  | ||||||
|     } |  | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ import { | |||||||
|   getStatisticMetadata, |   getStatisticMetadata, | ||||||
|   statisticsHaveType, |   statisticsHaveType, | ||||||
| } from "../../data/recorder"; | } from "../../data/recorder"; | ||||||
| import type { ECOption } from "../../resources/echarts/echarts"; | import type { ECOption } from "../../resources/echarts"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import type { CustomLegendOption } from "./ha-chart-base"; | import type { CustomLegendOption } from "./ha-chart-base"; | ||||||
| import "./ha-chart-base"; | import "./ha-chart-base"; | ||||||
|   | |||||||
| @@ -6,8 +6,6 @@ import { computeDomain } from "../../common/entity/compute_domain"; | |||||||
| import { stateColorProperties } from "../../common/entity/state_color"; | import { stateColorProperties } from "../../common/entity/state_color"; | ||||||
| import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; | import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; | ||||||
| import { computeCssValue } from "../../resources/css-variables"; | import { computeCssValue } from "../../resources/css-variables"; | ||||||
| import { computeStateDomain } from "../../common/entity/compute_state_domain"; |  | ||||||
| import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states"; |  | ||||||
|  |  | ||||||
| const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = { | const DOMAIN_STATE_SHADES: Record<string, Record<string, number>> = { | ||||||
|   media_player: { |   media_player: { | ||||||
| @@ -53,28 +51,6 @@ function computeTimelineStateColor( | |||||||
| let colorIndex = 0; | let colorIndex = 0; | ||||||
| const stateColorMap = new Map<string, string>(); | const stateColorMap = new Map<string, string>(); | ||||||
|  |  | ||||||
| function computeTimelineEnumColor( |  | ||||||
|   state: string, |  | ||||||
|   computedStyles: CSSStyleDeclaration, |  | ||||||
|   stateObj?: HassEntity |  | ||||||
| ): string | undefined { |  | ||||||
|   if (!stateObj) { |  | ||||||
|     return undefined; |  | ||||||
|   } |  | ||||||
|   const domain = computeStateDomain(stateObj); |  | ||||||
|   const states = |  | ||||||
|     FIXED_DOMAIN_STATES[domain] || |  | ||||||
|     (domain === "sensor" && |  | ||||||
|       stateObj.attributes.device_class === "enum" && |  | ||||||
|       stateObj.attributes.options) || |  | ||||||
|     []; |  | ||||||
|   const idx = states.indexOf(state); |  | ||||||
|   if (idx === -1) { |  | ||||||
|     return undefined; |  | ||||||
|   } |  | ||||||
|   return getGraphColorByIndex(idx, computedStyles); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function computeTimeLineGenericColor( | function computeTimeLineGenericColor( | ||||||
|   state: string, |   state: string, | ||||||
|   computedStyles: CSSStyleDeclaration |   computedStyles: CSSStyleDeclaration | ||||||
| @@ -95,7 +71,6 @@ export function computeTimelineColor( | |||||||
| ): string { | ): string { | ||||||
|   return ( |   return ( | ||||||
|     computeTimelineStateColor(state, computedStyles, stateObj) || |     computeTimelineStateColor(state, computedStyles, stateObj) || | ||||||
|     computeTimelineEnumColor(state, computedStyles, stateObj) || |  | ||||||
|     computeTimeLineGenericColor(state, computedStyles) |     computeTimeLineGenericColor(state, computedStyles) | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -290,9 +290,7 @@ export class DialogDataTableSettings extends LitElement { | |||||||
|           ha-dialog { |           ha-dialog { | ||||||
|             --vertical-align-dialog: flex-start; |             --vertical-align-dialog: flex-start; | ||||||
|             --dialog-surface-margin-top: 250px; |             --dialog-surface-margin-top: 250px; | ||||||
|             --ha-dialog-border-radius: var(--ha-border-radius-4xl) |             --ha-dialog-border-radius: 28px 28px 0 0; | ||||||
|               var(--ha-border-radius-4xl) var(--ha-border-radius-square) |  | ||||||
|               var(--ha-border-radius-square); |  | ||||||
|             --mdc-dialog-min-height: calc(100% - 250px); |             --mdc-dialog-min-height: calc(100% - 250px); | ||||||
|             --mdc-dialog-max-height: calc(100% - 250px); |             --mdc-dialog-max-height: calc(100% - 250px); | ||||||
|           } |           } | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user