mirror of
https://github.com/home-assistant/frontend.git
synced 2026-11-04 03:54:21 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe9f7eabe0 | ||
|
|
adaf7b47df | ||
|
|
2a356de25d |
No files matched your search
@@ -12,7 +12,3 @@
|
||||
|
||||
demo/public/api/camera_proxy_stream/* binary
|
||||
demo/public/api/media_player_proxy/* binary
|
||||
|
||||
# Generated by `gh actions-lock`. Highlight it as YAML and don't auto-merge it:
|
||||
# on a conflict, restore it from the base branch and run `gh actions-lock`.
|
||||
.github/workflows/actions.lock linguist-language=YAML -merge
|
||||
@@ -1,6 +0,0 @@
|
||||
paths:
|
||||
.github/workflows/**/*.{yml,yaml}:
|
||||
ignore:
|
||||
# actionlint doesn't support `$/` self-repository actions yet, remove once it does:
|
||||
# https://github.com/rhysd/actionlint/issues/711
|
||||
- 'specifying action "\$/[^"]+" in invalid format because ref is missing'
|
||||
@@ -24,7 +24,7 @@ runs:
|
||||
|
||||
- name: Setup Node and install
|
||||
if: steps.dependencies.outputs.cache-hit != 'true'
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
cache: false
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Verify actions lockfile
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
paths:
|
||||
- ".github/actions/**"
|
||||
- ".github/workflows/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Verify actions lockfile
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_ACTIONS_LOCK_VERSION: v0.4.1
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install gh-actions-lock
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: gh extension install github/gh-actions-lock --pin "${GH_ACTIONS_LOCK_VERSION}"
|
||||
- name: Verify lockfile
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: gh actions-lock --verify
|
||||
@@ -1,190 +0,0 @@
|
||||
# This file is machine-generated by `gh actions-lock`.
|
||||
# Do not edit by hand; run `gh actions-lock` to update.
|
||||
# Docs: https://gh.io/actions-lockfile
|
||||
version: 'v0.0.3'
|
||||
workflows:
|
||||
'.github/workflows/actionlint.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
'.github/workflows/actions-lock.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
'.github/workflows/cast_deployment.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
'.github/workflows/ci.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
- 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9'
|
||||
'.github/workflows/codeql-analysis.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'github/codeql-action@9b3254085494b55a0f7c4c2b3d2d52cc712ce370'
|
||||
'.github/workflows/copilot-setup-steps.yml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
'.github/workflows/demo_deployment.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
'.github/workflows/dependency-review.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294'
|
||||
- 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'
|
||||
'.github/workflows/design_deployment.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
'.github/workflows/design_preview.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
'.github/workflows/e2e.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333'
|
||||
- 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
- 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9'
|
||||
'.github/workflows/labeler.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'
|
||||
- 'actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13'
|
||||
'.github/workflows/lock.yaml':
|
||||
- 'dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c'
|
||||
'.github/workflows/nightly.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
- 'actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0'
|
||||
- 'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9'
|
||||
'.github/workflows/pull-request-standards.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'
|
||||
'.github/workflows/relative-ci.yaml':
|
||||
- 'relative-ci/agent-action@711f3a8fa1b928e6afde280011110c9b0a224a60'
|
||||
'.github/workflows/release-drafter.yaml':
|
||||
- 'release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0'
|
||||
'.github/workflows/release.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
- 'actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0'
|
||||
- 'home-assistant/actions@86d4564712fa02543c808e3a693dd04f9d3f8361'
|
||||
- 'home-assistant/wheels@9e17ab1ed5c4c79d8b61e29fa63de25ca2710716'
|
||||
- 'pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086'
|
||||
'.github/workflows/restrict-task-creation.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3'
|
||||
'.github/workflows/stale.yaml':
|
||||
- 'actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93'
|
||||
'.github/workflows/sync-device-classes.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
- 'peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1'
|
||||
'.github/workflows/sync-sensor-entity-constants.yaml':
|
||||
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
- 'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
- 'peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1'
|
||||
'.github/workflows/translations.yaml':
|
||||
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
dependencies:
|
||||
'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9':
|
||||
ref: 'v6.1.0'
|
||||
commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
|
||||
owner_id: 44036562
|
||||
repo_id: 215566462
|
||||
'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1':
|
||||
ref: 'v7.0.1'
|
||||
commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
owner_id: 44036562
|
||||
repo_id: 197814629
|
||||
'actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294':
|
||||
ref: 'v5.0.0'
|
||||
commit: 'sha1-a1d282b36b6f3519aa1f3fc636f609c47dddb294'
|
||||
owner_id: 44036562
|
||||
repo_id: 476372928
|
||||
'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333':
|
||||
ref: 'v8.0.2'
|
||||
commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333'
|
||||
owner_id: 44036562
|
||||
repo_id: 192626254
|
||||
'actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3':
|
||||
ref: 'v9.0.0'
|
||||
commit: 'sha1-3a2844b7e9c422d3c10d287c895573f7108da1b3'
|
||||
owner_id: 44036562
|
||||
repo_id: 205262760
|
||||
'actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13':
|
||||
ref: 'v7.0.0'
|
||||
commit: 'sha1-bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13'
|
||||
owner_id: 44036562
|
||||
repo_id: 177139928
|
||||
'actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1':
|
||||
ref: 'v7.1.0'
|
||||
commit: 'sha1-949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
owner_id: 44036562
|
||||
repo_id: 189476904
|
||||
'actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0':
|
||||
ref: 'v7.1.0'
|
||||
commit: 'sha1-6d4fd22b20465ccc0f7354476588e8df7c9ca0e0'
|
||||
owner_id: 44036562
|
||||
repo_id: 192625525
|
||||
'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405':
|
||||
ref: 'v6.2.0'
|
||||
commit: 'sha1-a309ff8b426b58ec0e2a45f0f869d46889d02405'
|
||||
owner_id: 44036562
|
||||
repo_id: 192625525
|
||||
'actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93':
|
||||
ref: 'v11.0.0'
|
||||
commit: 'sha1-4391f3da665fdf50b6810c1a66712fb9ba21aa93'
|
||||
owner_id: 44036562
|
||||
repo_id: 200437066
|
||||
'actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9':
|
||||
ref: 'v7.0.2'
|
||||
commit: 'sha1-cf430e030ddbb5b0abf93d22962f4752f3646cd9'
|
||||
owner_id: 44036562
|
||||
repo_id: 192625955
|
||||
'dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c':
|
||||
ref: 'v6.0.2'
|
||||
commit: 'sha1-89ae32b08ed1a541efecbab17912962a5e38981c'
|
||||
owner_id: 7513068
|
||||
repo_id: 113918130
|
||||
'github/codeql-action@9b3254085494b55a0f7c4c2b3d2d52cc712ce370':
|
||||
ref: 'v4.39.1'
|
||||
commit: 'sha1-9b3254085494b55a0f7c4c2b3d2d52cc712ce370'
|
||||
owner_id: 9919
|
||||
repo_id: 259445878
|
||||
'home-assistant/actions@86d4564712fa02543c808e3a693dd04f9d3f8361':
|
||||
ref: 'master'
|
||||
commit: 'sha1-86d4564712fa02543c808e3a693dd04f9d3f8361'
|
||||
owner_id: 13844975
|
||||
repo_id: 173703505
|
||||
'home-assistant/wheels@9e17ab1ed5c4c79d8b61e29fa63de25ca2710716':
|
||||
ref: '2026.07.0'
|
||||
commit: 'sha1-9e17ab1ed5c4c79d8b61e29fa63de25ca2710716'
|
||||
owner_id: 13844975
|
||||
repo_id: 184234812
|
||||
'peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1':
|
||||
ref: 'v8.1.1'
|
||||
commit: 'sha1-5f6978faf089d4d20b00c7766989d076bb2fc7f1'
|
||||
owner_id: 18365890
|
||||
repo_id: 197176365
|
||||
'pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086':
|
||||
ref: 'v1.14.3'
|
||||
commit: 'sha1-7245906fb61fa33a13554f9d5fb856fe61758086'
|
||||
owner_id: 647025
|
||||
repo_id: 178055147
|
||||
uses:
|
||||
- 'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405'
|
||||
'relative-ci/agent-action@711f3a8fa1b928e6afde280011110c9b0a224a60':
|
||||
ref: 'v3.2.4'
|
||||
commit: 'sha1-711f3a8fa1b928e6afde280011110c9b0a224a60'
|
||||
owner_id: 43522883
|
||||
repo_id: 398376354
|
||||
'release-drafter/release-drafter@978bb0b30c5ee8cdd332ced253758157150d43f0':
|
||||
ref: 'v7.10.0'
|
||||
commit: 'sha1-978bb0b30c5ee8cdd332ced253758157150d43f0'
|
||||
owner_id: 58292376
|
||||
repo_id: 139022359
|
||||
@@ -30,17 +30,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build Cast
|
||||
uses: $/.github/actions/build
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
target: build-cast
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
uses: $/.github/actions/netlify-deploy
|
||||
uses: ./.github/actions/netlify-deploy
|
||||
with:
|
||||
dir: cast/dist
|
||||
alias: dev
|
||||
@@ -62,17 +62,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build Cast
|
||||
uses: $/.github/actions/build
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
target: build-cast
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
uses: $/.github/actions/netlify-deploy
|
||||
uses: ./.github/actions/netlify-deploy
|
||||
with:
|
||||
dir: cast/dist
|
||||
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Prepare dependencies
|
||||
uses: $/.github/actions/prepare-dependencies
|
||||
uses: ./.github/actions/prepare-dependencies
|
||||
|
||||
lint:
|
||||
name: Lint and check format
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
- name: Check for duplicate dependencies
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
- name: Build resources
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
# Read-only reuse of the rspack cache written by the nightly (see
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
restore-keys: |
|
||||
rspack-cache-${{ runner.os }}-
|
||||
- name: Build Application
|
||||
uses: $/.github/actions/build
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
target: build-app
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -22,45 +22,10 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# These steps mirror .github/actions/setup with node-modules-cache enabled.
|
||||
# Copilot runs this workflow in a context that can't resolve `$/`
|
||||
# self-repository actions, so the steps are inlined here.
|
||||
- name: Restore complete dependency tree
|
||||
id: dependency-cache
|
||||
continue-on-error: true
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
- name: Setup Node and install
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
~/.local/share/pnpm/store
|
||||
key: >-
|
||||
node-modules-v1-${{ runner.os }}-${{ runner.arch }}-${{
|
||||
hashFiles('.nvmrc', 'package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'patches/**') }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-store
|
||||
if: steps.dependency-cache.outputs.cache-hit != 'true'
|
||||
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache pnpm store
|
||||
if: steps.pnpm-store.outputs.path
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.path }}
|
||||
key: pnpm-store-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }}
|
||||
restore-keys: pnpm-store-${{ runner.os }}-${{ runner.arch }}-
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.dependency-cache.outputs.cache-hit != 'true'
|
||||
run: pnpm install --frozen-lockfile
|
||||
node-modules-cache: true
|
||||
|
||||
- name: Skip fetching nightly translations
|
||||
run: echo "SKIP_FETCH_NIGHTLY_TRANSLATIONS=1" >> "$GITHUB_ENV"
|
||||
|
||||
@@ -31,17 +31,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build Demo
|
||||
uses: $/.github/actions/build
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
target: build-demo
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
uses: $/.github/actions/netlify-deploy
|
||||
uses: ./.github/actions/netlify-deploy
|
||||
with:
|
||||
dir: demo/dist
|
||||
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
@@ -62,17 +62,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build Demo
|
||||
uses: $/.github/actions/build
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
target: build-demo
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
uses: $/.github/actions/netlify-deploy
|
||||
uses: ./.github/actions/netlify-deploy
|
||||
with:
|
||||
dir: demo/dist
|
||||
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
|
||||
@@ -25,17 +25,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build Gallery
|
||||
uses: $/.github/actions/build
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
target: build-gallery
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
uses: $/.github/actions/netlify-deploy
|
||||
uses: ./.github/actions/netlify-deploy
|
||||
with:
|
||||
dir: gallery/dist
|
||||
auth-token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
|
||||
@@ -29,17 +29,17 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build Gallery
|
||||
uses: $/.github/actions/build
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
target: build-gallery
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy preview to Netlify
|
||||
id: deploy
|
||||
uses: $/.github/actions/netlify-deploy
|
||||
uses: ./.github/actions/netlify-deploy
|
||||
with:
|
||||
dir: gallery/dist
|
||||
alias: deploy-preview-${{ github.event.number }}
|
||||
|
||||
+16
-16
@@ -32,13 +32,13 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare dependencies
|
||||
uses: $/.github/actions/prepare-dependencies
|
||||
uses: ./.github/actions/prepare-dependencies
|
||||
|
||||
prepare-container-dependencies:
|
||||
name: Prepare container dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble
|
||||
options: --user 1001
|
||||
defaults:
|
||||
run:
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Prepare dependencies
|
||||
uses: $/.github/actions/prepare-dependencies
|
||||
uses: ./.github/actions/prepare-dependencies
|
||||
with:
|
||||
node-modules-cache-key: node-modules-container-v1
|
||||
|
||||
@@ -66,12 +66,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
- name: Build demo
|
||||
uses: $/.github/actions/build
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
target: build-demo-e2e
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -97,12 +97,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
- name: Build e2e test app
|
||||
uses: $/.github/actions/build
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
target: build-e2e-test-app-e2e
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -128,12 +128,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
- name: Build gallery
|
||||
uses: $/.github/actions/build
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
target: build-gallery
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
- prepare-container-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble
|
||||
options: --user 1001 --ipc=host
|
||||
defaults:
|
||||
run:
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
node-modules-cache-key: node-modules-container-v1
|
||||
@@ -207,7 +207,7 @@ jobs:
|
||||
- prepare-container-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble
|
||||
options: --user 1001 --ipc=host
|
||||
defaults:
|
||||
run:
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
node-modules-cache-key: node-modules-container-v1
|
||||
@@ -261,7 +261,7 @@ jobs:
|
||||
- prepare-container-dependencies
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble@sha256:47b510dd9230bf0b9bfc9b20102241e577ea7a99334ab4c3ca19f19770e9b8ea
|
||||
image: mcr.microsoft.com/playwright:v1.64.1-noble
|
||||
options: --user 1001 --ipc=host
|
||||
defaults:
|
||||
run:
|
||||
@@ -284,7 +284,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
node-modules-cache-key: node-modules-container-v1
|
||||
@@ -326,7 +326,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node with shared dependencies
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
node-modules-cache: true
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
immutable: false
|
||||
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
immutable: false
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
uses: home-assistant/actions/helpers/verify-version@86d4564712fa02543c808e3a693dd04f9d3f8361 # master
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
immutable: false
|
||||
cache: false
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node and install
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
immutable: false
|
||||
cache: false
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Regenerate device class constants
|
||||
run: ./script/gen_device_classes
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node and install
|
||||
uses: $/.github/actions/setup
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Regenerate sensor entity constants
|
||||
run: ./script/gen_sensor_entity_constants
|
||||
|
||||
@@ -284,7 +284,6 @@ module.exports.config = {
|
||||
name: "demo" + nameSuffix(latestBuild),
|
||||
entry: {
|
||||
main: path.resolve(paths.demo_dir, "src/entrypoint.ts"),
|
||||
embed: path.resolve(paths.demo_dir, "src/embed/entrypoint.ts"),
|
||||
...module.exports.mapWorkerEntry(),
|
||||
},
|
||||
outputPath: outputPath(paths.demo_output_root, latestBuild),
|
||||
@@ -292,8 +291,6 @@ module.exports.config = {
|
||||
defineOverlay: {
|
||||
__VERSION__: JSON.stringify(`DEMO-${env.version()}`),
|
||||
__DEMO__: true,
|
||||
// The embed sets the global to load static files from its own server.
|
||||
__STATIC_PATH__: '(globalThis.__HA_STATIC_PATH__ || "/static/")',
|
||||
},
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
|
||||
@@ -232,11 +232,7 @@ gulp.task(
|
||||
)
|
||||
);
|
||||
|
||||
const DEMO_PAGE_ENTRIES = {
|
||||
"index.html": ["main"],
|
||||
"components.html": ["embed"],
|
||||
"embed.js": ["embed"],
|
||||
};
|
||||
const DEMO_PAGE_ENTRIES = { "index.html": ["main"] };
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-demo-dev",
|
||||
|
||||
@@ -214,18 +214,16 @@ const createRspackConfig = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
// MapLibre's worker loads ESM plugins through `import(url)`, which
|
||||
// is never taken here: the RTL plugin is UMD. Rspack's stub for that
|
||||
// fully dynamic request is exactly what should remain - rejecting,
|
||||
// and free of syntax the legacy floor cannot parse - so the warning
|
||||
// about it is noise. The page side builds a blob import from
|
||||
// `new URL(url, import.meta.url)` for a worker on another origin,
|
||||
// as in the demo embed. That is a URL, not an asset to bundle, and
|
||||
// the worker URL is absolute, so leave it as it is.
|
||||
// MapLibre's worker loads ESM plugins through `import(url)`, and the
|
||||
// page side builds a blob import from `new URL(url, import.meta.url)`
|
||||
// for a cross-origin worker. Neither path is taken here: the RTL
|
||||
// plugin is UMD and the worker is served from the page's origin.
|
||||
// Rspack's stubs for these fully dynamic requests are exactly what
|
||||
// should remain - rejecting, and free of syntax the legacy floor
|
||||
// cannot parse - so the warnings about them are noise.
|
||||
test: /[\\/]maplibre-gl[\\/]dist[\\/]maplibre-gl(?:-worker)?\.mjs$/,
|
||||
parser: {
|
||||
exprContextCritical: false,
|
||||
url: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -248,15 +246,7 @@ const createRspackConfig = ({
|
||||
},
|
||||
],
|
||||
},
|
||||
// The demo embed reads its own URL at runtime to find its files.
|
||||
// Keep import.meta.url instead of replacing it with the source path.
|
||||
// Development builds wrap modules in eval(), which rejects import.meta.
|
||||
latestBuild &&
|
||||
isProdBuild && {
|
||||
test: path.resolve(paths.demo_dir, "src/embed/public-path.ts"),
|
||||
parser: { importMeta: false },
|
||||
},
|
||||
].filter(Boolean),
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimizer: [
|
||||
|
||||
@@ -16,10 +16,3 @@
|
||||
|
||||
/frontend_latest/*
|
||||
Cache-Control: public, max-age: 604800, s-maxage=604800
|
||||
Access-Control-Allow-Origin: *
|
||||
|
||||
/embed.js
|
||||
Access-Control-Allow-Origin: *
|
||||
|
||||
/static/*
|
||||
Access-Control-Allow-Origin: *
|
||||
@@ -1,4 +0,0 @@
|
||||
import { createContext } from "@lit/context";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
|
||||
export const embedHassContext = createContext<HomeAssistant>("embedHass");
|
||||
@@ -1,25 +0,0 @@
|
||||
import "./public-path";
|
||||
import "./workers";
|
||||
// The provider must be defined first. Custom elements upgrade in the order
|
||||
// they are defined, and cards need a provider to request their context from.
|
||||
import "./ha-embed-provider";
|
||||
import "./ha-embed-card";
|
||||
// For custom cards, which can't load them themselves. The built-in cards only
|
||||
// load ha-list-item-button with the map card. The full app loads ha-select
|
||||
// with the more info dialog.
|
||||
import "../../../src/components/item/ha-list-item-button";
|
||||
import "../../../src/components/ha-select";
|
||||
import { fontStyles } from "../../../src/resources/roboto";
|
||||
|
||||
// Font faces only work in the document, not in the shadow root of the card.
|
||||
// Defining Roboto does not change the page unless it already uses Roboto.
|
||||
document.head.append(
|
||||
Object.assign(document.createElement("style"), {
|
||||
textContent: fontStyles.cssText,
|
||||
})
|
||||
);
|
||||
|
||||
// Custom cards use the helpers to create built-in cards. The dashboard panel
|
||||
// sets the same global in the full app.
|
||||
(window as any).loadCardHelpers = () =>
|
||||
import("../../../src/panels/lovelace/custom-card-helpers");
|
||||
@@ -1,218 +0,0 @@
|
||||
import { ContextEvent } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import { consume } from "../../../src/common/decorators/consume";
|
||||
import { computeRTLDirection } from "../../../src/common/util/compute_rtl";
|
||||
import type { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
|
||||
import { handleAction } from "../../../src/panels/lovelace/common/handle-action";
|
||||
import { computeCardGridSize } from "../../../src/panels/lovelace/common/compute-card-grid-size";
|
||||
import "../../../src/panels/lovelace/cards/hui-card";
|
||||
import type { HuiCard } from "../../../src/panels/lovelace/cards/hui-card";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import { embedHassContext } from "./context";
|
||||
import { readScriptData } from "./script-data";
|
||||
import { embedThemeStyles } from "./theme-styles";
|
||||
|
||||
const pageProvider = (): HTMLElement =>
|
||||
document.querySelector("ha-embed-provider") ??
|
||||
document.body.appendChild(document.createElement("ha-embed-provider"));
|
||||
|
||||
// Looks past shadow roots, so a card inside another component still finds
|
||||
// the provider around that component.
|
||||
const insideProvider = (element: Element): boolean => {
|
||||
let current: Element | null = element;
|
||||
|
||||
while (current) {
|
||||
if (current.closest("ha-embed-provider")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const root = current.getRootNode();
|
||||
current = root instanceof ShadowRoot ? root.host : null;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a Lovelace card. The card uses the provider with the ID in
|
||||
* `provider`, else the closest provider around it, else the first provider on
|
||||
* the page. If the page has no provider, the card adds an empty one.
|
||||
*
|
||||
* The card takes the columns and rows it would take in a section, so cards
|
||||
* in an element with `display: grid` and 12 columns lay out like a section.
|
||||
* It also sets `--column-size` and `--row-size` on itself.
|
||||
*/
|
||||
@customElement("ha-embed-card")
|
||||
export class HaEmbedCard extends LitElement {
|
||||
/**
|
||||
* The card configuration. Also read from a JSON or YAML script child, see
|
||||
* `readScriptData`.
|
||||
*/
|
||||
@property({ type: Object }) public config?: LovelaceCardConfig;
|
||||
|
||||
/** The ID of the `ha-embed-provider` to use. */
|
||||
@property() public provider?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: embedHassContext, subscribe: true })
|
||||
private _hass?: HomeAssistant;
|
||||
|
||||
// A provider that is not an ancestor, which gets the context requests of the
|
||||
// card and its content. Context requests only reach ancestors by themselves.
|
||||
private _linkedProvider?: HTMLElement;
|
||||
|
||||
private _scriptRead = false;
|
||||
|
||||
private _card = createRef<HuiCard>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener("context-request", (ev) => {
|
||||
if (!this._linkedProvider) {
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
this._linkedProvider.dispatchEvent(
|
||||
new ContextEvent(
|
||||
ev.context,
|
||||
ev.contextTarget,
|
||||
ev.callback,
|
||||
ev.subscribe
|
||||
)
|
||||
);
|
||||
});
|
||||
// Cards fire their tap, hold and double tap actions for the app to handle
|
||||
this.addEventListener("hass-action", (ev) => {
|
||||
if (this._hass) {
|
||||
// The element that fired the action, so that fire-dom-event reaches
|
||||
// listeners inside custom cards, as in action-mixin
|
||||
const node = (ev.composedPath()[0] as HTMLElement | undefined) ?? this;
|
||||
handleAction(node, this._hass, ev.detail.config, ev.detail.action);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
// Before super.connectedCallback(), which sends the context requests.
|
||||
this._linkedProvider = this.provider
|
||||
? (document.getElementById(this.provider) ?? undefined)
|
||||
: insideProvider(this)
|
||||
? undefined
|
||||
: pageProvider();
|
||||
super.connectedCallback();
|
||||
|
||||
// Only once, so that moving the card keeps a config set since
|
||||
if (this._scriptRead) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._scriptRead = true;
|
||||
|
||||
const config = readScriptData(this) as LovelaceCardConfig | undefined;
|
||||
if (config) {
|
||||
this.config = config;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._hass || !this.config) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<div
|
||||
class=${classMap({ theme: true, dark: this._hass.themes.darkMode })}
|
||||
dir=${computeRTLDirection(this._hass)}
|
||||
>
|
||||
<hui-card
|
||||
${ref(this._card)}
|
||||
.hass=${this._hass}
|
||||
.config=${this.config}
|
||||
.layout=${"grid"}
|
||||
@card-updated=${this._updateSize}
|
||||
@card-visibility-changed=${this._updateSize}
|
||||
></hui-card>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected updated() {
|
||||
// The card element only exists once hui-card has updated
|
||||
this._card.value?.updateComplete.then(() => this._updateSize());
|
||||
}
|
||||
|
||||
// The size the card gets in a section, set on the host so that the page can
|
||||
// lay out cards in a grid of 12 columns, like a section
|
||||
private _updateSize = () => {
|
||||
const card = this._card.value;
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
const { rows, columns } = computeCardGridSize(card.getGridOptions());
|
||||
this.hidden = card.hidden;
|
||||
this.toggleAttribute("full-width", columns === "full");
|
||||
this.toggleAttribute("fit-rows", typeof rows === "number");
|
||||
this.style.setProperty(
|
||||
"--column-size",
|
||||
typeof columns === "number" ? String(columns) : null
|
||||
);
|
||||
this.style.setProperty(
|
||||
"--row-size",
|
||||
typeof rows === "number" ? String(rows) : null
|
||||
);
|
||||
};
|
||||
|
||||
static styles = [
|
||||
embedThemeStyles,
|
||||
css`
|
||||
/* The sizes of a card in a section, see hui-grid-section */
|
||||
:host {
|
||||
--row-gap: var(--ha-section-grid-row-gap, 8px);
|
||||
--row-height: var(--ha-section-grid-row-height, 56px);
|
||||
display: block;
|
||||
grid-row: span var(--row-size, 1);
|
||||
grid-column: span min(var(--column-size, 12), 12);
|
||||
}
|
||||
:host([full-width]) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
:host([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
:host([fit-rows]) .theme {
|
||||
height: calc(
|
||||
(var(--row-size, 1) * (var(--row-height) + var(--row-gap))) - var(
|
||||
--row-gap
|
||||
)
|
||||
);
|
||||
}
|
||||
/* The text styles the app sets on html and body, so that the card does
|
||||
not inherit the text styles of the page that embeds it. */
|
||||
.theme {
|
||||
color: var(--primary-text-color);
|
||||
font-family: var(--ha-font-family-body);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-body);
|
||||
line-height: normal;
|
||||
letter-spacing: normal;
|
||||
text-align: start;
|
||||
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
|
||||
-webkit-font-smoothing: var(--ha-font-smoothing);
|
||||
}
|
||||
/* What setDirectionStyles sets on the app for a right-to-left language,
|
||||
here only for the card */
|
||||
.theme[dir="rtl"] {
|
||||
--direction: rtl;
|
||||
--float-start: right;
|
||||
--float-end: left;
|
||||
--scale-direction: -1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-embed-card": HaEmbedCard;
|
||||
}
|
||||
}
|
||||
@@ -1,293 +0,0 @@
|
||||
import { ContextProvider, provide } from "@lit/context";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../../../src/common/dom/media_query";
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
|
||||
import type { CalendarEventApiData } from "../../../src/data/calendar";
|
||||
import { reducedMotionContext } from "../../../src/data/context";
|
||||
import type { EnergyInfo, EnergyPreferences } from "../../../src/data/energy";
|
||||
import { EMPTY_PREFERENCES } from "../../../src/data/energy";
|
||||
import type { EntityRegistryEntry } from "../../../src/data/entity/entity_registry";
|
||||
import type { EntityInput } from "../../../src/fake_data/entities/types";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import type { DemoArea } from "../stubs/area_registry";
|
||||
import { toAreaRegistryEntry } from "../stubs/area_registry";
|
||||
import { mockAuth } from "../stubs/auth";
|
||||
import {
|
||||
FAMILY_EVENTS,
|
||||
mockCalendarEvents,
|
||||
WORK_EVENTS,
|
||||
} from "../stubs/calendar";
|
||||
import { extEntry, newEntry } from "../stubs/entity_registry";
|
||||
import { mockHistory } from "../stubs/history";
|
||||
import { mockIcons } from "../stubs/icons";
|
||||
import { mockLogbook } from "../stubs/logbook";
|
||||
import { mockMediaPlayer } from "../stubs/media_player";
|
||||
import { mockRecorder } from "../stubs/recorder";
|
||||
import { mockSensor } from "../stubs/sensor";
|
||||
import { mockExtractFromTarget } from "../stubs/target";
|
||||
import { mockTemplate } from "../stubs/template";
|
||||
import { mockTodo } from "../stubs/todo";
|
||||
import { mockWeather } from "../stubs/weather";
|
||||
import { embedHassContext } from "./context";
|
||||
import { readScriptData } from "./script-data";
|
||||
|
||||
/** The data of a provider, as set by its script child. */
|
||||
interface EmbedProviderData {
|
||||
entities?: EntityInput[];
|
||||
areas?: DemoArea[];
|
||||
calendars?: Record<string, CalendarEventApiData[]>;
|
||||
energy?: Partial<EnergyPreferences>;
|
||||
}
|
||||
|
||||
const SAMPLE_CALENDAR_EVENTS = [FAMILY_EVENTS, WORK_EVENTS];
|
||||
|
||||
// "living_room" becomes "Living room"
|
||||
const areaName = (areaId: string) => {
|
||||
const name = areaId.replaceAll("_", " ");
|
||||
|
||||
return name.charAt(0).toUpperCase() + name.slice(1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Holds a mocked Home Assistant backend for all `ha-embed-card` elements
|
||||
* inside it. Cards in the same provider share their entities.
|
||||
*/
|
||||
@customElement("ha-embed-provider")
|
||||
export class HaEmbedProvider extends LitElement {
|
||||
@provide({ context: embedHassContext })
|
||||
@property({ attribute: false })
|
||||
public hass!: MockHomeAssistant;
|
||||
|
||||
/**
|
||||
* All entities of the backend. Each change replaces all entities.
|
||||
* Also read from a JSON or YAML script child, see `readScriptData`. The
|
||||
* script holds either the list of entities or an object with `entities`,
|
||||
* `areas`, `calendars` and `energy`.
|
||||
*/
|
||||
@property({ type: Array }) public entities: EntityInput[] = [];
|
||||
|
||||
/** All areas. Without them, an area for each `area_id` of the entities. */
|
||||
@property({ attribute: false }) public areas?: DemoArea[];
|
||||
|
||||
/**
|
||||
* Events of calendar entities by entity ID. Calendars without events here
|
||||
* get sample events around today.
|
||||
*/
|
||||
@property({ attribute: false }) public calendars?: Record<
|
||||
string,
|
||||
CalendarEventApiData[]
|
||||
>;
|
||||
|
||||
/** Energy preferences. Without them, energy is not set up. */
|
||||
@property({ attribute: false }) public energy?: Partial<EnergyPreferences>;
|
||||
|
||||
/** Light or dark mode of the cards. Without it, the browser setting. */
|
||||
@property({ attribute: "color-scheme" }) public colorScheme?:
|
||||
"light" | "dark";
|
||||
|
||||
private _reducedMotion = new ContextProvider(this, {
|
||||
context: reducedMotionContext,
|
||||
initialValue: false,
|
||||
});
|
||||
|
||||
private _prefersDark = matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
|
||||
private _unsubMediaQueries: (() => void)[] = [];
|
||||
|
||||
private _scriptRead = false;
|
||||
|
||||
private _applyColorScheme() {
|
||||
const dark = this.colorScheme
|
||||
? this.colorScheme === "dark"
|
||||
: this._prefersDark;
|
||||
if (this.hass.themes.darkMode !== dark) {
|
||||
this.hass.updateHass({
|
||||
themes: { ...this.hass.themes, darkMode: dark },
|
||||
selectedTheme: { ...this.hass.selectedTheme!, dark },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// Set up in the constructor so that the contexts exist before the
|
||||
// child cards connect and request them.
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null);
|
||||
hass.updateTranslations("lovelace");
|
||||
mockIcons(hass);
|
||||
mockAuth(hass);
|
||||
mockHistory(hass);
|
||||
mockRecorder(hass);
|
||||
mockTemplate(hass);
|
||||
mockSensor(hass);
|
||||
mockMediaPlayer(hass);
|
||||
mockTodo(hass);
|
||||
mockWeather(hass);
|
||||
mockLogbook(hass);
|
||||
hass.updateHass({
|
||||
config: {
|
||||
...hass.config,
|
||||
components: [...hass.config.components, "logbook"],
|
||||
},
|
||||
});
|
||||
mockCalendarEvents(hass, (entityId, currentHass) => {
|
||||
if (!(entityId in currentHass.states)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const calendarIds = Object.keys(currentHass.states).filter((id) =>
|
||||
id.startsWith("calendar.")
|
||||
);
|
||||
|
||||
return (
|
||||
this.calendars?.[entityId] ??
|
||||
SAMPLE_CALENDAR_EVENTS[
|
||||
calendarIds.indexOf(entityId) % SAMPLE_CALENDAR_EVENTS.length
|
||||
]
|
||||
);
|
||||
});
|
||||
hass.mockWS("config/area_registry/list", (_msg, currentHass) =>
|
||||
Object.values(currentHass.areas)
|
||||
);
|
||||
// The registry of the entities, without settings such as calendar colors
|
||||
const registryEntries = (currentHass: MockHomeAssistant) =>
|
||||
Object.values(currentHass.entities).map(
|
||||
(entity): EntityRegistryEntry => ({
|
||||
...newEntry({
|
||||
entity_id: entity.entity_id,
|
||||
platform: entity.platform ?? "demo",
|
||||
unique_id: entity.entity_id,
|
||||
}),
|
||||
area_id: entity.area_id ?? null,
|
||||
device_id: entity.device_id ?? null,
|
||||
})
|
||||
);
|
||||
hass.mockWS("config/entity_registry/list", (_msg, currentHass) =>
|
||||
registryEntries(currentHass)
|
||||
);
|
||||
hass.mockWS(
|
||||
"config/entity_registry/get",
|
||||
(msg: { entity_id: string }, currentHass: MockHomeAssistant) => {
|
||||
const entry = registryEntries(currentHass).find(
|
||||
(item) => item.entity_id === msg.entity_id
|
||||
);
|
||||
if (!entry) {
|
||||
throw new Error(`Entity not found: ${msg.entity_id}`);
|
||||
}
|
||||
return extEntry(entry);
|
||||
}
|
||||
);
|
||||
mockExtractFromTarget(hass);
|
||||
hass.mockWS("energy/get_prefs", (): EnergyPreferences => ({
|
||||
...EMPTY_PREFERENCES,
|
||||
...this.energy,
|
||||
}));
|
||||
hass.mockWS("energy/info", (): EnergyInfo => ({
|
||||
cost_sensors: {},
|
||||
solar_forecast_domains: [],
|
||||
}));
|
||||
this._applyColorScheme();
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._unsubMediaQueries = [
|
||||
listenMediaQuery("(prefers-color-scheme: dark)", (matches) => {
|
||||
this._prefersDark = matches;
|
||||
this._applyColorScheme();
|
||||
}),
|
||||
listenMediaQuery("(prefers-reduced-motion: reduce)", (matches) =>
|
||||
this._reducedMotion.setValue(matches)
|
||||
),
|
||||
];
|
||||
|
||||
// Only once, so that moving the provider keeps the current entities
|
||||
if (this._scriptRead) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._scriptRead = true;
|
||||
|
||||
const data = readScriptData(this) as
|
||||
EntityInput[] | EmbedProviderData | undefined;
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
this.entities = data;
|
||||
} else if (data) {
|
||||
this.entities = data.entities ?? [];
|
||||
this.areas = data.areas;
|
||||
this.calendars = data.calendars;
|
||||
this.energy = data.energy;
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubMediaQueries.forEach((unsub) => unsub());
|
||||
this._unsubMediaQueries = [];
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
if (changedProps.has("colorScheme")) {
|
||||
this._applyColorScheme();
|
||||
}
|
||||
|
||||
if (changedProps.has("entities") || changedProps.has("areas")) {
|
||||
const areas: DemoArea[] =
|
||||
this.areas ??
|
||||
[
|
||||
...new Set(
|
||||
this.entities
|
||||
.map((entity) => entity.area_id)
|
||||
.filter((areaId) => areaId !== undefined)
|
||||
),
|
||||
].map((areaId) => ({ area_id: areaId, name: areaName(areaId) }));
|
||||
|
||||
const areasById: Record<string, AreaRegistryEntry> = {};
|
||||
areas.forEach((area) => {
|
||||
areasById[area.area_id] = toAreaRegistryEntry(area);
|
||||
});
|
||||
// Before the entities, so that their names can include the area
|
||||
this.hass.updateHass({ areas: areasById });
|
||||
|
||||
// Entities that services changed keep their state when only the areas
|
||||
// change
|
||||
if (changedProps.has("entities")) {
|
||||
this.hass.addEntities(this.entities, true);
|
||||
} else {
|
||||
this.hass.updateFormatFunctions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
const oldHass = changedProps.get("hass");
|
||||
// The first update holds the initial entities. They are not changes.
|
||||
if (!oldHass) {
|
||||
return;
|
||||
}
|
||||
// Other updates, such as loaded translations, keep the same states object.
|
||||
if (oldHass.states !== this.hass.states) {
|
||||
fireEvent(this, "states-changed", { states: this.hass.states });
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-embed-provider": HaEmbedProvider;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"states-changed": { states: HassEntities };
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
declare global {
|
||||
var __HA_STATIC_PATH__: string | undefined;
|
||||
}
|
||||
declare let __webpack_public_path__: string;
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
|
||||
// Load chunks and static files from the server that hosts this script, so
|
||||
// that other sites can embed the cards. Only the modern production build keeps
|
||||
// `import.meta.url` (see build-scripts/rspack.cjs). The legacy and development
|
||||
// builds only run on the demo site, where the default paths are correct.
|
||||
// `new URL(path, import.meta.url)` in one expression is an asset reference for
|
||||
// the bundler, so the script URL goes into a variable first.
|
||||
if (__BUILD__ === "modern" && !__DEV__) {
|
||||
const scriptUrl = import.meta.url;
|
||||
__webpack_public_path__ = new URL(".", scriptUrl).href;
|
||||
globalThis.__HA_STATIC_PATH__ = new URL("../static/", scriptUrl).href;
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,17 +0,0 @@
|
||||
import { load } from "js-yaml";
|
||||
|
||||
/**
|
||||
* Parses the data in a `<script type="application/json">` or
|
||||
* `<script type="text/yaml">` child of the element.
|
||||
*/
|
||||
export const readScriptData = (element: HTMLElement): unknown => {
|
||||
const script = element.querySelector<HTMLScriptElement>(
|
||||
':scope > script[type="application/json"], :scope > script[type="text/yaml"]'
|
||||
);
|
||||
if (!script) {
|
||||
return undefined;
|
||||
}
|
||||
return script.type === "text/yaml"
|
||||
? load(script.textContent!)
|
||||
: JSON.parse(script.textContent!);
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
import { unsafeCSS } from "lit";
|
||||
import { hex2rgb } from "../../../src/common/color/convert-color";
|
||||
import { darkColorVariables } from "../../../src/resources/theme/color";
|
||||
import { darkSemanticVariables } from "../../../src/resources/theme/semantic.globals";
|
||||
import { themeStyles } from "../../../src/resources/theme/theme";
|
||||
|
||||
// The theme styles set variables on `html`, next to fonts and other page
|
||||
// styles. Only the variables, media queries, and keyframes are copied, and
|
||||
// `html` becomes `selector`. This keeps the theme inside the card, so that it
|
||||
// does not change the page that embeds the card.
|
||||
const scopeThemeStyles = (cssText: string, selector: string): string => {
|
||||
// A style element in a separate document parses the CSS without applying it.
|
||||
const doc = document.implementation.createHTMLDocument("");
|
||||
const style = doc.createElement("style");
|
||||
style.textContent = cssText;
|
||||
doc.head.append(style);
|
||||
|
||||
const convert = (rules: CSSRuleList): string =>
|
||||
Array.from(rules, (rule) => {
|
||||
if (rule instanceof CSSStyleRule) {
|
||||
if (rule.selectorText !== "html") {
|
||||
return "";
|
||||
}
|
||||
const variables = Array.from(rule.style)
|
||||
.filter((name) => name.startsWith("--"))
|
||||
.map((name) => `${name}:${rule.style.getPropertyValue(name)};`);
|
||||
return `${selector}{${variables.join("")}}`;
|
||||
}
|
||||
if (rule instanceof CSSMediaRule) {
|
||||
return `@media ${rule.conditionText}{${convert(rule.cssRules)}}`;
|
||||
}
|
||||
if (rule instanceof CSSKeyframesRule) {
|
||||
return rule.cssText;
|
||||
}
|
||||
return "";
|
||||
}).join("");
|
||||
|
||||
return convert(style.sheet!.cssRules);
|
||||
};
|
||||
|
||||
const darkTheme = {
|
||||
...darkSemanticVariables,
|
||||
...darkColorVariables,
|
||||
};
|
||||
|
||||
// Like processTheme in apply_themes_on_element, each hex color also sets its
|
||||
// `--rgb-` variable, else those keep their light theme values
|
||||
const darkVariables = Object.entries(darkTheme).flatMap(([name, value]) => {
|
||||
const variable = `--${name}:${value};`;
|
||||
|
||||
if (!value.startsWith("#") || `rgb-${name}` in darkTheme) {
|
||||
return [variable];
|
||||
}
|
||||
|
||||
return [variable, `--rgb-${name}:${hex2rgb(value).join(",")};`];
|
||||
});
|
||||
|
||||
export const embedThemeStyles = unsafeCSS(
|
||||
scopeThemeStyles(themeStyles, ".theme") +
|
||||
`.theme.dark{${darkVariables.join("")}}`
|
||||
);
|
||||
@@ -1,32 +0,0 @@
|
||||
// Browsers only start a worker from a script on the page's own origin, and
|
||||
// the workers of the cards, such as the markdown worker, are served next to
|
||||
// this script. A worker from another origin starts from a blob instead, which
|
||||
// loads the real worker script. The demo serves its scripts with CORS.
|
||||
const pageWorker = globalThis.Worker;
|
||||
|
||||
globalThis.Worker = class extends pageWorker {
|
||||
constructor(scriptUrl: string | URL, options?: WorkerOptions) {
|
||||
const url = new URL(scriptUrl, location.href);
|
||||
|
||||
if (url.origin === location.origin) {
|
||||
super(scriptUrl, options);
|
||||
return;
|
||||
}
|
||||
|
||||
const source =
|
||||
options?.type === "module"
|
||||
? `import ${JSON.stringify(url.href)};`
|
||||
: `importScripts(${JSON.stringify(url.href)});`;
|
||||
const blobUrl = URL.createObjectURL(
|
||||
new Blob([source], { type: "text/javascript" })
|
||||
);
|
||||
|
||||
try {
|
||||
super(blobUrl, options);
|
||||
} finally {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export {};
|
||||
@@ -36,7 +36,6 @@ import { mockHardware } from "./stubs/hardware";
|
||||
import { mockHassioSupervisor } from "./stubs/hassio_supervisor";
|
||||
import { mockIntegration } from "./stubs/integration";
|
||||
import { mockLabelRegistry } from "./stubs/label_registry";
|
||||
import { mockLogbook } from "./stubs/logbook";
|
||||
import { automationPlatformDomains } from "./stubs/automation_platform_domains";
|
||||
import { mockIcons } from "./stubs/icons";
|
||||
import { mockHistory } from "./stubs/history";
|
||||
@@ -51,7 +50,6 @@ import { mockTemplate } from "./stubs/template";
|
||||
import { mockTodo } from "./stubs/todo";
|
||||
import { mockTranslations } from "./stubs/translations";
|
||||
import { mockUsagePrediction } from "./stubs/usage_prediction";
|
||||
import { mockWeather } from "./stubs/weather";
|
||||
|
||||
// WS command / REST path prefixes whose mocks live in the lazily imported
|
||||
// config-panel chunk (see ./stubs/config-panel). Must stay in sync with it.
|
||||
@@ -192,9 +190,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
mockSystemLog(hass);
|
||||
mockTemplate(hass);
|
||||
mockEvents(hass);
|
||||
mockLogbook(hass);
|
||||
mockMediaPlayer(hass);
|
||||
mockWeather(hass);
|
||||
mockFrontend(hass);
|
||||
mockHardware(hass);
|
||||
mockHassioSupervisor(hass);
|
||||
|
||||
@@ -1,762 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Home Assistant card components</title>
|
||||
<%= renderTemplate("../../../src/html/_header.html.template") %>
|
||||
<meta name="referrer" content="same-origin" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="theme-color" content="#03a9f4" />
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--page-background: #fafafa;
|
||||
--page-text: #212121;
|
||||
--page-code-background: #e5e5e5;
|
||||
--page-divider: #dddddd;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-scheme="light"]) {
|
||||
color-scheme: dark;
|
||||
--page-background: #111111;
|
||||
--page-text: #e1e1e1;
|
||||
--page-code-background: #1c1c1c;
|
||||
--page-divider: #333333;
|
||||
}
|
||||
}
|
||||
:root[data-scheme="dark"] {
|
||||
color-scheme: dark;
|
||||
--page-background: #111111;
|
||||
--page-text: #e1e1e1;
|
||||
--page-code-background: #1c1c1c;
|
||||
--page-divider: #333333;
|
||||
}
|
||||
html {
|
||||
background-color: var(--page-background);
|
||||
color: var(--page-text);
|
||||
}
|
||||
body {
|
||||
font-family: Roboto, Noto, sans-serif;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
padding: 0 16px 64px;
|
||||
}
|
||||
main {
|
||||
max-width: 880px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
margin: 32px 0 8px;
|
||||
}
|
||||
h2 {
|
||||
margin: 48px 0 8px;
|
||||
}
|
||||
pre {
|
||||
background: var(--page-code-background);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
overflow-x: auto;
|
||||
font-size: 13px;
|
||||
}
|
||||
code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
text-align: start;
|
||||
vertical-align: top;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--page-divider);
|
||||
}
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
margin: 16px 0;
|
||||
}
|
||||
.single {
|
||||
max-width: 400px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
.log {
|
||||
height: 160px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
}
|
||||
button {
|
||||
font: inherit;
|
||||
padding: 6px 12px;
|
||||
margin-inline-end: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Home Assistant card components</h1>
|
||||
<p>
|
||||
Embed working Home Assistant dashboard cards in any page. The cards run
|
||||
against a mocked backend in the browser. No Home Assistant server is
|
||||
necessary.
|
||||
</p>
|
||||
<p>There are two components:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<code><ha-embed-provider></code> holds the mocked backend and
|
||||
its entities.
|
||||
</li>
|
||||
<li>
|
||||
<code><ha-embed-card></code> renders one card. It uses the
|
||||
backend of a provider.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
All cards in the same provider share their entities. When you change
|
||||
an entity in one card, the other cards show the change.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Color scheme of this page and its cards:
|
||||
<button data-scheme="">Auto</button>
|
||||
<button data-scheme="light">Light</button>
|
||||
<button data-scheme="dark">Dark</button>
|
||||
</p>
|
||||
|
||||
<h2>Use on your site</h2>
|
||||
<p>
|
||||
Load the script from the demo site. The script loads all other files
|
||||
from the demo site too.
|
||||
</p>
|
||||
<pre><code><script type="module" src="https://demo.home-assistant.io/embed.js"></script></code></pre>
|
||||
|
||||
<h2>One card</h2>
|
||||
<p>
|
||||
Put the entities in a script tag inside the provider. Put the card
|
||||
configuration in a script tag inside the card. Both take JSON with
|
||||
<code>type="application/json"</code> or YAML with
|
||||
<code>type="text/yaml"</code>. The card configuration is the same as in
|
||||
a dashboard. YAML needs its indentation. If your site minifies HTML,
|
||||
make sure the minifier keeps the whitespace in script tags, or use
|
||||
JSON.
|
||||
</p>
|
||||
<pre><code><ha-embed-provider>
|
||||
<script type="text/yaml">
|
||||
- entity_id: light.kitchen
|
||||
state: "on"
|
||||
attributes:
|
||||
friendly_name: Kitchen
|
||||
supported_color_modes: [brightness]
|
||||
color_mode: brightness
|
||||
brightness: 180
|
||||
</script>
|
||||
<ha-embed-card>
|
||||
<script type="text/yaml">
|
||||
type: tile
|
||||
entity: light.kitchen
|
||||
tap_action:
|
||||
action: toggle
|
||||
features:
|
||||
- type: light-brightness
|
||||
</script>
|
||||
</ha-embed-card>
|
||||
</ha-embed-provider></code></pre>
|
||||
<ha-embed-provider>
|
||||
<!-- htmlmin:ignore --><script type="text/yaml">
|
||||
- entity_id: light.kitchen
|
||||
state: "on"
|
||||
attributes:
|
||||
friendly_name: Kitchen
|
||||
supported_color_modes: [brightness]
|
||||
color_mode: brightness
|
||||
brightness: 180
|
||||
</script><!-- htmlmin:ignore -->
|
||||
<div class="single">
|
||||
<ha-embed-card>
|
||||
<!-- htmlmin:ignore --><script type="text/yaml">
|
||||
type: tile
|
||||
entity: light.kitchen
|
||||
tap_action:
|
||||
action: toggle
|
||||
features:
|
||||
- type: light-brightness
|
||||
</script><!-- htmlmin:ignore -->
|
||||
</ha-embed-card>
|
||||
</div>
|
||||
</ha-embed-provider>
|
||||
<p>
|
||||
The <code>entities</code> attribute of the provider and the
|
||||
<code>config</code> attribute of the card also take JSON.
|
||||
</p>
|
||||
|
||||
<h2>Cards with shared state</h2>
|
||||
<p>
|
||||
These three cards are in one provider. Turn on a light in one card. The
|
||||
other cards update. The log below shows the events of the provider.
|
||||
</p>
|
||||
<ha-embed-provider id="shared">
|
||||
<script type="application/json">
|
||||
[
|
||||
{
|
||||
"entity_id": "light.living_room",
|
||||
"state": "off",
|
||||
"attributes": {
|
||||
"friendly_name": "Living room",
|
||||
"supported_color_modes": ["brightness"],
|
||||
"color_mode": "brightness",
|
||||
"brightness": 255
|
||||
}
|
||||
},
|
||||
{
|
||||
"entity_id": "switch.coffee_maker",
|
||||
"state": "on",
|
||||
"attributes": {
|
||||
"friendly_name": "Coffee maker",
|
||||
"device_class": "outlet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"entity_id": "climate.hallway",
|
||||
"state": "heat",
|
||||
"attributes": {
|
||||
"friendly_name": "Hallway",
|
||||
"current_temperature": 19.5,
|
||||
"temperature": 21,
|
||||
"min_temp": 7,
|
||||
"max_temp": 30,
|
||||
"target_temp_step": 0.5,
|
||||
"hvac_modes": ["heat", "off"],
|
||||
"hvac_action": "heating",
|
||||
"supported_features": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.outside_temperature",
|
||||
"state": "12.3",
|
||||
"attributes": {
|
||||
"friendly_name": "Outside temperature",
|
||||
"device_class": "temperature",
|
||||
"state_class": "measurement",
|
||||
"unit_of_measurement": "°C"
|
||||
}
|
||||
}
|
||||
]
|
||||
</script>
|
||||
<div class="cards">
|
||||
<ha-embed-card>
|
||||
<script type="application/json">
|
||||
{
|
||||
"type": "tile",
|
||||
"entity": "light.living_room",
|
||||
"tap_action": { "action": "toggle" }
|
||||
}
|
||||
</script>
|
||||
</ha-embed-card>
|
||||
<ha-embed-card>
|
||||
<!-- htmlmin:ignore --><script type="text/yaml">
|
||||
type: entities
|
||||
title: Living room
|
||||
entities:
|
||||
- light.living_room
|
||||
- switch.coffee_maker
|
||||
- sensor.outside_temperature
|
||||
</script><!-- htmlmin:ignore -->
|
||||
</ha-embed-card>
|
||||
<ha-embed-card>
|
||||
<!-- htmlmin:ignore --><script type="text/yaml">
|
||||
type: thermostat
|
||||
entity: climate.hallway
|
||||
</script><!-- htmlmin:ignore -->
|
||||
</ha-embed-card>
|
||||
</div>
|
||||
</ha-embed-provider>
|
||||
<pre class="log" id="log"><code>Use the cards to see events here.</code></pre>
|
||||
|
||||
<h2>Cards outside a provider</h2>
|
||||
<p>
|
||||
A card does not have to be inside a provider. A card outside a provider
|
||||
uses the first provider on the page. Put one provider with all entities
|
||||
at the top of the page, and put the cards anywhere below it. If the
|
||||
page has no provider, the card adds an empty provider to the page.
|
||||
</p>
|
||||
<p>
|
||||
To use a different provider, set the <code>provider</code> attribute of
|
||||
the card to the ID of the provider.
|
||||
</p>
|
||||
<pre><code><ha-embed-provider id="living-room">…</ha-embed-provider>
|
||||
|
||||
<p>Some text about the coffee maker.</p>
|
||||
|
||||
<ha-embed-card provider="living-room">
|
||||
<script type="text/yaml">
|
||||
type: tile
|
||||
entity: switch.coffee_maker
|
||||
</script>
|
||||
</ha-embed-card></code></pre>
|
||||
<p>
|
||||
These two cards are not inside a provider. The first card uses the
|
||||
first provider on this page, the kitchen light at the top. The second
|
||||
card uses the provider with the ID <code>shared</code> above.
|
||||
</p>
|
||||
<div class="cards">
|
||||
<ha-embed-card>
|
||||
<!-- htmlmin:ignore --><script type="text/yaml">
|
||||
type: tile
|
||||
entity: light.kitchen
|
||||
tap_action:
|
||||
action: toggle
|
||||
</script><!-- htmlmin:ignore -->
|
||||
</ha-embed-card>
|
||||
<ha-embed-card provider="shared">
|
||||
<!-- htmlmin:ignore --><script type="text/yaml">
|
||||
type: tile
|
||||
entity: switch.coffee_maker
|
||||
tap_action:
|
||||
action: toggle
|
||||
</script><!-- htmlmin:ignore -->
|
||||
</ha-embed-card>
|
||||
</div>
|
||||
|
||||
<h2>Light and dark mode</h2>
|
||||
<p>
|
||||
The cards follow the light or dark mode of the browser. To choose the
|
||||
mode, set the <code>color-scheme</code> attribute of the provider to
|
||||
<code>light</code> or <code>dark</code>. Use this when your site has its
|
||||
own switch for dark mode. The buttons at the top of this page do this.
|
||||
The cards keep their styles inside. They do not change the styles of
|
||||
your page.
|
||||
</p>
|
||||
<pre><code><ha-embed-provider color-scheme="dark">…</ha-embed-provider></code></pre>
|
||||
|
||||
<h2>Change entities from the page</h2>
|
||||
<p>
|
||||
Set the <code>entities</code> property to change entities from
|
||||
JavaScript. The new list replaces all entities. To change one entity,
|
||||
start from the current states.
|
||||
</p>
|
||||
<p>
|
||||
<button id="set-temperature">Set a random outside temperature</button>
|
||||
<button id="toggle-coffee">Toggle the coffee maker</button>
|
||||
</p>
|
||||
<pre><code>const provider = document.querySelector("ha-embed-provider");
|
||||
provider.entities = Object.values(provider.hass.states).map((entity) =>
|
||||
entity.entity_id === "sensor.outside_temperature"
|
||||
? { ...entity, state: "18.1" }
|
||||
: entity
|
||||
);</code></pre>
|
||||
<p>
|
||||
To use the actions of an entity, call a service. The provider updates
|
||||
the state and the cards.
|
||||
</p>
|
||||
<pre><code>provider.hass.callService("switch", "toggle", {
|
||||
entity_id: "switch.coffee_maker",
|
||||
});</code></pre>
|
||||
|
||||
<h2>Listen to changes</h2>
|
||||
<p>
|
||||
The provider fires a <code>states-changed</code> event when the state
|
||||
of one or more entities changes. The event has the states of all
|
||||
entities. The event bubbles.
|
||||
</p>
|
||||
<pre><code>provider.addEventListener("states-changed", (ev) => {
|
||||
const { states } = ev.detail;
|
||||
console.log(states["light.living_room"].state);
|
||||
});</code></pre>
|
||||
<p>
|
||||
Read the current state of all entities with
|
||||
<code>provider.hass.states</code>.
|
||||
</p>
|
||||
|
||||
<h2>Areas, calendars and energy</h2>
|
||||
<p>
|
||||
Some cards need data that is not an entity. To set it, put an object in
|
||||
the script tag of the provider instead of the list. The object has the
|
||||
list in <code>entities</code>, and any of these keys:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<code>areas</code>: a list of areas, each with an
|
||||
<code>area_id</code> and a <code>name</code>. Without it, each
|
||||
<code>area_id</code> of the entities becomes an area.
|
||||
</li>
|
||||
<li>
|
||||
<code>calendars</code>: the events of calendar entities, by entity
|
||||
ID. Each event has a <code>summary</code>, a <code>start</code> and
|
||||
an <code>end</code>. Calendars without events here get sample events
|
||||
around today.
|
||||
</li>
|
||||
<li>
|
||||
<code>energy</code>: the energy settings, the same as the
|
||||
<code>energy/get_prefs</code> response. Without it, energy is not
|
||||
set up.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Weather entities get a forecast made up from their condition and
|
||||
temperature, or their <code>forecast</code> attribute when they have
|
||||
one. Set <code>supported_features</code> to show forecasts.
|
||||
</p>
|
||||
<pre><code><ha-embed-provider>
|
||||
<script type="text/yaml">
|
||||
entities:
|
||||
- entity_id: calendar.family
|
||||
state: "off"
|
||||
attributes:
|
||||
friendly_name: Family
|
||||
areas:
|
||||
- area_id: kitchen
|
||||
name: Kitchen
|
||||
calendars:
|
||||
calendar.family:
|
||||
- summary: Dentist
|
||||
start: "2026-11-02T15:30:00+00:00"
|
||||
end: "2026-11-02T16:15:00+00:00"
|
||||
</script>
|
||||
…
|
||||
</ha-embed-provider></code></pre>
|
||||
<p>This calendar card uses the sample events.</p>
|
||||
<ha-embed-provider>
|
||||
<script type="application/json">
|
||||
{
|
||||
"entities": [
|
||||
{
|
||||
"entity_id": "calendar.family",
|
||||
"state": "off",
|
||||
"attributes": { "friendly_name": "Family" }
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
<div class="single">
|
||||
<ha-embed-card
|
||||
config='{ "type": "calendar", "entities": ["calendar.family"], "initial_view": "listWeek" }'
|
||||
></ha-embed-card>
|
||||
</div>
|
||||
</ha-embed-provider>
|
||||
|
||||
<h2>Custom cards</h2>
|
||||
<p>
|
||||
Custom cards work too. Load the script of the custom card on the page,
|
||||
and use <code>custom:</code> and the element name as the card type.
|
||||
The custom card gets the same mocked backend as the other cards.
|
||||
<code>window.loadCardHelpers()</code> is available to create built-in
|
||||
cards.
|
||||
</p>
|
||||
<p>
|
||||
The mocked backend only knows a part of the Home Assistant API. History
|
||||
and statistics are made up from the current state of each entity.
|
||||
Templates are not rendered, they come back as they are. The logbook is
|
||||
empty. Other requests get an error. From another site, the script of
|
||||
the custom card needs CORS headers, like the embed script.
|
||||
</p>
|
||||
<pre><code><script type="module" src="https://example.com/embed-demo-custom-card.js"></script>
|
||||
<ha-embed-card>
|
||||
<script type="text/yaml">
|
||||
type: custom:embed-demo-custom-card
|
||||
entity: light.office
|
||||
</script>
|
||||
</ha-embed-card></code></pre>
|
||||
<ha-embed-provider>
|
||||
<script type="application/json">
|
||||
[
|
||||
{
|
||||
"entity_id": "light.office",
|
||||
"state": "on",
|
||||
"attributes": {
|
||||
"friendly_name": "Office",
|
||||
"supported_color_modes": ["brightness"],
|
||||
"color_mode": "brightness",
|
||||
"brightness": 120
|
||||
}
|
||||
}
|
||||
]
|
||||
</script>
|
||||
<div class="single">
|
||||
<ha-embed-card
|
||||
config='{ "type": "custom:embed-demo-custom-card", "entity": "light.office" }'
|
||||
></ha-embed-card>
|
||||
</div>
|
||||
</ha-embed-provider>
|
||||
<p>This is the code of the custom card:</p>
|
||||
<pre><code>class EmbedDemoCustomCard extends HTMLElement {
|
||||
setConfig(config) {
|
||||
if (!config.entity) {
|
||||
throw new Error("Set an entity");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
set hass(hass) {
|
||||
this._hass = hass;
|
||||
this._render();
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
async _render() {
|
||||
if (!this.shadowRoot) {
|
||||
this.attachShadow({ mode: "open" }).innerHTML = `
|
||||
<style>
|
||||
ha-card { padding: 16px; }
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
background: var(--accent-color);
|
||||
color: var(--text-accent-color, #fff);
|
||||
font-size: 12px;
|
||||
}
|
||||
.tile { margin: 12px 0; }
|
||||
</style>
|
||||
<ha-card>
|
||||
<span class="badge">Custom card</span>
|
||||
<p>
|
||||
This is a custom card. It is not part of Home Assistant. The page
|
||||
defines it, and it creates the tile card below with the card
|
||||
helpers.
|
||||
</p>
|
||||
<div class="tile"></div>
|
||||
<button>Toggle from the custom card</button>
|
||||
</ha-card>`;
|
||||
this.shadowRoot.querySelector("button").addEventListener("click", () =>
|
||||
this._hass.callService("homeassistant", "toggle", {
|
||||
entity_id: this._config.entity,
|
||||
})
|
||||
);
|
||||
}
|
||||
// Keep the promise, so that a second update does not create a second tile.
|
||||
if (!this._tile) {
|
||||
this._tile = window.loadCardHelpers().then((helpers) => {
|
||||
const tile = helpers.createCardElement({
|
||||
type: "tile",
|
||||
entity: this._config.entity,
|
||||
});
|
||||
this.shadowRoot.querySelector(".tile").append(tile);
|
||||
return tile;
|
||||
});
|
||||
}
|
||||
(await this._tile).hass = this._hass;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("embed-demo-custom-card", EmbedDemoCustomCard);</code></pre>
|
||||
<script type="module">
|
||||
class EmbedDemoCustomCard extends HTMLElement {
|
||||
setConfig(config) {
|
||||
if (!config.entity) {
|
||||
throw new Error("Set an entity");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
set hass(hass) {
|
||||
this._hass = hass;
|
||||
this._render();
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
async _render() {
|
||||
if (!this.shadowRoot) {
|
||||
this.attachShadow({ mode: "open" }).innerHTML = `
|
||||
<style>
|
||||
ha-card { padding: 16px; }
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
background: var(--accent-color);
|
||||
color: var(--text-accent-color, #fff);
|
||||
font-size: 12px;
|
||||
}
|
||||
.tile { margin: 12px 0; }
|
||||
</style>
|
||||
<ha-card>
|
||||
<span class="badge">Custom card</span>
|
||||
<p>
|
||||
This is a custom card. It is not part of Home Assistant. The page
|
||||
defines it, and it creates the tile card below with the card
|
||||
helpers.
|
||||
</p>
|
||||
<div class="tile"></div>
|
||||
<button>Toggle from the custom card</button>
|
||||
</ha-card>`;
|
||||
this.shadowRoot.querySelector("button").addEventListener("click", () =>
|
||||
this._hass.callService("homeassistant", "toggle", {
|
||||
entity_id: this._config.entity,
|
||||
})
|
||||
);
|
||||
}
|
||||
// Keep the promise, so that a second update does not create a second tile.
|
||||
if (!this._tile) {
|
||||
this._tile = window.loadCardHelpers().then((helpers) => {
|
||||
const tile = helpers.createCardElement({
|
||||
type: "tile",
|
||||
entity: this._config.entity,
|
||||
});
|
||||
this.shadowRoot.querySelector(".tile").append(tile);
|
||||
return tile;
|
||||
});
|
||||
}
|
||||
(await this._tile).hass = this._hass;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("embed-demo-custom-card", EmbedDemoCustomCard);
|
||||
</script>
|
||||
|
||||
<h2>Reference</h2>
|
||||
<h3><code><ha-embed-provider></code></h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>entities</code></td>
|
||||
<td>Attribute and property</td>
|
||||
<td>
|
||||
A list of entities. Each entity has an <code>entity_id</code>, a
|
||||
<code>state</code>, and <code>attributes</code>. The optional keys
|
||||
<code>area_id</code>, <code>device_id</code>, and
|
||||
<code>platform</code> set the entity registry entry. A new list
|
||||
replaces all entities. A JSON or YAML script child sets the first
|
||||
list.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>color-scheme</code></td>
|
||||
<td>Attribute and property (<code>colorScheme</code>)</td>
|
||||
<td>
|
||||
<code>light</code> or <code>dark</code>. Without it, the cards
|
||||
follow the browser setting.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>hass</code></td>
|
||||
<td>Property, read-only</td>
|
||||
<td>The mocked Home Assistant object that the cards use.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>states-changed</code></td>
|
||||
<td>Event</td>
|
||||
<td>
|
||||
Fires when the state of one or more entities changes. The detail
|
||||
has <code>states</code>, the states of all entities by entity ID.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h3><code><ha-embed-card></code></h3>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>config</code></td>
|
||||
<td>Attribute and property</td>
|
||||
<td>
|
||||
The card configuration, the same as in a dashboard. A JSON or YAML
|
||||
script child sets the first configuration.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>provider</code></td>
|
||||
<td>Attribute and property</td>
|
||||
<td>
|
||||
The ID of the provider to use. Without it, the card uses the
|
||||
provider around it, else the first provider on the page.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>
|
||||
The mocked backend knows the actions of lights, switches, climate,
|
||||
covers, fans, locks, media players, and more. Actions that open dialogs,
|
||||
such as <code>more-info</code>, do nothing.
|
||||
</p>
|
||||
</main>
|
||||
<script>
|
||||
(function () {
|
||||
var shared = document.getElementById("shared");
|
||||
var log = document.getElementById("log");
|
||||
var first = true;
|
||||
shared.addEventListener("states-changed", function (ev) {
|
||||
var states = ev.detail.states;
|
||||
var line =
|
||||
new Date().toLocaleTimeString() +
|
||||
" " +
|
||||
Object.keys(states)
|
||||
.map(function (entityId) {
|
||||
return entityId + "=" + states[entityId].state;
|
||||
})
|
||||
.join(", ");
|
||||
if (first) {
|
||||
log.textContent = "";
|
||||
first = false;
|
||||
}
|
||||
log.textContent = line + "\n" + log.textContent;
|
||||
});
|
||||
document
|
||||
.getElementById("set-temperature")
|
||||
.addEventListener("click", function () {
|
||||
var states = shared.hass.states;
|
||||
shared.entities = Object.keys(states).map(function (entityId) {
|
||||
var entity = states[entityId];
|
||||
return {
|
||||
entity_id: entityId,
|
||||
state:
|
||||
entityId === "sensor.outside_temperature"
|
||||
? (Math.random() * 30 - 5).toFixed(1)
|
||||
: entity.state,
|
||||
attributes: entity.attributes,
|
||||
};
|
||||
});
|
||||
});
|
||||
document
|
||||
.querySelectorAll("button[data-scheme]")
|
||||
.forEach(function (button) {
|
||||
button.addEventListener("click", function () {
|
||||
var scheme = button.getAttribute("data-scheme");
|
||||
if (scheme) {
|
||||
document.documentElement.setAttribute("data-scheme", scheme);
|
||||
} else {
|
||||
document.documentElement.removeAttribute("data-scheme");
|
||||
}
|
||||
document
|
||||
.querySelectorAll("ha-embed-provider")
|
||||
.forEach(function (provider) {
|
||||
if (scheme) {
|
||||
provider.setAttribute("color-scheme", scheme);
|
||||
} else {
|
||||
provider.removeAttribute("color-scheme");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
document
|
||||
.getElementById("toggle-coffee")
|
||||
.addEventListener("click", function () {
|
||||
shared.hass.callService("switch", "toggle", {
|
||||
entity_id: "switch.coffee_maker",
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
|
||||
<%= renderTemplate("../../../src/html/_script_loader.html.template") %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +0,0 @@
|
||||
import "<%= latestEntryJS[0] %>";
|
||||
@@ -12,20 +12,6 @@ export interface DemoArea {
|
||||
|
||||
let areas: AreaRegistryEntry[] = [];
|
||||
|
||||
export const toAreaRegistryEntry = (area: DemoArea): AreaRegistryEntry => ({
|
||||
area_id: area.area_id,
|
||||
name: area.name,
|
||||
floor_id: area.floor_id ?? null,
|
||||
icon: area.icon ?? null,
|
||||
temperature_entity_id: area.temperature_entity_id ?? null,
|
||||
humidity_entity_id: area.humidity_entity_id ?? null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
picture: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
});
|
||||
|
||||
export const mockAreaRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: DemoArea[] = []
|
||||
@@ -39,7 +25,19 @@ export const setDemoAreas = (
|
||||
hass: MockHomeAssistant,
|
||||
demoAreas: DemoArea[] = []
|
||||
) => {
|
||||
areas = demoAreas.map(toAreaRegistryEntry);
|
||||
areas = demoAreas.map((area) => ({
|
||||
area_id: area.area_id,
|
||||
name: area.name,
|
||||
floor_id: area.floor_id ?? null,
|
||||
icon: area.icon ?? null,
|
||||
temperature_entity_id: area.temperature_entity_id ?? null,
|
||||
humidity_entity_id: area.humidity_entity_id ?? null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
picture: null,
|
||||
created_at: 0,
|
||||
modified_at: 0,
|
||||
}));
|
||||
const areasById: Record<string, AreaRegistryEntry> = {};
|
||||
areas.forEach((area) => {
|
||||
areasById[area.area_id] = area;
|
||||
|
||||
@@ -2,13 +2,14 @@ import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../../../src/common/array/ensure-array";
|
||||
import { computeDomain } from "../../../src/common/entity/compute_domain";
|
||||
import type { ConditionDescriptions } from "../../../src/data/condition";
|
||||
import type { ExtractFromTargetResult } from "../../../src/data/target";
|
||||
import type { TriggerDescriptions } from "../../../src/data/trigger";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
import { getLabelIds } from "./label_registry";
|
||||
import {
|
||||
conditionDescriptions,
|
||||
triggerDescriptions,
|
||||
} from "./automation_platforms_data";
|
||||
import { expandTarget, mockExtractFromTarget } from "./target";
|
||||
|
||||
type Descriptions = TriggerDescriptions | ConditionDescriptions;
|
||||
|
||||
@@ -19,6 +20,109 @@ interface EntityFilter {
|
||||
supported_features?: number[];
|
||||
}
|
||||
|
||||
// Expands a target to its areas, devices and entities, like core does
|
||||
const expandTarget = (
|
||||
hass: MockHomeAssistant,
|
||||
target: HassServiceTarget,
|
||||
primaryEntitiesOnly = true
|
||||
) => {
|
||||
const missing = {
|
||||
floors: [] as string[],
|
||||
areas: [] as string[],
|
||||
devices: [] as string[],
|
||||
labels: [] as string[],
|
||||
};
|
||||
const knownLabelIds = getLabelIds();
|
||||
const floorIds = ensureArray(target.floor_id ?? []);
|
||||
const labelIds = ensureArray(target.label_id ?? []);
|
||||
missing.floors.push(...floorIds.filter((id) => !(id in hass.floors)));
|
||||
// Like core, unknown labels are reported, but still expanded
|
||||
missing.labels.push(...labelIds.filter((id) => !knownLabelIds.includes(id)));
|
||||
const hasLabel = (labels?: string[]) =>
|
||||
labelIds.some((labelId) => labels?.includes(labelId));
|
||||
|
||||
// Unknown areas and devices are reported, but still referenced
|
||||
const areaIds = new Set(ensureArray(target.area_id ?? []));
|
||||
missing.areas.push(...[...areaIds].filter((id) => !(id in hass.areas)));
|
||||
for (const area of Object.values(hass.areas)) {
|
||||
if (
|
||||
(area.floor_id && floorIds.includes(area.floor_id)) ||
|
||||
hasLabel(area.labels)
|
||||
) {
|
||||
areaIds.add(area.area_id);
|
||||
}
|
||||
}
|
||||
|
||||
// A targeted or labeled device includes its child devices
|
||||
const deviceIds = new Set<string>();
|
||||
const addDevice = (deviceId: string) => {
|
||||
deviceIds.add(deviceId);
|
||||
for (const device of Object.values(hass.devices)) {
|
||||
if (device.parent_device_id === deviceId) {
|
||||
deviceIds.add(device.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const deviceId of ensureArray(target.device_id ?? [])) {
|
||||
if (!(deviceId in hass.devices)) {
|
||||
missing.devices.push(deviceId);
|
||||
}
|
||||
addDevice(deviceId);
|
||||
}
|
||||
for (const device of Object.values(hass.devices)) {
|
||||
if (hasLabel(device.labels)) {
|
||||
addDevice(device.id);
|
||||
}
|
||||
}
|
||||
|
||||
// A child device without its own area is in the area of its parent device
|
||||
const deviceArea = (deviceId: string) => {
|
||||
const device = hass.devices[deviceId];
|
||||
return (
|
||||
device?.area_id ??
|
||||
(device?.parent_device_id
|
||||
? hass.devices[device.parent_device_id]?.area_id
|
||||
: undefined)
|
||||
);
|
||||
};
|
||||
const areaDeviceIds = new Set<string>();
|
||||
for (const device of Object.values(hass.devices)) {
|
||||
const areaId = deviceArea(device.id);
|
||||
if (areaId && areaIds.has(areaId)) {
|
||||
areaDeviceIds.add(device.id);
|
||||
}
|
||||
}
|
||||
|
||||
const entityIds = new Set(ensureArray(target.entity_id ?? []));
|
||||
for (const entity of Object.values(hass.entities)) {
|
||||
// The display registry keeps the entities of a previous demo, and like
|
||||
// core, hidden entities are never included indirectly
|
||||
if (!(entity.entity_id in hass.states) || entity.hidden) {
|
||||
continue;
|
||||
}
|
||||
const primary = !primaryEntitiesOnly || !entity.entity_category;
|
||||
if (
|
||||
hasLabel(entity.labels) ||
|
||||
(primary &&
|
||||
((entity.device_id && deviceIds.has(entity.device_id)) ||
|
||||
(entity.area_id && areaIds.has(entity.area_id)) ||
|
||||
// An entity with its own area is not in the area of its device
|
||||
(!entity.area_id &&
|
||||
entity.device_id &&
|
||||
areaDeviceIds.has(entity.device_id))))
|
||||
) {
|
||||
entityIds.add(entity.entity_id);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
areas: [...areaIds],
|
||||
devices: [...new Set([...deviceIds, ...areaDeviceIds])],
|
||||
entities: [...entityIds],
|
||||
missing,
|
||||
};
|
||||
};
|
||||
|
||||
const matchesFilter = (
|
||||
hass: MockHomeAssistant,
|
||||
entityId: string,
|
||||
@@ -124,7 +228,28 @@ export const mockAutomationPlatforms = (hass: MockHomeAssistant) => {
|
||||
);
|
||||
mockForTarget(hass, "get_triggers_for_target", triggerDescriptions);
|
||||
mockForTarget(hass, "get_conditions_for_target", conditionDescriptions);
|
||||
mockExtractFromTarget(hass);
|
||||
hass.mockWS(
|
||||
"extract_from_target",
|
||||
(
|
||||
msg: { target: HassServiceTarget; primary_entities_only?: boolean },
|
||||
currentHass: MockHomeAssistant
|
||||
): ExtractFromTargetResult => {
|
||||
const { areas, devices, entities, missing } = expandTarget(
|
||||
currentHass,
|
||||
msg.target,
|
||||
msg.primary_entities_only ?? true
|
||||
);
|
||||
return {
|
||||
missing_areas: missing.areas,
|
||||
missing_devices: missing.devices,
|
||||
missing_floors: missing.floors,
|
||||
missing_labels: missing.labels,
|
||||
referenced_areas: areas,
|
||||
referenced_devices: devices,
|
||||
referenced_entities: entities,
|
||||
};
|
||||
}
|
||||
);
|
||||
hass.mockWS(
|
||||
"get_services_for_target",
|
||||
(msg: { target: HassServiceTarget }, currentHass: MockHomeAssistant) =>
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import type { CalendarEventApiData } from "../../../src/data/calendar";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
// Sample events relative to today, so demos always show something in the
|
||||
// current week and month.
|
||||
|
||||
const pad = (value: number) => String(value).padStart(2, "0");
|
||||
|
||||
const day = (offset: number): Date => {
|
||||
const date = new Date();
|
||||
date.setHours(0, 0, 0, 0);
|
||||
date.setDate(date.getDate() + offset);
|
||||
|
||||
return date;
|
||||
};
|
||||
|
||||
const isoDate = (date: Date) =>
|
||||
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
|
||||
const at = (offset: number, hours: number, minutes = 0) => {
|
||||
const date = day(offset);
|
||||
date.setHours(hours, minutes);
|
||||
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
const allDay = (offset: number, days = 1) => ({
|
||||
start: isoDate(day(offset)),
|
||||
end: isoDate(day(offset + days)),
|
||||
all_day: true,
|
||||
});
|
||||
|
||||
export const FAMILY_EVENTS: CalendarEventApiData[] = [
|
||||
{ summary: "School run", start: at(0, 8), end: at(0, 8, 30) },
|
||||
{
|
||||
summary: "Dentist",
|
||||
start: at(0, 15, 30),
|
||||
end: at(0, 16, 15),
|
||||
location: "Main Street 12",
|
||||
},
|
||||
{ summary: "Grandma's birthday", ...allDay(1) },
|
||||
{ summary: "Football practice", start: at(2, 17), end: at(2, 18, 30) },
|
||||
{
|
||||
summary: "Dinner with friends",
|
||||
start: at(3, 19),
|
||||
end: at(3, 22),
|
||||
status: "tentative",
|
||||
},
|
||||
{ summary: "Summer cabin", ...allDay(5, 3) },
|
||||
{ summary: "Parent-teacher meeting", start: at(9, 18), end: at(9, 19) },
|
||||
{ summary: "Football practice", start: at(9, 17), end: at(9, 18, 30) },
|
||||
{ summary: "Swimming lessons", start: at(12, 10), end: at(12, 11) },
|
||||
{ summary: "Car service", start: at(-3, 9), end: at(-3, 11) },
|
||||
{ summary: "Book club", start: at(-6, 19), end: at(-6, 21) },
|
||||
];
|
||||
|
||||
export const WORK_EVENTS: CalendarEventApiData[] = [
|
||||
{ summary: "Stand-up", start: at(0, 9), end: at(0, 9, 15) },
|
||||
{ summary: "Sprint planning", start: at(2, 10), end: at(2, 12) },
|
||||
{ summary: "Team lunch", start: at(4, 12), end: at(4, 13) },
|
||||
{ summary: "Conference", ...allDay(8, 2) },
|
||||
{ summary: "Quarterly review", start: at(-2, 14), end: at(-2, 15, 30) },
|
||||
];
|
||||
|
||||
const toTime = (value: CalendarEventApiData["start"]) =>
|
||||
new Date(
|
||||
typeof value === "string"
|
||||
? value
|
||||
: "dateTime" in value
|
||||
? value.dateTime
|
||||
: value.date
|
||||
).getTime();
|
||||
|
||||
const inRange = (
|
||||
events: CalendarEventApiData[],
|
||||
start: string | null,
|
||||
end: string | null
|
||||
): CalendarEventApiData[] => {
|
||||
const startTime = start ? new Date(start).getTime() : -Infinity;
|
||||
const endTime = end ? new Date(end).getTime() : Infinity;
|
||||
|
||||
return events.filter(
|
||||
(event) => toTime(event.end) > startTime && toTime(event.start) < endTime
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Answers event requests of calendar entities with the events of
|
||||
* `getEvents`. A calendar without events gets `null`, like a calendar whose
|
||||
* integration fails to return them.
|
||||
*/
|
||||
export const mockCalendarEvents = (
|
||||
hass: MockHomeAssistant,
|
||||
getEvents: (
|
||||
entityId: string,
|
||||
currentHass: MockHomeAssistant
|
||||
) => CalendarEventApiData[] | null | undefined
|
||||
) => {
|
||||
hass.mockWS("calendar/event/subscribe", (msg, currentHass, onChange) => {
|
||||
const events = getEvents(msg.entity_id, currentHass);
|
||||
onChange?.({ events: events ? inRange(events, msg.start, msg.end) : null });
|
||||
|
||||
return () => undefined;
|
||||
});
|
||||
// Home Assistant before 2026.5 has no event subscription, so cards that
|
||||
// support it ask the REST API instead.
|
||||
hass.mockAPI(/^calendars\/[^/?]+\?/, (currentHass, _method, path) => {
|
||||
const url = new URL(path, location.origin);
|
||||
const entityId = url.pathname.slice(url.pathname.lastIndexOf("/") + 1);
|
||||
const events = getEvents(entityId, currentHass);
|
||||
|
||||
if (!events) {
|
||||
return Promise.reject(new Error("Calendar not found"));
|
||||
}
|
||||
|
||||
return inRange(
|
||||
events,
|
||||
url.searchParams.get("start"),
|
||||
url.searchParams.get("end")
|
||||
);
|
||||
});
|
||||
};
|
||||
+20
-20
@@ -11,7 +11,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.solar_production",
|
||||
state: "88.6",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Solar",
|
||||
unit_of_measurement: "kWh",
|
||||
},
|
||||
@@ -20,7 +20,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.battery_input",
|
||||
state: "4",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Battery Input",
|
||||
unit_of_measurement: "kWh",
|
||||
},
|
||||
@@ -29,7 +29,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.battery_output",
|
||||
state: "3",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Battery Output",
|
||||
unit_of_measurement: "kWh",
|
||||
},
|
||||
@@ -38,7 +38,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_consumption_tarif_1",
|
||||
state: "88.6",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Grid consumption low tariff",
|
||||
unit_of_measurement: "kWh",
|
||||
},
|
||||
@@ -47,7 +47,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_consumption_tarif_2",
|
||||
state: "88.6",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Grid consumption high tariff",
|
||||
unit_of_measurement: "kWh",
|
||||
},
|
||||
@@ -56,7 +56,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_production_tarif_1",
|
||||
state: "88.6",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Returned to grid low tariff",
|
||||
unit_of_measurement: "kWh",
|
||||
},
|
||||
@@ -65,7 +65,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_production_tarif_2",
|
||||
state: "88.6",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Returned to grid high tariff",
|
||||
unit_of_measurement: "kWh",
|
||||
},
|
||||
@@ -74,7 +74,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_consumption_tarif_1_cost",
|
||||
state: "2",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
unit_of_measurement: "EUR",
|
||||
},
|
||||
},
|
||||
@@ -82,7 +82,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_consumption_tarif_2_cost",
|
||||
state: "2",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
unit_of_measurement: "EUR",
|
||||
},
|
||||
},
|
||||
@@ -90,7 +90,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_production_tarif_1_compensation",
|
||||
state: "2",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
unit_of_measurement: "EUR",
|
||||
},
|
||||
},
|
||||
@@ -98,7 +98,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_production_tarif_2_compensation",
|
||||
state: "2",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
unit_of_measurement: "EUR",
|
||||
},
|
||||
},
|
||||
@@ -138,7 +138,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_gas_cost",
|
||||
state: "2",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
unit_of_measurement: "EUR",
|
||||
},
|
||||
},
|
||||
@@ -146,7 +146,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_gas",
|
||||
state: "4",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Gas",
|
||||
unit_of_measurement: "m³",
|
||||
},
|
||||
@@ -155,7 +155,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_water",
|
||||
state: "4000",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Water",
|
||||
unit_of_measurement: "L",
|
||||
},
|
||||
@@ -164,7 +164,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_car",
|
||||
state: "4",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Electric car",
|
||||
unit_of_measurement: "kWh",
|
||||
},
|
||||
@@ -173,7 +173,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_ac",
|
||||
state: "3",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Air conditioning",
|
||||
unit_of_measurement: "kWh",
|
||||
},
|
||||
@@ -182,7 +182,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_washing_machine",
|
||||
state: "6",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Washing machine",
|
||||
unit_of_measurement: "kWh",
|
||||
},
|
||||
@@ -191,7 +191,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_dryer",
|
||||
state: "5.5",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Dryer",
|
||||
unit_of_measurement: "kWh",
|
||||
},
|
||||
@@ -200,7 +200,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_heat_pump",
|
||||
state: "6",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Heat pump",
|
||||
unit_of_measurement: "kWh",
|
||||
},
|
||||
@@ -209,7 +209,7 @@ export const energyEntities = () =>
|
||||
entity_id: "sensor.energy_boiler",
|
||||
state: "7",
|
||||
attributes: {
|
||||
state_class: "total_increasing",
|
||||
last_reset: "1970-01-01T00:00:00:00+00",
|
||||
friendly_name: "Boiler",
|
||||
unit_of_measurement: "kWh",
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
let entries: EntityRegistryEntry[] = [];
|
||||
|
||||
export const newEntry = (
|
||||
const newEntry = (
|
||||
entry: Pick<EntityRegistryEntry, "entity_id" | "platform" | "unique_id">
|
||||
): EntityRegistryEntry => {
|
||||
const now = Date.now() / 1000;
|
||||
@@ -43,9 +43,7 @@ const displayFields = (entry: EntityRegistryEntry) => ({
|
||||
hidden: entry.hidden_by !== null,
|
||||
});
|
||||
|
||||
export const extEntry = (
|
||||
entry: EntityRegistryEntry
|
||||
): ExtEntityRegistryEntry => ({
|
||||
const extEntry = (entry: EntityRegistryEntry): ExtEntityRegistryEntry => ({
|
||||
...entry,
|
||||
capabilities: {},
|
||||
aliases: [],
|
||||
@@ -58,16 +56,6 @@ export const mockEntityRegistry = (
|
||||
entries = [...data];
|
||||
// A new array, so subscribers see the registry changed after an update
|
||||
hass.mockWS("config/entity_registry/list", () => [...entries]);
|
||||
hass.mockWS(
|
||||
"config/entity_registry/get",
|
||||
(msg: { entity_id: string }): ExtEntityRegistryEntry => {
|
||||
const entry = entries.find((e) => e.entity_id === msg.entity_id);
|
||||
if (!entry) {
|
||||
throw new Error(`Entity not found: ${msg.entity_id}`);
|
||||
}
|
||||
return extEntry(entry);
|
||||
}
|
||||
);
|
||||
hass.mockWS(
|
||||
"config/entity_registry/get_entries",
|
||||
(msg: { entity_ids: string[] }) => {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { LogbookStreamMessage } from "../../../src/data/logbook";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
/**
|
||||
* Answers logbook subscriptions with an empty logbook. No entries come from
|
||||
* an automation or script trace.
|
||||
*/
|
||||
export const mockLogbook = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("logbook/event_stream", (_msg, _currentHass, onChange) => {
|
||||
onChange?.({ events: [] } satisfies LogbookStreamMessage);
|
||||
|
||||
return () => undefined;
|
||||
});
|
||||
hass.mockWS("trace/contexts", () => ({}));
|
||||
};
|
||||
@@ -149,6 +149,7 @@ const repositories: RepositoryBase[] = [
|
||||
const repositoryInfo = (repository: RepositoryBase): RepositoryInfo => ({
|
||||
...repository,
|
||||
additional_info: `# ${repository.name}\n\n${repository.description}\n\nThis is a demo. Installing is not available.`,
|
||||
beta: false,
|
||||
default_branch: "main",
|
||||
issues: 12,
|
||||
releases: [repository.available_version],
|
||||
|
||||
@@ -372,9 +372,7 @@ export const mockRecorder = (mockHass: MockHomeAssistant) => {
|
||||
const entityState = hass.states[id];
|
||||
const state = entityState ? Number(entityState.state) : 1;
|
||||
statistics[id] =
|
||||
entityState &&
|
||||
(entityState.attributes.state_class === "total" ||
|
||||
entityState.attributes.state_class === "total_increasing")
|
||||
entityState && "last_reset" in entityState.attributes
|
||||
? generateSumStatistics(
|
||||
start,
|
||||
end,
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../../../src/common/array/ensure-array";
|
||||
import type { ExtractFromTargetResult } from "../../../src/data/target";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
import { getLabelIds } from "./label_registry";
|
||||
|
||||
// Expands a target to its areas, devices and entities, like core does
|
||||
export const expandTarget = (
|
||||
hass: MockHomeAssistant,
|
||||
target: HassServiceTarget,
|
||||
primaryEntitiesOnly = true
|
||||
) => {
|
||||
const missing = {
|
||||
floors: [] as string[],
|
||||
areas: [] as string[],
|
||||
devices: [] as string[],
|
||||
labels: [] as string[],
|
||||
};
|
||||
const knownLabelIds = getLabelIds();
|
||||
const floorIds = ensureArray(target.floor_id ?? []);
|
||||
const labelIds = ensureArray(target.label_id ?? []);
|
||||
missing.floors.push(...floorIds.filter((id) => !(id in hass.floors)));
|
||||
// Like core, unknown labels are reported, but still expanded
|
||||
missing.labels.push(...labelIds.filter((id) => !knownLabelIds.includes(id)));
|
||||
const hasLabel = (labels?: string[]) =>
|
||||
labelIds.some((labelId) => labels?.includes(labelId));
|
||||
|
||||
// Unknown areas and devices are reported, but still referenced
|
||||
const areaIds = new Set(ensureArray(target.area_id ?? []));
|
||||
missing.areas.push(...[...areaIds].filter((id) => !(id in hass.areas)));
|
||||
for (const area of Object.values(hass.areas)) {
|
||||
if (
|
||||
(area.floor_id && floorIds.includes(area.floor_id)) ||
|
||||
hasLabel(area.labels)
|
||||
) {
|
||||
areaIds.add(area.area_id);
|
||||
}
|
||||
}
|
||||
|
||||
// A targeted or labeled device includes its child devices
|
||||
const deviceIds = new Set<string>();
|
||||
const addDevice = (deviceId: string) => {
|
||||
deviceIds.add(deviceId);
|
||||
for (const device of Object.values(hass.devices)) {
|
||||
if (device.parent_device_id === deviceId) {
|
||||
deviceIds.add(device.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const deviceId of ensureArray(target.device_id ?? [])) {
|
||||
if (!(deviceId in hass.devices)) {
|
||||
missing.devices.push(deviceId);
|
||||
}
|
||||
addDevice(deviceId);
|
||||
}
|
||||
for (const device of Object.values(hass.devices)) {
|
||||
if (hasLabel(device.labels)) {
|
||||
addDevice(device.id);
|
||||
}
|
||||
}
|
||||
|
||||
// A child device without its own area is in the area of its parent device
|
||||
const deviceArea = (deviceId: string) => {
|
||||
const device = hass.devices[deviceId];
|
||||
return (
|
||||
device?.area_id ??
|
||||
(device?.parent_device_id
|
||||
? hass.devices[device.parent_device_id]?.area_id
|
||||
: undefined)
|
||||
);
|
||||
};
|
||||
const areaDeviceIds = new Set<string>();
|
||||
for (const device of Object.values(hass.devices)) {
|
||||
const areaId = deviceArea(device.id);
|
||||
if (areaId && areaIds.has(areaId)) {
|
||||
areaDeviceIds.add(device.id);
|
||||
}
|
||||
}
|
||||
|
||||
const entityIds = new Set(ensureArray(target.entity_id ?? []));
|
||||
for (const entity of Object.values(hass.entities)) {
|
||||
// The display registry keeps the entities of a previous demo, and like
|
||||
// core, hidden entities are never included indirectly
|
||||
if (!(entity.entity_id in hass.states) || entity.hidden) {
|
||||
continue;
|
||||
}
|
||||
const primary = !primaryEntitiesOnly || !entity.entity_category;
|
||||
if (
|
||||
hasLabel(entity.labels) ||
|
||||
(primary &&
|
||||
((entity.device_id && deviceIds.has(entity.device_id)) ||
|
||||
(entity.area_id && areaIds.has(entity.area_id)) ||
|
||||
// An entity with its own area is not in the area of its device
|
||||
(!entity.area_id &&
|
||||
entity.device_id &&
|
||||
areaDeviceIds.has(entity.device_id))))
|
||||
) {
|
||||
entityIds.add(entity.entity_id);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
areas: [...areaIds],
|
||||
devices: [...new Set([...deviceIds, ...areaDeviceIds])],
|
||||
entities: [...entityIds],
|
||||
missing,
|
||||
};
|
||||
};
|
||||
|
||||
export const mockExtractFromTarget = (hass: MockHomeAssistant) =>
|
||||
hass.mockWS(
|
||||
"extract_from_target",
|
||||
(
|
||||
msg: { target: HassServiceTarget; primary_entities_only?: boolean },
|
||||
currentHass: MockHomeAssistant
|
||||
): ExtractFromTargetResult => {
|
||||
const { areas, devices, entities, missing } = expandTarget(
|
||||
currentHass,
|
||||
msg.target,
|
||||
msg.primary_entities_only ?? true
|
||||
);
|
||||
return {
|
||||
missing_areas: missing.areas,
|
||||
missing_devices: missing.devices,
|
||||
missing_floors: missing.floors,
|
||||
missing_labels: missing.labels,
|
||||
referenced_areas: areas,
|
||||
referenced_devices: devices,
|
||||
referenced_entities: entities,
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -1,133 +0,0 @@
|
||||
import type {
|
||||
ForecastAttribute,
|
||||
ModernForecastType,
|
||||
} from "../../../src/data/weather";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
const HOUR = 3600000;
|
||||
|
||||
const CONDITIONS = [
|
||||
"partlycloudy",
|
||||
"rainy",
|
||||
"cloudy",
|
||||
"sunny",
|
||||
"pouring",
|
||||
"partlycloudy",
|
||||
"sunny",
|
||||
];
|
||||
|
||||
// Daily changes around the current temperature, so the forecast looks real.
|
||||
const TEMPERATURE_CHANGES = [0, -1.5, -3, -1, 1.5, 2.5, 1];
|
||||
|
||||
const PRECIPITATION = new Map([
|
||||
["rainy", 2.4],
|
||||
["pouring", 9.1],
|
||||
]);
|
||||
|
||||
const forecastEntry = (
|
||||
datetime: Date,
|
||||
temperature: number,
|
||||
condition: string
|
||||
): ForecastAttribute => ({
|
||||
datetime: datetime.toISOString(),
|
||||
condition,
|
||||
temperature: Math.round(temperature * 10) / 10,
|
||||
precipitation: PRECIPITATION.get(condition) ?? 0,
|
||||
precipitation_probability: PRECIPITATION.has(condition) ? 80 : 10,
|
||||
});
|
||||
|
||||
const generateForecast = (
|
||||
forecastType: ModernForecastType,
|
||||
temperature: number,
|
||||
condition: string
|
||||
): ForecastAttribute[] => {
|
||||
const conditions = [condition, ...CONDITIONS.slice(1)];
|
||||
const now = new Date();
|
||||
|
||||
if (forecastType === "hourly") {
|
||||
now.setMinutes(0, 0, 0);
|
||||
|
||||
return Array.from({ length: 24 }, (_, hour) => {
|
||||
const datetime = new Date(now.getTime() + (hour + 1) * HOUR);
|
||||
|
||||
// Warmest mid-afternoon and coldest before dawn
|
||||
const change =
|
||||
3 * Math.sin(((datetime.getHours() - 9) / 24) * 2 * Math.PI) -
|
||||
3 * Math.sin(((now.getHours() - 9) / 24) * 2 * Math.PI);
|
||||
|
||||
return forecastEntry(
|
||||
datetime,
|
||||
temperature + change,
|
||||
conditions[Math.floor(hour / 6)]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
now.setHours(12, 0, 0, 0);
|
||||
|
||||
if (forecastType === "twice_daily") {
|
||||
return conditions.flatMap((dayCondition, day) => {
|
||||
const datetime = new Date(now.getTime() + day * 24 * HOUR);
|
||||
const dayTemperature = temperature + TEMPERATURE_CHANGES[day];
|
||||
|
||||
return [
|
||||
{
|
||||
...forecastEntry(datetime, dayTemperature, dayCondition),
|
||||
is_daytime: true,
|
||||
},
|
||||
{
|
||||
...forecastEntry(
|
||||
new Date(datetime.getTime() + 12 * HOUR),
|
||||
dayTemperature - 6,
|
||||
dayCondition
|
||||
),
|
||||
is_daytime: false,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
return conditions.map((dayCondition, day) => {
|
||||
const dayTemperature = temperature + TEMPERATURE_CHANGES[day];
|
||||
|
||||
return {
|
||||
...forecastEntry(
|
||||
new Date(now.getTime() + day * 24 * HOUR),
|
||||
dayTemperature,
|
||||
dayCondition
|
||||
),
|
||||
templow: Math.round((dayTemperature - 6) * 10) / 10,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Answers forecast subscriptions of weather entities. An entity with a
|
||||
* `forecast` attribute gets that forecast. Other entities get a forecast made
|
||||
* up from their condition and temperature.
|
||||
*/
|
||||
export const mockWeather = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS(
|
||||
"weather/subscribe_forecast",
|
||||
(
|
||||
msg: { entity_id: string; forecast_type: ModernForecastType },
|
||||
currentHass,
|
||||
onChange
|
||||
) => {
|
||||
const stateObj = currentHass.states[msg.entity_id];
|
||||
onChange?.({
|
||||
type: msg.forecast_type,
|
||||
forecast: stateObj
|
||||
? (stateObj.attributes.forecast ??
|
||||
generateForecast(
|
||||
msg.forecast_type,
|
||||
Number(stateObj.attributes.temperature ?? 15),
|
||||
stateObj.state
|
||||
))
|
||||
: null,
|
||||
});
|
||||
|
||||
return () => undefined;
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,14 +1,68 @@
|
||||
import {
|
||||
FAMILY_EVENTS,
|
||||
mockCalendarEvents,
|
||||
WORK_EVENTS,
|
||||
} from "../../../demo/src/stubs/calendar";
|
||||
import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
|
||||
import type { CalendarEventApiData } from "../../../src/data/calendar";
|
||||
import { CalendarEntityFeature } from "../../../src/data/calendar";
|
||||
import type { EntityRegistryEntry } from "../../../src/data/entity/entity_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
// Sample calendars with events relative to today, so demos always show
|
||||
// something in the current week and month.
|
||||
|
||||
const pad = (value: number) => String(value).padStart(2, "0");
|
||||
|
||||
const day = (offset: number): Date => {
|
||||
const date = new Date();
|
||||
date.setHours(0, 0, 0, 0);
|
||||
date.setDate(date.getDate() + offset);
|
||||
return date;
|
||||
};
|
||||
|
||||
const isoDate = (date: Date) =>
|
||||
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
|
||||
const at = (offset: number, hours: number, minutes = 0) => {
|
||||
const date = day(offset);
|
||||
date.setHours(hours, minutes);
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
const allDay = (offset: number, days = 1) => ({
|
||||
start: isoDate(day(offset)),
|
||||
end: isoDate(day(offset + days)),
|
||||
all_day: true,
|
||||
});
|
||||
|
||||
const FAMILY_EVENTS: CalendarEventApiData[] = [
|
||||
{ summary: "School run", start: at(0, 8), end: at(0, 8, 30) },
|
||||
{
|
||||
summary: "Dentist",
|
||||
start: at(0, 15, 30),
|
||||
end: at(0, 16, 15),
|
||||
location: "Main Street 12",
|
||||
},
|
||||
{ summary: "Grandma's birthday", ...allDay(1) },
|
||||
{ summary: "Football practice", start: at(2, 17), end: at(2, 18, 30) },
|
||||
{
|
||||
summary: "Dinner with friends",
|
||||
start: at(3, 19),
|
||||
end: at(3, 22),
|
||||
status: "tentative",
|
||||
},
|
||||
{ summary: "Summer cabin", ...allDay(5, 3) },
|
||||
{ summary: "Parent-teacher meeting", start: at(9, 18), end: at(9, 19) },
|
||||
{ summary: "Football practice", start: at(9, 17), end: at(9, 18, 30) },
|
||||
{ summary: "Swimming lessons", start: at(12, 10), end: at(12, 11) },
|
||||
{ summary: "Car service", start: at(-3, 9), end: at(-3, 11) },
|
||||
{ summary: "Book club", start: at(-6, 19), end: at(-6, 21) },
|
||||
];
|
||||
|
||||
const WORK_EVENTS: CalendarEventApiData[] = [
|
||||
{ summary: "Stand-up", start: at(0, 9), end: at(0, 9, 15) },
|
||||
{ summary: "Sprint planning", start: at(2, 10), end: at(2, 12) },
|
||||
{ summary: "Team lunch", start: at(4, 12), end: at(4, 13) },
|
||||
{ summary: "Conference", ...allDay(8, 2) },
|
||||
{ summary: "Quarterly review", start: at(-2, 14), end: at(-2, 15, 30) },
|
||||
];
|
||||
|
||||
const EVENTS: Record<string, CalendarEventApiData[] | null> = {
|
||||
"calendar.family": FAMILY_EVENTS,
|
||||
"calendar.work": WORK_EVENTS,
|
||||
@@ -25,8 +79,8 @@ export const CALENDAR_ENTITIES = [
|
||||
friendly_name: "Family",
|
||||
message: "School run",
|
||||
all_day: false,
|
||||
start_time: FAMILY_EVENTS[0].start,
|
||||
end_time: FAMILY_EVENTS[0].end,
|
||||
start_time: at(0, 8),
|
||||
end_time: at(0, 8, 30),
|
||||
location: "",
|
||||
description: "",
|
||||
supported_features:
|
||||
@@ -42,8 +96,8 @@ export const CALENDAR_ENTITIES = [
|
||||
friendly_name: "Work",
|
||||
message: "Stand-up",
|
||||
all_day: false,
|
||||
start_time: WORK_EVENTS[0].start,
|
||||
end_time: WORK_EVENTS[0].end,
|
||||
start_time: at(0, 9),
|
||||
end_time: at(0, 9, 15),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -91,7 +145,20 @@ export const mockCalendars = (hass: MockHomeAssistant) => {
|
||||
registryEntry("calendar.empty"),
|
||||
registryEntry("calendar.holidays"),
|
||||
]);
|
||||
mockCalendarEvents(hass, (entityId) => EVENTS[entityId]);
|
||||
hass.mockWS("calendar/event/subscribe", (msg, _hass, onChange) => {
|
||||
const start = new Date(msg.start).getTime();
|
||||
const end = new Date(msg.end).getTime();
|
||||
const events = EVENTS[msg.entity_id];
|
||||
onChange?.({
|
||||
events:
|
||||
events?.filter(
|
||||
(event) =>
|
||||
new Date(event.end as string).getTime() > start &&
|
||||
new Date(event.start as string).getTime() < end
|
||||
) ?? null,
|
||||
});
|
||||
return () => undefined;
|
||||
});
|
||||
// Changes made in the event dialogs are accepted but not stored
|
||||
hass.mockWS("calendar/event/create", () => null);
|
||||
hass.mockWS("calendar/event/update", () => null);
|
||||
|
||||
+11
-11
@@ -54,17 +54,17 @@
|
||||
"@codemirror/view": "6.43.15",
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.9.0",
|
||||
"@formatjs/intl-displaynames": "7.4.0",
|
||||
"@formatjs/intl-durationformat": "0.12.0",
|
||||
"@formatjs/intl-getcanonicallocales": "3.3.0",
|
||||
"@formatjs/intl-listformat": "8.4.0",
|
||||
"@formatjs/intl-locale": "5.4.1",
|
||||
"@formatjs/intl-numberformat": "9.5.0",
|
||||
"@formatjs/intl-pluralrules": "6.4.0",
|
||||
"@formatjs/intl-relativetimeformat": "12.4.0",
|
||||
"@formatjs/intl-datetimeformat": "7.8.2",
|
||||
"@formatjs/intl-displaynames": "7.3.15",
|
||||
"@formatjs/intl-durationformat": "0.11.1",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.12",
|
||||
"@formatjs/intl-listformat": "8.3.15",
|
||||
"@formatjs/intl-locale": "5.3.12",
|
||||
"@formatjs/intl-numberformat": "9.4.3",
|
||||
"@formatjs/intl-pluralrules": "6.3.16",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.16",
|
||||
"@home-assistant/webawesome": "3.14.0-ha.2",
|
||||
"@lezer/common": "1.5.5",
|
||||
"@lezer/common": "1.5.4",
|
||||
"@lezer/highlight": "1.2.5",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -177,7 +177,7 @@
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"core-js-compat": "3.51.0",
|
||||
"del": "9.0.1",
|
||||
"eslint": "10.14.0",
|
||||
"eslint": "10.13.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import-x": "4.17.1",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
|
||||
index cb433ff25353f4836d2ffc813356f945f42acc6b..0a86ad22d05390a5b9ef002905fcd5926852b956 100644
|
||||
index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa526090a00 100644
|
||||
--- a/modular/sortable.core.esm.js
|
||||
+++ b/modular/sortable.core.esm.js
|
||||
@@ -1777,11 +1777,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||
@@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||
}
|
||||
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
|
||||
capture();
|
||||
@@ -24,7 +24,7 @@ index cb433ff25353f4836d2ffc813356f945f42acc6b..0a86ad22d05390a5b9ef002905fcd592
|
||||
}
|
||||
parentEl = el; // actualization
|
||||
|
||||
@@ -1798,7 +1803,12 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||
@@ -1802,7 +1807,12 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||
targetRect = getRect(target);
|
||||
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) {
|
||||
capture();
|
||||
@@ -38,16 +38,7 @@ index cb433ff25353f4836d2ffc813356f945f42acc6b..0a86ad22d05390a5b9ef002905fcd592
|
||||
parentEl = el; // actualization
|
||||
|
||||
changed();
|
||||
@@ -1818,7 +1828,7 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||
pastFirstInvertThresh = false;
|
||||
isCircumstantialInvert = !differentRowCol && options.invertSwap || differentLevel;
|
||||
}
|
||||
- direction = _getSwapDirection(evt, target, targetRect, vertical, differentRowCol ? 1 : options.swapThreshold, options.invertedSwapThreshold == null ? options.swapThreshold : options.invertedSwapThreshold, isCircumstantialInvert, lastTarget === target);
|
||||
+ direction = _getSwapDirection(evt, target, targetRect, vertical, differentRowCol ? 1 : options.swapThreshold, options.invertedSwapThreshold == null ? options.swapThreshold : options.invertedSwapThreshold, isCircumstantialInvert, lastTarget === target, _isRtl(el, vertical));
|
||||
var sibling;
|
||||
if (direction !== 0) {
|
||||
// Check if target is beside dragEl in respective direction (ignoring hidden elements)
|
||||
@@ -1845,10 +1855,15 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||
@@ -1849,10 +1859,15 @@ Sortable.prototype = /** @lends Sortable.prototype */{
|
||||
_silent = true;
|
||||
setTimeout(_unsilent, 30);
|
||||
capture();
|
||||
@@ -67,53 +58,3 @@ index cb433ff25353f4836d2ffc813356f945f42acc6b..0a86ad22d05390a5b9ef002905fcd592
|
||||
}
|
||||
|
||||
// Undo chrome's scroll adjustment (has no effect on other browsers)
|
||||
@@ -2242,19 +2257,48 @@ function _disableDraggable(el) {
|
||||
function _unsilent() {
|
||||
_silent = false;
|
||||
}
|
||||
+// Horizontal RTL lists run from right to left. Mirroring the x axis lets the
|
||||
+// checks below keep assuming that the list runs from left to right.
|
||||
+function _isRtl(el, vertical) {
|
||||
+ return !vertical && css(el, 'direction') === 'rtl';
|
||||
+}
|
||||
+function _mirrorX(rect) {
|
||||
+ return {
|
||||
+ left: -rect.right,
|
||||
+ right: -rect.left,
|
||||
+ top: rect.top,
|
||||
+ bottom: rect.bottom,
|
||||
+ width: rect.width,
|
||||
+ height: rect.height
|
||||
+ };
|
||||
+}
|
||||
function _ghostIsFirst(evt, vertical, sortable) {
|
||||
var firstElRect = getRect(getChild(sortable.el, 0, sortable.options, true));
|
||||
var childContainingRect = getChildContainingRectFromElement(sortable.el, sortable.options, ghostEl);
|
||||
+ if (_isRtl(sortable.el, vertical)) {
|
||||
+ evt = { clientX: -evt.clientX, clientY: evt.clientY };
|
||||
+ firstElRect = _mirrorX(firstElRect);
|
||||
+ childContainingRect = _mirrorX(childContainingRect);
|
||||
+ }
|
||||
var spacer = 10;
|
||||
return vertical ? evt.clientX < childContainingRect.left - spacer || evt.clientY < firstElRect.top && evt.clientX < firstElRect.right : evt.clientY < childContainingRect.top - spacer || evt.clientY < firstElRect.bottom && evt.clientX < firstElRect.left;
|
||||
}
|
||||
function _ghostIsLast(evt, vertical, sortable) {
|
||||
var lastElRect = getRect(lastChild(sortable.el, sortable.options.draggable));
|
||||
var childContainingRect = getChildContainingRectFromElement(sortable.el, sortable.options, ghostEl);
|
||||
+ if (_isRtl(sortable.el, vertical)) {
|
||||
+ evt = { clientX: -evt.clientX, clientY: evt.clientY };
|
||||
+ lastElRect = _mirrorX(lastElRect);
|
||||
+ childContainingRect = _mirrorX(childContainingRect);
|
||||
+ }
|
||||
var spacer = 10;
|
||||
return vertical ? evt.clientX > childContainingRect.right + spacer || evt.clientY > lastElRect.bottom && evt.clientX > lastElRect.left : evt.clientY > childContainingRect.bottom + spacer || evt.clientX > lastElRect.right && evt.clientY > lastElRect.top;
|
||||
}
|
||||
-function _getSwapDirection(evt, target, targetRect, vertical, swapThreshold, invertedSwapThreshold, invertSwap, isLastTarget) {
|
||||
+function _getSwapDirection(evt, target, targetRect, vertical, swapThreshold, invertedSwapThreshold, invertSwap, isLastTarget, rtl) {
|
||||
+ if (rtl) {
|
||||
+ evt = { clientX: -evt.clientX, clientY: evt.clientY };
|
||||
+ targetRect = _mirrorX(targetRect);
|
||||
+ }
|
||||
var mouseOnAxis = vertical ? evt.clientY : evt.clientX,
|
||||
targetLength = vertical ? targetRect.height : targetRect.width,
|
||||
targetS1 = vertical ? targetRect.top : targetRect.left,
|
||||
Generated
+194
-194
@@ -167,12 +167,12 @@ overrides:
|
||||
lit-html: 3.3.3
|
||||
clean-css: 5.3.3
|
||||
'@lit/reactive-element': 2.1.2
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
patchedDependencies:
|
||||
'@material/[email protected]': 9450e509a86887d8f68ca9cf3f81e6ed70e94e49de13c5f6f20f94519ad4a987
|
||||
[email protected]: 4c014bd1952b4d1f21311ed2212dc87192e70b6e782597ecc7a485270247bbb5
|
||||
[email protected]: f25f02dcc8720341c3cfac312e0502081328108270852ad039187c2c5e2ea432
|
||||
[email protected]: f2ec2ae6877dfc201c542b4f9ded1d03831258bf328ae7bba876d14ce4508a06
|
||||
[email protected]: cdc7ece96d53357d38ff3c124a2553f86e3a5dc333d641d223eb6f07e28275f0
|
||||
[email protected]: 75e6346ad8d8af554c167f77ac7d9b9652f010398efdc6b399925a4795133099
|
||||
|
||||
@@ -223,38 +223,38 @@ importers:
|
||||
specifier: 2.0.17
|
||||
version: 2.0.17
|
||||
'@formatjs/intl-datetimeformat':
|
||||
specifier: 7.9.0
|
||||
version: 7.9.0
|
||||
specifier: 7.8.2
|
||||
version: 7.8.2
|
||||
'@formatjs/intl-displaynames':
|
||||
specifier: 7.4.0
|
||||
version: 7.4.0
|
||||
specifier: 7.3.15
|
||||
version: 7.3.15
|
||||
'@formatjs/intl-durationformat':
|
||||
specifier: 0.12.0
|
||||
version: 0.12.0
|
||||
specifier: 0.11.1
|
||||
version: 0.11.1
|
||||
'@formatjs/intl-getcanonicallocales':
|
||||
specifier: 3.3.0
|
||||
version: 3.3.0
|
||||
specifier: 3.2.12
|
||||
version: 3.2.12
|
||||
'@formatjs/intl-listformat':
|
||||
specifier: 8.4.0
|
||||
version: 8.4.0
|
||||
specifier: 8.3.15
|
||||
version: 8.3.15
|
||||
'@formatjs/intl-locale':
|
||||
specifier: 5.4.1
|
||||
version: 5.4.1
|
||||
specifier: 5.3.12
|
||||
version: 5.3.12
|
||||
'@formatjs/intl-numberformat':
|
||||
specifier: 9.5.0
|
||||
version: 9.5.0
|
||||
specifier: 9.4.3
|
||||
version: 9.4.3
|
||||
'@formatjs/intl-pluralrules':
|
||||
specifier: 6.4.0
|
||||
version: 6.4.0
|
||||
specifier: 6.3.16
|
||||
version: 6.3.16
|
||||
'@formatjs/intl-relativetimeformat':
|
||||
specifier: 12.4.0
|
||||
version: 12.4.0
|
||||
specifier: 12.3.16
|
||||
version: 12.3.16
|
||||
'@home-assistant/webawesome':
|
||||
specifier: 3.14.0-ha.2
|
||||
version: 3.14.0-ha.2(@floating-ui/[email protected])(@types/[email protected])
|
||||
'@lezer/common':
|
||||
specifier: 1.5.5
|
||||
version: 1.5.5
|
||||
specifier: 1.5.4
|
||||
version: 1.5.4
|
||||
'@lezer/highlight':
|
||||
specifier: 1.2.5
|
||||
version: 1.2.5
|
||||
@@ -422,7 +422,7 @@ importers:
|
||||
version: 2.8.1
|
||||
sortablejs:
|
||||
specifier: 1.15.7
|
||||
version: 1.15.7(patch_hash=f25f02dcc8720341c3cfac312e0502081328108270852ad039187c2c5e2ea432)
|
||||
version: 1.15.7(patch_hash=f2ec2ae6877dfc201c542b4f9ded1d03831258bf328ae7bba876d14ce4508a06)
|
||||
stacktrace-js:
|
||||
specifier: 2.0.2
|
||||
version: 2.0.2
|
||||
@@ -492,13 +492,13 @@ importers:
|
||||
version: 4.22.3([email protected])
|
||||
'@eslint/js':
|
||||
specifier: 10.0.1
|
||||
version: 10.0.1([email protected]4.0([email protected]))
|
||||
version: 10.0.1([email protected]3.0([email protected]))
|
||||
'@gfx/zopfli':
|
||||
specifier: 1.0.15
|
||||
version: 1.0.15
|
||||
'@html-eslint/eslint-plugin':
|
||||
specifier: 0.67.0
|
||||
version: 0.67.0([email protected]4.0([email protected]))
|
||||
version: 0.67.0([email protected]3.0([email protected]))
|
||||
'@lokalise/node-api':
|
||||
specifier: 17.0.1
|
||||
version: 17.0.1
|
||||
@@ -587,29 +587,29 @@ importers:
|
||||
specifier: 9.0.1
|
||||
version: 9.0.1
|
||||
eslint:
|
||||
specifier: 10.14.0
|
||||
version: 10.14.0([email protected])
|
||||
specifier: 10.13.0
|
||||
version: 10.13.0([email protected])
|
||||
eslint-config-prettier:
|
||||
specifier: 10.1.8
|
||||
version: 10.1.8([email protected]4.0([email protected]))
|
||||
version: 10.1.8([email protected]3.0([email protected]))
|
||||
eslint-import-resolver-webpack:
|
||||
specifier: 0.13.11
|
||||
version: 0.13.11([email protected])
|
||||
eslint-plugin-import-x:
|
||||
specifier: 4.17.1
|
||||
version: 4.17.1(@typescript-eslint/[email protected]([email protected]4.0([email protected]))([email protected])([email protected]))([email protected]4.0([email protected]))([email protected])
|
||||
version: 4.17.1(@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))([email protected])
|
||||
eslint-plugin-lit:
|
||||
specifier: 2.3.1
|
||||
version: 2.3.1([email protected]4.0([email protected]))
|
||||
version: 2.3.1([email protected]3.0([email protected]))
|
||||
eslint-plugin-lit-a11y:
|
||||
specifier: 5.1.3
|
||||
version: 5.1.3([email protected]4.0([email protected]))
|
||||
version: 5.1.3([email protected]3.0([email protected]))
|
||||
eslint-plugin-unused-imports:
|
||||
specifier: 4.4.1
|
||||
version: 4.4.1(@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]4.0([email protected]))([email protected])([email protected]))([email protected]4.0([email protected]))([email protected])([email protected]))([email protected]4.0([email protected]))
|
||||
version: 4.4.1(@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))
|
||||
eslint-plugin-wc:
|
||||
specifier: 3.1.0
|
||||
version: 3.1.0([email protected]4.0([email protected]))
|
||||
version: 3.1.0([email protected]3.0([email protected]))
|
||||
fancy-log:
|
||||
specifier: 2.0.0
|
||||
version: 2.0.0
|
||||
@@ -699,7 +699,7 @@ importers:
|
||||
version: 6.0.3
|
||||
typescript-eslint:
|
||||
specifier: 8.72.1
|
||||
version: 8.72.1([email protected]4.0([email protected]))([email protected])([email protected])
|
||||
version: 8.72.1([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
vite:
|
||||
specifier: 8.3.5
|
||||
version: 8.3.5(@types/[email protected])([email protected])([email protected])
|
||||
@@ -1953,41 +1953,41 @@ packages:
|
||||
'@formatjs/[email protected]':
|
||||
resolution: {integrity: sha512-ca7tZMLpfHX3sUD7hP6gE/qr6Iq89EDd3ZLeRhbsB7r0UD55DGJq+tmFLzrGrnx5StahUevQmQQVtbDRHsfDHA==}
|
||||
|
||||
'@formatjs/intl-datetimeformat@7.9.0':
|
||||
resolution: {integrity: sha512-C17ilxM6zJO1wAYDwoW4E0Oy8K6krEC7MjcZAArW/wEthBb3Lab0LBTh0G7liLOGEj74iCTWm2TYHtLv+SbtYg==}
|
||||
'@formatjs/intl-datetimeformat@7.8.2':
|
||||
resolution: {integrity: sha512-5iCy2mXr6cpEd4N87HLVbN2zRz71dRLMmI8hMY9IgAVaob5fsVocEah6T4w3pPUVYz/aDO2TFGXErPY3JAEUsQ==}
|
||||
|
||||
'@formatjs/intl-displaynames@7.4.0':
|
||||
resolution: {integrity: sha512-D2SZXvkeMadjGeg1vlqcqtNg0hJIKxUL5HiFkSdCt8pmKH5sNMP0DKND+X1cfuOV0EMyKIp04WYRSIHFFVYAfA==}
|
||||
'@formatjs/intl-displaynames@7.3.15':
|
||||
resolution: {integrity: sha512-U26iRwxxnD4aZY+aDiSEV5Ym/7Db5S9OxKQ4c3T+WFFIj+DPKEQLh+tC++dMH1SuEa8CSWvPJkpaADa+WHn3qA==}
|
||||
|
||||
'@formatjs/[email protected]2.0':
|
||||
resolution: {integrity: sha512-5TJSDf5ti5+Zdo/rE6tgyl06Wv4MJKGZqvxxmb3i3uIVtRXYzDgEaiZOCcOjh+UoxIMBHqBn1GhKooep8Wq0KQ==}
|
||||
'@formatjs/[email protected]1.1':
|
||||
resolution: {integrity: sha512-Mkuc+Q8/96Hb0w4Pwa9sqkKVQVl75TCaqaXIXHgsVsLDko+FKQhw9n/7BnbonueqPxkQkGYvn8/G2IKzT970og==}
|
||||
|
||||
'@formatjs/intl-getcanonicallocales@3.3.0':
|
||||
resolution: {integrity: sha512-ndj9jw/LHAftSe34D/A6+lj3Y7GHVfPEZ6FZP2wjd8/beYeh3d1sjlSpZSBlzegSKS3U3mUEIzLCpI0+jKNT6w==}
|
||||
'@formatjs/intl-getcanonicallocales@3.2.12':
|
||||
resolution: {integrity: sha512-THqQLTJUH2z2A7oE1guW8VzpGpsGFxwWZ+l8cI8Bk75xUU8DxtJoLfKl+5a6fYK26GeDWDWE0UbDhE5SwcLoFg==}
|
||||
|
||||
'@formatjs/intl-listformat@8.4.0':
|
||||
resolution: {integrity: sha512-swZL3hgHN22Ga57H+GDT/PgQnF+eeAIj8irudwMupOJR4df5b1M67GwkrAOSJc+K6CdKxdArajrMlOcTLfg2wQ==}
|
||||
'@formatjs/intl-listformat@8.3.15':
|
||||
resolution: {integrity: sha512-dZEUYngBoAWLgYXvoBX3P6jIk5cg2rva7p5ZJTTn4lsCx/9abUKoPI2Wf59twxl5jgw1SDQMR1MBzJ7oiHwffw==}
|
||||
|
||||
'@formatjs/intl-locale@5.4.1':
|
||||
resolution: {integrity: sha512-whv9X0efjqLE6VJGXVr5+mwFfCTiPYDq6/jw5aZEwBJIlTyHJjKLGzXEzyjwmarzYCzLMFhepKYf/XeTR/Z8qA==}
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
resolution: {integrity: sha512-OvfBelurObm02D1+4BsuHSt3Xkn49RZPo7uJXt/1Z0dNET3EN39kY1rKMV3kG0tn0p4gyGWrcFx9HyeF07cq5g==}
|
||||
'@formatjs/intl-locale@5.3.12':
|
||||
resolution: {integrity: sha512-bI5CrvuC6A5PMOIiHX+6ETTHXzUofHw1U/fkI59sQJ+BFbkglxIVVncFPxHf4IHmAqAs/Rlp2bdzTEcLaq2tQw==}
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
resolution: {integrity: sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==}
|
||||
|
||||
'@formatjs/intl-[email protected].0':
|
||||
resolution: {integrity: sha512-l2kTmIyUEw/peqc01A1Fz3WxJHsb9ntAUIgAe0016svJJa6TBxriGARfTzOOI5WvsDO1dWXHHQ7gkOv4gtHm2w==}
|
||||
'@formatjs/intl-[email protected].0':
|
||||
resolution: {integrity: sha512-Z7TD3ssNry1Av1dVDoq/mzpxGyxqWwU0ClWEo+BB3k0JQTeQvUGmeK9WZGYvlqStS0UqkaKXk3dfjsb6NaHAxw==}
|
||||
|
||||
'@formatjs/intl-pluralrules@6.4.0':
|
||||
resolution: {integrity: sha512-dPsgM2WeLIRPzH8wqv9mnq2sdnAeyz7n7gqv/JSK6jFVvJEEZIyZX0XalFZsuuNQ8U2CQQ5RxCy0h9U/nirJIQ==}
|
||||
'@formatjs/intl-numberformat@9.4.3':
|
||||
resolution: {integrity: sha512-absVu2I7FlWl+Buictw2PL8Fl385uab3bEA3TCCTu+0DBHHFE+RTbmPmj0nUVO82U1b2Qw3/OA2I7KKLI48EpQ==}
|
||||
|
||||
'@formatjs/intl-[email protected]':
|
||||
resolution: {integrity: sha512-sXYW00Zh2rodj2HguxY/IoXSsrru3YAIbvH+JeXPvpQs5ukS2eWwv3zvcPTWZyFzqSiwFiKAuPtdr/6kQn3u6g==}
|
||||
'@formatjs/intl-[email protected]':
|
||||
resolution: {integrity: sha512-iOqCpBM2fo7bbbWqno+yN9WioepV01N2q3KawFZhRYADzijpzmIIA6cbFXrriCI1Mt2q5c7mceiKHoQkEFf0/Q==}
|
||||
|
||||
'@formatjs/intl-[email protected]':
|
||||
resolution: {integrity: sha512-0nL6a4aMrBVwFZX7udcuxxUtxldY/Q4KoGuQVNx4/roQqnZmOwDvzRb4DkEvtJZ3b76mk+V7zfJmm3nJycOFHQ==}
|
||||
'@formatjs/intl-[email protected]':
|
||||
resolution: {integrity: sha512-wIFsJiH/p9yMLzY3jyvht2vcjgJopMn30ePQQh7upkB5YYSAX3whtiqytG1EPhy2QKDy1PKOk+BJVNLA6HLePg==}
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
resolution: {integrity: sha512-oRzzAbJA08psr6AO0jgXAdx4HZ2vGNeja1fgaio2R2ne8DLtGPV+fa3gxI/t8J2dNPOndrwVAZVVfbH6GDFDwA==}
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
resolution: {integrity: sha512-tDkXnA4qpIFcDWac8CyVJq6oW8DR7W44QDUBsfXWIIJD/FYYen0QoH46W7XsVMFfPOVKkvbufjboZrrWbEfmww==}
|
||||
@@ -2162,8 +2162,8 @@ packages:
|
||||
'@konnorr/[email protected]':
|
||||
resolution: {integrity: sha512-6Ae3AfG45MOqF9Qde+1YxIxKf/GUsRmcriV7iCb/HaR+Mk1YDFYP3RINDRMI8z8YMWW3BnSthOujX9RoOex/Kw==}
|
||||
|
||||
'@lezer/[email protected].5':
|
||||
resolution: {integrity: sha512-Pw7PVCndruF1pb3ebCxpSVpkMHSDjwRCLcTRw+RizUKco5QbWylwq2g4lnG/RYN7yTTQDXE9F77MwtkAyphrjg==}
|
||||
'@lezer/[email protected].4':
|
||||
resolution: {integrity: sha512-xt3Q4z64xM+xhg4M7NXqw1N//1O74bUFo5kz6fwMeC4qwxSuRdwkYSmjcqCEEKYkNw5e67f2hcNqBoNERAEU0w==}
|
||||
|
||||
'@lezer/[email protected]':
|
||||
resolution: {integrity: sha512-YJE78Wcg+zX8f10hiHWQ4Az48Qr/c13eId0VtRQYLBpxHDmDeSrXIlkbl+fJGW42rWC/uoUco9mhBZeVWP/A1g==}
|
||||
@@ -4803,8 +4803,8 @@ packages:
|
||||
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
|
||||
[email protected]4.0:
|
||||
resolution: {integrity: sha512-T0pNsxkb9IKmtQjhpK3d7nWOpFv+YHuMbTJnSVMoQblF0onvXfErrtp4V1jHtsQPan7dKaGVD0q1WCXnzMF31A==}
|
||||
[email protected]3.0:
|
||||
resolution: {integrity: sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -6452,8 +6452,8 @@ packages:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
[email protected].7:
|
||||
resolution: {integrity: sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g==}
|
||||
[email protected].5:
|
||||
resolution: {integrity: sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
[email protected]:
|
||||
@@ -7258,8 +7258,8 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
tslib@2.9.1:
|
||||
resolution: {integrity: sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==}
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==}
|
||||
@@ -9138,7 +9138,7 @@ snapshots:
|
||||
'@bundle-stats/[email protected]([email protected])':
|
||||
dependencies:
|
||||
core-js: 3.51.0
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@cacheable/[email protected]':
|
||||
dependencies:
|
||||
@@ -9157,21 +9157,21 @@ snapshots:
|
||||
'@codemirror/language': 6.13.1
|
||||
'@codemirror/state': 6.7.6
|
||||
'@codemirror/view': 6.43.15
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
|
||||
'@codemirror/[email protected]':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.13.1
|
||||
'@codemirror/state': 6.7.6
|
||||
'@codemirror/view': 6.43.15
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
|
||||
'@codemirror/[email protected]':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.20.4
|
||||
'@codemirror/language': 6.13.1
|
||||
'@codemirror/state': 6.7.6
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
'@lezer/css': 1.3.6
|
||||
|
||||
'@codemirror/[email protected]':
|
||||
@@ -9182,7 +9182,7 @@ snapshots:
|
||||
'@codemirror/language': 6.13.1
|
||||
'@codemirror/state': 6.7.6
|
||||
'@codemirror/view': 6.43.15
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
'@lezer/css': 1.3.6
|
||||
'@lezer/html': 1.3.13
|
||||
|
||||
@@ -9193,7 +9193,7 @@ snapshots:
|
||||
'@codemirror/lint': 6.9.7
|
||||
'@codemirror/state': 6.7.6
|
||||
'@codemirror/view': 6.43.15
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
'@lezer/javascript': 1.5.4
|
||||
|
||||
'@codemirror/[email protected]':
|
||||
@@ -9203,7 +9203,7 @@ snapshots:
|
||||
'@codemirror/language': 6.13.1
|
||||
'@codemirror/state': 6.7.6
|
||||
'@codemirror/view': 6.43.15
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
@@ -9212,7 +9212,7 @@ snapshots:
|
||||
'@codemirror/autocomplete': 6.20.4
|
||||
'@codemirror/language': 6.13.1
|
||||
'@codemirror/state': 6.7.6
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
'@lezer/python': 1.1.19
|
||||
|
||||
'@codemirror/[email protected]':
|
||||
@@ -9220,7 +9220,7 @@ snapshots:
|
||||
'@codemirror/autocomplete': 6.20.4
|
||||
'@codemirror/language': 6.13.1
|
||||
'@codemirror/state': 6.7.6
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/lr': 1.4.10
|
||||
'@lezer/yaml': 1.0.4
|
||||
@@ -9230,7 +9230,7 @@ snapshots:
|
||||
'@codemirror/state': 6.7.6
|
||||
'@codemirror/streamparser': 6.0.0
|
||||
'@codemirror/view': 6.43.15
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/lr': 1.4.10
|
||||
style-mod: 4.1.3
|
||||
@@ -9299,38 +9299,38 @@ snapshots:
|
||||
'@emnapi/[email protected]':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.2.1
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/[email protected]':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.2.3
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@emnapi/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@eslint-community/[email protected]([email protected]4.0([email protected]))':
|
||||
'@eslint-community/[email protected]([email protected]3.0([email protected]))':
|
||||
dependencies:
|
||||
eslint: 10.14.0([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/[email protected]': {}
|
||||
@@ -9355,9 +9355,9 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
|
||||
'@eslint/[email protected]([email protected]4.0([email protected]))':
|
||||
'@eslint/[email protected]([email protected]3.0([email protected]))':
|
||||
optionalDependencies:
|
||||
eslint: 10.14.0([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
|
||||
'@eslint/[email protected]': {}
|
||||
|
||||
@@ -9391,11 +9391,11 @@ snapshots:
|
||||
'@formatjs/fast-memoize': 2.2.7
|
||||
'@formatjs/intl-localematcher': 0.6.1
|
||||
decimal.js: 10.6.0
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/[email protected]': {}
|
||||
|
||||
@@ -9403,7 +9403,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.4
|
||||
'@formatjs/icu-skeleton-parser': 1.8.14
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
dependencies:
|
||||
@@ -9412,58 +9412,58 @@ snapshots:
|
||||
'@formatjs/[email protected]':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.4
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/[email protected]': {}
|
||||
|
||||
'@formatjs/intl-datetimeformat@7.9.0':
|
||||
'@formatjs/intl-datetimeformat@7.8.2':
|
||||
dependencies:
|
||||
'@formatjs/bigdecimal': 0.2.7
|
||||
'@formatjs/intl-localematcher': 0.10.0
|
||||
'@formatjs/intl-localematcher': 0.9.0
|
||||
|
||||
'@formatjs/intl-displaynames@7.4.0':
|
||||
'@formatjs/intl-displaynames@7.3.15':
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.10.0
|
||||
'@formatjs/intl-localematcher': 0.9.0
|
||||
|
||||
'@formatjs/[email protected]2.0':
|
||||
'@formatjs/[email protected]1.1':
|
||||
dependencies:
|
||||
'@formatjs/bigdecimal': 0.2.7
|
||||
'@formatjs/intl-localematcher': 0.10.0
|
||||
'@formatjs/intl-localematcher': 0.9.0
|
||||
|
||||
'@formatjs/intl-getcanonicallocales@3.3.0': {}
|
||||
'@formatjs/intl-getcanonicallocales@3.2.12': {}
|
||||
|
||||
'@formatjs/intl-listformat@8.4.0':
|
||||
'@formatjs/intl-listformat@8.3.15':
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.10.0
|
||||
'@formatjs/intl-localematcher': 0.9.0
|
||||
|
||||
'@formatjs/intl-locale@5.4.1':
|
||||
'@formatjs/intl-locale@5.3.12':
|
||||
dependencies:
|
||||
'@formatjs/intl-getcanonicallocales': 3.3.0
|
||||
'@formatjs/intl-supportedvaluesof': 2.5.0
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
dependencies:
|
||||
'@formatjs/fast-memoize': 3.1.7
|
||||
'@formatjs/intl-getcanonicallocales': 3.2.12
|
||||
'@formatjs/intl-supportedvaluesof': 2.3.11
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/intl-[email protected].0':
|
||||
'@formatjs/intl-[email protected].0':
|
||||
dependencies:
|
||||
'@formatjs/fast-memoize': 3.1.7
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
dependencies:
|
||||
'@formatjs/bigdecimal': 0.2.7
|
||||
'@formatjs/intl-localematcher': 0.10.0
|
||||
'@formatjs/intl-localematcher': 0.9.0
|
||||
|
||||
'@formatjs/intl-pluralrules@6.4.0':
|
||||
'@formatjs/intl-pluralrules@6.3.16':
|
||||
dependencies:
|
||||
'@formatjs/bigdecimal': 0.2.7
|
||||
'@formatjs/intl-localematcher': 0.10.0
|
||||
'@formatjs/intl-localematcher': 0.9.0
|
||||
|
||||
'@formatjs/intl-relativetimeformat@12.4.0':
|
||||
'@formatjs/intl-relativetimeformat@12.3.16':
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.10.0
|
||||
'@formatjs/intl-localematcher': 0.9.0
|
||||
|
||||
'@formatjs/intl-supportedvaluesof@2.5.0':
|
||||
'@formatjs/intl-supportedvaluesof@2.3.11':
|
||||
dependencies:
|
||||
'@formatjs/fast-memoize': 3.1.7
|
||||
|
||||
@@ -9473,7 +9473,7 @@ snapshots:
|
||||
'@formatjs/fast-memoize': 2.2.7
|
||||
'@formatjs/icu-messageformat-parser': 2.11.2
|
||||
intl-messageformat: 10.7.16
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
@@ -9523,7 +9523,7 @@ snapshots:
|
||||
'@html-eslint/types': 0.67.0
|
||||
html-standard: 0.0.13
|
||||
|
||||
'@html-eslint/[email protected]([email protected]4.0([email protected]))':
|
||||
'@html-eslint/[email protected]([email protected]3.0([email protected]))':
|
||||
dependencies:
|
||||
'@eslint/plugin-kit': 0.4.1
|
||||
'@html-eslint/core': 0.67.0
|
||||
@@ -9532,7 +9532,7 @@ snapshots:
|
||||
'@html-eslint/template-syntax-parser': 0.67.0
|
||||
'@html-eslint/types': 0.67.0
|
||||
'@rviscomi/capo.js': 2.2.1
|
||||
eslint: 10.14.0([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
html-standard: 0.0.13
|
||||
|
||||
'@html-eslint/[email protected]':
|
||||
@@ -9690,43 +9690,43 @@ snapshots:
|
||||
|
||||
'@konnorr/[email protected]': {}
|
||||
|
||||
'@lezer/[email protected].5': {}
|
||||
'@lezer/[email protected].4': {}
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.5
|
||||
'@lezer/common': 1.5.4
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
@@ -9765,7 +9765,7 @@ snapshots:
|
||||
'@lit-labs/[email protected]':
|
||||
dependencies:
|
||||
lit: 3.3.3
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@lit/[email protected]':
|
||||
dependencies:
|
||||
@@ -9830,24 +9830,24 @@ snapshots:
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
'@material/feature-targeting': 14.0.0-canary.53b3cad2f.0
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
@@ -9866,21 +9866,21 @@ snapshots:
|
||||
'@material/shape': 14.0.0-canary.53b3cad2f.0
|
||||
'@material/theme': 14.0.0-canary.53b3cad2f.0
|
||||
'@material/typography': 14.0.0-canary.53b3cad2f.0
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
'@material/base': 14.0.0-canary.53b3cad2f.0
|
||||
'@material/dom': 14.0.0-canary.53b3cad2f.0
|
||||
lit: 3.3.3
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
'@material/mwc-base': 0.27.0
|
||||
'@material/mwc-ripple': 0.27.0
|
||||
lit: 3.3.3
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected](patch_hash=9450e509a86887d8f68ca9cf3f81e6ed70e94e49de13c5f6f20f94519ad4a987)':
|
||||
dependencies:
|
||||
@@ -9892,7 +9892,7 @@ snapshots:
|
||||
'@material/mwc-radio': 0.27.0
|
||||
'@material/mwc-ripple': 0.27.0
|
||||
lit: 3.3.3
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
@@ -9900,7 +9900,7 @@ snapshots:
|
||||
'@material/mwc-ripple': 0.27.0
|
||||
'@material/radio': 14.0.0-canary.53b3cad2f.0
|
||||
lit: 3.3.3
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
@@ -9908,7 +9908,7 @@ snapshots:
|
||||
'@material/mwc-base': 0.27.0
|
||||
'@material/ripple': 14.0.0-canary.53b3cad2f.0
|
||||
lit: 3.3.3
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
@@ -9921,7 +9921,7 @@ snapshots:
|
||||
'@material/ripple': 14.0.0-canary.53b3cad2f.0
|
||||
'@material/theme': 14.0.0-canary.53b3cad2f.0
|
||||
'@material/touch-target': 14.0.0-canary.53b3cad2f.0
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
@@ -9931,37 +9931,37 @@ snapshots:
|
||||
'@material/feature-targeting': 14.0.0-canary.53b3cad2f.0
|
||||
'@material/rtl': 14.0.0-canary.53b3cad2f.0
|
||||
'@material/theme': 14.0.0-canary.53b3cad2f.0
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
'@material/theme': 14.0.0-canary.53b3cad2f.0
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
'@material/feature-targeting': 14.0.0-canary.53b3cad2f.0
|
||||
'@material/rtl': 14.0.0-canary.53b3cad2f.0
|
||||
'@material/theme': 14.0.0-canary.53b3cad2f.0
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
'@material/feature-targeting': 14.0.0-canary.53b3cad2f.0
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
'@material/base': 14.0.0-canary.53b3cad2f.0
|
||||
'@material/feature-targeting': 14.0.0-canary.53b3cad2f.0
|
||||
'@material/rtl': 14.0.0-canary.53b3cad2f.0
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@material/[email protected]':
|
||||
dependencies:
|
||||
'@material/feature-targeting': 14.0.0-canary.53b3cad2f.0
|
||||
'@material/theme': 14.0.0-canary.53b3cad2f.0
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@mdi/[email protected]': {}
|
||||
|
||||
@@ -10218,7 +10218,7 @@ snapshots:
|
||||
|
||||
'@npmcli/[email protected]':
|
||||
dependencies:
|
||||
postcss-selector-parser: 7.1.7
|
||||
postcss-selector-parser: 7.1.5
|
||||
|
||||
'@npmcli/[email protected]': {}
|
||||
|
||||
@@ -10846,7 +10846,7 @@ snapshots:
|
||||
|
||||
'@swc/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
'@thepassle/[email protected]':
|
||||
dependencies:
|
||||
@@ -10958,7 +10958,7 @@ snapshots:
|
||||
|
||||
'@tybys/[email protected]':
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@types/[email protected]': {}
|
||||
@@ -11075,15 +11075,15 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 24.19.2
|
||||
|
||||
'@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]4.0([email protected]))([email protected])([email protected]))([email protected]4.0([email protected]))([email protected])([email protected])':
|
||||
'@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 8.72.1([email protected]4.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/parser': 8.72.1([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/scope-manager': 8.72.1
|
||||
'@typescript-eslint/type-utils': 8.72.1([email protected]4.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/utils': 8.72.1([email protected]4.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/type-utils': 8.72.1([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/utils': 8.72.1([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/visitor-keys': 8.72.1
|
||||
eslint: 10.14.0([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
ignore: 7.0.6
|
||||
natural-compare: 1.4.0
|
||||
ts-api-utils: 2.5.0([email protected])
|
||||
@@ -11091,14 +11091,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected]4.0([email protected]))([email protected])([email protected])':
|
||||
'@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.72.1
|
||||
'@typescript-eslint/types': 8.72.1
|
||||
'@typescript-eslint/typescript-estree': 8.72.1([email protected])([email protected])
|
||||
'@typescript-eslint/visitor-keys': 8.72.1
|
||||
debug: 4.4.3([email protected])
|
||||
eslint: 10.14.0([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -11121,13 +11121,13 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected]4.0([email protected]))([email protected])([email protected])':
|
||||
'@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.72.1
|
||||
'@typescript-eslint/typescript-estree': 8.72.1([email protected])([email protected])
|
||||
'@typescript-eslint/utils': 8.72.1([email protected]4.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/utils': 8.72.1([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
debug: 4.4.3([email protected])
|
||||
eslint: 10.14.0([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
ts-api-utils: 2.5.0([email protected])
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
@@ -11150,13 +11150,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/[email protected]([email protected]4.0([email protected]))([email protected])([email protected])':
|
||||
'@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.10.1([email protected]4.0([email protected]))
|
||||
'@eslint-community/eslint-utils': 4.10.1([email protected]3.0([email protected]))
|
||||
'@typescript-eslint/scope-manager': 8.72.1
|
||||
'@typescript-eslint/types': 8.72.1
|
||||
'@typescript-eslint/typescript-estree': 8.72.1([email protected])([email protected])
|
||||
eslint: 10.14.0([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -11804,7 +11804,7 @@ snapshots:
|
||||
[email protected]:
|
||||
dependencies:
|
||||
pascal-case: 3.1.2
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -12184,7 +12184,7 @@ snapshots:
|
||||
[email protected]:
|
||||
dependencies:
|
||||
no-case: 3.0.4
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
@@ -12216,7 +12216,7 @@ snapshots:
|
||||
|
||||
[email protected](patch_hash=4c014bd1952b4d1f21311ed2212dc87192e70b6e782597ecc7a485270247bbb5):
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
zrender: 6.1.0
|
||||
|
||||
[email protected]:
|
||||
@@ -12392,9 +12392,9 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]([email protected]4.0([email protected])):
|
||||
[email protected]([email protected]3.0([email protected])):
|
||||
dependencies:
|
||||
eslint: 10.14.0([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
@@ -12418,12 +12418,12 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected](@typescript-eslint/[email protected]([email protected]4.0([email protected]))([email protected])([email protected]))([email protected]4.0([email protected]))([email protected]):
|
||||
[email protected](@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))([email protected]):
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.72.1
|
||||
comment-parser: 1.4.8
|
||||
debug: 4.4.3([email protected])
|
||||
eslint: 10.14.0([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint-import-context: 0.1.9([email protected])
|
||||
is-glob: 4.0.3
|
||||
minimatch: 10.2.6
|
||||
@@ -12431,39 +12431,39 @@ snapshots:
|
||||
stable-hash-x: 0.2.0
|
||||
unrs-resolver: 1.12.2
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/utils': 8.72.1([email protected]4.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/utils': 8.72.1([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected]([email protected]4.0([email protected])):
|
||||
[email protected]([email protected]3.0([email protected])):
|
||||
dependencies:
|
||||
'@thepassle/axobject-query': 4.0.0
|
||||
aria-query: 5.3.2
|
||||
axe-core: 4.13.0
|
||||
dom5: 3.0.1
|
||||
emoji-regex: 10.6.0
|
||||
eslint: 10.14.0([email protected])
|
||||
eslint-plugin-lit: 2.3.1([email protected]4.0([email protected]))
|
||||
eslint: 10.13.0([email protected])
|
||||
eslint-plugin-lit: 2.3.1([email protected]3.0([email protected]))
|
||||
eslint-rule-extender: 0.0.1
|
||||
language-tags: 1.0.9
|
||||
parse5: 7.3.0
|
||||
parse5-htmlparser2-tree-adapter: 6.0.1
|
||||
|
||||
[email protected]([email protected]4.0([email protected])):
|
||||
[email protected]([email protected]3.0([email protected])):
|
||||
dependencies:
|
||||
eslint: 10.14.0([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
parse5: 8.0.1
|
||||
parse5-htmlparser2-tree-adapter: 8.0.1
|
||||
|
||||
[email protected](@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]4.0([email protected]))([email protected])([email protected]))([email protected]4.0([email protected]))([email protected])([email protected]))([email protected]4.0([email protected])):
|
||||
[email protected](@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected])):
|
||||
dependencies:
|
||||
eslint: 10.14.0([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.72.1(@typescript-eslint/[email protected]([email protected]4.0([email protected]))([email protected])([email protected]))([email protected]4.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/eslint-plugin': 8.72.1(@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
|
||||
[email protected]([email protected]4.0([email protected])):
|
||||
[email protected]([email protected]3.0([email protected])):
|
||||
dependencies:
|
||||
eslint: 10.14.0([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
is-valid-element-name: 1.0.0
|
||||
js-levenshtein-esm: 2.0.0
|
||||
|
||||
@@ -12480,9 +12480,9 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]4.0([email protected]):
|
||||
[email protected]3.0([email protected]):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.10.1([email protected]4.0([email protected]))
|
||||
'@eslint-community/eslint-utils': 4.10.1([email protected]3.0([email protected]))
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@eslint/config-array': 0.23.5([email protected])
|
||||
'@eslint/config-helpers': 0.7.0
|
||||
@@ -13137,7 +13137,7 @@ snapshots:
|
||||
'@formatjs/ecma402-abstract': 2.3.4
|
||||
'@formatjs/fast-memoize': 2.2.7
|
||||
'@formatjs/icu-messageformat-parser': 2.11.2
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
@@ -13628,7 +13628,7 @@ snapshots:
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -13849,7 +13849,7 @@ snapshots:
|
||||
no-[email protected]:
|
||||
dependencies:
|
||||
lower-case: 2.0.2
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -14170,7 +14170,7 @@ snapshots:
|
||||
[email protected]:
|
||||
dependencies:
|
||||
dot-case: 3.0.4
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
@@ -14220,7 +14220,7 @@ snapshots:
|
||||
[email protected]:
|
||||
dependencies:
|
||||
no-case: 3.0.4
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -14289,7 +14289,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected].7:
|
||||
[email protected].5:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
util-deprecate: 1.0.2
|
||||
@@ -14566,7 +14566,7 @@ snapshots:
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -14809,7 +14809,7 @@ snapshots:
|
||||
ip-address: 10.7.2
|
||||
smart-buffer: 4.2.0
|
||||
|
||||
[email protected](patch_hash=f25f02dcc8720341c3cfac312e0502081328108270852ad039187c2c5e2ea432): {}
|
||||
[email protected](patch_hash=f2ec2ae6877dfc201c542b4f9ded1d03831258bf328ae7bba876d14ce4508a06): {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -15176,7 +15176,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
tslib@2.9.1: {}
|
||||
tslib@2.8.1: {}
|
||||
|
||||
[email protected]([email protected]):
|
||||
dependencies:
|
||||
@@ -15243,13 +15243,13 @@ snapshots:
|
||||
possible-typed-array-names: 1.1.0
|
||||
reflect.getprototypeof: 1.0.10
|
||||
|
||||
[email protected]([email protected]4.0([email protected]))([email protected])([email protected]):
|
||||
[email protected]([email protected]3.0([email protected]))([email protected])([email protected]):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.72.1(@typescript-eslint/[email protected]([email protected]4.0([email protected]))([email protected])([email protected]))([email protected]4.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/parser': 8.72.1([email protected]4.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/eslint-plugin': 8.72.1(@typescript-eslint/[email protected]([email protected]3.0([email protected]))([email protected])([email protected]))([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/parser': 8.72.1([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
'@typescript-eslint/typescript-estree': 8.72.1([email protected])([email protected])
|
||||
'@typescript-eslint/utils': 8.72.1([email protected]4.0([email protected]))([email protected])([email protected])
|
||||
eslint: 10.14.0([email protected])
|
||||
'@typescript-eslint/utils': 8.72.1([email protected]3.0([email protected]))([email protected])([email protected])
|
||||
eslint: 10.13.0([email protected])
|
||||
typescript: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -15872,7 +15872,7 @@ snapshots:
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ overrides:
|
||||
lit-html: 3.3.3
|
||||
clean-css: 5.3.3
|
||||
"@lit/reactive-element": 2.1.2
|
||||
tslib: 2.9.1
|
||||
tslib: 2.8.1
|
||||
|
||||
patchedDependencies:
|
||||
"@material/[email protected]": patches/@[email protected]
|
||||
|
||||
+1
-18
@@ -13,9 +13,6 @@
|
||||
"enabledManagers": ["npm", "nvm", "custom.regex", "github-actions"],
|
||||
"minimumReleaseAge": "3 days",
|
||||
"postUpdateOptions": ["pnpmDedupe"],
|
||||
"constraints": {
|
||||
"ghActionsLock": "v0.4.1"
|
||||
},
|
||||
"lockFileMaintenance": {
|
||||
"description": ["Run after patch releases but before next beta"],
|
||||
"enabled": true,
|
||||
@@ -62,26 +59,12 @@
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.+)$"
|
||||
},
|
||||
{
|
||||
"description": "Keep gh-actions-lock in CI and Renovate's lockfile generator on the same version",
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": [
|
||||
"/^\\.github/workflows/actions-lock\\.yaml$/",
|
||||
"/^renovate\\.json$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"GH_ACTIONS_LOCK_VERSION: (?<currentValue>v\\d+\\.\\d+\\.\\d+)",
|
||||
"\"ghActionsLock\": \"(?<currentValue>v\\d+\\.\\d+\\.\\d+)\""
|
||||
],
|
||||
"depNameTemplate": "github/gh-actions-lock",
|
||||
"datasourceTemplate": "github-releases"
|
||||
},
|
||||
{
|
||||
"description": "Keep Playwright CI container image up to date",
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": ["/^\\.github/workflows/e2e\\.yaml$/"],
|
||||
"matchStrings": [
|
||||
"mcr\\.microsoft\\.com/playwright:(?<currentValue>v\\d+\\.\\d+\\.\\d+-noble)(?:@(?<currentDigest>sha256:[a-f0-9]{64}))?"
|
||||
"mcr\\.microsoft\\.com/playwright:(?<currentValue>v\\d+\\.\\d+\\.\\d+-noble)"
|
||||
],
|
||||
"depNameTemplate": "mcr.microsoft.com/playwright",
|
||||
"datasourceTemplate": "docker",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// The attributes that hold the address of a link or an image
|
||||
export const URL_ATTRIBUTES = [
|
||||
const URL_ATTRIBUTES = [
|
||||
["a", "href"],
|
||||
["img", "src"],
|
||||
["source", "src"],
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
// other one is built in the browser. The attribution comes from the TileJSON,
|
||||
// deliberately: it follows whoever serves the tiles.
|
||||
const SHIPPED_STYLES = {
|
||||
light: `${__STATIC_PATH__}map/light.json`,
|
||||
dark: `${__STATIC_PATH__}map/dark.json`,
|
||||
light: "/static/map/light.json",
|
||||
dark: "/static/map/dark.json",
|
||||
} as const;
|
||||
|
||||
// MapLibre needs WebGL2 even for raster, so the fallback stays a Leaflet layer.
|
||||
@@ -92,12 +92,8 @@ const useDemoUpstream = (style: StyleSpecification): StyleSpecification => {
|
||||
return style;
|
||||
};
|
||||
|
||||
// The origin that serves the frontend's files: the page, except for the demo
|
||||
// embed on another site
|
||||
const filesBase = () => new URL(__STATIC_PATH__, location.href);
|
||||
|
||||
// MapLibre rejects a relative sprite URL
|
||||
const absoluteSprite = (url: string) => new URL(url, filesBase()).href;
|
||||
const absoluteSprite = (url: string) => new URL(url, location.href).href;
|
||||
|
||||
// Shared only while in flight, or a style that failed once would never be
|
||||
// fetched again. Text rather than a parsed style: MapLibre mutates what it
|
||||
@@ -129,7 +125,7 @@ export const loadStyle = async (
|
||||
).buildMapStyle(
|
||||
mapStyle.palette,
|
||||
mapStyle.options ?? {},
|
||||
mapStyle.styleOptions
|
||||
mapStyle.baseColors
|
||||
);
|
||||
|
||||
if (typeof style.sprite === "string") {
|
||||
@@ -145,15 +141,14 @@ export const loadStyle = async (
|
||||
|
||||
// Global to MapLibre; set once before the first map is created. Absolute,
|
||||
// because on Cast the page is not served from the instance. The URL comes
|
||||
// from the build: the worker is an entry of its own, next to the app. MapLibre
|
||||
// starts a worker from another origin itself.
|
||||
// from the build: the worker is an entry of its own, next to the app.
|
||||
let workerUrlSet = false;
|
||||
export const ensureWorkerUrl = (setUrl: typeof setWorkerUrl) => {
|
||||
if (workerUrlSet) {
|
||||
return;
|
||||
}
|
||||
workerUrlSet = true;
|
||||
setUrl(new URL(__MAPLIBRE_WORKER_URL__, filesBase()).href);
|
||||
setUrl(new URL(__MAPLIBRE_WORKER_URL__, location.href).href);
|
||||
};
|
||||
|
||||
const createVectorLayer = async (
|
||||
|
||||
@@ -16,7 +16,7 @@ import { finalizeMapStyle } from "./map-style-transforms";
|
||||
*/
|
||||
// Versioned: /static is served with a month of max-age, so a cached manifest
|
||||
// could otherwise pair a new build with the sprite sheet of an old one.
|
||||
const STYLE_URLS_PATH = `${__STATIC_PATH__}map/urls.json?v=${__VERSION__}`;
|
||||
const STYLE_URLS_PATH = `/static/map/urls.json?v=${__VERSION__}`;
|
||||
|
||||
let urls: Promise<OsmOptions["urls"]> | undefined;
|
||||
|
||||
@@ -35,7 +35,7 @@ const styleUrls = () => {
|
||||
export const buildMapStyle = async (
|
||||
palette: MapPalette,
|
||||
options: Record<string, unknown>,
|
||||
styleOptions?: Record<string, unknown>
|
||||
baseColors?: Record<string, string>
|
||||
): Promise<StyleSpecification> => {
|
||||
const [{ osm }, resolvedUrls] = await Promise.all([
|
||||
import("@versatiles/style"),
|
||||
@@ -50,9 +50,13 @@ export const buildMapStyle = async (
|
||||
// The builder throws on an option it does not know and on a color it cannot
|
||||
// parse. Those come from a card config or a theme, so one typo would
|
||||
// otherwise drop the whole card to raster tiles. Step back to the style as it
|
||||
// comes - colors and adjustments included, or the default would come out as
|
||||
// the builder's unpainted cartography - and only then to the bare palette.
|
||||
const attempts = [options, ...(styleOptions ? [styleOptions] : []), {}];
|
||||
// ships - colors included, or the default would come out as the builder's
|
||||
// unpainted cartography - and only then to the bare palette.
|
||||
const attempts = [
|
||||
options,
|
||||
...(baseColors ? [{ colors: baseColors }] : []),
|
||||
{},
|
||||
];
|
||||
let style: StyleSpecification | undefined;
|
||||
for (const attempt of attempts) {
|
||||
try {
|
||||
|
||||
@@ -78,7 +78,7 @@ const POSITIONS: Record<
|
||||
bottomright: "bottom-right",
|
||||
};
|
||||
|
||||
const MAPLIBRE_CSS_URL = `${__STATIC_PATH__}map/maplibre-gl.css`;
|
||||
const MAPLIBRE_CSS_URL = "/static/map/maplibre-gl.css";
|
||||
// Fully hidden behind the globe, not MapLibre's faint ghost
|
||||
const MARKER_OPTIONS = { opacityWhenCovered: 0 };
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// The vector cartographies the map can draw. Each is a light and a dark
|
||||
// palette; the theme mode picks the half, unless the style only has one.
|
||||
// palette; the theme mode picks the half.
|
||||
//
|
||||
// Only the default pair is generated at build time (build-scripts/gulp/
|
||||
// map-assets.js); everything else is built in the browser from the same
|
||||
// builder, against the tiles and glyphs core's proxy already serves.
|
||||
|
||||
import { HA_MAP_COLORS, HA_MAP_COLORS_DARK } from "./ha-map-palette";
|
||||
import { NEON_MAP_COLORS_DARK, NEON_MAP_OPTIONS } from "./neon-map-palette";
|
||||
|
||||
/** Style ids, in the order the card editor offers them */
|
||||
export const MAP_STYLES = [
|
||||
@@ -16,7 +15,6 @@ export const MAP_STYLES = [
|
||||
"muted",
|
||||
"gray",
|
||||
"toner",
|
||||
"neon",
|
||||
] as const;
|
||||
|
||||
export type MapStyle = (typeof MAP_STYLES)[number];
|
||||
@@ -28,19 +26,13 @@ type VersatilesPalette = "colorful" | "natural" | "muted" | "gray" | "toner";
|
||||
|
||||
export type MapPalette = VersatilesPalette | `${VersatilesPalette}-dark`;
|
||||
|
||||
/**
|
||||
* A cartography plus what is painted over it; the VersaTiles palettes have
|
||||
* nothing. The color sets say which halves a style has: both, or neither, and
|
||||
* the theme mode picks; only one, and that is the half the map draws.
|
||||
*/
|
||||
/** A cartography plus the colors painted over it; only the default has any */
|
||||
const STYLE_PALETTES: Record<
|
||||
MapStyle,
|
||||
{
|
||||
palette: VersatilesPalette;
|
||||
colors?: Record<string, string>;
|
||||
colorsDark?: Record<string, string>;
|
||||
/** Builder options the style comes with, by option group */
|
||||
options?: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
> = {
|
||||
default: {
|
||||
@@ -48,11 +40,6 @@ const STYLE_PALETTES: Record<
|
||||
colors: HA_MAP_COLORS,
|
||||
colorsDark: HA_MAP_COLORS_DARK,
|
||||
},
|
||||
neon: {
|
||||
palette: "colorful",
|
||||
colorsDark: NEON_MAP_COLORS_DARK,
|
||||
options: NEON_MAP_OPTIONS,
|
||||
},
|
||||
colorful: { palette: "colorful" },
|
||||
natural: { palette: "natural" },
|
||||
muted: { palette: "muted" },
|
||||
@@ -109,12 +96,11 @@ export interface ResolvedMapStyle {
|
||||
/** Fetched rather than built: the default, while nothing was added to it */
|
||||
shipped?: "light" | "dark";
|
||||
/**
|
||||
* The style as it comes: its colors and its own adjustments, without what
|
||||
* the theme or the card painted over them. What to fall back on when the
|
||||
* builder rejects an adjustment, so a bad value costs the card's adjustments
|
||||
* rather than the style.
|
||||
* The style's own colors, without what the theme or the card painted over
|
||||
* them. What to fall back on when the builder rejects an adjustment, so a
|
||||
* bad value costs the adjustments rather than the cartography.
|
||||
*/
|
||||
styleOptions?: Record<string, unknown>;
|
||||
baseColors?: Record<string, string>;
|
||||
}
|
||||
|
||||
const isStyle = (value: unknown): value is MapStyle =>
|
||||
@@ -124,31 +110,9 @@ export const isCustomMapStyle = (
|
||||
style: MapStyleConfig | undefined
|
||||
): style is CustomMapStyleConfig => typeof style === "object" && style !== null;
|
||||
|
||||
const toStyle = (style: MapStyleConfig | undefined): MapStyle => {
|
||||
const named = isCustomMapStyle(style) ? style.base : style;
|
||||
return isStyle(named) ? named : DEFAULT_MAP_STYLE;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the map comes out dark: the theme mode's answer, unless the style
|
||||
* only has the one half. What the map's own chrome and the theme tokens have
|
||||
* to follow, rather than the theme mode.
|
||||
*/
|
||||
export const isDarkMapStyle = (
|
||||
style: MapStyleConfig | undefined,
|
||||
darkMode: boolean
|
||||
): boolean => {
|
||||
const { colors, colorsDark } = STYLE_PALETTES[toStyle(style)];
|
||||
if (Boolean(colors) !== Boolean(colorsDark)) {
|
||||
return Boolean(colorsDark);
|
||||
}
|
||||
return darkMode;
|
||||
};
|
||||
|
||||
/** The half to draw; `dark` is the map's, from isDarkMapStyle */
|
||||
export const paletteFor = (style: MapStyle, dark: boolean): MapPalette => {
|
||||
export const paletteFor = (style: MapStyle, darkMode: boolean): MapPalette => {
|
||||
const { palette } = STYLE_PALETTES[style];
|
||||
return dark ? `${palette}-dark` : palette;
|
||||
return darkMode ? `${palette}-dark` : palette;
|
||||
};
|
||||
|
||||
// An allowlist, not a passthrough: the builder also takes the tile, glyph and
|
||||
@@ -234,33 +198,19 @@ export const resolveMapStyle = (
|
||||
themeColors?: Record<string, string>
|
||||
): ResolvedMapStyle => {
|
||||
const custom = isCustomMapStyle(style) ? style : undefined;
|
||||
const mapStyle = toStyle(style);
|
||||
// A dark-only style picks the dark half of everything, not just the palette.
|
||||
const dark = isDarkMapStyle(style, darkMode);
|
||||
const named = custom ? custom.base : style;
|
||||
const mapStyle = isStyle(named) ? named : DEFAULT_MAP_STYLE;
|
||||
|
||||
const palette = paletteFor(mapStyle, dark);
|
||||
const options = custom ? builderOptions(custom, dark) : {};
|
||||
const palette = paletteFor(mapStyle, darkMode);
|
||||
const options = custom ? builderOptions(custom, darkMode) : {};
|
||||
// Before the colors below are merged in, so it answers whether the card
|
||||
// asked for anything of its own.
|
||||
const cardAdjusted = Object.keys(options).length > 0;
|
||||
|
||||
// The style's own adjustments go under the card's, group by group, so a
|
||||
// card that sets one recolor value keeps the style's others.
|
||||
const {
|
||||
colors,
|
||||
colorsDark,
|
||||
options: styleOptions,
|
||||
} = STYLE_PALETTES[mapStyle];
|
||||
for (const [group, values] of Object.entries(styleOptions ?? {})) {
|
||||
options[group] = {
|
||||
...values,
|
||||
...(options[group] as Record<string, unknown> | undefined),
|
||||
};
|
||||
}
|
||||
|
||||
// Bottom up: the style's colors, what the theme repaints, then what the card
|
||||
// names - so a card that sets one color keeps the rest of the look.
|
||||
const styleColors = dark ? colorsDark : colors;
|
||||
const { colors, colorsDark } = STYLE_PALETTES[mapStyle];
|
||||
const styleColors = (darkMode ? colorsDark : colors) ?? colors;
|
||||
if (styleColors || themeColors) {
|
||||
options.colors = {
|
||||
...styleColors,
|
||||
@@ -273,19 +223,11 @@ export const resolveMapStyle = (
|
||||
return { palette };
|
||||
}
|
||||
|
||||
const own = styleColors || styleOptions;
|
||||
const base = own
|
||||
? {
|
||||
styleOptions: {
|
||||
...styleOptions,
|
||||
...(styleColors && { colors: styleColors }),
|
||||
},
|
||||
}
|
||||
: {};
|
||||
const base = styleColors ? { baseColors: styleColors } : {};
|
||||
|
||||
// Anything added on top of the shipped default has to be built here.
|
||||
const untouched = !themeColors && !cardAdjusted;
|
||||
return mapStyle === DEFAULT_MAP_STYLE && untouched
|
||||
? { palette, options, ...base, shipped: dark ? "dark" : "light" }
|
||||
? { palette, options, ...base, shipped: darkMode ? "dark" : "light" }
|
||||
: { palette, options, ...base };
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import { isAndroidDevice } from "../../util/is_android";
|
||||
import { isIosDevice } from "../../util/is_ios";
|
||||
|
||||
export const navigationUrl = (latitude: number, longitude: number): string => {
|
||||
const point = `${latitude},${longitude}`;
|
||||
if (isIosDevice()) {
|
||||
return `https://maps.apple.com/?daddr=${point}`;
|
||||
}
|
||||
if (isAndroidDevice()) {
|
||||
return `geo:${point}?q=${point}`;
|
||||
}
|
||||
return `https://www.openstreetmap.org/directions?to=${encodeURIComponent(point)}#map=15/${latitude}/${longitude}`;
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
// The "neon" style: the VersaTiles "colorful" cartography repainted as a night
|
||||
// map, glowing roads on near-black ground. A dark set and no light one, which
|
||||
// is what makes the style dark whatever the theme mode.
|
||||
|
||||
export const NEON_MAP_COLORS_DARK = {
|
||||
background: "#07021a",
|
||||
land: "#07021a",
|
||||
water: "#13215e",
|
||||
labelWater: "#00e5ff",
|
||||
glacier: "#1b2a6b",
|
||||
natureWood: "#0d1f2e",
|
||||
natureGrass: "#101a33",
|
||||
naturePark: "#12203a",
|
||||
natureLeisure: "#111a30",
|
||||
natureAgriculture: "#0d1528",
|
||||
natureWetland: "#0e2440",
|
||||
natureSand: "#1a1230",
|
||||
natureRock: "#140c28",
|
||||
areaResidential: "#0c0524",
|
||||
areaCommercial: "#140430",
|
||||
areaIndustrial: "#10062a",
|
||||
areaWaste: "#160a2e",
|
||||
areaBurial: "#0d0a26",
|
||||
siteParking: "#0f0728",
|
||||
siteSports: "#0a1c2c",
|
||||
siteEducation: "#160a34",
|
||||
siteHospital: "#2a0a2a",
|
||||
siteDanger: "#ff1744",
|
||||
sitePrison: "#1c0a28",
|
||||
siteConstruction: "#3a1d5c",
|
||||
building: "#160a33",
|
||||
buildingBg: "#8a2be2",
|
||||
roadStreet: "#00e5ff",
|
||||
roadStreetBg: "#062b52",
|
||||
roadSecondary: "#8f7bff",
|
||||
roadSecondaryBg: "#241058",
|
||||
roadTrunk: "#ff6ec7",
|
||||
roadTrunkBg: "#4a0f38",
|
||||
roadMotorway: "#ff2d95",
|
||||
roadMotorwayBg: "#5c0033",
|
||||
transitRail: "#7b2cff",
|
||||
transitSubway: "#b14cff",
|
||||
transitCycle: "#00ffa3",
|
||||
transitFoot: "#2e2a55",
|
||||
boundary: "#39ff14",
|
||||
boundaryDisputed: "#1f7a0d",
|
||||
label: "#ff9ad5",
|
||||
labelHalo: "rgba(7,2,26,0.9)",
|
||||
labelSymbol: "#ff9ad5",
|
||||
labelPoi: "#00e5ff",
|
||||
labelShield: "#12002a",
|
||||
labelHousenumber: "rgba(0,229,255,0.35)",
|
||||
};
|
||||
|
||||
/** Builder options the style comes with, under whatever a card adds */
|
||||
export const NEON_MAP_OPTIONS = {
|
||||
recolor: { saturate: 0.35, contrast: 1.15 },
|
||||
text: {
|
||||
transform: "uppercase",
|
||||
letterSpacing: 0.12,
|
||||
haloWidth: 2,
|
||||
haloBlur: 0,
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { SYSTEM_LOG_LEVELS, type SystemLogLevel } from "../../data/system_log";
|
||||
import {
|
||||
decodeQueryParams,
|
||||
hasQueryParams,
|
||||
@@ -18,7 +17,7 @@ export const logSearchQueryParamConfig = {
|
||||
// `raw` shows the full Home Assistant Core log instead of the errors.
|
||||
export const logsQueryParamConfig = {
|
||||
string: ["provider", "search", "filter"],
|
||||
list: ["integration", "level"],
|
||||
list: ["integration"],
|
||||
boolean: [{ key: "raw", trueValue: "1" }],
|
||||
} as const satisfies QueryParamConfig;
|
||||
|
||||
@@ -29,19 +28,13 @@ export interface LogsState {
|
||||
search: string;
|
||||
// Only apply to the Home Assistant Core log
|
||||
integrations: string[];
|
||||
// Only apply to the Home Assistant Core errors, not the raw log
|
||||
levels: SystemLogLevel[];
|
||||
raw: boolean;
|
||||
}
|
||||
|
||||
const isLevel = (level: string): level is SystemLogLevel =>
|
||||
(SYSTEM_LOG_LEVELS as readonly string[]).includes(level);
|
||||
|
||||
export const DEFAULT_LOGS_STATE: LogsState = {
|
||||
provider: DEFAULT_LOG_PROVIDER,
|
||||
search: "",
|
||||
integrations: [],
|
||||
levels: [],
|
||||
raw: false,
|
||||
};
|
||||
|
||||
@@ -60,7 +53,6 @@ export const logsQueryParams = (state: LogsState): LogsQueryParams => ({
|
||||
search: state.search,
|
||||
integration:
|
||||
state.provider === DEFAULT_LOG_PROVIDER ? state.integrations : undefined,
|
||||
level: state.provider === DEFAULT_LOG_PROVIDER ? state.levels : undefined,
|
||||
raw: state.provider === DEFAULT_LOG_PROVIDER && state.raw,
|
||||
});
|
||||
|
||||
@@ -79,7 +71,6 @@ export const logsLinkFromUrl = (
|
||||
provider,
|
||||
search: params.search ?? params.filter ?? "",
|
||||
integrations: core ? (params.integration?.filter(Boolean) ?? []) : [],
|
||||
levels: core ? (params.level?.filter(isLevel) ?? []) : [],
|
||||
raw: core && !!params.raw,
|
||||
};
|
||||
};
|
||||
@@ -67,88 +67,3 @@ export const itemTooltipPosition: TooltipPositionCallback = (
|
||||
|
||||
return [x, y];
|
||||
};
|
||||
|
||||
const ECHARTS_TOOLTIP_GAP_PX = 20;
|
||||
export const WINDOW_MARGIN_PX = 8;
|
||||
|
||||
const fitInto = (
|
||||
pos: number,
|
||||
size: number,
|
||||
alternatives: number[],
|
||||
min: number,
|
||||
max: number
|
||||
) =>
|
||||
size > max - min
|
||||
? min
|
||||
: ([pos, ...alternatives].find((p) => p >= min && p + size <= max) ??
|
||||
Math.max(min, Math.min(pos, max - size)));
|
||||
|
||||
/**
|
||||
* ECharts' default placement (or the chart-confined one), moved into the
|
||||
* window when it would leave it. The other side of the cursor is tried before
|
||||
* clamping, below first vertically. A tooltip taller than the window starts at
|
||||
* its top. A confined tooltip stays in the chart, leaving it only as far as the
|
||||
* window needs.
|
||||
*/
|
||||
export const windowTooltipPosition =
|
||||
(
|
||||
getChartRect: () => DOMRect | undefined,
|
||||
confine: boolean
|
||||
): TooltipPositionCallback =>
|
||||
(point, _params, _dom, _rect, size) => {
|
||||
const [cursorX, cursorY] = point;
|
||||
const [viewW, viewH] = size.viewSize;
|
||||
const [tipW, tipH] = size.contentSize;
|
||||
|
||||
const right = cursorX + ECHARTS_TOOLTIP_GAP_PX;
|
||||
const left = cursorX - ECHARTS_TOOLTIP_GAP_PX - tipW;
|
||||
const below = cursorY + ECHARTS_TOOLTIP_GAP_PX;
|
||||
const above = cursorY - ECHARTS_TOOLTIP_GAP_PX - tipH;
|
||||
let x = right + tipW + 2 > viewW ? left : right;
|
||||
let y = below + tipH > viewH ? above : below;
|
||||
if (confine) {
|
||||
x = Math.max(0, Math.min(x, viewW - tipW));
|
||||
y = Math.max(0, Math.min(y, viewH - tipH));
|
||||
}
|
||||
|
||||
const rect = getChartRect();
|
||||
if (!rect?.height) {
|
||||
return [x, y];
|
||||
}
|
||||
const fit = (
|
||||
pos: number,
|
||||
tipSize: number,
|
||||
alternatives: number[],
|
||||
windowMin: number,
|
||||
windowMax: number,
|
||||
viewSize: number
|
||||
) => {
|
||||
if (!confine) {
|
||||
return fitInto(pos, tipSize, alternatives, windowMin, windowMax);
|
||||
}
|
||||
const min = Math.max(0, windowMin);
|
||||
const max = Math.min(viewSize, windowMax);
|
||||
return tipSize > max - min
|
||||
? fitInto(pos, tipSize, [], windowMin, windowMax)
|
||||
: fitInto(pos, tipSize, alternatives, min, max);
|
||||
};
|
||||
const { clientWidth, clientHeight } = document.documentElement;
|
||||
return [
|
||||
fit(
|
||||
x,
|
||||
tipW,
|
||||
[right, left],
|
||||
WINDOW_MARGIN_PX - rect.left,
|
||||
clientWidth - WINDOW_MARGIN_PX - rect.left,
|
||||
viewW
|
||||
),
|
||||
fit(
|
||||
y,
|
||||
tipH,
|
||||
[below, above],
|
||||
WINDOW_MARGIN_PX - rect.top,
|
||||
clientHeight - WINDOW_MARGIN_PX - rect.top,
|
||||
viewH
|
||||
),
|
||||
];
|
||||
};
|
||||
@@ -68,7 +68,6 @@ import { ZOOM_SLIDER_HEIGHT } from "./ha-chart-zoom-slider";
|
||||
import { formatTimeLabel } from "./axis-label";
|
||||
import type { ChartSonification } from "./chart-sonification";
|
||||
import { canSonifyChart, sonifyChart } from "./chart-sonification";
|
||||
import { windowTooltipPosition } from "./chart-tooltip-position";
|
||||
import { downSampleLineData } from "./down-sample";
|
||||
import { wrapLitTooltipFormatter } from "./lit-tooltip-formatter";
|
||||
|
||||
@@ -1253,19 +1252,9 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
const processTooltip = (tooltip: HaTooltipOption): TooltipOption => {
|
||||
const next = convertHaTooltipFormatter(tooltip);
|
||||
if (isMobile) {
|
||||
next.appendTo = undefined;
|
||||
}
|
||||
if (next.position === undefined) {
|
||||
// ECharts confines after the position callback, which would undo the
|
||||
// window fit, so the callback confines itself
|
||||
next.position = windowTooltipPosition(
|
||||
() => this._chartContainer?.getBoundingClientRect(),
|
||||
isMobile || next.confine === true
|
||||
);
|
||||
next.confine = false;
|
||||
} else if (isMobile) {
|
||||
// mobile charts are full width so we need to confine the tooltip to the chart
|
||||
next.confine = true;
|
||||
next.appendTo = undefined;
|
||||
}
|
||||
if (isMobile || this._touchInput) {
|
||||
next.triggerOn = "click";
|
||||
|
||||
@@ -31,14 +31,8 @@ export interface NetworkNode {
|
||||
color?: string;
|
||||
borderColor?: string;
|
||||
borderWidth?: number;
|
||||
opacity?: number;
|
||||
};
|
||||
label?: {
|
||||
position?: "left" | "right" | "top" | "bottom";
|
||||
show?: boolean;
|
||||
fontWeight?: "normal" | "bold";
|
||||
fontSize?: number;
|
||||
};
|
||||
label?: { position: "left" | "right" | "top" | "bottom" };
|
||||
fixed?: boolean;
|
||||
/**
|
||||
* Distance from the center, where 0 is the center and 1 is the edge
|
||||
@@ -57,7 +51,6 @@ export interface NetworkLink {
|
||||
width?: number;
|
||||
color?: string;
|
||||
type?: "solid" | "dashed" | "dotted";
|
||||
opacity?: number;
|
||||
};
|
||||
symbolSize?: number | number[];
|
||||
symbol?: string;
|
||||
@@ -293,8 +286,9 @@ export class HaNetworkGraph extends LitElement {
|
||||
return node.name ?? "";
|
||||
},
|
||||
rich: {
|
||||
// inherits the node label font, so a node can override it
|
||||
primary: {},
|
||||
primary: {
|
||||
fontSize: 12,
|
||||
},
|
||||
secondary: {
|
||||
fontSize: 12,
|
||||
color: getComputedStyle(document.body).getPropertyValue(
|
||||
|
||||
@@ -22,8 +22,6 @@ export interface Node {
|
||||
color?: string;
|
||||
passThrough?: boolean;
|
||||
entityId?: string;
|
||||
// Nodes of one group take a single position when ordering their children
|
||||
group?: string;
|
||||
}
|
||||
export interface Link {
|
||||
source: string;
|
||||
@@ -48,46 +46,6 @@ const NODE_SIZE = 30;
|
||||
const LABEL_MIN_MARGIN = 5;
|
||||
const BIDI_MARKS = /[\u200E\u200F\u202A-\u202E\u2066-\u2069]/g;
|
||||
|
||||
// Links without a value take the remaining capacity of their nodes, in order.
|
||||
export const fillSankeyLinkValues = (
|
||||
nodes: Node[],
|
||||
rawLinks: Link[]
|
||||
): ProcessedLink[] => {
|
||||
const accountedIn = new Map<string, number>();
|
||||
const accountedOut = new Map<string, number>();
|
||||
const links: ProcessedLink[] = [];
|
||||
rawLinks.forEach((link) => {
|
||||
const sourceNode = nodes.find((n) => n.id === link.source);
|
||||
const targetNode = nodes.find((n) => n.id === link.target);
|
||||
if (!sourceNode || !targetNode) {
|
||||
return;
|
||||
}
|
||||
const sourceAccounted = accountedOut.get(sourceNode.id) || 0;
|
||||
const targetAccounted = accountedIn.get(targetNode.id) || 0;
|
||||
|
||||
// if no value is provided, we infer it from the remaining capacity of the source and target nodes
|
||||
const sourceRemaining = sourceNode.value - sourceAccounted;
|
||||
const targetRemaining = targetNode.value - targetAccounted;
|
||||
// ensure the value is not greater than the remaining capacity of the nodes
|
||||
const value = Math.min(
|
||||
link.value ?? sourceRemaining,
|
||||
sourceRemaining,
|
||||
targetRemaining
|
||||
);
|
||||
|
||||
accountedIn.set(targetNode.id, targetAccounted + value);
|
||||
accountedOut.set(sourceNode.id, sourceAccounted + value);
|
||||
|
||||
if (value > 0) {
|
||||
links.push({
|
||||
...link,
|
||||
value,
|
||||
});
|
||||
}
|
||||
});
|
||||
return links;
|
||||
};
|
||||
|
||||
@customElement("ha-sankey-chart")
|
||||
export class HaSankeyChart extends LitElement {
|
||||
@property({ attribute: false }) public data: SankeyChartData = {
|
||||
@@ -232,7 +190,7 @@ export class HaSankeyChart extends LitElement {
|
||||
);
|
||||
}
|
||||
});
|
||||
const links = fillSankeyLinkValues(filteredNodes, data.links);
|
||||
const links = this._processLinks(filteredNodes, data.links);
|
||||
const sectionWidth = width / indexes.length;
|
||||
const labelSpace = sectionWidth - NODE_SIZE - LABEL_DISTANCE;
|
||||
// Two-line values can wrap the unit onto a third line; keep that text inside the chart.
|
||||
@@ -346,31 +304,60 @@ export class HaSankeyChart extends LitElement {
|
||||
|
||||
private _createData = memoizeOne(this._computeData);
|
||||
|
||||
private _processLinks(nodes: Node[], rawLinks: Link[]) {
|
||||
const accountedIn = new Map<string, number>();
|
||||
const accountedOut = new Map<string, number>();
|
||||
const links: ProcessedLink[] = [];
|
||||
rawLinks.forEach((link) => {
|
||||
const sourceNode = nodes.find((n) => n.id === link.source);
|
||||
const targetNode = nodes.find((n) => n.id === link.target);
|
||||
if (!sourceNode || !targetNode) {
|
||||
return;
|
||||
}
|
||||
const sourceAccounted = accountedOut.get(sourceNode.id) || 0;
|
||||
const targetAccounted = accountedIn.get(targetNode.id) || 0;
|
||||
|
||||
// if no value is provided, we infer it from the remaining capacity of the source and target nodes
|
||||
const sourceRemaining = sourceNode.value - sourceAccounted;
|
||||
const targetRemaining = targetNode.value - targetAccounted;
|
||||
// ensure the value is not greater than the remaining capacity of the nodes
|
||||
const value = Math.min(
|
||||
link.value ?? sourceRemaining,
|
||||
sourceRemaining,
|
||||
targetRemaining
|
||||
);
|
||||
|
||||
accountedIn.set(targetNode.id, targetAccounted + value);
|
||||
accountedOut.set(sourceNode.id, sourceAccounted + value);
|
||||
|
||||
if (value > 0) {
|
||||
links.push({
|
||||
...link,
|
||||
value,
|
||||
});
|
||||
}
|
||||
});
|
||||
return links;
|
||||
}
|
||||
|
||||
private _findParentIndex(id: string, links: Link[], sections: Node[][]) {
|
||||
const parents = links.filter((l) => l.target === id).map((l) => l.source);
|
||||
if (parents.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
const countedGroups = new Set<string>();
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
for (const parent of parents) {
|
||||
let offset = 0;
|
||||
for (let i = sections.length - 1; i >= 0; i--) {
|
||||
const positions = [...new Set(sections[i].map((n) => n.group ?? n.id))];
|
||||
const node = sections[i].find((n) => n.id === parent);
|
||||
if (node) {
|
||||
if (node.group !== undefined) {
|
||||
if (countedGroups.has(node.group)) {
|
||||
break;
|
||||
}
|
||||
countedGroups.add(node.group);
|
||||
}
|
||||
sum += offset + positions.indexOf(node.group ?? node.id);
|
||||
const section = sections[i];
|
||||
const index = section.findIndex((n) => n.id === parent);
|
||||
if (index !== -1) {
|
||||
sum += offset + index;
|
||||
count++;
|
||||
break;
|
||||
}
|
||||
offset += positions.length;
|
||||
offset += section.length;
|
||||
}
|
||||
}
|
||||
return count > 0 ? sum / count : -1;
|
||||
|
||||
@@ -161,8 +161,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
return html`<ha-generic-picker
|
||||
.label=${this.label}
|
||||
.value=${value}
|
||||
.disabled=${this._automations?.length === 0}
|
||||
.loading=${this._automations === undefined}
|
||||
.disabled=${!this._automations || this._automations.length === 0}
|
||||
.getItems=${this._getItems(
|
||||
value,
|
||||
this._automations,
|
||||
|
||||
@@ -189,10 +189,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
@state() private _loadFailed = false;
|
||||
|
||||
private _statisticIdsRequest = 0;
|
||||
|
||||
@property({ attribute: "can-edit", type: Boolean }) public canEdit?: boolean;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
@@ -205,36 +201,11 @@ export class HaStatisticPicker extends LitElement {
|
||||
}
|
||||
|
||||
private async _getStatisticIds() {
|
||||
// Changing the statistic types starts a new request, and only the latest
|
||||
// one may update the picker
|
||||
const request = ++this._statisticIdsRequest;
|
||||
this._loadFailed = false;
|
||||
|
||||
let statisticIds: StatisticsMetaData[];
|
||||
try {
|
||||
statisticIds = await getStatisticIds(this._api, this.statisticTypes);
|
||||
} catch {
|
||||
if (request === this._statisticIdsRequest) {
|
||||
this._loadFailed = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (request !== this._statisticIdsRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.statisticIds = statisticIds;
|
||||
this.statisticIds = await getStatisticIds(this._api, this.statisticTypes);
|
||||
this._picker?.requestUpdate();
|
||||
this._valueRenderer = this._makeValueRenderer();
|
||||
}
|
||||
|
||||
private _retryGetStatisticIds(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._getStatisticIds();
|
||||
}
|
||||
|
||||
private _getItems = () =>
|
||||
this._getStatisticsItems(
|
||||
this._states,
|
||||
@@ -618,12 +589,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.getItems=${this._getItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.loading=${!this._loadFailed && this.statisticIds === undefined}
|
||||
.loadError=${
|
||||
this._loadFailed
|
||||
? this._i18n.localize("ui.components.statistic-picker.load_failed")
|
||||
: undefined
|
||||
}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
.searchFn=${this._searchFn}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
@@ -633,7 +598,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
"ui.components.statistic-picker.unknown"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
@picker-retry=${this._retryGetStatisticIds}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import {
|
||||
@@ -26,9 +25,6 @@ import type { IntegrationManifest } from "../data/integration";
|
||||
import { domainToName } from "../data/integration";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-badge";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-tooltip";
|
||||
import "./item/ha-list-item-option";
|
||||
import type { HaListItemOption } from "./item/ha-list-item-option";
|
||||
import "./list/ha-list-selectable";
|
||||
@@ -36,14 +32,6 @@ import type { HaListSelectable } from "./list/ha-list-selectable";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
export interface IntegrationFilterCount {
|
||||
key: string;
|
||||
path: string;
|
||||
count: number;
|
||||
label: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-filter-integrations")
|
||||
export class HaFilterIntegrations extends LitElement {
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
@@ -52,12 +40,6 @@ export class HaFilterIntegrations extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
/** Counts to show beside each integration, by domain. */
|
||||
@property({ attribute: false }) public counts?: Record<
|
||||
string,
|
||||
IntegrationFilterCount[]
|
||||
>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
@@ -147,7 +129,6 @@ export class HaFilterIntegrations extends LitElement {
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
<span slot="headline">${integration.name}</span>
|
||||
${this._renderCounts(integration.domain)}
|
||||
</ha-list-item-option>`
|
||||
)}
|
||||
</ha-list-selectable>`
|
||||
@@ -159,32 +140,6 @@ export class HaFilterIntegrations extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderCounts(domain: string): TemplateResult | typeof nothing {
|
||||
const counts = this.counts?.[domain];
|
||||
|
||||
if (!counts?.length) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<span slot="supporting-text" class="counts">
|
||||
${counts.map(
|
||||
(item) =>
|
||||
html`<ha-badge
|
||||
id="count-${domain}-${item.key}"
|
||||
role="img"
|
||||
aria-label=${item.label}
|
||||
style=${styleMap({ "--badge-color": item.color })}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${item.path}></ha-svg-icon>
|
||||
${item.count}
|
||||
</ha-badge>
|
||||
<ha-tooltip for="count-${domain}-${item.key}">
|
||||
${item.label}
|
||||
</ha-tooltip>`
|
||||
)}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
@@ -294,15 +249,6 @@ export class HaFilterIntegrations extends LitElement {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
.counts {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-1);
|
||||
margin-top: var(--ha-space-1);
|
||||
--ha-badge-size: 22px;
|
||||
--ha-badge-icon-size: 14px;
|
||||
--ha-badge-font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import "@home-assistant/webawesome/dist/components/popover/popover";
|
||||
import type WaPopover from "@home-assistant/webawesome/dist/components/popover/popover";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { mdiPlaylistPlus, mdiRefresh } from "@mdi/js";
|
||||
import { mdiPlaylistPlus } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
@@ -17,15 +17,13 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { configContext, internationalizationContext } from "../data/context";
|
||||
import { configContext } from "../data/context";
|
||||
import { PickerMixin } from "../mixins/picker-mixin";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-alert";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-button";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-picker-combo-box";
|
||||
import type {
|
||||
@@ -35,7 +33,6 @@ import type {
|
||||
} from "./ha-picker-combo-box";
|
||||
import "./ha-picker-field";
|
||||
import "./ha-svg-icon";
|
||||
import "./skeleton/ha-skeleton";
|
||||
|
||||
@customElement("ha-generic-picker")
|
||||
export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
@@ -117,15 +114,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
@property({ type: Boolean, attribute: "no-unknown-state" })
|
||||
public noUnknownState = false;
|
||||
|
||||
/** Shows a skeleton in place of the field while its items load. */
|
||||
@property({ type: Boolean, reflect: true }) public loading = false;
|
||||
|
||||
/**
|
||||
* Shows this error with a retry button in place of the field when its items
|
||||
* failed to load. The retry button fires `picker-retry`.
|
||||
*/
|
||||
@property({ attribute: false }) public loadError?: string;
|
||||
|
||||
@query(".container") private _containerElement?: HTMLDivElement;
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
@@ -136,14 +124,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig?: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
// Set once loading starts, then hidden with CSS once it ends, so the
|
||||
// skeleton fades out over the field
|
||||
@state() private _skeletonShown = false;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _pickerWrapperOpen = false;
|
||||
@@ -175,14 +155,9 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
private _unsubscribeTinyKeys?: () => void;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("loading") && this.loading) {
|
||||
this._skeletonShown = true;
|
||||
}
|
||||
if (
|
||||
changedProperties.has("value") ||
|
||||
changedProperties.has("noUnknownState") ||
|
||||
changedProperties.has("loading") ||
|
||||
changedProperties.has("loadError")
|
||||
changedProperties.has("noUnknownState")
|
||||
) {
|
||||
this._setUnknownValue();
|
||||
}
|
||||
@@ -208,73 +183,45 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
: nothing
|
||||
}
|
||||
<div id="picker">
|
||||
<div class="field" ?inert=${this.loading || !!this.loadError}>
|
||||
<slot name="field">
|
||||
${
|
||||
this.addButtonLabel && !this.value
|
||||
? html`<ha-button
|
||||
size="s"
|
||||
appearance="filled"
|
||||
@click=${this.open}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiPlaylistPlus}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.addButtonLabel}
|
||||
</ha-button>`
|
||||
: html`<ha-picker-field
|
||||
type="button"
|
||||
class=${this._opened ? "opened" : ""}
|
||||
compact
|
||||
.unknown=${this._unknownValue}
|
||||
.unknownItemText=${this.unknownItemText}
|
||||
aria-label=${ifDefined(this.ariaLabel || this.label)}
|
||||
@click=${this.open}
|
||||
@clear=${this._clear}
|
||||
.icon=${this.icon}
|
||||
.image=${this.image}
|
||||
.label=${label}
|
||||
.placeholder=${this.placeholder}
|
||||
.value=${this.value}
|
||||
.valueRenderer=${this.valueRenderer}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.invalid=${this.invalid}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
>
|
||||
<slot name="start"></slot>
|
||||
</ha-picker-field>`
|
||||
}
|
||||
</slot>
|
||||
</div>
|
||||
${
|
||||
this._skeletonShown
|
||||
? html`<ha-skeleton
|
||||
class="loading-skeleton"
|
||||
role=${ifDefined(this.loading ? "progressbar" : undefined)}
|
||||
aria-label=${ifDefined(
|
||||
this.loading ? this._loadingLabel() : undefined
|
||||
)}
|
||||
></ha-skeleton>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.loadError
|
||||
? html`<div class="load-error">
|
||||
<ha-alert alert-type="error">
|
||||
${this.loadError}
|
||||
<ha-icon-button
|
||||
slot="action"
|
||||
.path=${mdiRefresh}
|
||||
.label=${this._i18n?.localize("ui.common.retry")}
|
||||
@click=${this._retry}
|
||||
></ha-icon-button>
|
||||
</ha-alert>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
<slot name="field">
|
||||
${
|
||||
this.addButtonLabel && !this.value
|
||||
? html`<ha-button
|
||||
size="s"
|
||||
appearance="filled"
|
||||
@click=${this.open}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiPlaylistPlus}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.addButtonLabel}
|
||||
</ha-button>`
|
||||
: html`<ha-picker-field
|
||||
type="button"
|
||||
class=${this._opened ? "opened" : ""}
|
||||
compact
|
||||
.unknown=${this._unknownValue}
|
||||
.unknownItemText=${this.unknownItemText}
|
||||
aria-label=${ifDefined(this.ariaLabel || this.label)}
|
||||
@click=${this.open}
|
||||
@clear=${this._clear}
|
||||
.icon=${this.icon}
|
||||
.image=${this.image}
|
||||
.label=${label}
|
||||
.placeholder=${this.placeholder}
|
||||
.value=${this.value}
|
||||
.valueRenderer=${this.valueRenderer}
|
||||
.required=${this.required}
|
||||
.disabled=${this.disabled}
|
||||
.invalid=${this.invalid}
|
||||
.hideClearIcon=${this.hideClearIcon}
|
||||
>
|
||||
<slot name="start"></slot>
|
||||
</ha-picker-field>`
|
||||
}
|
||||
</slot>
|
||||
</div>
|
||||
${
|
||||
this._pickerWrapperOpen || this._opened
|
||||
@@ -351,25 +298,9 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _loadingLabel() {
|
||||
const label = this.ariaLabel || this.label || this.placeholder;
|
||||
return label
|
||||
? this._i18n?.localize("ui.components.generic-picker.loading", {
|
||||
label,
|
||||
})
|
||||
: this._i18n?.localize("ui.common.loading");
|
||||
}
|
||||
|
||||
private _retry(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "picker-retry");
|
||||
}
|
||||
|
||||
private _setUnknownValue = () => {
|
||||
const items = this.getItems();
|
||||
if (
|
||||
this.loading ||
|
||||
this.loadError ||
|
||||
this.noUnknownState ||
|
||||
this.allowCustomValue ||
|
||||
this.value === undefined ||
|
||||
@@ -482,8 +413,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
}
|
||||
) {
|
||||
ev?.stopPropagation();
|
||||
|
||||
if (this.disabled || this.loading || this.loadError) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
this._selectedValue = options?.selectedValue;
|
||||
@@ -529,53 +459,6 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
/* The error shares the field's cell, so a wrapping error grows the
|
||||
picker instead of covering what follows */
|
||||
#picker {
|
||||
position: relative;
|
||||
display: grid;
|
||||
}
|
||||
#picker > .field,
|
||||
#picker > .load-error {
|
||||
grid-area: 1 / 1;
|
||||
min-width: 0;
|
||||
}
|
||||
/* The field keeps its place under the skeleton, so nothing moves and
|
||||
the two cross fade once loading ends */
|
||||
.field {
|
||||
transition: opacity var(--ha-animation-duration-fast, 150ms) ease-out;
|
||||
}
|
||||
:host([loading]) .field,
|
||||
.field[inert] {
|
||||
opacity: 0;
|
||||
}
|
||||
.loading-skeleton {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
--ha-skeleton-border-radius: var(--ha-border-radius-sm);
|
||||
transition: opacity var(--ha-animation-duration-fast, 150ms) ease-out;
|
||||
}
|
||||
@starting-style {
|
||||
.loading-skeleton {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
/* Hidden once faded out, and at once when no fade runs */
|
||||
:host(:not([loading])) .loading-skeleton {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition:
|
||||
opacity var(--ha-animation-duration-fast, 150ms) ease-out,
|
||||
visibility 0s linear var(--ha-animation-duration-fast, 150ms);
|
||||
}
|
||||
.load-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.load-error ha-alert {
|
||||
flex: 1;
|
||||
}
|
||||
label[disabled] {
|
||||
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.6));
|
||||
}
|
||||
@@ -657,6 +540,5 @@ declare global {
|
||||
interface HASSDomEvents {
|
||||
"picker-closed": undefined;
|
||||
"picker-opened": undefined;
|
||||
"picker-retry": undefined;
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,6 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
allowSvg: this.allowSvg,
|
||||
allowDataUrl: this.allowDataUrl,
|
||||
breaks: this.breaks,
|
||||
rewriteUrl: Boolean(this.rewriteUrl),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,7 +99,6 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
{
|
||||
allowSvg: this.allowSvg,
|
||||
allowDataUrl: this.allowDataUrl,
|
||||
allowRelativeUrls: Boolean(this.rewriteUrl),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -178,8 +178,7 @@ export class HaNavigationPicker extends LitElement {
|
||||
allow-custom-value
|
||||
.placeholder=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.loading=${this._loading}
|
||||
.disabled=${this._loading || this.disabled}
|
||||
.required=${this.required}
|
||||
.getItems=${this._getItems}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
|
||||
@@ -162,7 +162,7 @@ export class HaObjectSelector extends LitElement {
|
||||
description = this._formatField(item, descriptionField);
|
||||
}
|
||||
|
||||
const reorderable = this._reorderable;
|
||||
const reorderable = this.selector.object!.multiple || false;
|
||||
const multiple = this.selector.object!.multiple || false;
|
||||
return html`
|
||||
<ha-list-item-base class="item">
|
||||
@@ -212,7 +212,6 @@ export class HaObjectSelector extends LitElement {
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<div class="items-container">
|
||||
<ha-sortable
|
||||
.disabled=${!this._reorderable}
|
||||
handle-selector=".handle"
|
||||
draggable-selector=".item"
|
||||
@item-moved=${this._itemMoved}
|
||||
@@ -292,18 +291,11 @@ export class HaObjectSelector extends LitElement {
|
||||
}));
|
||||
});
|
||||
|
||||
private get _reorderable(): boolean {
|
||||
return (
|
||||
(this.selector.object?.multiple ?? false) &&
|
||||
this.selector.object?.reorder !== false
|
||||
);
|
||||
}
|
||||
|
||||
private _itemMoved(ev) {
|
||||
ev.stopPropagation();
|
||||
const newIndex = ev.detail.newIndex;
|
||||
const oldIndex = ev.detail.oldIndex;
|
||||
if (!this._reorderable) {
|
||||
if (!this.selector.object!.multiple) {
|
||||
return;
|
||||
}
|
||||
const newValue = ensureArray(this.value ?? []).concat();
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import TabGroup from "@home-assistant/webawesome/dist/components/tab-group/tab-group";
|
||||
import { css, type CSSResultGroup, type PropertyValues } from "lit";
|
||||
import { css, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { SortableEvent } from "sortablejs";
|
||||
import { DragScrollController } from "../common/controllers/drag-scroll-controller";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { SortableInstance } from "../resources/sortable";
|
||||
|
||||
const TOUCH_DRAG_DELAY_MS = 300;
|
||||
|
||||
const AUTO_SCROLL_SENSITIVITY = 30;
|
||||
|
||||
@customElement("ha-tab-group")
|
||||
export class HaTabGroup extends TabGroup {
|
||||
@@ -20,199 +13,24 @@ export class HaTabGroup extends TabGroup {
|
||||
|
||||
@property({ attribute: "tab-only", type: Boolean }) tabOnly = true;
|
||||
|
||||
/**
|
||||
* Allow reordering tabs by dragging or with Alt+ArrowUp/ArrowDown. Fires
|
||||
* `item-moved`; the owner applies the new order.
|
||||
*/
|
||||
@property({ type: Boolean }) public sortable = false;
|
||||
|
||||
/** Drag handle that starts a touch drag without the long press. */
|
||||
@property({ attribute: "handle-selector" }) public handleSelector?: string;
|
||||
|
||||
private _sortable?: SortableInstance;
|
||||
|
||||
private _sortPlaceholder?: Comment;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
// Prevent the tab group from consuming Alt+Arrow and Cmd+Arrow keys,
|
||||
// which browsers use for back/forward navigation.
|
||||
this.addEventListener("keydown", this._handleKeyDown, true);
|
||||
if (this.hasUpdated && this.sortable) {
|
||||
this._createSortable();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("keydown", this._handleKeyDown, true);
|
||||
this._destroySortable();
|
||||
}
|
||||
|
||||
protected override updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("sortable")) {
|
||||
// Dragging the tab bar to scroll would compete with dragging a tab.
|
||||
this._dragScrollController.enabled = !this.sortable;
|
||||
if (this.sortable) {
|
||||
this._createSortable();
|
||||
} else {
|
||||
this._destroySortable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _createSortable() {
|
||||
if (this._sortable) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const Sortable = (await import("../resources/sortable")).default;
|
||||
if (this._sortable || !this.sortable || !this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
// supportPointer is missing from the SortableJS typings.
|
||||
const options: SortableInstance.Options & { supportPointer?: boolean } = {
|
||||
draggable: this.tabTag,
|
||||
// SortableJS guesses vertical from the tabs being display: block.
|
||||
direction:
|
||||
this.placement === "start" || this.placement === "end"
|
||||
? "vertical"
|
||||
: "horizontal",
|
||||
// The tab bar scrolls inside the shadow root, where SortableJS can't find it.
|
||||
scroll: this.nav,
|
||||
// Force the autoscroll fallback because it works better than the native one
|
||||
forceAutoScrollFallback: true,
|
||||
scrollSensitivity: AUTO_SCROLL_SENSITIVITY,
|
||||
scrollSpeed: 20,
|
||||
animation: 150,
|
||||
// Keep swiping on touch screens for scrolling the tab bar.
|
||||
delay: TOUCH_DRAG_DELAY_MS,
|
||||
delayOnTouchOnly: true,
|
||||
// A scrolling swipe ends in pointercancel, after which SortableJS
|
||||
// ignores the next drag. Touch events end with touchend instead.
|
||||
supportPointer: false,
|
||||
onStart: this._handleSortableStart,
|
||||
onEnd: this._handleSortableEnd,
|
||||
onUpdate: this._handleSortableUpdate,
|
||||
};
|
||||
this._sortable = new Sortable(this, options);
|
||||
// Capture, so the delay is set before SortableJS reads it.
|
||||
this.addEventListener("touchstart", this._handleTouchStart, {
|
||||
capture: true,
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
||||
private _destroySortable() {
|
||||
this._sortable?.destroy();
|
||||
this._sortable = undefined;
|
||||
this.removeEventListener("touchstart", this._handleTouchStart, true);
|
||||
this._removeRtlAutoScrollListeners();
|
||||
}
|
||||
|
||||
private _handleTouchStart = (ev: TouchEvent) => {
|
||||
const onHandle =
|
||||
!!this.handleSelector &&
|
||||
ev
|
||||
.composedPath()
|
||||
.some(
|
||||
(el) => el instanceof Element && el.matches(this.handleSelector!)
|
||||
);
|
||||
this._sortable!.option("delay", onHandle ? 0 : TOUCH_DRAG_DELAY_MS);
|
||||
};
|
||||
|
||||
private _handleSortableStart = (evt: SortableEvent) => {
|
||||
// Put the tab back after dropping, the owner renders the new order.
|
||||
this._sortPlaceholder = document.createComment("sort-placeholder");
|
||||
evt.item.after(this._sortPlaceholder);
|
||||
// Touch drags add a copy of the tab to this element, which the tab group
|
||||
// would show and select as an extra tab. The dimmed tab shows the position.
|
||||
this.querySelector(":scope > .sortable-fallback")?.remove();
|
||||
if (getComputedStyle(this.nav).direction !== "rtl") {
|
||||
return;
|
||||
}
|
||||
// Capture, so this runs before SortableJS checks for auto scroll.
|
||||
const options = { capture: true, passive: true };
|
||||
window.addEventListener("dragover", this._unblockRtlAutoScroll, options);
|
||||
window.addEventListener("touchmove", this._unblockRtlAutoScroll, options);
|
||||
};
|
||||
|
||||
private _removeRtlAutoScrollListeners() {
|
||||
window.removeEventListener("dragover", this._unblockRtlAutoScroll, true);
|
||||
window.removeEventListener("touchmove", this._unblockRtlAutoScroll, true);
|
||||
}
|
||||
|
||||
// SortableJS only auto scrolls left while scrollLeft isn't 0, but in RTL the
|
||||
// tab bar starts at 0 and scrolls left into negative values.
|
||||
private _unblockRtlAutoScroll = (ev: DragEvent | TouchEvent) => {
|
||||
const x = "touches" in ev ? ev.touches[0]?.clientX : ev.clientX;
|
||||
if (x === undefined || this.nav.scrollLeft !== 0) {
|
||||
return;
|
||||
}
|
||||
const left = this.nav.getBoundingClientRect().left;
|
||||
if (Math.abs(x - left) <= AUTO_SCROLL_SENSITIVITY) {
|
||||
this.nav.scrollLeft = -1;
|
||||
}
|
||||
};
|
||||
|
||||
private _handleSortableEnd = (evt: SortableEvent) => {
|
||||
this._removeRtlAutoScrollListeners();
|
||||
this._sortPlaceholder?.replaceWith(evt.item);
|
||||
this._sortPlaceholder = undefined;
|
||||
};
|
||||
|
||||
private _handleSortableUpdate = (evt: SortableEvent) => {
|
||||
fireEvent(this, "item-moved", {
|
||||
oldIndex: evt.oldIndex!,
|
||||
newIndex: evt.newIndex!,
|
||||
});
|
||||
};
|
||||
|
||||
private _handleKeyDown = (event: KeyboardEvent) => {
|
||||
// The tab group cancels Enter and Space, so buttons inside a tab would not
|
||||
// be pressed.
|
||||
if (
|
||||
(event.key === "Enter" || event.key === " ") &&
|
||||
event.composedPath()[0] instanceof HTMLButtonElement &&
|
||||
(event.target as HTMLElement).closest(this.tabTag)
|
||||
) {
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if (event.altKey || event.metaKey) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
if (
|
||||
this.sortable &&
|
||||
event.altKey &&
|
||||
!(event.ctrlKey || event.metaKey) &&
|
||||
!event.shiftKey &&
|
||||
(event.key === "ArrowUp" || event.key === "ArrowDown")
|
||||
) {
|
||||
this._moveTabWithKeyboard(event);
|
||||
}
|
||||
};
|
||||
|
||||
// Up and down follow the tab order, so they work the same in RTL.
|
||||
private _moveTabWithKeyboard(event: KeyboardEvent) {
|
||||
const tab = (event.target as HTMLElement).closest(this.tabTag);
|
||||
if (!tab) {
|
||||
return;
|
||||
}
|
||||
const tabs = [...this.children].filter(
|
||||
(el) => el.localName === this.tabTag
|
||||
);
|
||||
const oldIndex = tabs.indexOf(tab);
|
||||
const newIndex = oldIndex + (event.key === "ArrowUp" ? -1 : 1);
|
||||
event.preventDefault();
|
||||
if (oldIndex === -1 || newIndex < 0 || newIndex >= tabs.length) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "item-moved", { oldIndex, newIndex });
|
||||
}
|
||||
|
||||
protected override handleClick(event: MouseEvent) {
|
||||
if (this._dragScrollController.scrolled) {
|
||||
return;
|
||||
@@ -249,10 +67,6 @@ export class HaTabGroup extends TabGroup {
|
||||
margin-inline-start: calc(-1 * var(--track-width));
|
||||
}
|
||||
|
||||
::slotted(.sortable-ghost) {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.scroll-button::part(base):hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
distanceMeters,
|
||||
} from "../../common/map/map-engine";
|
||||
import type { MapStyleConfig } from "../../common/map/map-styles";
|
||||
import { isDarkMapStyle, resolveMapStyle } from "../../common/map/map-styles";
|
||||
import { resolveMapStyle } from "../../common/map/map-styles";
|
||||
import { readMapThemeColors } from "../../common/map/map-theme-colors";
|
||||
import { editableCircleStyles } from "../../common/map/editable-circle";
|
||||
import { entityMapColor, zoneColor } from "../../common/map/entity-map-colors";
|
||||
@@ -618,15 +618,15 @@ export class HaMap extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (changedProps.has("mapStyle")) {
|
||||
this._engine?.setMapStyle(this._resolvedMapStyle);
|
||||
}
|
||||
|
||||
const oldUi = changedProps.get("_ui") as HomeAssistantUI | undefined;
|
||||
if (
|
||||
!changedProps.has("themeMode") &&
|
||||
(!changedProps.has("_ui") || (oldUi && oldUi.themes === this._ui.themes))
|
||||
) {
|
||||
// A style can have one half only, so the chrome follows it too
|
||||
if (changedProps.has("mapStyle")) {
|
||||
this._updateMapAppearance();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -647,11 +647,6 @@ export class HaMap extends ReactiveElement {
|
||||
);
|
||||
}
|
||||
|
||||
// What the map draws, which a style with one half decides over the theme mode
|
||||
private get _styleDark() {
|
||||
return isDarkMapStyle(this.mapStyle, this._darkMode);
|
||||
}
|
||||
|
||||
// Memoized: it is read on every appearance sync, and the engines compare
|
||||
// what they are handed against what they applied.
|
||||
private _resolveMapStyle = memoizeOne(resolveMapStyle);
|
||||
@@ -664,12 +659,12 @@ export class HaMap extends ReactiveElement {
|
||||
// arguments, and a fresh object per read would defeat it.
|
||||
private _readThemeColors(): void {
|
||||
// Only one set of --ha-color-map-* is live on the page, the one for the
|
||||
// mode the page is in. A card forced to the other mode, or a style with
|
||||
// only the other half, draws the other palette, so those values would be
|
||||
// the wrong half of the theme; the style's own colors are the better answer.
|
||||
// mode the page is in. A card forced to the other mode draws the other
|
||||
// palette, so those values would be the wrong half of the theme; the
|
||||
// style's own colors are the better answer there.
|
||||
const pageDark = Boolean(this._ui?.themes?.darkMode);
|
||||
const colors =
|
||||
this._styleDark === pageDark ? readMapThemeColors(this) : undefined;
|
||||
this._darkMode === pageDark ? readMapThemeColors(this) : undefined;
|
||||
if (!deepEqual(colors, this._themeColors)) {
|
||||
this._themeColors = colors;
|
||||
}
|
||||
@@ -695,19 +690,13 @@ export class HaMap extends ReactiveElement {
|
||||
return;
|
||||
}
|
||||
map.classList.toggle("clickable", this.clickable);
|
||||
map.classList.toggle("dark", this._styleDark);
|
||||
map.classList.toggle("dark", this._darkMode);
|
||||
map.classList.toggle("drawn", this._mapDrawn);
|
||||
// The sky belongs behind a drawn globe; on a blank canvas it is just a
|
||||
// gradient with a glow in it
|
||||
map.classList.toggle("space", this._vectorEngine && this._mapDrawn);
|
||||
map.classList.toggle(
|
||||
"forced-dark",
|
||||
this._styleDark && (this.themeMode === "dark" || !this._darkMode)
|
||||
);
|
||||
map.classList.toggle(
|
||||
"forced-light",
|
||||
!this._styleDark && (this.themeMode === "light" || this._darkMode)
|
||||
);
|
||||
map.classList.toggle("forced-dark", this.themeMode === "dark");
|
||||
map.classList.toggle("forced-light", this.themeMode === "light");
|
||||
this._engine?.setMapStyle(this._resolvedMapStyle);
|
||||
}
|
||||
|
||||
@@ -776,7 +765,7 @@ export class HaMap extends ReactiveElement {
|
||||
map.id = "map";
|
||||
// Which ground shows in the gap before the first frame; the rest of the
|
||||
// classes wait for the engine (_updateMapAppearance)
|
||||
map.classList.toggle("dark", this._styleDark);
|
||||
map.classList.toggle("dark", this._darkMode);
|
||||
this.shadowRoot!.append(map);
|
||||
this._loading = true;
|
||||
this._mapDrawn = false;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
mdiChartLine,
|
||||
mdiClose,
|
||||
mdiDevices,
|
||||
mdiHome,
|
||||
@@ -58,11 +57,6 @@ import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import type { LabelRegistryEntry } from "../../data/label/label_registry";
|
||||
import {
|
||||
getStatisticMetadata,
|
||||
isExternalStatistic,
|
||||
type StatisticsMetaData,
|
||||
} from "../../data/recorder";
|
||||
import {
|
||||
deviceMeetsFilter,
|
||||
extractFromTarget,
|
||||
@@ -183,9 +177,6 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
|
||||
@state() private _entries?: ExtractFromTargetResult;
|
||||
|
||||
/** Metadata of an external statistic picked as entity, which has no state. */
|
||||
@state() private _statistic?: StatisticsMetaData;
|
||||
|
||||
private _fetchedEntries?: ExtractFromTargetResult;
|
||||
|
||||
private _lastFetch = 0;
|
||||
@@ -782,19 +773,6 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
this._fetchedEntries = undefined;
|
||||
if (this.type === "entity") {
|
||||
this._entries = undefined;
|
||||
this._statistic = undefined;
|
||||
if (isExternalStatistic(this.itemId)) {
|
||||
try {
|
||||
const [metadata] = await getStatisticMetadata(this._api.callWS, [
|
||||
this.itemId,
|
||||
]);
|
||||
if (requestId === this._lastFetch) {
|
||||
this._statistic = metadata;
|
||||
}
|
||||
} catch {
|
||||
// Without metadata the statistic is shown as not found
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -881,15 +859,6 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
}
|
||||
if (type === "entity") {
|
||||
const stateObject: HassEntity | undefined = this._states?.[item];
|
||||
// External statistics have no state, only the metadata the recorder keeps.
|
||||
if (!stateObject && this._statistic?.statistic_id === item) {
|
||||
return {
|
||||
name: this._statistic.name || item,
|
||||
context: domainToName(this._i18n.localize, this._statistic.source),
|
||||
fallbackIconPath: mdiChartLine,
|
||||
notFound: false,
|
||||
};
|
||||
}
|
||||
const { primary, secondary } = stateObject
|
||||
? computeEntityPickerDisplay(
|
||||
{
|
||||
|
||||
@@ -48,8 +48,6 @@ class HaUserPicker extends LitElement {
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: ContextType<typeof apiContext>;
|
||||
|
||||
@state() private _loadFailed = false;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
if (!this.users) {
|
||||
@@ -58,18 +56,7 @@ class HaUserPicker extends LitElement {
|
||||
}
|
||||
|
||||
private async _fetchUsers() {
|
||||
this._loadFailed = false;
|
||||
|
||||
try {
|
||||
this.users = await fetchUsers(this._api.callWS);
|
||||
} catch {
|
||||
this._loadFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _retryFetchUsers(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._fetchUsers();
|
||||
this.users = await fetchUsers(this._api.callWS);
|
||||
}
|
||||
|
||||
private usersMap = memoizeOne((users?: User[]): Map<string, User> => {
|
||||
@@ -155,16 +142,9 @@ class HaUserPicker extends LitElement {
|
||||
.getItems=${this._getItems}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.loading=${!this._loadFailed && this.users === undefined}
|
||||
.loadError=${
|
||||
this._loadFailed
|
||||
? this._localize("ui.components.user-picker.load_failed")
|
||||
: undefined
|
||||
}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.unknownItemText=${this._localize("ui.components.user-picker.unknown")}
|
||||
@value-changed=${this._valueChanged}
|
||||
@picker-retry=${this._retryFetchUsers}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { getCollection } from "home-assistant-js-websocket";
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
import type { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -27,12 +27,12 @@ export const fetchCategoryRegistry = (conn: Connection, scope: string) =>
|
||||
categories.sort((ent1, ent2) => stringCompare(ent1.name, ent2.name))
|
||||
);
|
||||
|
||||
export const getCategoryRegistryCollection = (
|
||||
export const subscribeCategoryRegistry = (
|
||||
conn: Connection,
|
||||
scope: string
|
||||
scope: string,
|
||||
onChange: (floors: CategoryRegistryEntry[]) => void
|
||||
) =>
|
||||
getCollection<CategoryRegistryEntry[]>(
|
||||
conn,
|
||||
createCollection<CategoryRegistryEntry[]>(
|
||||
`_categoryRegistry_${scope}`,
|
||||
(conn2: Connection) => fetchCategoryRegistry(conn2, scope),
|
||||
(conn2: Connection, store: Store<CategoryRegistryEntry[]>) =>
|
||||
@@ -47,15 +47,11 @@ export const getCategoryRegistryCollection = (
|
||||
true
|
||||
),
|
||||
"category_registry_updated"
|
||||
)
|
||||
),
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
export const subscribeCategoryRegistry = (
|
||||
conn: Connection,
|
||||
scope: string,
|
||||
onChange: (floors: CategoryRegistryEntry[]) => void
|
||||
) => getCategoryRegistryCollection(conn, scope).subscribe(onChange);
|
||||
|
||||
export const createCategoryRegistryEntry = (
|
||||
callWS: HomeAssistant["callWS"],
|
||||
scope: string,
|
||||
|
||||
+1
-60
@@ -422,7 +422,6 @@ export interface EnergyData {
|
||||
startCompare?: Date;
|
||||
endCompare?: Date;
|
||||
compareMode?: CompareMode;
|
||||
combineSources?: boolean;
|
||||
prefs: EnergyPreferences;
|
||||
info: EnergyInfo;
|
||||
stats: Statistics;
|
||||
@@ -946,13 +945,10 @@ export interface EnergyCollection extends Omit<
|
||||
start: Date;
|
||||
end?: Date;
|
||||
compare?: CompareMode;
|
||||
combineSources?: boolean;
|
||||
prefs?: EnergyPreferences;
|
||||
clearPrefs(): void;
|
||||
setPeriod(newStart: Date, newEnd?: Date): void;
|
||||
setCompare(compare: CompareMode): void;
|
||||
setCombineSources(combine: boolean): void;
|
||||
setDefaultCombineSources(combine: boolean): void;
|
||||
isActive(): boolean;
|
||||
_refreshTimeout?: number;
|
||||
_updatePeriodTimeout?: number;
|
||||
@@ -1040,24 +1036,6 @@ export const getEnergyDefaultPeriodStorageKey = (
|
||||
return `energy-default-period-${key}`;
|
||||
};
|
||||
|
||||
export const getEnergyCombineSourcesStorageKey = (
|
||||
panelUrl: string,
|
||||
collectionKey?: string
|
||||
): string => {
|
||||
const [key] = convertCollectionKeyToConnection(panelUrl, collectionKey);
|
||||
return `energy-combine-sources-${key}`;
|
||||
};
|
||||
|
||||
export const hasCombinableEnergySources = (
|
||||
prefs: EnergyPreferences,
|
||||
types?: EnergySource["type"][]
|
||||
): boolean => {
|
||||
const sources = types
|
||||
? prefs.energy_sources.filter((source) => types.includes(source.type))
|
||||
: prefs.energy_sources;
|
||||
return new Set(sources.map((source) => source.type)).size < sources.length;
|
||||
};
|
||||
|
||||
// When today's first hourly statistic becomes available (01:00 in the
|
||||
// configured timezone). Rolling the statistics view over at midnight would
|
||||
// show an empty graph.
|
||||
@@ -1202,11 +1180,6 @@ export const getEnergyDataCollection = (
|
||||
: preferredPeriod;
|
||||
|
||||
const [initialStart, initialEnd] = calcDateRange(locale, config, period);
|
||||
const combineSourcesStorageKey = getEnergyCombineSourcesStorageKey(
|
||||
panelUrl,
|
||||
options.key
|
||||
);
|
||||
const storedCombineSources = localStorage.getItem(combineSourcesStorageKey);
|
||||
// True while tracking today (or hour-0 yesterday), rather than a custom range.
|
||||
let followLiveDay = preferredPeriod === "today";
|
||||
|
||||
@@ -1261,7 +1234,7 @@ export const getEnergyDataCollection = (
|
||||
return;
|
||||
}
|
||||
publishedSelectionRevision = requestSelectionRevision;
|
||||
store.setState(withCombineSources(data), true);
|
||||
store.setState(data, true);
|
||||
};
|
||||
|
||||
// Use the collection's subscription lifecycle, but publish explicitly so an
|
||||
@@ -1381,23 +1354,6 @@ export const getEnergyDataCollection = (
|
||||
};
|
||||
};
|
||||
|
||||
// The setting only applies while a source type is configured twice, so it
|
||||
// cannot leave cards split once there is nothing left to combine.
|
||||
const withCombineSources = (data: EnergyData): EnergyData => ({
|
||||
...data,
|
||||
combineSources: hasCombinableEnergySources(data.prefs)
|
||||
? collection.combineSources
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// Display-only: re-publish the current data instead of fetching it again.
|
||||
const applyCombineSources = (combine: boolean) => {
|
||||
collection.combineSources = combine;
|
||||
if (store.state) {
|
||||
store.setState(withCombineSources(store.state), true);
|
||||
}
|
||||
};
|
||||
|
||||
const collection: EnergyCollection = {
|
||||
get state() {
|
||||
return store.state;
|
||||
@@ -1406,12 +1362,6 @@ export const getEnergyDataCollection = (
|
||||
subscribe,
|
||||
start: calcDate(initialStart, startOfDay, locale, config),
|
||||
end: calcDate(initialEnd, endOfDay, locale, config),
|
||||
combineSources:
|
||||
storedCombineSources === "true"
|
||||
? true
|
||||
: storedCombineSources === "false"
|
||||
? false
|
||||
: undefined,
|
||||
prefs: options.prefs,
|
||||
_active: 0,
|
||||
isActive: () => !!collection._active,
|
||||
@@ -1448,15 +1398,6 @@ export const getEnergyDataCollection = (
|
||||
collection.compare = compare;
|
||||
}
|
||||
},
|
||||
setCombineSources(combine: boolean) {
|
||||
applyCombineSources(combine);
|
||||
localStorage.setItem(combineSourcesStorageKey, String(combine));
|
||||
},
|
||||
setDefaultCombineSources(combine: boolean) {
|
||||
if (collection.combineSources === undefined) {
|
||||
applyCombineSources(combine);
|
||||
}
|
||||
},
|
||||
};
|
||||
// Custom cards also access the Energy collection directly on the connection.
|
||||
(connection as any)[key] = collection;
|
||||
|
||||
+3
-30
@@ -13,11 +13,7 @@ import { computeRTL } from "../common/util/compute_rtl";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isNumericSensorDeviceClass } from "./sensor";
|
||||
import type { FrontendLocaleData } from "./translation";
|
||||
import {
|
||||
isExternalStatistic,
|
||||
type Statistics,
|
||||
type StatisticsMetaData,
|
||||
} from "./recorder";
|
||||
import type { Statistics } from "./recorder";
|
||||
|
||||
const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"];
|
||||
const NEED_ATTRIBUTE_DOMAINS = [
|
||||
@@ -521,29 +517,11 @@ const isNumericFromAttributes = (attributes: Record<string, any>) =>
|
||||
|
||||
const BLANK_UNIT = " ";
|
||||
|
||||
/**
|
||||
* Attributes for an external statistic, which has no entity. Without them the
|
||||
* history names the series after its ID and shows it without a unit.
|
||||
*/
|
||||
const statisticAttributes = (
|
||||
metadata: StatisticsMetaData | undefined
|
||||
): Record<string, string> => {
|
||||
const attributes: Record<string, string> = {};
|
||||
if (metadata?.name) {
|
||||
attributes.friendly_name = metadata.name;
|
||||
}
|
||||
if (metadata?.statistics_unit_of_measurement) {
|
||||
attributes.unit_of_measurement = metadata.statistics_unit_of_measurement;
|
||||
}
|
||||
return attributes;
|
||||
};
|
||||
|
||||
export const convertStatisticsToHistory = (
|
||||
hass: HomeAssistant,
|
||||
statistics: Statistics,
|
||||
statisticIds: string[],
|
||||
splitDeviceClasses = false,
|
||||
statisticsMetadata?: Record<string, StatisticsMetaData>
|
||||
splitDeviceClasses = false
|
||||
): HistoryResult => {
|
||||
// Maintain the statistic id ordering
|
||||
const orderedStatistics: Statistics = {};
|
||||
@@ -556,15 +534,10 @@ export const convertStatisticsToHistory = (
|
||||
// Convert statistics to HistoryResult format
|
||||
const statsHistoryStates: HistoryStates = {};
|
||||
Object.entries(orderedStatistics).forEach(([key, value]) => {
|
||||
// Other statistics keep a blank unit, so that they merge into the history
|
||||
// line of their entity even after the entity was removed.
|
||||
const attributes = isExternalStatistic(key)
|
||||
? statisticAttributes(statisticsMetadata?.[key])
|
||||
: {};
|
||||
const entityHistoryStates: EntityHistoryState[] = value.map((e) => ({
|
||||
s: e.mean != null ? e.mean.toString() : e.state!.toString(),
|
||||
lc: e.end / 1000,
|
||||
a: attributes,
|
||||
a: {},
|
||||
lu: e.end / 1000,
|
||||
}));
|
||||
statsHistoryStates[key] = entityHistoryStates;
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface RepositoryBase {
|
||||
|
||||
export interface RepositoryInfo extends RepositoryBase {
|
||||
additional_info: string;
|
||||
beta: boolean;
|
||||
default_branch: string;
|
||||
issues: number;
|
||||
releases: string[];
|
||||
@@ -98,6 +99,18 @@ export const uninstallMarketplaceRepository = (
|
||||
repository: repositoryId,
|
||||
});
|
||||
|
||||
// The latest version and the updates include pre-releases when on
|
||||
export const setMarketplaceRepositoryBeta = (
|
||||
hass: CallWS,
|
||||
repositoryId: string,
|
||||
showBeta: boolean
|
||||
) =>
|
||||
hass.callWS<null>({
|
||||
type: "marketplace/repository/beta",
|
||||
repository: repositoryId,
|
||||
show_beta: showBeta,
|
||||
});
|
||||
|
||||
// Fetches the information of the repository from GitHub again
|
||||
export const refreshMarketplaceRepository = (
|
||||
hass: CallWS,
|
||||
|
||||
@@ -469,7 +469,6 @@ export interface ObjectSelector {
|
||||
translation_key?: string;
|
||||
fields?: Record<string, ObjectSelectorField>;
|
||||
multiple?: boolean;
|
||||
reorder?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { THEME_COLORS } from "../../common/color/compute-color";
|
||||
import { createDurationData } from "../../common/datetime/create_duration_data";
|
||||
import { formatDurationLong } from "../../common/datetime/format_duration";
|
||||
import { normalizeDuration } from "../../common/datetime/normalize_duration";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../common/entity/compute_entity_name_display";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { HomeAssistantFormatters } from "../../types";
|
||||
import type { AreaRegistryEntry } from "../area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../device/device_registry";
|
||||
@@ -154,25 +150,6 @@ export const formatSelectorValue = (
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
if ("ui_color" in selector) {
|
||||
const color = String(value);
|
||||
const extraOption = selector.ui_color?.extra_options?.find(
|
||||
(option) => option.value === color
|
||||
);
|
||||
if (extraOption) {
|
||||
return extraOption.label;
|
||||
}
|
||||
if (color === "none" || color === "state") {
|
||||
return options.localize(`ui.components.color-picker.${color}`);
|
||||
}
|
||||
if (THEME_COLORS.has(color)) {
|
||||
return options.localize(
|
||||
`ui.components.color-picker.colors.${color}` as LocalizeKeys
|
||||
);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
if ("duration" in selector) {
|
||||
const data = createDurationData(value);
|
||||
if (!data) {
|
||||
|
||||
@@ -3,14 +3,6 @@ import type { HomeAssistant } from "../types";
|
||||
export type SystemLogLevel =
|
||||
"critical" | "error" | "warning" | "info" | "debug";
|
||||
|
||||
// The levels Core's system log keeps, most severe first. It drops anything
|
||||
// below warning.
|
||||
export const SYSTEM_LOG_LEVELS = [
|
||||
"critical",
|
||||
"error",
|
||||
"warning",
|
||||
] as const satisfies readonly SystemLogLevel[];
|
||||
|
||||
export interface LoggedError {
|
||||
name: string;
|
||||
message: [string, ...string[]];
|
||||
|
||||
@@ -3,8 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-icon-next";
|
||||
import "../../components/item/ha-list-item-button";
|
||||
import "../../components/list/ha-list-base";
|
||||
import "../../components/ha-list-item";
|
||||
import type { DataEntryFlowStepMenu } from "../../data/data_entry_flow";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
@@ -80,30 +79,28 @@ class StepFlowMenu extends LitElement {
|
||||
|
||||
return html`
|
||||
${description ? html`<div class="content">${description}</div>` : nothing}
|
||||
<ha-list-base
|
||||
class="options"
|
||||
aria-label=${this.flowConfig.renderMenuHeader(this.hass, this.step)}
|
||||
>
|
||||
<div class="options">
|
||||
${options.map((option) => {
|
||||
const optionDescription = optionDescriptions[option];
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
.headline=${translations[option]}
|
||||
<ha-list-item
|
||||
hasMeta
|
||||
.step=${option}
|
||||
@click=${this._handleStep}
|
||||
?twoline=${optionDescription}
|
||||
?multiline-secondary=${optionDescription}
|
||||
>
|
||||
<span>${translations[option]}</span>
|
||||
${
|
||||
optionDescription
|
||||
? html`<span slot="supporting-text" class="description"
|
||||
>${optionDescription}</span
|
||||
>`
|
||||
? html`<span slot="secondary"> ${optionDescription} </span>`
|
||||
: nothing
|
||||
}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`;
|
||||
})}
|
||||
</ha-list-base>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -132,14 +129,8 @@ class StepFlowMenu extends LitElement {
|
||||
.content + .options {
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
ha-list-item-button {
|
||||
--ha-row-item-padding-inline: var(--ha-space-6);
|
||||
}
|
||||
.description {
|
||||
white-space: normal;
|
||||
}
|
||||
ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
ha-list-item {
|
||||
--mdc-list-side-padding: var(--ha-space-6);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -122,13 +122,7 @@ export class MockBaseEntity {
|
||||
this.attributes = { ...this.attributes, ...changes.attributes };
|
||||
}
|
||||
|
||||
// A removed or replaced entity can finish its transitions, so that their
|
||||
// timers stop, but must not bring its state back
|
||||
if (this.hass?.mockEntities[this.entityId] !== this) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.updateStates({
|
||||
this.hass!.updateStates({
|
||||
[this.entityId]: this.toState(),
|
||||
});
|
||||
}
|
||||
@@ -165,11 +159,7 @@ export class MockBaseEntity {
|
||||
return {
|
||||
entity_id: this.entityId,
|
||||
state: this.state,
|
||||
// Every attribute of the input, like Core keeps the attributes of an
|
||||
// integration. What the mock works out itself wins, like a light that
|
||||
// is off having no brightness.
|
||||
attributes: {
|
||||
...this.attributes,
|
||||
...this._getBaseAttributes(),
|
||||
...this._getCapabilityAttributes(),
|
||||
...this._getStateAttributes(),
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { MockBaseEntity } from "./base-entity";
|
||||
import type { EntityAttributes } from "./types";
|
||||
|
||||
/**
|
||||
* Entity for domains without their own mock, such as sensor, weather and
|
||||
* calendar. Keeps all its attributes, as nothing here computes them.
|
||||
*/
|
||||
export class MockGenericEntity extends MockBaseEntity {
|
||||
protected _getStateAttributes(): EntityAttributes {
|
||||
return this.attributes;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { MockBaseEntity } from "./base-entity";
|
||||
import { MockBaseEntity } from "./base-entity";
|
||||
import type { EntityInput } from "./types";
|
||||
import { MockAlarmControlPanelEntity } from "./alarm-control-panel-entity";
|
||||
import { MockAutomationEntity } from "./automation-entity";
|
||||
import { MockClimateEntity } from "./climate-entity";
|
||||
import { MockCoverEntity } from "./cover-entity";
|
||||
import { MockFanEntity } from "./fan-entity";
|
||||
import { MockGenericEntity } from "./generic-entity";
|
||||
import { MockGroupEntity } from "./group-entity";
|
||||
import { MockHumidifierEntity } from "./humidifier-entity";
|
||||
import { MockInputNumberEntity } from "./input-number-entity";
|
||||
@@ -50,6 +49,5 @@ const TYPES: Record<string, EntityConstructor> = {
|
||||
|
||||
export const getEntity = (input: EntityInput): MockBaseEntity => {
|
||||
const [domain] = input.entity_id.split(".", 2);
|
||||
|
||||
return new (TYPES[domain] || MockGenericEntity)(input);
|
||||
return new (TYPES[domain] || MockBaseEntity)(input);
|
||||
};
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
invalidateThemeCache,
|
||||
} from "../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDeviceName } from "../common/entity/compute_device_name";
|
||||
import { computeEntityNameDisplayWithoutContext } from "../common/entity/compute_entity_name_display";
|
||||
import { computeFormatFunctions } from "../common/translations/entity-state";
|
||||
import { computeLocalize } from "../common/translations/localize";
|
||||
@@ -47,7 +46,6 @@ import { demoPanels } from "./demo_panels";
|
||||
import { demoServices } from "./demo_services";
|
||||
import { ENTITY_COMPONENT_ICONS } from "./entity_component_icons";
|
||||
import { getEntity } from "./entities/registry";
|
||||
import type { MockBaseEntity } from "./entities/base-entity";
|
||||
import type { EntityInput } from "./entities/types";
|
||||
|
||||
const ensureArray = <T>(val: T | T[]): T[] =>
|
||||
@@ -77,8 +75,6 @@ export interface MockHomeAssistant extends HomeAssistant {
|
||||
updateHass(obj: Partial<MockHomeAssistant>);
|
||||
updateStates(newStates: HassEntities);
|
||||
addEntities(entities: EntityInput | EntityInput[], replace?: boolean);
|
||||
// Recomputes the format functions, such as after the areas change.
|
||||
updateFormatFunctions(): Promise<void>;
|
||||
updateTranslations(fragment: null | string, language?: string);
|
||||
addTranslations(translations: Record<string, string>, language?: string);
|
||||
mockWS<T extends (...args) => any = any>(
|
||||
@@ -301,77 +297,34 @@ export const provideHass = (
|
||||
});
|
||||
}
|
||||
|
||||
// Like Core, an entity of a device has only its own name, which follows the
|
||||
// name of the device in its friendly name. An entity named like its device
|
||||
// has no name of its own.
|
||||
const registryName = (
|
||||
ent: MockBaseEntity
|
||||
): Pick<EntityRegistryDisplayEntry, "name" | "has_entity_name"> => {
|
||||
const friendlyName: string | undefined =
|
||||
ent.attributes.friendly_name || undefined;
|
||||
const device = ent.deviceId ? hass().devices[ent.deviceId] : undefined;
|
||||
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||
|
||||
if (
|
||||
!deviceName ||
|
||||
!friendlyName ||
|
||||
(friendlyName !== deviceName &&
|
||||
!friendlyName.startsWith(`${deviceName} `))
|
||||
) {
|
||||
return { name: friendlyName };
|
||||
}
|
||||
|
||||
return {
|
||||
name: friendlyName.slice(deviceName.length).trim() || undefined,
|
||||
has_entity_name: true,
|
||||
};
|
||||
};
|
||||
|
||||
function addEntities(
|
||||
newEntities: EntityInput | EntityInput[],
|
||||
replace = false
|
||||
) {
|
||||
if (replace) {
|
||||
// Drop the old mock entities too, so that services cannot bring them back.
|
||||
for (const entityId of Object.keys(entities)) {
|
||||
delete entities[entityId];
|
||||
}
|
||||
}
|
||||
|
||||
const states = {};
|
||||
|
||||
// A new object, so that registry consumers see the change
|
||||
const registry: HomeAssistant["entities"] = replace
|
||||
? {}
|
||||
: { ...hass().entities };
|
||||
ensureArray(newEntities).forEach((input) => {
|
||||
const ent = getEntity(input);
|
||||
ent.hass = hass();
|
||||
entities[ent.entityId] = ent;
|
||||
states[ent.entityId] = ent.toState();
|
||||
|
||||
const name = registryName(ent);
|
||||
registry[ent.entityId] = {
|
||||
hass().entities[ent.entityId] = {
|
||||
entity_id: ent.entityId,
|
||||
...name,
|
||||
name: ent.attributes.friendly_name || undefined,
|
||||
icon: undefined,
|
||||
platform: ent.platform ?? "demo",
|
||||
labels: [],
|
||||
area_id: ent.areaId,
|
||||
device_id: ent.deviceId,
|
||||
// Only follow the device when its name was split off, so a friendly
|
||||
// name that doesn't start with it is shown as it is
|
||||
next_name_part: ent.areaId
|
||||
? "area"
|
||||
: name.has_entity_name
|
||||
? "device"
|
||||
: undefined,
|
||||
} satisfies EntityRegistryDisplayEntry;
|
||||
});
|
||||
hass().updateHass({
|
||||
entities: registry,
|
||||
states: replace ? states : { ...hass().states, ...states },
|
||||
});
|
||||
if (replace) {
|
||||
hass().updateHass({
|
||||
states,
|
||||
});
|
||||
} else {
|
||||
updateStates(states);
|
||||
}
|
||||
|
||||
updateFormatFunctions();
|
||||
}
|
||||
@@ -394,18 +347,15 @@ export const provideHass = (
|
||||
|
||||
const localLanguage = getLocalLanguage();
|
||||
const noop = () => undefined;
|
||||
const auth = {
|
||||
data: {
|
||||
hassUrl: location.origin,
|
||||
},
|
||||
} as any;
|
||||
|
||||
const hassObj: MockHomeAssistant = {
|
||||
// Home Assistant properties
|
||||
auth,
|
||||
auth: {
|
||||
data: {
|
||||
hassUrl: location.origin,
|
||||
},
|
||||
} as any,
|
||||
connection: {
|
||||
// The map reads the URL of Home Assistant from here
|
||||
options: { auth },
|
||||
addEventListener: noop,
|
||||
removeEventListener: noop,
|
||||
sendMessage: (msg) => {
|
||||
@@ -590,7 +540,6 @@ export const provideHass = (
|
||||
return hass().localize;
|
||||
},
|
||||
addEntities,
|
||||
updateFormatFunctions,
|
||||
mockWS(type, callback) {
|
||||
wsCommands[type] = callback;
|
||||
},
|
||||
|
||||
@@ -175,7 +175,7 @@ class OnboardingCoreConfig extends LitElement {
|
||||
suggested = lang;
|
||||
}
|
||||
}
|
||||
this._country ??= suggested;
|
||||
this._country = suggested;
|
||||
|
||||
fireEvent(this, "onboarding-progress", { increase: 0.5 });
|
||||
await this.updateComplete;
|
||||
|
||||
@@ -306,7 +306,7 @@ class OnboardingLocation extends LitElement {
|
||||
(plc) => plc.place_id === Number(ev.detail.id)
|
||||
)!;
|
||||
this._location = [Number(place.lat), Number(place.lon)];
|
||||
this._setCountry(place.address.country_code);
|
||||
this._country = place.address.country_code.toUpperCase();
|
||||
}
|
||||
|
||||
private _itemClicked(ev) {
|
||||
@@ -315,7 +315,7 @@ class OnboardingLocation extends LitElement {
|
||||
(plc) => plc.place_id === ev.currentTarget.placeId
|
||||
)!;
|
||||
this._location = [Number(place.lat), Number(place.lon)];
|
||||
this._setCountry(place.address.country_code);
|
||||
this._country = place.address.country_code.toUpperCase();
|
||||
}
|
||||
|
||||
private async _addressSearch(ev: KeyboardEvent) {
|
||||
@@ -353,8 +353,7 @@ class OnboardingLocation extends LitElement {
|
||||
Number(this._places[0].lat),
|
||||
Number(this._places[0].lon),
|
||||
];
|
||||
this._setCountry(this._places[0].address.country_code);
|
||||
this._elevation = undefined;
|
||||
this._country = this._places[0].address.country_code.toUpperCase();
|
||||
}
|
||||
} catch (e: any) {
|
||||
this._places = undefined;
|
||||
@@ -370,19 +369,11 @@ class OnboardingLocation extends LitElement {
|
||||
}
|
||||
this._places = null;
|
||||
const reverse = await reverseGeocode(this._location, this.hass);
|
||||
this._setCountry(reverse.address.country_code);
|
||||
this._country = reverse.address.country_code.toUpperCase();
|
||||
this._places = [reverse];
|
||||
this._highlightedMarker = reverse.place_id;
|
||||
}
|
||||
|
||||
private _setCountry(countryCode: string) {
|
||||
this._country = countryCode.toUpperCase();
|
||||
// Detected values belong to the detected location
|
||||
this._unitSystem = undefined;
|
||||
this._currency = undefined;
|
||||
this._timeZone = undefined;
|
||||
}
|
||||
|
||||
private async _handleButtonClick(ev) {
|
||||
if (this._search) {
|
||||
this._searchAddress(ev.target.parentElement.value);
|
||||
|
||||
@@ -101,8 +101,8 @@ class SupervisorAppConfigDashboard extends LitElement {
|
||||
css`
|
||||
.content {
|
||||
margin: auto;
|
||||
padding: var(--ha-space-2) var(--ha-content-gutter);
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
padding: var(--ha-space-2);
|
||||
max-width: 1024px;
|
||||
}
|
||||
supervisor-app-network,
|
||||
supervisor-app-audio,
|
||||
|
||||
@@ -65,8 +65,8 @@ class SupervisorAppDocumentationDashboard extends LitElement {
|
||||
}
|
||||
.content {
|
||||
margin: auto;
|
||||
padding: var(--ha-space-2) var(--ha-content-gutter);
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
padding: var(--ha-space-2);
|
||||
max-width: 1024px;
|
||||
direction: ltr;
|
||||
}
|
||||
ha-markdown {
|
||||
|
||||
@@ -45,8 +45,8 @@ class SupervisorAppInfoDashboard extends LitElement {
|
||||
css`
|
||||
.content {
|
||||
margin: auto;
|
||||
padding: var(--ha-space-4) var(--ha-content-gutter);
|
||||
max-width: var(--ha-content-width-wide);
|
||||
padding: var(--ha-space-4);
|
||||
max-width: 1200px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -1593,6 +1593,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
.app {
|
||||
display: flex;
|
||||
gap: var(--ha-space-4);
|
||||
max-width: 1200px;
|
||||
}
|
||||
.app.column {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -99,9 +99,9 @@ class HaConfigAppsInfo extends LitElement {
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: var(--ha-space-4) var(--ha-content-gutter);
|
||||
padding: var(--ha-space-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-4);
|
||||
|
||||
@@ -980,8 +980,9 @@ class HaConfigAreaPage extends LitElement {
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-4);
|
||||
margin: auto;
|
||||
max-width: var(--ha-content-width-wide);
|
||||
padding: var(--ha-space-2) var(--ha-content-gutter);
|
||||
max-width: 1280px;
|
||||
box-sizing: border-box;
|
||||
padding: var(--ha-space-2) var(--ha-space-4);
|
||||
margin-top: var(--ha-space-8);
|
||||
margin-bottom: var(--ha-space-8);
|
||||
}
|
||||
|
||||
@@ -676,6 +676,7 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
grid-gap: 16px 16px;
|
||||
max-width: 2000px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.areas > * {
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
isDynamic,
|
||||
} from "../../../data/automation";
|
||||
import { getConditionDomain } from "../../../data/condition";
|
||||
import type { ConditionDescriptions } from "../../../data/condition";
|
||||
import {
|
||||
apiContext,
|
||||
conditionDescriptionsContext,
|
||||
@@ -57,7 +56,6 @@ import {
|
||||
getTriggerIcons,
|
||||
} from "../../../data/icons";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { DomainManifestLookup } from "../../../data/integration";
|
||||
import type { LabelRegistryEntry } from "../../../data/label/label_registry";
|
||||
import {
|
||||
TARGET_SEPARATOR,
|
||||
@@ -65,7 +63,6 @@ import {
|
||||
type SingleHassServiceTarget,
|
||||
} from "../../../data/target";
|
||||
import { getTriggerDomain } from "../../../data/trigger";
|
||||
import type { TriggerDescriptions } from "../../../data/trigger";
|
||||
import { DialogMixin } from "../../../dialogs/dialog-mixin";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { haStyleScrollbar } from "../../../resources/styles";
|
||||
@@ -157,29 +154,33 @@ class DialogAddAutomationElement extends DialogMixin<AddAutomationElementDialogP
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
@transform<HomeAssistant["states"], Set<string>>({
|
||||
transformer: function (this: DialogAddAutomationElement, states) {
|
||||
const entityIds = new Set(Object.keys(states));
|
||||
const domains = new Set(Object.keys(states).map(computeDomain));
|
||||
// keep the previous set, so a state change doesn't rerender the dialog
|
||||
return deepEqual(entityIds, this._entityIds)
|
||||
? this._entityIds
|
||||
: entityIds;
|
||||
return deepEqual(domains, this._usedDomains)
|
||||
? this._usedDomains
|
||||
: domains;
|
||||
},
|
||||
})
|
||||
private _entityIds?: Set<string>;
|
||||
private _usedDomains?: Set<string>;
|
||||
|
||||
private get _usedDomains(): Set<string> | undefined {
|
||||
return this._computeUsedDomains(this._entityIds);
|
||||
}
|
||||
|
||||
private get _systemDomains(): SystemDomains | undefined {
|
||||
return this._calculateSystemDomains(
|
||||
this.params?.type,
|
||||
this._triggerDescriptions,
|
||||
this._conditionDescriptions,
|
||||
this._manifests,
|
||||
this._registries,
|
||||
this._entityIds
|
||||
);
|
||||
}
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
@transform<HomeAssistant["states"], SystemDomains | undefined>({
|
||||
transformer: function (this: DialogAddAutomationElement, states) {
|
||||
const systemDomains = this._calculateSystemDomains(states);
|
||||
// keep the previous result, so a state change doesn't rerender the dialog
|
||||
return deepEqual(systemDomains, this._systemDomains)
|
||||
? this._systemDomains
|
||||
: systemDomains;
|
||||
},
|
||||
watch: [
|
||||
"_manifests",
|
||||
"_triggerDescriptions",
|
||||
"_conditionDescriptions",
|
||||
"_registries",
|
||||
],
|
||||
})
|
||||
private _systemDomains?: SystemDomains;
|
||||
|
||||
@state()
|
||||
@consume({ context: servicesContext, subscribe: true })
|
||||
@@ -883,43 +884,32 @@ class DialogAddAutomationElement extends DialogMixin<AddAutomationElementDialogP
|
||||
})
|
||||
);
|
||||
|
||||
private _computeUsedDomains = memoizeOne(
|
||||
(entityIds: Set<string> | undefined) =>
|
||||
entityIds && new Set(Array.from(entityIds, computeDomain))
|
||||
);
|
||||
|
||||
private _calculateSystemDomains = memoizeOne(
|
||||
(
|
||||
type: AddAutomationElementDialogParams["type"] | undefined,
|
||||
triggerDescriptions: TriggerDescriptions,
|
||||
conditionDescriptions: ConditionDescriptions,
|
||||
manifests: DomainManifestLookup | undefined,
|
||||
registries: ContextType<typeof registriesContext> | undefined,
|
||||
_entityIds: Set<string> | undefined
|
||||
): SystemDomains | undefined => {
|
||||
if (!type || type === "action" || !manifests || !registries) {
|
||||
return undefined;
|
||||
}
|
||||
const { entities, devices } = registries;
|
||||
return type === "trigger"
|
||||
? calculateActiveSystemDomains(
|
||||
triggerDescriptions,
|
||||
manifests,
|
||||
getTriggerDomain,
|
||||
this._states,
|
||||
entities,
|
||||
devices
|
||||
)
|
||||
: calculateActiveSystemDomains(
|
||||
conditionDescriptions,
|
||||
manifests,
|
||||
getConditionDomain,
|
||||
this._states,
|
||||
entities,
|
||||
devices
|
||||
);
|
||||
private _calculateSystemDomains(
|
||||
states: HomeAssistant["states"]
|
||||
): SystemDomains | undefined {
|
||||
const type = this.params?.type;
|
||||
if (!type || type === "action" || !this._manifests || !this._registries) {
|
||||
return undefined;
|
||||
}
|
||||
);
|
||||
const { entities, devices } = this._registries;
|
||||
return type === "trigger"
|
||||
? calculateActiveSystemDomains(
|
||||
this._triggerDescriptions,
|
||||
this._manifests,
|
||||
getTriggerDomain,
|
||||
states,
|
||||
entities,
|
||||
devices
|
||||
)
|
||||
: calculateActiveSystemDomains(
|
||||
this._conditionDescriptions,
|
||||
this._manifests,
|
||||
getConditionDomain,
|
||||
states,
|
||||
entities,
|
||||
devices
|
||||
);
|
||||
}
|
||||
|
||||
private _getAllItems = memoizeOne(getAllItems);
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ class HaBackupDetailsRestore extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
max-width: 690px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
gap: var(--ha-space-6);
|
||||
|
||||
@@ -135,7 +135,7 @@ class HaBackupDetailsSummary extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
max-width: 690px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
gap: var(--ha-space-6);
|
||||
|
||||
@@ -111,8 +111,8 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: 28px var(--ha-content-gutter) 0;
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
padding: 28px 20px 0;
|
||||
max-width: 690px;
|
||||
margin: 0 auto;
|
||||
gap: var(--ha-space-6);
|
||||
display: flex;
|
||||
|
||||
@@ -298,8 +298,8 @@ class HaBackupBackupsSummary extends LitElement {
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: 28px var(--ha-content-gutter) 0;
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
padding: 28px 20px 0;
|
||||
max-width: 690px;
|
||||
margin: 0 auto;
|
||||
gap: var(--ha-space-6);
|
||||
display: flex;
|
||||
|
||||
@@ -134,8 +134,8 @@ class HaConfigBackupAppUpdateBackups extends LitElement {
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 28px var(--ha-content-gutter) 0;
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
padding: 28px 20px 0;
|
||||
max-width: 690px;
|
||||
margin: 0 auto;
|
||||
gap: var(--ha-space-6);
|
||||
display: flex;
|
||||
|
||||
@@ -361,8 +361,8 @@ class HaConfigBackupDetails extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px var(--ha-content-gutter) 0;
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
padding: 28px 20px 0;
|
||||
max-width: 690px;
|
||||
margin: 0 auto;
|
||||
gap: var(--ha-space-6);
|
||||
display: grid;
|
||||
|
||||
@@ -328,8 +328,8 @@ class HaConfigBackupDetails extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px var(--ha-content-gutter) 0;
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
padding: 28px 20px 0;
|
||||
max-width: 690px;
|
||||
margin: 0 auto;
|
||||
gap: var(--ha-space-6);
|
||||
display: grid;
|
||||
|
||||
@@ -375,8 +375,8 @@ class HaConfigBackupOverview extends LitElement {
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 28px var(--ha-content-gutter) 0;
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
padding: 28px 20px 0;
|
||||
max-width: 690px;
|
||||
margin: 0 auto;
|
||||
gap: var(--ha-space-6);
|
||||
display: flex;
|
||||
|
||||
Loaded 100 of 253 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user