mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-11-04 00:19:47 +00:00 
			
		
		
		
	Compare commits
	
		
			64 Commits
		
	
	
		
			sec_pypi_p
			...
			20251001.4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					3e749ec085 | ||
| 
						 | 
					ee2ec00069 | ||
| 
						 | 
					0aa2941868 | ||
| 
						 | 
					46cd1d5156 | ||
| 
						 | 
					07a5c41fd4 | ||
| 
						 | 
					4ad3c553d5 | ||
| 
						 | 
					d40cc448a5 | ||
| 
						 | 
					e2f3f9d348 | ||
| 
						 | 
					98d44950f8 | ||
| 
						 | 
					8ae9edb1ef | ||
| 
						 | 
					84c4396c13 | ||
| 
						 | 
					2b937a30e3 | ||
| 
						 | 
					b7815bfd86 | ||
| 
						 | 
					d94fa03411 | ||
| 
						 | 
					0a7007ef9e | ||
| 
						 | 
					dd12136dee | ||
| 
						 | 
					6e2f89fe3d | ||
| 
						 | 
					092085b9af | ||
| 
						 | 
					1c06eb8661 | ||
| 
						 | 
					c7e87b06b5 | ||
| 
						 | 
					38c738c199 | ||
| 
						 | 
					e899587307 | ||
| 
						 | 
					c9feb0b75f | ||
| 
						 | 
					10718c35d1 | ||
| 
						 | 
					4dc6a37bad | ||
| 
						 | 
					ac49fc7aba | ||
| 
						 | 
					e4f008800b | ||
| 
						 | 
					0b0ffd7bab | ||
| 
						 | 
					dfa77526a2 | ||
| 
						 | 
					9a3bd6c613 | ||
| 
						 | 
					1161de5746 | ||
| 
						 | 
					9df8e20391 | ||
| 
						 | 
					11047a9c95 | ||
| 
						 | 
					18fa66f61c | ||
| 
						 | 
					758a048f34 | ||
| 
						 | 
					ee0fc360b0 | ||
| 
						 | 
					4012f95ec1 | ||
| 
						 | 
					0336ce4606 | ||
| 
						 | 
					9ba36ab7e2 | ||
| 
						 | 
					fe7a08a1b0 | ||
| 
						 | 
					87a8f9cedc | ||
| 
						 | 
					01df7e20ca | ||
| 
						 | 
					d181219522 | ||
| 
						 | 
					6ae24b8135 | ||
| 
						 | 
					8e009f24f9 | ||
| 
						 | 
					53031f44ac | ||
| 
						 | 
					af5a988457 | ||
| 
						 | 
					bab0391a19 | ||
| 
						 | 
					444123c47e | ||
| 
						 | 
					f123d34046 | ||
| 
						 | 
					1b40f99f68 | ||
| 
						 | 
					b314b3ed2b | ||
| 
						 | 
					59b8932969 | ||
| 
						 | 
					107af753ec | ||
| 
						 | 
					1f0acb3046 | ||
| 
						 | 
					431e533929 | ||
| 
						 | 
					02c845cbc6 | ||
| 
						 | 
					628111ed20 | ||
| 
						 | 
					e825a9c02f | ||
| 
						 | 
					7a35bddf36 | ||
| 
						 | 
					ad69270af8 | ||
| 
						 | 
					404edf9483 | ||
| 
						 | 
					a166b4e9b6 | ||
| 
						 | 
					7a285f11db | 
							
								
								
									
										4
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							@@ -26,7 +26,7 @@ jobs:
 | 
			
		||||
          ref: dev
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
			
		||||
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
@@ -61,7 +61,7 @@ jobs:
 | 
			
		||||
          ref: master
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
			
		||||
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							@@ -26,7 +26,7 @@ jobs:
 | 
			
		||||
      - name: Check out files from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
			
		||||
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
@@ -37,7 +37,7 @@ jobs:
 | 
			
		||||
      - name: Build resources
 | 
			
		||||
        run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
 | 
			
		||||
      - name: Setup lint cache
 | 
			
		||||
        uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
 | 
			
		||||
        with:
 | 
			
		||||
          path: |
 | 
			
		||||
            node_modules/.cache/prettier
 | 
			
		||||
@@ -60,7 +60,7 @@ jobs:
 | 
			
		||||
      - name: Check out files from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
			
		||||
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
@@ -78,7 +78,7 @@ jobs:
 | 
			
		||||
      - name: Check out files from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
			
		||||
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
@@ -102,7 +102,7 @@ jobs:
 | 
			
		||||
      - name: Check out files from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
			
		||||
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							@@ -36,14 +36,14 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      # Initializes the CodeQL tools for scanning.
 | 
			
		||||
      - name: Initialize CodeQL
 | 
			
		||||
        uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
 | 
			
		||||
        uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
 | 
			
		||||
        with:
 | 
			
		||||
          languages: ${{ matrix.language }}
 | 
			
		||||
 | 
			
		||||
      # 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)
 | 
			
		||||
      - name: Autobuild
 | 
			
		||||
        uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
 | 
			
		||||
        uses: github/codeql-action/autobuild@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
 | 
			
		||||
 | 
			
		||||
      # ℹ️ Command-line programs to run using the OS shell.
 | 
			
		||||
      # 📚 https://git.io/JvXDl
 | 
			
		||||
@@ -57,4 +57,4 @@ jobs:
 | 
			
		||||
      #   make release
 | 
			
		||||
 | 
			
		||||
      - name: Perform CodeQL Analysis
 | 
			
		||||
        uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
 | 
			
		||||
        uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							@@ -27,7 +27,7 @@ jobs:
 | 
			
		||||
          ref: dev
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
			
		||||
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
@@ -62,7 +62,7 @@ jobs:
 | 
			
		||||
          ref: master
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
			
		||||
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							@@ -19,7 +19,7 @@ jobs:
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
			
		||||
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							@@ -24,7 +24,7 @@ jobs:
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
			
		||||
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							@@ -28,7 +28,7 @@ jobs:
 | 
			
		||||
          python-version: ${{ env.PYTHON_VERSION }}
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
			
		||||
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/relative-ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/relative-ci.yaml
									
									
									
									
										vendored
									
									
								
							@@ -17,7 +17,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Send bundle stats and build information to RelativeCI
 | 
			
		||||
        uses: relative-ci/agent-action@8504826a02078b05756e4c07e380023cc2c4274a # v3.1.0
 | 
			
		||||
        uses: relative-ci/agent-action@1707825cbfcc7452b2913d273414705415ae64d4 # v3.0.1
 | 
			
		||||
        with:
 | 
			
		||||
          key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
 | 
			
		||||
          token: ${{ github.token }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										27
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							@@ -19,11 +19,8 @@ jobs:
 | 
			
		||||
  release:
 | 
			
		||||
    name: Release
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    environment: pypi
 | 
			
		||||
    permissions:
 | 
			
		||||
      contents: write # Required to upload release assets
 | 
			
		||||
      id-token: write # For "Trusted Publisher" to PyPi
 | 
			
		||||
    if: github.repository_owner == 'home-assistant'
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout the repository
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
@@ -37,7 +34,7 @@ jobs:
 | 
			
		||||
        uses: home-assistant/actions/helpers/verify-version@master
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
			
		||||
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
@@ -49,20 +46,16 @@ jobs:
 | 
			
		||||
        run: ./script/translations_download
 | 
			
		||||
        env:
 | 
			
		||||
          LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
 | 
			
		||||
 | 
			
		||||
      - name: Build and release package
 | 
			
		||||
        run: |
 | 
			
		||||
          python3 -m pip install build
 | 
			
		||||
          python3 -m pip install twine build
 | 
			
		||||
          export TWINE_USERNAME="__token__"
 | 
			
		||||
          export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
 | 
			
		||||
          export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
 | 
			
		||||
          script/release
 | 
			
		||||
 | 
			
		||||
      - name: Publish to PyPI
 | 
			
		||||
        uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
 | 
			
		||||
        with:
 | 
			
		||||
          skip-existing: true
 | 
			
		||||
 | 
			
		||||
      - name: Upload release assets
 | 
			
		||||
        uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
 | 
			
		||||
        uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
 | 
			
		||||
        with:
 | 
			
		||||
          files: |
 | 
			
		||||
            dist/*.whl
 | 
			
		||||
@@ -82,7 +75,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      # home-assistant/wheels doesn't support SHA pinning
 | 
			
		||||
      - name: Build wheels
 | 
			
		||||
        uses: home-assistant/wheels@2025.10.0
 | 
			
		||||
        uses: home-assistant/wheels@2025.07.0
 | 
			
		||||
        with:
 | 
			
		||||
          abi: cp313
 | 
			
		||||
          tag: musllinux_1_2
 | 
			
		||||
@@ -100,7 +93,7 @@ jobs:
 | 
			
		||||
      - name: Checkout the repository
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
			
		||||
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
@@ -115,7 +108,7 @@ jobs:
 | 
			
		||||
      - name: Tar folder
 | 
			
		||||
        run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
 | 
			
		||||
      - name: Upload release asset
 | 
			
		||||
        uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
 | 
			
		||||
        uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
 | 
			
		||||
        with:
 | 
			
		||||
          files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
 | 
			
		||||
 | 
			
		||||
@@ -129,7 +122,7 @@ jobs:
 | 
			
		||||
      - name: Checkout the repository
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
			
		||||
        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
@@ -144,6 +137,6 @@ jobs:
 | 
			
		||||
      - name: Tar folder
 | 
			
		||||
        run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
 | 
			
		||||
      - name: Upload release asset
 | 
			
		||||
        uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
 | 
			
		||||
        uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
 | 
			
		||||
        with:
 | 
			
		||||
          files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							@@ -10,7 +10,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: 90 days stale policy
 | 
			
		||||
        uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
 | 
			
		||||
        uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          repo-token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          days-before-stale: 90
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -6,4 +6,4 @@ enableGlobalCache: false
 | 
			
		||||
 | 
			
		||||
nodeLinker: node-modules
 | 
			
		||||
 | 
			
		||||
yarnPath: .yarn/releases/yarn-4.10.3.cjs
 | 
			
		||||
yarnPath: .yarn/releases/yarn-4.10.2.cjs
 | 
			
		||||
 
 | 
			
		||||
@@ -183,6 +183,7 @@ module.exports.babelOptions = ({
 | 
			
		||||
      include: /\/node_modules\//,
 | 
			
		||||
      exclude: [
 | 
			
		||||
        "element-internals-polyfill",
 | 
			
		||||
        "@shoelace-style",
 | 
			
		||||
        "@?lit(?:-labs|-element|-html)?",
 | 
			
		||||
      ].map((p) => new RegExp(`/node_modules/${p}/`)),
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -242,7 +242,7 @@ class HcCast extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .question:before {
 | 
			
		||||
      border-radius: var(--ha-border-radius-sm);
 | 
			
		||||
      border-radius: 4px;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      right: 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -95,8 +95,7 @@ class HcLayout extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .hero {
 | 
			
		||||
      border-radius: var(--ha-border-radius-sm) var(--ha-border-radius-sm)
 | 
			
		||||
        var(--ha-border-radius-square) var(--ha-border-radius-square);
 | 
			
		||||
      border-radius: 4px 4px 0 0;
 | 
			
		||||
    }
 | 
			
		||||
    .subtitle {
 | 
			
		||||
      font-size: var(--ha-font-size-m);
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,7 @@ export const castDemoEntities: () => Entity[] = () =>
 | 
			
		||||
        longitude: 4.8903147,
 | 
			
		||||
        radius: 100,
 | 
			
		||||
        friendly_name: "Home",
 | 
			
		||||
        icon: "mdi:home",
 | 
			
		||||
        icon: "hass:home",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    "input_number.harmonyvolume": {
 | 
			
		||||
@@ -88,7 +88,7 @@ export const castDemoEntities: () => Entity[] = () =>
 | 
			
		||||
        step: 1,
 | 
			
		||||
        mode: "slider",
 | 
			
		||||
        friendly_name: "Volume",
 | 
			
		||||
        icon: "mdi:volume-high",
 | 
			
		||||
        icon: "hass:volume-high",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    "climate.upstairs": {
 | 
			
		||||
 
 | 
			
		||||
@@ -56,7 +56,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => {
 | 
			
		||||
                type: "weblink",
 | 
			
		||||
                url: "/lovelace/climate",
 | 
			
		||||
                name: "Climate controls",
 | 
			
		||||
                icon: "mdi:arrow-right",
 | 
			
		||||
                icon: "hass:arrow-right",
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
@@ -76,7 +76,7 @@ export const castDemoLovelace: () => LovelaceConfig = () => {
 | 
			
		||||
                type: "weblink",
 | 
			
		||||
                url: "/lovelace/overview",
 | 
			
		||||
                name: "Back",
 | 
			
		||||
                icon: "mdi:arrow-left",
 | 
			
		||||
                icon: "hass:arrow-left",
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
 
 | 
			
		||||
@@ -143,7 +143,7 @@ export const demoEntitiesArsaboo: DemoConfig["entities"] = (localize) =>
 | 
			
		||||
      state: "on",
 | 
			
		||||
      attributes: {
 | 
			
		||||
        friendly_name: "Home Automation",
 | 
			
		||||
        icon: "mdi:home-automation",
 | 
			
		||||
        icon: "hass:home-automation",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    "input_boolean.tvtime": {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ export const demoLovelaceArsaboo: DemoConfig["lovelace"] = (localize) => ({
 | 
			
		||||
  title: "Home Assistant",
 | 
			
		||||
  views: [
 | 
			
		||||
    {
 | 
			
		||||
      icon: "mdi:home-assistant",
 | 
			
		||||
      icon: "hass:home-assistant",
 | 
			
		||||
      id: "home",
 | 
			
		||||
      title: "Home",
 | 
			
		||||
      cards: [
 | 
			
		||||
 
 | 
			
		||||
@@ -1236,7 +1236,7 @@ export const demoLovelaceJimpower: DemoConfig["lovelace"] = () => ({
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      path: "security",
 | 
			
		||||
      icon: "mdi:shield-home",
 | 
			
		||||
      icon: "hass:shield-home",
 | 
			
		||||
      name: "Security",
 | 
			
		||||
      background:
 | 
			
		||||
        'center / cover no-repeat url("/assets/jimpower/background-15.jpg") fixed',
 | 
			
		||||
 
 | 
			
		||||
@@ -208,7 +208,7 @@ class HaGallery extends LitElement {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .sidebar a[active]::before {
 | 
			
		||||
        border-radius: var(--ha-border-radius-lg);
 | 
			
		||||
        border-radius: 12px;
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        top: 0;
 | 
			
		||||
        right: 2px;
 | 
			
		||||
@@ -241,7 +241,7 @@ class HaGallery extends LitElement {
 | 
			
		||||
        text-align: center;
 | 
			
		||||
        margin: 16px;
 | 
			
		||||
        padding: 16px;
 | 
			
		||||
        border-radius: var(--ha-border-radius-lg);
 | 
			
		||||
        border-radius: 12px;
 | 
			
		||||
        background-color: var(--primary-background-color);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -117,7 +117,7 @@ export class DemoHaBadge extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
    .card-content {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      gap: var(--ha-space-6);
 | 
			
		||||
      gap: 24px;
 | 
			
		||||
    }
 | 
			
		||||
  `;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -155,11 +155,11 @@ export class DemoHaButton extends LitElement {
 | 
			
		||||
    .card-content {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      gap: var(--ha-space-6);
 | 
			
		||||
      gap: 24px;
 | 
			
		||||
    }
 | 
			
		||||
    .card-content div {
 | 
			
		||||
      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 { ifDefined } from "lit/directives/if-defined";
 | 
			
		||||
import { repeat } from "lit/directives/repeat";
 | 
			
		||||
import "../../../../src/components/ha-card";
 | 
			
		||||
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-control-button-group";
 | 
			
		||||
 | 
			
		||||
interface Button {
 | 
			
		||||
  label: string;
 | 
			
		||||
@@ -156,17 +156,17 @@ export class DemoHaBarButton extends LitElement {
 | 
			
		||||
      --control-button-icon-color: var(--primary-color);
 | 
			
		||||
      --control-button-background-color: var(--primary-color);
 | 
			
		||||
      --control-button-background-opacity: 0.2;
 | 
			
		||||
      --control-button-border-radius: var(--ha-border-radius-xl);
 | 
			
		||||
      --control-button-border-radius: 18px;
 | 
			
		||||
      height: 100px;
 | 
			
		||||
      width: 100px;
 | 
			
		||||
    }
 | 
			
		||||
    .custom-group {
 | 
			
		||||
      --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;
 | 
			
		||||
    }
 | 
			
		||||
    .custom-group ha-control-button {
 | 
			
		||||
      --control-button-border-radius: var(--ha-border-radius-xl);
 | 
			
		||||
      --control-button-border-radius: 18px;
 | 
			
		||||
      --mdc-icon-size: 32px;
 | 
			
		||||
    }
 | 
			
		||||
    .vertical-buttons {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
import type { TemplateResult } from "lit";
 | 
			
		||||
import { LitElement, css, html } from "lit";
 | 
			
		||||
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-control-number-buttons";
 | 
			
		||||
import { repeat } from "lit/directives/repeat";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined";
 | 
			
		||||
 | 
			
		||||
const buttons: {
 | 
			
		||||
  id: string;
 | 
			
		||||
@@ -94,7 +94,7 @@ export class DemoHarControlNumberButtons extends LitElement {
 | 
			
		||||
      --control-number-buttons-background-color: #2196f3;
 | 
			
		||||
      --control-number-buttons-background-opacity: 0.1;
 | 
			
		||||
      --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-background-color: var(--primary-color);
 | 
			
		||||
      --control-button-background-opacity: 0.2;
 | 
			
		||||
      --control-button-border-radius: var(--ha-border-radius-xl);
 | 
			
		||||
      --control-button-border-radius: 18px;
 | 
			
		||||
      height: 100px;
 | 
			
		||||
      width: 100px;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -187,7 +187,7 @@ export class DemoHaControlSelect extends LitElement {
 | 
			
		||||
      --mdc-icon-size: 24px;
 | 
			
		||||
      --control-select-color: var(--state-fan-active-color);
 | 
			
		||||
      --control-select-thickness: 130px;
 | 
			
		||||
      --control-select-border-radius: var(--ha-border-radius-6xl);
 | 
			
		||||
      --control-select-border-radius: 36px;
 | 
			
		||||
    }
 | 
			
		||||
    .vertical-selects {
 | 
			
		||||
      height: 300px;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,8 @@ import { css, html, LitElement } from "lit";
 | 
			
		||||
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-control-slider";
 | 
			
		||||
import "../../../../src/components/ha-card";
 | 
			
		||||
 | 
			
		||||
const sliders: {
 | 
			
		||||
  id: string;
 | 
			
		||||
@@ -151,7 +151,7 @@ export class DemoHaBarSlider extends LitElement {
 | 
			
		||||
      --control-slider-background: #ffcf4c;
 | 
			
		||||
      --control-slider-background-opacity: 0.2;
 | 
			
		||||
      --control-slider-thickness: 130px;
 | 
			
		||||
      --control-slider-border-radius: var(--ha-border-radius-6xl);
 | 
			
		||||
      --control-slider-border-radius: 36px;
 | 
			
		||||
    }
 | 
			
		||||
    .vertical-sliders {
 | 
			
		||||
      height: 300px;
 | 
			
		||||
 
 | 
			
		||||
@@ -9,8 +9,8 @@ import { css, html, LitElement } from "lit";
 | 
			
		||||
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-control-switch";
 | 
			
		||||
import "../../../../src/components/ha-card";
 | 
			
		||||
 | 
			
		||||
const switches: {
 | 
			
		||||
  id: string;
 | 
			
		||||
@@ -118,7 +118,7 @@ export class DemoHaControlSwitch extends LitElement {
 | 
			
		||||
      --control-switch-on-color: var(--green-color);
 | 
			
		||||
      --control-switch-off-color: var(--red-color);
 | 
			
		||||
      --control-switch-thickness: 130px;
 | 
			
		||||
      --control-switch-border-radius: var(--ha-border-radius-6xl);
 | 
			
		||||
      --control-switch-border-radius: 36px;
 | 
			
		||||
      --control-switch-padding: 6px;
 | 
			
		||||
      --mdc-icon-size: 24px;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -123,11 +123,11 @@ export class DemoHaProgressButton extends LitElement {
 | 
			
		||||
    .card-content {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      gap: var(--ha-space-6);
 | 
			
		||||
      gap: 24px;
 | 
			
		||||
    }
 | 
			
		||||
    .card-content div {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      gap: var(--ha-space-2);
 | 
			
		||||
      gap: 8px;
 | 
			
		||||
    }
 | 
			
		||||
  `;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -131,7 +131,7 @@ export class DemoHaSelectBox extends LitElement {
 | 
			
		||||
      --mdc-icon-size: 24px;
 | 
			
		||||
      --control-select-color: var(--state-fan-active-color);
 | 
			
		||||
      --control-select-thickness: 130px;
 | 
			
		||||
      --control-select-border-radius: var(--ha-border-radius-6xl);
 | 
			
		||||
      --control-select-border-radius: 36px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    p.title {
 | 
			
		||||
 
 | 
			
		||||
@@ -79,7 +79,7 @@ export class DemoHaSlider extends LitElement {
 | 
			
		||||
      background-color: var(--primary-background-color);
 | 
			
		||||
      padding: 0 50px;
 | 
			
		||||
      margin: 16px;
 | 
			
		||||
      border-radius: var(--ha-border-radius-md);
 | 
			
		||||
      border-radius: 8px;
 | 
			
		||||
    }
 | 
			
		||||
    ha-card {
 | 
			
		||||
      margin: 24px auto;
 | 
			
		||||
@@ -88,7 +88,7 @@ export class DemoHaSlider extends LitElement {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: var(--ha-space-6);
 | 
			
		||||
      gap: 24px;
 | 
			
		||||
    }
 | 
			
		||||
  `;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,7 @@ export class DemoHaSpinner extends LitElement {
 | 
			
		||||
      background-color: var(--primary-background-color);
 | 
			
		||||
      padding: 0 50px;
 | 
			
		||||
      margin: 16px;
 | 
			
		||||
      border-radius: var(--ha-border-radius-md);
 | 
			
		||||
      border-radius: 8px;
 | 
			
		||||
    }
 | 
			
		||||
    ha-card {
 | 
			
		||||
      margin: 24px auto;
 | 
			
		||||
@@ -70,7 +70,7 @@ export class DemoHaSpinner extends LitElement {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: var(--ha-space-6);
 | 
			
		||||
      gap: 24px;
 | 
			
		||||
    }
 | 
			
		||||
  `;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 { customElement, property } from "lit/decorators";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { mockIcons } from "../../../../demo/src/stubs/icons";
 | 
			
		||||
import { computeDomain } from "../../../../src/common/entity/compute_domain";
 | 
			
		||||
import { computeStateDisplay } from "../../../../src/common/entity/compute_state_display";
 | 
			
		||||
import "../../../../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 { provideHass } from "../../../../src/fake_data/provide_hass";
 | 
			
		||||
import { mockIcons } from "../../../../demo/src/stubs/icons";
 | 
			
		||||
import type { HomeAssistant } from "../../../../src/types";
 | 
			
		||||
 | 
			
		||||
const SENSOR_DEVICE_CLASSES = [
 | 
			
		||||
@@ -434,7 +434,7 @@ export class DemoEntityState extends LitElement {
 | 
			
		||||
      display: block;
 | 
			
		||||
      height: 20px;
 | 
			
		||||
      width: 20px;
 | 
			
		||||
      border-radius: var(--ha-border-radius-md);
 | 
			
		||||
      border-radius: 10px;
 | 
			
		||||
      background-color: rgb(--color);
 | 
			
		||||
    }
 | 
			
		||||
  `;
 | 
			
		||||
 
 | 
			
		||||
@@ -121,7 +121,7 @@ class HassioCardContent extends LitElement {
 | 
			
		||||
      height: 12px;
 | 
			
		||||
      top: 8px;
 | 
			
		||||
      right: 8px;
 | 
			
		||||
      border-radius: var(--ha-border-radius-circle);
 | 
			
		||||
      border-radius: 50%;
 | 
			
		||||
    }
 | 
			
		||||
    .topbar {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
 
 | 
			
		||||
@@ -164,7 +164,7 @@ class HassioHardwareDialog extends LitElement {
 | 
			
		||||
        pre,
 | 
			
		||||
        code {
 | 
			
		||||
          background-color: var(--markdown-code-background-color, none);
 | 
			
		||||
          border-radius: var(--ha-border-radius-sm);
 | 
			
		||||
          border-radius: 3px;
 | 
			
		||||
        }
 | 
			
		||||
        pre {
 | 
			
		||||
          padding: 16px;
 | 
			
		||||
 
 | 
			
		||||
@@ -228,7 +228,7 @@ class HassioRegistriesDialog extends LitElement {
 | 
			
		||||
      css`
 | 
			
		||||
        .registry {
 | 
			
		||||
          border: 1px solid var(--divider-color);
 | 
			
		||||
          border-radius: var(--ha-border-radius-sm);
 | 
			
		||||
          border-radius: 4px;
 | 
			
		||||
          margin-top: 4px;
 | 
			
		||||
        }
 | 
			
		||||
        .action {
 | 
			
		||||
 
 | 
			
		||||
@@ -193,7 +193,7 @@ class HassioRepositoriesDialog extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
        .option {
 | 
			
		||||
          border: 1px solid var(--divider-color);
 | 
			
		||||
          border-radius: var(--ha-border-radius-sm);
 | 
			
		||||
          border-radius: 4px;
 | 
			
		||||
          margin-top: 4px;
 | 
			
		||||
        }
 | 
			
		||||
        ha-button {
 | 
			
		||||
 
 | 
			
		||||
@@ -159,7 +159,7 @@ class HassioSystemManagedDialog extends LitElement {
 | 
			
		||||
          display: flex;
 | 
			
		||||
          justify-content: center;
 | 
			
		||||
          align-items: center;
 | 
			
		||||
          gap: var(--ha-space-4);
 | 
			
		||||
          gap: 16px;
 | 
			
		||||
          --mdc-icon-size: 48px;
 | 
			
		||||
          margin-bottom: 32px;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,7 @@ export const hassioStyle = css`
 | 
			
		||||
  .card-group {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
 | 
			
		||||
    grid-gap: var(--ha-space-2);
 | 
			
		||||
    grid-gap: 8px;
 | 
			
		||||
  }
 | 
			
		||||
  @media screen and (min-width: 640px) {
 | 
			
		||||
    .card-group {
 | 
			
		||||
 
 | 
			
		||||
@@ -302,7 +302,7 @@ class LandingPageLogs extends LitElement {
 | 
			
		||||
        max-height: 300px;
 | 
			
		||||
        overflow: auto;
 | 
			
		||||
        border: 1px solid var(--divider-color);
 | 
			
		||||
        border-radius: var(--ha-border-radius-sm);
 | 
			
		||||
        border-radius: 4px;
 | 
			
		||||
        padding: 4px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -213,7 +213,7 @@ class HaLandingPage extends LandingPageBaseElement {
 | 
			
		||||
      ha-card .card-content {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        gap: var(--ha-space-4);
 | 
			
		||||
        gap: 16px;
 | 
			
		||||
      }
 | 
			
		||||
      ha-alert p {
 | 
			
		||||
        text-align: unset;
 | 
			
		||||
@@ -221,7 +221,7 @@ class HaLandingPage extends LandingPageBaseElement {
 | 
			
		||||
      ha-language-picker {
 | 
			
		||||
        display: block;
 | 
			
		||||
        width: 200px;
 | 
			
		||||
        border-radius: var(--ha-border-radius-sm);
 | 
			
		||||
        border-radius: 4px;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        --ha-select-height: 40px;
 | 
			
		||||
        --mdc-select-fill-color: none;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										82
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										82
									
								
								package.json
									
									
									
									
									
								
							@@ -28,32 +28,32 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@babel/runtime": "7.28.4",
 | 
			
		||||
    "@braintree/sanitize-url": "7.1.1",
 | 
			
		||||
    "@codemirror/autocomplete": "6.19.1",
 | 
			
		||||
    "@codemirror/commands": "6.10.0",
 | 
			
		||||
    "@codemirror/autocomplete": "6.18.7",
 | 
			
		||||
    "@codemirror/commands": "6.8.1",
 | 
			
		||||
    "@codemirror/language": "6.11.3",
 | 
			
		||||
    "@codemirror/legacy-modes": "6.5.2",
 | 
			
		||||
    "@codemirror/legacy-modes": "6.5.1",
 | 
			
		||||
    "@codemirror/search": "6.5.11",
 | 
			
		||||
    "@codemirror/state": "6.5.2",
 | 
			
		||||
    "@codemirror/view": "6.38.6",
 | 
			
		||||
    "@codemirror/view": "6.38.2",
 | 
			
		||||
    "@date-fns/tz": "1.4.1",
 | 
			
		||||
    "@egjs/hammerjs": "2.0.17",
 | 
			
		||||
    "@formatjs/intl-datetimeformat": "6.18.2",
 | 
			
		||||
    "@formatjs/intl-displaynames": "6.8.13",
 | 
			
		||||
    "@formatjs/intl-durationformat": "0.7.6",
 | 
			
		||||
    "@formatjs/intl-getcanonicallocales": "2.5.6",
 | 
			
		||||
    "@formatjs/intl-listformat": "7.7.13",
 | 
			
		||||
    "@formatjs/intl-locale": "4.2.13",
 | 
			
		||||
    "@formatjs/intl-numberformat": "8.15.6",
 | 
			
		||||
    "@formatjs/intl-pluralrules": "5.4.6",
 | 
			
		||||
    "@formatjs/intl-relativetimeformat": "11.4.13",
 | 
			
		||||
    "@formatjs/intl-datetimeformat": "6.18.0",
 | 
			
		||||
    "@formatjs/intl-displaynames": "6.8.11",
 | 
			
		||||
    "@formatjs/intl-durationformat": "0.7.4",
 | 
			
		||||
    "@formatjs/intl-getcanonicallocales": "2.5.5",
 | 
			
		||||
    "@formatjs/intl-listformat": "7.7.11",
 | 
			
		||||
    "@formatjs/intl-locale": "4.2.11",
 | 
			
		||||
    "@formatjs/intl-numberformat": "8.15.4",
 | 
			
		||||
    "@formatjs/intl-pluralrules": "5.4.4",
 | 
			
		||||
    "@formatjs/intl-relativetimeformat": "11.4.11",
 | 
			
		||||
    "@fullcalendar/core": "6.1.19",
 | 
			
		||||
    "@fullcalendar/daygrid": "6.1.19",
 | 
			
		||||
    "@fullcalendar/interaction": "6.1.19",
 | 
			
		||||
    "@fullcalendar/list": "6.1.19",
 | 
			
		||||
    "@fullcalendar/luxon3": "6.1.19",
 | 
			
		||||
    "@fullcalendar/timegrid": "6.1.19",
 | 
			
		||||
    "@home-assistant/webawesome": "3.0.0-beta.6.ha.6",
 | 
			
		||||
    "@lezer/highlight": "1.2.2",
 | 
			
		||||
    "@home-assistant/webawesome": "3.0.0-beta.6.ha.4",
 | 
			
		||||
    "@lezer/highlight": "1.2.1",
 | 
			
		||||
    "@lit-labs/motion": "1.0.9",
 | 
			
		||||
    "@lit-labs/observers": "2.0.6",
 | 
			
		||||
    "@lit-labs/virtualizer": "2.1.1",
 | 
			
		||||
@@ -89,17 +89,17 @@
 | 
			
		||||
    "@thomasloven/round-slider": "0.6.0",
 | 
			
		||||
    "@tsparticles/engine": "3.9.1",
 | 
			
		||||
    "@tsparticles/preset-links": "3.2.0",
 | 
			
		||||
    "@vaadin/combo-box": "24.9.2",
 | 
			
		||||
    "@vaadin/vaadin-themable-mixin": "24.9.2",
 | 
			
		||||
    "@vaadin/combo-box": "24.9.0",
 | 
			
		||||
    "@vaadin/vaadin-themable-mixin": "24.9.0",
 | 
			
		||||
    "@vibrant/color": "4.0.0",
 | 
			
		||||
    "@vue/web-component-wrapper": "1.3.0",
 | 
			
		||||
    "@webcomponents/scoped-custom-element-registry": "0.0.10",
 | 
			
		||||
    "@webcomponents/webcomponentsjs": "2.8.0",
 | 
			
		||||
    "app-datepicker": "5.1.1",
 | 
			
		||||
    "barcode-detector": "3.0.6",
 | 
			
		||||
    "barcode-detector": "3.0.5",
 | 
			
		||||
    "color-name": "2.0.2",
 | 
			
		||||
    "comlink": "4.4.2",
 | 
			
		||||
    "core-js": "3.46.0",
 | 
			
		||||
    "core-js": "3.45.1",
 | 
			
		||||
    "cropperjs": "1.6.2",
 | 
			
		||||
    "culori": "4.0.2",
 | 
			
		||||
    "date-fns": "4.1.0",
 | 
			
		||||
@@ -111,10 +111,10 @@
 | 
			
		||||
    "fuse.js": "7.1.0",
 | 
			
		||||
    "google-timezones-json": "1.2.0",
 | 
			
		||||
    "gulp-zopfli-green": "6.0.2",
 | 
			
		||||
    "hls.js": "1.6.13",
 | 
			
		||||
    "hls.js": "1.6.12",
 | 
			
		||||
    "home-assistant-js-websocket": "9.5.0",
 | 
			
		||||
    "idb-keyval": "6.2.2",
 | 
			
		||||
    "intl-messageformat": "10.7.18",
 | 
			
		||||
    "intl-messageformat": "10.7.16",
 | 
			
		||||
    "js-yaml": "4.1.0",
 | 
			
		||||
    "leaflet": "1.9.4",
 | 
			
		||||
    "leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
 | 
			
		||||
@@ -122,7 +122,7 @@
 | 
			
		||||
    "lit": "3.3.1",
 | 
			
		||||
    "lit-html": "3.3.1",
 | 
			
		||||
    "luxon": "3.7.2",
 | 
			
		||||
    "marked": "16.4.1",
 | 
			
		||||
    "marked": "16.3.0",
 | 
			
		||||
    "memoize-one": "6.0.0",
 | 
			
		||||
    "node-vibrant": "4.0.3",
 | 
			
		||||
    "object-hash": "3.0.0",
 | 
			
		||||
@@ -135,7 +135,7 @@
 | 
			
		||||
    "stacktrace-js": "2.0.2",
 | 
			
		||||
    "superstruct": "2.0.2",
 | 
			
		||||
    "tinykeys": "3.0.0",
 | 
			
		||||
    "ua-parser-js": "2.0.6",
 | 
			
		||||
    "ua-parser-js": "2.0.5",
 | 
			
		||||
    "vue": "2.7.16",
 | 
			
		||||
    "vue2-daterange-picker": "0.6.8",
 | 
			
		||||
    "weekstart": "2.0.0",
 | 
			
		||||
@@ -152,13 +152,13 @@
 | 
			
		||||
    "@babel/helper-define-polyfill-provider": "0.6.5",
 | 
			
		||||
    "@babel/plugin-transform-runtime": "7.28.3",
 | 
			
		||||
    "@babel/preset-env": "7.28.3",
 | 
			
		||||
    "@bundle-stats/plugin-webpack-filter": "4.21.5",
 | 
			
		||||
    "@lokalise/node-api": "15.3.1",
 | 
			
		||||
    "@octokit/auth-oauth-device": "8.0.2",
 | 
			
		||||
    "@octokit/plugin-retry": "8.0.2",
 | 
			
		||||
    "@bundle-stats/plugin-webpack-filter": "4.21.3",
 | 
			
		||||
    "@lokalise/node-api": "15.2.1",
 | 
			
		||||
    "@octokit/auth-oauth-device": "8.0.1",
 | 
			
		||||
    "@octokit/plugin-retry": "8.0.1",
 | 
			
		||||
    "@octokit/rest": "22.0.0",
 | 
			
		||||
    "@rsdoctor/rspack-plugin": "1.3.4",
 | 
			
		||||
    "@rspack/core": "1.5.8",
 | 
			
		||||
    "@rsdoctor/rspack-plugin": "1.2.3",
 | 
			
		||||
    "@rspack/core": "1.5.5",
 | 
			
		||||
    "@rspack/dev-server": "1.1.4",
 | 
			
		||||
    "@types/babel__plugin-transform-runtime": "7.9.5",
 | 
			
		||||
    "@types/chromecast-caf-receiver": "6.0.22",
 | 
			
		||||
@@ -167,7 +167,7 @@
 | 
			
		||||
    "@types/culori": "4.0.1",
 | 
			
		||||
    "@types/html-minifier-terser": "7.0.2",
 | 
			
		||||
    "@types/js-yaml": "4.0.9",
 | 
			
		||||
    "@types/leaflet": "1.9.21",
 | 
			
		||||
    "@types/leaflet": "1.9.20",
 | 
			
		||||
    "@types/leaflet-draw": "1.0.13",
 | 
			
		||||
    "@types/leaflet.markercluster": "1.5.6",
 | 
			
		||||
    "@types/lodash.merge": "4.6.9",
 | 
			
		||||
@@ -178,20 +178,20 @@
 | 
			
		||||
    "@types/tar": "6.1.13",
 | 
			
		||||
    "@types/ua-parser-js": "0.7.39",
 | 
			
		||||
    "@types/webspeechapi": "0.0.29",
 | 
			
		||||
    "@vitest/coverage-v8": "4.0.1",
 | 
			
		||||
    "@vitest/coverage-v8": "3.2.4",
 | 
			
		||||
    "babel-loader": "10.0.0",
 | 
			
		||||
    "babel-plugin-template-html-minifier": "4.1.0",
 | 
			
		||||
    "browserslist-useragent-regexp": "4.1.3",
 | 
			
		||||
    "del": "8.0.1",
 | 
			
		||||
    "eslint": "9.38.0",
 | 
			
		||||
    "eslint": "9.36.0",
 | 
			
		||||
    "eslint-config-airbnb-base": "15.0.0",
 | 
			
		||||
    "eslint-config-prettier": "10.1.8",
 | 
			
		||||
    "eslint-import-resolver-webpack": "0.13.10",
 | 
			
		||||
    "eslint-plugin-import": "2.32.0",
 | 
			
		||||
    "eslint-plugin-lit": "2.1.1",
 | 
			
		||||
    "eslint-plugin-lit-a11y": "5.1.1",
 | 
			
		||||
    "eslint-plugin-unused-imports": "4.3.0",
 | 
			
		||||
    "eslint-plugin-wc": "3.0.2",
 | 
			
		||||
    "eslint-plugin-unused-imports": "4.2.0",
 | 
			
		||||
    "eslint-plugin-wc": "3.0.1",
 | 
			
		||||
    "fancy-log": "2.0.0",
 | 
			
		||||
    "fs-extra": "11.3.2",
 | 
			
		||||
    "glob": "11.0.3",
 | 
			
		||||
@@ -201,9 +201,9 @@
 | 
			
		||||
    "gulp-rename": "2.1.0",
 | 
			
		||||
    "html-minifier-terser": "7.2.0",
 | 
			
		||||
    "husky": "9.1.7",
 | 
			
		||||
    "jsdom": "27.0.1",
 | 
			
		||||
    "jsdom": "27.0.0",
 | 
			
		||||
    "jszip": "3.10.1",
 | 
			
		||||
    "lint-staged": "16.2.6",
 | 
			
		||||
    "lint-staged": "16.1.6",
 | 
			
		||||
    "lit-analyzer": "2.0.3",
 | 
			
		||||
    "lodash.merge": "4.6.2",
 | 
			
		||||
    "lodash.template": "4.5.0",
 | 
			
		||||
@@ -213,13 +213,13 @@
 | 
			
		||||
    "rspack-manifest-plugin": "5.1.0",
 | 
			
		||||
    "serve": "14.2.5",
 | 
			
		||||
    "sinon": "21.0.0",
 | 
			
		||||
    "tar": "7.5.1",
 | 
			
		||||
    "tar": "7.4.3",
 | 
			
		||||
    "terser-webpack-plugin": "5.3.14",
 | 
			
		||||
    "ts-lit-plugin": "2.0.2",
 | 
			
		||||
    "typescript": "5.9.3",
 | 
			
		||||
    "typescript-eslint": "8.46.2",
 | 
			
		||||
    "typescript": "5.9.2",
 | 
			
		||||
    "typescript-eslint": "8.44.0",
 | 
			
		||||
    "vite-tsconfig-paths": "5.1.4",
 | 
			
		||||
    "vitest": "4.0.1",
 | 
			
		||||
    "vitest": "3.2.4",
 | 
			
		||||
    "webpack-stats-plugin": "1.1.3",
 | 
			
		||||
    "webpackbar": "7.0.0",
 | 
			
		||||
    "workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
 | 
			
		||||
@@ -235,5 +235,5 @@
 | 
			
		||||
    "tslib": "2.8.1",
 | 
			
		||||
    "@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
 | 
			
		||||
  },
 | 
			
		||||
  "packageManager": "yarn@4.10.3"
 | 
			
		||||
  "packageManager": "yarn@4.10.2"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 | 
			
		||||
 | 
			
		||||
[project]
 | 
			
		||||
name         = "home-assistant-frontend"
 | 
			
		||||
version      = "20250924.0"
 | 
			
		||||
version      = "20251001.4"
 | 
			
		||||
license      = "Apache-2.0"
 | 
			
		||||
license-files = ["LICENSE*"]
 | 
			
		||||
description  = "The Home Assistant frontend"
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
    ":semanticCommitsDisabled",
 | 
			
		||||
    "group:monorepos",
 | 
			
		||||
    "group:recommended",
 | 
			
		||||
    "security:minimumReleaseAgeNpm"
 | 
			
		||||
    "npm:unpublishSafe"
 | 
			
		||||
  ],
 | 
			
		||||
  "enabledManagers": ["npm", "nvm"],
 | 
			
		||||
  "postUpdateOptions": ["yarnDedupeHighest"],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
# Pushes a new version to PyPi.
 | 
			
		||||
 | 
			
		||||
# Stop on errors
 | 
			
		||||
set -e
 | 
			
		||||
@@ -11,4 +12,5 @@ yarn install
 | 
			
		||||
script/build_frontend
 | 
			
		||||
 | 
			
		||||
rm -rf dist home_assistant_frontend.egg-info
 | 
			
		||||
python3 -m build -q
 | 
			
		||||
python3 -m build
 | 
			
		||||
python3 -m twine upload dist/*.whl --skip-existing
 | 
			
		||||
 
 | 
			
		||||
@@ -103,10 +103,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
 | 
			
		||||
          );
 | 
			
		||||
          box-shadow: var(--ha-card-box-shadow, none);
 | 
			
		||||
          box-sizing: border-box;
 | 
			
		||||
          border-radius: var(
 | 
			
		||||
            --ha-card-border-radius,
 | 
			
		||||
            var(--ha-border-radius-lg)
 | 
			
		||||
          );
 | 
			
		||||
          border-radius: var(--ha-card-border-radius, 12px);
 | 
			
		||||
          border-width: var(--ha-card-border-width, 1px);
 | 
			
		||||
          border-style: solid;
 | 
			
		||||
          border-color: var(
 | 
			
		||||
@@ -135,7 +132,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
 | 
			
		||||
        }
 | 
			
		||||
        ha-language-picker {
 | 
			
		||||
          width: 200px;
 | 
			
		||||
          border-radius: var(--ha-border-radius-sm);
 | 
			
		||||
          border-radius: 4px;
 | 
			
		||||
          overflow: hidden;
 | 
			
		||||
          --ha-select-height: 40px;
 | 
			
		||||
          --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 => {
 | 
			
		||||
  const color = parse(hex);
 | 
			
		||||
  if (!color) {
 | 
			
		||||
    throw new Error(`Invalid hex color: ${hex}`);
 | 
			
		||||
  hex = hex.replace("#", "");
 | 
			
		||||
  if (hex.length === 6) return hex;
 | 
			
		||||
  let result = "";
 | 
			
		||||
  for (const val of hex) {
 | 
			
		||||
    result += val + val;
 | 
			
		||||
  }
 | 
			
		||||
  const formattedColor = formatHex(color);
 | 
			
		||||
  if (!formattedColor) {
 | 
			
		||||
    throw new Error(`Could not format hex color: ${hex}`);
 | 
			
		||||
  }
 | 
			
		||||
  return formattedColor.replace("#", "");
 | 
			
		||||
  return result;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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.
 | 
			
		||||
 */
 | 
			
		||||
// Blend 2 hex colors: c1 is placed over c2, blend is c1's opacity.
 | 
			
		||||
export const hexBlend = (c1: string, c2: string, blend = 50): string => {
 | 
			
		||||
  let color = "";
 | 
			
		||||
  c1 = expandHex(c1);
 | 
			
		||||
  c2 = expandHex(c2);
 | 
			
		||||
  let color = "";
 | 
			
		||||
  for (let i = 0; i <= 5; i += 2) {
 | 
			
		||||
    const h1 = parseInt(c1.substring(i, i + 2), 16);
 | 
			
		||||
    const h2 = parseInt(c2.substring(i, i + 2), 16);
 | 
			
		||||
    const hex = Math.floor(h2 + (h1 - h2) * (blend / 100))
 | 
			
		||||
      .toString(16)
 | 
			
		||||
      .padStart(2, "0");
 | 
			
		||||
    let hex = Math.floor(h2 + (h1 - h2) * (blend / 100)).toString(16);
 | 
			
		||||
    while (hex.length < 2) hex = "0" + hex;
 | 
			
		||||
    color += hex;
 | 
			
		||||
  }
 | 
			
		||||
  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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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,
 | 
			
		||||
  });
 | 
			
		||||
  return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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 = (
 | 
			
		||||
  color1: [number, number, number],
 | 
			
		||||
  color2: [number, number, number]
 | 
			
		||||
) =>
 | 
			
		||||
  wcagContrast(
 | 
			
		||||
    {
 | 
			
		||||
      mode: "rgb",
 | 
			
		||||
      r: color1[0] / 255,
 | 
			
		||||
      g: color1[1] / 255,
 | 
			
		||||
      b: color1[2] / 255,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      mode: "rgb",
 | 
			
		||||
      r: color2[0] / 255,
 | 
			
		||||
      g: color2[1] / 255,
 | 
			
		||||
      b: color2[2] / 255,
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
) => {
 | 
			
		||||
  const lum1 = luminosity(color1);
 | 
			
		||||
  const lum2 = luminosity(color2);
 | 
			
		||||
 | 
			
		||||
  if (lum1 > lum2) {
 | 
			
		||||
    return (lum1 + 0.05) / (lum2 + 0.05);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (lum2 + 0.05) / (lum1 + 0.05);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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 = (
 | 
			
		||||
  rgb1: [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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -31,10 +31,10 @@ export const isNavigationClick = (e: MouseEvent, preventDefault = true) => {
 | 
			
		||||
 | 
			
		||||
  const location = window.location;
 | 
			
		||||
  const origin = location.origin || location.protocol + "//" + location.host;
 | 
			
		||||
  if (!href.startsWith(origin)) {
 | 
			
		||||
  if (href.indexOf(origin) !== 0) {
 | 
			
		||||
    return undefined;
 | 
			
		||||
  }
 | 
			
		||||
  href = href.slice(origin.length);
 | 
			
		||||
  href = href.substr(origin.length);
 | 
			
		||||
 | 
			
		||||
  if (href === "#") {
 | 
			
		||||
    return undefined;
 | 
			
		||||
 
 | 
			
		||||
@@ -61,9 +61,3 @@ export const computeEntityEntryName = (
 | 
			
		||||
 | 
			
		||||
  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. */
 | 
			
		||||
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 = (
 | 
			
		||||
  area: AreaRegistryEntry,
 | 
			
		||||
  hassFloors: HomeAssistant["floors"]
 | 
			
		||||
  hass: HomeAssistant
 | 
			
		||||
): AreaContext => {
 | 
			
		||||
  const floorId = area.floor_id;
 | 
			
		||||
  const floor = floorId ? hassFloors[floorId] : undefined;
 | 
			
		||||
  const floor = floorId ? hass.floors[floorId] : undefined;
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    area: area,
 | 
			
		||||
 
 | 
			
		||||
@@ -122,22 +122,3 @@ export const generateEntityFilter = (
 | 
			
		||||
    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",
 | 
			
		||||
    "triggered",
 | 
			
		||||
  ],
 | 
			
		||||
  alert: ["on", "off", "idle"],
 | 
			
		||||
  assist_satellite: ["idle", "listening", "responding", "processing"],
 | 
			
		||||
  automation: ["on", "off"],
 | 
			
		||||
  binary_sensor: ["on", "off"],
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,6 @@ const STATE_COLORED_DOMAIN = new Set([
 | 
			
		||||
  "vacuum",
 | 
			
		||||
  "valve",
 | 
			
		||||
  "water_heater",
 | 
			
		||||
  "weather",
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
export const stateColorCss = (stateObj: HassEntity, state?: string) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -32,8 +32,6 @@ export const numberFormatToLocale = (
 | 
			
		||||
      return ["de", "es", "it"]; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
 | 
			
		||||
    case NumberFormat.space_comma:
 | 
			
		||||
      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:
 | 
			
		||||
      return undefined;
 | 
			
		||||
    default:
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,10 @@ function isSeparatorAtPos(value: string, index: number): boolean {
 | 
			
		||||
    case undefined:
 | 
			
		||||
      return false;
 | 
			
		||||
    default:
 | 
			
		||||
      return isEmojiImprecise(code);
 | 
			
		||||
      if (isEmojiImprecise(code)) {
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,13 @@
 | 
			
		||||
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import type { FrontendLocaleData } from "../../data/translation";
 | 
			
		||||
import type { HomeAssistant } from "../../types";
 | 
			
		||||
import {
 | 
			
		||||
  computeEntityNameDisplay,
 | 
			
		||||
  type EntityNameItem,
 | 
			
		||||
  type EntityNameOptions,
 | 
			
		||||
} from "../entity/compute_entity_name_display";
 | 
			
		||||
import type { LocalizeFunc } from "./localize";
 | 
			
		||||
import { computeEntityName } from "../entity/compute_entity_name";
 | 
			
		||||
import { computeDeviceName } from "../entity/compute_device_name";
 | 
			
		||||
import { getEntityContext } from "../entity/context/get_entity_context";
 | 
			
		||||
import { computeAreaName } from "../entity/compute_area_name";
 | 
			
		||||
import { computeFloorName } from "../entity/compute_floor_name";
 | 
			
		||||
import { ensureArray } from "../array/ensure-array";
 | 
			
		||||
 | 
			
		||||
export type FormatEntityStateFunc = (
 | 
			
		||||
  stateObj: HassEntity,
 | 
			
		||||
@@ -26,8 +27,8 @@ export type EntityNameType = "entity" | "device" | "area" | "floor";
 | 
			
		||||
 | 
			
		||||
export type FormatEntityNameFunc = (
 | 
			
		||||
  stateObj: HassEntity,
 | 
			
		||||
  name: EntityNameItem | EntityNameItem[],
 | 
			
		||||
  options?: EntityNameOptions
 | 
			
		||||
  type: EntityNameType | EntityNameType[],
 | 
			
		||||
  separator?: string
 | 
			
		||||
) => string;
 | 
			
		||||
 | 
			
		||||
export const computeFormatFunctions = async (
 | 
			
		||||
@@ -74,15 +75,45 @@ export const computeFormatFunctions = async (
 | 
			
		||||
      ),
 | 
			
		||||
    formatEntityAttributeName: (stateObj, attribute) =>
 | 
			
		||||
      computeAttributeNameDisplay(localize, stateObj, entities, attribute),
 | 
			
		||||
    formatEntityName: (stateObj, name, options) =>
 | 
			
		||||
      computeEntityNameDisplay(
 | 
			
		||||
    formatEntityName: (stateObj, type, separator = " ") => {
 | 
			
		||||
      const types = ensureArray(type);
 | 
			
		||||
      const namesList: (string | undefined)[] = [];
 | 
			
		||||
 | 
			
		||||
      const { device, area, floor } = getEntityContext(
 | 
			
		||||
        stateObj,
 | 
			
		||||
        name,
 | 
			
		||||
        entities,
 | 
			
		||||
        devices,
 | 
			
		||||
        areas,
 | 
			
		||||
        floors,
 | 
			
		||||
        options
 | 
			
		||||
      ),
 | 
			
		||||
        floors
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      for (const t of types) {
 | 
			
		||||
        switch (t) {
 | 
			
		||||
          case "entity": {
 | 
			
		||||
            namesList.push(computeEntityName(stateObj, entities, devices));
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
          case "device": {
 | 
			
		||||
            if (device) {
 | 
			
		||||
              namesList.push(computeDeviceName(device));
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
          case "area": {
 | 
			
		||||
            if (area) {
 | 
			
		||||
              namesList.push(computeAreaName(area));
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
          case "floor": {
 | 
			
		||||
            if (floor) {
 | 
			
		||||
              namesList.push(computeFloorName(floor));
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return namesList.filter((name) => name !== undefined).join(separator);
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ export default function parseAspectRatio(input: string) {
 | 
			
		||||
  }
 | 
			
		||||
  try {
 | 
			
		||||
    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");
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
 | 
			
		||||
import "./ha-progress-button";
 | 
			
		||||
import type { HomeAssistant } from "../../types";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import type { Appearance } from "../ha-button";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-call-service-button")
 | 
			
		||||
class HaCallServiceButton extends LitElement {
 | 
			
		||||
@@ -26,14 +25,12 @@ class HaCallServiceButton extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @property() public confirmation?;
 | 
			
		||||
 | 
			
		||||
  @property() public appearance: Appearance = "plain";
 | 
			
		||||
 | 
			
		||||
  public render(): TemplateResult {
 | 
			
		||||
    return html`
 | 
			
		||||
      <ha-progress-button
 | 
			
		||||
        .progress=${this.progress}
 | 
			
		||||
        .disabled=${this.disabled}
 | 
			
		||||
        .appearance=${this.appearance}
 | 
			
		||||
        appearance="plain"
 | 
			
		||||
        @click=${this._buttonTapped}
 | 
			
		||||
        tabindex="0"
 | 
			
		||||
      >
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,21 @@
 | 
			
		||||
import type { LineSeriesOption } from "echarts";
 | 
			
		||||
 | 
			
		||||
export function downSampleLineData<
 | 
			
		||||
  T extends [number, number] | NonNullable<LineSeriesOption["data"]>[number],
 | 
			
		||||
>(
 | 
			
		||||
  data: T[] | undefined,
 | 
			
		||||
  maxDetails: number,
 | 
			
		||||
export function downSampleLineData(
 | 
			
		||||
  data: LineSeriesOption["data"],
 | 
			
		||||
  chartWidth: number,
 | 
			
		||||
  minX?: number,
 | 
			
		||||
  maxX?: number
 | 
			
		||||
): T[] {
 | 
			
		||||
  if (!data) {
 | 
			
		||||
    return [];
 | 
			
		||||
) {
 | 
			
		||||
  if (!data || data.length < 10) {
 | 
			
		||||
    return data;
 | 
			
		||||
  }
 | 
			
		||||
  if (data.length <= maxDetails) {
 | 
			
		||||
  const width = chartWidth * window.devicePixelRatio;
 | 
			
		||||
  if (data.length <= width) {
 | 
			
		||||
    return data;
 | 
			
		||||
  }
 | 
			
		||||
  const min = minX ?? getPointData(data[0]!)[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<
 | 
			
		||||
    number,
 | 
			
		||||
    {
 | 
			
		||||
@@ -48,7 +47,7 @@ export function downSampleLineData<
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Convert frames back to points
 | 
			
		||||
  const result: T[] = [];
 | 
			
		||||
  const result: typeof data = [];
 | 
			
		||||
  for (const [_i, frame] of frames) {
 | 
			
		||||
    // Use min/max points to preserve visual accuracy
 | 
			
		||||
    // 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 { ResizeController } from "@lit-labs/observers/resize-controller";
 | 
			
		||||
import { mdiChevronDown, mdiChevronUp, mdiRestart } from "@mdi/js";
 | 
			
		||||
import { differenceInMinutes } from "date-fns";
 | 
			
		||||
import type { DataZoomComponentOption } from "echarts/components";
 | 
			
		||||
@@ -7,28 +7,28 @@ import type { EChartsType } from "echarts/core";
 | 
			
		||||
import type {
 | 
			
		||||
  ECElementEvent,
 | 
			
		||||
  LegendComponentOption,
 | 
			
		||||
  LineSeriesOption,
 | 
			
		||||
  XAXisOption,
 | 
			
		||||
  YAXisOption,
 | 
			
		||||
  LineSeriesOption,
 | 
			
		||||
} from "echarts/types/dist/shared";
 | 
			
		||||
import type { PropertyValues } from "lit";
 | 
			
		||||
import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
import { classMap } from "lit/directives/class-map";
 | 
			
		||||
import { styleMap } from "lit/directives/style-map";
 | 
			
		||||
import { ensureArray } from "../../common/array/ensure-array";
 | 
			
		||||
import { getAllGraphColors } from "../../common/color/colors";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import { listenMediaQuery } from "../../common/dom/media_query";
 | 
			
		||||
import { themesContext } from "../../data/context";
 | 
			
		||||
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 { isMac } from "../../util/is_mac";
 | 
			
		||||
import "../chips/ha-assist-chip";
 | 
			
		||||
import "../ha-icon-button";
 | 
			
		||||
import { filterXSS } from "../../common/util/xss";
 | 
			
		||||
import { formatTimeLabel } from "./axis-label";
 | 
			
		||||
import { ensureArray } from "../../common/array/ensure-array";
 | 
			
		||||
import "../chips/ha-assist-chip";
 | 
			
		||||
import { downSampleLineData } from "./down-sample";
 | 
			
		||||
 | 
			
		||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
 | 
			
		||||
@@ -88,19 +88,9 @@ export class HaChartBase extends LitElement {
 | 
			
		||||
 | 
			
		||||
  private _lastTapTime?: number;
 | 
			
		||||
 | 
			
		||||
  private _shouldResizeChart = false;
 | 
			
		||||
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  private _resizeController = new ResizeController(this, {
 | 
			
		||||
    callback: () => {
 | 
			
		||||
      if (this.chart) {
 | 
			
		||||
        if (!this.chart.getZr().animation.isFinished()) {
 | 
			
		||||
          this._shouldResizeChart = true;
 | 
			
		||||
        } else {
 | 
			
		||||
          this.chart.resize();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    callback: () => this.chart?.resize(),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  private _loading = false;
 | 
			
		||||
@@ -356,7 +346,7 @@ export class HaChartBase extends LitElement {
 | 
			
		||||
      if (this.chart) {
 | 
			
		||||
        this.chart.dispose();
 | 
			
		||||
      }
 | 
			
		||||
      const echarts = (await import("../../resources/echarts/echarts")).default;
 | 
			
		||||
      const echarts = (await import("../../resources/echarts")).default;
 | 
			
		||||
 | 
			
		||||
      if (this.extraComponents?.length) {
 | 
			
		||||
        echarts.use(this.extraComponents);
 | 
			
		||||
@@ -376,7 +366,6 @@ export class HaChartBase extends LitElement {
 | 
			
		||||
      if (!this.options?.dataZoom) {
 | 
			
		||||
        this.chart.getZr().on("dblclick", this._handleClickZoom);
 | 
			
		||||
      }
 | 
			
		||||
      this.chart.on("finished", this._handleChartRenderFinished);
 | 
			
		||||
      if (this._isTouchDevice) {
 | 
			
		||||
        this.chart.getZr().on("click", (e: ECElementEvent) => {
 | 
			
		||||
          if (!e.zrByTouch) {
 | 
			
		||||
@@ -816,7 +805,7 @@ export class HaChartBase extends LitElement {
 | 
			
		||||
            sampling: undefined,
 | 
			
		||||
            data: downSampleLineData(
 | 
			
		||||
              data as LineSeriesOption["data"],
 | 
			
		||||
              this.clientWidth * window.devicePixelRatio,
 | 
			
		||||
              this.clientWidth,
 | 
			
		||||
              minX,
 | 
			
		||||
              maxX
 | 
			
		||||
            ),
 | 
			
		||||
@@ -956,13 +945,6 @@ export class HaChartBase extends LitElement {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _handleChartRenderFinished = () => {
 | 
			
		||||
    if (this._shouldResizeChart) {
 | 
			
		||||
      this.chart?.resize();
 | 
			
		||||
      this._shouldResizeChart = false;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static styles = css`
 | 
			
		||||
    :host {
 | 
			
		||||
      display: block;
 | 
			
		||||
@@ -994,7 +976,7 @@ export class HaChartBase extends LitElement {
 | 
			
		||||
      right: 4px;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex-direction: column;
 | 
			
		||||
      gap: var(--ha-space-1);
 | 
			
		||||
      gap: 4px;
 | 
			
		||||
    }
 | 
			
		||||
    .chart-controls.small {
 | 
			
		||||
      top: 0;
 | 
			
		||||
@@ -1003,7 +985,7 @@ export class HaChartBase extends LitElement {
 | 
			
		||||
    .chart-controls ha-icon-button,
 | 
			
		||||
    .chart-controls ::slotted(ha-icon-button) {
 | 
			
		||||
      background: var(--card-background-color);
 | 
			
		||||
      border-radius: var(--ha-border-radius-sm);
 | 
			
		||||
      border-radius: 4px;
 | 
			
		||||
      --mdc-icon-button-size: 32px;
 | 
			
		||||
      color: var(--primary-color);
 | 
			
		||||
      border: 1px solid var(--divider-color);
 | 
			
		||||
@@ -1031,7 +1013,7 @@ export class HaChartBase extends LitElement {
 | 
			
		||||
      flex-wrap: wrap;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      gap: var(--ha-space-2);
 | 
			
		||||
      gap: 8px;
 | 
			
		||||
    }
 | 
			
		||||
    .chart-legend li {
 | 
			
		||||
      height: 24px;
 | 
			
		||||
@@ -1056,7 +1038,7 @@ export class HaChartBase extends LitElement {
 | 
			
		||||
    .chart-legend .bullet {
 | 
			
		||||
      border-width: 1px;
 | 
			
		||||
      border-style: solid;
 | 
			
		||||
      border-radius: var(--ha-border-radius-circle);
 | 
			
		||||
      border-radius: 50%;
 | 
			
		||||
      display: block;
 | 
			
		||||
      height: 16px;
 | 
			
		||||
      width: 16px;
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import type { TopLevelFormatterParams } from "echarts/types/dist/shared";
 | 
			
		||||
import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
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 type { HaChartBase } from "./ha-chart-base";
 | 
			
		||||
import type { HomeAssistant } from "../../types";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,13 @@
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
import { LitElement, html, css } from "lit";
 | 
			
		||||
import type { EChartsType } from "echarts/core";
 | 
			
		||||
import type { CallbackDataParams } from "echarts/types/dist/shared";
 | 
			
		||||
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 { ResizeController } from "@lit-labs/observers/resize-controller";
 | 
			
		||||
import SankeyChart from "../../resources/echarts/components/sankey/install";
 | 
			
		||||
import type { HomeAssistant } from "../../types";
 | 
			
		||||
import type { ECOption } from "../../resources/echarts/echarts";
 | 
			
		||||
import type { ECOption } from "../../resources/echarts";
 | 
			
		||||
import { measureTextWidth } from "../../util/text";
 | 
			
		||||
import { filterXSS } from "../../common/util/xss";
 | 
			
		||||
import "./ha-chart-base";
 | 
			
		||||
@@ -39,7 +39,7 @@ type ProcessedLink = Link & {
 | 
			
		||||
 | 
			
		||||
const OVERFLOW_MARGIN = 5;
 | 
			
		||||
const FONT_SIZE = 12;
 | 
			
		||||
const NODE_GAP = 6;
 | 
			
		||||
const NODE_GAP = 8;
 | 
			
		||||
const LABEL_DISTANCE = 5;
 | 
			
		||||
 | 
			
		||||
@customElement("ha-sankey-chart")
 | 
			
		||||
@@ -164,7 +164,6 @@ export class HaSankeyChart extends LitElement {
 | 
			
		||||
      lineStyle: {
 | 
			
		||||
        color: "gradient",
 | 
			
		||||
        opacity: 0.4,
 | 
			
		||||
        curveness: 0.5,
 | 
			
		||||
      },
 | 
			
		||||
      layoutIterations: 0,
 | 
			
		||||
      label: {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ import { computeRTL } from "../../common/util/compute_rtl";
 | 
			
		||||
import type { LineChartEntity, LineChartState } from "../../data/history";
 | 
			
		||||
import type { HomeAssistant } from "../../types";
 | 
			
		||||
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 {
 | 
			
		||||
  getNumberFormatOptions,
 | 
			
		||||
 
 | 
			
		||||
@@ -15,8 +15,8 @@ import type { TimelineEntity } from "../../data/history";
 | 
			
		||||
import type { HomeAssistant } from "../../types";
 | 
			
		||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
 | 
			
		||||
import { computeTimelineColor } from "./timeline-color";
 | 
			
		||||
import type { ECOption } from "../../resources/echarts/echarts";
 | 
			
		||||
import echarts from "../../resources/echarts/echarts";
 | 
			
		||||
import type { ECOption } from "../../resources/echarts";
 | 
			
		||||
import echarts from "../../resources/echarts";
 | 
			
		||||
import { luminosity } from "../../common/color/rgb";
 | 
			
		||||
import { hex2rgb } from "../../common/color/convert-color";
 | 
			
		||||
import { measureTextWidth } from "../../util/text";
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ import {
 | 
			
		||||
  getStatisticMetadata,
 | 
			
		||||
  statisticsHaveType,
 | 
			
		||||
} from "../../data/recorder";
 | 
			
		||||
import type { ECOption } from "../../resources/echarts/echarts";
 | 
			
		||||
import type { ECOption } from "../../resources/echarts";
 | 
			
		||||
import type { HomeAssistant } from "../../types";
 | 
			
		||||
import type { CustomLegendOption } from "./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 { UNAVAILABLE, UNKNOWN } from "../../data/entity";
 | 
			
		||||
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>> = {
 | 
			
		||||
  media_player: {
 | 
			
		||||
@@ -53,28 +51,6 @@ function computeTimelineStateColor(
 | 
			
		||||
let colorIndex = 0;
 | 
			
		||||
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(
 | 
			
		||||
  state: string,
 | 
			
		||||
  computedStyles: CSSStyleDeclaration
 | 
			
		||||
@@ -95,7 +71,6 @@ export function computeTimelineColor(
 | 
			
		||||
): string {
 | 
			
		||||
  return (
 | 
			
		||||
    computeTimelineStateColor(state, computedStyles, stateObj) ||
 | 
			
		||||
    computeTimelineEnumColor(state, computedStyles, stateObj) ||
 | 
			
		||||
    computeTimeLineGenericColor(state, computedStyles)
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles";
 | 
			
		||||
import { FilterChip } from "@material/web/chips/internal/filter-chip";
 | 
			
		||||
import { styles } from "@material/web/chips/internal/filter-styles";
 | 
			
		||||
import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles";
 | 
			
		||||
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles";
 | 
			
		||||
import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles";
 | 
			
		||||
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles";
 | 
			
		||||
import { css, html } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators";
 | 
			
		||||
 | 
			
		||||
@@ -30,7 +30,6 @@ export class HaFilterChip extends FilterChip {
 | 
			
		||||
          var(--rgb-primary-text-color),
 | 
			
		||||
          0.15
 | 
			
		||||
        );
 | 
			
		||||
        border-radius: var(--ha-border-radius-md);
 | 
			
		||||
      }
 | 
			
		||||
    `,
 | 
			
		||||
  ];
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { mdiDragHorizontalVariant, mdiEye, mdiEyeOff } from "@mdi/js";
 | 
			
		||||
import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js";
 | 
			
		||||
import type { CSSResultGroup } from "lit";
 | 
			
		||||
import { LitElement, css, html, nothing } from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
@@ -129,7 +129,7 @@ export class DialogDataTableSettings extends LitElement {
 | 
			
		||||
                  ${canMove && isVisible
 | 
			
		||||
                    ? html`<ha-svg-icon
 | 
			
		||||
                        class="handle"
 | 
			
		||||
                        .path=${mdiDragHorizontalVariant}
 | 
			
		||||
                        .path=${mdiDrag}
 | 
			
		||||
                        slot="graphic"
 | 
			
		||||
                      ></ha-svg-icon>`
 | 
			
		||||
                    : nothing}
 | 
			
		||||
@@ -290,9 +290,7 @@ export class DialogDataTableSettings extends LitElement {
 | 
			
		||||
          ha-dialog {
 | 
			
		||||
            --vertical-align-dialog: flex-start;
 | 
			
		||||
            --dialog-surface-margin-top: 250px;
 | 
			
		||||
            --ha-dialog-border-radius: var(--ha-border-radius-4xl)
 | 
			
		||||
              var(--ha-border-radius-4xl) var(--ha-border-radius-square)
 | 
			
		||||
              var(--ha-border-radius-square);
 | 
			
		||||
            --ha-dialog-border-radius: 28px 28px 0 0;
 | 
			
		||||
            --mdc-dialog-min-height: calc(100% - 250px);
 | 
			
		||||
            --mdc-dialog-max-height: calc(100% - 250px);
 | 
			
		||||
          }
 | 
			
		||||
 
 | 
			
		||||
@@ -1053,7 +1053,7 @@ export class HaDataTable extends LitElement {
 | 
			
		||||
 | 
			
		||||
        .mdc-data-table {
 | 
			
		||||
          background-color: var(--data-table-background-color);
 | 
			
		||||
          border-radius: var(--ha-border-radius-sm);
 | 
			
		||||
          border-radius: 4px;
 | 
			
		||||
          border-width: 1px;
 | 
			
		||||
          border-style: solid;
 | 
			
		||||
          border-color: var(--divider-color);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,6 @@
 | 
			
		||||
import { expose } from "comlink";
 | 
			
		||||
import Fuse from "fuse.js";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { ipCompare, stringCompare } from "../../common/string/compare";
 | 
			
		||||
import { stringCompare, ipCompare } from "../../common/string/compare";
 | 
			
		||||
import { stripDiacritics } from "../../common/string/strip-diacritics";
 | 
			
		||||
import { HaFuse } from "../../resources/fuse";
 | 
			
		||||
import type {
 | 
			
		||||
  ClonedDataTableColumnData,
 | 
			
		||||
  DataTableRowData,
 | 
			
		||||
@@ -11,48 +8,29 @@ import type {
 | 
			
		||||
  SortingDirection,
 | 
			
		||||
} from "./ha-data-table";
 | 
			
		||||
 | 
			
		||||
const fuseIndex = memoizeOne(
 | 
			
		||||
  (data: DataTableRowData[], columns: SortableColumnContainer) => {
 | 
			
		||||
    const searchKeys = new Set<string>();
 | 
			
		||||
    Object.entries(columns).forEach(([key, column]) => {
 | 
			
		||||
      if (column.filterable) {
 | 
			
		||||
        searchKeys.add(
 | 
			
		||||
          column.filterKey
 | 
			
		||||
            ? `${column.valueColumn || key}.${column.filterKey}`
 | 
			
		||||
            : key
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    return Fuse.createIndex([...searchKeys], data);
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const filterData = (
 | 
			
		||||
  data: DataTableRowData[],
 | 
			
		||||
  columns: SortableColumnContainer,
 | 
			
		||||
  filter: string
 | 
			
		||||
) => {
 | 
			
		||||
  filter = stripDiacritics(filter.toLowerCase());
 | 
			
		||||
  return data.filter((row) =>
 | 
			
		||||
    Object.entries(columns).some((columnEntry) => {
 | 
			
		||||
      const [key, column] = columnEntry;
 | 
			
		||||
      if (column.filterable) {
 | 
			
		||||
        const value = String(
 | 
			
		||||
          column.filterKey
 | 
			
		||||
            ? row[column.valueColumn || key][column.filterKey]
 | 
			
		||||
            : row[column.valueColumn || key]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
  if (filter === "") {
 | 
			
		||||
    return data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const index = fuseIndex(data, columns);
 | 
			
		||||
 | 
			
		||||
  const fuse = new HaFuse(
 | 
			
		||||
    data,
 | 
			
		||||
    { shouldSort: false, minMatchCharLength: 1 },
 | 
			
		||||
    index
 | 
			
		||||
        if (stripDiacritics(value).toLowerCase().includes(filter)) {
 | 
			
		||||
          return true;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return false;
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const searchResults = fuse.multiTermsSearch(filter);
 | 
			
		||||
 | 
			
		||||
  if (searchResults) {
 | 
			
		||||
    return searchResults.map((result) => result.item);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return data;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const sortData = (
 | 
			
		||||
 
 | 
			
		||||
@@ -4,11 +4,11 @@ import Vue from "vue";
 | 
			
		||||
import DateRangePicker from "vue2-daterange-picker";
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
 | 
			
		||||
import {
 | 
			
		||||
  localizeMonths,
 | 
			
		||||
  localizeWeekdays,
 | 
			
		||||
} from "../common/datetime/localize_date";
 | 
			
		||||
import { fireEvent } from "../common/dom/fire_event";
 | 
			
		||||
import {
 | 
			
		||||
  localizeWeekdays,
 | 
			
		||||
  localizeMonths,
 | 
			
		||||
} from "../common/datetime/localize_date";
 | 
			
		||||
import { mainWindow } from "../common/dom/get_main_window";
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/naming-convention
 | 
			
		||||
@@ -177,7 +177,7 @@ class DateRangePickerElement extends WrappedElement {
 | 
			
		||||
            top: auto;
 | 
			
		||||
            box-shadow: var(--ha-card-box-shadow, none);
 | 
			
		||||
            background-color: var(--card-background-color);
 | 
			
		||||
            border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
 | 
			
		||||
            border-radius: var(--ha-card-border-radius, 12px);
 | 
			
		||||
            border-width: var(--ha-card-border-width, 1px);
 | 
			
		||||
            border-style: solid;
 | 
			
		||||
            border-color: var(
 | 
			
		||||
@@ -203,7 +203,7 @@ class DateRangePickerElement extends WrappedElement {
 | 
			
		||||
          .daterangepicker .calendar-table th {
 | 
			
		||||
            background-color: transparent;
 | 
			
		||||
            color: var(--secondary-text-color);
 | 
			
		||||
            border-radius: var(--ha-border-radius-square);
 | 
			
		||||
            border-radius: 0;
 | 
			
		||||
            outline: none;
 | 
			
		||||
            min-width: 32px;
 | 
			
		||||
            height: 32px;
 | 
			
		||||
@@ -225,13 +225,13 @@ class DateRangePickerElement extends WrappedElement {
 | 
			
		||||
            color: var(--text-primary-color);
 | 
			
		||||
          }
 | 
			
		||||
          .daterangepicker td.start-date.end-date {
 | 
			
		||||
            border-radius: var(--ha-border-radius-circle);
 | 
			
		||||
            border-radius: 50%;
 | 
			
		||||
          }
 | 
			
		||||
          .daterangepicker td.start-date {
 | 
			
		||||
            border-radius: var(--ha-border-radius-circle) var(--ha-border-radius-square) var(--ha-border-radius-square) var(--ha-border-radius-circle);
 | 
			
		||||
            border-radius: 50% 0 0 50%;
 | 
			
		||||
          }
 | 
			
		||||
          .daterangepicker td.end-date {
 | 
			
		||||
            border-radius: var(--ha-border-radius-square) var(--ha-border-radius-circle) var(--ha-border-radius-circle) var(--ha-border-radius-square);
 | 
			
		||||
            border-radius: 0 50% 50% 0;
 | 
			
		||||
          }
 | 
			
		||||
          .reportrange-text {
 | 
			
		||||
            background: none !important;
 | 
			
		||||
@@ -265,7 +265,7 @@ class DateRangePickerElement extends WrappedElement {
 | 
			
		||||
            border: 1px solid var(--primary-color);
 | 
			
		||||
            background-color: transparent;
 | 
			
		||||
            color: var(--primary-color);
 | 
			
		||||
            border-radius: var(--ha-border-radius-sm);
 | 
			
		||||
            border-radius: 4px;
 | 
			
		||||
            padding: 8px;
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
          }
 | 
			
		||||
@@ -321,10 +321,10 @@ class DateRangePickerElement extends WrappedElement {
 | 
			
		||||
              -webkit-transform: rotate(-45deg);
 | 
			
		||||
            }
 | 
			
		||||
            .daterangepicker td.start-date {
 | 
			
		||||
              border-radius: var(--ha-border-radius-square) var(--ha-border-radius-circle) var(--ha-border-radius-circle) var(--ha-border-radius-square);
 | 
			
		||||
              border-radius: 0 50% 50% 0;
 | 
			
		||||
            }
 | 
			
		||||
            .daterangepicker td.end-date {
 | 
			
		||||
              border-radius: var(--ha-border-radius-circle) var(--ha-border-radius-square) var(--ha-border-radius-square) var(--ha-border-radius-circle);
 | 
			
		||||
              border-radius: 50% 0 0 50%;
 | 
			
		||||
            }
 | 
			
		||||
            `;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,18 +5,24 @@ import { customElement, property, query, state } from "lit/decorators";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import { computeAreaName } from "../../common/entity/compute_area_name";
 | 
			
		||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
 | 
			
		||||
import {
 | 
			
		||||
  computeDeviceName,
 | 
			
		||||
  computeDeviceNameDisplay,
 | 
			
		||||
} from "../../common/entity/compute_device_name";
 | 
			
		||||
import { computeDomain } from "../../common/entity/compute_domain";
 | 
			
		||||
import { getDeviceContext } from "../../common/entity/context/get_device_context";
 | 
			
		||||
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
 | 
			
		||||
import {
 | 
			
		||||
  getDevices,
 | 
			
		||||
  type DevicePickerItem,
 | 
			
		||||
  getDeviceEntityDisplayLookup,
 | 
			
		||||
  type DeviceEntityDisplayLookup,
 | 
			
		||||
  type DeviceRegistryEntry,
 | 
			
		||||
} from "../../data/device_registry";
 | 
			
		||||
import { domainToName } from "../../data/integration";
 | 
			
		||||
import type { HomeAssistant } from "../../types";
 | 
			
		||||
import { brandsUrl } from "../../util/brands-url";
 | 
			
		||||
import "../ha-generic-picker";
 | 
			
		||||
import type { HaGenericPicker } from "../ha-generic-picker";
 | 
			
		||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
 | 
			
		||||
 | 
			
		||||
export type HaDevicePickerDeviceFilterFunc = (
 | 
			
		||||
  device: DeviceRegistryEntry
 | 
			
		||||
@@ -24,6 +30,11 @@ export type HaDevicePickerDeviceFilterFunc = (
 | 
			
		||||
 | 
			
		||||
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
 | 
			
		||||
 | 
			
		||||
interface DevicePickerItem extends PickerComboBoxItem {
 | 
			
		||||
  domain?: string;
 | 
			
		||||
  domain_name?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@customElement("ha-device-picker")
 | 
			
		||||
export class HaDevicePicker extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
@@ -93,8 +104,6 @@ export class HaDevicePicker extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @state() private _configEntryLookup: Record<string, ConfigEntry> = {};
 | 
			
		||||
 | 
			
		||||
  private _getDevicesMemoized = memoizeOne(getDevices);
 | 
			
		||||
 | 
			
		||||
  protected firstUpdated(_changedProperties: PropertyValues): void {
 | 
			
		||||
    super.firstUpdated(_changedProperties);
 | 
			
		||||
    this._loadConfigEntries();
 | 
			
		||||
@@ -108,18 +117,162 @@ export class HaDevicePicker extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _getItems = () =>
 | 
			
		||||
    this._getDevicesMemoized(
 | 
			
		||||
      this.hass,
 | 
			
		||||
    this._getDevices(
 | 
			
		||||
      this.hass.devices,
 | 
			
		||||
      this.hass.entities,
 | 
			
		||||
      this._configEntryLookup,
 | 
			
		||||
      this.includeDomains,
 | 
			
		||||
      this.excludeDomains,
 | 
			
		||||
      this.includeDeviceClasses,
 | 
			
		||||
      this.deviceFilter,
 | 
			
		||||
      this.entityFilter,
 | 
			
		||||
      this.excludeDevices,
 | 
			
		||||
      this.value
 | 
			
		||||
      this.excludeDevices
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  private _getDevices = memoizeOne(
 | 
			
		||||
    (
 | 
			
		||||
      haDevices: HomeAssistant["devices"],
 | 
			
		||||
      haEntities: HomeAssistant["entities"],
 | 
			
		||||
      configEntryLookup: Record<string, ConfigEntry>,
 | 
			
		||||
      includeDomains: this["includeDomains"],
 | 
			
		||||
      excludeDomains: this["excludeDomains"],
 | 
			
		||||
      includeDeviceClasses: this["includeDeviceClasses"],
 | 
			
		||||
      deviceFilter: this["deviceFilter"],
 | 
			
		||||
      entityFilter: this["entityFilter"],
 | 
			
		||||
      excludeDevices: this["excludeDevices"]
 | 
			
		||||
    ): DevicePickerItem[] => {
 | 
			
		||||
      const devices = Object.values(haDevices);
 | 
			
		||||
      const entities = Object.values(haEntities);
 | 
			
		||||
 | 
			
		||||
      let deviceEntityLookup: DeviceEntityDisplayLookup = {};
 | 
			
		||||
 | 
			
		||||
      if (
 | 
			
		||||
        includeDomains ||
 | 
			
		||||
        excludeDomains ||
 | 
			
		||||
        includeDeviceClasses ||
 | 
			
		||||
        entityFilter
 | 
			
		||||
      ) {
 | 
			
		||||
        deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let inputDevices = devices.filter(
 | 
			
		||||
        (device) => device.id === this.value || !device.disabled_by
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (includeDomains) {
 | 
			
		||||
        inputDevices = inputDevices.filter((device) => {
 | 
			
		||||
          const devEntities = deviceEntityLookup[device.id];
 | 
			
		||||
          if (!devEntities || !devEntities.length) {
 | 
			
		||||
            return false;
 | 
			
		||||
          }
 | 
			
		||||
          return deviceEntityLookup[device.id].some((entity) =>
 | 
			
		||||
            includeDomains.includes(computeDomain(entity.entity_id))
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (excludeDomains) {
 | 
			
		||||
        inputDevices = inputDevices.filter((device) => {
 | 
			
		||||
          const devEntities = deviceEntityLookup[device.id];
 | 
			
		||||
          if (!devEntities || !devEntities.length) {
 | 
			
		||||
            return true;
 | 
			
		||||
          }
 | 
			
		||||
          return entities.every(
 | 
			
		||||
            (entity) =>
 | 
			
		||||
              !excludeDomains.includes(computeDomain(entity.entity_id))
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (excludeDevices) {
 | 
			
		||||
        inputDevices = inputDevices.filter(
 | 
			
		||||
          (device) => !excludeDevices!.includes(device.id)
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (includeDeviceClasses) {
 | 
			
		||||
        inputDevices = inputDevices.filter((device) => {
 | 
			
		||||
          const devEntities = deviceEntityLookup[device.id];
 | 
			
		||||
          if (!devEntities || !devEntities.length) {
 | 
			
		||||
            return false;
 | 
			
		||||
          }
 | 
			
		||||
          return deviceEntityLookup[device.id].some((entity) => {
 | 
			
		||||
            const stateObj = this.hass.states[entity.entity_id];
 | 
			
		||||
            if (!stateObj) {
 | 
			
		||||
              return false;
 | 
			
		||||
            }
 | 
			
		||||
            return (
 | 
			
		||||
              stateObj.attributes.device_class &&
 | 
			
		||||
              includeDeviceClasses.includes(stateObj.attributes.device_class)
 | 
			
		||||
            );
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (entityFilter) {
 | 
			
		||||
        inputDevices = inputDevices.filter((device) => {
 | 
			
		||||
          const devEntities = deviceEntityLookup[device.id];
 | 
			
		||||
          if (!devEntities || !devEntities.length) {
 | 
			
		||||
            return false;
 | 
			
		||||
          }
 | 
			
		||||
          return devEntities.some((entity) => {
 | 
			
		||||
            const stateObj = this.hass.states[entity.entity_id];
 | 
			
		||||
            if (!stateObj) {
 | 
			
		||||
              return false;
 | 
			
		||||
            }
 | 
			
		||||
            return entityFilter(stateObj);
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (deviceFilter) {
 | 
			
		||||
        inputDevices = inputDevices.filter(
 | 
			
		||||
          (device) =>
 | 
			
		||||
            // We always want to include the device of the current value
 | 
			
		||||
            device.id === this.value || deviceFilter!(device)
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const outputDevices = inputDevices.map<DevicePickerItem>((device) => {
 | 
			
		||||
        const deviceName = computeDeviceNameDisplay(
 | 
			
		||||
          device,
 | 
			
		||||
          this.hass,
 | 
			
		||||
          deviceEntityLookup[device.id]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const { area } = getDeviceContext(device, this.hass);
 | 
			
		||||
 | 
			
		||||
        const areaName = area ? computeAreaName(area) : undefined;
 | 
			
		||||
 | 
			
		||||
        const configEntry = device.primary_config_entry
 | 
			
		||||
          ? configEntryLookup?.[device.primary_config_entry]
 | 
			
		||||
          : undefined;
 | 
			
		||||
 | 
			
		||||
        const domain = configEntry?.domain;
 | 
			
		||||
        const domainName = domain
 | 
			
		||||
          ? domainToName(this.hass.localize, domain)
 | 
			
		||||
          : undefined;
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          id: device.id,
 | 
			
		||||
          label: "",
 | 
			
		||||
          primary:
 | 
			
		||||
            deviceName ||
 | 
			
		||||
            this.hass.localize("ui.components.device-picker.unnamed_device"),
 | 
			
		||||
          secondary: areaName,
 | 
			
		||||
          domain: configEntry?.domain,
 | 
			
		||||
          domain_name: domainName,
 | 
			
		||||
          search_labels: [deviceName, areaName, domain, domainName].filter(
 | 
			
		||||
            Boolean
 | 
			
		||||
          ) as string[],
 | 
			
		||||
          sorting_label: deviceName || "zzz",
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return outputDevices;
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  private _valueRenderer = memoizeOne(
 | 
			
		||||
    (configEntriesLookup: Record<string, ConfigEntry>) => (value: string) => {
 | 
			
		||||
      const deviceId = value;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,13 @@
 | 
			
		||||
import { mdiDragHorizontalVariant } from "@mdi/js";
 | 
			
		||||
import { mdiDrag } from "@mdi/js";
 | 
			
		||||
import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
 | 
			
		||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
 | 
			
		||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
 | 
			
		||||
import "../ha-sortable";
 | 
			
		||||
import "./ha-entity-picker";
 | 
			
		||||
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-entities-picker")
 | 
			
		||||
class HaEntitiesPicker extends LitElement {
 | 
			
		||||
@@ -118,7 +118,7 @@ class HaEntitiesPicker extends LitElement {
 | 
			
		||||
                  ? html`
 | 
			
		||||
                      <ha-svg-icon
 | 
			
		||||
                        class="entity-handle"
 | 
			
		||||
                        .path=${mdiDragHorizontalVariant}
 | 
			
		||||
                        .path=${mdiDrag}
 | 
			
		||||
                      ></ha-svg-icon>
 | 
			
		||||
                    `
 | 
			
		||||
                  : nothing}
 | 
			
		||||
@@ -147,7 +147,6 @@ class HaEntitiesPicker extends LitElement {
 | 
			
		||||
          .createDomains=${this.createDomains}
 | 
			
		||||
          .required=${this.required && !currentEntities.length}
 | 
			
		||||
          @value-changed=${this._addEntity}
 | 
			
		||||
          add-button
 | 
			
		||||
        ></ha-entity-picker>
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import type { HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import type { PropertyValues } from "lit";
 | 
			
		||||
import { LitElement, html, nothing } from "lit";
 | 
			
		||||
import { customElement, property, query, state } from "lit/decorators";
 | 
			
		||||
@@ -7,6 +8,8 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
 | 
			
		||||
import "../ha-combo-box";
 | 
			
		||||
import type { HaComboBox } from "../ha-combo-box";
 | 
			
		||||
 | 
			
		||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
 | 
			
		||||
 | 
			
		||||
interface AttributeOption {
 | 
			
		||||
  value: string;
 | 
			
		||||
  label: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,536 +0,0 @@
 | 
			
		||||
import "@material/mwc-menu/mwc-menu-surface";
 | 
			
		||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
 | 
			
		||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
			
		||||
import type { IFuseOptions } from "fuse.js";
 | 
			
		||||
import Fuse from "fuse.js";
 | 
			
		||||
import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property, query, state } from "lit/decorators";
 | 
			
		||||
import { repeat } from "lit/directives/repeat";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { ensureArray } from "../../common/array/ensure-array";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import { stopPropagation } from "../../common/dom/stop_propagation";
 | 
			
		||||
import type { EntityNameItem } from "../../common/entity/compute_entity_name_display";
 | 
			
		||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
 | 
			
		||||
import type { EntityNameType } from "../../common/translations/entity-state";
 | 
			
		||||
import type { LocalizeKeys } from "../../common/translations/localize";
 | 
			
		||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
 | 
			
		||||
import "../chips/ha-assist-chip";
 | 
			
		||||
import "../chips/ha-chip-set";
 | 
			
		||||
import "../chips/ha-input-chip";
 | 
			
		||||
import "../ha-combo-box";
 | 
			
		||||
import type { HaComboBox } from "../ha-combo-box";
 | 
			
		||||
import "../ha-input-helper-text";
 | 
			
		||||
import "../ha-sortable";
 | 
			
		||||
 | 
			
		||||
interface EntityNameOption {
 | 
			
		||||
  primary: string;
 | 
			
		||||
  secondary?: string;
 | 
			
		||||
  field_label: string;
 | 
			
		||||
  value: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const rowRenderer: ComboBoxLitRenderer<EntityNameOption> = (item) => html`
 | 
			
		||||
  <ha-combo-box-item type="button">
 | 
			
		||||
    <span slot="headline">${item.primary}</span>
 | 
			
		||||
    ${item.secondary
 | 
			
		||||
      ? html`<span slot="supporting-text">${item.secondary}</span>`
 | 
			
		||||
      : nothing}
 | 
			
		||||
  </ha-combo-box-item>
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const KNOWN_TYPES = new Set(["entity", "device", "area", "floor"]);
 | 
			
		||||
 | 
			
		||||
const UNIQUE_TYPES = new Set(["entity", "device", "area", "floor"]);
 | 
			
		||||
 | 
			
		||||
const formatOptionValue = (item: EntityNameItem) => {
 | 
			
		||||
  if (item.type === "text" && item.text) {
 | 
			
		||||
    return item.text;
 | 
			
		||||
  }
 | 
			
		||||
  return `___${item.type}___`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const parseOptionValue = (value: string): EntityNameItem => {
 | 
			
		||||
  if (value.startsWith("___") && value.endsWith("___")) {
 | 
			
		||||
    const type = value.slice(3, -3);
 | 
			
		||||
    if (KNOWN_TYPES.has(type)) {
 | 
			
		||||
      return { type: type as EntityNameType };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return { type: "text", text: value };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@customElement("ha-entity-name-picker")
 | 
			
		||||
export class HaEntityNamePicker extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: false }) public entityId?: string;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: false }) public value?:
 | 
			
		||||
    | string
 | 
			
		||||
    | EntityNameItem
 | 
			
		||||
    | EntityNameItem[];
 | 
			
		||||
 | 
			
		||||
  @property() public label?: string;
 | 
			
		||||
 | 
			
		||||
  @property() public helper?: string;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public required = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, reflect: true }) public disabled = false;
 | 
			
		||||
 | 
			
		||||
  @query(".container", true) private _container?: HTMLDivElement;
 | 
			
		||||
 | 
			
		||||
  @query("ha-combo-box", true) private _comboBox!: HaComboBox;
 | 
			
		||||
 | 
			
		||||
  @state() private _opened = false;
 | 
			
		||||
 | 
			
		||||
  private _editIndex?: number;
 | 
			
		||||
 | 
			
		||||
  private _validTypes = memoizeOne((entityId?: string) => {
 | 
			
		||||
    const options = new Set<string>(["text"]);
 | 
			
		||||
    if (!entityId) {
 | 
			
		||||
      return options;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const stateObj = this.hass.states[entityId];
 | 
			
		||||
 | 
			
		||||
    if (!stateObj) {
 | 
			
		||||
      return options;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    options.add("entity");
 | 
			
		||||
 | 
			
		||||
    const context = getEntityContext(
 | 
			
		||||
      stateObj,
 | 
			
		||||
      this.hass.entities,
 | 
			
		||||
      this.hass.devices,
 | 
			
		||||
      this.hass.areas,
 | 
			
		||||
      this.hass.floors
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (context.device) options.add("device");
 | 
			
		||||
    if (context.area) options.add("area");
 | 
			
		||||
    if (context.floor) options.add("floor");
 | 
			
		||||
    return options;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  private _getOptions = memoizeOne((entityId?: string) => {
 | 
			
		||||
    if (!entityId) {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const types = this._validTypes(entityId);
 | 
			
		||||
 | 
			
		||||
    const items = (
 | 
			
		||||
      ["entity", "device", "area", "floor"] as const
 | 
			
		||||
    ).map<EntityNameOption>((name) => {
 | 
			
		||||
      const stateObj = this.hass.states[entityId];
 | 
			
		||||
      const isValid = types.has(name);
 | 
			
		||||
      const primary = this.hass.localize(
 | 
			
		||||
        `ui.components.entity.entity-name-picker.types.${name}`
 | 
			
		||||
      );
 | 
			
		||||
      const secondary =
 | 
			
		||||
        (stateObj && isValid
 | 
			
		||||
          ? this.hass.formatEntityName(stateObj, { type: name })
 | 
			
		||||
          : this.hass.localize(
 | 
			
		||||
              `ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys
 | 
			
		||||
            )) || "-";
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        primary,
 | 
			
		||||
        secondary,
 | 
			
		||||
        field_label: primary,
 | 
			
		||||
        value: formatOptionValue({ type: name }),
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return items;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  private _customNameOption = memoizeOne((text: string) => ({
 | 
			
		||||
    primary: this.hass.localize(
 | 
			
		||||
      "ui.components.entity.entity-name-picker.custom_name"
 | 
			
		||||
    ),
 | 
			
		||||
    secondary: `"${text}"`,
 | 
			
		||||
    field_label: text,
 | 
			
		||||
    value: formatOptionValue({ type: "text", text }),
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  private _formatItem = (item: EntityNameItem) => {
 | 
			
		||||
    if (item.type === "text") {
 | 
			
		||||
      return `"${item.text}"`;
 | 
			
		||||
    }
 | 
			
		||||
    if (KNOWN_TYPES.has(item.type)) {
 | 
			
		||||
      return this.hass.localize(
 | 
			
		||||
        `ui.components.entity.entity-name-picker.types.${item.type as EntityNameType}`
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return item.type;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    const value = this._items;
 | 
			
		||||
    const options = this._getOptions(this.entityId);
 | 
			
		||||
    const validTypes = this._validTypes(this.entityId);
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      ${this.label ? html`<label>${this.label}</label>` : nothing}
 | 
			
		||||
      <div class="container">
 | 
			
		||||
        <ha-sortable
 | 
			
		||||
          no-style
 | 
			
		||||
          @item-moved=${this._moveItem}
 | 
			
		||||
          .disabled=${this.disabled}
 | 
			
		||||
          handle-selector="button.primary.action"
 | 
			
		||||
          filter=".add"
 | 
			
		||||
        >
 | 
			
		||||
          <ha-chip-set>
 | 
			
		||||
            ${repeat(
 | 
			
		||||
              this._items,
 | 
			
		||||
              (item) => item,
 | 
			
		||||
              (item: EntityNameItem, idx) => {
 | 
			
		||||
                const label = this._formatItem(item);
 | 
			
		||||
                const isValid = validTypes.has(item.type);
 | 
			
		||||
                return html`
 | 
			
		||||
                  <ha-input-chip
 | 
			
		||||
                    data-idx=${idx}
 | 
			
		||||
                    @remove=${this._removeItem}
 | 
			
		||||
                    @click=${this._editItem}
 | 
			
		||||
                    .label=${label}
 | 
			
		||||
                    .selected=${!this.disabled}
 | 
			
		||||
                    .disabled=${this.disabled}
 | 
			
		||||
                    class=${!isValid ? "invalid" : ""}
 | 
			
		||||
                  >
 | 
			
		||||
                    <ha-svg-icon
 | 
			
		||||
                      slot="icon"
 | 
			
		||||
                      .path=${mdiDragHorizontalVariant}
 | 
			
		||||
                    ></ha-svg-icon>
 | 
			
		||||
                    <span>${label}</span>
 | 
			
		||||
                  </ha-input-chip>
 | 
			
		||||
                `;
 | 
			
		||||
              }
 | 
			
		||||
            )}
 | 
			
		||||
            ${this.disabled
 | 
			
		||||
              ? nothing
 | 
			
		||||
              : html`
 | 
			
		||||
                  <ha-assist-chip
 | 
			
		||||
                    @click=${this._addItem}
 | 
			
		||||
                    .disabled=${this.disabled}
 | 
			
		||||
                    label=${this.hass.localize(
 | 
			
		||||
                      "ui.components.entity.entity-name-picker.add"
 | 
			
		||||
                    )}
 | 
			
		||||
                    class="add"
 | 
			
		||||
                  >
 | 
			
		||||
                    <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
 | 
			
		||||
                  </ha-assist-chip>
 | 
			
		||||
                `}
 | 
			
		||||
          </ha-chip-set>
 | 
			
		||||
        </ha-sortable>
 | 
			
		||||
 | 
			
		||||
        <mwc-menu-surface
 | 
			
		||||
          .open=${this._opened}
 | 
			
		||||
          @closed=${this._onClosed}
 | 
			
		||||
          @opened=${this._onOpened}
 | 
			
		||||
          @input=${stopPropagation}
 | 
			
		||||
          .anchor=${this._container}
 | 
			
		||||
        >
 | 
			
		||||
          <ha-combo-box
 | 
			
		||||
            .hass=${this.hass}
 | 
			
		||||
            .value=${""}
 | 
			
		||||
            .autofocus=${this.autofocus}
 | 
			
		||||
            .disabled=${this.disabled}
 | 
			
		||||
            .required=${this.required && !value.length}
 | 
			
		||||
            .items=${options}
 | 
			
		||||
            allow-custom-value
 | 
			
		||||
            item-id-path="value"
 | 
			
		||||
            item-value-path="value"
 | 
			
		||||
            item-label-path="field_label"
 | 
			
		||||
            .renderer=${rowRenderer}
 | 
			
		||||
            @opened-changed=${this._openedChanged}
 | 
			
		||||
            @value-changed=${this._comboBoxValueChanged}
 | 
			
		||||
            @filter-changed=${this._filterChanged}
 | 
			
		||||
          >
 | 
			
		||||
          </ha-combo-box>
 | 
			
		||||
        </mwc-menu-surface>
 | 
			
		||||
      </div>
 | 
			
		||||
      ${this._renderHelper()}
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _renderHelper() {
 | 
			
		||||
    return this.helper
 | 
			
		||||
      ? html`
 | 
			
		||||
          <ha-input-helper-text .disabled=${this.disabled}>
 | 
			
		||||
            ${this.helper}
 | 
			
		||||
          </ha-input-helper-text>
 | 
			
		||||
        `
 | 
			
		||||
      : nothing;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _onClosed(ev) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    this._opened = false;
 | 
			
		||||
    this._editIndex = undefined;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _onOpened(ev) {
 | 
			
		||||
    if (!this._opened) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    this._opened = true;
 | 
			
		||||
    await this._comboBox?.focus();
 | 
			
		||||
    await this._comboBox?.open();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _addItem(ev) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    this._opened = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _editItem(ev) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    const idx = parseInt(ev.currentTarget.dataset.idx, 10);
 | 
			
		||||
    this._editIndex = idx;
 | 
			
		||||
    this._opened = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private get _items(): EntityNameItem[] {
 | 
			
		||||
    return this._toItems(this.value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _toItems = memoizeOne((value?: typeof this.value) => {
 | 
			
		||||
    if (typeof value === "string") {
 | 
			
		||||
      if (value === "") {
 | 
			
		||||
        return [];
 | 
			
		||||
      }
 | 
			
		||||
      return [{ type: "text", text: value } satisfies EntityNameItem];
 | 
			
		||||
    }
 | 
			
		||||
    return value ? ensureArray(value) : [];
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  private _toValue = memoizeOne(
 | 
			
		||||
    (items: EntityNameItem[]): typeof this.value => {
 | 
			
		||||
      if (items.length === 0) {
 | 
			
		||||
        return "";
 | 
			
		||||
      }
 | 
			
		||||
      if (items.length === 1) {
 | 
			
		||||
        const item = items[0];
 | 
			
		||||
        return item.type === "text" ? item.text : item;
 | 
			
		||||
      }
 | 
			
		||||
      return items;
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  private _openedChanged(ev: ValueChangedEvent<boolean>) {
 | 
			
		||||
    const open = ev.detail.value;
 | 
			
		||||
    if (open) {
 | 
			
		||||
      const options = this._comboBox.items || [];
 | 
			
		||||
 | 
			
		||||
      const initialItem =
 | 
			
		||||
        this._editIndex != null ? this._items[this._editIndex] : undefined;
 | 
			
		||||
 | 
			
		||||
      const initialValue = initialItem ? formatOptionValue(initialItem) : "";
 | 
			
		||||
 | 
			
		||||
      const filteredItems = this._filterSelectedOptions(options, initialValue);
 | 
			
		||||
 | 
			
		||||
      if (initialItem?.type === "text" && initialItem.text) {
 | 
			
		||||
        filteredItems.push(this._customNameOption(initialItem.text));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this._comboBox.filteredItems = filteredItems;
 | 
			
		||||
      this._comboBox.setInputValue(initialValue);
 | 
			
		||||
    } else {
 | 
			
		||||
      this._opened = false;
 | 
			
		||||
      this._comboBox.setInputValue("");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _filterSelectedOptions = (
 | 
			
		||||
    options: EntityNameOption[],
 | 
			
		||||
    current?: string
 | 
			
		||||
  ) => {
 | 
			
		||||
    const items = this._items;
 | 
			
		||||
 | 
			
		||||
    const excludedValues = new Set(
 | 
			
		||||
      items
 | 
			
		||||
        .filter((item) => UNIQUE_TYPES.has(item.type))
 | 
			
		||||
        .map((item) => formatOptionValue(item))
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const filteredOptions = options.filter(
 | 
			
		||||
      (option) => !excludedValues.has(option.value) || option.value === current
 | 
			
		||||
    );
 | 
			
		||||
    return filteredOptions;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private _filterChanged(ev: ValueChangedEvent<string>) {
 | 
			
		||||
    const input = ev.detail.value;
 | 
			
		||||
    const filter = input?.toLowerCase() || "";
 | 
			
		||||
    const options = this._comboBox.items || [];
 | 
			
		||||
 | 
			
		||||
    const currentItem =
 | 
			
		||||
      this._editIndex != null ? this._items[this._editIndex] : undefined;
 | 
			
		||||
 | 
			
		||||
    const currentValue = currentItem ? formatOptionValue(currentItem) : "";
 | 
			
		||||
 | 
			
		||||
    let filteredItems = this._filterSelectedOptions(options, currentValue);
 | 
			
		||||
 | 
			
		||||
    if (!filter) {
 | 
			
		||||
      this._comboBox.filteredItems = filteredItems;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const fuseOptions: IFuseOptions<EntityNameOption> = {
 | 
			
		||||
      keys: ["primary", "secondary", "value"],
 | 
			
		||||
      isCaseSensitive: false,
 | 
			
		||||
      minMatchCharLength: Math.min(filter.length, 2),
 | 
			
		||||
      threshold: 0.2,
 | 
			
		||||
      ignoreDiacritics: true,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const fuse = new Fuse(filteredItems, fuseOptions);
 | 
			
		||||
    filteredItems = fuse.search(filter).map((result) => result.item);
 | 
			
		||||
    filteredItems.push(this._customNameOption(input));
 | 
			
		||||
    this._comboBox.filteredItems = filteredItems;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _moveItem(ev: CustomEvent) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    const { oldIndex, newIndex } = ev.detail;
 | 
			
		||||
    const value = this._items;
 | 
			
		||||
    const newValue = value.concat();
 | 
			
		||||
    const element = newValue.splice(oldIndex, 1)[0];
 | 
			
		||||
    newValue.splice(newIndex, 0, element);
 | 
			
		||||
    this._setValue(newValue);
 | 
			
		||||
    await this.updateComplete;
 | 
			
		||||
    this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _removeItem(ev) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    const value = [...this._items];
 | 
			
		||||
    const idx = parseInt(ev.target.dataset.idx, 10);
 | 
			
		||||
    value.splice(idx, 1);
 | 
			
		||||
    this._setValue(value);
 | 
			
		||||
    await this.updateComplete;
 | 
			
		||||
    this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    const value = ev.detail.value;
 | 
			
		||||
 | 
			
		||||
    if (this.disabled || value === "") {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const item: EntityNameItem = parseOptionValue(value);
 | 
			
		||||
 | 
			
		||||
    const newValue = [...this._items];
 | 
			
		||||
 | 
			
		||||
    if (this._editIndex != null) {
 | 
			
		||||
      newValue[this._editIndex] = item;
 | 
			
		||||
    } else {
 | 
			
		||||
      newValue.push(item);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._setValue(newValue);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _setValue(value: EntityNameItem[]) {
 | 
			
		||||
    const newValue = this._toValue(value);
 | 
			
		||||
    this.value = newValue;
 | 
			
		||||
    fireEvent(this, "value-changed", {
 | 
			
		||||
      value: newValue,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static styles = css`
 | 
			
		||||
    :host {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .container {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      background-color: var(--mdc-text-field-fill-color, whitesmoke);
 | 
			
		||||
      border-radius: var(--ha-border-radius-sm);
 | 
			
		||||
      border-end-end-radius: var(--ha-border-radius-square);
 | 
			
		||||
      border-end-start-radius: var(--ha-border-radius-square);
 | 
			
		||||
    }
 | 
			
		||||
    .container:after {
 | 
			
		||||
      display: block;
 | 
			
		||||
      content: "";
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      pointer-events: none;
 | 
			
		||||
      bottom: 0;
 | 
			
		||||
      left: 0;
 | 
			
		||||
      right: 0;
 | 
			
		||||
      height: 1px;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      background-color: var(
 | 
			
		||||
        --mdc-text-field-idle-line-color,
 | 
			
		||||
        rgba(0, 0, 0, 0.42)
 | 
			
		||||
      );
 | 
			
		||||
      transform:
 | 
			
		||||
        height 180ms ease-in-out,
 | 
			
		||||
        background-color 180ms ease-in-out;
 | 
			
		||||
    }
 | 
			
		||||
    :host([disabled]) .container:after {
 | 
			
		||||
      background-color: var(
 | 
			
		||||
        --mdc-text-field-disabled-line-color,
 | 
			
		||||
        rgba(0, 0, 0, 0.42)
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    .container:focus-within:after {
 | 
			
		||||
      height: 2px;
 | 
			
		||||
      background-color: var(--mdc-theme-primary);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    label {
 | 
			
		||||
      display: block;
 | 
			
		||||
      margin: 0 0 var(--ha-space-2);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .add {
 | 
			
		||||
      order: 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    mwc-menu-surface {
 | 
			
		||||
      --mdc-menu-min-width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ha-chip-set {
 | 
			
		||||
      padding: var(--ha-space-2) var(--ha-space-2);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .invalid {
 | 
			
		||||
      text-decoration: line-through;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .sortable-fallback {
 | 
			
		||||
      display: none;
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .sortable-ghost {
 | 
			
		||||
      opacity: 0.4;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .sortable-drag {
 | 
			
		||||
      cursor: grabbing;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ha-input-helper-text {
 | 
			
		||||
      display: block;
 | 
			
		||||
      margin: var(--ha-space-2) 0 0;
 | 
			
		||||
    }
 | 
			
		||||
  `;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-entity-name-picker": HaEntityNamePicker;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,17 +1,14 @@
 | 
			
		||||
import { mdiPlus, mdiShape } from "@mdi/js";
 | 
			
		||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
			
		||||
import type { HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
 | 
			
		||||
import { customElement, property, query } from "lit/decorators";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
 | 
			
		||||
import { computeDomain } from "../../common/entity/compute_domain";
 | 
			
		||||
import { computeStateName } from "../../common/entity/compute_state_name";
 | 
			
		||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
 | 
			
		||||
import { computeRTL } from "../../common/util/compute_rtl";
 | 
			
		||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
 | 
			
		||||
import {
 | 
			
		||||
  getEntities,
 | 
			
		||||
  type EntityComboBoxItem,
 | 
			
		||||
} from "../../data/entity_registry";
 | 
			
		||||
import { domainToName } from "../../data/integration";
 | 
			
		||||
import {
 | 
			
		||||
  isHelperDomain,
 | 
			
		||||
@@ -22,11 +19,21 @@ import type { HomeAssistant } from "../../types";
 | 
			
		||||
import "../ha-combo-box-item";
 | 
			
		||||
import "../ha-generic-picker";
 | 
			
		||||
import type { HaGenericPicker } from "../ha-generic-picker";
 | 
			
		||||
import type { PickerComboBoxSearchFn } from "../ha-picker-combo-box";
 | 
			
		||||
import type {
 | 
			
		||||
  PickerComboBoxItem,
 | 
			
		||||
  PickerComboBoxSearchFn,
 | 
			
		||||
} from "../ha-picker-combo-box";
 | 
			
		||||
import type { PickerValueRenderer } from "../ha-picker-field";
 | 
			
		||||
import "../ha-svg-icon";
 | 
			
		||||
import "./state-badge";
 | 
			
		||||
 | 
			
		||||
interface EntityComboBoxItem extends PickerComboBoxItem {
 | 
			
		||||
  domain_name?: string;
 | 
			
		||||
  stateObj?: HassEntity;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
 | 
			
		||||
 | 
			
		||||
const CREATE_ID = "___create-new-entity___";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-entity-picker")
 | 
			
		||||
@@ -113,9 +120,6 @@ export class HaEntityPicker extends LitElement {
 | 
			
		||||
  @property({ attribute: "hide-clear-icon", type: Boolean })
 | 
			
		||||
  public hideClearIcon = false;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: "add-button", type: Boolean })
 | 
			
		||||
  public addButton = false;
 | 
			
		||||
 | 
			
		||||
  @query("ha-generic-picker") private _picker?: HaGenericPicker;
 | 
			
		||||
 | 
			
		||||
  protected firstUpdated(changedProperties: PropertyValues): void {
 | 
			
		||||
@@ -140,14 +144,9 @@ export class HaEntityPicker extends LitElement {
 | 
			
		||||
      `;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const [entityName, deviceName, areaName] = computeEntityNameList(
 | 
			
		||||
      stateObj,
 | 
			
		||||
      [{ type: "entity" }, { type: "device" }, { type: "area" }],
 | 
			
		||||
      this.hass.entities,
 | 
			
		||||
      this.hass.devices,
 | 
			
		||||
      this.hass.areas,
 | 
			
		||||
      this.hass.floors
 | 
			
		||||
    );
 | 
			
		||||
    const entityName = this.hass.formatEntityName(stateObj, "entity");
 | 
			
		||||
    const deviceName = this.hass.formatEntityName(stateObj, "device");
 | 
			
		||||
    const areaName = this.hass.formatEntityName(stateObj, "area");
 | 
			
		||||
 | 
			
		||||
    const isRTL = computeRTL(this.hass);
 | 
			
		||||
 | 
			
		||||
@@ -250,10 +249,8 @@ export class HaEntityPicker extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  private _getEntitiesMemoized = memoizeOne(getEntities);
 | 
			
		||||
 | 
			
		||||
  private _getItems = () =>
 | 
			
		||||
    this._getEntitiesMemoized(
 | 
			
		||||
    this._getEntities(
 | 
			
		||||
      this.hass,
 | 
			
		||||
      this.includeDomains,
 | 
			
		||||
      this.excludeDomains,
 | 
			
		||||
@@ -261,10 +258,125 @@ export class HaEntityPicker extends LitElement {
 | 
			
		||||
      this.includeDeviceClasses,
 | 
			
		||||
      this.includeUnitOfMeasurement,
 | 
			
		||||
      this.includeEntities,
 | 
			
		||||
      this.excludeEntities,
 | 
			
		||||
      this.value
 | 
			
		||||
      this.excludeEntities
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  private _getEntities = memoizeOne(
 | 
			
		||||
    (
 | 
			
		||||
      hass: this["hass"],
 | 
			
		||||
      includeDomains: this["includeDomains"],
 | 
			
		||||
      excludeDomains: this["excludeDomains"],
 | 
			
		||||
      entityFilter: this["entityFilter"],
 | 
			
		||||
      includeDeviceClasses: this["includeDeviceClasses"],
 | 
			
		||||
      includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
 | 
			
		||||
      includeEntities: this["includeEntities"],
 | 
			
		||||
      excludeEntities: this["excludeEntities"]
 | 
			
		||||
    ): EntityComboBoxItem[] => {
 | 
			
		||||
      let items: EntityComboBoxItem[] = [];
 | 
			
		||||
 | 
			
		||||
      let entityIds = Object.keys(hass.states);
 | 
			
		||||
 | 
			
		||||
      if (includeEntities) {
 | 
			
		||||
        entityIds = entityIds.filter((entityId) =>
 | 
			
		||||
          includeEntities.includes(entityId)
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (excludeEntities) {
 | 
			
		||||
        entityIds = entityIds.filter(
 | 
			
		||||
          (entityId) => !excludeEntities.includes(entityId)
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (includeDomains) {
 | 
			
		||||
        entityIds = entityIds.filter((eid) =>
 | 
			
		||||
          includeDomains.includes(computeDomain(eid))
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (excludeDomains) {
 | 
			
		||||
        entityIds = entityIds.filter(
 | 
			
		||||
          (eid) => !excludeDomains.includes(computeDomain(eid))
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const isRTL = computeRTL(this.hass);
 | 
			
		||||
 | 
			
		||||
      items = entityIds.map<EntityComboBoxItem>((entityId) => {
 | 
			
		||||
        const stateObj = hass!.states[entityId];
 | 
			
		||||
 | 
			
		||||
        const friendlyName = computeStateName(stateObj); // Keep this for search
 | 
			
		||||
        const entityName = this.hass.formatEntityName(stateObj, "entity");
 | 
			
		||||
        const deviceName = this.hass.formatEntityName(stateObj, "device");
 | 
			
		||||
        const areaName = this.hass.formatEntityName(stateObj, "area");
 | 
			
		||||
 | 
			
		||||
        const domainName = domainToName(
 | 
			
		||||
          this.hass.localize,
 | 
			
		||||
          computeDomain(entityId)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const primary = entityName || deviceName || entityId;
 | 
			
		||||
        const secondary = [areaName, entityName ? deviceName : undefined]
 | 
			
		||||
          .filter(Boolean)
 | 
			
		||||
          .join(isRTL ? " ◂ " : " ▸ ");
 | 
			
		||||
        const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
          id: entityId,
 | 
			
		||||
          primary: primary,
 | 
			
		||||
          secondary: secondary,
 | 
			
		||||
          domain_name: domainName,
 | 
			
		||||
          sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
 | 
			
		||||
          search_labels: [
 | 
			
		||||
            entityName,
 | 
			
		||||
            deviceName,
 | 
			
		||||
            areaName,
 | 
			
		||||
            domainName,
 | 
			
		||||
            friendlyName,
 | 
			
		||||
            entityId,
 | 
			
		||||
          ].filter(Boolean) as string[],
 | 
			
		||||
          a11y_label: a11yLabel,
 | 
			
		||||
          stateObj: stateObj,
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (includeDeviceClasses) {
 | 
			
		||||
        items = items.filter(
 | 
			
		||||
          (item) =>
 | 
			
		||||
            // We always want to include the entity of the current value
 | 
			
		||||
            item.id === this.value ||
 | 
			
		||||
            (item.stateObj?.attributes.device_class &&
 | 
			
		||||
              includeDeviceClasses.includes(
 | 
			
		||||
                item.stateObj.attributes.device_class
 | 
			
		||||
              ))
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (includeUnitOfMeasurement) {
 | 
			
		||||
        items = items.filter(
 | 
			
		||||
          (item) =>
 | 
			
		||||
            // We always want to include the entity of the current value
 | 
			
		||||
            item.id === this.value ||
 | 
			
		||||
            (item.stateObj?.attributes.unit_of_measurement &&
 | 
			
		||||
              includeUnitOfMeasurement.includes(
 | 
			
		||||
                item.stateObj.attributes.unit_of_measurement
 | 
			
		||||
              ))
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (entityFilter) {
 | 
			
		||||
        items = items.filter(
 | 
			
		||||
          (item) =>
 | 
			
		||||
            // We always want to include the entity of the current value
 | 
			
		||||
            item.id === this.value ||
 | 
			
		||||
            (item.stateObj && entityFilter!(item.stateObj))
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return items;
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    const placeholder =
 | 
			
		||||
      this.placeholder ??
 | 
			
		||||
@@ -284,7 +396,7 @@ export class HaEntityPicker extends LitElement {
 | 
			
		||||
        .searchLabel=${this.searchLabel}
 | 
			
		||||
        .notFoundLabel=${notFoundLabel}
 | 
			
		||||
        .placeholder=${placeholder}
 | 
			
		||||
        .value=${this.addButton ? undefined : this.value}
 | 
			
		||||
        .value=${this.value}
 | 
			
		||||
        .rowRenderer=${this._rowRenderer}
 | 
			
		||||
        .getItems=${this._getItems}
 | 
			
		||||
        .getAdditionalItems=${this._getAdditionalItems}
 | 
			
		||||
@@ -292,9 +404,6 @@ export class HaEntityPicker extends LitElement {
 | 
			
		||||
        .searchFn=${this._searchFn}
 | 
			
		||||
        .valueRenderer=${this._valueRenderer}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
        .addButtonLabel=${this.addButton
 | 
			
		||||
          ? this.hass.localize("ui.components.entity.entity-picker.add")
 | 
			
		||||
          : undefined}
 | 
			
		||||
      >
 | 
			
		||||
      </ha-generic-picker>
 | 
			
		||||
    `;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,39 +1,23 @@
 | 
			
		||||
import "@material/mwc-menu/mwc-menu-surface";
 | 
			
		||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
 | 
			
		||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
			
		||||
import type { IFuseOptions } from "fuse.js";
 | 
			
		||||
import Fuse from "fuse.js";
 | 
			
		||||
import { mdiDrag } from "@mdi/js";
 | 
			
		||||
import type { HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import type { PropertyValues } from "lit";
 | 
			
		||||
import { LitElement, css, html, nothing } from "lit";
 | 
			
		||||
import { customElement, property, query, state } from "lit/decorators";
 | 
			
		||||
import { repeat } from "lit/directives/repeat";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { ensureArray } from "../../common/array/ensure-array";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import { stopPropagation } from "../../common/dom/stop_propagation";
 | 
			
		||||
import { computeDomain } from "../../common/entity/compute_domain";
 | 
			
		||||
import {
 | 
			
		||||
  STATE_DISPLAY_SPECIAL_CONTENT,
 | 
			
		||||
  STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS,
 | 
			
		||||
} from "../../state-display/state-display";
 | 
			
		||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
 | 
			
		||||
import "../chips/ha-assist-chip";
 | 
			
		||||
import "../chips/ha-chip-set";
 | 
			
		||||
import "../chips/ha-input-chip";
 | 
			
		||||
import "../ha-combo-box";
 | 
			
		||||
import type { HaComboBox } from "../ha-combo-box";
 | 
			
		||||
import "../ha-sortable";
 | 
			
		||||
 | 
			
		||||
interface StateContentOption {
 | 
			
		||||
  primary: string;
 | 
			
		||||
  value: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const rowRenderer: ComboBoxLitRenderer<StateContentOption> = (item) => html`
 | 
			
		||||
  <ha-combo-box-item type="button">
 | 
			
		||||
    <span slot="headline">${item.primary}</span>
 | 
			
		||||
  </ha-combo-box-item>
 | 
			
		||||
`;
 | 
			
		||||
import "../chips/ha-input-chip";
 | 
			
		||||
import "../chips/ha-chip-set";
 | 
			
		||||
import type { HaComboBox } from "../ha-combo-box";
 | 
			
		||||
 | 
			
		||||
const HIDDEN_ATTRIBUTES = [
 | 
			
		||||
  "access_token",
 | 
			
		||||
@@ -90,7 +74,7 @@ const HIDDEN_ATTRIBUTES = [
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@customElement("ha-entity-state-content-picker")
 | 
			
		||||
export class HaStateContentPicker extends LitElement {
 | 
			
		||||
class HaEntityStatePicker extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: false }) public entityId?: string;
 | 
			
		||||
@@ -111,28 +95,26 @@ export class HaStateContentPicker extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @property() public helper?: string;
 | 
			
		||||
 | 
			
		||||
  @query(".container", true) private _container?: HTMLDivElement;
 | 
			
		||||
  @state() private _opened = false;
 | 
			
		||||
 | 
			
		||||
  @query("ha-combo-box", true) private _comboBox!: HaComboBox;
 | 
			
		||||
 | 
			
		||||
  @state() private _opened = false;
 | 
			
		||||
  protected shouldUpdate(changedProps: PropertyValues) {
 | 
			
		||||
    return !(!changedProps.has("_opened") && this._opened);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _editIndex?: number;
 | 
			
		||||
 | 
			
		||||
  private _options = memoizeOne(
 | 
			
		||||
  private options = memoizeOne(
 | 
			
		||||
    (entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
 | 
			
		||||
      const domain = entityId ? computeDomain(entityId) : undefined;
 | 
			
		||||
      return [
 | 
			
		||||
        {
 | 
			
		||||
          primary: this.hass.localize(
 | 
			
		||||
            "ui.components.state-content-picker.state"
 | 
			
		||||
          ),
 | 
			
		||||
          label: this.hass.localize("ui.components.state-content-picker.state"),
 | 
			
		||||
          value: "state",
 | 
			
		||||
        },
 | 
			
		||||
        ...(allowName
 | 
			
		||||
          ? [
 | 
			
		||||
              {
 | 
			
		||||
                primary: this.hass.localize(
 | 
			
		||||
                label: this.hass.localize(
 | 
			
		||||
                  "ui.components.state-content-picker.name"
 | 
			
		||||
                ),
 | 
			
		||||
                value: "name",
 | 
			
		||||
@@ -140,13 +122,13 @@ export class HaStateContentPicker extends LitElement {
 | 
			
		||||
            ]
 | 
			
		||||
          : []),
 | 
			
		||||
        {
 | 
			
		||||
          primary: this.hass.localize(
 | 
			
		||||
          label: this.hass.localize(
 | 
			
		||||
            "ui.components.state-content-picker.last_changed"
 | 
			
		||||
          ),
 | 
			
		||||
          value: "last_changed",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          primary: this.hass.localize(
 | 
			
		||||
          label: this.hass.localize(
 | 
			
		||||
            "ui.components.state-content-picker.last_updated"
 | 
			
		||||
          ),
 | 
			
		||||
          value: "last_updated",
 | 
			
		||||
@@ -155,7 +137,7 @@ export class HaStateContentPicker extends LitElement {
 | 
			
		||||
          ? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
 | 
			
		||||
              STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
 | 
			
		||||
            ).map((content) => ({
 | 
			
		||||
              primary: this.hass.localize(
 | 
			
		||||
              label: this.hass.localize(
 | 
			
		||||
                `ui.components.state-content-picker.${content}`
 | 
			
		||||
              ),
 | 
			
		||||
              value: content,
 | 
			
		||||
@@ -164,201 +146,105 @@ export class HaStateContentPicker extends LitElement {
 | 
			
		||||
        ...Object.keys(stateObj?.attributes ?? {})
 | 
			
		||||
          .filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
 | 
			
		||||
          .map((attribute) => ({
 | 
			
		||||
            primary: this.hass.formatEntityAttributeName(stateObj!, attribute),
 | 
			
		||||
            value: attribute,
 | 
			
		||||
            label: this.hass.formatEntityAttributeName(stateObj!, attribute),
 | 
			
		||||
          })),
 | 
			
		||||
      ] satisfies StateContentOption[];
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  private _filter = "";
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    if (!this.hass) {
 | 
			
		||||
      return nothing;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const value = this._value;
 | 
			
		||||
 | 
			
		||||
    const stateObj = this.entityId
 | 
			
		||||
      ? this.hass.states[this.entityId]
 | 
			
		||||
      : undefined;
 | 
			
		||||
 | 
			
		||||
    const options = this._options(this.entityId, stateObj, this.allowName);
 | 
			
		||||
    const options = this.options(this.entityId, stateObj, this.allowName);
 | 
			
		||||
    const optionItems = options.filter(
 | 
			
		||||
      (option) => !this._value.includes(option.value)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      ${this.label ? html`<label>${this.label}</label>` : nothing}
 | 
			
		||||
      <div class="container ${this.disabled ? "disabled" : ""}">
 | 
			
		||||
        <ha-sortable
 | 
			
		||||
          no-style
 | 
			
		||||
          @item-moved=${this._moveItem}
 | 
			
		||||
          .disabled=${this.disabled}
 | 
			
		||||
          handle-selector="button.primary.action"
 | 
			
		||||
          filter=".add"
 | 
			
		||||
        >
 | 
			
		||||
          <ha-chip-set>
 | 
			
		||||
            ${repeat(
 | 
			
		||||
              this._value,
 | 
			
		||||
              (item) => item,
 | 
			
		||||
              (item: string, idx) => {
 | 
			
		||||
                const label = options.find((o) => o.value === item)?.primary;
 | 
			
		||||
                const isValid = !!label;
 | 
			
		||||
                return html`
 | 
			
		||||
                  <ha-input-chip
 | 
			
		||||
                    data-idx=${idx}
 | 
			
		||||
                    @remove=${this._removeItem}
 | 
			
		||||
                    @click=${this._editItem}
 | 
			
		||||
                    .label=${label || item}
 | 
			
		||||
                    .selected=${!this.disabled}
 | 
			
		||||
                    .disabled=${this.disabled}
 | 
			
		||||
                    class=${!isValid ? "invalid" : ""}
 | 
			
		||||
                  >
 | 
			
		||||
                    <ha-svg-icon
 | 
			
		||||
                      slot="icon"
 | 
			
		||||
                      .path=${mdiDragHorizontalVariant}
 | 
			
		||||
                    ></ha-svg-icon>
 | 
			
		||||
                  </ha-input-chip>
 | 
			
		||||
                `;
 | 
			
		||||
              }
 | 
			
		||||
            )}
 | 
			
		||||
            ${this.disabled
 | 
			
		||||
              ? nothing
 | 
			
		||||
              : html`
 | 
			
		||||
                  <ha-assist-chip
 | 
			
		||||
                    @click=${this._addItem}
 | 
			
		||||
                    .disabled=${this.disabled}
 | 
			
		||||
                    label=${this.hass.localize(
 | 
			
		||||
                      "ui.components.entity.entity-state-content-picker.add"
 | 
			
		||||
                    )}
 | 
			
		||||
                    class="add"
 | 
			
		||||
                  >
 | 
			
		||||
                    <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
 | 
			
		||||
                  </ha-assist-chip>
 | 
			
		||||
                `}
 | 
			
		||||
          </ha-chip-set>
 | 
			
		||||
        </ha-sortable>
 | 
			
		||||
      ${value?.length
 | 
			
		||||
        ? html`
 | 
			
		||||
            <ha-sortable
 | 
			
		||||
              no-style
 | 
			
		||||
              @item-moved=${this._moveItem}
 | 
			
		||||
              .disabled=${this.disabled}
 | 
			
		||||
              handle-selector="button.primary.action"
 | 
			
		||||
            >
 | 
			
		||||
              <ha-chip-set>
 | 
			
		||||
                ${repeat(
 | 
			
		||||
                  this._value,
 | 
			
		||||
                  (item) => item,
 | 
			
		||||
                  (item, idx) => {
 | 
			
		||||
                    const label =
 | 
			
		||||
                      options.find((option) => option.value === item)?.label ||
 | 
			
		||||
                      item;
 | 
			
		||||
                    return html`
 | 
			
		||||
                      <ha-input-chip
 | 
			
		||||
                        .idx=${idx}
 | 
			
		||||
                        @remove=${this._removeItem}
 | 
			
		||||
                        .label=${label}
 | 
			
		||||
                        selected
 | 
			
		||||
                      >
 | 
			
		||||
                        <ha-svg-icon slot="icon" .path=${mdiDrag}></ha-svg-icon>
 | 
			
		||||
                        ${label}
 | 
			
		||||
                      </ha-input-chip>
 | 
			
		||||
                    `;
 | 
			
		||||
                  }
 | 
			
		||||
                )}
 | 
			
		||||
              </ha-chip-set>
 | 
			
		||||
            </ha-sortable>
 | 
			
		||||
          `
 | 
			
		||||
        : nothing}
 | 
			
		||||
 | 
			
		||||
        <mwc-menu-surface
 | 
			
		||||
          .open=${this._opened}
 | 
			
		||||
          @closed=${this._onClosed}
 | 
			
		||||
          @opened=${this._onOpened}
 | 
			
		||||
          @input=${stopPropagation}
 | 
			
		||||
          .anchor=${this._container}
 | 
			
		||||
        >
 | 
			
		||||
          <ha-combo-box
 | 
			
		||||
            .hass=${this.hass}
 | 
			
		||||
            .value=${""}
 | 
			
		||||
            .autofocus=${this.autofocus}
 | 
			
		||||
            .disabled=${this.disabled || !this.entityId}
 | 
			
		||||
            .required=${this.required && !value.length}
 | 
			
		||||
            .helper=${this.helper}
 | 
			
		||||
            .items=${options}
 | 
			
		||||
            allow-custom-value
 | 
			
		||||
            item-id-path="value"
 | 
			
		||||
            item-value-path="value"
 | 
			
		||||
            item-label-path="primary"
 | 
			
		||||
            .renderer=${rowRenderer}
 | 
			
		||||
            @opened-changed=${this._openedChanged}
 | 
			
		||||
            @value-changed=${this._comboBoxValueChanged}
 | 
			
		||||
            @filter-changed=${this._filterChanged}
 | 
			
		||||
          >
 | 
			
		||||
          </ha-combo-box>
 | 
			
		||||
        </mwc-menu-surface>
 | 
			
		||||
      </div>
 | 
			
		||||
      <ha-combo-box
 | 
			
		||||
        item-value-path="value"
 | 
			
		||||
        item-label-path="label"
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        .label=${this.label}
 | 
			
		||||
        .helper=${this.helper}
 | 
			
		||||
        .disabled=${this.disabled}
 | 
			
		||||
        .required=${this.required && !value.length}
 | 
			
		||||
        .value=${""}
 | 
			
		||||
        .items=${optionItems}
 | 
			
		||||
        allow-custom-value
 | 
			
		||||
        @filter-changed=${this._filterChanged}
 | 
			
		||||
        @value-changed=${this._comboBoxValueChanged}
 | 
			
		||||
        @opened-changed=${this._openedChanged}
 | 
			
		||||
      ></ha-combo-box>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _onClosed(ev) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    this._opened = false;
 | 
			
		||||
    this._editIndex = undefined;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _onOpened(ev) {
 | 
			
		||||
    if (!this._opened) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    this._opened = true;
 | 
			
		||||
    await this._comboBox?.focus();
 | 
			
		||||
    await this._comboBox?.open();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _addItem(ev) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    this._opened = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _editItem(ev) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    const idx = parseInt(ev.currentTarget.dataset.idx, 10);
 | 
			
		||||
    this._editIndex = idx;
 | 
			
		||||
    this._opened = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private get _value() {
 | 
			
		||||
    return !this.value ? [] : ensureArray(this.value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _toValue = memoizeOne((value: string[]): typeof this.value => {
 | 
			
		||||
    if (value.length === 0) {
 | 
			
		||||
      return undefined;
 | 
			
		||||
    }
 | 
			
		||||
    if (value.length === 1) {
 | 
			
		||||
      return value[0];
 | 
			
		||||
    }
 | 
			
		||||
    return value;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  private _openedChanged(ev: ValueChangedEvent<boolean>) {
 | 
			
		||||
    const open = ev.detail.value;
 | 
			
		||||
    if (open) {
 | 
			
		||||
      const options = this._comboBox.items || [];
 | 
			
		||||
 | 
			
		||||
      const initialValue =
 | 
			
		||||
        this._editIndex != null ? this._value[this._editIndex] : "";
 | 
			
		||||
      const filteredItems = this._filterSelectedOptions(options, initialValue);
 | 
			
		||||
 | 
			
		||||
      this._comboBox.filteredItems = filteredItems;
 | 
			
		||||
      this._comboBox.setInputValue(initialValue);
 | 
			
		||||
    } else {
 | 
			
		||||
      this._opened = false;
 | 
			
		||||
    }
 | 
			
		||||
    this._opened = ev.detail.value;
 | 
			
		||||
    this._comboBox.filteredItems = this._comboBox.items;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _filterSelectedOptions = (
 | 
			
		||||
    options: StateContentOption[],
 | 
			
		||||
    current?: string
 | 
			
		||||
  ) => {
 | 
			
		||||
    const value = this._value;
 | 
			
		||||
  private _filterChanged(ev?: CustomEvent): void {
 | 
			
		||||
    this._filter = ev?.detail.value || "";
 | 
			
		||||
 | 
			
		||||
    return options.filter(
 | 
			
		||||
      (option) => !value.includes(option.value) || option.value === current
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
    const filteredItems = this._comboBox.items?.filter((item) => {
 | 
			
		||||
      const label = item.label || item.value;
 | 
			
		||||
      return label.toLowerCase().includes(this._filter?.toLowerCase());
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  private _filterChanged(ev: ValueChangedEvent<string>) {
 | 
			
		||||
    const input = ev.detail.value;
 | 
			
		||||
    const filter = input?.toLowerCase() || "";
 | 
			
		||||
    const options = this._comboBox.items || [];
 | 
			
		||||
 | 
			
		||||
    const currentValue =
 | 
			
		||||
      this._editIndex != null ? this._value[this._editIndex] : "";
 | 
			
		||||
 | 
			
		||||
    this._comboBox.filteredItems = this._filterSelectedOptions(
 | 
			
		||||
      options,
 | 
			
		||||
      currentValue
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!filter) {
 | 
			
		||||
      return;
 | 
			
		||||
    if (this._filter) {
 | 
			
		||||
      filteredItems?.unshift({ label: this._filter, value: this._filter });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const fuseOptions: IFuseOptions<StateContentOption> = {
 | 
			
		||||
      keys: ["primary", "secondary", "value"],
 | 
			
		||||
      isCaseSensitive: false,
 | 
			
		||||
      minMatchCharLength: Math.min(filter.length, 2),
 | 
			
		||||
      threshold: 0.2,
 | 
			
		||||
      ignoreDiacritics: true,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions);
 | 
			
		||||
    const filteredItems = fuse.search(filter).map((result) => result.item);
 | 
			
		||||
 | 
			
		||||
    this._comboBox.filteredItems = filteredItems;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -371,40 +257,43 @@ export class HaStateContentPicker extends LitElement {
 | 
			
		||||
    newValue.splice(newIndex, 0, element);
 | 
			
		||||
    this._setValue(newValue);
 | 
			
		||||
    await this.updateComplete;
 | 
			
		||||
    this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
 | 
			
		||||
    this._filterChanged();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _removeItem(ev) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    const value = [...this._value];
 | 
			
		||||
    const idx = parseInt(ev.target.dataset.idx, 10);
 | 
			
		||||
    value.splice(idx, 1);
 | 
			
		||||
    const value: string[] = [...this._value];
 | 
			
		||||
    value.splice(ev.target.idx, 1);
 | 
			
		||||
    this._setValue(value);
 | 
			
		||||
    await this.updateComplete;
 | 
			
		||||
    this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
 | 
			
		||||
    this._filterChanged();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
 | 
			
		||||
  private _comboBoxValueChanged(ev: CustomEvent): void {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    const value = ev.detail.value;
 | 
			
		||||
    const newValue = ev.detail.value;
 | 
			
		||||
 | 
			
		||||
    if (this.disabled || value === "") {
 | 
			
		||||
    if (this.disabled || newValue === "") {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const newValue = [...this._value];
 | 
			
		||||
    const currentValue = this._value;
 | 
			
		||||
 | 
			
		||||
    if (this._editIndex != null) {
 | 
			
		||||
      newValue[this._editIndex] = value;
 | 
			
		||||
    } else {
 | 
			
		||||
      newValue.push(value);
 | 
			
		||||
    if (currentValue.includes(newValue)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._setValue(newValue);
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      this._filterChanged();
 | 
			
		||||
      this._comboBox.setInputValue("");
 | 
			
		||||
    }, 0);
 | 
			
		||||
 | 
			
		||||
    this._setValue([...currentValue, newValue]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _setValue(value: string[]) {
 | 
			
		||||
    const newValue = this._toValue(value);
 | 
			
		||||
    const newValue =
 | 
			
		||||
      value.length === 0 ? undefined : value.length === 1 ? value[0] : value;
 | 
			
		||||
    this.value = newValue;
 | 
			
		||||
    fireEvent(this, "value-changed", {
 | 
			
		||||
      value: newValue,
 | 
			
		||||
@@ -414,64 +303,10 @@ export class HaStateContentPicker extends LitElement {
 | 
			
		||||
  static styles = css`
 | 
			
		||||
    :host {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .container {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      background-color: var(--mdc-text-field-fill-color, whitesmoke);
 | 
			
		||||
      border-radius: var(--ha-border-radius-sm);
 | 
			
		||||
      border-end-end-radius: var(--ha-border-radius-square);
 | 
			
		||||
      border-end-start-radius: var(--ha-border-radius-square);
 | 
			
		||||
    }
 | 
			
		||||
    .container:after {
 | 
			
		||||
      display: block;
 | 
			
		||||
      content: "";
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      pointer-events: none;
 | 
			
		||||
      bottom: 0;
 | 
			
		||||
      left: 0;
 | 
			
		||||
      right: 0;
 | 
			
		||||
      height: 1px;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      background-color: var(
 | 
			
		||||
        --mdc-text-field-idle-line-color,
 | 
			
		||||
        rgba(0, 0, 0, 0.42)
 | 
			
		||||
      );
 | 
			
		||||
      transform:
 | 
			
		||||
        height 180ms ease-in-out,
 | 
			
		||||
        background-color 180ms ease-in-out;
 | 
			
		||||
    }
 | 
			
		||||
    .container.disabled:after {
 | 
			
		||||
      background-color: var(
 | 
			
		||||
        --mdc-text-field-disabled-line-color,
 | 
			
		||||
        rgba(0, 0, 0, 0.42)
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    .container:focus-within:after {
 | 
			
		||||
      height: 2px;
 | 
			
		||||
      background-color: var(--mdc-theme-primary);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    label {
 | 
			
		||||
      display: block;
 | 
			
		||||
      margin: 0 0 var(--ha-space-2);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .add {
 | 
			
		||||
      order: 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    mwc-menu-surface {
 | 
			
		||||
      --mdc-menu-min-width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ha-chip-set {
 | 
			
		||||
      padding: var(--ha-space-2) var(--ha-space-2);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .invalid {
 | 
			
		||||
      text-decoration: line-through;
 | 
			
		||||
      padding: 8px 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .sortable-fallback {
 | 
			
		||||
@@ -491,6 +326,6 @@ export class HaStateContentPicker extends LitElement {
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-entity-state-content-picker": HaStateContentPicker;
 | 
			
		||||
    "ha-entity-state-content-picker": HaEntityStatePicker;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import type { HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import type { PropertyValues } from "lit";
 | 
			
		||||
import { LitElement, html, nothing } from "lit";
 | 
			
		||||
import { customElement, property, query, state } from "lit/decorators";
 | 
			
		||||
@@ -8,6 +9,8 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
 | 
			
		||||
import "../ha-combo-box";
 | 
			
		||||
import type { HaComboBox } from "../ha-combo-box";
 | 
			
		||||
 | 
			
		||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
 | 
			
		||||
 | 
			
		||||
interface StateOption {
 | 
			
		||||
  value: string;
 | 
			
		||||
  label: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -112,7 +112,7 @@ export class HaEntityToggle extends LitElement {
 | 
			
		||||
    if (!this.hass || !this.stateObj) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    forwardHaptic(this, "light");
 | 
			
		||||
    forwardHaptic("light");
 | 
			
		||||
    const stateDomain = computeStateDomain(this.stateObj);
 | 
			
		||||
    let serviceDomain;
 | 
			
		||||
    let service;
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ import { customElement, property, query } from "lit/decorators";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { ensureArray } from "../../common/array/ensure-array";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
 | 
			
		||||
import { computeStateName } from "../../common/entity/compute_state_name";
 | 
			
		||||
import { computeRTL } from "../../common/util/compute_rtl";
 | 
			
		||||
import { domainToName } from "../../data/integration";
 | 
			
		||||
@@ -200,7 +199,7 @@ export class HaStatisticPicker extends LitElement {
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const isRTL = computeRTL(hass);
 | 
			
		||||
      const isRTL = computeRTL(this.hass);
 | 
			
		||||
 | 
			
		||||
      const output: StatisticComboBoxItem[] = [];
 | 
			
		||||
 | 
			
		||||
@@ -257,15 +256,9 @@ export class HaStatisticPicker extends LitElement {
 | 
			
		||||
        const id = meta.statistic_id;
 | 
			
		||||
 | 
			
		||||
        const friendlyName = computeStateName(stateObj); // Keep this for search
 | 
			
		||||
 | 
			
		||||
        const [entityName, deviceName, areaName] = computeEntityNameList(
 | 
			
		||||
          stateObj,
 | 
			
		||||
          [{ type: "entity" }, { type: "device" }, { type: "area" }],
 | 
			
		||||
          hass.entities,
 | 
			
		||||
          hass.devices,
 | 
			
		||||
          hass.areas,
 | 
			
		||||
          hass.floors
 | 
			
		||||
        );
 | 
			
		||||
        const entityName = hass.formatEntityName(stateObj, "entity");
 | 
			
		||||
        const deviceName = hass.formatEntityName(stateObj, "device");
 | 
			
		||||
        const areaName = hass.formatEntityName(stateObj, "area");
 | 
			
		||||
 | 
			
		||||
        const primary = entityName || deviceName || id;
 | 
			
		||||
        const secondary = [areaName, entityName ? deviceName : undefined]
 | 
			
		||||
@@ -338,14 +331,9 @@ export class HaStatisticPicker extends LitElement {
 | 
			
		||||
    const stateObj = this.hass.states[statisticId];
 | 
			
		||||
 | 
			
		||||
    if (stateObj) {
 | 
			
		||||
      const [entityName, deviceName, areaName] = computeEntityNameList(
 | 
			
		||||
        stateObj,
 | 
			
		||||
        [{ type: "entity" }, { type: "device" }, { type: "area" }],
 | 
			
		||||
        this.hass.entities,
 | 
			
		||||
        this.hass.devices,
 | 
			
		||||
        this.hass.areas,
 | 
			
		||||
        this.hass.floors
 | 
			
		||||
      );
 | 
			
		||||
      const entityName = this.hass.formatEntityName(stateObj, "entity");
 | 
			
		||||
      const deviceName = this.hass.formatEntityName(stateObj, "device");
 | 
			
		||||
      const areaName = this.hass.formatEntityName(stateObj, "area");
 | 
			
		||||
 | 
			
		||||
      const isRTL = computeRTL(this.hass);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -236,7 +236,7 @@ export class StateBadge extends LitElement {
 | 
			
		||||
          border-radius: var(--state-badge-with-media-image-border-radius, 8%);
 | 
			
		||||
        }
 | 
			
		||||
        :host(.has-no-radius) {
 | 
			
		||||
          border-radius: var(--ha-border-radius-square);
 | 
			
		||||
          border-radius: 0;
 | 
			
		||||
        }
 | 
			
		||||
        :host(:focus) {
 | 
			
		||||
          outline: none;
 | 
			
		||||
 
 | 
			
		||||
@@ -99,7 +99,7 @@ class HaAlert extends LitElement {
 | 
			
		||||
      opacity: 0.12;
 | 
			
		||||
      pointer-events: none;
 | 
			
		||||
      content: "";
 | 
			
		||||
      border-radius: var(--ha-border-radius-sm);
 | 
			
		||||
      border-radius: 4px;
 | 
			
		||||
    }
 | 
			
		||||
    .icon.no-title {
 | 
			
		||||
      align-self: center;
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,7 @@ export class HaAnalytics extends LitElement {
 | 
			
		||||
        </span>
 | 
			
		||||
        <ha-switch
 | 
			
		||||
          @change=${this._handleRowClick}
 | 
			
		||||
          .checked=${!!baseEnabled}
 | 
			
		||||
          .checked=${baseEnabled}
 | 
			
		||||
          .preference=${"base"}
 | 
			
		||||
          .disabled=${loading}
 | 
			
		||||
          name="base"
 | 
			
		||||
@@ -70,7 +70,7 @@ export class HaAnalytics extends LitElement {
 | 
			
		||||
              <ha-switch
 | 
			
		||||
                .id="switch-${preference}"
 | 
			
		||||
                @change=${this._handleRowClick}
 | 
			
		||||
                .checked=${!!this.analytics?.preferences[preference]}
 | 
			
		||||
                .checked=${this.analytics?.preferences[preference]}
 | 
			
		||||
                .preference=${preference}
 | 
			
		||||
                name=${preference}
 | 
			
		||||
              >
 | 
			
		||||
@@ -102,7 +102,7 @@ export class HaAnalytics extends LitElement {
 | 
			
		||||
        </span>
 | 
			
		||||
        <ha-switch
 | 
			
		||||
          @change=${this._handleRowClick}
 | 
			
		||||
          .checked=${!!this.analytics?.preferences.diagnostics}
 | 
			
		||||
          .checked=${this.analytics?.preferences.diagnostics}
 | 
			
		||||
          .preference=${"diagnostics"}
 | 
			
		||||
          .disabled=${loading}
 | 
			
		||||
          name="diagnostics"
 | 
			
		||||
 
 | 
			
		||||
@@ -8,13 +8,21 @@ import { styleMap } from "lit/directives/style-map";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { fireEvent } from "../common/dom/fire_event";
 | 
			
		||||
import { computeAreaName } from "../common/entity/compute_area_name";
 | 
			
		||||
import { computeDomain } from "../common/entity/compute_domain";
 | 
			
		||||
import { computeFloorName } from "../common/entity/compute_floor_name";
 | 
			
		||||
import { stringCompare } from "../common/string/compare";
 | 
			
		||||
import { computeRTL } from "../common/util/compute_rtl";
 | 
			
		||||
import type { AreaRegistryEntry } from "../data/area_registry";
 | 
			
		||||
import type {
 | 
			
		||||
  DeviceEntityDisplayLookup,
 | 
			
		||||
  DeviceRegistryEntry,
 | 
			
		||||
} from "../data/device_registry";
 | 
			
		||||
import { getDeviceEntityDisplayLookup } from "../data/device_registry";
 | 
			
		||||
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
 | 
			
		||||
import {
 | 
			
		||||
  getAreasAndFloors,
 | 
			
		||||
  type AreaFloorValue,
 | 
			
		||||
  type FloorComboBoxItem,
 | 
			
		||||
} from "../data/area_floor";
 | 
			
		||||
  getFloorAreaLookup,
 | 
			
		||||
  type FloorRegistryEntry,
 | 
			
		||||
} from "../data/floor_registry";
 | 
			
		||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
 | 
			
		||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
 | 
			
		||||
import "./ha-combo-box-item";
 | 
			
		||||
@@ -22,12 +30,24 @@ import "./ha-floor-icon";
 | 
			
		||||
import "./ha-generic-picker";
 | 
			
		||||
import type { HaGenericPicker } from "./ha-generic-picker";
 | 
			
		||||
import "./ha-icon-button";
 | 
			
		||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
 | 
			
		||||
import type { PickerValueRenderer } from "./ha-picker-field";
 | 
			
		||||
import "./ha-svg-icon";
 | 
			
		||||
import "./ha-tree-indicator";
 | 
			
		||||
 | 
			
		||||
const SEPARATOR = "________";
 | 
			
		||||
 | 
			
		||||
interface FloorComboBoxItem extends PickerComboBoxItem {
 | 
			
		||||
  type: "floor" | "area";
 | 
			
		||||
  floor?: FloorRegistryEntry;
 | 
			
		||||
  area?: AreaRegistryEntry;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface AreaFloorValue {
 | 
			
		||||
  id: string;
 | 
			
		||||
  type: "floor" | "area";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@customElement("ha-area-floor-picker")
 | 
			
		||||
export class HaAreaFloorPicker extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
@@ -134,6 +154,243 @@ export class HaAreaFloorPicker extends LitElement {
 | 
			
		||||
    `;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private _getAreasAndFloors = memoizeOne(
 | 
			
		||||
    (
 | 
			
		||||
      haFloors: HomeAssistant["floors"],
 | 
			
		||||
      haAreas: HomeAssistant["areas"],
 | 
			
		||||
      haDevices: HomeAssistant["devices"],
 | 
			
		||||
      haEntities: HomeAssistant["entities"],
 | 
			
		||||
      includeDomains: this["includeDomains"],
 | 
			
		||||
      excludeDomains: this["excludeDomains"],
 | 
			
		||||
      includeDeviceClasses: this["includeDeviceClasses"],
 | 
			
		||||
      deviceFilter: this["deviceFilter"],
 | 
			
		||||
      entityFilter: this["entityFilter"],
 | 
			
		||||
      excludeAreas: this["excludeAreas"],
 | 
			
		||||
      excludeFloors: this["excludeFloors"]
 | 
			
		||||
    ): FloorComboBoxItem[] => {
 | 
			
		||||
      const floors = Object.values(haFloors);
 | 
			
		||||
      const areas = Object.values(haAreas);
 | 
			
		||||
      const devices = Object.values(haDevices);
 | 
			
		||||
      const entities = Object.values(haEntities);
 | 
			
		||||
 | 
			
		||||
      let deviceEntityLookup: DeviceEntityDisplayLookup = {};
 | 
			
		||||
      let inputDevices: DeviceRegistryEntry[] | undefined;
 | 
			
		||||
      let inputEntities: EntityRegistryDisplayEntry[] | undefined;
 | 
			
		||||
 | 
			
		||||
      if (
 | 
			
		||||
        includeDomains ||
 | 
			
		||||
        excludeDomains ||
 | 
			
		||||
        includeDeviceClasses ||
 | 
			
		||||
        deviceFilter ||
 | 
			
		||||
        entityFilter
 | 
			
		||||
      ) {
 | 
			
		||||
        deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
 | 
			
		||||
        inputDevices = devices;
 | 
			
		||||
        inputEntities = entities.filter((entity) => entity.area_id);
 | 
			
		||||
 | 
			
		||||
        if (includeDomains) {
 | 
			
		||||
          inputDevices = inputDevices!.filter((device) => {
 | 
			
		||||
            const devEntities = deviceEntityLookup[device.id];
 | 
			
		||||
            if (!devEntities || !devEntities.length) {
 | 
			
		||||
              return false;
 | 
			
		||||
            }
 | 
			
		||||
            return deviceEntityLookup[device.id].some((entity) =>
 | 
			
		||||
              includeDomains.includes(computeDomain(entity.entity_id))
 | 
			
		||||
            );
 | 
			
		||||
          });
 | 
			
		||||
          inputEntities = inputEntities!.filter((entity) =>
 | 
			
		||||
            includeDomains.includes(computeDomain(entity.entity_id))
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (excludeDomains) {
 | 
			
		||||
          inputDevices = inputDevices!.filter((device) => {
 | 
			
		||||
            const devEntities = deviceEntityLookup[device.id];
 | 
			
		||||
            if (!devEntities || !devEntities.length) {
 | 
			
		||||
              return true;
 | 
			
		||||
            }
 | 
			
		||||
            return entities.every(
 | 
			
		||||
              (entity) =>
 | 
			
		||||
                !excludeDomains.includes(computeDomain(entity.entity_id))
 | 
			
		||||
            );
 | 
			
		||||
          });
 | 
			
		||||
          inputEntities = inputEntities!.filter(
 | 
			
		||||
            (entity) =>
 | 
			
		||||
              !excludeDomains.includes(computeDomain(entity.entity_id))
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (includeDeviceClasses) {
 | 
			
		||||
          inputDevices = inputDevices!.filter((device) => {
 | 
			
		||||
            const devEntities = deviceEntityLookup[device.id];
 | 
			
		||||
            if (!devEntities || !devEntities.length) {
 | 
			
		||||
              return false;
 | 
			
		||||
            }
 | 
			
		||||
            return deviceEntityLookup[device.id].some((entity) => {
 | 
			
		||||
              const stateObj = this.hass.states[entity.entity_id];
 | 
			
		||||
              if (!stateObj) {
 | 
			
		||||
                return false;
 | 
			
		||||
              }
 | 
			
		||||
              return (
 | 
			
		||||
                stateObj.attributes.device_class &&
 | 
			
		||||
                includeDeviceClasses.includes(stateObj.attributes.device_class)
 | 
			
		||||
              );
 | 
			
		||||
            });
 | 
			
		||||
          });
 | 
			
		||||
          inputEntities = inputEntities!.filter((entity) => {
 | 
			
		||||
            const stateObj = this.hass.states[entity.entity_id];
 | 
			
		||||
            return (
 | 
			
		||||
              stateObj.attributes.device_class &&
 | 
			
		||||
              includeDeviceClasses.includes(stateObj.attributes.device_class)
 | 
			
		||||
            );
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (deviceFilter) {
 | 
			
		||||
          inputDevices = inputDevices!.filter((device) =>
 | 
			
		||||
            deviceFilter!(device)
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (entityFilter) {
 | 
			
		||||
          inputDevices = inputDevices!.filter((device) => {
 | 
			
		||||
            const devEntities = deviceEntityLookup[device.id];
 | 
			
		||||
            if (!devEntities || !devEntities.length) {
 | 
			
		||||
              return false;
 | 
			
		||||
            }
 | 
			
		||||
            return deviceEntityLookup[device.id].some((entity) => {
 | 
			
		||||
              const stateObj = this.hass.states[entity.entity_id];
 | 
			
		||||
              if (!stateObj) {
 | 
			
		||||
                return false;
 | 
			
		||||
              }
 | 
			
		||||
              return entityFilter(stateObj);
 | 
			
		||||
            });
 | 
			
		||||
          });
 | 
			
		||||
          inputEntities = inputEntities!.filter((entity) => {
 | 
			
		||||
            const stateObj = this.hass.states[entity.entity_id];
 | 
			
		||||
            if (!stateObj) {
 | 
			
		||||
              return false;
 | 
			
		||||
            }
 | 
			
		||||
            return entityFilter!(stateObj);
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let outputAreas = areas;
 | 
			
		||||
 | 
			
		||||
      let areaIds: string[] | undefined;
 | 
			
		||||
 | 
			
		||||
      if (inputDevices) {
 | 
			
		||||
        areaIds = inputDevices
 | 
			
		||||
          .filter((device) => device.area_id)
 | 
			
		||||
          .map((device) => device.area_id!);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (inputEntities) {
 | 
			
		||||
        areaIds = (areaIds ?? []).concat(
 | 
			
		||||
          inputEntities
 | 
			
		||||
            .filter((entity) => entity.area_id)
 | 
			
		||||
            .map((entity) => entity.area_id!)
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (areaIds) {
 | 
			
		||||
        outputAreas = outputAreas.filter((area) =>
 | 
			
		||||
          areaIds!.includes(area.area_id)
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (excludeAreas) {
 | 
			
		||||
        outputAreas = outputAreas.filter(
 | 
			
		||||
          (area) => !excludeAreas!.includes(area.area_id)
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (excludeFloors) {
 | 
			
		||||
        outputAreas = outputAreas.filter(
 | 
			
		||||
          (area) => !area.floor_id || !excludeFloors!.includes(area.floor_id)
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const floorAreaLookup = getFloorAreaLookup(outputAreas);
 | 
			
		||||
      const unassisgnedAreas = Object.values(outputAreas).filter(
 | 
			
		||||
        (area) => !area.floor_id || !floorAreaLookup[area.floor_id]
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      const floorAreaEntries: [
 | 
			
		||||
        FloorRegistryEntry | undefined,
 | 
			
		||||
        AreaRegistryEntry[],
 | 
			
		||||
      ][] = Object.entries(floorAreaLookup)
 | 
			
		||||
        .map(([floorId, floorAreas]) => {
 | 
			
		||||
          const floor = floors.find((fl) => fl.floor_id === floorId)!;
 | 
			
		||||
          return [floor, floorAreas] as const;
 | 
			
		||||
        })
 | 
			
		||||
        .sort(([floorA], [floorB]) => {
 | 
			
		||||
          if (floorA.level !== floorB.level) {
 | 
			
		||||
            return (floorA.level ?? 0) - (floorB.level ?? 0);
 | 
			
		||||
          }
 | 
			
		||||
          return stringCompare(floorA.name, floorB.name);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
      const items: FloorComboBoxItem[] = [];
 | 
			
		||||
 | 
			
		||||
      floorAreaEntries.forEach(([floor, floorAreas]) => {
 | 
			
		||||
        if (floor) {
 | 
			
		||||
          const floorName = computeFloorName(floor);
 | 
			
		||||
 | 
			
		||||
          const areaSearchLabels = floorAreas
 | 
			
		||||
            .map((area) => {
 | 
			
		||||
              const areaName = computeAreaName(area) || area.area_id;
 | 
			
		||||
              return [area.area_id, areaName, ...area.aliases];
 | 
			
		||||
            })
 | 
			
		||||
            .flat();
 | 
			
		||||
 | 
			
		||||
          items.push({
 | 
			
		||||
            id: this._formatValue({ id: floor.floor_id, type: "floor" }),
 | 
			
		||||
            type: "floor",
 | 
			
		||||
            primary: floorName,
 | 
			
		||||
            floor: floor,
 | 
			
		||||
            search_labels: [
 | 
			
		||||
              floor.floor_id,
 | 
			
		||||
              floorName,
 | 
			
		||||
              ...floor.aliases,
 | 
			
		||||
              ...areaSearchLabels,
 | 
			
		||||
            ],
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        items.push(
 | 
			
		||||
          ...floorAreas.map((area) => {
 | 
			
		||||
            const areaName = computeAreaName(area) || area.area_id;
 | 
			
		||||
            return {
 | 
			
		||||
              id: this._formatValue({ id: area.area_id, type: "area" }),
 | 
			
		||||
              type: "area" as const,
 | 
			
		||||
              primary: areaName,
 | 
			
		||||
              area: area,
 | 
			
		||||
              icon: area.icon || undefined,
 | 
			
		||||
              search_labels: [area.area_id, areaName, ...area.aliases],
 | 
			
		||||
            };
 | 
			
		||||
          })
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      items.push(
 | 
			
		||||
        ...unassisgnedAreas.map((area) => {
 | 
			
		||||
          const areaName = computeAreaName(area) || area.area_id;
 | 
			
		||||
          return {
 | 
			
		||||
            id: this._formatValue({ id: area.area_id, type: "area" }),
 | 
			
		||||
            type: "area" as const,
 | 
			
		||||
            primary: areaName,
 | 
			
		||||
            icon: area.icon || undefined,
 | 
			
		||||
            search_labels: [area.area_id, areaName, ...area.aliases],
 | 
			
		||||
          };
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      return items;
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = (
 | 
			
		||||
    item,
 | 
			
		||||
    { index },
 | 
			
		||||
@@ -188,16 +445,12 @@ export class HaAreaFloorPicker extends LitElement {
 | 
			
		||||
    `;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
 | 
			
		||||
 | 
			
		||||
  private _getItems = () =>
 | 
			
		||||
    this._getAreasAndFloorsMemoized(
 | 
			
		||||
      this.hass.states,
 | 
			
		||||
    this._getAreasAndFloors(
 | 
			
		||||
      this.hass.floors,
 | 
			
		||||
      this.hass.areas,
 | 
			
		||||
      this.hass.devices,
 | 
			
		||||
      this.hass.entities,
 | 
			
		||||
      this._formatValue,
 | 
			
		||||
      this.includeDomains,
 | 
			
		||||
      this.excludeDomains,
 | 
			
		||||
      this.includeDeviceClasses,
 | 
			
		||||
 
 | 
			
		||||
@@ -107,7 +107,7 @@ export class HaAreaPicker extends LitElement {
 | 
			
		||||
          `;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const { floor } = getAreaContext(area, this.hass.floors);
 | 
			
		||||
        const { floor } = getAreaContext(area, this.hass);
 | 
			
		||||
 | 
			
		||||
        const areaName = area ? computeAreaName(area) : undefined;
 | 
			
		||||
        const floorName = floor ? computeFloorName(floor) : undefined;
 | 
			
		||||
@@ -279,7 +279,7 @@ export class HaAreaPicker extends LitElement {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const items = outputAreas.map<PickerComboBoxItem>((area) => {
 | 
			
		||||
        const { floor } = getAreaContext(area, this.hass.floors);
 | 
			
		||||
        const { floor } = getAreaContext(area, this.hass);
 | 
			
		||||
        const floorName = floor ? computeFloorName(floor) : undefined;
 | 
			
		||||
        const areaName = computeAreaName(area);
 | 
			
		||||
        return {
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,7 @@ export class HaAreasDisplayEditor extends LitElement {
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const items: DisplayItem[] = areas.map((area) => {
 | 
			
		||||
      const { floor } = getAreaContext(area, this.hass.floors);
 | 
			
		||||
      const { floor } = getAreaContext(area, this.hass!);
 | 
			
		||||
      return {
 | 
			
		||||
        value: area.area_id,
 | 
			
		||||
        label: area.name,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { mdiDragHorizontalVariant, mdiTextureBox } from "@mdi/js";
 | 
			
		||||
import { mdiDrag, mdiTextureBox } from "@mdi/js";
 | 
			
		||||
import type { TemplateResult } from "lit";
 | 
			
		||||
import { LitElement, css, html, nothing } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators";
 | 
			
		||||
@@ -105,7 +105,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
 | 
			
		||||
                      <ha-svg-icon
 | 
			
		||||
                        class="handle"
 | 
			
		||||
                        slot="icons"
 | 
			
		||||
                        .path=${mdiDragHorizontalVariant}
 | 
			
		||||
                        .path=${mdiDrag}
 | 
			
		||||
                      ></ha-svg-icon>
 | 
			
		||||
                    `}
 | 
			
		||||
                <ha-items-display-editor
 | 
			
		||||
@@ -138,7 +138,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
 | 
			
		||||
      );
 | 
			
		||||
      const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
 | 
			
		||||
        (acc, area) => {
 | 
			
		||||
          const { floor } = getAreaContext(area, this.hass.floors);
 | 
			
		||||
          const { floor } = getAreaContext(area, this.hass!);
 | 
			
		||||
          const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR;
 | 
			
		||||
 | 
			
		||||
          if (!acc[floorId]) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,24 @@
 | 
			
		||||
import { mdiAlertCircle, mdiMicrophone, mdiSend } from "@mdi/js";
 | 
			
		||||
import type { PropertyValues, TemplateResult } from "lit";
 | 
			
		||||
import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { css, LitElement, html, nothing } from "lit";
 | 
			
		||||
import { mdiAlertCircle, mdiMicrophone, mdiSend } from "@mdi/js";
 | 
			
		||||
import { customElement, property, query, state } from "lit/decorators";
 | 
			
		||||
import { classMap } from "lit/directives/class-map";
 | 
			
		||||
import { supportsFeature } from "../common/entity/supports-feature";
 | 
			
		||||
import type { HomeAssistant } from "../types";
 | 
			
		||||
import {
 | 
			
		||||
  type PipelineRunEvent,
 | 
			
		||||
  runAssistPipeline,
 | 
			
		||||
  type AssistPipeline,
 | 
			
		||||
  type ConversationChatLogAssistantDelta,
 | 
			
		||||
  type ConversationChatLogToolResultDelta,
 | 
			
		||||
  type PipelineRunEvent,
 | 
			
		||||
} from "../data/assist_pipeline";
 | 
			
		||||
import { supportsFeature } from "../common/entity/supports-feature";
 | 
			
		||||
import { ConversationEntityFeature } from "../data/conversation";
 | 
			
		||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
 | 
			
		||||
import type { HomeAssistant } from "../types";
 | 
			
		||||
import { AudioRecorder } from "../util/audio-recorder";
 | 
			
		||||
import { documentationUrl } from "../util/documentation-url";
 | 
			
		||||
import "./ha-alert";
 | 
			
		||||
import "./ha-textfield";
 | 
			
		||||
import type { HaTextField } from "./ha-textfield";
 | 
			
		||||
import { documentationUrl } from "../util/documentation-url";
 | 
			
		||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
 | 
			
		||||
 | 
			
		||||
interface AssistMessage {
 | 
			
		||||
  who: string;
 | 
			
		||||
@@ -591,7 +591,7 @@ export class HaAssistChat extends LitElement {
 | 
			
		||||
      clear: both;
 | 
			
		||||
      margin: 8px 0;
 | 
			
		||||
      padding: 8px;
 | 
			
		||||
      border-radius: var(--ha-border-radius-xl);
 | 
			
		||||
      border-radius: 15px;
 | 
			
		||||
    }
 | 
			
		||||
    .message:last-child {
 | 
			
		||||
      margin-bottom: 0;
 | 
			
		||||
@@ -659,7 +659,7 @@ export class HaAssistChat extends LitElement {
 | 
			
		||||
    .double-bounce2 {
 | 
			
		||||
      width: 48px;
 | 
			
		||||
      height: 48px;
 | 
			
		||||
      border-radius: var(--ha-border-radius-circle);
 | 
			
		||||
      border-radius: 50%;
 | 
			
		||||
      background-color: var(--primary-color);
 | 
			
		||||
      opacity: 0.2;
 | 
			
		||||
      position: absolute;
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ export class HaBadge extends LitElement {
 | 
			
		||||
      flex-direction: row;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      gap: var(--ha-space-2);
 | 
			
		||||
      gap: 8px;
 | 
			
		||||
      height: var(--ha-badge-size, 36px);
 | 
			
		||||
      min-width: var(--ha-badge-size, 36px);
 | 
			
		||||
      padding: 0px 12px;
 | 
			
		||||
@@ -122,7 +122,7 @@ export class HaBadge extends LitElement {
 | 
			
		||||
    ::slotted(img[slot="icon"]) {
 | 
			
		||||
      width: 30px;
 | 
			
		||||
      height: 30px;
 | 
			
		||||
      border-radius: var(--ha-border-radius-circle);
 | 
			
		||||
      border-radius: 50%;
 | 
			
		||||
      object-fit: cover;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      margin-left: -10px;
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,7 @@ export class HaBar extends LitElement {
 | 
			
		||||
      fill: var(--ha-bar-primary-color, var(--primary-color));
 | 
			
		||||
    }
 | 
			
		||||
    svg {
 | 
			
		||||
      border-radius: var(--ha-bar-border-radius, var(--ha-border-radius-sm));
 | 
			
		||||
      border-radius: var(--ha-bar-border-radius, 4px);
 | 
			
		||||
      height: 12px;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -337,9 +337,7 @@ export class HaBaseTimeInput extends LitElement {
 | 
			
		||||
    .time-input-wrap {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      flex: var(--time-input-flex, unset);
 | 
			
		||||
      border-radius: var(--mdc-shape-small, var(--ha-border-radius-sm))
 | 
			
		||||
        var(--mdc-shape-small, var(--ha-border-radius-sm))
 | 
			
		||||
        var(--ha-border-radius-square) var(--ha-border-radius-square);
 | 
			
		||||
      border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0;
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      position: relative;
 | 
			
		||||
      direction: ltr;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import "@home-assistant/webawesome/dist/components/drawer/drawer";
 | 
			
		||||
import { css, html, LitElement, type PropertyValues } from "lit";
 | 
			
		||||
import "@home-assistant/webawesome/dist/components/drawer/drawer";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
import { haStyleScrollbar } from "../resources/styles";
 | 
			
		||||
 | 
			
		||||
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
 | 
			
		||||
 | 
			
		||||
@@ -9,9 +8,6 @@ export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
 | 
			
		||||
export class HaBottomSheet extends LitElement {
 | 
			
		||||
  @property({ type: Boolean }) public open = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, reflect: true, attribute: "flexcontent" })
 | 
			
		||||
  public flexContent = false;
 | 
			
		||||
 | 
			
		||||
  @state() private _drawerOpen = false;
 | 
			
		||||
 | 
			
		||||
  private _handleAfterHide() {
 | 
			
		||||
@@ -38,61 +34,37 @@ export class HaBottomSheet extends LitElement {
 | 
			
		||||
        @wa-after-hide=${this._handleAfterHide}
 | 
			
		||||
        without-header
 | 
			
		||||
      >
 | 
			
		||||
        <slot name="header"></slot>
 | 
			
		||||
        <div class="body ha-scrollbar">
 | 
			
		||||
          <slot></slot>
 | 
			
		||||
        </div>
 | 
			
		||||
        <slot></slot>
 | 
			
		||||
      </wa-drawer>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static styles = [
 | 
			
		||||
    haStyleScrollbar,
 | 
			
		||||
    css`
 | 
			
		||||
      wa-drawer {
 | 
			
		||||
        --wa-color-surface-raised: transparent;
 | 
			
		||||
        --spacing: 0;
 | 
			
		||||
        --size: var(--ha-bottom-sheet-height, auto);
 | 
			
		||||
        --show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
 | 
			
		||||
        --hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
 | 
			
		||||
      }
 | 
			
		||||
      wa-drawer::part(dialog) {
 | 
			
		||||
        max-height: var(--ha-bottom-sheet-max-height, 90vh);
 | 
			
		||||
        align-items: center;
 | 
			
		||||
      }
 | 
			
		||||
      wa-drawer::part(body) {
 | 
			
		||||
        max-width: var(--ha-bottom-sheet-max-width);
 | 
			
		||||
        width: 100%;
 | 
			
		||||
        border-top-left-radius: var(
 | 
			
		||||
          --ha-bottom-sheet-border-radius,
 | 
			
		||||
          var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
 | 
			
		||||
        );
 | 
			
		||||
        border-top-right-radius: var(
 | 
			
		||||
          --ha-bottom-sheet-border-radius,
 | 
			
		||||
          var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
 | 
			
		||||
        );
 | 
			
		||||
        background-color: var(
 | 
			
		||||
          --ha-bottom-sheet-surface-background,
 | 
			
		||||
          var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
 | 
			
		||||
        );
 | 
			
		||||
        padding: var(
 | 
			
		||||
          --ha-bottom-sheet-padding,
 | 
			
		||||
          0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
 | 
			
		||||
            var(--safe-area-inset-left)
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      :host([flexcontent]) wa-drawer::part(body) {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
      }
 | 
			
		||||
      :host([flexcontent]) .body {
 | 
			
		||||
        flex: 1;
 | 
			
		||||
        max-width: 100%;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
      }
 | 
			
		||||
    `,
 | 
			
		||||
  ];
 | 
			
		||||
  static styles = css`
 | 
			
		||||
    wa-drawer {
 | 
			
		||||
      --wa-color-surface-raised: var(
 | 
			
		||||
        --ha-bottom-sheet-surface-background,
 | 
			
		||||
        var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
 | 
			
		||||
      );
 | 
			
		||||
      --spacing: 0;
 | 
			
		||||
      --size: auto;
 | 
			
		||||
      --show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
 | 
			
		||||
      --hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
 | 
			
		||||
    }
 | 
			
		||||
    wa-drawer::part(dialog) {
 | 
			
		||||
      border-top-left-radius: var(
 | 
			
		||||
        --ha-bottom-sheet-border-radius,
 | 
			
		||||
        var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
 | 
			
		||||
      );
 | 
			
		||||
      border-top-right-radius: var(
 | 
			
		||||
        --ha-bottom-sheet-border-radius,
 | 
			
		||||
        var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
 | 
			
		||||
      );
 | 
			
		||||
      max-height: 90vh;
 | 
			
		||||
      padding-bottom: var(--safe-area-inset-bottom);
 | 
			
		||||
      padding-left: var(--safe-area-inset-left);
 | 
			
		||||
      padding-right: var(--safe-area-inset-right);
 | 
			
		||||
    }
 | 
			
		||||
  `;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user