mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-26 00:27:35 +00:00
Compare commits
161
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c93661eaf7 | ||
|
|
7dac2e243f | ||
|
|
f9e1023a2e | ||
|
|
82a0d9a413 | ||
|
|
1325acdba3 | ||
|
|
07878563be | ||
|
|
b0d5aa5e27 | ||
|
|
444f98df66 | ||
|
|
f5e62a2c7d | ||
|
|
32b9676f97 | ||
|
|
7876642f35 | ||
|
|
0e3bcfad5e | ||
|
|
cd1c273d5a | ||
|
|
d92ac4b4b7 | ||
|
|
bfecb1d4a9 | ||
|
|
69a8db00fa | ||
|
|
bbda7affdc | ||
|
|
10c90d222d | ||
|
|
072f70b49f | ||
|
|
7f2a5ecc27 | ||
|
|
a42f6f864a | ||
|
|
a07772c514 | ||
|
|
a6ab6e218f | ||
|
|
ed96657085 | ||
|
|
50ca39722e | ||
|
|
7026e5b375 | ||
|
|
37e8e1b728 | ||
|
|
48369854af | ||
|
|
7715e01126 | ||
|
|
e4ee108e14 | ||
|
|
407609c118 | ||
|
|
68cbaf6481 | ||
|
|
b9b249a317 | ||
|
|
df6578dfdf | ||
|
|
b4a07f504c | ||
|
|
24b8078a9c | ||
|
|
2bd8a657b8 | ||
|
|
61724a52ba | ||
|
|
67d73261a4 | ||
|
|
419ba64bed | ||
|
|
e87155bba3 | ||
|
|
d8a3939e22 | ||
|
|
5141b6aea9 | ||
|
|
9aaf1bdd07 | ||
|
|
a0e7c8e9bb | ||
|
|
8f482776b9 | ||
|
|
2c80183ded | ||
|
|
9bd5e25ee3 | ||
|
|
994193465f | ||
|
|
530b7ed457 | ||
|
|
24332e8288 | ||
|
|
980aaa2bac | ||
|
|
344856cbc4 | ||
|
|
06877cbaaa | ||
|
|
e1ac7e98b0 | ||
|
|
4b2426ab77 | ||
|
|
f9471d6b4c | ||
|
|
0645484258 | ||
|
|
2da3efb812 | ||
|
|
e3674e550f | ||
|
|
cff038fea4 | ||
|
|
bed39fd8e9 | ||
|
|
b8346d0286 | ||
|
|
5f80b74322 | ||
|
|
73e0f1e18d | ||
|
|
0b16b735e8 | ||
|
|
c88dcf10b9 | ||
|
|
08bc23e2e7 | ||
|
|
97d49f06b1 | ||
|
|
50b727393d | ||
|
|
5c6dd2a697 | ||
|
|
87758cc228 | ||
|
|
60e8b8b505 | ||
|
|
3c012c30ac | ||
|
|
84d234a330 | ||
|
|
a12543fe74 | ||
|
|
cc53f977a2 | ||
|
|
71541625d7 | ||
|
|
43da700ccc | ||
|
|
efbbdbf3e8 | ||
|
|
eee6f79639 | ||
|
|
9381bbd656 | ||
|
|
2724087290 | ||
|
|
6bdf1ccd8c | ||
|
|
79743c0afa | ||
|
|
15b1df5a58 | ||
|
|
8222d9796c | ||
|
|
3337b414d7 | ||
|
|
ce90d83c92 | ||
|
|
1f6d0d2e63 | ||
|
|
0327b02d0b | ||
|
|
780db9b066 | ||
|
|
a2a38e1da7 | ||
|
|
88c063ba2a | ||
|
|
eb8b2a9d17 | ||
|
|
10e8c2a148 | ||
|
|
e1a8616ab0 | ||
|
|
ccdd71dd64 | ||
|
|
d3e1d55686 | ||
|
|
4f916abcbf | ||
|
|
4548f9daae | ||
|
|
4020bcec42 | ||
|
|
22c0035e60 | ||
|
|
6b6ad8dd2c | ||
|
|
9c4aacdb1f | ||
|
|
3feb40a8f4 | ||
|
|
7a310812e0 | ||
|
|
ee77619da3 | ||
|
|
cfa8eb5370 | ||
|
|
d9d2d6aa03 | ||
|
|
1f46f477c7 | ||
|
|
52667b3266 | ||
|
|
c790d2356c | ||
|
|
f24c009dd7 | ||
|
|
8d42395938 | ||
|
|
1a6d46a7ff | ||
|
|
b286b07cfd | ||
|
|
1859d35f7b | ||
|
|
5709af57de | ||
|
|
bb16cc8c00 | ||
|
|
17c6dc52a8 | ||
|
|
1b8211db6d | ||
|
|
2b2bb77a2b | ||
|
|
64749350ef | ||
|
|
043d4eed85 | ||
|
|
2f2e64bb1d | ||
|
|
b74b02c09f | ||
|
|
ab4c3a4316 | ||
|
|
15de137591 | ||
|
|
465c10b945 | ||
|
|
457c51cf58 | ||
|
|
640f2b9245 | ||
|
|
852caa32be | ||
|
|
67ccfa0f6e | ||
|
|
c3cc566fe3 | ||
|
|
38d02a3f30 | ||
|
|
ad1d1e2260 | ||
|
|
b2eb8ec968 | ||
|
|
7b8884f0fd | ||
|
|
aff1fedc9d | ||
|
|
8f5059c24a | ||
|
|
1e72ad1411 | ||
|
|
c9f96bbe69 | ||
|
|
616c3d4657 | ||
|
|
b1ceece224 | ||
|
|
d695c4c845 | ||
|
|
fdbeb12622 | ||
|
|
29ede122a1 | ||
|
|
519d3d0e53 | ||
|
|
030a9a492c | ||
|
|
2685a007e7 | ||
|
|
9ca1cfbf4a | ||
|
|
0793af6846 | ||
|
|
bb7f441d8d | ||
|
|
2813ed7938 | ||
|
|
9ebfa4029b | ||
|
|
6190ba18ea | ||
|
|
81feea1109 | ||
|
|
be430931cc | ||
|
|
e07194027a | ||
|
|
17d9cd192f |
@@ -5,6 +5,9 @@ updates:
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "06:00"
|
||||
cooldown:
|
||||
default-days-before-reopen: 30
|
||||
default-days: 7
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- Dependencies
|
||||
|
||||
@@ -8,6 +8,9 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
@@ -24,6 +27,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: dev
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
@@ -59,6 +63,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: master
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
@@ -18,6 +18,9 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint and check format
|
||||
@@ -25,6 +28,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
@@ -59,6 +64,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
@@ -77,6 +84,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
|
||||
@@ -7,6 +7,10 @@ on:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [dev]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
@@ -28,6 +32,7 @@ jobs:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
@@ -36,14 +41,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +62,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||
|
||||
@@ -9,6 +9,9 @@ on:
|
||||
- dev
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
@@ -25,6 +28,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: dev
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
@@ -60,6 +64,7 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: master
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
@@ -5,6 +5,9 @@ on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
@@ -17,6 +20,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
@@ -10,6 +10,9 @@ on:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
@@ -22,6 +25,8 @@ jobs:
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: "Pull Request Labeler"
|
||||
|
||||
on: pull_request_target
|
||||
on: pull_request_target # zizmor: ignore[dangerous-triggers] -- safe: only runs actions/labeler, no PR code checkout
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
|
||||
@@ -5,6 +5,10 @@ on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -21,6 +21,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
name: RelativeCI
|
||||
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] -- safe: only downloads artifacts, no PR code checkout
|
||||
workflow_run:
|
||||
workflows: [CI]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
name: Upload stats
|
||||
upload-frontend-modern:
|
||||
name: Upload stats (frontend/modern)
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
strategy:
|
||||
matrix:
|
||||
bundle: [frontend]
|
||||
build: [modern, legacy]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
key: ${{ secrets.RELATIVE_CI_KEY_frontend_modern }}
|
||||
token: ${{ github.token }}
|
||||
artifactName: ${{ format('{0}-bundle-stats', matrix.bundle) }}
|
||||
webpackStatsFile: ${{ format('{0}-{1}.json', matrix.bundle, matrix.build) }}
|
||||
artifactName: frontend-bundle-stats
|
||||
webpackStatsFile: frontend-modern.json
|
||||
|
||||
upload-frontend-legacy:
|
||||
name: Upload stats (frontend/legacy)
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
|
||||
with:
|
||||
key: ${{ secrets.RELATIVE_CI_KEY_frontend_legacy }}
|
||||
token: ${{ github.token }}
|
||||
artifactName: frontend-bundle-stats
|
||||
webpackStatsFile: frontend-legacy.json
|
||||
|
||||
@@ -27,6 +27,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
@@ -34,13 +36,12 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
uses: home-assistant/actions/helpers/verify-version@d56d093b9ab8d2105bc0cb6ee9bcc0ef4ec8b96d # master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
@@ -62,11 +63,10 @@ jobs:
|
||||
skip-existing: true
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
dist/*.tar.gz
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: gh release upload "$TAG_NAME" dist/*.whl dist/*.tar.gz --clobber
|
||||
|
||||
wheels-init:
|
||||
name: Init wheels build
|
||||
@@ -74,15 +74,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate requirements.txt
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: |
|
||||
# Sleep to give pypi time to populate the new version across mirrors
|
||||
sleep 240
|
||||
version=$(echo "${{ github.ref }}" | awk -F"/" '{print $NF}' )
|
||||
version=$(echo "$GITHUB_REF" | awk -F"/" '{print $NF}' )
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
# home-assistant/wheels doesn't support SHA pinning
|
||||
- name: Build wheels
|
||||
uses: home-assistant/[email protected]
|
||||
uses: home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
||||
with:
|
||||
abi: cp314
|
||||
tag: musllinux_1_2
|
||||
@@ -99,11 +101,12 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: Download Translations
|
||||
@@ -113,8 +116,11 @@ jobs:
|
||||
- name: Build landing-page
|
||||
run: landing-page/script/build_landing_page
|
||||
- name: Tar folder
|
||||
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
|
||||
env:
|
||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: tar -czf "landing-page/home_assistant_frontend_landingpage-${TAG_NAME}.tar.gz" -C landing-page/dist .
|
||||
- name: Upload release asset
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
run: gh release upload "$TAG_NAME" "landing-page/home_assistant_frontend_landingpage-${TAG_NAME}.tar.gz" --clobber
|
||||
|
||||
@@ -5,6 +5,10 @@ on:
|
||||
schedule:
|
||||
- cron: "0 * * * *"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -8,6 +8,9 @@ on:
|
||||
paths:
|
||||
- src/translations/en.json
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
name: Upload
|
||||
@@ -15,6 +18,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Input
|
||||
---
|
||||
|
||||
# Input `<ha-input>`
|
||||
|
||||
A text input component supporting Home Assistant theming and validation, based on webawesome input.
|
||||
Supports multiple input types including text, number, password, email, search, and more.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Example usage
|
||||
|
||||
```html
|
||||
<ha-input label="Name" value="Hello"></ha-input>
|
||||
|
||||
<ha-input label="Email" type="email" placeholder="[email protected]"></ha-input>
|
||||
|
||||
<ha-input label="Password" type="password" password-toggle></ha-input>
|
||||
|
||||
<ha-input label="Required" required></ha-input>
|
||||
|
||||
<ha-input label="Disabled" disabled value="Can't touch this"></ha-input>
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
This component is based on the webawesome input component.
|
||||
|
||||
**Slots**
|
||||
|
||||
- `start`: Content placed before the input (usually for icons or prefixes).
|
||||
- `end`: Content placed after the input (usually for icons or suffixes).
|
||||
- `label`: Custom label content. Overrides the `label` property.
|
||||
- `hint`: Custom hint content. Overrides the `hint` property.
|
||||
- `clear-icon`: Custom clear icon.
|
||||
- `show-password-icon`: Custom show password icon.
|
||||
- `hide-password-icon`: Custom hide password icon.
|
||||
|
||||
**Properties/Attributes**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| -------------------- | ---------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| appearance | "material"/"outlined" | "material" | Sets the input appearance style. "material" is the default filled style, "outlined" uses a bordered style. |
|
||||
| type | "text"/"number"/"password"/"email"/"search"/"tel"/"url"/"date"/"datetime-local"/"time"/"color" | "text" | Sets the input type. |
|
||||
| value | String | - | The current value of the input. |
|
||||
| label | String | "" | The input's label text. |
|
||||
| hint | String | "" | The input's hint/helper text. |
|
||||
| placeholder | String | "" | Placeholder text shown when the input is empty. |
|
||||
| with-clear | Boolean | false | Adds a clear button when the input is not empty. |
|
||||
| readonly | Boolean | false | Makes the input readonly. |
|
||||
| disabled | Boolean | false | Disables the input and prevents user interaction. |
|
||||
| required | Boolean | false | Makes the input a required field. |
|
||||
| password-toggle | Boolean | false | Adds a button to toggle the password visibility. |
|
||||
| without-spin-buttons | Boolean | false | Hides the browser's built-in spin buttons for number inputs. |
|
||||
| auto-validate | Boolean | false | Validates the input on blur instead of on form submit. |
|
||||
| invalid | Boolean | false | Marks the input as invalid. |
|
||||
| inset-label | Boolean | false | Uses an inset label style where the label stays inside the input. |
|
||||
| validation-message | String | "" | Custom validation message shown when the input is invalid. |
|
||||
| pattern | String | - | A regular expression pattern to validate input against. |
|
||||
| minlength | Number | - | The minimum length of input that will be considered valid. |
|
||||
| maxlength | Number | - | The maximum length of input that will be considered valid. |
|
||||
| min | Number/String | - | The input's minimum value. Only applies to date and number input types. |
|
||||
| max | Number/String | - | The input's maximum value. Only applies to date and number input types. |
|
||||
| step | Number/"any" | - | Specifies the granularity that the value must adhere to. |
|
||||
|
||||
**CSS Custom Properties**
|
||||
|
||||
- `--ha-input-padding-top` - Padding above the input.
|
||||
- `--ha-input-padding-bottom` - Padding below the input. Defaults to `var(--ha-space-2)`.
|
||||
- `--ha-input-text-align` - Text alignment of the input. Defaults to `start`.
|
||||
- `--ha-input-required-marker` - The marker shown after the label for required fields. Defaults to `"*"`.
|
||||
|
||||
---
|
||||
|
||||
## Derivatives
|
||||
|
||||
The following components extend or wrap `ha-input` for specific use cases:
|
||||
|
||||
- **`<ha-input-search>`** — A pre-configured search input with a magnify icon, clear button, and localized "Search" placeholder. Extends `ha-input`.
|
||||
- **`<ha-input-copy>`** — A read-only input with a copy-to-clipboard button. Supports optional value masking with a reveal toggle.
|
||||
- **`<ha-input-multi>`** — A dynamic list of text inputs for managing arrays of strings. Supports adding, removing, and drag-and-drop reordering.
|
||||
@@ -0,0 +1,232 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import { mdiMagnify } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/components/input/ha-input";
|
||||
import "../../../../src/components/input/ha-input-copy";
|
||||
import "../../../../src/components/input/ha-input-multi";
|
||||
import "../../../../src/components/input/ha-input-search";
|
||||
import { localizeContext } from "../../../../src/data/context";
|
||||
|
||||
const LOCALIZE_KEYS: Record<string, string> = {
|
||||
"ui.common.copy": "Copy",
|
||||
"ui.common.show": "Show",
|
||||
"ui.common.hide": "Hide",
|
||||
"ui.common.add": "Add",
|
||||
"ui.common.remove": "Remove",
|
||||
"ui.common.search": "Search",
|
||||
"ui.common.copied_clipboard": "Copied to clipboard",
|
||||
};
|
||||
|
||||
@customElement("demo-components-ha-input")
|
||||
export class DemoHaInput extends LitElement {
|
||||
constructor() {
|
||||
super();
|
||||
// Provides localizeContext for ha-input-copy, ha-input-multi and ha-input-search
|
||||
// eslint-disable-next-line no-new
|
||||
new ContextProvider(this, {
|
||||
context: localizeContext,
|
||||
initialValue: ((key: string) => LOCALIZE_KEYS[key] ?? key) as any,
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-input in ${mode}">
|
||||
<div class="card-content">
|
||||
<h3>Basic</h3>
|
||||
<div class="row">
|
||||
<ha-input label="Default"></ha-input>
|
||||
<ha-input label="With value" value="Hello"></ha-input>
|
||||
<ha-input
|
||||
label="With placeholder"
|
||||
placeholder="Type here..."
|
||||
></ha-input>
|
||||
</div>
|
||||
|
||||
<h3>Input types</h3>
|
||||
<div class="row">
|
||||
<ha-input label="Text" type="text" value="Text"></ha-input>
|
||||
<ha-input label="Number" type="number" value="42"></ha-input>
|
||||
<ha-input
|
||||
label="Email"
|
||||
type="email"
|
||||
placeholder="[email protected]"
|
||||
></ha-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
label="Password"
|
||||
type="password"
|
||||
value="secret"
|
||||
password-toggle
|
||||
></ha-input>
|
||||
<ha-input label="URL" type="url" placeholder="https://...">
|
||||
</ha-input>
|
||||
<ha-input label="Date" type="date"></ha-input>
|
||||
</div>
|
||||
|
||||
<h3>States</h3>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
label="Disabled"
|
||||
disabled
|
||||
value="Disabled"
|
||||
></ha-input>
|
||||
<ha-input
|
||||
label="Readonly"
|
||||
readonly
|
||||
value="Readonly"
|
||||
></ha-input>
|
||||
<ha-input label="Required" required></ha-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
label="Invalid"
|
||||
invalid
|
||||
validation-message="This field is required"
|
||||
value=""
|
||||
></ha-input>
|
||||
<ha-input label="With hint" hint="This is a hint"></ha-input>
|
||||
<ha-input
|
||||
label="With clear"
|
||||
with-clear
|
||||
value="Clear me"
|
||||
></ha-input>
|
||||
</div>
|
||||
|
||||
<h3>With slots</h3>
|
||||
<div class="row">
|
||||
<ha-input label="With prefix">
|
||||
<span slot="start">$</span>
|
||||
</ha-input>
|
||||
<ha-input label="With suffix">
|
||||
<span slot="end">kg</span>
|
||||
</ha-input>
|
||||
<ha-input label="With icon">
|
||||
<ha-svg-icon .path=${mdiMagnify} slot="start"></ha-svg-icon>
|
||||
</ha-input>
|
||||
</div>
|
||||
|
||||
<h3>Appearance: outlined</h3>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
label="Outlined"
|
||||
value="Hello"
|
||||
></ha-input>
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
label="Outlined disabled"
|
||||
disabled
|
||||
value="Disabled"
|
||||
></ha-input>
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
label="Outlined invalid"
|
||||
invalid
|
||||
validation-message="Required"
|
||||
></ha-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
placeholder="Placeholder only"
|
||||
></ha-input>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Derivatives in ${mode}">
|
||||
<div class="card-content">
|
||||
<h3>ha-input-search</h3>
|
||||
<ha-input-search label="Search label"></ha-input-search>
|
||||
<ha-input-search appearance="outlined"></ha-input-search>
|
||||
|
||||
<h3>ha-input-copy</h3>
|
||||
<ha-input-copy
|
||||
value="my-api-token-123"
|
||||
masked-value="••••••••••••••••••"
|
||||
masked-toggle
|
||||
></ha-input-copy>
|
||||
|
||||
<h3>ha-input-multi</h3>
|
||||
<ha-input-multi
|
||||
label="URL"
|
||||
add-label="Add URL"
|
||||
.value=${["https://example.com"]}
|
||||
></ha-input-multi>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
h3 {
|
||||
margin: var(--ha-space-4) 0 var(--ha-space-1) 0;
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
.row > * {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-input": DemoHaInput;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ The Home Assistant interface is based on Material Design. It's a design system c
|
||||
We want to make it as easy for designers to contribute as it is for developers. There’s a lot a designer can contribute to:
|
||||
|
||||
- Meet us at <a href="https://www.home-assistant.io/join-chat-design" rel="noopener noreferrer" target="_blank">Discord #designers channel</a>. If you can't see the channel, make sure you set the correct role in Channels & Roles.
|
||||
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||
- Start designing with our <a href="https://www.figma.com/design/2WGI8IDGyxINjSV6NRvPur/Home-Assistant-Design-Kit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
||||
|
||||
## Developers
|
||||
|
||||
+8
-10
@@ -30,7 +30,7 @@
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.1",
|
||||
"@codemirror/commands": "6.10.3",
|
||||
"@codemirror/language": "6.12.2",
|
||||
"@codemirror/language": "6.12.3",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.6.0",
|
||||
@@ -82,7 +82,7 @@
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.19",
|
||||
"@swc/helpers": "0.5.20",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
@@ -129,7 +129,6 @@
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"ua-parser-js": "2.0.9",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.4.0",
|
||||
"workbox-core": "7.4.0",
|
||||
@@ -151,7 +150,7 @@
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.5",
|
||||
"@rspack/core": "1.7.9",
|
||||
"@rspack/core": "1.7.10",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
@@ -169,9 +168,8 @@
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.1.0",
|
||||
"@vitest/coverage-v8": "4.1.2",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
@@ -206,13 +204,13 @@
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.6",
|
||||
"sinon": "21.0.3",
|
||||
"tar": "7.5.12",
|
||||
"tar": "7.5.13",
|
||||
"terser-webpack-plugin": "5.4.0",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.57.1",
|
||||
"typescript-eslint": "8.57.2",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.0",
|
||||
"vitest": "4.1.2",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.4.0#~/.yarn/patches/workbox-build-npm-7.4.0-c84561662c.patch"
|
||||
@@ -230,6 +228,6 @@
|
||||
},
|
||||
"packageManager": "[email protected]",
|
||||
"volta": {
|
||||
"node": "24.14.0"
|
||||
"node": "24.14.1"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260128.0"
|
||||
version = "20260325.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -58,9 +58,11 @@ export class CastManager {
|
||||
this._eventListeners[event].push(listener);
|
||||
|
||||
return () => {
|
||||
this._eventListeners[event].splice(
|
||||
this._eventListeners[event].indexOf(listener)
|
||||
);
|
||||
const listeners = this._eventListeners[event];
|
||||
const index = listeners.indexOf(listener);
|
||||
if (index !== -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,12 @@ const YAML_ONLY_THEMES_COLORS = new Set([
|
||||
"disabled",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Compose a CSS variable out of a theme color
|
||||
* @param color - Theme color (examples: `red`, `primary-text`)
|
||||
* @returns CSS variable in `--xxx-color` format;
|
||||
* initial color if not found in theme colors
|
||||
*/
|
||||
export function computeCssVariableName(color: string): string {
|
||||
if (THEME_COLORS.has(color) || YAML_ONLY_THEMES_COLORS.has(color)) {
|
||||
return `--${color}-color`;
|
||||
@@ -39,6 +45,12 @@ export function computeCssVariableName(color: string): string {
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose a CSS variable out of a theme color & then resolve it
|
||||
* @param color - Theme color (examples: `red`, `primary-text`)
|
||||
* @returns Resolved CSS variable in `var(--xxx-color)` format;
|
||||
* initial color if not found in theme colors
|
||||
*/
|
||||
export function computeCssColor(color: string): string {
|
||||
const cssVarName = computeCssVariableName(color);
|
||||
if (cssVarName !== color) {
|
||||
@@ -47,6 +59,22 @@ export function computeCssColor(color: string): string {
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a color from document's styles
|
||||
* @param color - Named theme color (examples: `red`, `primary-text`)
|
||||
* @returns Resolved color; initial color if not found in document's styles
|
||||
*/
|
||||
export function resolveThemeColor(color: string): string {
|
||||
const cssColor = computeCssVariableName(color);
|
||||
if (cssColor.startsWith("--")) {
|
||||
const resolved = getComputedStyle(document.body)
|
||||
.getPropertyValue(cssColor)
|
||||
.trim();
|
||||
return resolved || color;
|
||||
}
|
||||
return cssColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a string is a valid color.
|
||||
* Accepts: hex colors (#xxx, #xxxxxx), theme colors, and valid CSS color names.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import colors from "color-name";
|
||||
import { expandHex } from "./hex";
|
||||
import { resolveThemeColor } from "./compute-color";
|
||||
|
||||
const rgb_hex = (component: number): string => {
|
||||
const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16);
|
||||
@@ -130,26 +131,43 @@ export const rgb2hs = (rgb: [number, number, number]): [number, number] =>
|
||||
export const hs2rgb = (hs: [number, number]): [number, number, number] =>
|
||||
hsv2rgb([hs[0], hs[1], 255]);
|
||||
|
||||
export function theme2hex(themeColor: string): string {
|
||||
if (themeColor.startsWith("#")) {
|
||||
if (themeColor.length === 4 || themeColor.length === 5) {
|
||||
const c = themeColor;
|
||||
/**
|
||||
* Attempt to get a HEX color from a color defined in different formats:
|
||||
* HEX, rgb/rgba, named color
|
||||
* @param color - Color (HEX, rgb/rgba, named color) to be converted to HEX
|
||||
* @returns HEX color
|
||||
*/
|
||||
export function theme2hex(color: string): string {
|
||||
// Attempting to find a HEX pattern in the input string
|
||||
if (color.startsWith("#")) {
|
||||
if (color.length === 4 || color.length === 5) {
|
||||
const c = color;
|
||||
// Convert short-form hex (#abc) to 6 digit (#aabbcc). Ignore alpha channel.
|
||||
return `#${c[1]}${c[1]}${c[2]}${c[2]}${c[3]}${c[3]}`;
|
||||
}
|
||||
if (themeColor.length === 9) {
|
||||
if (color.length === 9) {
|
||||
// Ignore alpha channel.
|
||||
return themeColor.substring(0, 7);
|
||||
return color.substring(0, 7);
|
||||
}
|
||||
return themeColor;
|
||||
return color;
|
||||
}
|
||||
|
||||
const rgbFromColorName = colors[themeColor.toLowerCase()];
|
||||
if (rgbFromColorName) {
|
||||
return rgb2hex(rgbFromColorName);
|
||||
// Attempting to find a match in a HA Frontend theme colors
|
||||
const themeColor = resolveThemeColor(color.toLowerCase());
|
||||
if (themeColor !== color.toLowerCase()) {
|
||||
// theme color is recognized, now re-attempt
|
||||
return theme2hex(themeColor);
|
||||
}
|
||||
|
||||
const rgbMatch = themeColor.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
||||
// Attempting to find a match in a web colors array
|
||||
const rgbFromWebColor = colors[color.toLowerCase()];
|
||||
if (rgbFromWebColor) {
|
||||
// HEX color is recognized for the input named color
|
||||
return rgb2hex(rgbFromWebColor);
|
||||
}
|
||||
|
||||
// Attempting to find an RGB pattern in the input string
|
||||
const rgbMatch = color.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
||||
if (rgbMatch) {
|
||||
const [, r, g, b] = rgbMatch.map(Number);
|
||||
return rgb2hex([r, g, b]);
|
||||
@@ -158,5 +176,5 @@ export function theme2hex(themeColor: string): string {
|
||||
// We have a named color, and there's nothing in the table,
|
||||
// so nothing further we can do with it.
|
||||
// Compare/border/background color will all be the same.
|
||||
return themeColor;
|
||||
return color;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { wcagLuminance, wcagContrast } from "culori";
|
||||
import { theme2hex } from "./convert-color";
|
||||
|
||||
/**
|
||||
* Calculates the luminosity of an RGB color.
|
||||
@@ -48,3 +49,13 @@ export const getRGBContrastRatio = (
|
||||
rgb1: [number, number, number],
|
||||
rgb2: [number, number, number]
|
||||
) => Math.round((rgbContrast(rgb1, rgb2) + Number.EPSILON) * 100) / 100;
|
||||
|
||||
/**
|
||||
* Returns a contrasted color (black or white) based on the luminance of another color
|
||||
* @param color - Color (HEX, rgb/rgba, named color) to calculate a contrasted color
|
||||
* @returns HEX color ("#000000" for dark backgrounds, "#ffffff" for light backgrounds)
|
||||
*/
|
||||
export const getContrastedColorHex = (color: string): string => {
|
||||
const lum = wcagLuminance(theme2hex(color));
|
||||
return lum > 0.5 ? "#000000" : "#ffffff";
|
||||
};
|
||||
|
||||
@@ -4,11 +4,14 @@ import type {
|
||||
EntityRegistryEntry,
|
||||
} from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeDeviceName } from "./compute_device_name";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
import { stripPrefixFromEntityName } from "./strip_prefix_from_entity_name";
|
||||
|
||||
export const computeEntityName = (
|
||||
stateObj: HassEntity,
|
||||
entities: HomeAssistant["entities"]
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): string | undefined => {
|
||||
const entry = entities[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
@@ -18,22 +21,49 @@ export const computeEntityName = (
|
||||
// Fall back to state name if not in the entity registry (friendly name)
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
return computeEntityEntryName(entry);
|
||||
return computeEntityEntryName(entry, devices);
|
||||
};
|
||||
|
||||
export const computeEntityEntryName = (
|
||||
entry: EntityRegistryDisplayEntry | EntityRegistryEntry
|
||||
entry: EntityRegistryDisplayEntry | EntityRegistryEntry,
|
||||
devices: HomeAssistant["devices"],
|
||||
fallbackStateObj?: HassEntity
|
||||
): string | undefined => {
|
||||
if (entry.name != null) {
|
||||
return entry.name;
|
||||
const name =
|
||||
entry.name ||
|
||||
("original_name" in entry && entry.original_name != null
|
||||
? String(entry.original_name)
|
||||
: undefined);
|
||||
|
||||
const device = entry.device_id ? devices[entry.device_id] : undefined;
|
||||
|
||||
if (!device) {
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
if (fallbackStateObj) {
|
||||
return computeStateName(fallbackStateObj);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if ("original_name" in entry && entry.original_name != null) {
|
||||
return String(entry.original_name);
|
||||
|
||||
const deviceName = computeDeviceName(device);
|
||||
|
||||
// If the device name is the same as the entity name, consider empty entity name
|
||||
if (deviceName === name) {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
// Remove the device name from the entity name if it starts with it
|
||||
if (deviceName && name) {
|
||||
return stripPrefixFromEntityName(name, deviceName) || name;
|
||||
}
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
export const entityUseDeviceName = (
|
||||
stateObj: HassEntity,
|
||||
entities: HomeAssistant["entities"]
|
||||
): boolean => !computeEntityName(stateObj, entities);
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): boolean => !computeEntityName(stateObj, entities, devices);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { computeAreaName } from "./compute_area_name";
|
||||
import { computeDeviceName } from "./compute_device_name";
|
||||
import { computeEntityName, entityUseDeviceName } from "./compute_entity_name";
|
||||
import { computeFloorName } from "./compute_floor_name";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
import { getEntityContext } from "./context/get_entity_context";
|
||||
|
||||
const DEFAULT_SEPARATOR = " ";
|
||||
@@ -29,14 +30,23 @@ export interface EntityNameOptions {
|
||||
|
||||
export const computeEntityNameDisplay = (
|
||||
stateObj: HassEntity,
|
||||
name: EntityNameItem | EntityNameItem[] | undefined,
|
||||
name: string | EntityNameItem | EntityNameItem[] | undefined,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"],
|
||||
options?: EntityNameOptions
|
||||
) => {
|
||||
let items = ensureArray(name || DEFAULT_ENTITY_NAME);
|
||||
if (typeof name === "string") {
|
||||
return name;
|
||||
}
|
||||
|
||||
// If no name config is provided, fall back to the friendly name
|
||||
if (!name) {
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
|
||||
let items = ensureArray(name);
|
||||
|
||||
const separator = options?.separator ?? DEFAULT_SEPARATOR;
|
||||
|
||||
@@ -45,7 +55,7 @@ export const computeEntityNameDisplay = (
|
||||
return items.map((item) => item.text).join(separator);
|
||||
}
|
||||
|
||||
const useDeviceName = entityUseDeviceName(stateObj, entities);
|
||||
const useDeviceName = entityUseDeviceName(stateObj, entities, devices);
|
||||
|
||||
// If entity uses device name, and device is not already included, replace it with device name
|
||||
if (useDeviceName) {
|
||||
@@ -91,7 +101,7 @@ export const computeEntityNameList = (
|
||||
const names = name.map((item) => {
|
||||
switch (item.type) {
|
||||
case "entity":
|
||||
return computeEntityName(stateObj, entities);
|
||||
return computeEntityName(stateObj, entities, devices);
|
||||
case "device":
|
||||
return device ? computeDeviceName(device) : undefined;
|
||||
case "area":
|
||||
|
||||
@@ -142,9 +142,10 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
minusSign: "value",
|
||||
plusSign: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
minusSign: "value",
|
||||
};
|
||||
|
||||
const valueParts: ValuePart[] = [];
|
||||
@@ -153,7 +154,7 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
const type = TYPE_MAP[part.type];
|
||||
if (!type) continue;
|
||||
const last = valueParts[valueParts.length - 1];
|
||||
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
|
||||
// Merge consecutive value parts (e.g. "-" + "12" + "." + "00" → "-12.00")
|
||||
if (type === "value" && last?.type === "value") {
|
||||
last.value += part.value;
|
||||
} else {
|
||||
@@ -254,6 +255,7 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
"conversation",
|
||||
"event",
|
||||
"image",
|
||||
"infrared",
|
||||
"input_button",
|
||||
"notify",
|
||||
"scene",
|
||||
|
||||
@@ -29,6 +29,7 @@ export const FIXED_DOMAIN_STATES = {
|
||||
device_tracker: ["home", "not_home"],
|
||||
fan: ["on", "off"],
|
||||
humidifier: ["on", "off"],
|
||||
infrared: [],
|
||||
input_boolean: ["on", "off"],
|
||||
input_button: [],
|
||||
lawn_mower: ["error", "paused", "mowing", "returning", "docked"],
|
||||
@@ -270,6 +271,8 @@ export const getStates = (
|
||||
result.push(...state.attributes.preset_modes);
|
||||
} else if (attribute === "swing_mode") {
|
||||
result.push(...state.attributes.swing_modes);
|
||||
} else if (attribute === "swing_horizontal_mode") {
|
||||
result.push(...state.attributes.swing_horizontal_modes);
|
||||
}
|
||||
break;
|
||||
case "device_tracker":
|
||||
|
||||
@@ -6,7 +6,9 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
const compareState = state !== undefined ? state : stateObj?.state;
|
||||
|
||||
if (["button", "event", "input_button", "scene"].includes(domain)) {
|
||||
if (
|
||||
["button", "event", "infrared", "input_button", "scene"].includes(domain)
|
||||
) {
|
||||
return compareState !== UNAVAILABLE;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Indicates whether the current browser supports the Popover API.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Popover_API
|
||||
*/
|
||||
export const popoverSupported = globalThis?.HTMLElement?.prototype
|
||||
? Object.prototype.hasOwnProperty.call(
|
||||
globalThis.HTMLElement.prototype,
|
||||
"popover"
|
||||
)
|
||||
: false;
|
||||
@@ -71,13 +71,6 @@ export const formatNumberToParts = (
|
||||
? numberFormatToLocale(localeOptions)
|
||||
: undefined;
|
||||
|
||||
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
|
||||
Number.isNaN =
|
||||
Number.isNaN ||
|
||||
function isNaN(input) {
|
||||
return typeof input === "number" && isNaN(input);
|
||||
};
|
||||
|
||||
if (
|
||||
localeOptions?.number_format !== NumberFormat.none &&
|
||||
!Number.isNaN(Number(num))
|
||||
|
||||
@@ -5,12 +5,41 @@ import {
|
||||
formatDateMonthYear,
|
||||
formatDateVeryShort,
|
||||
formatDateWeekdayShort,
|
||||
formatDateYear,
|
||||
} from "../../common/datetime/format_date";
|
||||
import {
|
||||
formatTime,
|
||||
formatTimeWithSeconds,
|
||||
} from "../../common/datetime/format_time";
|
||||
|
||||
export function getPeriodicAxisLabelConfig(
|
||||
period: string,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
):
|
||||
| {
|
||||
formatter: (value: number) => string;
|
||||
}
|
||||
| undefined {
|
||||
if (period === "month") {
|
||||
return {
|
||||
formatter: (value: number) => {
|
||||
const date = new Date(value);
|
||||
return date.getMonth() === 0
|
||||
? `{bold|${formatDateMonthYear(date, locale, config)}}`
|
||||
: formatDateMonth(date, locale, config);
|
||||
},
|
||||
};
|
||||
}
|
||||
if (period === "year") {
|
||||
return {
|
||||
formatter: (value: number) =>
|
||||
formatDateYear(new Date(value), locale, config),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function formatTimeLabel(
|
||||
value: number | Date,
|
||||
locale: FrontendLocaleData,
|
||||
|
||||
@@ -91,6 +91,10 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _lastTapTime?: number;
|
||||
|
||||
private _longPressTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _longPressTriggered = false;
|
||||
|
||||
private _shouldResizeChart = false;
|
||||
|
||||
private _resizeAnimationDuration?: number;
|
||||
@@ -128,6 +132,7 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._legendPointerCancel();
|
||||
this._pendingSetup = false;
|
||||
while (this._listeners.length) {
|
||||
this._listeners.pop()!();
|
||||
@@ -280,39 +285,53 @@ export class HaChartBase extends LitElement {
|
||||
<div class="chart"></div>
|
||||
</div>
|
||||
${this._renderLegend()}
|
||||
<div class="chart-controls ${classMap({ small: this.smallControls })}">
|
||||
${this._isZoomed && !this.hideResetButton
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
.path=${mdiRestart}
|
||||
@click=${this._handleZoomReset}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.history_charts.zoom_reset"
|
||||
)}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
<slot name="button"></slot>
|
||||
<div class="top-controls ${classMap({ small: this.smallControls })}">
|
||||
<slot name="search"></slot>
|
||||
<div
|
||||
class="chart-controls ${classMap({ small: this.smallControls })}"
|
||||
>
|
||||
${this._isZoomed && !this.hideResetButton
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
.path=${mdiRestart}
|
||||
@click=${this._handleZoomReset}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.history_charts.zoom_reset"
|
||||
)}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
<slot name="button"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderLegend() {
|
||||
private _getLegendItems() {
|
||||
if (!this.options?.legend || !this.data) {
|
||||
return nothing;
|
||||
return undefined;
|
||||
}
|
||||
const legend = ensureArray(this.options.legend).find(
|
||||
(l) => l.show && l.type === "custom"
|
||||
) as CustomLegendOption | undefined;
|
||||
if (!legend) {
|
||||
return nothing;
|
||||
return undefined;
|
||||
}
|
||||
const datasets = ensureArray(this.data);
|
||||
const items =
|
||||
return (
|
||||
legend.data ||
|
||||
datasets
|
||||
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
|
||||
.map((d) => ({ id: d.id, name: d.name }));
|
||||
.map((d) => ({ id: d.id, name: d.name }))
|
||||
);
|
||||
}
|
||||
|
||||
private _renderLegend() {
|
||||
const items = this._getLegendItems();
|
||||
if (!items) {
|
||||
return nothing;
|
||||
}
|
||||
const datasets = ensureArray(this.data!);
|
||||
|
||||
const isMobile = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
@@ -357,6 +376,11 @@ export class HaChartBase extends LitElement {
|
||||
return html`<li
|
||||
.id=${id}
|
||||
@click=${this._legendClick}
|
||||
@pointerdown=${this._legendPointerDown}
|
||||
@pointerup=${this._legendPointerCancel}
|
||||
@pointerleave=${this._legendPointerCancel}
|
||||
@pointercancel=${this._legendPointerCancel}
|
||||
@contextmenu=${this._legendContextMenu}
|
||||
class=${classMap({ hidden: this._hiddenDatasets.has(id) })}
|
||||
.title=${name}
|
||||
>
|
||||
@@ -627,7 +651,7 @@ export class HaChartBase extends LitElement {
|
||||
hideOverlap: true,
|
||||
...axis.axisLabel,
|
||||
},
|
||||
minInterval,
|
||||
minInterval: axis.minInterval ?? minInterval,
|
||||
} as XAXisOption;
|
||||
});
|
||||
}
|
||||
@@ -1017,11 +1041,52 @@ export class HaChartBase extends LitElement {
|
||||
fireEvent(this, "chart-zoom", { start, end });
|
||||
}
|
||||
|
||||
private _legendClick(ev: any) {
|
||||
// Long-press to solo on touch/pen devices (500ms, consistent with action-handler-directive)
|
||||
private _legendPointerDown(ev: PointerEvent) {
|
||||
// Mouse uses Ctrl/Cmd+click instead
|
||||
if (ev.pointerType === "mouse") {
|
||||
return;
|
||||
}
|
||||
const id = (ev.currentTarget as HTMLElement)?.id;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
this._longPressTriggered = false;
|
||||
this._longPressTimer = setTimeout(() => {
|
||||
this._longPressTriggered = true;
|
||||
this._longPressTimer = undefined;
|
||||
this._soloLegend(id);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private _legendPointerCancel() {
|
||||
if (this._longPressTimer) {
|
||||
clearTimeout(this._longPressTimer);
|
||||
this._longPressTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _legendContextMenu(ev: Event) {
|
||||
if (this._longPressTimer || this._longPressTriggered) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private _legendClick(ev: MouseEvent) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
const id = ev.currentTarget?.id;
|
||||
if (this._longPressTriggered) {
|
||||
this._longPressTriggered = false;
|
||||
return;
|
||||
}
|
||||
const id = (ev.currentTarget as HTMLElement)?.id;
|
||||
// Cmd+click on Mac (Ctrl+click is right-click there), Ctrl+click elsewhere
|
||||
const soloModifier = isMac ? ev.metaKey : ev.ctrlKey;
|
||||
if (soloModifier) {
|
||||
this._soloLegend(id);
|
||||
return;
|
||||
}
|
||||
if (this._hiddenDatasets.has(id)) {
|
||||
this._getAllIdsFromLegend(this.options, id).forEach((i) =>
|
||||
this._hiddenDatasets.delete(i)
|
||||
@@ -1036,6 +1101,60 @@ export class HaChartBase extends LitElement {
|
||||
this.requestUpdate("_hiddenDatasets");
|
||||
}
|
||||
|
||||
private _soloLegend(id: string) {
|
||||
const allIds = this._getAllLegendIds();
|
||||
const clickedIds = this._getAllIdsFromLegend(this.options, id);
|
||||
const otherIds = allIds.filter((i) => !clickedIds.includes(i));
|
||||
|
||||
const clickedIsOnlyVisible =
|
||||
clickedIds.every((i) => !this._hiddenDatasets.has(i)) &&
|
||||
otherIds.every((i) => this._hiddenDatasets.has(i));
|
||||
|
||||
if (clickedIsOnlyVisible) {
|
||||
// Already solo'd on this item — restore all series to visible
|
||||
for (const hiddenId of [...this._hiddenDatasets]) {
|
||||
this._hiddenDatasets.delete(hiddenId);
|
||||
fireEvent(this, "dataset-unhidden", { id: hiddenId });
|
||||
}
|
||||
} else {
|
||||
// Solo: hide every other series, unhide clicked if it was hidden
|
||||
for (const otherId of otherIds) {
|
||||
if (!this._hiddenDatasets.has(otherId)) {
|
||||
this._hiddenDatasets.add(otherId);
|
||||
fireEvent(this, "dataset-hidden", { id: otherId });
|
||||
}
|
||||
}
|
||||
for (const clickedId of clickedIds) {
|
||||
if (this._hiddenDatasets.has(clickedId)) {
|
||||
this._hiddenDatasets.delete(clickedId);
|
||||
fireEvent(this, "dataset-unhidden", { id: clickedId });
|
||||
}
|
||||
}
|
||||
}
|
||||
this.requestUpdate("_hiddenDatasets");
|
||||
}
|
||||
|
||||
private _getAllLegendIds(): string[] {
|
||||
const items = this._getLegendItems();
|
||||
if (!items) {
|
||||
return [];
|
||||
}
|
||||
const allIds = new Set<string>();
|
||||
for (const item of items) {
|
||||
const primaryId =
|
||||
typeof item === "string"
|
||||
? item
|
||||
: ((item.id as string) ?? (item.name as string) ?? "");
|
||||
for (const expandedId of this._getAllIdsFromLegend(
|
||||
this.options,
|
||||
primaryId
|
||||
)) {
|
||||
allIds.add(expandedId);
|
||||
}
|
||||
}
|
||||
return [...allIds];
|
||||
}
|
||||
|
||||
private _toggleExpandedLegend() {
|
||||
this.expandLegend = !this.expandLegend;
|
||||
setTimeout(() => {
|
||||
@@ -1116,16 +1235,35 @@ export class HaChartBase extends LitElement {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.chart-controls {
|
||||
.top-controls {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 4px;
|
||||
top: var(--ha-space-4);
|
||||
inset-inline-start: var(--ha-space-4);
|
||||
inset-inline-end: var(--ha-space-1);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--ha-space-2);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
::slotted([slot="search"]) {
|
||||
flex: 1 1 250px;
|
||||
min-width: 0;
|
||||
max-width: 250px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.chart-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-1);
|
||||
margin-inline-start: auto;
|
||||
flex-shrink: 0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.top-controls.small {
|
||||
top: 0;
|
||||
}
|
||||
.chart-controls.small {
|
||||
top: 0;
|
||||
flex-direction: row;
|
||||
}
|
||||
.chart-controls ha-icon-button,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type { GraphSeriesOption } from "echarts/charts";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
|
||||
import type {
|
||||
CallbackDataParams,
|
||||
TopLevelFormatterParams,
|
||||
@@ -63,6 +65,8 @@ export interface NetworkData {
|
||||
categories?: { name: string; symbol: string }[];
|
||||
}
|
||||
|
||||
const PHYSICS_DISABLE_THRESHOLD = 512;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
|
||||
let GraphChart: typeof import("echarts/lib/chart/graph/install");
|
||||
|
||||
@@ -76,11 +80,23 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
params: TopLevelFormatterParams
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* Optional callback that returns additional searchable strings for a node.
|
||||
* These are matched against the search filter in addition to the node's name and context.
|
||||
*/
|
||||
@property({ attribute: false }) public searchableAttributes?: (
|
||||
nodeId: string
|
||||
) => string[];
|
||||
|
||||
@property({ attribute: false }) public searchFilter = "";
|
||||
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@state() private _highlightedNodes?: Set<string>;
|
||||
|
||||
@state() private _reducedMotion = false;
|
||||
|
||||
@state() private _physicsEnabled = true;
|
||||
@state() private _physicsEnabled?: boolean;
|
||||
|
||||
@state() private _showLabels = true;
|
||||
|
||||
@@ -108,6 +124,14 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
super.willUpdate(changedProperties);
|
||||
if (this._physicsEnabled === undefined && this.data?.nodes?.length > 1) {
|
||||
this._physicsEnabled =
|
||||
this.data.nodes.length <= PHYSICS_DISABLE_THRESHOLD;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!GraphChart || !this.data.nodes?.length) {
|
||||
return nothing;
|
||||
@@ -117,19 +141,24 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
|
||||
const hasHighlightedNodes =
|
||||
this._highlightedNodes && this._highlightedNodes.size > 0;
|
||||
|
||||
return html`<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.data=${this._getSeries(
|
||||
this.data,
|
||||
this._physicsEnabled,
|
||||
this._physicsEnabled ?? false,
|
||||
this._reducedMotion,
|
||||
this._showLabels,
|
||||
isMobile
|
||||
isMobile,
|
||||
hasHighlightedNodes
|
||||
)}
|
||||
.options=${this._createOptions(this.data?.categories)}
|
||||
height="100%"
|
||||
.extraComponents=${[GraphChart]}
|
||||
>
|
||||
<slot name="search" slot="search"></slot>
|
||||
<slot name="button" slot="button"></slot>
|
||||
<ha-icon-button
|
||||
slot="button"
|
||||
@@ -165,7 +194,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
...category,
|
||||
icon: category.symbol,
|
||||
})),
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
},
|
||||
dataZoom: {
|
||||
type: "inside",
|
||||
@@ -175,13 +204,56 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
deepEqual
|
||||
);
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("searchFilter")) {
|
||||
const filter = this.searchFilter;
|
||||
if (!filter) {
|
||||
this._highlightedNodes = undefined;
|
||||
} else {
|
||||
const lowerFilter = filter.toLowerCase();
|
||||
const matchingIds = new Set<string>();
|
||||
for (const node of this.data.nodes) {
|
||||
if (this._nodeMatchesFilter(node, lowerFilter)) {
|
||||
matchingIds.add(node.id);
|
||||
}
|
||||
}
|
||||
this._highlightedNodes = matchingIds;
|
||||
}
|
||||
this._applyHighlighting();
|
||||
this._updateMouseoverHandler();
|
||||
}
|
||||
}
|
||||
|
||||
private _nodeMatchesFilter(node: NetworkNode, lowerFilter: string): boolean {
|
||||
if (node.name?.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
if (node.context?.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
if (node.id?.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
if (this.searchableAttributes) {
|
||||
const extraValues = this.searchableAttributes(node.id);
|
||||
for (const value of extraValues) {
|
||||
if (value?.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _getSeries = memoizeOne(
|
||||
(
|
||||
data: NetworkData,
|
||||
physicsEnabled: boolean,
|
||||
reducedMotion: boolean,
|
||||
showLabels: boolean,
|
||||
isMobile: boolean
|
||||
isMobile: boolean,
|
||||
hasHighlightedNodes?: boolean
|
||||
) => ({
|
||||
id: "network",
|
||||
type: "graph",
|
||||
@@ -214,7 +286,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
},
|
||||
emphasis: {
|
||||
focus: isMobile ? "none" : "adjacency",
|
||||
focus: hasHighlightedNodes ? "self" : isMobile ? "none" : "adjacency",
|
||||
},
|
||||
force: {
|
||||
repulsion: [400, 600],
|
||||
@@ -362,6 +434,68 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _applyHighlighting() {
|
||||
const chart = this._baseChart?.chart;
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
// Reset all nodes to normal opacity first
|
||||
chart.dispatchAction({ type: "downplay" });
|
||||
|
||||
const highlighted = this._highlightedNodes;
|
||||
if (!highlighted || highlighted.size === 0) {
|
||||
return;
|
||||
}
|
||||
const dataIndices: number[] = [];
|
||||
this.data.nodes.forEach((node, index) => {
|
||||
if (highlighted.has(node.id)) {
|
||||
dataIndices.push(index);
|
||||
}
|
||||
});
|
||||
if (dataIndices.length > 0) {
|
||||
chart.dispatchAction({ type: "highlight", dataIndex: dataIndices });
|
||||
}
|
||||
}
|
||||
|
||||
private _emphasisGuardHandler?: () => void;
|
||||
|
||||
private _updateMouseoverHandler() {
|
||||
const chart = this._baseChart?.chart;
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When there are highlighted nodes, re-apply highlighting on hover
|
||||
// and mouseout to prevent hover from overriding the search state
|
||||
if (this._highlightedNodes && this._highlightedNodes.size > 0) {
|
||||
if (this._emphasisGuardHandler) {
|
||||
// Guard already set
|
||||
return;
|
||||
}
|
||||
this._emphasisGuardHandler = () => {
|
||||
this._applyHighlighting();
|
||||
};
|
||||
chart.on("mouseover", this._emphasisGuardHandler);
|
||||
chart.on("mouseout", this._emphasisGuardHandler);
|
||||
} else {
|
||||
if (!this._emphasisGuardHandler) {
|
||||
return;
|
||||
}
|
||||
chart.off("mouseover", this._emphasisGuardHandler);
|
||||
chart.off("mouseout", this._emphasisGuardHandler);
|
||||
this._emphasisGuardHandler = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._emphasisGuardHandler) {
|
||||
this._baseChart?.chart?.off("mouseover", this._emphasisGuardHandler);
|
||||
this._baseChart?.chart?.off("mouseout", this._emphasisGuardHandler);
|
||||
this._emphasisGuardHandler = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _togglePhysics() {
|
||||
this._saveNodePositions();
|
||||
this._physicsEnabled = !this._physicsEnabled;
|
||||
|
||||
@@ -28,6 +28,13 @@ const safeParseFloat = (value) => {
|
||||
return isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
const CLIMATE_MODE_CONFIGS = [
|
||||
{ mode: "heat", action: "heating", cssVar: "--state-climate-heat-color" },
|
||||
{ mode: "cool", action: "cooling", cssVar: "--state-climate-cool-color" },
|
||||
{ mode: "dry", action: "drying", cssVar: "--state-climate-dry-color" },
|
||||
{ mode: "fan_only", action: "fan", cssVar: "--state-climate-fan_only-color" },
|
||||
] as const;
|
||||
|
||||
@customElement("state-history-chart-line")
|
||||
export class StateHistoryChartLine extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -429,23 +436,18 @@ export class StateHistoryChartLine extends LitElement {
|
||||
(entityState) => entityState.attributes?.hvac_action
|
||||
);
|
||||
|
||||
const isHeating =
|
||||
domain === "climate" && hasHvacAction
|
||||
? (entityState: LineChartState) =>
|
||||
CLIMATE_HVAC_ACTION_TO_MODE[
|
||||
entityState.attributes?.hvac_action
|
||||
] === "heat"
|
||||
: (entityState: LineChartState) => entityState.state === "heat";
|
||||
const isCooling =
|
||||
domain === "climate" && hasHvacAction
|
||||
? (entityState: LineChartState) =>
|
||||
CLIMATE_HVAC_ACTION_TO_MODE[
|
||||
entityState.attributes?.hvac_action
|
||||
] === "cool"
|
||||
: (entityState: LineChartState) => entityState.state === "cool";
|
||||
|
||||
const hasHeat = states.states.some(isHeating);
|
||||
const hasCool = states.states.some(isCooling);
|
||||
const activeModes = CLIMATE_MODE_CONFIGS.map(
|
||||
({ mode, action, cssVar }) => {
|
||||
const isActive =
|
||||
domain === "climate" && hasHvacAction
|
||||
? (entityState: LineChartState) =>
|
||||
CLIMATE_HVAC_ACTION_TO_MODE[
|
||||
entityState.attributes?.hvac_action
|
||||
] === mode
|
||||
: (entityState: LineChartState) => entityState.state === mode;
|
||||
return { action, cssVar, isActive };
|
||||
}
|
||||
).filter(({ isActive }) => states.states.some(isActive));
|
||||
// We differentiate between thermostats that have a target temperature
|
||||
// range versus ones that have just a target temperature
|
||||
|
||||
@@ -466,33 +468,19 @@ export class StateHistoryChartLine extends LitElement {
|
||||
"component.climate.entity_component._.state_attributes.current_temperature.name"
|
||||
)
|
||||
);
|
||||
if (hasHeat) {
|
||||
for (const { action, cssVar } of activeModes) {
|
||||
addDataSet(
|
||||
states.entity_id + "-heating",
|
||||
`${states.entity_id}-${action}`,
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.heating", { name: name })
|
||||
? this.hass.localize(`ui.card.climate.${action}`, {
|
||||
name: name,
|
||||
})
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.hvac_action.state.heating"
|
||||
`component.climate.entity_component._.state_attributes.hvac_action.state.${action}`
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-climate-heat-color"),
|
||||
computedStyles.getPropertyValue(cssVar),
|
||||
true
|
||||
);
|
||||
// The "heating" series uses steppedArea to shade the area below the current
|
||||
// temperature when the thermostat is calling for heat.
|
||||
}
|
||||
if (hasCool) {
|
||||
addDataSet(
|
||||
states.entity_id + "-cooling",
|
||||
this.showNames
|
||||
? this.hass.localize("ui.card.climate.cooling", { name: name })
|
||||
: this.hass.localize(
|
||||
"component.climate.entity_component._.state_attributes.hvac_action.state.cooling"
|
||||
),
|
||||
computedStyles.getPropertyValue("--state-climate-cool-color"),
|
||||
true
|
||||
);
|
||||
// The "cooling" series uses steppedArea to shade the area below the current
|
||||
// temperature when the thermostat is calling for heat.
|
||||
}
|
||||
|
||||
if (hasTargetRange) {
|
||||
@@ -540,11 +528,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
entityState.attributes.current_temperature
|
||||
);
|
||||
const series = [curTemp];
|
||||
if (hasHeat) {
|
||||
series.push(isHeating(entityState) ? curTemp : null);
|
||||
}
|
||||
if (hasCool) {
|
||||
series.push(isCooling(entityState) ? curTemp : null);
|
||||
for (const { isActive } of activeModes) {
|
||||
series.push(isActive(entityState) ? curTemp : null);
|
||||
}
|
||||
if (hasTargetRange) {
|
||||
const targetHigh = safeParseFloat(
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
} from "../../data/recorder";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { getPeriodicAxisLabelConfig } from "./axis-label";
|
||||
import type { CustomLegendOption } from "./ha-chart-base";
|
||||
import "./ha-chart-base";
|
||||
|
||||
@@ -293,6 +294,22 @@ export class StatisticsChart extends LitElement {
|
||||
type: "time",
|
||||
min: startTime,
|
||||
max: this.endTime,
|
||||
...(this.period === "month" && {
|
||||
minInterval: 28 * 24 * 3600 * 1000,
|
||||
axisLabel: getPeriodicAxisLabelConfig(
|
||||
"month",
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
}),
|
||||
...(this.period === "year" && {
|
||||
minInterval: 365 * 24 * 3600 * 1000,
|
||||
axisLabel: getPeriodicAxisLabelConfig(
|
||||
"year",
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "hiddenAxis",
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { computeCssColor } from "../../common/color/compute-color";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
@@ -53,16 +52,15 @@ class HaDataTableLabels extends LitElement {
|
||||
}
|
||||
|
||||
private _renderLabel(label: LabelRegistryEntry, clickAction: boolean) {
|
||||
const color = label?.color ? computeCssColor(label.color) : undefined;
|
||||
return html`
|
||||
<ha-label
|
||||
dense
|
||||
role="button"
|
||||
tabindex="0"
|
||||
.color=${label.color}
|
||||
.item=${label}
|
||||
@click=${clickAction ? this._labelClicked : undefined}
|
||||
@keydown=${clickAction ? this._labelClicked : undefined}
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label?.icon
|
||||
@@ -102,10 +100,6 @@ class HaDataTableLabels extends LitElement {
|
||||
position: fixed;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
ha-label {
|
||||
--ha-label-background-color: var(--color, var(--grey-color));
|
||||
--ha-label-background-opacity: 0.5;
|
||||
}
|
||||
.plus {
|
||||
--ha-label-background-color: transparent;
|
||||
border: 1px solid var(--divider-color);
|
||||
|
||||
@@ -27,7 +27,7 @@ import type { HomeAssistant } from "../../types";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-svg-icon";
|
||||
import "../search-input";
|
||||
import "../input/ha-input-search";
|
||||
import { filterData, sortData } from "./sort-filter";
|
||||
|
||||
export interface RowClickedEvent {
|
||||
@@ -391,11 +391,11 @@ export class HaDataTable extends LitElement {
|
||||
${this._filterable
|
||||
? html`
|
||||
<div class="table-header">
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.searchLabel}
|
||||
></search-input>
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
@input=${this._handleSearchChange}
|
||||
.placeholder=${this.searchLabel}
|
||||
></ha-input-search>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
@@ -970,12 +970,12 @@ export class HaDataTable extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent): void {
|
||||
private _handleSearchChange(ev: InputEvent): void {
|
||||
if (this.filter) {
|
||||
return;
|
||||
}
|
||||
this._lastSelectedRowId = null;
|
||||
this._debounceSearch(ev.detail.value);
|
||||
this._debounceSearch((ev.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
private async _calcTableHeight() {
|
||||
@@ -1388,11 +1388,9 @@ export class HaDataTable extends LitElement {
|
||||
.table-header {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
search-input {
|
||||
display: block;
|
||||
ha-input-search {
|
||||
flex: 1;
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: transparent;
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
slot[name="header"] {
|
||||
display: block;
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiCalendarToday } from "@mdi/js";
|
||||
import "cally";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
|
||||
import {
|
||||
formatCallyDateRange,
|
||||
@@ -29,6 +29,7 @@ import "../ha-list-item";
|
||||
import "../ha-time-input";
|
||||
import type { DateRangePickerRanges } from "./ha-date-range-picker";
|
||||
import { datePickerStyles, dateRangePickerStyles } from "./styles";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
|
||||
@customElement("date-range-picker")
|
||||
export class DateRangePicker extends LitElement {
|
||||
@@ -69,6 +70,8 @@ export class DateRangePicker extends LitElement {
|
||||
to: { hours: 23, minutes: 59 },
|
||||
};
|
||||
|
||||
@queryAll("ha-time-input") private _timeInputs?: NodeListOf<HaTimeInput>;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -153,6 +156,7 @@ export class DateRangePicker extends LitElement {
|
||||
)}
|
||||
id="from"
|
||||
placeholder-labels
|
||||
auto-validate
|
||||
></ha-time-input>
|
||||
<ha-time-input
|
||||
.value=${`${this._timeValue.to.hours}:${this._timeValue.to.minutes}`}
|
||||
@@ -163,6 +167,7 @@ export class DateRangePicker extends LitElement {
|
||||
)}
|
||||
id="to"
|
||||
placeholder-labels
|
||||
auto-validate
|
||||
></ha-time-input>
|
||||
</div>
|
||||
`
|
||||
@@ -200,6 +205,14 @@ export class DateRangePicker extends LitElement {
|
||||
let endDate = new Date(`${dates[1]}T23:59:00`);
|
||||
|
||||
if (this.timePicker) {
|
||||
const timeInputs = this._timeInputs;
|
||||
if (
|
||||
timeInputs &&
|
||||
![...timeInputs].every((input) => input.reportValidity())
|
||||
) {
|
||||
// If we have time inputs, and they don't all report valid, don't save
|
||||
return;
|
||||
}
|
||||
startDate.setHours(this._timeValue.from.hours);
|
||||
startDate.setMinutes(this._timeValue.from.minutes);
|
||||
endDate.setHours(this._timeValue.to.hours);
|
||||
@@ -261,12 +274,6 @@ export class DateRangePicker extends LitElement {
|
||||
const dateRange: [Date, Date] = Object.values(this.ranges!)[
|
||||
ev.detail.index
|
||||
];
|
||||
this._dateValue = formatCallyDateRange(
|
||||
dateRange[0],
|
||||
dateRange[1],
|
||||
this.locale,
|
||||
this.hassConfig
|
||||
);
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
startDate: dateRange[0],
|
||||
@@ -281,7 +288,8 @@ export class DateRangePicker extends LitElement {
|
||||
private _handleChangeTime(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const time = ev.detail.value;
|
||||
const type = (ev.target as HaBaseTimeInput).id;
|
||||
const target = ev.target as HaBaseTimeInput;
|
||||
const type = target.id;
|
||||
if (time) {
|
||||
if (!this._timeValue) {
|
||||
this._timeValue = {
|
||||
@@ -301,17 +309,39 @@ export class DateRangePicker extends LitElement {
|
||||
css`
|
||||
.picker {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.date-range-ranges {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
min-width: 140px;
|
||||
flex: 0 1 30%;
|
||||
}
|
||||
|
||||
.range {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
padding: var(--ha-space-3);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 460px) {
|
||||
.picker {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.date-range-ranges {
|
||||
flex-basis: 180px;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
border-right: none;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.range {
|
||||
flex-basis: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.times {
|
||||
@@ -326,12 +356,6 @@ export class DateRangePicker extends LitElement {
|
||||
padding: var(--ha-space-2);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.date-range-ranges {
|
||||
max-width: 30%;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -80,33 +80,6 @@ export const datePickerStyles = css`
|
||||
text-align: center;
|
||||
margin-left: 48px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
calendar-month {
|
||||
min-height: calc(34px * 7);
|
||||
}
|
||||
calendar-month::part(day) {
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
calendar-month::part(button) {
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
}
|
||||
calendar-month::part(range-inner),
|
||||
calendar-month::part(range-start),
|
||||
calendar-month::part(range-end),
|
||||
calendar-month::part(selected),
|
||||
calendar-month::part(selected):hover {
|
||||
height: 34px;
|
||||
width: 34px;
|
||||
}
|
||||
.heading {
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
.month-year {
|
||||
margin-left: 40px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const dateRangePickerStyles = css`
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
DEFAULT_ENTITY_NAME,
|
||||
type EntityNameItem,
|
||||
} from "../../common/entity/compute_entity_name_display";
|
||||
import type { EntityNameItem } from "../../common/entity/compute_entity_name_display";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import type { EntityNameType } from "../../common/translations/entity-state";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
@@ -17,12 +15,14 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-button-toggle-group";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import "../ha-input-helper-text";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import "../ha-sortable";
|
||||
import "../input/ha-input";
|
||||
|
||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
@@ -73,10 +73,291 @@ export class HaEntityNamePicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
|
||||
@state() private _mode?: "composed" | "custom";
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
private _editIndex?: number;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
const items = this._toItems(this.value);
|
||||
this._mode =
|
||||
items.length === 1 && items[0].type === "text" ? "custom" : "composed";
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||
if (this._mode === undefined) {
|
||||
const items = this._toItems(this.value);
|
||||
this._mode =
|
||||
items.length === 1 && items[0].type === "text" ? "custom" : "composed";
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const modeButtons = [
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.mode_composed"
|
||||
),
|
||||
value: "composed",
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.mode_custom"
|
||||
),
|
||||
value: "custom",
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-button-toggle-group
|
||||
size="small"
|
||||
.buttons=${modeButtons}
|
||||
.active=${this._mode}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._modeChanged}
|
||||
></ha-button-toggle-group>
|
||||
</div>
|
||||
<div class="content">
|
||||
${this._mode === "custom"
|
||||
? this._renderTextInput()
|
||||
: this._renderPicker()}
|
||||
</div>
|
||||
</div>
|
||||
${this.helper
|
||||
? html`
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTextInput() {
|
||||
const items = this._items;
|
||||
const value =
|
||||
items.length === 1 && items[0].type === "text" ? items[0].text || "" : "";
|
||||
return html`
|
||||
<ha-input
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.value=${value}
|
||||
@input=${this._textInputChanged}
|
||||
></ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPicker() {
|
||||
const value = this._items;
|
||||
const validTypes = this._validTypes(this.entityId);
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.rowRenderer=${rowRenderer}
|
||||
.value=${this._getPickerValue()}
|
||||
allow-custom-value
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.custom_name"
|
||||
)}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
.searchFn=${this._searchFn}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.search"
|
||||
)}
|
||||
>
|
||||
<div slot="field" class="field">
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
this._items,
|
||||
(item) => item,
|
||||
(item: EntityNameItem, idx) => {
|
||||
const label = this._formatItem(item);
|
||||
const isValid = validTypes.has(item.type);
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._editItem}
|
||||
.label=${label}
|
||||
.selected=${!this.disabled}
|
||||
.disabled=${this.disabled}
|
||||
class=${!isValid ? "invalid" : ""}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
<span>${label}</span>
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</div>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _modeChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._mode = ev.detail.value as "composed" | "custom";
|
||||
}
|
||||
|
||||
private _textInputChanged(ev: Event) {
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
const newValue: EntityNameItem[] = value
|
||||
? [{ type: "text", text: value }]
|
||||
: [];
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private async _addItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._editIndex = undefined;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private async _editItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const idx = parseInt(
|
||||
(ev.currentTarget as HTMLElement).dataset.idx || "",
|
||||
10
|
||||
);
|
||||
this._editIndex = idx;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
const value = this._items[idx];
|
||||
// Pre-fill the field value when editing a text item
|
||||
if (value.type === "text" && value.text) {
|
||||
this._picker?.setFieldValue(value.text);
|
||||
}
|
||||
}
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const value = this._items;
|
||||
const newValue = value.concat();
|
||||
const element = newValue.splice(oldIndex, 1)[0];
|
||||
newValue.splice(newIndex, 0, element);
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private async _removeItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const value = [...this._items];
|
||||
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
||||
value.splice(idx, 1);
|
||||
this._setValue(value);
|
||||
}
|
||||
|
||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (this.disabled || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item: EntityNameItem = parseOptionValue(value);
|
||||
|
||||
const newValue = [...this._items];
|
||||
|
||||
if (this._editIndex != null) {
|
||||
newValue[this._editIndex] = item;
|
||||
this._editIndex = undefined;
|
||||
} else {
|
||||
newValue.push(item);
|
||||
}
|
||||
|
||||
this._setValue(newValue);
|
||||
|
||||
if (this._picker) {
|
||||
this._picker.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: EntityNameItem[]) {
|
||||
const newValue = this._toValue(value);
|
||||
this.value = newValue;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
private get _items(): EntityNameItem[] {
|
||||
return this._toItems(this.value);
|
||||
}
|
||||
|
||||
private _toItems = memoizeOne((value?: typeof this.value) => {
|
||||
if (typeof value === "string") {
|
||||
if (value === "") {
|
||||
return [];
|
||||
}
|
||||
return [{ type: "text", text: value } satisfies EntityNameItem];
|
||||
}
|
||||
return value ? ensureArray(value) : [];
|
||||
});
|
||||
|
||||
private _toValue = memoizeOne(
|
||||
(items: EntityNameItem[]): typeof this.value => {
|
||||
if (items.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (items.length === 1) {
|
||||
const item = items[0];
|
||||
return item.type === "text" ? item.text : item;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _formatItem = (item: EntityNameItem) => {
|
||||
if (item.type === "text") {
|
||||
return `"${item.text}"`;
|
||||
}
|
||||
if (KNOWN_TYPES.has(item.type)) {
|
||||
return this.hass.localize(
|
||||
`ui.components.entity.entity-name-picker.types.${item.type as EntityNameType}`
|
||||
);
|
||||
}
|
||||
return item.type;
|
||||
};
|
||||
|
||||
private _validTypes = memoizeOne((entityId?: string) => {
|
||||
const options = new Set<string>(["text"]);
|
||||
if (!entityId) {
|
||||
@@ -161,157 +442,6 @@ export class HaEntityNamePicker extends LitElement {
|
||||
})
|
||||
);
|
||||
|
||||
private _formatItem = (item: EntityNameItem) => {
|
||||
if (item.type === "text") {
|
||||
return `"${item.text}"`;
|
||||
}
|
||||
if (KNOWN_TYPES.has(item.type)) {
|
||||
return this.hass.localize(
|
||||
`ui.components.entity.entity-name-picker.types.${item.type as EntityNameType}`
|
||||
);
|
||||
}
|
||||
return item.type;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
const value = this._items;
|
||||
const validTypes = this._validTypes(this.entityId);
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.rowRenderer=${rowRenderer}
|
||||
.value=${this._getPickerValue()}
|
||||
allow-custom-value
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.custom_name"
|
||||
)}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
.searchFn=${this._searchFn}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.search"
|
||||
)}
|
||||
>
|
||||
<div slot="field" class="container">
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
this._items,
|
||||
(item) => item,
|
||||
(item: EntityNameItem, idx) => {
|
||||
const label = this._formatItem(item);
|
||||
const isValid = validTypes.has(item.type);
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._editItem}
|
||||
.label=${label}
|
||||
.selected=${!this.disabled}
|
||||
.disabled=${this.disabled}
|
||||
class=${!isValid ? "invalid" : ""}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
<span>${label}</span>
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</div>
|
||||
</ha-generic-picker>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private async _addItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._editIndex = undefined;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private async _editItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const idx = parseInt(
|
||||
(ev.currentTarget as HTMLElement).dataset.idx || "",
|
||||
10
|
||||
);
|
||||
this._editIndex = idx;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
const value = this._items[idx];
|
||||
// Pre-fill the field value when editing a text item
|
||||
if (value.type === "text" && value.text) {
|
||||
this._picker?.setFieldValue(value.text);
|
||||
}
|
||||
}
|
||||
|
||||
private get _items(): EntityNameItem[] {
|
||||
return this._toItems(this.value);
|
||||
}
|
||||
|
||||
private _toItems = memoizeOne((value?: typeof this.value) => {
|
||||
if (typeof value === "string") {
|
||||
if (value === "") {
|
||||
return [];
|
||||
}
|
||||
return [{ type: "text", text: value } satisfies EntityNameItem];
|
||||
}
|
||||
return value ? ensureArray(value) : [...DEFAULT_ENTITY_NAME];
|
||||
});
|
||||
|
||||
private _toValue = memoizeOne(
|
||||
(items: EntityNameItem[]): typeof this.value => {
|
||||
if (items.length === 0) {
|
||||
return "";
|
||||
}
|
||||
if (items.length === 1) {
|
||||
const item = items[0];
|
||||
return item.type === "text" ? item.text : item;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _getPickerValue(): string | undefined {
|
||||
if (this._editIndex != null) {
|
||||
const item = this._items[this._editIndex];
|
||||
@@ -362,58 +492,6 @@ export class HaEntityNamePicker extends LitElement {
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const value = this._items;
|
||||
const newValue = value.concat();
|
||||
const element = newValue.splice(oldIndex, 1)[0];
|
||||
newValue.splice(newIndex, 0, element);
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private async _removeItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const value = [...this._items];
|
||||
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
||||
value.splice(idx, 1);
|
||||
this._setValue(value);
|
||||
}
|
||||
|
||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (this.disabled || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item: EntityNameItem = parseOptionValue(value);
|
||||
|
||||
const newValue = [...this._items];
|
||||
|
||||
if (this._editIndex != null) {
|
||||
newValue[this._editIndex] = item;
|
||||
this._editIndex = undefined;
|
||||
} else {
|
||||
newValue.push(item);
|
||||
}
|
||||
|
||||
this._setValue(newValue);
|
||||
|
||||
if (this._picker) {
|
||||
this._picker.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: EntityNameItem[]) {
|
||||
const newValue = this._toValue(value);
|
||||
this.value = newValue;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
@@ -421,13 +499,42 @@ export class HaEntityNamePicker extends LitElement {
|
||||
}
|
||||
|
||||
.container {
|
||||
--ha-input-padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
ha-generic-picker,
|
||||
ha-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field {
|
||||
position: relative;
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
border-end-end-radius: var(--ha-border-radius-square);
|
||||
border-end-start-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
.container:after {
|
||||
.field:after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -445,30 +552,25 @@ export class HaEntityNamePicker extends LitElement {
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
}
|
||||
:host([disabled]) .container:after {
|
||||
:host([disabled]) .field:after {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
}
|
||||
.container:focus-within:after {
|
||||
.field:focus-within:after {
|
||||
height: 2px;
|
||||
background-color: var(--mdc-theme-primary);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 0 0 var(--ha-space-2);
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
|
||||
.add {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-2) var(--ha-space-2);
|
||||
}
|
||||
|
||||
.invalid {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-relative-time";
|
||||
import "./state-badge";
|
||||
import "../ha-tooltip";
|
||||
import "./state-badge";
|
||||
|
||||
@customElement("state-info")
|
||||
class StateInfo extends LitElement {
|
||||
@@ -22,7 +21,7 @@ class StateInfo extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const name = computeStateName(this.stateObj);
|
||||
const name = this.hass.formatEntityName(this.stateObj, { type: "entity" });
|
||||
|
||||
return html`<state-badge
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -21,7 +21,6 @@ class AliasesEditor extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-input-multi
|
||||
.hass=${this.hass}
|
||||
.value=${this.aliases}
|
||||
.disabled=${this.disabled}
|
||||
.sortable=${this.sortable}
|
||||
|
||||
@@ -56,7 +56,10 @@ class HaAttributeValue extends LitElement {
|
||||
this.stateObj!,
|
||||
this.attribute
|
||||
);
|
||||
return parts.find((part) => part.type === "value")?.value;
|
||||
return parts
|
||||
.filter((part) => part.type === "value")
|
||||
.map((part) => part.value)
|
||||
.join("");
|
||||
}
|
||||
|
||||
return this.hass.formatEntityAttributeValue(this.stateObj!, this.attribute);
|
||||
|
||||
@@ -137,7 +137,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
@property({ attribute: "placeholder-labels", type: Boolean })
|
||||
public placeholderLabels = false;
|
||||
|
||||
@queryAll("ha-input") private _inputs?: HaInput[];
|
||||
@queryAll("ha-input") private _inputs?: NodeListOf<HaInput>;
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
@@ -145,7 +145,9 @@ export class HaBaseTimeInput extends LitElement {
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._inputs?.every((input) => input.reportValidity()) ?? true;
|
||||
const inputs = this._inputs;
|
||||
if (!inputs) return true;
|
||||
return [...inputs].every((input) => input.reportValidity());
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -399,7 +401,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
|
||||
.time-separator,
|
||||
ha-icon-button {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
background-color: var(--ha-color-form-background);
|
||||
color: var(--ha-color-text-secondary);
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-loud);
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -57,6 +57,7 @@ export class HaButton extends Button {
|
||||
.button {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: 1;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
transition: background-color var(--ha-animation-duration-fast)
|
||||
ease-out;
|
||||
|
||||
@@ -100,6 +100,9 @@ export class HaDropdown extends Dropdown {
|
||||
#menu {
|
||||
padding: var(--ha-space-1);
|
||||
}
|
||||
wa-popup::part(popup) {
|
||||
z-index: 200;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./search-input-outlined";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-devices")
|
||||
export class HaFilterDevices extends LitElement {
|
||||
@@ -67,12 +68,12 @@ export class HaFilterDevices extends LitElement {
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
</ha-input-search>
|
||||
<ha-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._devices(
|
||||
@@ -138,8 +139,9 @@ export class HaFilterDevices extends LitElement {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value.toLowerCase();
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const target = ev.target as HaInputSearch;
|
||||
this._filter = (target.value ?? "").toLowerCase();
|
||||
}
|
||||
|
||||
private _devices = memoizeOne(
|
||||
@@ -249,7 +251,7 @@ export class HaFilterDevices extends LitElement {
|
||||
ha-check-list-item {
|
||||
width: 100%;
|
||||
}
|
||||
search-input-outlined {
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ import "./ha-check-list-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./search-input-outlined";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-domains")
|
||||
export class HaFilterDomains extends LitElement {
|
||||
@@ -49,12 +50,12 @@ export class HaFilterDomains extends LitElement {
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
class="ha-scrollbar"
|
||||
@click=${this._handleItemClick}
|
||||
@@ -155,8 +156,9 @@ export class HaFilterDomains extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value.toLowerCase();
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const target = ev.target as HaInputSearch;
|
||||
this._filter = (target.value ?? "").toLowerCase();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -201,7 +203,7 @@ export class HaFilterDomains extends LitElement {
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
search-input-outlined {
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./ha-state-icon";
|
||||
import "./search-input-outlined";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-entities")
|
||||
export class HaFilterEntities extends LitElement {
|
||||
@@ -70,12 +71,12 @@ export class HaFilterEntities extends LitElement {
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`
|
||||
<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
</ha-input-search>
|
||||
<ha-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._entities(
|
||||
@@ -149,8 +150,9 @@ export class HaFilterEntities extends LitElement {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value.toLowerCase();
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const target = ev.target as HaInputSearch;
|
||||
this._filter = (target.value ?? "").toLowerCase();
|
||||
}
|
||||
|
||||
private _entities = memoizeOne(
|
||||
@@ -265,7 +267,7 @@ export class HaFilterEntities extends LitElement {
|
||||
--mdc-list-item-graphic-margin: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
search-input-outlined {
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import "./ha-check-list-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./search-input-outlined";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-integrations")
|
||||
export class HaFilterIntegrations extends LitElement {
|
||||
@@ -52,12 +53,12 @@ export class HaFilterIntegrations extends LitElement {
|
||||
: nothing}
|
||||
</div>
|
||||
${this._manifests && this._shouldRender
|
||||
? html`<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
class="ha-scrollbar"
|
||||
@click=${this._handleItemClick}
|
||||
@@ -175,8 +176,9 @@ export class HaFilterIntegrations extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value.toLowerCase();
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const target = ev.target as HaInputSearch;
|
||||
this._filter = (target.value ?? "").toLowerCase();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -221,7 +223,7 @@ export class HaFilterIntegrations extends LitElement {
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
search-input-outlined {
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,11 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
||||
import { labelsContext } from "../data/context";
|
||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
@@ -21,7 +20,8 @@ import "./ha-icon-button";
|
||||
import "./ha-label";
|
||||
import "./ha-list";
|
||||
import "./ha-list-item";
|
||||
import "./search-input-outlined";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-labels")
|
||||
export class HaFilterLabels extends LitElement {
|
||||
@@ -79,12 +79,12 @@ export class HaFilterLabels extends LitElement {
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
@selected=${this._labelSelected}
|
||||
class="ha-scrollbar"
|
||||
@@ -97,17 +97,14 @@ export class HaFilterLabels extends LitElement {
|
||||
this.value
|
||||
),
|
||||
(label) => label.label_id,
|
||||
(label) => {
|
||||
const color = label.color
|
||||
? computeCssColor(label.color)
|
||||
: undefined;
|
||||
return html`<ha-check-list-item
|
||||
(label) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${label.label_id}
|
||||
.selected=${(this.value || []).includes(label.label_id)}
|
||||
hasMeta
|
||||
>
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.color=${label.color}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label.icon
|
||||
@@ -118,8 +115,7 @@ export class HaFilterLabels extends LitElement {
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-check-list-item>`;
|
||||
}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list> `
|
||||
: nothing}
|
||||
@@ -163,8 +159,9 @@ export class HaFilterLabels extends LitElement {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value.toLowerCase();
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const value = (ev.target as HaInputSearch).value ?? "";
|
||||
this._filter = value.toLowerCase();
|
||||
}
|
||||
|
||||
private async _labelSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
@@ -251,17 +248,13 @@ export class HaFilterLabels extends LitElement {
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
}
|
||||
ha-label {
|
||||
--ha-label-background-color: var(--color, var(--grey-color));
|
||||
--ha-label-background-opacity: 0.5;
|
||||
}
|
||||
.add {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
search-input-outlined {
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
|
||||
@@ -142,6 +142,19 @@ export const computeInitialHaFormData = (
|
||||
])[firstChoice],
|
||||
};
|
||||
}
|
||||
} else if ("numeric_threshold" in selector) {
|
||||
const mode = selector.numeric_threshold?.mode ?? "crossed";
|
||||
const type = mode === "changed" ? "any" : "above";
|
||||
data[field.name] =
|
||||
type === "any"
|
||||
? { type }
|
||||
: {
|
||||
type,
|
||||
value: {
|
||||
number: selector.numeric_threshold?.number?.min ?? 0,
|
||||
active_choice: "number",
|
||||
},
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
`Selector ${Object.keys(selector)[0]} not supported in initial form data`
|
||||
|
||||
@@ -1,18 +1,44 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { computeCssColor } from "../common/color/compute-color";
|
||||
import { getContrastedColorHex } from "../common/color/rgb";
|
||||
import { uid } from "../common/util/uid";
|
||||
import "./ha-tooltip";
|
||||
|
||||
/**
|
||||
* Returns CSS styles for a label's background & icon/text
|
||||
* @param color Label color defined in HEX format
|
||||
* @returns CSS styles
|
||||
*/
|
||||
export const getLabelColorStyle = (labelColor: string | undefined | null) => {
|
||||
const color = labelColor ? computeCssColor(labelColor) : undefined;
|
||||
return color
|
||||
? `--ha-label-background-color: ${color};
|
||||
--primary-text-color: ${getContrastedColorHex(labelColor!)};`
|
||||
: `--ha-label-background-color: rgba(var(--rgb-primary-text-color), 0.15);`;
|
||||
};
|
||||
|
||||
@customElement("ha-label")
|
||||
class HaLabel extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) dense = false;
|
||||
|
||||
@property()
|
||||
public color?: string;
|
||||
|
||||
@property({ attribute: "description" })
|
||||
public description?: string;
|
||||
|
||||
private _elementId = "label-" + uid();
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
if (!changedProps.has("color")) {
|
||||
return;
|
||||
}
|
||||
this.style.cssText = getLabelColorStyle(this.color);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-tooltip
|
||||
@@ -36,10 +62,6 @@ class HaLabel extends LitElement {
|
||||
:host {
|
||||
--ha-label-text-color: var(--primary-text-color);
|
||||
--ha-label-icon-color: var(--primary-text-color);
|
||||
--ha-label-background-color: rgba(
|
||||
var(--rgb-primary-text-color),
|
||||
0.15
|
||||
);
|
||||
--ha-label-background-opacity: 1;
|
||||
border: 1px solid var(--outline-color);
|
||||
position: relative;
|
||||
|
||||
@@ -6,7 +6,6 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { labelsContext } from "../data/context";
|
||||
@@ -17,6 +16,7 @@ import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./chips/ha-chip-set";
|
||||
import "./chips/ha-input-chip";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import { getLabelColorStyle } from "./ha-label";
|
||||
import "./ha-label-picker";
|
||||
import type { HaLabelPicker } from "./ha-label-picker";
|
||||
import "./ha-tooltip";
|
||||
@@ -106,9 +106,14 @@ export class HaLabelsPicker extends LitElement {
|
||||
labels?.find((label) => label.label_id === id) || {
|
||||
label_id: id,
|
||||
name: id,
|
||||
color: "rgba(var(--rgb-primary-text-color), 0.15)",
|
||||
}
|
||||
)
|
||||
.sort((a, b) => stringCompare(a?.name || "", b?.name || "", language))
|
||||
.map((label) => ({
|
||||
...label,
|
||||
style: getLabelColorStyle(label.color),
|
||||
}))
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -135,9 +140,6 @@ export class HaLabelsPicker extends LitElement {
|
||||
(label) => label?.label_id,
|
||||
(label) => {
|
||||
if (!label) return nothing;
|
||||
const color = label.color
|
||||
? computeCssColor(label.color)
|
||||
: undefined;
|
||||
const elementId = "label-" + label.label_id;
|
||||
return html`
|
||||
<ha-tooltip
|
||||
@@ -154,7 +156,7 @@ export class HaLabelsPicker extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.label=${label.name}
|
||||
selected
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
style=${label.style}
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon
|
||||
@@ -239,8 +241,10 @@ export class HaLabelsPicker extends LitElement {
|
||||
height: var(--ha-space-8);
|
||||
}
|
||||
ha-input-chip {
|
||||
--md-input-chip-selected-container-color: var(--color, var(--grey-color));
|
||||
--ha-input-chip-selected-container-opacity: 0.5;
|
||||
--md-input-chip-selected-container-color: var(
|
||||
--ha-label-background-color,
|
||||
var(--grey-color)
|
||||
);
|
||||
--md-input-chip-selected-outline-width: 1px;
|
||||
}
|
||||
label {
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { OutlinedTextField } from "@material/web/textfield/internal/outlined-text-field";
|
||||
import { styles } from "@material/web/textfield/internal/outlined-styles";
|
||||
import { styles as sharedStyles } from "@material/web/textfield/internal/shared-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { literal } from "lit/static-html";
|
||||
import "./ha-outlined-field";
|
||||
|
||||
@customElement("ha-outlined-text-field")
|
||||
export class HaOutlinedTextField extends OutlinedTextField {
|
||||
protected readonly fieldTag = literal`ha-outlined-field`;
|
||||
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-outlined-text-field-input-text-color: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
--md-outlined-field-outline-color: var(--outline-color);
|
||||
--md-outlined-field-focus-outline-color: var(--primary-color);
|
||||
--md-outlined-field-hover-outline-color: var(--outline-hover-color);
|
||||
}
|
||||
:host([dense]) {
|
||||
--md-outlined-field-top-space: 5.5px;
|
||||
--md-outlined-field-bottom-space: 5.5px;
|
||||
--md-outlined-field-container-shape-start-start: 10px;
|
||||
--md-outlined-field-container-shape-start-end: 10px;
|
||||
--md-outlined-field-container-shape-end-end: 10px;
|
||||
--md-outlined-field-container-shape-end-start: 10px;
|
||||
--md-outlined-field-focus-outline-width: 1px;
|
||||
--md-outlined-field-with-leading-content-leading-space: 8px;
|
||||
--md-outlined-field-with-trailing-content-trailing-space: 8px;
|
||||
--md-outlined-field-content-space: 8px;
|
||||
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||
}
|
||||
.input {
|
||||
font-family: var(--ha-font-family-body);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-outlined-text-field": HaOutlinedTextField;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiClose, mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
|
||||
import { mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
@@ -30,8 +30,8 @@ import "./ha-combo-box-item";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
export const DEFAULT_SEARCH_KEYS: FuseWeightedKey[] = [
|
||||
{
|
||||
@@ -159,7 +159,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@query("lit-virtualizer") public virtualizerElement?: LitVirtualizer;
|
||||
|
||||
@query("ha-textfield") private _searchFieldElement?: HaTextField;
|
||||
@query("ha-input-search") private _searchFieldElement?: HaInputSearch;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
@@ -226,19 +226,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
"Search | Add custom value")
|
||||
: (this.localize?.("ui.common.search") ?? "Search"));
|
||||
|
||||
return html`<ha-textfield
|
||||
.label=${searchLabel}
|
||||
return html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.placeholder=${searchLabel}
|
||||
@blur=${this._resetSelectedItem}
|
||||
@input=${this._filterChanged}
|
||||
.iconTrailing=${this.clearable && !!this._search}
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${this._clearSearch}
|
||||
slot="trailingIcon"
|
||||
.label=${this.localize?.("ui.common.clear") || "Clear"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</ha-textfield>
|
||||
</ha-input-search>
|
||||
${this._renderSectionButtons()}
|
||||
${this.sections?.length
|
||||
? html`
|
||||
@@ -455,21 +449,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
fireEvent(this, "index-selected", { index, newTab });
|
||||
}
|
||||
|
||||
private _clearSearch = () => {
|
||||
if (this._searchFieldElement) {
|
||||
this._searchFieldElement.value = "";
|
||||
this._searchFieldElement.dispatchEvent(new Event("input"));
|
||||
}
|
||||
};
|
||||
|
||||
private _fuseIndex = memoizeOne(
|
||||
(states: PickerComboBoxItem[], searchKeys?: FuseWeightedKey[]) =>
|
||||
Fuse.createIndex(searchKeys || DEFAULT_SEARCH_KEYS, states)
|
||||
);
|
||||
|
||||
private _filterChanged = (ev: Event) => {
|
||||
const textfield = ev.target as HaTextField;
|
||||
const searchString = textfield.value.trim();
|
||||
private _filterChanged = (ev: InputEvent) => {
|
||||
const textfield = ev.target as HaInputSearch;
|
||||
const searchString = (textfield.value ?? "").trim();
|
||||
this._search = searchString;
|
||||
|
||||
if (this.sections?.length) {
|
||||
@@ -810,12 +797,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
--text-field-padding-end: 0;
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
ha-input-search {
|
||||
padding: 0 var(--ha-space-3);
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
|
||||
:host([mode="dialog"]) ha-textfield {
|
||||
:host([mode="dialog"]) ha-input-search {
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
@@ -929,7 +915,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
padding: var(--ha-space-1) var(--ha-space-4);
|
||||
}
|
||||
|
||||
:host([mode="dialog"]) ha-textfield {
|
||||
:host([mode="dialog"]) ha-input-search {
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,8 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
css`
|
||||
ha-combo-box-item[disabled] {
|
||||
background-color: var(--ha-color-form-background-disabled);
|
||||
--md-list-item-disabled-opacity: 0.5;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
ha-combo-box-item {
|
||||
@@ -141,13 +143,6 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
--md-focus-ring-duration: 0s;
|
||||
}
|
||||
|
||||
/* Add Similar focus style as the text field */
|
||||
ha-combo-box-item[disabled]:after {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
}
|
||||
ha-combo-box-item:after {
|
||||
display: block;
|
||||
content: "";
|
||||
@@ -158,10 +153,7 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
right: 0;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: var(
|
||||
--mdc-text-field-idle-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
background-color: var(--ha-color-border-neutral-loud);
|
||||
transform:
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
|
||||
@@ -17,8 +17,8 @@ import "./ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-spinner";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
import "./input/ha-input";
|
||||
import type { HaInput } from "./input/ha-input";
|
||||
|
||||
prepareZXingModule({
|
||||
overrides: {
|
||||
@@ -64,7 +64,7 @@ class HaQrScanner extends LitElement {
|
||||
|
||||
@query("#canvas-container", true) private _canvasContainer?: HTMLDivElement;
|
||||
|
||||
@query("ha-textfield") private _manualInput?: HaTextField;
|
||||
@query("ha-input") private _manualInput?: HaInput;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
@@ -153,13 +153,13 @@ class HaQrScanner extends LitElement {
|
||||
</ha-alert>
|
||||
<p>${this.hass.localize("ui.components.qr-scanner.manual_input")}</p>
|
||||
<div class="row">
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.qr-scanner.enter_qr_code"
|
||||
)}
|
||||
@keyup=${this._manualKeyup}
|
||||
@paste=${this._manualPaste}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
<ha-button @click=${this._manualSubmit}>
|
||||
${this.hass.localize("ui.common.submit")}
|
||||
</ha-button>
|
||||
@@ -242,7 +242,7 @@ class HaQrScanner extends LitElement {
|
||||
|
||||
private _manualKeyup(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter") {
|
||||
this._qrCodeScanned((ev.target as HaTextField).value);
|
||||
this._qrCodeScanned((ev.target as HaInput).value ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ class HaQrScanner extends LitElement {
|
||||
}
|
||||
|
||||
private _manualSubmit() {
|
||||
this._qrCodeScanned(this._manualInput!.value);
|
||||
this._qrCodeScanned(this._manualInput!.value ?? "");
|
||||
}
|
||||
|
||||
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
|
||||
@@ -382,7 +382,7 @@ class HaQrScanner extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
ha-textfield {
|
||||
ha-input {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
export const RETRO_THEME = {
|
||||
// Sharp corners
|
||||
"ha-border-radius-sm": "0",
|
||||
"ha-border-radius-md": "0",
|
||||
"ha-border-radius-lg": "0",
|
||||
"ha-border-radius-xl": "0",
|
||||
"ha-border-radius-2xl": "0",
|
||||
"ha-border-radius-3xl": "0",
|
||||
"ha-border-radius-4xl": "0",
|
||||
"ha-border-radius-5xl": "0",
|
||||
"ha-border-radius-6xl": "0",
|
||||
"ha-border-radius-pill": "0",
|
||||
"ha-border-radius-circle": "0",
|
||||
|
||||
// Fonts
|
||||
"ha-font-family-body":
|
||||
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
|
||||
"ha-font-family-heading":
|
||||
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
|
||||
"ha-font-family-code": "'Courier New', Courier, monospace",
|
||||
"ha-font-family-longform":
|
||||
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
|
||||
|
||||
// No transparency
|
||||
"ha-dialog-scrim-backdrop-filter": "none",
|
||||
|
||||
// Disable animations
|
||||
"ha-animation-duration-fast": "1ms",
|
||||
"ha-animation-duration-normal": "1ms",
|
||||
"ha-animation-duration-slow": "1ms",
|
||||
|
||||
modes: {
|
||||
light: {
|
||||
// Base colors
|
||||
"primary-color": "#000080",
|
||||
"dark-primary-color": "#00006B",
|
||||
"light-primary-color": "#4040C0",
|
||||
"accent-color": "#000080",
|
||||
"primary-text-color": "#000000",
|
||||
"secondary-text-color": "#404040",
|
||||
"text-primary-color": "#ffffff",
|
||||
"text-light-primary-color": "#000000",
|
||||
"disabled-text-color": "#808080",
|
||||
|
||||
// Backgrounds
|
||||
"primary-background-color": "#C0C0C0",
|
||||
"lovelace-background": "#008080",
|
||||
"secondary-background-color": "#C0C0C0",
|
||||
"card-background-color": "#C0C0C0",
|
||||
"clear-background-color": "#C0C0C0",
|
||||
|
||||
// RGB values
|
||||
"rgb-primary-color": "0, 0, 128",
|
||||
"rgb-accent-color": "0, 0, 128",
|
||||
"rgb-primary-text-color": "0, 0, 0",
|
||||
"rgb-secondary-text-color": "64, 64, 64",
|
||||
"rgb-text-primary-color": "255, 255, 255",
|
||||
"rgb-card-background-color": "192, 192, 192",
|
||||
|
||||
// UI chrome
|
||||
"divider-color": "#808080",
|
||||
"outline-color": "#808080",
|
||||
"outline-hover-color": "#404040",
|
||||
"shadow-color": "rgba(0, 0, 0, 0.5)",
|
||||
"scrollbar-thumb-color": "#808080",
|
||||
"disabled-color": "#808080",
|
||||
|
||||
// Cards - retro bevel effect
|
||||
"ha-card-border-width": "1px",
|
||||
"ha-card-border-color": "#808080",
|
||||
"ha-card-box-shadow": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
|
||||
"ha-card-border-radius": "0",
|
||||
|
||||
// Dialogs
|
||||
"ha-dialog-border-radius": "0",
|
||||
"ha-dialog-surface-background": "#C0C0C0",
|
||||
"dialog-box-shadow": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
|
||||
|
||||
// Box shadows - retro bevel
|
||||
"ha-box-shadow-s": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
|
||||
"ha-box-shadow-m": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
|
||||
"ha-box-shadow-l": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
|
||||
|
||||
// Header
|
||||
"app-header-background-color": "#000080",
|
||||
"app-header-text-color": "#ffffff",
|
||||
"app-header-border-bottom": "2px outset #C0C0C0",
|
||||
|
||||
// Sidebar
|
||||
"sidebar-background-color": "#C0C0C0",
|
||||
"sidebar-text-color": "#000000",
|
||||
"sidebar-selected-text-color": "#ffffff",
|
||||
"sidebar-selected-icon-color": "#000080",
|
||||
"sidebar-icon-color": "#000000",
|
||||
|
||||
// Input
|
||||
"input-fill-color": "#C0C0C0",
|
||||
"input-disabled-fill-color": "#C0C0C0",
|
||||
"input-ink-color": "#000000",
|
||||
"input-label-ink-color": "#000000",
|
||||
"input-disabled-ink-color": "#808080",
|
||||
"input-idle-line-color": "#808080",
|
||||
"input-hover-line-color": "#000000",
|
||||
"input-disabled-line-color": "#808080",
|
||||
"input-outlined-idle-border-color": "#808080",
|
||||
"input-outlined-hover-border-color": "#000000",
|
||||
"input-outlined-disabled-border-color": "#C0C0C0",
|
||||
"input-dropdown-icon-color": "#000000",
|
||||
|
||||
// Status colors
|
||||
"error-color": "#FF0000",
|
||||
"warning-color": "#FF8000",
|
||||
"success-color": "#008000",
|
||||
"info-color": "#000080",
|
||||
|
||||
// State
|
||||
"state-icon-color": "#000080",
|
||||
"state-active-color": "#000080",
|
||||
"state-inactive-color": "#808080",
|
||||
|
||||
// Data table
|
||||
"data-table-border-width": "0",
|
||||
|
||||
// Primary scale
|
||||
"ha-color-primary-05": "#00003A",
|
||||
"ha-color-primary-10": "#000050",
|
||||
"ha-color-primary-20": "#000066",
|
||||
"ha-color-primary-30": "#00007A",
|
||||
"ha-color-primary-40": "#000080",
|
||||
"ha-color-primary-50": "#0000AA",
|
||||
"ha-color-primary-60": "#4040C0",
|
||||
"ha-color-primary-70": "#6060D0",
|
||||
"ha-color-primary-80": "#8080E0",
|
||||
"ha-color-primary-90": "#C8C8D8",
|
||||
"ha-color-primary-95": "#D8D8E0",
|
||||
|
||||
// Neutral scale
|
||||
"ha-color-neutral-05": "#000000",
|
||||
"ha-color-neutral-10": "#2A2A2A",
|
||||
"ha-color-neutral-20": "#404040",
|
||||
"ha-color-neutral-30": "#606060",
|
||||
"ha-color-neutral-40": "#707070",
|
||||
"ha-color-neutral-50": "#808080",
|
||||
"ha-color-neutral-60": "#909090",
|
||||
"ha-color-neutral-70": "#A0A0A0",
|
||||
"ha-color-neutral-80": "#B0B0B0",
|
||||
"ha-color-neutral-90": "#C8C8C8",
|
||||
"ha-color-neutral-95": "#D0D0D0",
|
||||
|
||||
// Codemirror
|
||||
"codemirror-keyword": "#000080",
|
||||
"codemirror-operator": "#000000",
|
||||
"codemirror-variable": "#008080",
|
||||
"codemirror-variable-2": "#000080",
|
||||
"codemirror-variable-3": "#808000",
|
||||
"codemirror-builtin": "#800080",
|
||||
"codemirror-atom": "#008080",
|
||||
"codemirror-number": "#FF0000",
|
||||
"codemirror-def": "#000080",
|
||||
"codemirror-string": "#008000",
|
||||
"codemirror-string-2": "#808000",
|
||||
"codemirror-comment": "#808080",
|
||||
"codemirror-tag": "#800000",
|
||||
"codemirror-meta": "#000080",
|
||||
"codemirror-attribute": "#FF0000",
|
||||
"codemirror-property": "#000080",
|
||||
"codemirror-qualifier": "#808000",
|
||||
"codemirror-type": "#000080",
|
||||
},
|
||||
dark: {
|
||||
// Base colors
|
||||
"primary-color": "#4040C0",
|
||||
"dark-primary-color": "#000080",
|
||||
"light-primary-color": "#6060D0",
|
||||
"accent-color": "#4040C0",
|
||||
"primary-text-color": "#C0C0C0",
|
||||
"secondary-text-color": "#A0A0A0",
|
||||
"text-primary-color": "#ffffff",
|
||||
"text-light-primary-color": "#C0C0C0",
|
||||
"disabled-text-color": "#606060",
|
||||
|
||||
// Backgrounds
|
||||
"primary-background-color": "#2A2A2A",
|
||||
"lovelace-background": "#003030",
|
||||
"secondary-background-color": "#2A2A2A",
|
||||
"card-background-color": "#3A3A3A",
|
||||
"clear-background-color": "#2A2A2A",
|
||||
|
||||
// RGB values
|
||||
"rgb-primary-color": "64, 64, 192",
|
||||
"rgb-accent-color": "64, 64, 192",
|
||||
"rgb-primary-text-color": "192, 192, 192",
|
||||
"rgb-secondary-text-color": "160, 160, 160",
|
||||
"rgb-text-primary-color": "255, 255, 255",
|
||||
"rgb-card-background-color": "58, 58, 58",
|
||||
|
||||
// UI chrome
|
||||
"divider-color": "#606060",
|
||||
"outline-color": "#606060",
|
||||
"outline-hover-color": "#808080",
|
||||
"shadow-color": "rgba(0, 0, 0, 0.7)",
|
||||
"scrollbar-thumb-color": "#606060",
|
||||
"disabled-color": "#606060",
|
||||
|
||||
// Cards - retro bevel effect
|
||||
"ha-card-border-width": "1px",
|
||||
"ha-card-border-color": "#606060",
|
||||
"ha-card-box-shadow": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
|
||||
"ha-card-border-radius": "0",
|
||||
|
||||
// Dialogs
|
||||
"ha-dialog-border-radius": "0",
|
||||
"ha-dialog-surface-background": "#3A3A3A",
|
||||
"dialog-box-shadow": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
|
||||
|
||||
// Box shadows - retro bevel
|
||||
"ha-box-shadow-s": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
|
||||
"ha-box-shadow-m": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
|
||||
"ha-box-shadow-l": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
|
||||
|
||||
// Header
|
||||
"app-header-background-color": "#000060",
|
||||
"app-header-text-color": "#ffffff",
|
||||
"app-header-border-bottom": "2px outset #3A3A3A",
|
||||
|
||||
// Sidebar
|
||||
"sidebar-background-color": "#2A2A2A",
|
||||
"sidebar-text-color": "#C0C0C0",
|
||||
"sidebar-selected-text-color": "#ffffff",
|
||||
"sidebar-selected-icon-color": "#4040C0",
|
||||
"sidebar-icon-color": "#A0A0A0",
|
||||
|
||||
// Input
|
||||
"input-fill-color": "#3A3A3A",
|
||||
"input-disabled-fill-color": "#3A3A3A",
|
||||
"input-ink-color": "#C0C0C0",
|
||||
"input-label-ink-color": "#A0A0A0",
|
||||
"input-disabled-ink-color": "#606060",
|
||||
"input-idle-line-color": "#606060",
|
||||
"input-hover-line-color": "#808080",
|
||||
"input-disabled-line-color": "#404040",
|
||||
"input-outlined-idle-border-color": "#606060",
|
||||
"input-outlined-hover-border-color": "#808080",
|
||||
"input-outlined-disabled-border-color": "#404040",
|
||||
"input-dropdown-icon-color": "#A0A0A0",
|
||||
|
||||
// Status colors
|
||||
"error-color": "#FF4040",
|
||||
"warning-color": "#FFA040",
|
||||
"success-color": "#40C040",
|
||||
"info-color": "#4040C0",
|
||||
|
||||
// State
|
||||
"state-icon-color": "#4040C0",
|
||||
"state-active-color": "#4040C0",
|
||||
"state-inactive-color": "#606060",
|
||||
|
||||
// Data table
|
||||
"data-table-border-width": "0",
|
||||
|
||||
// Primary scale
|
||||
"ha-color-primary-05": "#00002A",
|
||||
"ha-color-primary-10": "#000040",
|
||||
"ha-color-primary-20": "#000060",
|
||||
"ha-color-primary-30": "#000080",
|
||||
"ha-color-primary-40": "#4040C0",
|
||||
"ha-color-primary-50": "#6060D0",
|
||||
"ha-color-primary-60": "#8080E0",
|
||||
"ha-color-primary-70": "#A0A0F0",
|
||||
"ha-color-primary-80": "#C0C0FF",
|
||||
"ha-color-primary-90": "#3A3A58",
|
||||
"ha-color-primary-95": "#303048",
|
||||
|
||||
// Neutral scale
|
||||
"ha-color-neutral-05": "#1A1A1A",
|
||||
"ha-color-neutral-10": "#2A2A2A",
|
||||
"ha-color-neutral-20": "#3A3A3A",
|
||||
"ha-color-neutral-30": "#4A4A4A",
|
||||
"ha-color-neutral-40": "#606060",
|
||||
"ha-color-neutral-50": "#707070",
|
||||
"ha-color-neutral-60": "#808080",
|
||||
"ha-color-neutral-70": "#909090",
|
||||
"ha-color-neutral-80": "#A0A0A0",
|
||||
"ha-color-neutral-90": "#C0C0C0",
|
||||
"ha-color-neutral-95": "#D0D0D0",
|
||||
|
||||
// Codemirror
|
||||
"codemirror-keyword": "#8080E0",
|
||||
"codemirror-operator": "#C0C0C0",
|
||||
"codemirror-variable": "#40C0C0",
|
||||
"codemirror-variable-2": "#8080E0",
|
||||
"codemirror-variable-3": "#C0C040",
|
||||
"codemirror-builtin": "#C040C0",
|
||||
"codemirror-atom": "#40C0C0",
|
||||
"codemirror-number": "#FF6060",
|
||||
"codemirror-def": "#8080E0",
|
||||
"codemirror-string": "#40C040",
|
||||
"codemirror-string-2": "#C0C040",
|
||||
"codemirror-comment": "#808080",
|
||||
"codemirror-tag": "#C04040",
|
||||
"codemirror-meta": "#8080E0",
|
||||
"codemirror-attribute": "#FF6060",
|
||||
"codemirror-property": "#8080E0",
|
||||
"codemirror-qualifier": "#C0C040",
|
||||
"codemirror-type": "#8080E0",
|
||||
"map-filter":
|
||||
"invert(0.9) hue-rotate(170deg) brightness(1.5) contrast(1.2) saturate(0.3)",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,683 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
applyThemesOnElement,
|
||||
invalidateThemeCache,
|
||||
} from "../common/dom/apply_themes_on_element";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import { subscribeLabFeature } from "../data/labs";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { RETRO_THEME } from "./ha-retro-theme";
|
||||
|
||||
const TIP_COUNT = 25;
|
||||
|
||||
type CasitaExpression =
|
||||
| "hi"
|
||||
| "ok-nabu"
|
||||
| "heart"
|
||||
| "sleep"
|
||||
| "great-job"
|
||||
| "error";
|
||||
|
||||
const STORAGE_KEY = "retro-position";
|
||||
const DRAG_THRESHOLD = 5;
|
||||
const BUBBLE_TIMEOUT = 8000;
|
||||
const SLEEP_TIMEOUT = 30000;
|
||||
const BSOD_CLICK_COUNT = 5;
|
||||
const BSOD_CLICK_TIMEOUT = 3000;
|
||||
const BSOD_DISMISS_DELAY = 500;
|
||||
|
||||
@customElement("ha-retro")
|
||||
export class HaRetro extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _enabled = false;
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
subscribeLabFeature(
|
||||
this.hass!.connection,
|
||||
"frontend",
|
||||
"retro",
|
||||
(feature) => {
|
||||
this._enabled = feature.enabled;
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@state() private _casitaVisible = true;
|
||||
|
||||
@state() private _showBubble = false;
|
||||
|
||||
@state() private _bubbleText = "";
|
||||
|
||||
@state() private _expression: CasitaExpression = "hi";
|
||||
|
||||
@state() private _position: { x: number; y: number } | null = null;
|
||||
|
||||
@state() private _showBsod = false;
|
||||
|
||||
private _clickCount = 0;
|
||||
|
||||
private _clickTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _dragging = false;
|
||||
|
||||
private _dragStartX = 0;
|
||||
|
||||
private _dragStartY = 0;
|
||||
|
||||
private _dragOffsetX = 0;
|
||||
|
||||
private _dragOffsetY = 0;
|
||||
|
||||
private _dragMoved = false;
|
||||
|
||||
private _bubbleTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _sleepTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _boundPointerMove = this._onPointerMove.bind(this);
|
||||
|
||||
private _boundPointerUp = this._onPointerUp.bind(this);
|
||||
|
||||
private _themeApplied = false;
|
||||
|
||||
private _isApplyingTheme = false;
|
||||
|
||||
private _themeObserver?: MutationObserver;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._loadPosition();
|
||||
this._resetSleepTimer();
|
||||
this._applyRetroTheme();
|
||||
this._startThemeObserver();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._clearTimers();
|
||||
this._stopThemeObserver();
|
||||
this._revertTheme();
|
||||
document.removeEventListener("pointermove", this._boundPointerMove);
|
||||
document.removeEventListener("pointerup", this._boundPointerUp);
|
||||
document.removeEventListener("keydown", this._boundDismissBsod);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: Map<string, unknown>): void {
|
||||
if (changedProps.has("_enabled")) {
|
||||
if (this._enabled) {
|
||||
this.hass!.loadFragmentTranslation("retro");
|
||||
this._applyRetroTheme();
|
||||
this._startThemeObserver();
|
||||
} else {
|
||||
this._stopThemeObserver();
|
||||
this._revertTheme();
|
||||
}
|
||||
}
|
||||
if (changedProps.has("hass") && this._enabled) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
// Re-apply if darkMode changed
|
||||
if (oldHass && oldHass.themes.darkMode !== this.hass!.themes.darkMode) {
|
||||
this._themeApplied = false;
|
||||
this._applyRetroTheme();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _startThemeObserver(): void {
|
||||
if (this._themeObserver) return;
|
||||
this._themeObserver = new MutationObserver(() => {
|
||||
if (this._isApplyingTheme || !this._enabled || !this.hass) return;
|
||||
// Check if our theme was overwritten by the themes mixin
|
||||
const el = document.documentElement as HTMLElement & {
|
||||
__themes?: { cacheKey?: string };
|
||||
};
|
||||
if (!el.__themes?.cacheKey?.startsWith("Retro")) {
|
||||
this._themeApplied = false;
|
||||
this._applyRetroTheme();
|
||||
}
|
||||
});
|
||||
this._themeObserver.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["style"],
|
||||
});
|
||||
}
|
||||
|
||||
private _stopThemeObserver(): void {
|
||||
this._themeObserver?.disconnect();
|
||||
this._themeObserver = undefined;
|
||||
}
|
||||
|
||||
private _applyRetroTheme(): void {
|
||||
if (!this.hass || this._themeApplied) return;
|
||||
|
||||
this._isApplyingTheme = true;
|
||||
|
||||
const themes = {
|
||||
...this.hass.themes,
|
||||
themes: {
|
||||
...this.hass.themes.themes,
|
||||
Retro: RETRO_THEME,
|
||||
},
|
||||
};
|
||||
|
||||
invalidateThemeCache();
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
themes,
|
||||
"Retro",
|
||||
{ dark: this.hass.themes.darkMode },
|
||||
true
|
||||
);
|
||||
this._themeApplied = true;
|
||||
this._isApplyingTheme = false;
|
||||
}
|
||||
|
||||
private _revertTheme(): void {
|
||||
if (!this.hass || !this._themeApplied) return;
|
||||
|
||||
this._isApplyingTheme = true;
|
||||
|
||||
invalidateThemeCache();
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
this.hass.themes,
|
||||
this.hass.selectedTheme?.theme || "default",
|
||||
{
|
||||
dark: this.hass.themes.darkMode,
|
||||
primaryColor: this.hass.selectedTheme?.primaryColor,
|
||||
accentColor: this.hass.selectedTheme?.accentColor,
|
||||
},
|
||||
true
|
||||
);
|
||||
this._themeApplied = false;
|
||||
this._isApplyingTheme = false;
|
||||
}
|
||||
|
||||
private _loadPosition(): void {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const pos = JSON.parse(stored);
|
||||
if (typeof pos.x === "number" && typeof pos.y === "number") {
|
||||
this._position = this._clampPosition(pos.x, pos.y);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore invalid stored position
|
||||
}
|
||||
}
|
||||
|
||||
private _savePosition(): void {
|
||||
if (this._position) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(this._position));
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _clampPosition(x: number, y: number): { x: number; y: number } {
|
||||
const size = 80;
|
||||
return {
|
||||
x: Math.max(0, Math.min(window.innerWidth - size, x)),
|
||||
y: Math.max(0, Math.min(window.innerHeight - size, y)),
|
||||
};
|
||||
}
|
||||
|
||||
private _onPointerDown(ev: PointerEvent): void {
|
||||
if (ev.button !== 0 || this._showBsod) return;
|
||||
|
||||
this._dragging = true;
|
||||
this._dragMoved = false;
|
||||
this._dragStartX = ev.clientX;
|
||||
this._dragStartY = ev.clientY;
|
||||
|
||||
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
this._dragOffsetX = ev.clientX - rect.left;
|
||||
this._dragOffsetY = ev.clientY - rect.top;
|
||||
|
||||
(ev.currentTarget as HTMLElement).setPointerCapture(ev.pointerId);
|
||||
document.addEventListener("pointermove", this._boundPointerMove);
|
||||
document.addEventListener("pointerup", this._boundPointerUp);
|
||||
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private _onPointerMove(ev: PointerEvent): void {
|
||||
if (!this._dragging) return;
|
||||
|
||||
const dx = ev.clientX - this._dragStartX;
|
||||
const dy = ev.clientY - this._dragStartY;
|
||||
|
||||
if (!this._dragMoved && Math.hypot(dx, dy) < DRAG_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._dragMoved = true;
|
||||
|
||||
const x = ev.clientX - this._dragOffsetX;
|
||||
const y = ev.clientY - this._dragOffsetY;
|
||||
this._position = this._clampPosition(x, y);
|
||||
}
|
||||
|
||||
private _onPointerUp(ev: PointerEvent): void {
|
||||
document.removeEventListener("pointermove", this._boundPointerMove);
|
||||
document.removeEventListener("pointerup", this._boundPointerUp);
|
||||
|
||||
this._dragging = false;
|
||||
|
||||
if (this._dragMoved) {
|
||||
this._savePosition();
|
||||
} else {
|
||||
this._toggleBubble();
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private _stopPropagation(ev: Event): void {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _dismiss(ev: Event): void {
|
||||
ev.stopPropagation();
|
||||
this._casitaVisible = false;
|
||||
this._clearTimers();
|
||||
}
|
||||
|
||||
private _toggleBubble(): void {
|
||||
this._clickCount++;
|
||||
if (this._clickTimer) {
|
||||
clearTimeout(this._clickTimer);
|
||||
}
|
||||
this._clickTimer = setTimeout(() => {
|
||||
this._clickCount = 0;
|
||||
}, BSOD_CLICK_TIMEOUT);
|
||||
|
||||
if (this._clickCount >= BSOD_CLICK_COUNT) {
|
||||
this._clickCount = 0;
|
||||
this._triggerBsod();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._showBubble) {
|
||||
this._hideBubble();
|
||||
} else {
|
||||
this._showTip();
|
||||
}
|
||||
}
|
||||
|
||||
private _boundDismissBsod = this._dismissBsodOnKey.bind(this);
|
||||
|
||||
private _bsodReadyToDismiss = false;
|
||||
|
||||
private _triggerBsod(): void {
|
||||
this._hideBubble();
|
||||
this._showBsod = true;
|
||||
this._bsodReadyToDismiss = false;
|
||||
this._expression = "error";
|
||||
// Delay enabling dismiss so the rapid clicks that triggered the BSOD don't immediately close it
|
||||
setTimeout(() => {
|
||||
this._bsodReadyToDismiss = true;
|
||||
document.addEventListener("keydown", this._boundDismissBsod);
|
||||
}, BSOD_DISMISS_DELAY);
|
||||
}
|
||||
|
||||
private _dismissBsod(): void {
|
||||
if (!this._bsodReadyToDismiss) return;
|
||||
this._showBsod = false;
|
||||
this._expression = "hi";
|
||||
this._resetSleepTimer();
|
||||
document.removeEventListener("keydown", this._boundDismissBsod);
|
||||
}
|
||||
|
||||
private _dismissBsodOnKey(): void {
|
||||
this._dismissBsod();
|
||||
}
|
||||
|
||||
private _showTip(): void {
|
||||
const tipIndex = Math.floor(Math.random() * TIP_COUNT) + 1;
|
||||
this._bubbleText = this.hass!.localize(
|
||||
`ui.panel.retro.tip_${tipIndex}` as LocalizeKeys
|
||||
);
|
||||
this._showBubble = true;
|
||||
this._expression = "ok-nabu";
|
||||
this._resetSleepTimer();
|
||||
|
||||
if (this._bubbleTimer) {
|
||||
clearTimeout(this._bubbleTimer);
|
||||
}
|
||||
this._bubbleTimer = setTimeout(() => {
|
||||
this._hideBubble();
|
||||
}, BUBBLE_TIMEOUT);
|
||||
}
|
||||
|
||||
private _hideBubble(): void {
|
||||
this._showBubble = false;
|
||||
this._expression = "hi";
|
||||
this._resetSleepTimer();
|
||||
|
||||
if (this._bubbleTimer) {
|
||||
clearTimeout(this._bubbleTimer);
|
||||
this._bubbleTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _closeBubble(ev: Event): void {
|
||||
ev.stopPropagation();
|
||||
this._hideBubble();
|
||||
}
|
||||
|
||||
private _resetSleepTimer(): void {
|
||||
if (this._sleepTimer) {
|
||||
clearTimeout(this._sleepTimer);
|
||||
}
|
||||
this._sleepTimer = setTimeout(() => {
|
||||
if (!this._showBubble) {
|
||||
this._expression = "sleep";
|
||||
}
|
||||
}, SLEEP_TIMEOUT);
|
||||
}
|
||||
|
||||
private _clearTimers(): void {
|
||||
if (this._bubbleTimer) {
|
||||
clearTimeout(this._bubbleTimer);
|
||||
this._bubbleTimer = undefined;
|
||||
}
|
||||
if (this._sleepTimer) {
|
||||
clearTimeout(this._sleepTimer);
|
||||
this._sleepTimer = undefined;
|
||||
}
|
||||
if (this._clickTimer) {
|
||||
clearTimeout(this._clickTimer);
|
||||
this._clickTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._enabled || !this._casitaVisible) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const size = 80;
|
||||
const posStyle = this._position
|
||||
? `left: ${this._position.x}px; top: ${this._position.y}px;`
|
||||
: `right: 16px; bottom: 16px;`;
|
||||
|
||||
return html`
|
||||
${this._showBsod
|
||||
? html`
|
||||
<div class="bsod" @click=${this._dismissBsod}>
|
||||
<div class="bsod-content">
|
||||
<h1 class="bsod-title">
|
||||
${this.hass!.localize("ui.panel.retro.bsod_title")}
|
||||
</h1>
|
||||
<p>${this.hass!.localize("ui.panel.retro.bsod_error")}</p>
|
||||
<p>
|
||||
* ${this.hass!.localize("ui.panel.retro.bsod_line_1")}<br />
|
||||
* ${this.hass!.localize("ui.panel.retro.bsod_line_2")}
|
||||
</p>
|
||||
<p class="bsod-prompt">
|
||||
${this.hass!.localize("ui.panel.retro.bsod_continue")}
|
||||
<span class="bsod-cursor">_</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div
|
||||
class="casita-container ${this._dragging ? "dragging" : ""}"
|
||||
style="width: ${size}px; ${posStyle}"
|
||||
aria-hidden="true"
|
||||
@pointerdown=${this._onPointerDown}
|
||||
>
|
||||
${this._showBubble
|
||||
? html`
|
||||
<div class="speech-bubble">
|
||||
<span class="bubble-text">${this._bubbleText}</span>
|
||||
<button
|
||||
class="bubble-close"
|
||||
@pointerdown=${this._stopPropagation}
|
||||
@click=${this._closeBubble}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<button
|
||||
class="bubble-dismiss"
|
||||
@pointerdown=${this._stopPropagation}
|
||||
@click=${this._dismiss}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.retro.dismiss")}
|
||||
</button>
|
||||
<div class="bubble-arrow"></div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<img
|
||||
class="casita-image"
|
||||
src="/static/images/voice-assistant/${this._expression}.png"
|
||||
alt="Casita"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static readonly styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.casita-container {
|
||||
position: fixed;
|
||||
pointer-events: auto;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
.casita-container.dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.casita-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
animation: bob 3s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dragging .casita-image {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.speech-bubble {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
right: 0;
|
||||
background: #ffffe1;
|
||||
color: #000000;
|
||||
border-radius: 12px;
|
||||
border: 2px solid #000000;
|
||||
padding: 12px 28px 12px 12px;
|
||||
font-family: Tahoma, "MS Sans Serif", Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
width: 300px;
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
animation: bubble-in 200ms ease-out;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.bubble-close {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #000000;
|
||||
font-size: 14px;
|
||||
padding: 2px 6px;
|
||||
line-height: 1;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.bubble-close:hover {
|
||||
background: #e0e0c0;
|
||||
}
|
||||
|
||||
.bubble-dismiss {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #808080;
|
||||
font-family: Tahoma, "MS Sans Serif", Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.bubble-dismiss:hover {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.bubble-arrow {
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
right: 32px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-top: 8px solid #ffffe1;
|
||||
}
|
||||
|
||||
@keyframes bob {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bubble-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.bsod {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #0000aa;
|
||||
color: #ffffff;
|
||||
font-family: "Lucida Console", "Courier New", monospace;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
animation: bsod-in 100ms ease-out;
|
||||
}
|
||||
|
||||
.bsod-content {
|
||||
max-width: 700px;
|
||||
padding: 32px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bsod-title {
|
||||
display: inline-block;
|
||||
background: #aaaaaa;
|
||||
color: #0000aa;
|
||||
padding: 2px 12px;
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
|
||||
.bsod-content p {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.bsod-prompt {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.bsod-cursor {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
@keyframes bsod-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.casita-image {
|
||||
animation: none;
|
||||
}
|
||||
.speech-bubble {
|
||||
animation: none;
|
||||
}
|
||||
.bsod {
|
||||
animation: none;
|
||||
}
|
||||
.bsod-cursor {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-retro": HaRetro;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,32 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
mdiArrowCollapseVertical,
|
||||
mdiArrowExpandVertical,
|
||||
mdiGreaterThan,
|
||||
mdiLessThan,
|
||||
} from "@mdi/js";
|
||||
import { mdiChartBellCurveCumulative } from "@mdi/js";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { NumericThresholdSelector } from "../../data/selector";
|
||||
import type {
|
||||
NumericThresholdSelector,
|
||||
ThresholdMode,
|
||||
} from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button-toggle-group";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-select";
|
||||
import "./ha-selector";
|
||||
|
||||
type ThresholdType = "above" | "below" | "between" | "outside";
|
||||
const iconThresholdAbove =
|
||||
"M 2 2 L 2 22 L 22 22 L 22 20 L 4 20 L 4 2 L 2 2 z M 17.976562 6 C 17.965863 6.00017 17.951347 6.0014331 17.935547 6.0019531 C 17.903847 6.0030031 17.862047 6.0043225 17.810547 6.0078125 C 17.707247 6.0148425 17.564772 6.0273144 17.388672 6.0527344 C 17.036572 6.1035743 16.54829 6.2035746 15.962891 6.3964844 C 14.788292 6.783584 13.232027 7.5444846 11.611328 9.0332031 C 10.753918 9.820771 9.8854345 10.808987 9.0449219 12.042969 C 7.881634 12.257047 7 13.274809 7 14.5 C 7 15.880699 8.1192914 17 9.5 17 C 10.880699 17 12 15.880699 12 14.5 C 12 13.732663 11.653544 13.046487 11.109375 12.587891 C 11.732682 11.74814 12.359503 11.061942 12.964844 10.505859 C 14.359842 9.2245207 15.662945 8.6023047 16.589844 8.296875 C 17.054643 8.1437252 17.426428 8.0689231 17.673828 8.0332031 C 17.797428 8.0153531 17.891466 8.0076962 17.947266 8.0039062 C 17.974966 8.0020263 17.992753 8.0003 18.001953 8 L 17.998047 6 L 17.976562 6 z";
|
||||
|
||||
const iconThresholdBelow =
|
||||
"M 2 2 L 2 22 L 22 22 L 22 20 L 4 20 L 4 2 L 2 2 z M 9.5 7 C 8.1192914 7 7 8.1192914 7 9.5 C 7 10.880699 8.1192914 12 9.5 12 C 9.598408 12 9.6955741 11.993483 9.7910156 11.982422 C 10.39444 12.754246 11.005767 13.410563 11.611328 13.966797 C 13.232027 15.455495 14.788292 16.216416 15.962891 16.603516 C 16.54829 16.796415 17.036572 16.896366 17.388672 16.947266 C 17.564772 16.972666 17.707247 16.985188 17.810547 16.992188 C 17.862047 16.995687 17.903847 16.997047 17.935547 16.998047 C 17.951347 16.998547 17.965863 16.9998 17.976562 17 L 17.998047 17 L 18.001953 15 C 17.992753 14.9997 17.974966 14.999947 17.947266 14.998047 C 17.891466 14.994247 17.797428 14.984597 17.673828 14.966797 C 17.426428 14.931097 17.054643 14.856325 16.589844 14.703125 C 15.662945 14.397725 14.359842 13.775439 12.964844 12.494141 C 12.496227 12.063656 12.015935 11.551532 11.533203 10.955078 C 11.826929 10.545261 12 10.042666 12 9.5 C 12 8.1192914 10.880699 7 9.5 7 z";
|
||||
|
||||
const iconThresholdBetween =
|
||||
"M 2 2 L 2 22 L 22 22 L 22 20 L 4 20 L 4 2 L 2 2 z M 16.5 4 C 15.119301 4 14 5.1192914 14 6.5 C 14 6.8572837 14.075904 7.196497 14.210938 7.5039062 C 13.503071 8.3427071 12.800578 9.3300361 12.130859 10.501953 C 11.718781 11.223082 11.287475 11.849823 10.845703 12.394531 C 10.457136 12.145771 9.9956073 12 9.5 12 C 8.1192914 12 7 13.119301 7 14.5 C 7 15.880699 8.1192914 17 9.5 17 C 10.880699 17 12 15.880699 12 14.5 C 12 14.38201 11.990422 14.26598 11.974609 14.152344 C 12.636605 13.409426 13.276156 12.531884 13.869141 11.494141 C 14.462491 10.455789 15.073208 9.5905169 15.681641 8.8613281 C 15.938115 8.9501682 16.213303 9 16.5 9 C 17.880699 9 19 7.8807086 19 6.5 C 19 5.1192914 17.880699 4 16.5 4 z";
|
||||
|
||||
const iconThresholdOutside =
|
||||
"M 2 2 L 2 22 L 22 22 L 22 20 L 4 20 L 4 19.046875 C 4.226574 19.041905 4.4812768 19.028419 4.7597656 19 C 5.8832145 18.8854 7.4011147 18.537974 9.0019531 17.609375 L 8.8847656 17.408203 C 9.320466 17.777433 9.8841605 18 10.5 18 C 11.880699 18 13 16.880699 13 15.5 C 13 14.119301 11.880699 13 10.5 13 C 9.1192914 13 8 14.119301 8 15.5 C 8 15.654727 8.0141099 15.806171 8.0410156 15.953125 L 7.9980469 15.876953 C 6.6882482 16.636752 5.4555097 16.918066 4.5566406 17.009766 C 4.3512557 17.030705 4.166436 17.040275 4 17.044922 L 4 2 L 2 2 z M 21.976562 4 C 21.965863 4.00017 21.951347 4.0014331 21.935547 4.0019531 C 21.903847 4.0030031 21.862047 4.0043225 21.810547 4.0078125 C 21.707247 4.0148425 21.564772 4.0273144 21.388672 4.0527344 C 21.036572 4.1035743 20.54829 4.2035846 19.962891 4.3964844 C 19.34193 4.6011277 18.613343 4.9149715 17.826172 5.3808594 C 17.441793 5.1398775 16.987134 5 16.5 5 C 15.119301 5 14 6.1192914 14 7.5 C 14 8.8807086 15.119301 10 16.5 10 C 17.880699 10 19 8.8807086 19 7.5 C 19 7.3403872 18.983669 7.1845035 18.955078 7.0332031 C 19.569666 6.6795942 20.126994 6.4493921 20.589844 6.296875 C 21.054643 6.1437252 21.426428 6.0689231 21.673828 6.0332031 C 21.797428 6.0153531 21.891466 6.0076962 21.947266 6.0039062 C 21.974966 6.0020263 21.992753 6.0003 22.001953 6 L 21.998047 4 L 21.976562 4 z";
|
||||
|
||||
type ThresholdType = "above" | "below" | "between" | "outside" | "any";
|
||||
|
||||
interface ThresholdValueEntry {
|
||||
active_choice?: string;
|
||||
@@ -31,6 +42,12 @@ interface NumericThresholdValue {
|
||||
value_max?: ThresholdValueEntry;
|
||||
}
|
||||
|
||||
const DEFAULT_TYPE: Record<ThresholdMode, ThresholdType> = {
|
||||
crossed: "above",
|
||||
changed: "any",
|
||||
is: "above",
|
||||
};
|
||||
|
||||
@customElement("ha-selector-numeric_threshold")
|
||||
export class HaNumericThresholdSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -49,9 +66,26 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
|
||||
@state() private _type?: ThresholdType;
|
||||
|
||||
private _getMode(): ThresholdMode {
|
||||
return this.selector.numeric_threshold?.mode ?? "crossed";
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.has("value")) {
|
||||
this._type = this.value?.type || "above";
|
||||
if (changedProperties.has("value") || changedProperties.has("selector")) {
|
||||
const mode = this._getMode();
|
||||
this._type = this.value?.type || DEFAULT_TYPE[mode];
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
(changedProperties.has("value") || changedProperties.has("selector")) &&
|
||||
!this.value
|
||||
) {
|
||||
const mode = this._getMode();
|
||||
const type = DEFAULT_TYPE[mode];
|
||||
fireEvent(this, "value-changed", { value: { type } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,41 +117,13 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const type = this._type || "above";
|
||||
const mode = this._getMode();
|
||||
const type = this._type || DEFAULT_TYPE[mode];
|
||||
const showSingleValue = type === "above" || type === "below";
|
||||
const showRangeValues = type === "between" || type === "outside";
|
||||
const unitOptions = this._getUnitOptions();
|
||||
|
||||
const typeOptions = [
|
||||
{
|
||||
value: "above",
|
||||
label: this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.above"
|
||||
),
|
||||
iconPath: mdiGreaterThan,
|
||||
},
|
||||
{
|
||||
value: "below",
|
||||
label: this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.below"
|
||||
),
|
||||
iconPath: mdiLessThan,
|
||||
},
|
||||
{
|
||||
value: "between",
|
||||
label: this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.in_range"
|
||||
),
|
||||
iconPath: mdiArrowCollapseVertical,
|
||||
},
|
||||
{
|
||||
value: "outside",
|
||||
label: this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.outside_range"
|
||||
),
|
||||
iconPath: mdiArrowExpandVertical,
|
||||
},
|
||||
];
|
||||
const typeOptions = this._buildTypeOptions(this.hass.localize, mode);
|
||||
|
||||
const choiceToggleButtons = [
|
||||
{
|
||||
@@ -134,6 +140,16 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
},
|
||||
];
|
||||
|
||||
// Value-row label for single-value types (above/below).
|
||||
const singleValueLabel = this.hass.localize(
|
||||
`ui.components.selectors.numeric_threshold.${mode}.${type as "above" | "below"}`
|
||||
);
|
||||
|
||||
// Determine which type-select label to use per mode
|
||||
const typeSelectLabel = this.hass.localize(
|
||||
`ui.components.selectors.numeric_threshold.${mode}.type`
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
${this.label
|
||||
@@ -141,9 +157,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
: nothing}
|
||||
<div class="inputs">
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.type"
|
||||
)}
|
||||
.label=${typeSelectLabel}
|
||||
.value=${type}
|
||||
.options=${typeOptions}
|
||||
.disabled=${this.disabled}
|
||||
@@ -152,11 +166,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
|
||||
${showSingleValue
|
||||
? this._renderValueRow(
|
||||
this.hass.localize(
|
||||
type === "above"
|
||||
? "ui.components.selectors.numeric_threshold.above"
|
||||
: "ui.components.selectors.numeric_threshold.below"
|
||||
),
|
||||
singleValueLabel,
|
||||
this.value?.value,
|
||||
this._valueChanged,
|
||||
this._valueChoiceChanged,
|
||||
@@ -199,6 +209,40 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _buildTypeOptions = memoizeOne(
|
||||
(localize: HomeAssistant["localize"], mode: ThresholdMode) => {
|
||||
const baseOptions = (
|
||||
[
|
||||
{ value: "above", iconPath: iconThresholdAbove },
|
||||
{ value: "below", iconPath: iconThresholdBelow },
|
||||
{ value: "between", iconPath: iconThresholdBetween },
|
||||
{ value: "outside", iconPath: iconThresholdOutside },
|
||||
] as const
|
||||
).map(({ value, iconPath }) => ({
|
||||
value,
|
||||
iconPath,
|
||||
label: localize(
|
||||
`ui.components.selectors.numeric_threshold.${mode}.${value}`
|
||||
),
|
||||
}));
|
||||
|
||||
if (mode !== "changed") {
|
||||
return baseOptions;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
value: "any",
|
||||
iconPath: mdiChartBellCurveCumulative,
|
||||
label: localize(
|
||||
"ui.components.selectors.numeric_threshold.changed.any"
|
||||
),
|
||||
},
|
||||
...baseOptions,
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
private _renderUnitSelect(
|
||||
entry: ThresholdValueEntry | undefined,
|
||||
handler: (ev: CustomEvent) => void,
|
||||
@@ -243,7 +287,9 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
const numberSelector = {
|
||||
number: {
|
||||
...this.selector.numeric_threshold?.number,
|
||||
...(effectiveUnit ? { unit_of_measurement: effectiveUnit } : {}),
|
||||
...(!showUnit && effectiveUnit
|
||||
? { unit_of_measurement: effectiveUnit }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
const entitySelector = {
|
||||
@@ -255,9 +301,11 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
return html`
|
||||
<div class="value-row">
|
||||
<div class="value-header">
|
||||
<span class="value-label"
|
||||
>${rowLabel}${this.required ? "*" : ""}</span
|
||||
>
|
||||
${rowLabel
|
||||
? html`<span class="value-label"
|
||||
>${rowLabel}${this.required ? "*" : ""}</span
|
||||
>`
|
||||
: nothing}
|
||||
<ha-button-toggle-group
|
||||
size="small"
|
||||
.buttons=${choiceToggleButtons}
|
||||
@@ -302,6 +350,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
newValue.value_min = this.value?.value_min ?? this.value?.value;
|
||||
newValue.value_max = this.value?.value_max;
|
||||
}
|
||||
// "any" type has no value fields
|
||||
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
@@ -428,6 +477,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
|
||||
.inputs,
|
||||
.value-row {
|
||||
--ha-input-padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
@@ -447,7 +497,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
.value-inputs {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
align-items: flex-end;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.value-selector {
|
||||
|
||||
@@ -21,6 +21,8 @@ export class HaTimeInput extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ attribute: "auto-validate", type: Boolean }) autoValidate = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "enable-second" })
|
||||
public enableSecond = false;
|
||||
|
||||
@@ -71,6 +73,7 @@ export class HaTimeInput extends LitElement {
|
||||
.clearable=${this.clearable && this.value !== undefined}
|
||||
.helper=${this.helper}
|
||||
.placeholderLabels=${this.placeholderLabels}
|
||||
.autoValidate=${this.autoValidate}
|
||||
day-label="dd"
|
||||
hour-label="hh"
|
||||
min-label="mm"
|
||||
@@ -86,6 +89,7 @@ export class HaTimeInput extends LitElement {
|
||||
|
||||
const useAMPM = useAmPm(this.locale);
|
||||
let value: string | undefined;
|
||||
let updateHours = 0;
|
||||
|
||||
// An undefined eventValue means the time selector is being cleared,
|
||||
// the `value` variable will (intentionally) be left undefined.
|
||||
@@ -97,6 +101,8 @@ export class HaTimeInput extends LitElement {
|
||||
) {
|
||||
let hours = eventValue.hours || 0;
|
||||
if (eventValue && useAMPM) {
|
||||
updateHours =
|
||||
hours >= 12 && hours < 24 ? hours - 12 : hours === 0 ? 12 : 0;
|
||||
if (eventValue.amPm === "PM" && hours < 12) {
|
||||
hours += 12;
|
||||
}
|
||||
@@ -115,6 +121,17 @@ export class HaTimeInput extends LitElement {
|
||||
}`;
|
||||
}
|
||||
|
||||
if (updateHours) {
|
||||
// If the user entered a 24hr time in a 12hr input, we need to refresh the
|
||||
// input to ensure it resets back to the 12hr equivalent.
|
||||
this.updateComplete.then(() => {
|
||||
const input = this._input;
|
||||
if (input) {
|
||||
input.hours = updateHours;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (value === this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
+194
-85
@@ -1,7 +1,16 @@
|
||||
import "@home-assistant/webawesome/dist/components/popover/popover";
|
||||
import type WaPopover from "@home-assistant/webawesome/dist/components/popover/popover";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
query,
|
||||
queryAssignedElements,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { popoverSupported } from "../common/feature-detect/support-popover";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
|
||||
export type ToastCloseReason =
|
||||
| "dismiss"
|
||||
@@ -19,23 +28,103 @@ export class HaToast extends LitElement {
|
||||
|
||||
@property({ type: Number, attribute: "timeout-ms" }) public timeoutMs = 4000;
|
||||
|
||||
@query("wa-popover")
|
||||
private _popover?: WaPopover;
|
||||
@query(".toast")
|
||||
private _toast?: HTMLDivElement;
|
||||
|
||||
@queryAssignedElements({ slot: "action", flatten: true })
|
||||
private _actionElements?: Element[];
|
||||
|
||||
@queryAssignedElements({ slot: "dismiss", flatten: true })
|
||||
private _dismissElements?: Element[];
|
||||
|
||||
@state() private _active = false;
|
||||
|
||||
@state() private _visible = false;
|
||||
|
||||
private _dismissTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _closeReason: ToastCloseReason = "programmatic";
|
||||
|
||||
private _transitionId = 0;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
clearTimeout(this._dismissTimer);
|
||||
this._transitionId += 1;
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
public async show(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
await this._popover?.show();
|
||||
|
||||
clearTimeout(this._dismissTimer);
|
||||
|
||||
if (this._active && this._visible) {
|
||||
this._setDismissTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
const transitionId = ++this._transitionId;
|
||||
|
||||
this._active = true;
|
||||
await this.updateComplete;
|
||||
|
||||
if (transitionId !== this._transitionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._showToastPopover();
|
||||
await nextRender();
|
||||
|
||||
if (transitionId !== this._transitionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._visible = true;
|
||||
await this.updateComplete;
|
||||
await this._waitForTransitionEnd();
|
||||
|
||||
if (transitionId !== this._transitionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._setDismissTimer();
|
||||
}
|
||||
|
||||
public async hide(reason: ToastCloseReason = "programmatic"): Promise<void> {
|
||||
clearTimeout(this._dismissTimer);
|
||||
this._closeReason = reason;
|
||||
|
||||
if (!this._active) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transitionId = ++this._transitionId;
|
||||
const wasVisible = this._visible;
|
||||
|
||||
this._visible = false;
|
||||
await this.updateComplete;
|
||||
|
||||
if (wasVisible) {
|
||||
await this._waitForTransitionEnd();
|
||||
}
|
||||
|
||||
if (transitionId !== this._transitionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._hideToastPopover();
|
||||
this._active = false;
|
||||
await this.updateComplete;
|
||||
|
||||
fireEvent(this, "toast-closed", {
|
||||
reason: this._closeReason,
|
||||
});
|
||||
this._closeReason = "programmatic";
|
||||
}
|
||||
|
||||
public close(reason: ToastCloseReason = "programmatic"): void {
|
||||
this.hide(reason);
|
||||
}
|
||||
|
||||
private _setDismissTimer() {
|
||||
if (this.timeoutMs > 0) {
|
||||
this._dismissTimer = setTimeout(() => {
|
||||
this.hide("timeout");
|
||||
@@ -43,96 +132,113 @@ export class HaToast extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public async hide(reason: ToastCloseReason = "programmatic"): Promise<void> {
|
||||
clearTimeout(this._dismissTimer);
|
||||
this._closeReason = reason;
|
||||
await this._popover?.hide();
|
||||
private _isPopoverOpen(): boolean {
|
||||
if (!this._toast || !popoverSupported) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return this._toast.matches(":popover-open");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public close(reason: ToastCloseReason = "programmatic"): void {
|
||||
this.hide(reason);
|
||||
private _showToastPopover(): void {
|
||||
if (!this._toast || !popoverSupported || this._isPopoverOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._toast.showPopover?.();
|
||||
}
|
||||
|
||||
private _handleAfterHide() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<ToastClosedEventDetail>("toast-closed", {
|
||||
detail: { reason: this._closeReason },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
this._closeReason = "programmatic";
|
||||
private _hideToastPopover(): void {
|
||||
if (!this._toast || !popoverSupported || !this._isPopoverOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._toast.hidePopover?.();
|
||||
}
|
||||
|
||||
private async _waitForTransitionEnd(): Promise<void> {
|
||||
const toastEl = this._toast;
|
||||
if (!toastEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const animations = toastEl.getAnimations({ subtree: true });
|
||||
if (animations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.allSettled(animations.map((animation) => animation.finished));
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const hasAction =
|
||||
(this._actionElements?.length ?? 0) > 0 ||
|
||||
(this._dismissElements?.length ?? 0) > 0;
|
||||
|
||||
return html`
|
||||
<div id="toast-anchor" aria-hidden="true"></div>
|
||||
<wa-popover
|
||||
for="toast-anchor"
|
||||
placement="top"
|
||||
distance="16"
|
||||
skidding="0"
|
||||
without-arrow
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
<div
|
||||
class=${classMap({
|
||||
toast: true,
|
||||
active: this._active,
|
||||
visible: this._visible,
|
||||
})}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
popover=${ifDefined(popoverSupported ? "manual" : undefined)}
|
||||
>
|
||||
<div class="toast" role="status" aria-live="polite">
|
||||
<span class="message">${this.labelText}</span>
|
||||
<div class="actions">
|
||||
<slot name="action"></slot>
|
||||
<slot name="dismiss"></slot>
|
||||
</div>
|
||||
<span class="message">${this.labelText}</span>
|
||||
<div class=${classMap({ actions: true, "has-action": hasAction })}>
|
||||
<slot name="action"></slot>
|
||||
<slot name="dismiss"></slot>
|
||||
</div>
|
||||
</wa-popover>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
#toast-anchor {
|
||||
position: fixed;
|
||||
bottom: calc(8px + var(--safe-area-inset-bottom));
|
||||
inset-inline-start: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
wa-popover {
|
||||
--arrow-size: 0;
|
||||
--max-width: min(
|
||||
650px,
|
||||
calc(
|
||||
100vw -
|
||||
16px - var(--safe-area-inset-left) - var(--safe-area-inset-right)
|
||||
)
|
||||
);
|
||||
--show-duration: var(--ha-animation-duration-fast, 150ms);
|
||||
--hide-duration: var(--ha-animation-duration-fast, 150ms);
|
||||
}
|
||||
|
||||
wa-popover::part(body) {
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
box-sizing: border-box;
|
||||
min-width: min(
|
||||
350px,
|
||||
calc(
|
||||
100vw -
|
||||
16px - var(--safe-area-inset-left) - var(--safe-area-inset-right)
|
||||
)
|
||||
position: fixed;
|
||||
inset-block-start: auto;
|
||||
inset-inline-end: auto;
|
||||
inset-block-end: calc(
|
||||
var(--safe-area-inset-bottom, 0px) + var(--ha-space-4)
|
||||
);
|
||||
max-width: 650px;
|
||||
inset-inline-start: 50%;
|
||||
margin: 0;
|
||||
width: max-content;
|
||||
height: auto;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
min-width: min(350px, calc(var(--safe-width) - var(--ha-space-4)));
|
||||
max-width: min(650px, var(--safe-width));
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-2) var(--ha-space-3);
|
||||
color: var(--inverse-primary-text-color);
|
||||
background-color: var(--inverse-surface-color);
|
||||
padding: var(--ha-space-3) var(--ha-space-4);
|
||||
color: var(--ha-color-on-neutral-loud);
|
||||
background-color: var(--ha-color-neutral-10);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
box-shadow: var(--wa-shadow-l);
|
||||
opacity: 0;
|
||||
transform: translate(-50%, var(--ha-space-2));
|
||||
transition:
|
||||
opacity var(--ha-animation-duration-fast, 150ms) ease,
|
||||
transform var(--ha-animation-duration-fast, 150ms) ease;
|
||||
}
|
||||
|
||||
.toast:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toast.visible {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.message {
|
||||
@@ -144,23 +250,26 @@ export class HaToast extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
color: var(--ha-color-on-neutral-loud);
|
||||
}
|
||||
|
||||
.actions:not(.has-action) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.toast {
|
||||
min-width: calc(
|
||||
100vw - var(--safe-area-inset-left) - var(--safe-area-inset-right)
|
||||
);
|
||||
border-radius: 0;
|
||||
min-width: var(--safe-width);
|
||||
max-width: var(--safe-width);
|
||||
border-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementEventMap {
|
||||
"toast-closed": CustomEvent<ToastClosedEventDetail>;
|
||||
interface HASSDomEvents {
|
||||
"toast-closed": ToastClosedEventDetail;
|
||||
}
|
||||
|
||||
interface HTMLElementTagNameMap {
|
||||
|
||||
@@ -11,6 +11,27 @@ import "../ha-svg-icon";
|
||||
import "./ha-input";
|
||||
import type { HaInput, InputType } from "./ha-input";
|
||||
|
||||
/**
|
||||
* Home Assistant input with copy button
|
||||
*
|
||||
* @element ha-input-copy
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A read-only input component with a copy-to-clipboard button.
|
||||
* Supports optional value masking with a toggle to reveal the hidden value.
|
||||
*
|
||||
* @attr {string} value - The value to display and copy.
|
||||
* @attr {string} masked-value - An alternative masked display value (for example, "••••••").
|
||||
* @attr {string} label - Label for the copy button. Defaults to the localized "Copy" text.
|
||||
* @attr {boolean} readonly - Makes the inner input readonly.
|
||||
* @attr {boolean} disabled - Disables the inner input.
|
||||
* @attr {boolean} masked-toggle - Shows a toggle button to reveal/hide the masked value.
|
||||
* @attr {("text"|"password"|"email"|"number"|"tel"|"url"|"search"|"date"|"datetime-local"|"time"|"color")} type - Sets the input type.
|
||||
* @attr {string} placeholder - Placeholder text for the input.
|
||||
* @attr {string} validation-message - Custom validation message.
|
||||
* @attr {boolean} auto-validate - Validates the input on blur.
|
||||
*/
|
||||
@customElement("ha-input-copy")
|
||||
export class HaInputCopy extends LitElement {
|
||||
@property({ attribute: "value" }) public value!: string;
|
||||
|
||||
@@ -14,6 +14,23 @@ import "../ha-sortable";
|
||||
import "./ha-input";
|
||||
import type { HaInput, InputType } from "./ha-input";
|
||||
|
||||
/**
|
||||
* Home Assistant multi-value input component
|
||||
*
|
||||
* @element ha-input-multi
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A dynamic list of text inputs that allows adding, removing, and optionally reordering values.
|
||||
* Useful for managing arrays of strings such as URLs, tags, or other repeated text values.
|
||||
*
|
||||
* @attr {boolean} disabled - Disables all inputs and buttons.
|
||||
* @attr {boolean} sortable - Enables drag-and-drop reordering of items.
|
||||
* @attr {boolean} item-index - Appends a 1-based index number to each item's label.
|
||||
* @attr {boolean} update-on-blur - Fires value-changed on blur instead of on input.
|
||||
*
|
||||
* @fires value-changed - Fired when the list of values changes. `event.detail.value` contains the new string array.
|
||||
*/
|
||||
@customElement("ha-input-multi")
|
||||
class HaInputMulti extends LitElement {
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
@@ -22,19 +39,19 @@ class HaInputMulti extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public helper?: string;
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ attribute: false }) public inputType?: InputType;
|
||||
@property({ attribute: "input-type" }) public inputType?: InputType;
|
||||
|
||||
@property({ attribute: false }) public inputSuffix?: string;
|
||||
@property({ attribute: "input-suffix" }) public inputSuffix?: string;
|
||||
|
||||
@property({ attribute: false }) public inputPrefix?: string;
|
||||
@property({ attribute: "input-prefix" }) public inputPrefix?: string;
|
||||
|
||||
@property({ attribute: false }) public autocomplete?: string;
|
||||
@property() public autocomplete?: string;
|
||||
|
||||
@property({ attribute: false }) public addLabel?: string;
|
||||
@property({ attribute: "add-label" }) public addLabel?: string;
|
||||
|
||||
@property({ attribute: false }) public removeLabel?: string;
|
||||
@property({ attribute: "remove-label" }) public removeLabel?: string;
|
||||
|
||||
@property({ attribute: "item-index", type: Boolean })
|
||||
public itemIndex = false;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiMagnify } from "@mdi/js";
|
||||
import { html, type PropertyValues } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { localizeContext } from "../../data/context";
|
||||
import { HaInput } from "./ha-input";
|
||||
|
||||
/**
|
||||
* Home Assistant search input component
|
||||
*
|
||||
* @element ha-input-search
|
||||
* @extends {HaInput}
|
||||
*
|
||||
* @summary
|
||||
* A pre-configured search input that extends `ha-input` with a magnify icon, clear button,
|
||||
* and a localized "Search" placeholder. Autocomplete is disabled by default.
|
||||
*/
|
||||
@customElement("ha-input-search")
|
||||
export class HaInputSearch extends HaInput {
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.withClear = true;
|
||||
this.autocomplete = this.autocomplete || "off";
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (
|
||||
!this.label &&
|
||||
!this.placeholder &&
|
||||
(!this.hasUpdated || changedProps.has("localize"))
|
||||
) {
|
||||
this.placeholder = this.localize("ui.common.search");
|
||||
}
|
||||
}
|
||||
|
||||
protected renderStartDefault() {
|
||||
return html`<ha-svg-icon slot="start" .path=${mdiMagnify}></ha-svg-icon>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-input-search": HaInputSearch;
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,21 @@ import "@home-assistant/webawesome/dist/components/input/input";
|
||||
import type WaInput from "@home-assistant/webawesome/dist/components/input/input";
|
||||
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
|
||||
import { mdiClose, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { LitElement, type PropertyValues, css, html, nothing } from "lit";
|
||||
import {
|
||||
LitElement,
|
||||
type PropertyValues,
|
||||
type TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
|
||||
export type InputType =
|
||||
@@ -25,18 +33,59 @@ export type InputType =
|
||||
| "color"
|
||||
| "url";
|
||||
|
||||
/**
|
||||
* Home Assistant input component
|
||||
*
|
||||
* @element ha-input
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A text input component supporting Home Assistant theming and validation, based on webawesome input.
|
||||
* Supports multiple input types including text, number, password, email, search, and more.
|
||||
*
|
||||
* @slot start - Content placed before the input (usually for icons or prefixes).
|
||||
* @slot end - Content placed after the input (usually for icons or suffixes).
|
||||
* @slot label - Custom label content. Overrides the `label` property.
|
||||
* @slot hint - Custom hint content. Overrides the `hint` property.
|
||||
* @slot clear-icon - Custom clear icon. Defaults to a close icon button.
|
||||
* @slot show-password-icon - Custom show password icon. Defaults to an eye icon button.
|
||||
* @slot hide-password-icon - Custom hide password icon. Defaults to an eye-off icon button.
|
||||
*
|
||||
* @csspart wa-base - The underlying wa-input base wrapper.
|
||||
* @csspart wa-hint - The underlying wa-input hint container.
|
||||
* @csspart wa-input - The underlying wa-input input element.
|
||||
*
|
||||
* @cssprop --ha-input-padding-top - Padding above the input.
|
||||
* @cssprop --ha-input-padding-bottom - Padding below the input. Defaults to `var(--ha-space-2)`.
|
||||
* @cssprop --ha-input-text-align - Text alignment of the input. Defaults to `start`.
|
||||
* @cssprop --ha-input-required-marker - The marker shown after the label for required fields. Defaults to `"*"`.
|
||||
*
|
||||
* @attr {("material"|"outlined")} appearance - Sets the input appearance style. "material" is the default filled style, "outlined" uses a bordered style.
|
||||
* @attr {("date"|"datetime-local"|"email"|"number"|"password"|"search"|"tel"|"text"|"time"|"color"|"url")} type - Sets the input type.
|
||||
* @attr {string} label - The input's label text.
|
||||
* @attr {string} hint - The input's hint/helper text.
|
||||
* @attr {string} placeholder - Placeholder text shown when the input is empty.
|
||||
* @attr {boolean} with-clear - Adds a clear button when the input is not empty.
|
||||
* @attr {boolean} readonly - Makes the input readonly.
|
||||
* @attr {boolean} disabled - Disables the input and prevents user interaction.
|
||||
* @attr {boolean} required - Makes the input a required field.
|
||||
* @attr {boolean} password-toggle - Adds a button to toggle the password visibility.
|
||||
* @attr {boolean} without-spin-buttons - Hides the browser's built-in spin buttons for number inputs.
|
||||
* @attr {boolean} auto-validate - Validates the input on blur instead of on form submit.
|
||||
* @attr {boolean} invalid - Marks the input as invalid.
|
||||
* @attr {boolean} inset-label - Uses an inset label style where the label stays inside the input.
|
||||
* @attr {string} validation-message - Custom validation message shown when the input is invalid.
|
||||
*/
|
||||
@customElement("ha-input")
|
||||
export class HaInput extends LitElement {
|
||||
@property({ reflect: true }) appearance: "material" | "outlined" = "material";
|
||||
|
||||
@property({ reflect: true })
|
||||
public type: InputType = "text";
|
||||
|
||||
@property()
|
||||
public value?: string;
|
||||
|
||||
/** Draws a pill-style input with rounded edges. */
|
||||
@property({ type: Boolean })
|
||||
public pill = false;
|
||||
|
||||
/** The input's label. */
|
||||
@property()
|
||||
public label = "";
|
||||
@@ -319,12 +368,10 @@ export class HaInput extends LitElement {
|
||||
>${this._renderLabel(this.label, this.required)}</slot
|
||||
>`
|
||||
: nothing}
|
||||
<slot
|
||||
name="start"
|
||||
slot="start"
|
||||
@slotchange=${this._syncStartSlotWidth}
|
||||
></slot>
|
||||
<slot name="end" slot="end"></slot>
|
||||
<slot name="start" slot="start" @slotchange=${this._syncStartSlotWidth}>
|
||||
${this.renderStartDefault()}
|
||||
</slot>
|
||||
<slot name="end" slot="end"> ${this.renderEndDefault()} </slot>
|
||||
<slot name="clear-icon" slot="clear-icon">
|
||||
<ha-icon-button .path=${mdiClose}></ha-icon-button>
|
||||
</slot>
|
||||
@@ -357,6 +404,14 @@ export class HaInput extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderStartDefault(): TemplateResult | typeof nothing {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
protected renderEndDefault(): TemplateResult | typeof nothing {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private _handleInput() {
|
||||
this.value = this._input?.value ?? undefined;
|
||||
if (this._invalid && this._input?.checkValidity()) {
|
||||
@@ -430,6 +485,9 @@ export class HaInput extends LitElement {
|
||||
padding-bottom: var(--ha-input-padding-bottom, var(--ha-space-2));
|
||||
text-align: var(--ha-input-text-align, start);
|
||||
}
|
||||
:host([appearance="outlined"]) {
|
||||
padding-bottom: var(--ha-input-padding-bottom);
|
||||
}
|
||||
wa-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -455,7 +513,7 @@ export class HaInput extends LitElement {
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
:host(:focus-within) wa-input::part(label) {
|
||||
:host([appearance="material"]:focus-within) wa-input::part(label) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
@@ -465,7 +523,8 @@ export class HaInput extends LitElement {
|
||||
}
|
||||
|
||||
wa-input.label-raised::part(label),
|
||||
:host(:focus-within) wa-input::part(label) {
|
||||
:host(:focus-within) wa-input::part(label),
|
||||
:host([type="date"]) wa-input::part(label) {
|
||||
padding-top: var(--ha-space-3);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
@@ -483,7 +542,19 @@ export class HaInput extends LitElement {
|
||||
transition: background-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
|
||||
wa-input::part(base)::after {
|
||||
:host([appearance="outlined"]) wa-input.no-label::part(base) {
|
||||
height: 32px;
|
||||
padding: 0 var(--ha-space-2);
|
||||
}
|
||||
|
||||
:host([appearance="outlined"]) wa-input::part(base) {
|
||||
border: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
background-color: var(--card-background-color);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
transition: border-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
|
||||
:host([appearance="material"]) ::part(base)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@@ -496,13 +567,15 @@ export class HaInput extends LitElement {
|
||||
background-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
|
||||
:host(:focus-within) wa-input::part(base)::after {
|
||||
:host([appearance="material"]:focus-within) wa-input::part(base)::after {
|
||||
height: 2px;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
:host(:focus-within) wa-input.invalid::part(base)::after,
|
||||
wa-input.invalid:not([disabled])::part(base)::after {
|
||||
:host([appearance="material"]:focus-within)
|
||||
wa-input.invalid::part(base)::after,
|
||||
:host([appearance="material"])
|
||||
wa-input.invalid:not([disabled])::part(base)::after {
|
||||
background-color: var(--ha-color-border-danger-normal);
|
||||
}
|
||||
|
||||
@@ -510,6 +583,7 @@ export class HaInput extends LitElement {
|
||||
padding-top: var(--ha-space-3);
|
||||
padding-inline-start: var(--input-padding-inline-start, 0);
|
||||
}
|
||||
|
||||
wa-input.no-label::part(input) {
|
||||
padding-top: 0;
|
||||
}
|
||||
@@ -535,10 +609,21 @@ export class HaInput extends LitElement {
|
||||
background-color: var(--ha-color-form-background-hover);
|
||||
}
|
||||
|
||||
:host([appearance="outlined"]) wa-input::part(base):hover {
|
||||
border-color: var(--ha-color-border-neutral-normal);
|
||||
}
|
||||
:host([appearance="outlined"]:focus-within) wa-input::part(base) {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
wa-input:disabled::part(base) {
|
||||
background-color: var(--ha-color-form-background-disabled);
|
||||
}
|
||||
|
||||
wa-input:disabled::part(label) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
wa-input::part(hint) {
|
||||
height: var(--ha-space-5);
|
||||
margin-block-start: 0;
|
||||
@@ -560,6 +645,11 @@ export class HaInput extends LitElement {
|
||||
wa-input::part(end) {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
:host([appearance="outlined"]) wa-input.no-label {
|
||||
--ha-icon-button-size: 24px;
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { mdiClose, mdiMagnify } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-outlined-text-field";
|
||||
import type { HaOutlinedTextField } from "./ha-outlined-text-field";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("search-input-outlined")
|
||||
class SearchInputOutlined extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public filter?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public suffix = false;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
@property({ type: String })
|
||||
public placeholder?: string;
|
||||
|
||||
public focus() {
|
||||
this._input?.focus();
|
||||
}
|
||||
|
||||
@query("ha-outlined-text-field", true) private _input!: HaOutlinedTextField;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const placeholder =
|
||||
this.placeholder || this.hass.localize("ui.common.search");
|
||||
|
||||
return html`
|
||||
<ha-outlined-text-field
|
||||
.autofocus=${this.autofocus}
|
||||
.aria-label=${this.label || this.hass.localize("ui.common.search")}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.filter || ""}
|
||||
icon
|
||||
.iconTrailing=${this.filter || this.suffix}
|
||||
@input=${this._filterInputChanged}
|
||||
dense
|
||||
>
|
||||
<slot name="prefix" slot="leading-icon">
|
||||
<ha-svg-icon
|
||||
tabindex="-1"
|
||||
class="prefix"
|
||||
.path=${mdiMagnify}
|
||||
></ha-svg-icon>
|
||||
</slot>
|
||||
${this.filter
|
||||
? html`<ha-icon-button
|
||||
aria-label="Clear input"
|
||||
slot="trailing-icon"
|
||||
@click=${this._clearSearch}
|
||||
.path=${mdiClose}
|
||||
>
|
||||
</ha-icon-button>`
|
||||
: nothing}
|
||||
</ha-outlined-text-field>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _filterChanged(value: string) {
|
||||
fireEvent(this, "value-changed", { value: String(value) });
|
||||
}
|
||||
|
||||
private async _filterInputChanged(e) {
|
||||
this._filterChanged(e.target.value);
|
||||
}
|
||||
|
||||
private async _clearSearch() {
|
||||
this._filterChanged("");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
/* For iOS */
|
||||
z-index: 0;
|
||||
}
|
||||
ha-outlined-text-field {
|
||||
display: block;
|
||||
width: 100%;
|
||||
--ha-outlined-field-container-color: var(--card-background-color);
|
||||
}
|
||||
ha-svg-icon,
|
||||
ha-icon-button {
|
||||
--ha-icon-button-size: 24px;
|
||||
height: var(--ha-icon-button-size);
|
||||
display: flex;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-svg-icon {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"search-input-outlined": SearchInputOutlined;
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import { mdiClose, mdiMagnify } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@customElement("search-input")
|
||||
class SearchInput extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public filter?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public suffix = false;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
public focus() {
|
||||
this._input?.focus();
|
||||
}
|
||||
|
||||
@query("ha-textfield", true) private _input!: HaTextField;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-textfield
|
||||
.autofocus=${this.autofocus}
|
||||
autocomplete="off"
|
||||
.label=${this.label || this.hass.localize("ui.common.search")}
|
||||
.value=${this.filter || ""}
|
||||
icon
|
||||
.iconTrailing=${this.filter || this.suffix}
|
||||
@input=${this._filterInputChanged}
|
||||
>
|
||||
<slot name="prefix" slot="leadingIcon">
|
||||
<ha-svg-icon
|
||||
tabindex="-1"
|
||||
class="prefix"
|
||||
.path=${mdiMagnify}
|
||||
></ha-svg-icon>
|
||||
</slot>
|
||||
<div class="trailing" slot="trailingIcon">
|
||||
${this.filter &&
|
||||
html`
|
||||
<ha-icon-button
|
||||
@click=${this._clearSearch}
|
||||
.label=${this.hass.localize("ui.common.clear")}
|
||||
.path=${mdiClose}
|
||||
class="clear-button"
|
||||
></ha-icon-button>
|
||||
`}
|
||||
<slot name="suffix"></slot>
|
||||
</div>
|
||||
</ha-textfield>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _filterChanged(value: string) {
|
||||
fireEvent(this, "value-changed", { value: String(value) });
|
||||
}
|
||||
|
||||
private async _filterInputChanged(e) {
|
||||
this._filterChanged(e.target.value);
|
||||
}
|
||||
|
||||
private async _clearSearch() {
|
||||
this._filterChanged("");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
}
|
||||
ha-svg-icon,
|
||||
ha-icon-button {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-svg-icon {
|
||||
outline: none;
|
||||
}
|
||||
.clear-button {
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: inherit;
|
||||
}
|
||||
.trailing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"search-input": SearchInput;
|
||||
}
|
||||
}
|
||||
@@ -535,7 +535,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
|
||||
const stateObject: HassEntity | undefined = this.hass.states[item];
|
||||
const entityName = stateObject
|
||||
? computeEntityName(stateObject, this.hass.entities)
|
||||
? computeEntityName(stateObject, this.hass.entities, this.hass.devices)
|
||||
: item;
|
||||
const { area, device } = stateObject
|
||||
? getEntityContext(
|
||||
|
||||
@@ -15,7 +15,7 @@ interface HassioHardwareAudioList {
|
||||
};
|
||||
}
|
||||
|
||||
interface HardwareDevice {
|
||||
export interface HardwareDevice {
|
||||
attributes: Record<string, string>;
|
||||
by_id: null | string;
|
||||
dev_path: string;
|
||||
|
||||
+12
-7
@@ -464,6 +464,15 @@ export const convertStatisticsToHistory = (
|
||||
return statisticsHistory;
|
||||
};
|
||||
|
||||
export const limitedHistoryFromStateObj = (
|
||||
state: HassEntity
|
||||
): EntityHistoryState[] => [
|
||||
{
|
||||
s: state.state,
|
||||
a: state.attributes,
|
||||
lu: new Date(state.last_updated).getTime() / 1000,
|
||||
},
|
||||
];
|
||||
export const computeHistory = (
|
||||
hass: HomeAssistant,
|
||||
stateHistory: HistoryStates,
|
||||
@@ -484,13 +493,9 @@ export const computeHistory = (
|
||||
if (entity in stateHistory) {
|
||||
localStateHistory[entity] = stateHistory[entity];
|
||||
} else if (hass.states[entity]) {
|
||||
localStateHistory[entity] = [
|
||||
{
|
||||
s: hass.states[entity].state,
|
||||
a: hass.states[entity].attributes,
|
||||
lu: new Date(hass.states[entity].last_updated).getTime() / 1000,
|
||||
},
|
||||
];
|
||||
localStateHistory[entity] = limitedHistoryFromStateObj(
|
||||
hass.states[entity]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
mdiHomeAutomation,
|
||||
mdiImage,
|
||||
mdiImageFilterFrames,
|
||||
mdiLedOn,
|
||||
mdiLightbulb,
|
||||
mdiMapMarkerRadius,
|
||||
mdiMicrophoneMessage,
|
||||
@@ -89,6 +90,7 @@ export const FALLBACK_DOMAIN_ICONS = {
|
||||
homekit: mdiHomeAutomation,
|
||||
image_processing: mdiImageFilterFrames,
|
||||
image: mdiImage,
|
||||
infrared: mdiLedOn,
|
||||
input_boolean: mdiToggleSwitch,
|
||||
input_button: mdiButtonPointer,
|
||||
input_datetime: mdiCalendarClock,
|
||||
|
||||
@@ -12,6 +12,7 @@ export const SCENE_IGNORED_DOMAINS = [
|
||||
"device_tracker",
|
||||
"event",
|
||||
"image_processing",
|
||||
"infrared",
|
||||
"input_button",
|
||||
"persistent_notification",
|
||||
"person",
|
||||
|
||||
@@ -22,6 +22,8 @@ import type {
|
||||
} from "./entity/entity_registry";
|
||||
import type { EntitySources } from "./entity/entity_sources";
|
||||
|
||||
export type ThresholdMode = "crossed" | "changed" | "is";
|
||||
|
||||
export type Selector =
|
||||
| ActionSelector
|
||||
| AddonSelector
|
||||
@@ -366,6 +368,7 @@ export interface NumberSelector {
|
||||
|
||||
export interface NumericThresholdSelector {
|
||||
numeric_threshold: {
|
||||
mode?: ThresholdMode;
|
||||
unit_of_measurement?: readonly string[];
|
||||
number?: NumberSelector["number"];
|
||||
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
|
||||
export interface ThemeVars {
|
||||
// Incomplete
|
||||
"primary-color": string;
|
||||
"text-primary-color": string;
|
||||
"accent-color": string;
|
||||
[key: string]: string;
|
||||
}
|
||||
export type ThemeVars = Record<string, string>;
|
||||
|
||||
export type Theme = ThemeVars & {
|
||||
modes?: {
|
||||
|
||||
@@ -16,6 +16,7 @@ import "../../../components/ha-humidifier-state";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-slider";
|
||||
import "../../../components/ha-time-input";
|
||||
import "../../../components/input/ha-input";
|
||||
import { isTiltOnly } from "../../../data/cover";
|
||||
import { isUnavailableState } from "../../../data/entity/entity";
|
||||
import type { ImageEntity } from "../../../data/image";
|
||||
@@ -72,7 +73,7 @@ class EntityPreviewRow extends LitElement {
|
||||
min-width: 45px;
|
||||
text-align: end;
|
||||
}
|
||||
ha-textfield {
|
||||
ha-input {
|
||||
text-align: end;
|
||||
direction: ltr !important;
|
||||
}
|
||||
@@ -273,18 +274,23 @@ class EntityPreviewRow extends LitElement {
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
: html` <div class="numberflex numberstate">
|
||||
<ha-textfield
|
||||
autoValidate
|
||||
: html`<div class="numberflex numberstate">
|
||||
<ha-input
|
||||
auto-validate
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
pattern="[0-9]+([\\.][0-9]+)?"
|
||||
.step=${Number(stateObj.attributes.step)}
|
||||
.min=${Number(stateObj.attributes.min)}
|
||||
.max=${Number(stateObj.attributes.max)}
|
||||
.value=${stateObj.state}
|
||||
.suffix=${stateObj.attributes.unit_of_measurement}
|
||||
type="number"
|
||||
></ha-textfield>
|
||||
>
|
||||
${stateObj.attributes.unit_of_measurement
|
||||
? html`<span slot="end"
|
||||
>${stateObj.attributes.unit_of_measurement}</span
|
||||
>`
|
||||
: nothing}
|
||||
</ha-input>
|
||||
</div>`}
|
||||
`;
|
||||
}
|
||||
@@ -323,7 +329,7 @@ class EntityPreviewRow extends LitElement {
|
||||
|
||||
if (domain === "text") {
|
||||
return html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
.label=${computeStateName(stateObj)}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.value=${stateObj.state}
|
||||
@@ -333,7 +339,7 @@ class EntityPreviewRow extends LitElement {
|
||||
.pattern=${stateObj.attributes.pattern}
|
||||
.type=${stateObj.attributes.mode}
|
||||
placeholder=${this.hass!.localize("ui.card.text.emtpy_value")}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import "../../components/ha-area-picker";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/input/ha-input";
|
||||
import type { HaInput } from "../../components/input/ha-input";
|
||||
import { assistSatelliteSupportsSetupFlow } from "../../data/assist_satellite";
|
||||
import { getConfigEntries } from "../../data/config_entries";
|
||||
import type { DataEntryFlowStepCreateEntry } from "../../data/data_entry_flow";
|
||||
@@ -22,7 +24,7 @@ import {
|
||||
type EntityRegistryDisplayEntry,
|
||||
} from "../../data/entity/entity_registry";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import { showVoiceAssistantSetupDialog } from "../voice-assistant-setup/show-voice-assistant-setup-dialog";
|
||||
@@ -162,7 +164,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
.label=${localize(
|
||||
"ui.panel.config.integrations.config_flow.device_name"
|
||||
)}
|
||||
@@ -174,7 +176,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
computeDeviceName(device)}
|
||||
@change=${this._deviceNameChanged}
|
||||
.device=${device.id}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.device=${device.id}
|
||||
@@ -278,7 +280,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _areaPicked(ev: CustomEvent) {
|
||||
private async _areaPicked(ev: ValueChangedEvent<string>) {
|
||||
const picker = ev.currentTarget as any;
|
||||
const device = picker.device;
|
||||
const area = ev.detail.value;
|
||||
@@ -290,9 +292,9 @@ class StepFlowCreateEntry extends LitElement {
|
||||
this.requestUpdate("_deviceUpdate");
|
||||
}
|
||||
|
||||
private _deviceNameChanged(ev): void {
|
||||
const picker = ev.currentTarget as any;
|
||||
const device = picker.device;
|
||||
private _deviceNameChanged(ev: InputEvent): void {
|
||||
const picker = ev.currentTarget as HaInput;
|
||||
const device = (picker as any).device;
|
||||
const name = picker.value;
|
||||
|
||||
if (!(device in this._deviceUpdate)) {
|
||||
@@ -343,12 +345,11 @@ class StepFlowCreateEntry extends LitElement {
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-textfield,
|
||||
ha-area-picker {
|
||||
display: block;
|
||||
}
|
||||
ha-textfield {
|
||||
margin: 8px 0;
|
||||
ha-input {
|
||||
margin: var(--ha-space-2) 0;
|
||||
}
|
||||
.buttons > *:last-child {
|
||||
margin-left: auto;
|
||||
|
||||
@@ -5,10 +5,10 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-control-button";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-textfield";
|
||||
import "../../components/ha-dialog";
|
||||
import type { HaTextField } from "../../components/ha-textfield";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/input/ha-input";
|
||||
import type { HaInput } from "../../components/input/ha-input";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HassDialog } from "../make-dialog-manager";
|
||||
import type { EnterCodeDialogParams } from "./show-enter-code-dialog";
|
||||
@@ -39,7 +39,7 @@ export class DialogEnterCode
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@query("#code") private _input?: HaTextField;
|
||||
@query("#code") private _input?: HaInput;
|
||||
|
||||
@state() private _showClearButton = false;
|
||||
|
||||
@@ -97,7 +97,7 @@ export class DialogEnterCode
|
||||
}
|
||||
|
||||
private _inputValueChange(e) {
|
||||
const field = e.currentTarget as HaTextField;
|
||||
const field = e.currentTarget as HaInput;
|
||||
const val = field.value;
|
||||
this._showClearButton = !!val;
|
||||
}
|
||||
@@ -119,7 +119,7 @@ export class DialogEnterCode
|
||||
width="small"
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
class="input"
|
||||
?autofocus=${!this._narrow}
|
||||
id="code"
|
||||
@@ -129,7 +129,7 @@ export class DialogEnterCode
|
||||
validateOnInitialRender
|
||||
pattern=${ifDefined(this._dialogParams.codePattern)}
|
||||
inputmode="text"
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@@ -157,14 +157,15 @@ export class DialogEnterCode
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div class="container">
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
@input=${this._inputValueChange}
|
||||
id="code"
|
||||
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
||||
type="password"
|
||||
inputmode="numeric"
|
||||
?autofocus=${!this._narrow}
|
||||
></ha-textfield>
|
||||
password-toggle
|
||||
></ha-input>
|
||||
<div class="keypad">
|
||||
${BUTTONS.map((value) =>
|
||||
value === ""
|
||||
@@ -212,7 +213,7 @@ export class DialogEnterCode
|
||||
/* Place above other dialogs */
|
||||
--dialog-z-index: 104;
|
||||
}
|
||||
ha-textfield {
|
||||
ha-input {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-dialog-header";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../components/ha-textfield";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/input/ha-input";
|
||||
import type { HaInput } from "../../components/input/ha-input";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { DialogBoxParams } from "./show-dialog-box";
|
||||
|
||||
@@ -28,7 +28,7 @@ class DialogBox extends LitElement {
|
||||
|
||||
@state() private _validInput = true;
|
||||
|
||||
@query("ha-textfield") private _textField?: HaTextField;
|
||||
@query("ha-input") private _textField?: HaInput;
|
||||
|
||||
private _closePromise?: Promise<void>;
|
||||
|
||||
@@ -73,7 +73,6 @@ class DialogBox extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
type=${confirmPrompt ? "alert" : "standard"}
|
||||
?prevent-scrim-close=${confirmPrompt}
|
||||
@@ -104,19 +103,21 @@ class DialogBox extends LitElement {
|
||||
: nothing}
|
||||
${dialogTitle}
|
||||
</span>
|
||||
${this._params.subtitle
|
||||
? html`<span slot="subtitle">${this._params.subtitle}</span>`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
<div id="dialog-box-description">
|
||||
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
|
||||
${this._params.prompt
|
||||
? html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
autofocus
|
||||
value=${ifDefined(this._params.defaultValue)}
|
||||
.placeholder=${this._params.placeholder}
|
||||
.label=${this._params.inputLabel
|
||||
? this._params.inputLabel
|
||||
: ""}
|
||||
.suffix=${this._params.inputSuffix}
|
||||
.type=${this._params.inputType
|
||||
? this._params.inputType
|
||||
: "text"}
|
||||
@@ -124,9 +125,13 @@ class DialogBox extends LitElement {
|
||||
.max=${this._params.inputMax}
|
||||
.disabled=${this._loading}
|
||||
@input=${this._validateInput}
|
||||
></ha-textfield>
|
||||
>
|
||||
${this._params.inputSuffix
|
||||
? html`<span slot="end">${this._params.inputSuffix}</span>`
|
||||
: nothing}
|
||||
</ha-input>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
${confirmPrompt
|
||||
@@ -240,7 +245,7 @@ class DialogBox extends LitElement {
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-textfield {
|
||||
ha-input {
|
||||
width: 100%;
|
||||
}
|
||||
.title.alert {
|
||||
|
||||
@@ -5,6 +5,7 @@ interface BaseDialogBoxParams {
|
||||
confirmText?: string;
|
||||
text?: string | TemplateResult;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
warning?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-control-button";
|
||||
import type { HaSelectSelectEvent } from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-select";
|
||||
import "../../../../components/ha-textfield";
|
||||
import "../../../../components/ha-dialog";
|
||||
import type { HaSelectSelectEvent } from "../../../../components/ha-select";
|
||||
import "../../../../components/input/ha-input";
|
||||
import { SirenEntityFeature } from "../../../../data/siren";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -95,27 +95,31 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
: nothing}
|
||||
${supportsVolume
|
||||
? html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
type="number"
|
||||
.label=${this.hass.localize("ui.components.siren.volume")}
|
||||
.suffix=${"%"}
|
||||
.value=${this._volume ? this._volume * 100 : undefined}
|
||||
.value=${this._volume ? `${this._volume * 100}` : undefined}
|
||||
@change=${this._handleVolumeChange}
|
||||
.min=${0}
|
||||
.max=${100}
|
||||
.step=${1}
|
||||
></ha-textfield>
|
||||
>
|
||||
<span slot="end">%</span>
|
||||
</ha-input>
|
||||
`
|
||||
: nothing}
|
||||
${supportsDuration
|
||||
? html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
type="number"
|
||||
.label=${this.hass.localize("ui.components.siren.duration")}
|
||||
.value=${this._duration}
|
||||
suffix="s"
|
||||
.value=${this._duration !== undefined
|
||||
? this._duration.toString()
|
||||
: undefined}
|
||||
@change=${this._handleDurationChange}
|
||||
></ha-textfield>
|
||||
>
|
||||
<span slot="end">s</span>
|
||||
</ha-input>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/input/ha-input";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("more-info-configurator")
|
||||
@@ -33,15 +33,15 @@ export class MoreInfoConfigurator extends LitElement {
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this.stateObj.attributes.errors}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
: nothing}
|
||||
${this.stateObj.attributes.fields.map(
|
||||
(field) =>
|
||||
html`<ha-textfield
|
||||
html`<ha-input
|
||||
.label=${field.name}
|
||||
.name=${field.id}
|
||||
.type=${field.type}
|
||||
@change=${this._fieldChanged}
|
||||
></ha-textfield>`
|
||||
></ha-input>`
|
||||
)}
|
||||
${this.stateObj.attributes.submit_caption
|
||||
? html`<p class="submit">
|
||||
@@ -53,7 +53,7 @@ export class MoreInfoConfigurator extends LitElement {
|
||||
${this.stateObj.attributes.submit_caption}
|
||||
</ha-button>
|
||||
</p>`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
--ha-input-padding-bottom: 0;
|
||||
}
|
||||
ha-date-input + ha-time-input {
|
||||
margin-left: var(--ha-space-1);
|
||||
|
||||
@@ -536,9 +536,9 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
: undefined;
|
||||
|
||||
const entityName = stateObj
|
||||
? computeEntityName(stateObj, this.hass.entities)
|
||||
? computeEntityName(stateObj, this.hass.entities, this.hass.devices)
|
||||
: this._entry
|
||||
? computeEntityEntryName(this._entry)
|
||||
? computeEntityEntryName(this._entry, this.hass.devices)
|
||||
: entityId;
|
||||
|
||||
const deviceName = context?.device
|
||||
|
||||
@@ -107,7 +107,11 @@ class MoreInfoContent extends LitElement {
|
||||
if (!stateObj) {
|
||||
return null;
|
||||
}
|
||||
const entityName = computeEntityName(stateObj, hass.entities);
|
||||
const entityName = computeEntityName(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices
|
||||
);
|
||||
const { area } = getEntityContext(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
|
||||
@@ -4,13 +4,14 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import type { HaSelectSelectEvent } from "../../components/ha-select";
|
||||
import {
|
||||
computeDeviceName,
|
||||
computeDeviceNameDisplay,
|
||||
} from "../../common/entity/compute_device_name";
|
||||
import "../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../components/ha-select";
|
||||
import "../../components/ha-tts-voice-picker";
|
||||
import "../../components/input/ha-input";
|
||||
import type { AssistPipeline } from "../../data/assist_pipeline";
|
||||
import {
|
||||
listAssistPipelines,
|
||||
@@ -99,14 +100,14 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
: nothing}
|
||||
<div class="rows">
|
||||
<div class="row">
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.device_name"
|
||||
)}
|
||||
.placeholder=${computeDeviceNameDisplay(device, this.hass)}
|
||||
.value=${this._deviceName ?? computeDeviceName(device)}
|
||||
@change=${this._deviceNameChanged}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
</div>
|
||||
${this.assistConfiguration &&
|
||||
this.assistConfiguration.available_wake_words.length > 1
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
mdiUnfoldLessHorizontal,
|
||||
mdiUnfoldMoreHorizontal,
|
||||
} from "@mdi/js";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -29,14 +29,15 @@ import type {
|
||||
} from "../components/data-table/ha-data-table";
|
||||
import { showDataTableSettingsDialog } from "../components/data-table/show-dialog-data-table-settings";
|
||||
import "../components/ha-button";
|
||||
import "../components/ha-dialog-footer";
|
||||
import "../components/ha-dialog";
|
||||
import "../components/ha-dialog-footer";
|
||||
import "../components/ha-dropdown";
|
||||
import "../components/ha-icon-button";
|
||||
import "../components/ha-svg-icon";
|
||||
import type { HaDropdownSelectEvent } from "../components/ha-dropdown";
|
||||
import "../components/ha-dropdown-item";
|
||||
import "../components/search-input-outlined";
|
||||
import "../components/ha-icon-button";
|
||||
import "../components/ha-svg-icon";
|
||||
import "../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../components/input/ha-input-search";
|
||||
import { KeyboardShortcutMixin } from "../mixins/keyboard-shortcut-mixin";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
import "./hass-tabs-subpage";
|
||||
@@ -193,7 +194,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
@query("ha-data-table", true) private _dataTable!: HaDataTable;
|
||||
|
||||
@query("search-input-outlined") private _searchInput!: HTMLElement;
|
||||
@query("ha-input-search") private _searchInput!: HaInputSearch;
|
||||
|
||||
protected supportedShortcuts(): SupportedShortcuts {
|
||||
return {
|
||||
@@ -266,14 +267,13 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
</ha-assist-chip>`
|
||||
: nothing;
|
||||
|
||||
const searchBar = html`<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this.filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.searchLabel}
|
||||
const searchBar = html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this.filter}
|
||||
@input=${this._handleSearchChange}
|
||||
.placeholder=${this.searchLabel}
|
||||
>
|
||||
</search-input-outlined>`;
|
||||
</ha-input-search>`;
|
||||
|
||||
const sortByMenu = Object.values(this.columns).find((col) => col.sortable)
|
||||
? html`
|
||||
@@ -715,11 +715,12 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
this._dataTable.clearSelection();
|
||||
};
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
if (this.filter === ev.detail.value) {
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const target = ev.target as HaInputSearch;
|
||||
if (this.filter === target.value) {
|
||||
return;
|
||||
}
|
||||
this.filter = ev.detail.value;
|
||||
this.filter = target.value ?? "";
|
||||
fireEvent(this, "search-changed", { value: this.filter });
|
||||
}
|
||||
|
||||
@@ -795,7 +796,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
background: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
search-input-outlined {
|
||||
ha-input-search {
|
||||
flex: 1;
|
||||
}
|
||||
.search-toolbar {
|
||||
|
||||
@@ -52,6 +52,7 @@ export class HomeAssistantMain extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-snowflakes .hass=${this.hass} .narrow=${this.narrow}></ha-snowflakes>
|
||||
<ha-retro .hass=${this.hass} .narrow=${this.narrow}></ha-retro>
|
||||
<ha-drawer
|
||||
.type=${sidebarNarrow ? "modal" : ""}
|
||||
.open=${sidebarNarrow ? this._drawerOpen : false}
|
||||
@@ -79,6 +80,7 @@ export class HomeAssistantMain extends LitElement {
|
||||
protected firstUpdated() {
|
||||
import(/* webpackPreload: true */ "../components/ha-sidebar");
|
||||
import("../components/ha-snowflakes");
|
||||
import("../components/ha-retro");
|
||||
|
||||
if (this.hass.auth.external) {
|
||||
this._externalSidebar =
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import "../components/ha-button";
|
||||
import "../components/ha-icon-button";
|
||||
import "../components/ha-toast";
|
||||
import type { ToastClosedEventDetail } from "../components/ha-toast";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface ShowToastParams {
|
||||
@@ -39,7 +41,7 @@ class NotificationManager extends LitElement {
|
||||
await this._toast?.hide();
|
||||
}
|
||||
|
||||
if (!parameters || parameters.duration === 0) {
|
||||
if (parameters.duration === 0) {
|
||||
this._parameters = undefined;
|
||||
return;
|
||||
}
|
||||
@@ -57,7 +59,7 @@ class NotificationManager extends LitElement {
|
||||
this._toast?.show();
|
||||
}
|
||||
|
||||
private _toastClosed(_ev: HTMLElementEventMap["toast-closed"]) {
|
||||
private _toastClosed(_ev: HASSDomEvent<ToastClosedEventDetail>) {
|
||||
this._parameters = undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ import "../components/ha-list";
|
||||
import "../components/ha-list-item";
|
||||
import "../components/ha-radio";
|
||||
import "../components/ha-spinner";
|
||||
import "../components/ha-textfield";
|
||||
import type { HaTextField } from "../components/ha-textfield";
|
||||
import "../components/input/ha-input";
|
||||
import type { HaInput } from "../components/input/ha-input";
|
||||
import "../components/map/ha-locations-editor";
|
||||
import type {
|
||||
HaLocationsEditor,
|
||||
@@ -103,22 +103,20 @@ class OnboardingLocation extends LitElement {
|
||||
</p>
|
||||
|
||||
<div class="location-search">
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
label=${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.address_label"
|
||||
)}
|
||||
.disabled=${this._working}
|
||||
icon
|
||||
iconTrailing
|
||||
@keyup=${this._addressSearch}
|
||||
>
|
||||
<ha-svg-icon slot="leadingIcon" .path=${mdiMagnify}></ha-svg-icon>
|
||||
<ha-svg-icon slot="start" .path=${mdiMagnify}></ha-svg-icon>
|
||||
${this._working
|
||||
? html` <ha-spinner slot="trailingIcon" size="small"></ha-spinner> `
|
||||
? html`<ha-spinner slot="end" size="small"></ha-spinner>`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
@click=${this._handleButtonClick}
|
||||
slot="trailingIcon"
|
||||
slot="end"
|
||||
.disabled=${this._working}
|
||||
.label=${this.onboardingLocalize(
|
||||
this._search
|
||||
@@ -128,7 +126,7 @@ class OnboardingLocation extends LitElement {
|
||||
.path=${this._search ? mdiMapSearchOutline : mdiCrosshairsGps}
|
||||
></ha-icon-button>
|
||||
`}
|
||||
</ha-textfield>
|
||||
</ha-input>
|
||||
${this._places !== undefined
|
||||
? html`
|
||||
<ha-list activatable>
|
||||
@@ -204,10 +202,7 @@ class OnboardingLocation extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
setTimeout(
|
||||
() => this.renderRoot.querySelector("ha-textfield")!.focus(),
|
||||
100
|
||||
);
|
||||
setTimeout(() => this.renderRoot.querySelector("ha-input")!.focus(), 100);
|
||||
this.addEventListener("keyup", (ev) => {
|
||||
if (ev.key === "Enter") {
|
||||
this._save(ev);
|
||||
@@ -299,11 +294,11 @@ class OnboardingLocation extends LitElement {
|
||||
|
||||
private async _addressSearch(ev: KeyboardEvent) {
|
||||
ev.stopPropagation();
|
||||
this._search = (ev.currentTarget as HaTextField).value.length > 0;
|
||||
this._search = ((ev.currentTarget as HaInput).value ?? "").length > 0;
|
||||
if (ev.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
this._searchAddress((ev.currentTarget as HaTextField).value);
|
||||
this._searchAddress((ev.currentTarget as HaInput).value ?? "");
|
||||
}
|
||||
|
||||
private async _searchAddress(address: string) {
|
||||
@@ -477,28 +472,6 @@ class OnboardingLocation extends LitElement {
|
||||
margin-top: 32px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
ha-textfield > ha-icon-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
inset-inline-end: 10px;
|
||||
inset-inline-start: initial;
|
||||
--ha-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 10px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-textfield > ha-spinner {
|
||||
position: relative;
|
||||
left: 12px;
|
||||
inset-inline-start: 12px;
|
||||
inset-inline-end: initial;
|
||||
}
|
||||
ha-locations-editor {
|
||||
display: block;
|
||||
height: 300px;
|
||||
@@ -519,7 +492,7 @@ class OnboardingLocation extends LitElement {
|
||||
height: 72px;
|
||||
}
|
||||
.attribution {
|
||||
/* textfield helper style */
|
||||
/* input helper style */
|
||||
margin: 0;
|
||||
padding: 4px 16px 12px 16px;
|
||||
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||
|
||||
@@ -271,6 +271,7 @@ class DialogCalendarEventDetail extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
max-width: 300px;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-line;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -10,6 +10,11 @@ import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
formatDate,
|
||||
formatTime,
|
||||
parseDate,
|
||||
} from "../../common/datetime/calc_date";
|
||||
import { resolveTimeZone } from "../../common/datetime/resolve-time-zone";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
@@ -19,13 +24,13 @@ import "../../components/entity/ha-entity-picker";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-date-input";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-formfield";
|
||||
import "../../components/ha-switch";
|
||||
import "../../components/ha-textarea";
|
||||
import "../../components/ha-textfield";
|
||||
import "../../components/ha-time-input";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/input/ha-input";
|
||||
import type { CalendarEventMutableParams } from "../../data/calendar";
|
||||
import {
|
||||
CalendarEntityFeature,
|
||||
@@ -40,11 +45,6 @@ import "../lovelace/components/hui-generic-entity-row";
|
||||
import "./ha-recurrence-rule-editor";
|
||||
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
|
||||
import type { CalendarEventEditDialogParams } from "./show-dialog-calendar-event-editor";
|
||||
import {
|
||||
formatDate,
|
||||
formatTime,
|
||||
parseDate,
|
||||
} from "../../common/datetime/calc_date";
|
||||
|
||||
const CALENDAR_DOMAINS = ["calendar"];
|
||||
|
||||
@@ -170,7 +170,7 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
>`
|
||||
: ""}
|
||||
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
class="summary"
|
||||
name="summary"
|
||||
.label=${this.hass.localize("ui.components.calendar.event.summary")}
|
||||
@@ -179,8 +179,8 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
@input=${this._handleSummaryChanged}
|
||||
.validationMessage=${this.hass.localize("ui.common.error_required")}
|
||||
autofocus
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
></ha-input>
|
||||
<ha-input
|
||||
class="location"
|
||||
name="location"
|
||||
.label=${this.hass.localize(
|
||||
@@ -188,7 +188,7 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
)}
|
||||
.value=${this._location}
|
||||
@change=${this._handleLocationChanged}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
<ha-textarea
|
||||
class="description"
|
||||
name="description"
|
||||
@@ -624,7 +624,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-textfield,
|
||||
ha-textarea {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import "../../components/chips/ha-filter-chip";
|
||||
import "../../components/ha-date-input";
|
||||
import "../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../components/ha-select";
|
||||
import "../../components/ha-textfield";
|
||||
import "../../components/input/ha-input";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type {
|
||||
MonthlyRepeatItem,
|
||||
@@ -231,7 +231,7 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
|
||||
renderInterval() {
|
||||
return html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
id="interval"
|
||||
label=${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.interval.label"
|
||||
@@ -239,12 +239,15 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
type="number"
|
||||
min="1"
|
||||
.value=${this._interval}
|
||||
.suffix=${this.hass.localize(
|
||||
`ui.components.calendar.event.repeat.interval.${this
|
||||
._freq!}` as LocalizeKeys
|
||||
)}
|
||||
@change=${this._onIntervalChange}
|
||||
></ha-textfield>
|
||||
>
|
||||
<span slot="end">
|
||||
${this.hass.localize(
|
||||
`ui.components.calendar.event.repeat.interval.${this
|
||||
._freq!}` as LocalizeKeys
|
||||
)}
|
||||
</span>
|
||||
</ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -267,7 +270,7 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
</ha-select>
|
||||
${this._end === "after"
|
||||
? html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
id="after"
|
||||
label=${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.end_after.label"
|
||||
@@ -275,11 +278,14 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
type="number"
|
||||
min="1"
|
||||
.value=${this._count!}
|
||||
suffix=${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.end_after.ocurrences"
|
||||
)}
|
||||
@change=${this._onCountChange}
|
||||
></ha-textfield>
|
||||
>
|
||||
<span slot="end">
|
||||
${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.end_after.ocurrences"
|
||||
)}
|
||||
</span>
|
||||
</ha-input>
|
||||
`
|
||||
: nothing}
|
||||
${this._end === "on"
|
||||
@@ -452,15 +458,16 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-textfield,
|
||||
ha-input,
|
||||
ha-select {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
--ha-input-padding-bottom: 0;
|
||||
}
|
||||
.weekdays {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-textfield:last-child,
|
||||
ha-input:last-child,
|
||||
ha-select:last-child,
|
||||
.weekdays:last-child {
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -65,6 +65,9 @@ const processAreasForClimate = (
|
||||
if (temperatureEntityId && hass.states[temperatureEntityId]) {
|
||||
areaCards.push({
|
||||
...computeTileCard(temperatureEntityId),
|
||||
name:
|
||||
hass.localize("component.sensor.entity_component.temperature.name") ||
|
||||
"Temperature",
|
||||
features: [{ type: "trend-graph" }],
|
||||
});
|
||||
}
|
||||
@@ -73,6 +76,9 @@ const processAreasForClimate = (
|
||||
if (humidityEntityId && hass.states[humidityEntityId]) {
|
||||
areaCards.push({
|
||||
...computeTileCard(humidityEntityId),
|
||||
name:
|
||||
hass.localize("component.sensor.entity_component.humidity.name") ||
|
||||
"Humidity",
|
||||
features: [{ type: "trend-graph" }],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1038,6 +1038,7 @@ class SupervisorAppInfo extends LitElement {
|
||||
}
|
||||
|
||||
private _updateComplete() {
|
||||
this._scheduleDataUpdate();
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
@@ -1059,11 +1060,16 @@ class SupervisorAppInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.action_error.install"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
confirmText: this.hass.localize("ui.common.ok"),
|
||||
dismissText: this.hass.localize(
|
||||
"ui.panel.config.apps.dashboard.action_error.view_supervisor_logs"
|
||||
),
|
||||
cancel: () => navigate("/config/logs?provider=supervisor"),
|
||||
});
|
||||
}
|
||||
button.progress = false;
|
||||
|
||||
@@ -6,14 +6,14 @@ import {
|
||||
type TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { extractSearchParam } from "../../../../../common/url/search-params";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/input/ha-input-search";
|
||||
import type { HassioAddonDetails } from "../../../../../data/hassio/addon";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
import "../../../logs/error-log-card";
|
||||
import "../../../../../components/search-input";
|
||||
import { extractSearchParam } from "../../../../../common/url/search-params";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
|
||||
@customElement("supervisor-app-log-tab")
|
||||
class SupervisorAppLogDashboard extends LitElement {
|
||||
@@ -29,12 +29,12 @@ class SupervisorAppLogDashboard extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<div class="search">
|
||||
<search-input
|
||||
@value-changed=${this._filterChanged}
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></search-input>
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
@input=${this._filterChanged}
|
||||
.value=${this._filter}
|
||||
.placeholder=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></ha-input-search>
|
||||
</div>
|
||||
<div class="content">
|
||||
<error-log-card
|
||||
@@ -48,8 +48,8 @@ class SupervisorAppLogDashboard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _filterChanged(ev) {
|
||||
this._filter = ev.detail.value;
|
||||
private async _filterChanged(ev: InputEvent) {
|
||||
this._filter = (ev.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -66,10 +66,10 @@ class SupervisorAppLogDashboard extends LitElement {
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
search-input {
|
||||
display: block;
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
ha-input-search {
|
||||
padding: var(--ha-space-3) var(--ha-space-2);
|
||||
background: var(--sidebar-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
@media all and (max-width: 870px) {
|
||||
:host {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { goBack, navigate } from "../../../common/navigate";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import type { HassioAddonDetails } from "../../../data/hassio/addon";
|
||||
import { fetchHassioAddonInfo } from "../../../data/hassio/addon";
|
||||
@@ -234,7 +234,7 @@ class HaConfigAppDashboard extends LitElement {
|
||||
|
||||
if (path === "uninstall") {
|
||||
// Navigate back to installed apps after uninstall
|
||||
window.history.back();
|
||||
goBack(this._fromStore ? "/config/apps/available" : "/config/apps");
|
||||
} else {
|
||||
// Reload app info
|
||||
await this._loadAddon();
|
||||
|
||||
@@ -9,7 +9,7 @@ import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/search-input";
|
||||
import "../../../components/input/ha-input-search";
|
||||
import type {
|
||||
HassioAddonRepository,
|
||||
HassioAddonsInfo,
|
||||
@@ -143,11 +143,11 @@ export class HaConfigAppsAvailable extends LitElement {
|
||||
? html`<hass-loading-screen no-toolbar></hass-loading-screen>`
|
||||
: html`
|
||||
<div class="search">
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._filterChanged}
|
||||
></search-input>
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._filterChanged}
|
||||
></ha-input-search>
|
||||
</div>
|
||||
|
||||
${repos}
|
||||
@@ -260,8 +260,8 @@ export class HaConfigAppsAvailable extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
private _filterChanged(e) {
|
||||
this._filter = e.detail.value;
|
||||
private _filterChanged(e: InputEvent) {
|
||||
this._filter = (e.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
@@ -278,10 +278,10 @@ export class HaConfigAppsAvailable extends LitElement {
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
search-input {
|
||||
display: block;
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
ha-input-search {
|
||||
padding: var(--ha-space-3) var(--ha-space-2);
|
||||
background: var(--sidebar-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user