mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-31 12:00:26 +00:00
Compare commits
277 Commits
20250806.0
...
automation
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e3e0d7fa49 | ||
![]() |
1fd7c84583 | ||
![]() |
0269540ee9 | ||
![]() |
6025d269b5 | ||
![]() |
b58c40aa77 | ||
![]() |
da40e0d395 | ||
![]() |
7d46071428 | ||
![]() |
b93caee122 | ||
![]() |
ea692ddde8 | ||
![]() |
6684dbd897 | ||
![]() |
edfd9d0991 | ||
![]() |
fad7663c38 | ||
![]() |
2662636975 | ||
![]() |
a844ff3921 | ||
![]() |
5a1d8d4fb1 | ||
![]() |
d78a598ffa | ||
![]() |
62b6ef40c8 | ||
![]() |
fb71ea00ee | ||
![]() |
98390b3843 | ||
![]() |
269628929c | ||
![]() |
21fcc84afd | ||
![]() |
b86c1db83d | ||
![]() |
a376670478 | ||
![]() |
72c62079aa | ||
![]() |
9baf875585 | ||
![]() |
175915218f | ||
![]() |
25f25243bd | ||
![]() |
cf8d36b1f3 | ||
![]() |
e3a9d754df | ||
![]() |
7b303a699b | ||
![]() |
ee45eb00f7 | ||
![]() |
24a6aa2669 | ||
![]() |
66d011cfb9 | ||
![]() |
35895735cc | ||
![]() |
e71df0b71a | ||
![]() |
2a9846c598 | ||
![]() |
b243d56bee | ||
![]() |
6a372a165e | ||
![]() |
a5dad9bc22 | ||
![]() |
954e0a5f63 | ||
![]() |
4dbd4eebaa | ||
![]() |
09b01df366 | ||
![]() |
a76539c732 | ||
![]() |
c7babe884c | ||
![]() |
ce83feec93 | ||
![]() |
150ee3fb12 | ||
![]() |
8fd3fcd323 | ||
![]() |
6e3b3a53e4 | ||
![]() |
22966485c7 | ||
![]() |
673ca8ba4b | ||
![]() |
c8be25dfc2 | ||
![]() |
edaaa00038 | ||
![]() |
2de605d97a | ||
![]() |
0b11302b1d | ||
![]() |
ddb224e145 | ||
![]() |
317149e51e | ||
![]() |
51840b88b3 | ||
![]() |
319a1ad8c6 | ||
![]() |
d75c84750d | ||
![]() |
492a73e345 | ||
![]() |
64bf101c95 | ||
![]() |
876ced25f1 | ||
![]() |
72e3b72854 | ||
![]() |
6ccd3d3b95 | ||
![]() |
5709cb6aa4 | ||
![]() |
1fe7282b0e | ||
![]() |
6d29063b35 | ||
![]() |
d3e0b94d27 | ||
![]() |
f4f1f98433 | ||
![]() |
69e3d8e13d | ||
![]() |
3ab6e389d3 | ||
![]() |
9cc85bc928 | ||
![]() |
4f4343d6c8 | ||
![]() |
7ab27d620a | ||
![]() |
fa4ee71803 | ||
![]() |
82026250c5 | ||
![]() |
bc7533bb42 | ||
![]() |
7110c0381f | ||
![]() |
87fa05accc | ||
![]() |
56ee3f82fb | ||
![]() |
11872b076b | ||
![]() |
620db4238d | ||
![]() |
ef78bec48d | ||
![]() |
4dcf2287ce | ||
![]() |
a9766ed66b | ||
![]() |
26a83feeb0 | ||
![]() |
8c5dd7cdba | ||
![]() |
0d025a2355 | ||
![]() |
8216778d0c | ||
![]() |
fe16b689a8 | ||
![]() |
2653f6c874 | ||
![]() |
8093f7f4cb | ||
![]() |
b26c914ff9 | ||
![]() |
b2fa97b6dc | ||
![]() |
8a8bba422a | ||
![]() |
76ca66b1b5 | ||
![]() |
280dbfc958 | ||
![]() |
0b10ad3e78 | ||
![]() |
c172f0c486 | ||
![]() |
1244ed73a2 | ||
![]() |
e2aef205cc | ||
![]() |
7434c9345a | ||
![]() |
287ff17107 | ||
![]() |
21309944e5 | ||
![]() |
c0e39ffd67 | ||
![]() |
3da2cb3123 | ||
![]() |
7957bd1f25 | ||
![]() |
04d0aa2f22 | ||
![]() |
4b901101da | ||
![]() |
4960284e2d | ||
![]() |
11d32300e9 | ||
![]() |
f131e93e63 | ||
![]() |
5a540dd889 | ||
![]() |
3a70310f78 | ||
![]() |
feed58c33e | ||
![]() |
e5585e13fe | ||
![]() |
edd49e1511 | ||
![]() |
057fad55e8 | ||
![]() |
d1a8165de2 | ||
![]() |
31c555445d | ||
![]() |
61bb5d33e5 | ||
![]() |
20a3bab5bc | ||
![]() |
e8d916acd7 | ||
![]() |
8b73d664b4 | ||
![]() |
10dcc08068 | ||
![]() |
fc5adc3753 | ||
![]() |
d1db8f456f | ||
![]() |
0dd07a395a | ||
![]() |
589771df5c | ||
![]() |
92812048dc | ||
![]() |
9fc14d6627 | ||
![]() |
3da6b85593 | ||
![]() |
d2a4f481be | ||
![]() |
e37f67c548 | ||
![]() |
e775a6770b | ||
![]() |
4ba5ef6c37 | ||
![]() |
d528ab06d9 | ||
![]() |
03a628cfe2 | ||
![]() |
973851b332 | ||
![]() |
4c5795c276 | ||
![]() |
41d016d96a | ||
![]() |
22e647cad4 | ||
![]() |
b63dd9dbbf | ||
![]() |
3b3b9e269d | ||
![]() |
d2c3b9ee83 | ||
![]() |
5b50a8692b | ||
![]() |
8a8bbee8e0 | ||
![]() |
28e28d1417 | ||
![]() |
ea77a0f3d6 | ||
![]() |
10e09b238a | ||
![]() |
f9cd2b66cb | ||
![]() |
b1c0fba8cf | ||
![]() |
fdae6257b3 | ||
![]() |
6a48aea128 | ||
![]() |
a90b173671 | ||
![]() |
d9971bfaa9 | ||
![]() |
369d56a809 | ||
![]() |
939a3cdf63 | ||
![]() |
208fd0662c | ||
![]() |
f133f246cb | ||
![]() |
b9b8997d68 | ||
![]() |
46c4a19a13 | ||
![]() |
8d63654211 | ||
![]() |
3bf25f125b | ||
![]() |
8c65876413 | ||
![]() |
2ab6d49553 | ||
![]() |
67b0cf0952 | ||
![]() |
5138276f8a | ||
![]() |
30e6777529 | ||
![]() |
1686ab4b9d | ||
![]() |
b7102c0d7d | ||
![]() |
d41d524850 | ||
![]() |
4f05f6305a | ||
![]() |
ba0b1239be | ||
![]() |
708b68f35d | ||
![]() |
3108e98b97 | ||
![]() |
ba7609cc2c | ||
![]() |
506fd7d480 | ||
![]() |
9767ebe1fb | ||
![]() |
539e89e7b5 | ||
![]() |
a7eef81272 | ||
![]() |
7986be103f | ||
![]() |
055e65c45e | ||
![]() |
fe762e9ae4 | ||
![]() |
5267c6fdfc | ||
![]() |
8eff913845 | ||
![]() |
1c845d0052 | ||
![]() |
60a1d25e1e | ||
![]() |
3439d1d663 | ||
![]() |
bf120d9cb2 | ||
![]() |
b5a024c879 | ||
![]() |
602d754e5e | ||
![]() |
b7c4f4029d | ||
![]() |
7fdb5d4862 | ||
![]() |
bc52ab410c | ||
![]() |
3b0220fa92 | ||
![]() |
64e00e559f | ||
![]() |
b407bd4c4f | ||
![]() |
d466abf9c4 | ||
![]() |
4d98230145 | ||
![]() |
8a5bca0eb0 | ||
![]() |
1638da858c | ||
![]() |
bfb11102cc | ||
![]() |
a3d3539e82 | ||
![]() |
1fc6cff857 | ||
![]() |
c5d7eb5384 | ||
![]() |
759e6eba35 | ||
![]() |
153129e066 | ||
![]() |
7bf3c7273e | ||
![]() |
08765e6ce2 | ||
![]() |
a60c9f788d | ||
![]() |
d9c297c06a | ||
![]() |
3789bebb2b | ||
![]() |
bbecf5f368 | ||
![]() |
e580b30219 | ||
![]() |
ed8c8ad3e3 | ||
![]() |
4f61d5689b | ||
![]() |
60a18185d7 | ||
![]() |
e0246b8488 | ||
![]() |
1cd0fae84a | ||
![]() |
e8a1ebbff4 | ||
![]() |
c5010b8502 | ||
![]() |
a7db401b62 | ||
![]() |
49c7dad6eb | ||
![]() |
521c3d40b7 | ||
![]() |
709a1d2ef0 | ||
![]() |
3c5d7b97d1 | ||
![]() |
9165c8bc57 | ||
![]() |
0b3e4eab23 | ||
![]() |
39d14c943c | ||
![]() |
09469be93f | ||
![]() |
6e215870ef | ||
![]() |
d5985dcaaf | ||
![]() |
bbd9d8887d | ||
![]() |
9588987e30 | ||
![]() |
52c05a4426 | ||
![]() |
e8224df4e5 | ||
![]() |
83a6df1621 | ||
![]() |
c46ebc8d3e | ||
![]() |
fca530411f | ||
![]() |
c2c64b9923 | ||
![]() |
9968c27a8e | ||
![]() |
96796ac5da | ||
![]() |
37def6d3e4 | ||
![]() |
013d603ba0 | ||
![]() |
b76407d28d | ||
![]() |
4e969ccf09 | ||
![]() |
cdfd6431c3 | ||
![]() |
c363995718 | ||
![]() |
53497aa632 | ||
![]() |
8d89b0e57f | ||
![]() |
92cf8b5579 | ||
![]() |
6068c32176 | ||
![]() |
38893324af | ||
![]() |
a39ab3c174 | ||
![]() |
797d2be5bf | ||
![]() |
99a91e1019 | ||
![]() |
5de8d07ce0 | ||
![]() |
3a31a4a721 | ||
![]() |
05f4419a92 | ||
![]() |
5ea8feb86b | ||
![]() |
8fd70b3ae6 | ||
![]() |
343aa40bc8 | ||
![]() |
6022f9a77e | ||
![]() |
bd9de0680e | ||
![]() |
b8000d5bc1 | ||
![]() |
c6efa1127f | ||
![]() |
688a3d91d3 | ||
![]() |
68151a2a70 | ||
![]() |
c2ca556151 | ||
![]() |
df86b27af4 | ||
![]() |
eba1f401cc | ||
![]() |
19c2f9c9e8 | ||
![]() |
4250447d14 | ||
![]() |
4666197f28 | ||
![]() |
a5ca36c93f | ||
![]() |
a88950e16c |
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
|
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
|
8
.github/workflows/release.yaml
vendored
8
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.03.0
|
||||
uses: home-assistant/wheels@2025.07.0
|
||||
with:
|
||||
abi: cp313
|
||||
tag: musllinux_1_2
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.4.0
|
||||
with:
|
||||
|
2
.github/workflows/restrict-task-creation.yml
vendored
2
.github/workflows/restrict-task-creation.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
check-authorization:
|
||||
runs-on: ubuntu-latest
|
||||
# Only run if this is a Task issue type (from the issue form)
|
||||
if: github.event.issue.issue_type == 'Task'
|
||||
if: github.event.issue.type.name == 'Task'
|
||||
steps:
|
||||
- name: Check if user is authorized
|
||||
uses: actions/github-script@v7
|
||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.2.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.9.4.cjs
|
||||
|
8
CODEOWNERS
Normal file
8
CODEOWNERS
Normal file
@@ -0,0 +1,8 @@
|
||||
# People marked here will be automatically requested for a review
|
||||
# when the code that they own is touched.
|
||||
# https://github.com/blog/2392-introducing-code-owners
|
||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
# Part of the frontend that mobile developper should review
|
||||
src/external_app/ @bgoncal @TimoPtr
|
||||
test/external_app/ @bgoncal @TimoPtr
|
@@ -14,5 +14,5 @@
|
||||
"name": "Home Assistant Cast",
|
||||
"short_name": "HA Cast",
|
||||
"start_url": "/?homescreen=1",
|
||||
"theme_color": "#03A9F4"
|
||||
"theme_color": "#009ac7"
|
||||
}
|
||||
|
@@ -75,5 +75,5 @@
|
||||
"name": "Home Assistant Demo",
|
||||
"short_name": "HA Demo",
|
||||
"start_url": "/?homescreen=1",
|
||||
"theme_color": "#03A9F4"
|
||||
"theme_color": "#009ac7"
|
||||
}
|
||||
|
@@ -68,7 +68,7 @@
|
||||
}
|
||||
#ha-launch-screen .ha-launch-screen-spacer-top {
|
||||
flex: 1;
|
||||
margin-top: calc( 2 * max(var(--safe-area-inset-bottom), 48px) + 46px );
|
||||
margin-top: calc( 2 * max(var(--safe-area-inset-bottom, 0px), 48px) + 46px );
|
||||
padding-top: 48px;
|
||||
}
|
||||
#ha-launch-screen .ha-launch-screen-spacer-bottom {
|
||||
@@ -76,7 +76,7 @@
|
||||
padding-top: 48px;
|
||||
}
|
||||
.ohf-logo {
|
||||
margin: max(var(--safe-area-inset-bottom), 48px) 0;
|
||||
margin: max(var(--safe-area-inset-bottom, 0px), 48px) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
@@ -18,7 +18,6 @@ import { HaDeviceAction } from "../../../../src/panels/config/automation/action/
|
||||
import { HaEventAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-event";
|
||||
import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if";
|
||||
import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel";
|
||||
import { HaPlayMediaAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-play_media";
|
||||
import { HaRepeatAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-repeat";
|
||||
import { HaSequenceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-sequence";
|
||||
import { HaServiceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-service";
|
||||
@@ -32,7 +31,6 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [
|
||||
{ name: "Service", actions: [HaServiceAction.defaultConfig] },
|
||||
{ name: "Condition", actions: [HaConditionAction.defaultConfig] },
|
||||
{ name: "Delay", actions: [HaDelayAction.defaultConfig] },
|
||||
{ name: "Play media", actions: [HaPlayMediaAction.defaultConfig] },
|
||||
{ name: "Wait", actions: [HaWaitAction.defaultConfig] },
|
||||
{ name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] },
|
||||
{ name: "Repeat", actions: [HaRepeatAction.defaultConfig] },
|
||||
|
@@ -64,4 +64,4 @@ Check the [webawesome documentation](https://webawesome.com/docs/components/butt
|
||||
**CSS Custom Properties**
|
||||
|
||||
- `--ha-button-height` - Height of the button.
|
||||
- `--ha-button-border-radius` - Border radius of the button. Defaults to `var(--border-radius-pill)`.
|
||||
- `--ha-button-border-radius` - Border radius of the button. Defaults to `var(--ha-border-radius-pill)`.
|
||||
|
@@ -11,6 +11,7 @@ import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-cards";
|
||||
import { mockIcons } from "../../../../demo/src/stubs/icons";
|
||||
import { ClimateEntityFeature } from "../../../../src/data/climate";
|
||||
import { FanEntityFeature } from "../../../../src/data/fan";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("switch", "tv_outlet", "on", {
|
||||
@@ -100,6 +101,15 @@ const ENTITIES = [
|
||||
ClimateEntityFeature.FAN_MODE +
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
|
||||
}),
|
||||
getEntity("fan", "fan_demo", "on", {
|
||||
friendly_name: "Ceiling fan",
|
||||
device_class: "fan",
|
||||
direction: "reverse",
|
||||
supported_features:
|
||||
FanEntityFeature.DIRECTION +
|
||||
FanEntityFeature.SET_SPEED +
|
||||
FanEntityFeature.OSCILLATE,
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
@@ -261,6 +271,33 @@ const CONFIGS = [
|
||||
- type: target-temperature
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Fan direction feature",
|
||||
config: `
|
||||
- type: tile
|
||||
entity: fan.fan_demo
|
||||
features:
|
||||
- type: fan-direction
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Fan speed feature",
|
||||
config: `
|
||||
- type: tile
|
||||
entity: fan.fan_demo
|
||||
features:
|
||||
- type: fan-speed
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Fan oscillate feature",
|
||||
config: `
|
||||
- type: tile
|
||||
entity: fan.fan_demo
|
||||
features:
|
||||
- type: fan-oscillate
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-lovelace-tile-card")
|
||||
|
3
gallery/src/pages/more-info/fan.markdown
Executable file
3
gallery/src/pages/more-info/fan.markdown
Executable file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Fan
|
||||
---
|
50
gallery/src/pages/more-info/fan.ts
Executable file
50
gallery/src/pages/more-info/fan.ts
Executable file
@@ -0,0 +1,50 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/dialogs/more-info/more-info-content";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import type { MockHomeAssistant } from "../../../../src/fake_data/provide_hass";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-more-infos";
|
||||
import { FanEntityFeature } from "../../../../src/data/fan";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("fan", "fan", "on", {
|
||||
friendly_name: "Fan",
|
||||
device_class: "fan",
|
||||
supported_features:
|
||||
FanEntityFeature.OSCILLATE +
|
||||
FanEntityFeature.DIRECTION +
|
||||
FanEntityFeature.SET_SPEED,
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-more-info-fan")
|
||||
class DemoMoreInfoFan extends LitElement {
|
||||
@property({ attribute: false }) public hass!: MockHomeAssistant;
|
||||
|
||||
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-fan": DemoMoreInfoFan;
|
||||
}
|
||||
}
|
@@ -2,13 +2,13 @@ import { mdiDelete, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-button";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../src/components/ha-form/types";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
addHassioDockerRegistry,
|
||||
|
@@ -7,10 +7,14 @@ import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../../../src/common/string/compare";
|
||||
import "../../../../src/components/ha-alert";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-tooltip";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-md-list";
|
||||
import "../../../../src/components/ha-md-list-item";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/ha-tooltip";
|
||||
import type {
|
||||
HassioAddonInfo,
|
||||
HassioAddonRepository,
|
||||
@@ -24,10 +28,6 @@ import {
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import type { HassioRepositoryDialogParams } from "./show-dialog-repositories";
|
||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/ha-md-list";
|
||||
import "../../../../src/components/ha-md-list-item";
|
||||
|
||||
@customElement("dialog-hassio-repositories")
|
||||
class HassioRepositoriesDialog extends LitElement {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
"*.?(c|m){js,ts}": [
|
||||
"eslint --flag unstable_config_lookup_from_file --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
|
||||
"eslint --flag v10_config_lookup_from_file --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
|
||||
"prettier --cache --write",
|
||||
"lit-analyzer --quiet",
|
||||
],
|
||||
|
75
package.json
75
package.json
@@ -8,8 +8,8 @@
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "script/build_frontend",
|
||||
"lint:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
|
||||
"format:eslint": "eslint --flag unstable_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
|
||||
"lint:eslint": "eslint --flag v10_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
|
||||
"format:eslint": "eslint --flag v10_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
|
||||
"lint:prettier": "prettier . --cache --check",
|
||||
"format:prettier": "prettier . --cache --write",
|
||||
"lint:types": "tsc",
|
||||
@@ -26,12 +26,12 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@awesome.me/webawesome": "3.0.0-beta.3",
|
||||
"@babel/runtime": "7.28.2",
|
||||
"@awesome.me/webawesome": "3.0.0-beta.4",
|
||||
"@babel/runtime": "7.28.3",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@codemirror/autocomplete": "6.18.6",
|
||||
"@codemirror/commands": "6.8.1",
|
||||
"@codemirror/language": "6.11.2",
|
||||
"@codemirror/language": "6.11.3",
|
||||
"@codemirror/legacy-modes": "6.5.1",
|
||||
"@codemirror/search": "6.5.11",
|
||||
"@codemirror/state": "6.5.2",
|
||||
@@ -46,12 +46,12 @@
|
||||
"@formatjs/intl-numberformat": "8.15.4",
|
||||
"@formatjs/intl-pluralrules": "5.4.4",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.11",
|
||||
"@fullcalendar/core": "6.1.18",
|
||||
"@fullcalendar/daygrid": "6.1.18",
|
||||
"@fullcalendar/interaction": "6.1.18",
|
||||
"@fullcalendar/list": "6.1.18",
|
||||
"@fullcalendar/luxon3": "6.1.18",
|
||||
"@fullcalendar/timegrid": "6.1.18",
|
||||
"@fullcalendar/core": "6.1.19",
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
"@fullcalendar/interaction": "6.1.19",
|
||||
"@fullcalendar/list": "6.1.19",
|
||||
"@fullcalendar/luxon3": "6.1.19",
|
||||
"@fullcalendar/timegrid": "6.1.19",
|
||||
"@lezer/highlight": "1.2.1",
|
||||
"@lit-labs/motion": "1.0.9",
|
||||
"@lit-labs/observers": "2.0.6",
|
||||
@@ -61,7 +61,6 @@
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-base": "0.27.0",
|
||||
"@material/mwc-button": "0.27.0",
|
||||
"@material/mwc-checkbox": "0.27.0",
|
||||
"@material/mwc-dialog": "0.27.0",
|
||||
"@material/mwc-drawer": "0.27.0",
|
||||
@@ -81,17 +80,17 @@
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "2.3.0",
|
||||
"@material/web": "2.4.0",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@shoelace-style/shoelace": "2.20.1",
|
||||
"@swc/helpers": "0.5.17",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.8.1",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vaadin/combo-box": "24.7.9",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.9",
|
||||
"@vaadin/combo-box": "24.8.6",
|
||||
"@vaadin/vaadin-themable-mixin": "24.8.6",
|
||||
"@vibrant/color": "4.0.0",
|
||||
"@vue/web-component-wrapper": "1.3.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
@@ -100,7 +99,7 @@
|
||||
"barcode-detector": "3.0.5",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.44.0",
|
||||
"core-js": "3.45.1",
|
||||
"cropperjs": "1.6.2",
|
||||
"culori": "4.0.2",
|
||||
"date-fns": "4.1.0",
|
||||
@@ -108,12 +107,12 @@
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"echarts": "5.6.0",
|
||||
"echarts": "6.0.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"hls.js": "1.6.7",
|
||||
"hls.js": "1.6.11",
|
||||
"home-assistant-js-websocket": "9.5.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.16",
|
||||
@@ -124,7 +123,7 @@
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"luxon": "3.7.1",
|
||||
"marked": "16.1.1",
|
||||
"marked": "16.2.0",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "4.0.3",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -150,18 +149,18 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.28.0",
|
||||
"@babel/core": "7.28.3",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.5",
|
||||
"@babel/plugin-transform-runtime": "7.28.0",
|
||||
"@babel/preset-env": "7.28.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.1",
|
||||
"@lokalise/node-api": "15.0.0",
|
||||
"@babel/plugin-transform-runtime": "7.28.3",
|
||||
"@babel/preset-env": "7.28.3",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.3",
|
||||
"@lokalise/node-api": "15.2.1",
|
||||
"@octokit/auth-oauth-device": "8.0.1",
|
||||
"@octokit/plugin-retry": "8.0.1",
|
||||
"@octokit/rest": "22.0.0",
|
||||
"@rsdoctor/rspack-plugin": "1.1.10",
|
||||
"@rspack/cli": "1.4.10",
|
||||
"@rspack/core": "1.4.10",
|
||||
"@rsdoctor/rspack-plugin": "1.2.3",
|
||||
"@rspack/core": "1.4.11",
|
||||
"@rspack/dev-server": "1.1.4",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.22",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
@@ -173,7 +172,7 @@
|
||||
"@types/leaflet-draw": "1.0.12",
|
||||
"@types/leaflet.markercluster": "1.5.5",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.6.2",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
@@ -185,17 +184,17 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.0",
|
||||
"eslint": "9.32.0",
|
||||
"eslint": "9.34.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-lit": "2.1.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.1",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "4.2.0",
|
||||
"eslint-plugin-wc": "3.0.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.0",
|
||||
"fs-extra": "11.3.1",
|
||||
"glob": "11.0.3",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
@@ -205,7 +204,7 @@
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "26.1.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.1.2",
|
||||
"lint-staged": "16.1.5",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
@@ -218,8 +217,8 @@
|
||||
"tar": "7.4.3",
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "8.38.0",
|
||||
"typescript": "5.9.2",
|
||||
"typescript-eslint": "8.41.0",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.2.4",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
@@ -232,11 +231,11 @@
|
||||
"lit-html": "3.3.1",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.1",
|
||||
"@fullcalendar/daygrid": "6.1.18",
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
"globals": "16.3.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"@vaadin/vaadin-themable-mixin": "24.7.9"
|
||||
"@vaadin/vaadin-themable-mixin": "24.8.6"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2"
|
||||
"packageManager": "yarn@4.9.4"
|
||||
}
|
||||
|
@@ -0,0 +1,76 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4744_40067)">
|
||||
<path d="M0 6C0 2.68629 2.68629 0 6 0H28C31.3137 0 34 2.68629 34 6C34 9.31371 31.3137 12 28 12H6C2.68629 12 0 9.31371 0 6Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M0 28C0 23.5817 3.58172 20 8 20H42.6667C47.0849 20 50.6667 23.5817 50.6667 28V36C50.6667 40.4183 47.0849 44 42.6667 44H8.00001C3.58173 44 0 40.4183 0 36V28Z" fill="#1C1C1C"/>
|
||||
<path d="M8 20.5H42.667C46.809 20.5002 50.167 23.858 50.167 28V36C50.167 40.142 46.809 43.4998 42.667 43.5H8C3.85787 43.5 0.5 40.1421 0.5 36V28C0.5 23.8579 3.85786 20.5 8 20.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M6 32C6 28.6863 8.68629 26 12 26C15.3137 26 18 28.6863 18 32C18 35.3137 15.3137 38 12 38C8.68629 38 6 35.3137 6 32Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M24 31C24 29.3431 25.3431 28 27 28H39.6667C41.3235 28 42.6667 29.3431 42.6667 31V33C42.6667 34.6569 41.3235 36 39.6667 36H27C25.3431 36 24 34.6569 24 33V31Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M54.6666 28C54.6666 23.5817 58.2483 20 62.6666 20H97.3333C101.752 20 105.333 23.5817 105.333 28V36C105.333 40.4183 101.752 44 97.3333 44H62.6666C58.2484 44 54.6666 40.4183 54.6666 36V28Z" fill="#1C1C1C"/>
|
||||
<path d="M62.6666 20.5H97.3336C101.476 20.5002 104.834 23.858 104.834 28V36C104.834 40.142 101.476 43.4998 97.3336 43.5H62.6666C58.5245 43.5 55.1666 40.1421 55.1666 36V28C55.1666 23.8579 58.5245 20.5 62.6666 20.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M60.6666 32C60.6666 28.6863 63.3529 26 66.6666 26C69.9803 26 72.6666 28.6863 72.6666 32C72.6666 35.3137 69.9803 38 66.6666 38C63.3529 38 60.6666 35.3137 60.6666 32Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M78.6666 31C78.6666 29.3431 80.0098 28 81.6666 28H94.3333C95.9901 28 97.3333 29.3431 97.3333 31V33C97.3333 34.6569 95.9901 36 94.3333 36H81.6666C80.0098 36 78.6666 34.6569 78.6666 33V31Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M109.333 28C109.333 23.5817 112.915 20 117.333 20H152C156.418 20 160 23.5817 160 28V36C160 40.4183 156.418 44 152 44H117.333C112.915 44 109.333 40.4183 109.333 36V28Z" fill="#1C1C1C"/>
|
||||
<path d="M117.333 20.5H152C156.142 20.5002 159.5 23.858 159.5 28V36C159.5 40.142 156.142 43.4998 152 43.5H117.333C113.191 43.5 109.833 40.1421 109.833 36V28C109.833 23.8579 113.191 20.5 117.333 20.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<path d="M115.333 32C115.333 28.6863 118.02 26 121.333 26C124.647 26 127.333 28.6863 127.333 32C127.333 35.3137 124.647 38 121.333 38C118.02 38 115.333 35.3137 115.333 32Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M133.333 31C133.333 29.3431 134.677 28 136.333 28H149C150.657 28 152 29.3431 152 31V33C152 34.6569 150.657 36 149 36H136.333C134.677 36 133.333 34.6569 133.333 33V31Z" fill="white" fill-opacity="0.24"/>
|
||||
<path d="M0 56C0 53.7909 1.79086 52 4 52H29C31.2091 52 33 53.7909 33 56C33 58.2091 31.2091 60 29 60H4C1.79086 60 0 58.2091 0 56Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M0 72C0 67.5817 3.58172 64 8 64H29C33.4183 64 37 67.5817 37 72V96C37 100.418 33.4183 104 29 104H8C3.58172 104 0 100.418 0 96V72Z" fill="#1C1C1C"/>
|
||||
<path d="M8 64.5H29C33.1421 64.5 36.5 67.8579 36.5 72V96C36.5 100.142 33.1421 103.5 29 103.5H8C3.85786 103.5 0.5 100.142 0.5 96V72C0.5 67.8579 3.85786 64.5 8 64.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<mask id="mask0_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="6" y="72" width="25" height="24">
|
||||
<path d="M18.5 74C16.6435 74 14.863 74.7375 13.5503 76.0503C12.2375 77.363 11.5 79.1435 11.5 81C11.5 83.38 12.69 85.47 14.5 86.74V89C14.5 89.2652 14.6054 89.5196 14.7929 89.7071C14.9804 89.8946 15.2348 90 15.5 90H21.5C21.7652 90 22.0196 89.8946 22.2071 89.7071C22.3946 89.5196 22.5 89.2652 22.5 89V86.74C24.31 85.47 25.5 83.38 25.5 81C25.5 79.1435 24.7625 77.363 23.4497 76.0503C22.137 74.7375 20.3565 74 18.5 74ZM15.5 93C15.5 93.2652 15.6054 93.5196 15.7929 93.7071C15.9804 93.8946 16.2348 94 16.5 94H20.5C20.7652 94 21.0196 93.8946 21.2071 93.7071C21.3946 93.5196 21.5 93.2652 21.5 93V92H15.5V93Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_4744_40067)">
|
||||
<rect x="6.5" y="72" width="24" height="24" fill="#03A9F4"/>
|
||||
</g>
|
||||
<path d="M41 72C41 67.5817 44.5817 64 49 64H70C74.4183 64 78 67.5817 78 72V96C78 100.418 74.4183 104 70 104H49C44.5817 104 41 100.418 41 96V72Z" fill="#1C1C1C"/>
|
||||
<path d="M49 64.5H70C74.1421 64.5 77.5 67.8579 77.5 72V96C77.5 100.142 74.1421 103.5 70 103.5H49C44.8579 103.5 41.5 100.142 41.5 96V72C41.5 67.8579 44.8579 64.5 49 64.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<mask id="mask1_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="47" y="72" width="25" height="24">
|
||||
<path d="M66.5 80C67.61 80 68.5 80.9 68.5 82V88.76C69.11 89.31 69.5 90.11 69.5 91C69.5 92.66 68.16 94 66.5 94C64.84 94 63.5 92.66 63.5 91C63.5 90.11 63.89 89.31 64.5 88.76V82C64.5 80.9 65.4 80 66.5 80ZM66.5 81C65.95 81 65.5 81.45 65.5 82V83H67.5V82C67.5 81.45 67.05 81 66.5 81ZM52.5 92V84H49.5L59.5 75L63.9 78.96C63.04 79.69 62.5 80.78 62.5 82V88C61.87 88.83 61.5 89.87 61.5 91L61.6 92H52.5Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_4744_40067)">
|
||||
<rect x="47.5" y="72" width="24" height="24" fill="#03A9F4"/>
|
||||
</g>
|
||||
<path d="M82 72C82 67.5817 85.5817 64 90 64H111C115.418 64 119 67.5817 119 72V96C119 100.418 115.418 104 111 104H90C85.5817 104 82 100.418 82 96V72Z" fill="#1C1C1C"/>
|
||||
<path d="M90 64.5H111C115.142 64.5 118.5 67.8579 118.5 72V96C118.5 100.142 115.142 103.5 111 103.5H90C85.8579 103.5 82.5 100.142 82.5 96V72C82.5 67.8579 85.8579 64.5 90 64.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<mask id="mask2_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="88" y="72" width="25" height="24">
|
||||
<path d="M100.5 84H107.5C106.97 88.11 104.22 91.78 100.5 92.92V84H93.5V78.3L100.5 75.19M100.5 73L91.5 77V83C91.5 88.55 95.34 93.73 100.5 95C105.66 93.73 109.5 88.55 109.5 83V77L100.5 73Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask2_4744_40067)">
|
||||
<rect x="88.5" y="72" width="24" height="24" fill="#03A9F4"/>
|
||||
</g>
|
||||
<path d="M123 72C123 67.5817 126.582 64 131 64H152C156.418 64 160 67.5817 160 72V96C160 100.418 156.418 104 152 104H131C126.582 104 123 100.418 123 96V72Z" fill="#1C1C1C"/>
|
||||
<path d="M131 64.5H152C156.142 64.5 159.5 67.8579 159.5 72V96C159.5 100.142 156.142 103.5 152 103.5H131C126.858 103.5 123.5 100.142 123.5 96V72C123.5 67.8579 126.858 64.5 131 64.5Z" stroke="white" stroke-opacity="0.24"/>
|
||||
<mask id="mask3_4744_40067" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="129" y="72" width="25" height="24">
|
||||
<path d="M145.5 84C145.5 83.4696 145.711 82.9609 146.086 82.5858C146.461 82.2107 146.97 82 147.5 82C148.03 82 148.539 82.2107 148.914 82.5858C149.289 82.9609 149.5 83.4696 149.5 84C149.5 84.5304 149.289 85.0391 148.914 85.4142C148.539 85.7893 148.03 86 147.5 86C146.97 86 146.461 85.7893 146.086 85.4142C145.711 85.0391 145.5 84.5304 145.5 84ZM139.5 84C139.5 83.4696 139.711 82.9609 140.086 82.5858C140.461 82.2107 140.97 82 141.5 82C142.03 82 142.539 82.2107 142.914 82.5858C143.289 82.9609 143.5 83.4696 143.5 84C143.5 84.5304 143.289 85.0391 142.914 85.4142C142.539 85.7893 142.03 86 141.5 86C140.97 86 140.461 85.7893 140.086 85.4142C139.711 85.0391 139.5 84.5304 139.5 84ZM133.5 84C133.5 83.4696 133.711 82.9609 134.086 82.5858C134.461 82.2107 134.97 82 135.5 82C136.03 82 136.539 82.2107 136.914 82.5858C137.289 82.9609 137.5 83.4696 137.5 84C137.5 84.5304 137.289 85.0391 136.914 85.4142C136.539 85.7893 136.03 86 135.5 86C134.97 86 134.461 85.7893 134.086 85.4142C133.711 85.0391 133.5 84.5304 133.5 84Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask3_4744_40067)">
|
||||
<rect x="129.5" y="72" width="24" height="24" fill="#03A9F4"/>
|
||||
</g>
|
||||
<path d="M0 116C0 113.791 1.79086 112 4 112H29C31.2091 112 33 113.791 33 116C33 118.209 31.2091 120 29 120H4C1.79086 120 0 118.209 0 116Z" fill="white" fill-opacity="0.48"/>
|
||||
<path d="M0 132C0 127.582 3.58172 124 8 124H70C74.4183 124 78 127.582 78 132V160H0V132Z" fill="url(#paint0_linear_4744_40067)"/>
|
||||
<path d="M8 124.5H70C74.1421 124.5 77.5 127.858 77.5 132V159.5H0.5V132C0.5 127.858 3.85786 124.5 8 124.5Z" stroke="url(#paint1_linear_4744_40067)" stroke-opacity="0.12"/>
|
||||
<path d="M82 132C82 127.582 85.5817 124 90 124H152C156.418 124 160 127.582 160 132V160H82V132Z" fill="url(#paint2_linear_4744_40067)"/>
|
||||
<path d="M90 124.5H152C156.142 124.5 159.5 127.858 159.5 132V159.5H82.5V132C82.5 127.858 85.8579 124.5 90 124.5Z" stroke="url(#paint3_linear_4744_40067)" stroke-opacity="0.12"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4744_40067" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-color="#1C1C1C"/>
|
||||
<stop offset="1" stop-color="#1C1C1C" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_4744_40067" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-color="white" stop-opacity="0.24"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_4744_40067" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-color="#1C1C1C"/>
|
||||
<stop offset="1" stop-color="#1C1C1C" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_4744_40067" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-color="white" stop-opacity="0.24"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_4744_40067">
|
||||
<rect width="160" height="160" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,76 @@
|
||||
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_4744_39984)">
|
||||
<path d="M0 6C0 2.68629 2.68629 0 6 0H28C31.3137 0 34 2.68629 34 6C34 9.31371 31.3137 12 28 12H6C2.68629 12 0 9.31371 0 6Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M0 28C0 23.5817 3.58172 20 8 20H42.6667C47.0849 20 50.6667 23.5817 50.6667 28V36C50.6667 40.4183 47.0849 44 42.6667 44H8.00001C3.58173 44 0 40.4183 0 36V28Z" fill="white"/>
|
||||
<path d="M8 20.5H42.667C46.809 20.5002 50.167 23.858 50.167 28V36C50.167 40.142 46.809 43.4998 42.667 43.5H8C3.85787 43.5 0.5 40.1421 0.5 36V28C0.5 23.8579 3.85786 20.5 8 20.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="6" y="26" width="12" height="12" rx="6" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M24 31C24 29.3431 25.3431 28 27 28H39.6667C41.3235 28 42.6667 29.3431 42.6667 31V33C42.6667 34.6569 41.3235 36 39.6667 36H27C25.3431 36 24 34.6569 24 33V31Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M54.6667 28C54.6667 23.5817 58.2484 20 62.6667 20H97.3333C101.752 20 105.333 23.5817 105.333 28V36C105.333 40.4183 101.752 44 97.3334 44H62.6667C58.2484 44 54.6667 40.4183 54.6667 36V28Z" fill="white"/>
|
||||
<path d="M62.6667 20.5H97.3337C101.476 20.5002 104.834 23.858 104.834 28V36C104.834 40.142 101.476 43.4998 97.3337 43.5H62.6667C58.5246 43.5 55.1667 40.1421 55.1667 36V28C55.1667 23.8579 58.5246 20.5 62.6667 20.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="60.6667" y="26" width="12" height="12" rx="6" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M78.6667 31C78.6667 29.3431 80.0098 28 81.6667 28H94.3334C95.9902 28 97.3334 29.3431 97.3334 31V33C97.3334 34.6569 95.9902 36 94.3334 36H81.6667C80.0098 36 78.6667 34.6569 78.6667 33V31Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M109.333 28C109.333 23.5817 112.915 20 117.333 20H152C156.418 20 160 23.5817 160 28V36C160 40.4183 156.418 44 152 44H117.333C112.915 44 109.333 40.4183 109.333 36V28Z" fill="white"/>
|
||||
<path d="M117.333 20.5H152C156.142 20.5002 159.5 23.858 159.5 28V36C159.5 40.142 156.142 43.4998 152 43.5H117.333C113.191 43.5 109.833 40.1421 109.833 36V28C109.833 23.8579 113.191 20.5 117.333 20.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<rect x="115.333" y="26" width="12" height="12" rx="6" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M133.333 31C133.333 29.3431 134.676 28 136.333 28H149C150.657 28 152 29.3431 152 31V33C152 34.6569 150.657 36 149 36H136.333C134.676 36 133.333 34.6569 133.333 33V31Z" fill="black" fill-opacity="0.12"/>
|
||||
<path d="M0 56C0 53.7909 1.79086 52 4 52H29C31.2091 52 33 53.7909 33 56C33 58.2091 31.2091 60 29 60H4C1.79086 60 0 58.2091 0 56Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M0 72C0 67.5817 3.58172 64 8 64H29C33.4183 64 37 67.5817 37 72V96C37 100.418 33.4183 104 29 104H8C3.58172 104 0 100.418 0 96V72Z" fill="white"/>
|
||||
<path d="M8 64.5H29C33.1421 64.5 36.5 67.8579 36.5 72V96C36.5 100.142 33.1421 103.5 29 103.5H8C3.85786 103.5 0.5 100.142 0.5 96V72C0.5 67.8579 3.85786 64.5 8 64.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<mask id="mask0_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="6" y="72" width="25" height="24">
|
||||
<path d="M18.5 74C16.6435 74 14.863 74.7375 13.5503 76.0503C12.2375 77.363 11.5 79.1435 11.5 81C11.5 83.38 12.69 85.47 14.5 86.74V89C14.5 89.2652 14.6054 89.5196 14.7929 89.7071C14.9804 89.8946 15.2348 90 15.5 90H21.5C21.7652 90 22.0196 89.8946 22.2071 89.7071C22.3946 89.5196 22.5 89.2652 22.5 89V86.74C24.31 85.47 25.5 83.38 25.5 81C25.5 79.1435 24.7625 77.363 23.4497 76.0503C22.137 74.7375 20.3565 74 18.5 74ZM15.5 93C15.5 93.2652 15.6054 93.5196 15.7929 93.7071C15.9804 93.8946 16.2348 94 16.5 94H20.5C20.7652 94 21.0196 93.8946 21.2071 93.7071C21.3946 93.5196 21.5 93.2652 21.5 93V92H15.5V93Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_4744_39984)">
|
||||
<rect x="6.5" y="72" width="24" height="24" fill="#03A9F4"/>
|
||||
</g>
|
||||
<path d="M41 72C41 67.5817 44.5817 64 49 64H70C74.4183 64 78 67.5817 78 72V96C78 100.418 74.4183 104 70 104H49C44.5817 104 41 100.418 41 96V72Z" fill="white"/>
|
||||
<path d="M49 64.5H70C74.1421 64.5 77.5 67.8579 77.5 72V96C77.5 100.142 74.1421 103.5 70 103.5H49C44.8579 103.5 41.5 100.142 41.5 96V72C41.5 67.8579 44.8579 64.5 49 64.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<mask id="mask1_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="47" y="72" width="25" height="24">
|
||||
<path d="M66.5 80C67.61 80 68.5 80.9 68.5 82V88.76C69.11 89.31 69.5 90.11 69.5 91C69.5 92.66 68.16 94 66.5 94C64.84 94 63.5 92.66 63.5 91C63.5 90.11 63.89 89.31 64.5 88.76V82C64.5 80.9 65.4 80 66.5 80ZM66.5 81C65.95 81 65.5 81.45 65.5 82V83H67.5V82C67.5 81.45 67.05 81 66.5 81ZM52.5 92V84H49.5L59.5 75L63.9 78.96C63.04 79.69 62.5 80.78 62.5 82V88C61.87 88.83 61.5 89.87 61.5 91L61.6 92H52.5Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_4744_39984)">
|
||||
<rect x="47.5" y="72" width="24" height="24" fill="#03A9F4"/>
|
||||
</g>
|
||||
<path d="M82 72C82 67.5817 85.5817 64 90 64H111C115.418 64 119 67.5817 119 72V96C119 100.418 115.418 104 111 104H90C85.5817 104 82 100.418 82 96V72Z" fill="white"/>
|
||||
<path d="M90 64.5H111C115.142 64.5 118.5 67.8579 118.5 72V96C118.5 100.142 115.142 103.5 111 103.5H90C85.8579 103.5 82.5 100.142 82.5 96V72C82.5 67.8579 85.8579 64.5 90 64.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<mask id="mask2_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="88" y="72" width="25" height="24">
|
||||
<path d="M100.5 84H107.5C106.97 88.11 104.22 91.78 100.5 92.92V84H93.5V78.3L100.5 75.19M100.5 73L91.5 77V83C91.5 88.55 95.34 93.73 100.5 95C105.66 93.73 109.5 88.55 109.5 83V77L100.5 73Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask2_4744_39984)">
|
||||
<rect x="88.5" y="72" width="24" height="24" fill="#03A9F4"/>
|
||||
</g>
|
||||
<path d="M123 72C123 67.5817 126.582 64 131 64H152C156.418 64 160 67.5817 160 72V96C160 100.418 156.418 104 152 104H131C126.582 104 123 100.418 123 96V72Z" fill="white"/>
|
||||
<path d="M131 64.5H152C156.142 64.5 159.5 67.8579 159.5 72V96C159.5 100.142 156.142 103.5 152 103.5H131C126.858 103.5 123.5 100.142 123.5 96V72C123.5 67.8579 126.858 64.5 131 64.5Z" stroke="black" stroke-opacity="0.12"/>
|
||||
<mask id="mask3_4744_39984" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="129" y="72" width="25" height="24">
|
||||
<path d="M145.5 84C145.5 83.4696 145.711 82.9609 146.086 82.5858C146.461 82.2107 146.97 82 147.5 82C148.03 82 148.539 82.2107 148.914 82.5858C149.289 82.9609 149.5 83.4696 149.5 84C149.5 84.5304 149.289 85.0391 148.914 85.4142C148.539 85.7893 148.03 86 147.5 86C146.97 86 146.461 85.7893 146.086 85.4142C145.711 85.0391 145.5 84.5304 145.5 84ZM139.5 84C139.5 83.4696 139.711 82.9609 140.086 82.5858C140.461 82.2107 140.97 82 141.5 82C142.03 82 142.539 82.2107 142.914 82.5858C143.289 82.9609 143.5 83.4696 143.5 84C143.5 84.5304 143.289 85.0391 142.914 85.4142C142.539 85.7893 142.03 86 141.5 86C140.97 86 140.461 85.7893 140.086 85.4142C139.711 85.0391 139.5 84.5304 139.5 84ZM133.5 84C133.5 83.4696 133.711 82.9609 134.086 82.5858C134.461 82.2107 134.97 82 135.5 82C136.03 82 136.539 82.2107 136.914 82.5858C137.289 82.9609 137.5 83.4696 137.5 84C137.5 84.5304 137.289 85.0391 136.914 85.4142C136.539 85.7893 136.03 86 135.5 86C134.97 86 134.461 85.7893 134.086 85.4142C133.711 85.0391 133.5 84.5304 133.5 84Z" fill="black"/>
|
||||
</mask>
|
||||
<g mask="url(#mask3_4744_39984)">
|
||||
<rect x="129.5" y="72" width="24" height="24" fill="#18BCF2"/>
|
||||
</g>
|
||||
<path d="M0 116C0 113.791 1.79086 112 4 112H29C31.2091 112 33 113.791 33 116C33 118.209 31.2091 120 29 120H4C1.79086 120 0 118.209 0 116Z" fill="black" fill-opacity="0.32"/>
|
||||
<path d="M0 132C0 127.582 3.58172 124 8 124H70C74.4183 124 78 127.582 78 132V160H0V132Z" fill="url(#paint0_linear_4744_39984)"/>
|
||||
<path d="M8 124.5H70C74.1421 124.5 77.5 127.858 77.5 132V159.5H0.5V132C0.5 127.858 3.85786 124.5 8 124.5Z" stroke="url(#paint1_linear_4744_39984)" stroke-opacity="0.12"/>
|
||||
<path d="M82 132C82 127.582 85.5817 124 90 124H152C156.418 124 160 127.582 160 132V160H82V132Z" fill="url(#paint2_linear_4744_39984)"/>
|
||||
<path d="M90 124.5H152C156.142 124.5 159.5 127.858 159.5 132V159.5H82.5V132C82.5 127.858 85.8579 124.5 90 124.5Z" stroke="url(#paint3_linear_4744_39984)" stroke-opacity="0.12"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4744_39984" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_4744_39984" x1="39" y1="124" x2="39" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-opacity="0.12"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_4744_39984" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_4744_39984" x1="121" y1="124" x2="121" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.5" stop-opacity="0.12"/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_4744_39984">
|
||||
<rect width="160" height="160" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20250806.0"
|
||||
version = "20250827.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
@@ -237,10 +237,11 @@ export class HaAuthFlow extends LitElement {
|
||||
@value-changed=${this._stepDataChanged}
|
||||
></ha-auth-form>`
|
||||
)}
|
||||
${this.clientId === genClientId() &&
|
||||
!["select_mfa_module", "mfa"].includes(step.step_id)
|
||||
? html`
|
||||
<div class="space-between">
|
||||
|
||||
<div class="space-between">
|
||||
${this.clientId === genClientId() &&
|
||||
!["select_mfa_module", "mfa"].includes(step.step_id)
|
||||
? html`
|
||||
<ha-formfield
|
||||
class="store-token"
|
||||
.label=${this.localize(
|
||||
@@ -252,18 +253,16 @@ export class HaAuthFlow extends LitElement {
|
||||
@change=${this._storeTokenChanged}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
<a
|
||||
class="forgot-password"
|
||||
href="https://www.home-assistant.io/docs/locked_out/#forgot-password"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>${this.localize(
|
||||
"ui.panel.page-authorize.forgot_password"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: ""}
|
||||
<a
|
||||
class="forgot-password"
|
||||
href="https://www.home-assistant.io/docs/locked_out/#forgot-password"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>${this.localize("ui.panel.page-authorize.forgot_password")}</a
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
default:
|
||||
return nothing;
|
||||
|
@@ -132,15 +132,13 @@ export const shiftDateRange = (
|
||||
end = calcDate(endDate, addDays, locale, config, difference);
|
||||
} else {
|
||||
const difference =
|
||||
((calcDateDifferenceProperty(
|
||||
(calcDateDifferenceProperty(
|
||||
endDate,
|
||||
startDate,
|
||||
differenceInMilliseconds,
|
||||
locale,
|
||||
config
|
||||
) as number) +
|
||||
1) *
|
||||
(forward ? 1 : -1);
|
||||
) as number) * (forward ? 1 : -1);
|
||||
start = calcDate(startDate, addMilliseconds, locale, config, difference);
|
||||
end = calcDate(endDate, addMilliseconds, locale, config, difference);
|
||||
}
|
||||
|
4
src/common/dom/prevent_default_stop_propagation.ts
Normal file
4
src/common/dom/prevent_default_stop_propagation.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const preventDefaultStopPropagation = (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
};
|
@@ -28,7 +28,10 @@ export const computeEntityEntryName = (
|
||||
hass: HomeAssistant
|
||||
): string | undefined => {
|
||||
const name =
|
||||
entry.name || ("original_name" in entry ? entry.original_name : undefined);
|
||||
entry.name ||
|
||||
("original_name" in entry && entry.original_name != null
|
||||
? String(entry.original_name)
|
||||
: undefined);
|
||||
|
||||
const device = entry.device_id ? hass.devices[entry.device_id] : undefined;
|
||||
|
||||
|
@@ -14,6 +14,7 @@ export type LocalizeKeys =
|
||||
| `ui.card.weather.attributes.${string}`
|
||||
| `ui.card.weather.cardinal_direction.${string}`
|
||||
| `ui.card.lawn_mower.actions.${string}`
|
||||
| `ui.common.${string}`
|
||||
| `ui.components.calendar.event.rrule.${string}`
|
||||
| `ui.components.selectors.file.${string}`
|
||||
| `ui.components.logbook.messages.detected_device_classes.${string}`
|
||||
@@ -26,6 +27,7 @@ export type LocalizeKeys =
|
||||
| `ui.dialogs.unsupported.reasons.${string}`
|
||||
| `ui.panel.config.${string}.${"caption" | "description"}`
|
||||
| `ui.panel.config.dashboard.${string}`
|
||||
| `ui.panel.config.storage.segments.${string}`
|
||||
| `ui.panel.config.zha.${string}`
|
||||
| `ui.panel.config.zwave_js.${string}`
|
||||
| `ui.panel.lovelace.card.${string}`
|
||||
|
@@ -39,6 +39,7 @@ export type CustomLegendOption = ECOption["legend"] & {
|
||||
type: "custom";
|
||||
data?: {
|
||||
id?: string;
|
||||
secondaryIds?: string[]; // Other dataset IDs that should be controlled by this legend item.
|
||||
name: string;
|
||||
itemStyle?: Record<string, any>;
|
||||
}[];
|
||||
@@ -181,6 +182,10 @@ export class HaChartBase extends LitElement {
|
||||
return;
|
||||
}
|
||||
let chartOptions: ECOption = {};
|
||||
if (changedProps.has("options")) {
|
||||
// Separate 'if' from below since this must updated before _getSeries()
|
||||
this._updateHiddenStatsFromOptions(this.options);
|
||||
}
|
||||
if (changedProps.has("data") || changedProps.has("_hiddenDatasets")) {
|
||||
chartOptions.series = this._getSeries();
|
||||
}
|
||||
@@ -387,24 +392,25 @@ export class HaChartBase extends LitElement {
|
||||
lastTipX = e.x;
|
||||
lastTipY = e.y;
|
||||
this.chart?.setOption({
|
||||
xAxis: ensureArray(this.chart?.getOption().xAxis as any).map(
|
||||
(axis: XAXisOption) =>
|
||||
axis.show
|
||||
? {
|
||||
...axis,
|
||||
axisPointer: {
|
||||
...axis.axisPointer,
|
||||
status: "show",
|
||||
handle: {
|
||||
color: style.getPropertyValue("primary-color"),
|
||||
margin: 0,
|
||||
size: 20,
|
||||
...axis.axisPointer?.handle,
|
||||
show: true,
|
||||
},
|
||||
xAxis: ensureArray(
|
||||
(this.chart?.getOption().xAxis as any) ?? []
|
||||
).map((axis: XAXisOption) =>
|
||||
axis.show
|
||||
? {
|
||||
...axis,
|
||||
axisPointer: {
|
||||
...axis.axisPointer,
|
||||
status: "show",
|
||||
handle: {
|
||||
color: style.getPropertyValue("--primary-color"),
|
||||
margin: 0,
|
||||
size: 20,
|
||||
...axis.axisPointer?.handle,
|
||||
show: true,
|
||||
},
|
||||
}
|
||||
: axis
|
||||
},
|
||||
}
|
||||
: axis
|
||||
),
|
||||
});
|
||||
});
|
||||
@@ -417,21 +423,22 @@ export class HaChartBase extends LitElement {
|
||||
return;
|
||||
}
|
||||
this.chart?.setOption({
|
||||
xAxis: ensureArray(this.chart?.getOption().xAxis as any).map(
|
||||
(axis: XAXisOption) =>
|
||||
axis.show
|
||||
? {
|
||||
...axis,
|
||||
axisPointer: {
|
||||
...axis.axisPointer,
|
||||
handle: {
|
||||
...axis.axisPointer?.handle,
|
||||
show: false,
|
||||
},
|
||||
status: "hide",
|
||||
xAxis: ensureArray(
|
||||
(this.chart?.getOption().xAxis as any) ?? []
|
||||
).map((axis: XAXisOption) =>
|
||||
axis.show
|
||||
? {
|
||||
...axis,
|
||||
axisPointer: {
|
||||
...axis.axisPointer,
|
||||
handle: {
|
||||
...axis.axisPointer?.handle,
|
||||
show: false,
|
||||
},
|
||||
}
|
||||
: axis
|
||||
status: "hide",
|
||||
},
|
||||
}
|
||||
: axis
|
||||
),
|
||||
});
|
||||
this.chart?.dispatchAction({
|
||||
@@ -449,14 +456,7 @@ export class HaChartBase extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
const legend = ensureArray(this.options?.legend || [])[0] as
|
||||
| LegendComponentOption
|
||||
| undefined;
|
||||
Object.entries(legend?.selected || {}).forEach(([stat, selected]) => {
|
||||
if (selected === false) {
|
||||
this._hiddenDatasets.add(stat);
|
||||
}
|
||||
});
|
||||
this._updateHiddenStatsFromOptions(this.options);
|
||||
|
||||
this.chart.setOption({
|
||||
...this._createOptions(),
|
||||
@@ -467,6 +467,42 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Return an array of all IDs associated with the legend item of the primaryId
|
||||
private _getAllIdsFromLegend(
|
||||
options: ECOption | undefined,
|
||||
primaryId: string
|
||||
): string[] {
|
||||
if (!options) return [primaryId];
|
||||
const legend = ensureArray(this.options?.legend || [])[0] as
|
||||
| LegendComponentOption
|
||||
| undefined;
|
||||
|
||||
let customLegendItem;
|
||||
if (legend?.type === "custom") {
|
||||
customLegendItem = (legend as CustomLegendOption).data?.find(
|
||||
(li) => typeof li === "object" && li.id === primaryId
|
||||
);
|
||||
}
|
||||
|
||||
return [primaryId, ...(customLegendItem?.secondaryIds || [])];
|
||||
}
|
||||
|
||||
// Parses the options structure and adds all ids of unselected legend items to hiddenDatasets.
|
||||
// No known need to remove items at this time.
|
||||
private _updateHiddenStatsFromOptions(options: ECOption | undefined) {
|
||||
if (!options) return;
|
||||
const legend = ensureArray(this.options?.legend || [])[0] as
|
||||
| LegendComponentOption
|
||||
| undefined;
|
||||
Object.entries(legend?.selected || {}).forEach(([stat, selected]) => {
|
||||
if (selected === false) {
|
||||
this._getAllIdsFromLegend(options, stat).forEach((id) =>
|
||||
this._hiddenDatasets.add(id)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _getDataZoomConfig(): DataZoomComponentOption | undefined {
|
||||
const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
|
||||
| XAXisOption
|
||||
@@ -598,6 +634,13 @@ export class HaChartBase extends LitElement {
|
||||
textBorderWidth: 2,
|
||||
},
|
||||
},
|
||||
sankey: {
|
||||
label: {
|
||||
color: style.getPropertyValue("--primary-text-color"),
|
||||
textBorderColor: style.getPropertyValue("--primary-background-color"),
|
||||
textBorderWidth: 2,
|
||||
},
|
||||
},
|
||||
categoryAxis: {
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
@@ -835,10 +878,14 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
const id = ev.currentTarget?.id;
|
||||
if (this._hiddenDatasets.has(id)) {
|
||||
this._hiddenDatasets.delete(id);
|
||||
this._getAllIdsFromLegend(this.options, id).forEach((i) =>
|
||||
this._hiddenDatasets.delete(i)
|
||||
);
|
||||
fireEvent(this, "dataset-unhidden", { id });
|
||||
} else {
|
||||
this._hiddenDatasets.add(id);
|
||||
this._getAllIdsFromLegend(this.options, id).forEach((i) =>
|
||||
this._hiddenDatasets.add(i)
|
||||
);
|
||||
fireEvent(this, "dataset-hidden", { id });
|
||||
}
|
||||
this.requestUpdate("_hiddenDatasets");
|
||||
|
@@ -18,7 +18,6 @@ export interface Node {
|
||||
value: number;
|
||||
index: number; // like z-index but for x/y
|
||||
label?: string;
|
||||
tooltip?: string;
|
||||
color?: string;
|
||||
passThrough?: boolean;
|
||||
}
|
||||
@@ -186,23 +185,22 @@ export class HaSankeyChart extends LitElement {
|
||||
""
|
||||
);
|
||||
const wordWidth = measureTextWidth(longestWord, FONT_SIZE);
|
||||
const availableWidth = params.rect.width + 6;
|
||||
const fontSize = Math.min(
|
||||
FONT_SIZE,
|
||||
(params.rect.width / wordWidth) * FONT_SIZE
|
||||
(availableWidth / wordWidth) * FONT_SIZE
|
||||
);
|
||||
return {
|
||||
fontSize: fontSize > 1 ? fontSize : 0,
|
||||
width: params.rect.width,
|
||||
width: availableWidth,
|
||||
align: "center",
|
||||
dy: -2, // shift up or the lowest row labels may be cut off
|
||||
};
|
||||
}
|
||||
|
||||
// estimate the number of lines after the label is wrapped
|
||||
// this is a very rough estimate, but it works for now
|
||||
const lineCount = Math.ceil(params.labelRect.width / labelSpace);
|
||||
// `overflow: "break"` allows the label to overflow outside its height, so we need to account for that
|
||||
const availableHeight = params.rect.height + 8; // account for the margin
|
||||
const fontSize = Math.min(
|
||||
(params.rect.height / lineCount) * FONT_SIZE,
|
||||
(availableHeight / params.labelRect.height) * FONT_SIZE,
|
||||
FONT_SIZE
|
||||
);
|
||||
return {
|
||||
|
@@ -101,7 +101,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
fill: api.value(4) as string,
|
||||
},
|
||||
};
|
||||
const text = api.value(3) as string;
|
||||
const text = (api.value(3) as string).replaceAll("\n", " ");
|
||||
const textWidth = measureTextWidth(text, 12);
|
||||
const LABEL_PADDING = 4;
|
||||
if (textWidth < rectShape.width - LABEL_PADDING * 2) {
|
||||
|
@@ -10,8 +10,8 @@ import {
|
||||
} from "../../data/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-list-item";
|
||||
import "../ha-select";
|
||||
import "../ha-md-select-option";
|
||||
import "../ha-md-select";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
|
||||
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
|
||||
@@ -100,35 +100,35 @@ export abstract class HaDeviceAutomationPicker<
|
||||
}
|
||||
const value = this._value;
|
||||
return html`
|
||||
<ha-select
|
||||
<ha-md-select
|
||||
.label=${this.label}
|
||||
.value=${value}
|
||||
@selected=${this._automationChanged}
|
||||
@change=${this._automationChanged}
|
||||
@closed=${stopPropagation}
|
||||
.disabled=${this._automations.length === 0}
|
||||
>
|
||||
${value === NO_AUTOMATION_KEY
|
||||
? html`<ha-list-item .value=${NO_AUTOMATION_KEY}>
|
||||
? html`<ha-md-select-option .value=${NO_AUTOMATION_KEY}>
|
||||
${this.NO_AUTOMATION_TEXT}
|
||||
</ha-list-item>`
|
||||
: ""}
|
||||
</ha-md-select-option>`
|
||||
: nothing}
|
||||
${value === UNKNOWN_AUTOMATION_KEY
|
||||
? html`<ha-list-item .value=${UNKNOWN_AUTOMATION_KEY}>
|
||||
? html`<ha-md-select-option .value=${UNKNOWN_AUTOMATION_KEY}>
|
||||
${this.UNKNOWN_AUTOMATION_TEXT}
|
||||
</ha-list-item>`
|
||||
: ""}
|
||||
</ha-md-select-option>`
|
||||
: nothing}
|
||||
${this._automations.map(
|
||||
(automation, idx) => html`
|
||||
<ha-list-item .value=${`${automation.device_id}_${idx}`}>
|
||||
<ha-md-select-option .value=${`${automation.device_id}_${idx}`}>
|
||||
${this._localizeDeviceAutomation(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
automation
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-md-select-option>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
</ha-md-select>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -63,10 +63,10 @@ class HaEntityStatePicker extends LitElement {
|
||||
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
|
||||
|
||||
const entitiesOptions = entityIds.map<StateOption[]>((entityId) => {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
if (!stateObj) {
|
||||
return [];
|
||||
}
|
||||
const stateObj = this.hass.states[entityId] || {
|
||||
entity_id: entityId,
|
||||
attributes: {},
|
||||
};
|
||||
|
||||
const states = getStates(this.hass, stateObj, this.attribute).filter(
|
||||
(s) => !this.hideStates?.includes(s)
|
||||
|
149
src/components/entity/ha-entity-states-picker.ts
Normal file
149
src/components/entity/ha-entity-states-picker.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-entity-state-picker";
|
||||
|
||||
@customElement("ha-entity-states-picker")
|
||||
export class HaEntityStatesPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId?: string;
|
||||
|
||||
@property() public attribute?: string;
|
||||
|
||||
@property({ attribute: false }) public extraOptions?: any[];
|
||||
|
||||
@property({ type: Boolean, attribute: "allow-custom-value" })
|
||||
public allowCustomValue;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Array }) public value?: string[];
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public hideStates?: string[];
|
||||
|
||||
private _keys: string[] = [];
|
||||
|
||||
private _getKey(index: number) {
|
||||
if (!this._keys[index]) {
|
||||
this._keys[index] = Math.random().toString();
|
||||
}
|
||||
return this._keys[index];
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("value")) {
|
||||
this.value = ensureArray(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const value = this.value || [];
|
||||
const hide = [...(this.hideStates || []), ...value];
|
||||
|
||||
return html`
|
||||
${repeat(
|
||||
value,
|
||||
(_state, index) => this._getKey(index),
|
||||
(state, index) => html`
|
||||
<div>
|
||||
<ha-entity-state-picker
|
||||
.index=${index}
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.entityId}
|
||||
.attribute=${this.attribute}
|
||||
.extraOptions=${this.extraOptions}
|
||||
.hideStates=${hide.filter((v) => v !== state)}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.label=${this.label}
|
||||
.value=${state}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.disabled && index === value.length - 1
|
||||
? this.helper
|
||||
: undefined}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-entity-state-picker>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div>
|
||||
${this.disabled && value.length
|
||||
? nothing
|
||||
: keyed(
|
||||
value.length,
|
||||
html`<ha-entity-state-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.entityId}
|
||||
.attribute=${this.attribute}
|
||||
.extraOptions=${this.extraOptions}
|
||||
.hideStates=${hide}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
@value-changed=${this._addValue}
|
||||
></ha-entity-state-picker>`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const newState = ev.detail.value;
|
||||
const newValue = [...this.value!];
|
||||
const index = (ev.currentTarget as any)?.index;
|
||||
if (index == null) {
|
||||
return;
|
||||
}
|
||||
if (newState === undefined) {
|
||||
newValue.splice(index, 1);
|
||||
this._keys.splice(index, 1);
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
return;
|
||||
}
|
||||
newValue[index] = newState;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
private _addValue(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: [...(this.value || []), ev.detail.value],
|
||||
});
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
div {
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-entity-states-picker": HaEntityStatesPicker;
|
||||
}
|
||||
}
|
@@ -97,9 +97,6 @@ class HaAlert extends LitElement {
|
||||
content: "";
|
||||
border-radius: 4px;
|
||||
}
|
||||
.icon {
|
||||
z-index: 1;
|
||||
}
|
||||
.icon.no-title {
|
||||
align-self: center;
|
||||
}
|
||||
@@ -122,10 +119,11 @@ class HaAlert extends LitElement {
|
||||
.main-content {
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
line-height: normal;
|
||||
margin-left: 8px;
|
||||
margin-right: 0;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.title {
|
||||
margin-top: 2px;
|
||||
|
@@ -3,11 +3,15 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
|
||||
import { STATE_ATTRIBUTES } from "../data/entity_attributes";
|
||||
import {
|
||||
STATE_ATTRIBUTES,
|
||||
STATE_ATTRIBUTES_DOMAIN_CLASS,
|
||||
} from "../data/entity_attributes";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-attribute-value";
|
||||
import "./ha-expansion-panel";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
|
||||
@customElement("ha-attributes")
|
||||
class HaAttributes extends LitElement {
|
||||
@@ -22,7 +26,12 @@ class HaAttributes extends LitElement {
|
||||
private get _filteredAttributes() {
|
||||
return this._computeDisplayAttributes(
|
||||
STATE_ATTRIBUTES.concat(
|
||||
this.extraFilters ? this.extraFilters.split(",") : []
|
||||
this.extraFilters ? this.extraFilters.split(",") : [],
|
||||
(this.stateObj &&
|
||||
STATE_ATTRIBUTES_DOMAIN_CLASS[computeStateDomain(this.stateObj)]?.[
|
||||
this.stateObj.attributes?.device_class
|
||||
]) ||
|
||||
[]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
210
src/components/ha-automation-row.ts
Normal file
210
src/components/ha-automation-row.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { mdiChevronUp } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@customElement("ha-automation-row")
|
||||
export class HaAutomationRow extends LitElement {
|
||||
@property({ attribute: "left-chevron", type: Boolean })
|
||||
public leftChevron = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public collapsed = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public selected = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "sort-selected" })
|
||||
public sortSelected = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public disabled = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "building-block" })
|
||||
public buildingBlock = false;
|
||||
|
||||
@query(".row")
|
||||
private _rowElement?: HTMLDivElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
class="row"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keydown=${this._handleKeydown}
|
||||
>
|
||||
${this.leftChevron
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="expand-button"
|
||||
.path=${mdiChevronUp}
|
||||
@click=${this._handleExpand}
|
||||
@keydown=${this._handleExpand}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
<div class="leading-icon-wrapper">
|
||||
<slot name="leading-icon"></slot>
|
||||
</div>
|
||||
<slot class="header" name="header"></slot>
|
||||
<slot name="icons"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleExpand(ev) {
|
||||
if (ev.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
fireEvent(this, "toggle-collapsed");
|
||||
}
|
||||
|
||||
private async _handleKeydown(ev: KeyboardEvent): Promise<void> {
|
||||
if (ev.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
ev.key !== "Enter" &&
|
||||
ev.key !== " " &&
|
||||
!(
|
||||
(this.sortSelected || ev.altKey) &&
|
||||
(ev.key === "ArrowUp" || ev.key === "ArrowDown")
|
||||
) &&
|
||||
!(
|
||||
(ev.ctrlKey || ev.metaKey) &&
|
||||
(ev.key === "c" || ev.key === "x" || ev.key === "Delete")
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (ev.key === "ArrowUp" || ev.key === "ArrowDown") {
|
||||
if (ev.key === "ArrowUp") {
|
||||
fireEvent(this, "move-up");
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "move-down");
|
||||
return;
|
||||
}
|
||||
if (this.sortSelected && (ev.key === "Enter" || ev.key === " ")) {
|
||||
fireEvent(this, "stop-sort-selection");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.ctrlKey || ev.metaKey) {
|
||||
if (ev.key === "c") {
|
||||
fireEvent(this, "copy-row");
|
||||
return;
|
||||
}
|
||||
if (ev.key === "x") {
|
||||
fireEvent(this, "cut-row");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.key === "Delete") {
|
||||
fireEvent(this, "delete-row");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.click();
|
||||
}
|
||||
|
||||
public focus() {
|
||||
requestAnimationFrame(() => {
|
||||
this._rowElement?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
padding: 0 8px;
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
outline: none;
|
||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
}
|
||||
.row:focus {
|
||||
outline: var(--wa-focus-ring);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
.expand-button {
|
||||
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
}
|
||||
:host([building-block]) .leading-icon-wrapper {
|
||||
background-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
::slotted([slot="leading-icon"]) {
|
||||
color: var(--ha-color-on-neutral-quiet);
|
||||
}
|
||||
:host([building-block]) ::slotted([slot="leading-icon"]) {
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--white-color);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
:host([collapsed]) .expand-button {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
:host([selected]) .row,
|
||||
:host([selected]) .row:focus {
|
||||
outline: solid;
|
||||
outline-color: var(--primary-color);
|
||||
outline-offset: -2px;
|
||||
outline-width: 2px;
|
||||
}
|
||||
:host([disabled]) .row {
|
||||
border-top-right-radius: var(--ha-border-radius-square);
|
||||
border-top-left-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
::slotted([slot="header"]) {
|
||||
flex: 1;
|
||||
overflow-wrap: anywhere;
|
||||
margin: 0 12px;
|
||||
}
|
||||
:host([sort-selected]) .row {
|
||||
box-shadow:
|
||||
0px 0px 8px 4px rgba(var(--rgb-accent-color), 0.8),
|
||||
inset 0px 2px 8px 4px rgba(var(--rgb-accent-color), 0.4);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-row": HaAutomationRow;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"toggle-collapsed": undefined;
|
||||
"stop-sort-selection": undefined;
|
||||
"copy-row": undefined;
|
||||
"cut-row": undefined;
|
||||
"delete-row": undefined;
|
||||
}
|
||||
}
|
269
src/components/ha-bottom-sheet.ts
Normal file
269
src/components/ha-bottom-sheet.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
const ANIMATION_DURATION_MS = 300;
|
||||
|
||||
/**
|
||||
* A bottom sheet component that slides up from the bottom of the screen.
|
||||
*
|
||||
* The bottom sheet provides a draggable interface that allows users to resize
|
||||
* the sheet by dragging the handle at the top. It supports both mouse and touch
|
||||
* interactions and automatically closes when dragged below a 20% of screen height.
|
||||
*
|
||||
* @fires bottom-sheet-closed - Fired when the bottom sheet is closed
|
||||
*
|
||||
* @cssprop --ha-bottom-sheet-border-width - Border width for the sheet
|
||||
* @cssprop --ha-bottom-sheet-border-style - Border style for the sheet
|
||||
* @cssprop --ha-bottom-sheet-border-color - Border color for the sheet
|
||||
*/
|
||||
@customElement("ha-bottom-sheet")
|
||||
export class HaBottomSheet extends LitElement {
|
||||
@query("dialog") private _dialog!: HTMLDialogElement;
|
||||
|
||||
private _dragging = false;
|
||||
|
||||
private _dragStartY = 0;
|
||||
|
||||
private _initialSize = 0;
|
||||
|
||||
@state() private _dialogMaxViewpointHeight = 70;
|
||||
|
||||
@state() private _dialogViewportHeight?: number;
|
||||
|
||||
render() {
|
||||
return html`<dialog
|
||||
open
|
||||
@transitionend=${this._handleTransitionEnd}
|
||||
style=${styleMap({
|
||||
height: this._dialogViewportHeight
|
||||
? `${this._dialogViewportHeight}vh`
|
||||
: "auto",
|
||||
maxHeight: `${this._dialogMaxViewpointHeight}vh`,
|
||||
})}
|
||||
>
|
||||
<div class="handle-wrapper">
|
||||
<div
|
||||
@mousedown=${this._handleMouseDown}
|
||||
@touchstart=${this._handleTouchStart}
|
||||
class="handle"
|
||||
></div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</dialog>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._openSheet();
|
||||
}
|
||||
|
||||
private _openSheet() {
|
||||
requestAnimationFrame(() => {
|
||||
// trigger opening animation
|
||||
this._dialog.classList.add("show");
|
||||
});
|
||||
}
|
||||
|
||||
public closeSheet() {
|
||||
requestAnimationFrame(() => {
|
||||
this._dialog.classList.remove("show");
|
||||
});
|
||||
}
|
||||
|
||||
private _handleTransitionEnd() {
|
||||
if (this._dialog.classList.contains("show")) {
|
||||
// after show animation is done
|
||||
// - set the height to the natural height, to prevent content shift when switch content
|
||||
// - set max height to 90vh, so it opens at max 70vh but can be resized to 90vh
|
||||
this._dialogViewportHeight =
|
||||
(this._dialog.offsetHeight / window.innerHeight) * 100;
|
||||
this._dialogMaxViewpointHeight = 90;
|
||||
} else {
|
||||
// after close animation is done close dialog element and fire closed event
|
||||
this._dialog.close();
|
||||
fireEvent(this, "bottom-sheet-closed");
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// register event listeners for drag handling
|
||||
document.addEventListener("mousemove", this._handleMouseMove);
|
||||
document.addEventListener("mouseup", this._handleMouseUp);
|
||||
document.addEventListener("touchmove", this._handleTouchMove, {
|
||||
passive: false,
|
||||
});
|
||||
document.addEventListener("touchend", this._handleTouchEnd);
|
||||
document.addEventListener("touchcancel", this._handleTouchEnd);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
// unregister event listeners for drag handling
|
||||
document.removeEventListener("mousemove", this._handleMouseMove);
|
||||
document.removeEventListener("mouseup", this._handleMouseUp);
|
||||
document.removeEventListener("touchmove", this._handleTouchMove);
|
||||
document.removeEventListener("touchend", this._handleTouchEnd);
|
||||
document.removeEventListener("touchcancel", this._handleTouchEnd);
|
||||
}
|
||||
|
||||
private _handleMouseDown = (ev: MouseEvent) => {
|
||||
this._startDrag(ev.clientY);
|
||||
};
|
||||
|
||||
private _handleTouchStart = (ev: TouchEvent) => {
|
||||
// Prevent the browser from interpreting this as a scroll/PTR gesture.
|
||||
ev.preventDefault();
|
||||
this._startDrag(ev.touches[0].clientY);
|
||||
};
|
||||
|
||||
private _startDrag(clientY: number) {
|
||||
this._dragging = true;
|
||||
this._dragStartY = clientY;
|
||||
this._initialSize = (this._dialog.offsetHeight / window.innerHeight) * 100;
|
||||
document.body.style.setProperty("cursor", "grabbing");
|
||||
}
|
||||
|
||||
private _handleMouseMove = (ev: MouseEvent) => {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
this._updateSize(ev.clientY);
|
||||
};
|
||||
|
||||
private _handleTouchMove = (ev: TouchEvent) => {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault(); // Prevent scrolling
|
||||
this._updateSize(ev.touches[0].clientY);
|
||||
};
|
||||
|
||||
private _updateSize(clientY: number) {
|
||||
const deltaY = this._dragStartY - clientY;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const deltaVh = (deltaY / viewportHeight) * 100;
|
||||
|
||||
// Calculate new size and clamp between 10vh and 90vh
|
||||
let newSize = this._initialSize + deltaVh;
|
||||
newSize = Math.max(10, Math.min(90, newSize));
|
||||
|
||||
// on drag down and below 20vh
|
||||
if (newSize < 20 && deltaY < 0) {
|
||||
this._endDrag();
|
||||
this.closeSheet();
|
||||
return;
|
||||
}
|
||||
|
||||
this._dialogViewportHeight = newSize;
|
||||
}
|
||||
|
||||
private _handleMouseUp = () => {
|
||||
this._endDrag();
|
||||
};
|
||||
|
||||
private _handleTouchEnd = () => {
|
||||
this._endDrag();
|
||||
};
|
||||
|
||||
private _endDrag() {
|
||||
if (!this._dragging) {
|
||||
return;
|
||||
}
|
||||
this._dragging = false;
|
||||
document.body.style.removeProperty("cursor");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.handle-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding-bottom: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
.handle-wrapper .handle {
|
||||
height: 20px;
|
||||
width: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 7;
|
||||
padding-bottom: 76px;
|
||||
}
|
||||
.handle-wrapper .handle::after {
|
||||
content: "";
|
||||
border-radius: 8px;
|
||||
height: 4px;
|
||||
background: var(--divider-color, #e0e0e0);
|
||||
width: 80px;
|
||||
}
|
||||
.handle-wrapper .handle:active::after {
|
||||
cursor: grabbing;
|
||||
}
|
||||
dialog {
|
||||
height: auto;
|
||||
max-height: 70vh;
|
||||
min-height: 30vh;
|
||||
background-color: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
position: fixed;
|
||||
width: calc(100% - 4px);
|
||||
max-width: 100%;
|
||||
border: none;
|
||||
box-shadow: var(--wa-shadow-l);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
top: auto;
|
||||
inset-inline-end: auto;
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
|
||||
border-top-left-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-2xl)
|
||||
);
|
||||
border-top-right-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-2xl)
|
||||
);
|
||||
transform: translateY(100%);
|
||||
transition: transform ${ANIMATION_DURATION_MS}ms ease;
|
||||
border-top-width: var(--ha-bottom-sheet-border-width);
|
||||
border-right-width: var(--ha-bottom-sheet-border-width);
|
||||
border-left-width: var(--ha-bottom-sheet-border-width);
|
||||
border-bottom-width: 0;
|
||||
border-style: var(--ha-bottom-sheet-border-style);
|
||||
border-color: var(--ha-bottom-sheet-border-color);
|
||||
}
|
||||
|
||||
dialog.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-bottom-sheet": HaBottomSheet;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"bottom-sheet-closed": undefined;
|
||||
}
|
||||
}
|
82
src/components/ha-button-group.ts
Normal file
82
src/components/ha-button-group.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import ButtonGroup from "@awesome.me/webawesome/dist/components/button-group/button-group";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { HaButton } from "./ha-button";
|
||||
import { StateSet } from "../resources/polyfills/stateset";
|
||||
|
||||
export type Appearance = "accent" | "filled" | "outlined" | "plain";
|
||||
|
||||
/**
|
||||
* Finds an ha-button element either as the current element or within its descendants.
|
||||
* @param el - The HTML element to search from
|
||||
* @returns The found HaButton element, or null if not found
|
||||
*/
|
||||
function findButton(el: HTMLElement) {
|
||||
const selector = "ha-button";
|
||||
return (el.closest(selector) ?? el.querySelector(selector)) as HaButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* @element ha-button-group
|
||||
* @extends {ButtonGroup}
|
||||
* @summary
|
||||
* Group buttons. Extend Webawesome to be able to work with ha-button tags
|
||||
*
|
||||
* @documentation https://webawesome.com/components/button-group
|
||||
*/
|
||||
@customElement("ha-button-group") // @ts-expect-error Intentionally overriding private methods
|
||||
export class HaButtonGroup extends ButtonGroup {
|
||||
attachInternals() {
|
||||
const internals = super.attachInternals();
|
||||
Object.defineProperty(internals, "states", {
|
||||
value: new StateSet(this, internals.states),
|
||||
});
|
||||
return internals;
|
||||
}
|
||||
|
||||
// @ts-expect-error updateClassNames is used in super class
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override updateClassNames() {
|
||||
const slottedElements = [
|
||||
...this.defaultSlot.assignedElements({ flatten: true }),
|
||||
] as HTMLElement[];
|
||||
this.hasOutlined = false;
|
||||
|
||||
slottedElements.forEach((el) => {
|
||||
const index = slottedElements.indexOf(el);
|
||||
const button = findButton(el);
|
||||
|
||||
if (button) {
|
||||
if ((button as HaButton).appearance === "outlined")
|
||||
this.hasOutlined = true;
|
||||
if (this.size) button.setAttribute("size", this.size);
|
||||
button.classList.add("wa-button-group__button");
|
||||
button.classList.toggle(
|
||||
"wa-button-group__horizontal",
|
||||
this.orientation === "horizontal"
|
||||
);
|
||||
button.classList.toggle(
|
||||
"wa-button-group__vertical",
|
||||
this.orientation === "vertical"
|
||||
);
|
||||
button.classList.toggle("wa-button-group__button-first", index === 0);
|
||||
button.classList.toggle(
|
||||
"wa-button-group__button-inner",
|
||||
index > 0 && index < slottedElements.length - 1
|
||||
);
|
||||
button.classList.toggle(
|
||||
"wa-button-group__button-last",
|
||||
index === slottedElements.length - 1
|
||||
);
|
||||
|
||||
// use button-group variant
|
||||
button.setAttribute("variant", this.variant);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-button-group": HaButtonGroup;
|
||||
}
|
||||
}
|
@@ -1,141 +1,72 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import type { Button } from "@material/mwc-button/mwc-button";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { ToggleButton } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-button";
|
||||
import "./ha-button-group";
|
||||
|
||||
/**
|
||||
* @element ha-button-toggle-group
|
||||
*
|
||||
* @summary
|
||||
* A button-group with one active selection.
|
||||
*
|
||||
* @attr {ToggleButton[]} buttons - the button config
|
||||
* @attr {string} active - The value of the currently active button.
|
||||
* @attr {("small"|"medium")} size - The size of the buttons in the group.
|
||||
* @attr {("brand"|"neutral"|"success"|"warning"|"danger")} variant - The variant of the buttons in the group.
|
||||
*
|
||||
* @fires value-changed - Dispatched when the active button changes.
|
||||
*/
|
||||
@customElement("ha-button-toggle-group")
|
||||
export class HaButtonToggleGroup extends LitElement {
|
||||
@property({ attribute: false }) public buttons!: ToggleButton[];
|
||||
|
||||
@property() public active?: string;
|
||||
|
||||
@property({ attribute: "full-width", type: Boolean })
|
||||
public fullWidth = false;
|
||||
@property({ reflect: true }) size: "small" | "medium" = "medium";
|
||||
|
||||
@property({ type: Boolean }) public dense = false;
|
||||
|
||||
@queryAll("mwc-button") private _buttons?: Button[];
|
||||
@property() public variant:
|
||||
| "brand"
|
||||
| "neutral"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "danger" = "brand";
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div>
|
||||
${this.buttons.map((button) =>
|
||||
button.iconPath
|
||||
? html`<ha-icon-button
|
||||
.label=${button.label}
|
||||
.path=${button.iconPath}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>`
|
||||
: html`<mwc-button
|
||||
style=${styleMap({
|
||||
width: this.fullWidth
|
||||
? `${100 / this.buttons.length}%`
|
||||
: "initial",
|
||||
})}
|
||||
outlined
|
||||
.dense=${this.dense}
|
||||
.value=${button.value}
|
||||
?active=${this.active === button.value}
|
||||
@click=${this._handleClick}
|
||||
>${button.label}</mwc-button
|
||||
>`
|
||||
<ha-button-group .variant=${this.variant} .size=${this.size}>
|
||||
${this.buttons.map(
|
||||
(button) =>
|
||||
html`<ha-button
|
||||
class="icon"
|
||||
.value=${button.value}
|
||||
@click=${this._handleClick}
|
||||
.title=${button.label}
|
||||
.appearance=${this.active === button.value ? "accent" : "filled"}
|
||||
>
|
||||
${button.iconPath
|
||||
? html`<ha-svg-icon
|
||||
aria-label=${button.label}
|
||||
.path=${button.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: button.label}
|
||||
</ha-button>`
|
||||
)}
|
||||
</div>
|
||||
</ha-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated() {
|
||||
// Work around Safari default margin that is not reset in mwc-button as of aug 2021
|
||||
this._buttons?.forEach(async (button) => {
|
||||
await button.updateComplete;
|
||||
(
|
||||
button.shadowRoot!.querySelector("button") as HTMLButtonElement
|
||||
).style.margin = "0";
|
||||
});
|
||||
}
|
||||
|
||||
private _handleClick(ev): void {
|
||||
this.active = ev.currentTarget.value;
|
||||
fireEvent(this, "value-changed", { value: this.active });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
div {
|
||||
display: flex;
|
||||
--mdc-icon-button-size: var(--button-toggle-size, 36px);
|
||||
:host {
|
||||
--mdc-icon-size: var(--button-toggle-icon-size, 20px);
|
||||
direction: ltr;
|
||||
}
|
||||
mwc-button {
|
||||
flex: 1;
|
||||
--mdc-shape-small: 0;
|
||||
--mdc-button-outline-width: 1px 0 1px 1px;
|
||||
--mdc-button-outline-color: var(--primary-color);
|
||||
}
|
||||
ha-icon-button {
|
||||
border: 1px solid var(--primary-color);
|
||||
border-right-width: 0px;
|
||||
}
|
||||
ha-icon-button,
|
||||
mwc-button {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
ha-icon-button::before,
|
||||
mwc-button::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background-color: var(--primary-color);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
transition:
|
||||
opacity 15ms linear,
|
||||
background-color 15ms linear;
|
||||
}
|
||||
ha-icon-button[active]::before,
|
||||
mwc-button[active]::before {
|
||||
opacity: 1;
|
||||
}
|
||||
ha-icon-button[active] {
|
||||
--icon-primary-color: var(--text-primary-color);
|
||||
}
|
||||
mwc-button[active] {
|
||||
--mdc-theme-primary: var(--text-primary-color);
|
||||
}
|
||||
ha-icon-button:first-child,
|
||||
mwc-button:first-child {
|
||||
--mdc-shape-small: 4px 0 0 4px;
|
||||
border-radius: 4px 0 0 4px;
|
||||
--mdc-button-outline-width: 1px;
|
||||
}
|
||||
mwc-button:first-child::before {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
ha-icon-button:last-child,
|
||||
mwc-button:last-child {
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-right-width: 1px;
|
||||
--mdc-shape-small: 0 4px 4px 0;
|
||||
--mdc-button-outline-width: 1px;
|
||||
}
|
||||
mwc-button:last-child::before {
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
ha-icon-button:only-child,
|
||||
mwc-button:only-child {
|
||||
--mdc-shape-small: 4px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -27,7 +27,7 @@ export type Appearance = "accent" | "filled" | "outlined" | "plain";
|
||||
* @csspart spinner - The spinner that shows when the button is in the loading state.
|
||||
*
|
||||
* @cssprop --ha-button-height - The height of the button.
|
||||
* @cssprop --ha-button-border-radius - The border radius of the button. defaults to `var(--border-radius-pill)`.
|
||||
* @cssprop --ha-button-border-radius - The border radius of the button. defaults to `var(--ha-border-radius-pill)`.
|
||||
*
|
||||
* @attr {("small"|"medium")} size - Sets the button size.
|
||||
* @attr {("brand"|"neutral"|"danger"|"warning"|"success")} variant - Sets the button color variant. "primary" is default.
|
||||
@@ -35,7 +35,7 @@ export type Appearance = "accent" | "filled" | "outlined" | "plain";
|
||||
* @attr {boolean} loading - shows a loading indicator instead of the buttons label and disable buttons click.
|
||||
* @attr {boolean} disabled - Disables the button and prevents user interaction.
|
||||
*/
|
||||
@customElement("ha-button")
|
||||
@customElement("ha-button") // @ts-expect-error Intentionally overriding private methods
|
||||
export class HaButton extends Button {
|
||||
variant: "brand" | "neutral" | "success" | "warning" | "danger" = "brand";
|
||||
|
||||
@@ -47,6 +47,42 @@ export class HaButton extends Button {
|
||||
return internals;
|
||||
}
|
||||
|
||||
// @ts-expect-error handleLabelSlotChange is used in super class
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
private override handleLabelSlotChange() {
|
||||
const nodes = this.labelSlot.assignedNodes({ flatten: true });
|
||||
let hasIconLabel = false;
|
||||
let hasIcon = false;
|
||||
let text = "";
|
||||
|
||||
// If there's only an icon and no text, it's an icon button
|
||||
[...nodes].forEach((node) => {
|
||||
if (
|
||||
node.nodeType === Node.ELEMENT_NODE &&
|
||||
(node as HTMLElement).localName === "ha-svg-icon"
|
||||
) {
|
||||
hasIcon = true;
|
||||
if (!hasIconLabel)
|
||||
hasIconLabel = (node as HTMLElement).hasAttribute("aria-label");
|
||||
}
|
||||
|
||||
// Concatenate text nodes
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
text += node.textContent;
|
||||
}
|
||||
});
|
||||
|
||||
this.isIconButton = text.trim() === "" && hasIcon;
|
||||
|
||||
if (__DEV__ && this.isIconButton && !hasIconLabel) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'Icon buttons must have a label for screen readers. Add <ha-svg-icon aria-label="..."> to remove this warning.',
|
||||
this
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Button.styles,
|
||||
@@ -57,7 +93,7 @@ export class HaButton extends Button {
|
||||
--wa-font-weight-action: var(--ha-font-weight-medium);
|
||||
--wa-form-control-border-radius: var(
|
||||
--ha-button-border-radius,
|
||||
var(--border-radius-pill)
|
||||
var(--ha-border-radius-pill)
|
||||
);
|
||||
|
||||
--wa-form-control-height: var(
|
||||
@@ -75,6 +111,7 @@ export class HaButton extends Button {
|
||||
var(--button-height, 32px)
|
||||
);
|
||||
font-size: var(--wa-font-size-s, var(--ha-font-size-m));
|
||||
--wa-form-control-padding-inline: 12px;
|
||||
}
|
||||
|
||||
:host([variant="brand"]) {
|
||||
@@ -154,6 +191,7 @@ export class HaButton extends Button {
|
||||
|
||||
:host([appearance~="plain"]) .button {
|
||||
color: var(--wa-color-on-normal);
|
||||
background-color: transparent;
|
||||
}
|
||||
:host([appearance~="plain"]) .button.disabled {
|
||||
background-color: transparent;
|
||||
@@ -179,6 +217,11 @@ export class HaButton extends Button {
|
||||
color: var(--wa-color-on-normal);
|
||||
}
|
||||
}
|
||||
:host([appearance~="filled"]) .button {
|
||||
color: var(--wa-color-on-normal);
|
||||
background-color: var(--wa-color-fill-normal);
|
||||
border-color: transparent;
|
||||
}
|
||||
:host([appearance~="filled"])
|
||||
.button:not(.disabled):not(.loading):active {
|
||||
background-color: var(--button-color-fill-normal-active);
|
||||
@@ -210,6 +253,20 @@ export class HaButton extends Button {
|
||||
.button.disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
slot[name="start"]::slotted(*) {
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
slot[name="end"]::slotted(*) {
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
|
||||
.button.has-start {
|
||||
padding-left: 8px;
|
||||
}
|
||||
.button.has-end {
|
||||
padding-right: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
58
src/components/ha-code-editor-completion-items.ts
Normal file
58
src/components/ha-code-editor-completion-items.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
export interface CompletionItem {
|
||||
label: string;
|
||||
value: string;
|
||||
subValue?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-code-editor-completion-items")
|
||||
export class HaCodeEditorCompletionItems extends LitElement {
|
||||
@property({ attribute: false }) public items: CompletionItem[] = [];
|
||||
|
||||
render() {
|
||||
return this.items.map(
|
||||
(item) => html`
|
||||
<span><strong>${item.label}</strong>:</span>
|
||||
<span
|
||||
>${item.value}${item.subValue && item.subValue.length > 0
|
||||
? // prettier-ignore
|
||||
html` (<pre>${item.subValue}</pre>)`
|
||||
: nothing}</span
|
||||
>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 6px;
|
||||
white-space: pre-wrap;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-flow: wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0 3px;
|
||||
padding: 3px;
|
||||
background-color: var(--markdown-code-background-color, none);
|
||||
border-radius: var(--ha-border-radius-sm, 4px);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-code-editor-completion-items": HaCodeEditorCompletionItems;
|
||||
}
|
||||
}
|
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
Completion,
|
||||
CompletionContext,
|
||||
CompletionInfo,
|
||||
CompletionResult,
|
||||
CompletionSource,
|
||||
} from "@codemirror/autocomplete";
|
||||
@@ -9,14 +10,17 @@ import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
|
||||
import { mdiArrowExpand, mdiArrowCollapse } from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, ReactiveElement } from "lit";
|
||||
import { css, ReactiveElement, html, render } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { getEntityContext } from "../common/entity/context/get_entity_context";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { CompletionItem } from "./ha-code-editor-completion-items";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-code-editor-completion-items";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -324,15 +328,72 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}
|
||||
};
|
||||
|
||||
private _renderInfo = (completion: Completion): CompletionInfo => {
|
||||
const key = completion.label;
|
||||
const context = getEntityContext(this.hass!.states[key], this.hass!);
|
||||
|
||||
const completionInfo = document.createElement("div");
|
||||
completionInfo.classList.add("completion-info");
|
||||
|
||||
const formattedState = this.hass!.formatEntityState(this.hass!.states[key]);
|
||||
|
||||
const completionItems: CompletionItem[] = [
|
||||
{
|
||||
label: this.hass!.localize(
|
||||
"ui.components.entity.entity-state-picker.state"
|
||||
),
|
||||
value: formattedState,
|
||||
subValue:
|
||||
// If the state exactly matches the formatted state, don't show the raw state
|
||||
this.hass!.states[key].state === formattedState
|
||||
? undefined
|
||||
: this.hass!.states[key].state,
|
||||
},
|
||||
];
|
||||
|
||||
if (context.device && context.device.name) {
|
||||
completionItems.push({
|
||||
label: this.hass!.localize("ui.components.device-picker.device"),
|
||||
value: context.device.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (context.area && context.area.name) {
|
||||
completionItems.push({
|
||||
label: this.hass!.localize("ui.components.area-picker.area"),
|
||||
value: context.area.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (context.floor && context.floor.name) {
|
||||
completionItems.push({
|
||||
label: this.hass!.localize("ui.components.floor-picker.floor"),
|
||||
value: context.floor.name,
|
||||
});
|
||||
}
|
||||
|
||||
render(
|
||||
html`
|
||||
<ha-code-editor-completion-items
|
||||
.items=${completionItems}
|
||||
></ha-code-editor-completion-items>
|
||||
`,
|
||||
completionInfo
|
||||
);
|
||||
|
||||
return completionInfo;
|
||||
};
|
||||
|
||||
private _getStates = memoizeOne((states: HassEntities): Completion[] => {
|
||||
if (!states) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const options = Object.keys(states).map((key) => ({
|
||||
type: "variable",
|
||||
label: key,
|
||||
detail: states[key].attributes.friendly_name,
|
||||
info: `State: ${states[key].state}`,
|
||||
info: this._renderInfo,
|
||||
}));
|
||||
|
||||
return options;
|
||||
@@ -615,6 +676,20 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
top: calc(var(--safe-area-inset-top, 0px) + 8px);
|
||||
right: calc(var(--safe-area-inset-right, 0px) + 8px);
|
||||
}
|
||||
|
||||
.completion-info {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Hide completion info on narrow screens */
|
||||
@media (max-width: 600px) {
|
||||
.cm-completionInfo,
|
||||
.completion-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -4,14 +4,14 @@ import { HaTextField } from "./ha-textfield";
|
||||
|
||||
@customElement("ha-combo-box-textfield")
|
||||
export class HaComboBoxTextField extends HaTextField {
|
||||
@property({ type: Boolean, attribute: "disable-set-value" })
|
||||
public disableSetValue = false;
|
||||
@property({ type: Boolean, attribute: "force-blank-value" })
|
||||
public forceBlankValue = false;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("value")) {
|
||||
if (this.disableSetValue) {
|
||||
this.value = changedProps.get("value") as string;
|
||||
if (changedProps.has("value") || changedProps.has("forceBlankValue")) {
|
||||
if (this.forceBlankValue && this.value) {
|
||||
this.value = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -117,7 +117,7 @@ export class HaComboBox extends LitElement {
|
||||
|
||||
@query("ha-combo-box-textfield", true) private _inputElement!: HaTextField;
|
||||
|
||||
@state({ type: Boolean }) private _disableSetValue = false;
|
||||
@state({ type: Boolean }) private _forceBlankValue = false;
|
||||
|
||||
private _overlayMutationObserver?: MutationObserver;
|
||||
|
||||
@@ -188,7 +188,7 @@ export class HaComboBox extends LitElement {
|
||||
class="input"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
.autocorrect=${false}
|
||||
input-spellcheck="false"
|
||||
.suffix=${html`<div
|
||||
style="width: 28px;"
|
||||
@@ -196,7 +196,7 @@ export class HaComboBox extends LitElement {
|
||||
></div>`}
|
||||
.icon=${this.icon}
|
||||
.invalid=${this.invalid}
|
||||
.disableSetValue=${this._disableSetValue}
|
||||
.forceBlankValue=${this._forceBlankValue}
|
||||
>
|
||||
<slot name="icon" slot="leadingIcon"></slot>
|
||||
</ha-combo-box-textfield>
|
||||
@@ -207,6 +207,7 @@ export class HaComboBox extends LitElement {
|
||||
aria-label=${ifDefined(this.hass?.localize("ui.common.clear"))}
|
||||
class=${`clear-button ${this.label ? "" : "no-label"}`}
|
||||
.path=${mdiClose}
|
||||
?disabled=${this.disabled}
|
||||
@click=${this._clearValue}
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
@@ -269,10 +270,10 @@ export class HaComboBox extends LitElement {
|
||||
if (opened) {
|
||||
// Wait 100ms to be sure vaddin-combo-box-light already tried to set the value
|
||||
setTimeout(() => {
|
||||
this._disableSetValue = false;
|
||||
this._forceBlankValue = false;
|
||||
}, 100);
|
||||
} else {
|
||||
this._disableSetValue = true;
|
||||
this._forceBlankValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,7 +394,8 @@ export class HaComboBox extends LitElement {
|
||||
:host([opened]) .toggle-button {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.toggle-button[disabled] {
|
||||
.toggle-button[disabled],
|
||||
.clear-button[disabled] {
|
||||
color: var(--disabled-text-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
@@ -506,7 +506,7 @@ export class HaControlSlider extends LitElement {
|
||||
width: 100%;
|
||||
}
|
||||
.slider .slider-track-bar {
|
||||
--border-radius: var(--control-slider-border-radius);
|
||||
--ha-border-radius: var(--control-slider-border-radius);
|
||||
--slider-size: 100%;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
|
@@ -254,21 +254,37 @@ export class HaDateRangePicker extends LitElement {
|
||||
}
|
||||
|
||||
private _applyDateRange() {
|
||||
if (this.hass.locale.time_zone === TimeZone.server) {
|
||||
const dateRangePicker = this._dateRangePicker;
|
||||
let start = new Date(this._dateRangePicker.start);
|
||||
let end = new Date(this._dateRangePicker.end);
|
||||
|
||||
const startDate = fromZonedTime(
|
||||
dateRangePicker.start,
|
||||
this.hass.config.time_zone
|
||||
);
|
||||
const endDate = fromZonedTime(
|
||||
dateRangePicker.end,
|
||||
this.hass.config.time_zone
|
||||
);
|
||||
if (this.timePicker) {
|
||||
start.setSeconds(0);
|
||||
start.setMilliseconds(0);
|
||||
end.setSeconds(0);
|
||||
end.setMilliseconds(0);
|
||||
|
||||
dateRangePicker.clickRange([startDate, endDate]);
|
||||
if (
|
||||
end.getHours() === 0 &&
|
||||
end.getMinutes() === 0 &&
|
||||
start.getFullYear() === end.getFullYear() &&
|
||||
start.getMonth() === end.getMonth() &&
|
||||
start.getDate() === end.getDate()
|
||||
) {
|
||||
end.setDate(end.getDate() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hass.locale.time_zone === TimeZone.server) {
|
||||
start = fromZonedTime(start, this.hass.config.time_zone);
|
||||
end = fromZonedTime(end, this.hass.config.time_zone);
|
||||
}
|
||||
|
||||
if (
|
||||
start.getTime() !== this._dateRangePicker.start.getTime() ||
|
||||
end.getTime() !== this._dateRangePicker.end.getTime()
|
||||
) {
|
||||
this._dateRangePicker.clickRange([start, end]);
|
||||
}
|
||||
this._dateRangePicker.clickedApply();
|
||||
}
|
||||
|
||||
|
@@ -148,6 +148,10 @@ export class HaDialog extends DialogBase {
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
margin-right: 12px;
|
||||
margin-inline-end: 12px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.header_button {
|
||||
text-decoration: none;
|
||||
|
@@ -23,13 +23,13 @@ export class HaFab extends FabBase {
|
||||
:host .mdc-fab--extended {
|
||||
border-radius: var(
|
||||
--ha-button-border-radius,
|
||||
var(--border-radius-pill)
|
||||
var(--ha-border-radius-pill)
|
||||
);
|
||||
}
|
||||
:host .mdc-fab.mdc-fab--extended .ripple {
|
||||
border-radius: var(
|
||||
--ha-button-border-radius,
|
||||
var(--border-radius-pill)
|
||||
var(--ha-border-radius-pill)
|
||||
);
|
||||
}
|
||||
:host .mdc-fab--extended .mdc-fab__icon {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import SlAnimation from "@shoelace-style/shoelace/dist/components/animation/animation.component";
|
||||
import WaAnimation from "@awesome.me/webawesome/dist/components/animation/animation";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-fade-in")
|
||||
export class HaFadeIn extends SlAnimation {
|
||||
export class HaFadeIn extends WaAnimation {
|
||||
@property() public name = "fadeIn";
|
||||
|
||||
@property() public fill: FillMode = "both";
|
||||
|
@@ -329,9 +329,8 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
}
|
||||
.subdir {
|
||||
margin-inline-end: 8px;
|
||||
opacity: .6;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -6,8 +6,9 @@ import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { IntegrationManifest } from "../data/integration";
|
||||
import { fetchIntegrationManifests } from "../data/integration";
|
||||
import { fetchIntegrationManifests, domainToName } from "../data/integration";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
@@ -63,7 +64,12 @@ export class HaFilterIntegrations extends LitElement {
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._integrations(this._manifests, this._filter, this.value),
|
||||
this._integrations(
|
||||
this.hass.localize,
|
||||
this._manifests,
|
||||
this._filter,
|
||||
this.value
|
||||
),
|
||||
(i) => i.domain,
|
||||
(integration) =>
|
||||
html`<ha-check-list-item
|
||||
@@ -79,7 +85,7 @@ export class HaFilterIntegrations extends LitElement {
|
||||
.domain=${integration.domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
${integration.name || integration.domain}
|
||||
${integration.name}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</ha-list> `
|
||||
@@ -108,11 +114,21 @@ export class HaFilterIntegrations extends LitElement {
|
||||
|
||||
protected async firstUpdated() {
|
||||
this._manifests = await fetchIntegrationManifests(this.hass);
|
||||
this.hass.loadBackendTranslation("title");
|
||||
}
|
||||
|
||||
private _integrations = memoizeOne(
|
||||
(manifest: IntegrationManifest[], filter: string | undefined, _value) =>
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
manifest: IntegrationManifest[],
|
||||
filter: string | undefined,
|
||||
_value
|
||||
) =>
|
||||
manifest
|
||||
.map((mnfst) => ({
|
||||
...mnfst,
|
||||
name: domainToName(localize, mnfst.domain, mnfst),
|
||||
}))
|
||||
.filter(
|
||||
(mnfst) =>
|
||||
(!mnfst.integration_type ||
|
||||
@@ -124,11 +140,7 @@ export class HaFilterIntegrations extends LitElement {
|
||||
mnfst.domain.toLowerCase().includes(filter))
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
a.name || a.domain,
|
||||
b.name || b.domain,
|
||||
this.hass.locale.language
|
||||
)
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
)
|
||||
);
|
||||
|
||||
|
@@ -106,6 +106,8 @@ export const computeInitialHaFormData = (
|
||||
data[field.name] = [];
|
||||
} else if ("media" in selector || "target" in selector) {
|
||||
data[field.name] = {};
|
||||
} else if ("state" in selector) {
|
||||
data[field.name] = selector.state?.multiple ? [] : "";
|
||||
} else {
|
||||
throw new Error(
|
||||
`Selector ${Object.keys(selector)[0]} not supported in initial form data`
|
||||
|
@@ -16,9 +16,23 @@ export class HaMdButtonMenu extends LitElement {
|
||||
|
||||
@property() public positioning?: "fixed" | "absolute" | "popover";
|
||||
|
||||
@property({ attribute: "anchor-corner" }) public anchorCorner:
|
||||
| "start-start"
|
||||
| "start-end"
|
||||
| "end-start"
|
||||
| "end-end" = "end-start";
|
||||
|
||||
@property({ attribute: "menu-corner" }) public menuCorner:
|
||||
| "start-start"
|
||||
| "start-end"
|
||||
| "end-start"
|
||||
| "end-end" = "start-start";
|
||||
|
||||
@property({ type: Boolean, attribute: "has-overflow" }) public hasOverflow =
|
||||
false;
|
||||
|
||||
@property({ type: Boolean }) public quick = false;
|
||||
|
||||
@query("ha-md-menu", true) private _menu!: HaMdMenu;
|
||||
|
||||
public get items() {
|
||||
@@ -39,8 +53,11 @@ export class HaMdButtonMenu extends LitElement {
|
||||
<slot name="trigger" @slotchange=${this._setTriggerAria}></slot>
|
||||
</div>
|
||||
<ha-md-menu
|
||||
.quick=${this.quick}
|
||||
.positioning=${this.positioning}
|
||||
.hasOverflow=${this.hasOverflow}
|
||||
.anchorCorner=${this.anchorCorner}
|
||||
.menuCorner=${this.menuCorner}
|
||||
@opening=${this._handleOpening}
|
||||
@closing=${this._handleClosing}
|
||||
>
|
||||
|
@@ -7,8 +7,8 @@ import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-textfield";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@customElement("ha-multi-textfield")
|
||||
@@ -79,6 +79,7 @@ class HaMultiTextField extends LitElement {
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.addLabel ??
|
||||
(this.label
|
||||
? this.hass?.localize("ui.components.multi-textfield.add_item", {
|
||||
@@ -86,7 +87,6 @@ class HaMultiTextField extends LitElement {
|
||||
})
|
||||
: this.hass?.localize("ui.common.add")) ??
|
||||
"Add"}
|
||||
<ha-svg-icon slot="end" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-button>
|
||||
</div>
|
||||
${this.helper
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { mdiStar } from "@mdi/js";
|
||||
import { mdiInformationOutline, mdiStar } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -71,6 +71,17 @@ export class HaNetwork extends LitElement {
|
||||
<span slot="description" data-for="auto_configure">
|
||||
${this.hass.localize("ui.panel.config.network.adapter.detected")}:
|
||||
${format_auto_detected_interfaces(this.networkConfig.adapters)}
|
||||
${!configured_adapters.length
|
||||
? html`<div class="info-text">
|
||||
<ha-svg-icon
|
||||
.path=${mdiInformationOutline}
|
||||
class="info-icon"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.network.adapter.auto_configure_manual_hint"
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
${configured_adapters.length || this._expanded
|
||||
@@ -171,6 +182,21 @@ export class HaNetwork extends LitElement {
|
||||
span[slot="description"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--info-color, var(--primary-color));
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -28,7 +28,7 @@ export class HaPasswordField extends LitElement {
|
||||
|
||||
@property() public autocomplete?: string;
|
||||
|
||||
@property() public autocorrect?: string;
|
||||
@property({ type: Boolean }) public autocorrect = true;
|
||||
|
||||
@property({ attribute: "input-spellcheck" })
|
||||
public inputSpellcheck?: string;
|
||||
|
@@ -1,12 +1,21 @@
|
||||
import ProgressRing from "@shoelace-style/shoelace/dist/components/progress-ring/progress-ring.component";
|
||||
import progressRingStyles from "@shoelace-style/shoelace/dist/components/progress-ring/progress-ring.styles";
|
||||
import ProgressRing from "@awesome.me/webawesome/dist/components/progress-ring/progress-ring";
|
||||
import { css } from "lit";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { StateSet } from "../resources/polyfills/stateset";
|
||||
|
||||
@customElement("ha-progress-ring")
|
||||
export class HaProgressRing extends ProgressRing {
|
||||
@property() public size?: "tiny" | "small" | "medium" | "large";
|
||||
|
||||
attachInternals() {
|
||||
const internals = super.attachInternals();
|
||||
Object.defineProperty(internals, "states", {
|
||||
value: new StateSet(this, internals.states),
|
||||
});
|
||||
return internals;
|
||||
}
|
||||
|
||||
public updated(changedProps) {
|
||||
super.updated(changedProps);
|
||||
|
||||
@@ -31,24 +40,26 @@ export class HaProgressRing extends ProgressRing {
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
progressRingStyles,
|
||||
css`
|
||||
:host {
|
||||
--indicator-color: var(
|
||||
--ha-progress-ring-indicator-color,
|
||||
var(--primary-color)
|
||||
);
|
||||
--track-color: var(
|
||||
--ha-progress-ring-divider-color,
|
||||
var(--divider-color)
|
||||
);
|
||||
--track-width: 4px;
|
||||
--speed: 3.5s;
|
||||
--size: var(--ha-progress-ring-size, 48px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
ProgressRing.styles,
|
||||
css`
|
||||
:host {
|
||||
--indicator-color: var(
|
||||
--ha-progress-ring-indicator-color,
|
||||
var(--primary-color)
|
||||
);
|
||||
--track-color: var(
|
||||
--ha-progress-ring-divider-color,
|
||||
var(--divider-color)
|
||||
);
|
||||
--track-width: 4px;
|
||||
--speed: 3.5s;
|
||||
--size: var(--ha-progress-ring-size, 48px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -93,7 +93,7 @@ class HaPushNotificationsToggle extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
let applicationServerKey: Uint8Array | null;
|
||||
let applicationServerKey: Uint8Array<ArrayBuffer> | null;
|
||||
try {
|
||||
applicationServerKey = await getAppKey(this.hass);
|
||||
} catch (_err) {
|
||||
|
152
src/components/ha-segmented-bar.ts
Normal file
152
src/components/ha-segmented-bar.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import "./ha-tooltip";
|
||||
|
||||
export interface Segment {
|
||||
value: number;
|
||||
color: string;
|
||||
label?: TemplateResult | string;
|
||||
}
|
||||
|
||||
@customElement("ha-segmented-bar")
|
||||
class HaSegmentedBar extends LitElement {
|
||||
@property({ attribute: false }) public segments!: Segment[];
|
||||
|
||||
@property({ type: String }) public heading!: string;
|
||||
|
||||
@property({ type: String }) public description?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "hide-legend" })
|
||||
public hideLegend = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "hide-tooltip" })
|
||||
public hideTooltip = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const totalValue = this.segments.reduce(
|
||||
(acc, segment) => acc + segment.value,
|
||||
0
|
||||
);
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="heading">
|
||||
<div class="title">
|
||||
<span>${this.heading}</span>
|
||||
<span>${this.description}</span>
|
||||
</div>
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
<div class="bar">
|
||||
${this.segments.map((segment) => {
|
||||
const bar = html`<div
|
||||
style=${styleMap({
|
||||
width: `${(segment.value / totalValue) * 100}%`,
|
||||
backgroundColor: segment.color,
|
||||
})}
|
||||
></div>`;
|
||||
return this.hideTooltip && !segment.label
|
||||
? bar
|
||||
: html`
|
||||
<ha-tooltip>
|
||||
<span slot="content">${segment.label}</span>
|
||||
${bar}
|
||||
</ha-tooltip>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
${this.hideLegend
|
||||
? nothing
|
||||
: html`
|
||||
<ul class="legend">
|
||||
${this.segments.map((segment) =>
|
||||
segment.label
|
||||
? html`
|
||||
<li>
|
||||
<div
|
||||
class="bullet"
|
||||
style=${styleMap({
|
||||
backgroundColor: segment.color,
|
||||
})}
|
||||
></div>
|
||||
<span class="label">${segment.label}</span>
|
||||
</li>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</ul>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
.heading {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
.heading .title {
|
||||
flex: 1;
|
||||
}
|
||||
.heading .title span {
|
||||
color: var(--secondary-text-color);
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
margin-right: 8px;
|
||||
}
|
||||
.heading .title span:first-child {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.bar {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border-radius: var(--ha-bar-border-radius, 4px);
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
margin: 2px 0;
|
||||
background-color: var(
|
||||
--ha-bar-background-color,
|
||||
var(--secondary-background-color)
|
||||
);
|
||||
}
|
||||
.bar div {
|
||||
height: 100%;
|
||||
}
|
||||
.bar div:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin: 12px 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.legend li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
.legend li .bullet {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-segmented-bar": HaSegmentedBar;
|
||||
}
|
||||
}
|
@@ -1,19 +1,20 @@
|
||||
import { ContextProvider, consume } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume, ContextProvider } from "@lit/context";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import type { Action } from "../../data/script";
|
||||
import { migrateAutomationAction } from "../../data/script";
|
||||
import type { ActionSelector } from "../../data/selector";
|
||||
import "../../panels/config/automation/action/ha-automation-action";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
subscribeEntityRegistry,
|
||||
type EntityRegistryEntry,
|
||||
} from "../../data/entity_registry";
|
||||
import type { Action } from "../../data/script";
|
||||
import { migrateAutomationAction } from "../../data/script";
|
||||
import type { ActionSelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import "../../panels/config/automation/action/ha-automation-action";
|
||||
import type HaAutomationAction from "../../panels/config/automation/action/ha-automation-action";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-selector-action")
|
||||
export class HaActionSelector extends SubscribeMixin(LitElement) {
|
||||
@@ -35,6 +36,9 @@ export class HaActionSelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _entitiesContext;
|
||||
|
||||
@query("ha-automation-action")
|
||||
private _actionElement?: HaAutomationAction;
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_entitiesContext"];
|
||||
|
||||
private _actions = memoizeOne((action: Action | undefined) => {
|
||||
@@ -61,6 +65,14 @@ export class HaActionSelector extends SubscribeMixin(LitElement) {
|
||||
];
|
||||
}
|
||||
|
||||
public expandAll() {
|
||||
this._actionElement?.expandAll();
|
||||
}
|
||||
|
||||
public collapseAll() {
|
||||
this._actionElement?.collapseAll();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
@@ -69,6 +81,7 @@ export class HaActionSelector extends SubscribeMixin(LitElement) {
|
||||
.actions=${this._actions(this.value)}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.optionsInSidebar=${!!this.selector.action?.optionsInSidebar}
|
||||
></ha-automation-action>
|
||||
`;
|
||||
}
|
||||
@@ -76,12 +89,12 @@ export class HaActionSelector extends SubscribeMixin(LitElement) {
|
||||
static styles = css`
|
||||
ha-automation-action {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { Condition } from "../../data/automation";
|
||||
import type { ConditionSelector } from "../../data/selector";
|
||||
import "../../panels/config/automation/condition/ha-automation-condition";
|
||||
import type HaAutomationCondition from "../../panels/config/automation/condition/ha-automation-condition";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-selector-condition")
|
||||
@@ -19,6 +20,9 @@ export class HaConditionSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@query("ha-automation-condition")
|
||||
private _conditionElement?: HaAutomationCondition;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
@@ -27,10 +31,19 @@ export class HaConditionSelector extends LitElement {
|
||||
.conditions=${this.value || []}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.optionsInSidebar=${!!this.selector.condition?.optionsInSidebar}
|
||||
></ha-automation-condition>
|
||||
`;
|
||||
}
|
||||
|
||||
public expandAll() {
|
||||
this._conditionElement?.expandAll();
|
||||
}
|
||||
|
||||
public collapseAll() {
|
||||
this._conditionElement?.collapseAll();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-automation-condition {
|
||||
display: block;
|
||||
@@ -40,6 +53,7 @@ export class HaConditionSelector extends LitElement {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ import "../ha-alert";
|
||||
import "../ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../ha-form/types";
|
||||
import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
|
||||
const MANUAL_SCHEMA = [
|
||||
{ name: "media_content_id", required: false, selector: { text: {} } },
|
||||
@@ -44,9 +45,19 @@ export class HaMediaSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public required = true;
|
||||
|
||||
@property({ attribute: false }) public context?: {
|
||||
filter_entity?: string | string[];
|
||||
};
|
||||
|
||||
@state() private _thumbnailUrl?: string | null;
|
||||
|
||||
private _contextEntities: string[] | undefined;
|
||||
|
||||
willUpdate(changedProps: PropertyValues<this>) {
|
||||
if (changedProps.has("context")) {
|
||||
this._contextEntities = ensureArray(this.context?.filter_entity);
|
||||
}
|
||||
|
||||
if (changedProps.has("value")) {
|
||||
const thumbnail = this.value?.metadata?.thumbnail;
|
||||
const oldThumbnail = (changedProps.get("value") as this["value"])
|
||||
@@ -79,24 +90,25 @@ export class HaMediaSelector extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const stateObj = this.value?.entity_id
|
||||
? this.hass.states[this.value.entity_id]
|
||||
: undefined;
|
||||
const entityId = this._getActiveEntityId();
|
||||
|
||||
const stateObj = entityId ? this.hass.states[entityId] : undefined;
|
||||
|
||||
const supportsBrowse =
|
||||
!this.value?.entity_id ||
|
||||
!entityId ||
|
||||
(stateObj &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
|
||||
|
||||
const hasAccept = this.selector?.media?.accept?.length;
|
||||
|
||||
return html`
|
||||
${hasAccept
|
||||
${hasAccept ||
|
||||
(this._contextEntities && this._contextEntities.length <= 1)
|
||||
? nothing
|
||||
: html`
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.entity_id}
|
||||
.value=${entityId}
|
||||
.label=${this.label ||
|
||||
this.hass.localize(
|
||||
"ui.components.selectors.media.pick_media_player"
|
||||
@@ -104,8 +116,10 @@ export class HaMediaSelector extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
.required=${this.required}
|
||||
.hideClearIcon=${!!this._contextEntities}
|
||||
.includeDomains=${INCLUDE_DOMAINS}
|
||||
allow-custom-entity
|
||||
.includeEntities=${this._contextEntities}
|
||||
.allowCustomEntity=${!this._contextEntities}
|
||||
@value-changed=${this._entityChanged}
|
||||
></ha-entity-picker>
|
||||
`}
|
||||
@@ -121,6 +135,7 @@ export class HaMediaSelector extends LitElement {
|
||||
.data=${this.value || EMPTY_FORM}
|
||||
.schema=${MANUAL_SCHEMA}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
></ha-form>
|
||||
`
|
||||
: html`
|
||||
@@ -133,7 +148,7 @@ export class HaMediaSelector extends LitElement {
|
||||
: this.value.metadata?.title || this.value.media_content_id}
|
||||
@click=${this._pickMedia}
|
||||
@keydown=${this._handleKeyDown}
|
||||
class=${this.disabled || (!this.value?.entity_id && !hasAccept)
|
||||
class=${this.disabled || (!entityId && !hasAccept)
|
||||
? "disabled"
|
||||
: ""}
|
||||
>
|
||||
@@ -193,21 +208,38 @@ export class HaMediaSelector extends LitElement {
|
||||
): string =>
|
||||
this.hass.localize(`ui.components.selectors.media.${schema.name}`);
|
||||
|
||||
private _computeHelperCallback = (
|
||||
schema: SchemaUnion<typeof MANUAL_SCHEMA>
|
||||
): string =>
|
||||
this.hass.localize(`ui.components.selectors.media.${schema.name}_detail`);
|
||||
|
||||
private _entityChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
entity_id: ev.detail.value,
|
||||
media_content_id: "",
|
||||
media_content_type: "",
|
||||
},
|
||||
});
|
||||
if (this.context?.filter_entity) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
media_content_id: "",
|
||||
media_content_type: "",
|
||||
metadata: {
|
||||
browse_entity_id: ev.detail.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
entity_id: ev.detail.value,
|
||||
media_content_id: "",
|
||||
media_content_type: "",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _pickMedia() {
|
||||
showMediaBrowserDialog(this, {
|
||||
action: "pick",
|
||||
entityId: this.value?.entity_id,
|
||||
entityId: this._getActiveEntityId(),
|
||||
navigateIds: this.value?.metadata?.navigateIds,
|
||||
accept: this.selector.media?.accept,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
|
||||
@@ -225,6 +257,9 @@ export class HaMediaSelector extends LitElement {
|
||||
media_content_type: id.media_content_type,
|
||||
media_content_id: id.media_content_id,
|
||||
})),
|
||||
...(this.context?.filter_entity
|
||||
? { browse_entity_id: this._getActiveEntityId() }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -232,6 +267,15 @@ export class HaMediaSelector extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _getActiveEntityId(): string | undefined {
|
||||
const metaId = this.value?.metadata?.browse_entity_id;
|
||||
return (
|
||||
this.value?.entity_id ||
|
||||
(metaId && this._contextEntities?.includes(metaId) && metaId) ||
|
||||
this._contextEntities?.[0]
|
||||
);
|
||||
}
|
||||
|
||||
private _handleKeyDown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
|
@@ -8,7 +8,6 @@ import type {
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-alert";
|
||||
import "../ha-form/ha-form";
|
||||
|
||||
const SELECTOR_DEFAULTS = {
|
||||
@@ -121,6 +120,10 @@ const SELECTOR_SCHEMAS = {
|
||||
name: "entity_id",
|
||||
selector: { entity: {} },
|
||||
},
|
||||
{
|
||||
name: "multiple",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const,
|
||||
target: [] as const,
|
||||
template: [] as const,
|
||||
@@ -152,6 +155,8 @@ export class HaSelectorSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public required = true;
|
||||
|
||||
private _yamlMode = false;
|
||||
@@ -168,10 +173,10 @@ export class HaSelectorSelector extends LitElement {
|
||||
[
|
||||
{
|
||||
name: "type",
|
||||
required: true,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
required: true,
|
||||
options: Object.keys(SELECTOR_SCHEMAS)
|
||||
.concat("manual")
|
||||
.map((key) => ({
|
||||
@@ -224,17 +229,17 @@ export class HaSelectorSelector extends LitElement {
|
||||
|
||||
const schema = this._schema(type, this.hass.localize);
|
||||
|
||||
return html`<ha-card>
|
||||
<div class="card-content">
|
||||
<p>${this.label ? this.label : ""}</p>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form></div
|
||||
></ha-card>`;
|
||||
return html`<div>
|
||||
<p>${this.label ? this.label : ""}</p>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
.narrow=${this.narrow}
|
||||
></ha-form>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
@@ -281,23 +286,6 @@ export class HaSelectorSelector extends LitElement {
|
||||
) || schema.name;
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-card {
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
ha-card.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
.card-content {
|
||||
padding: 0px 16px 16px 16px;
|
||||
}
|
||||
.title {
|
||||
font-size: var(--ha-font-size-l);
|
||||
padding-top: 16px;
|
||||
|
@@ -4,6 +4,7 @@ import type { StateSelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-state-picker";
|
||||
import "../entity/ha-entity-states-picker";
|
||||
|
||||
@customElement("ha-selector-state")
|
||||
export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
@@ -27,6 +28,25 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (this.selector.state?.multiple) {
|
||||
return html`
|
||||
<ha-entity-states-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.selector.state?.entity_id ||
|
||||
this.context?.filter_entity}
|
||||
.attribute=${this.selector.state?.attribute ||
|
||||
this.context?.filter_attribute}
|
||||
.extraOptions=${this.selector.state?.extra_options}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
allow-custom-value
|
||||
.hideStates=${this.selector.state?.hide_states}
|
||||
></ha-entity-states-picker>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<ha-entity-state-picker
|
||||
.hass=${this.hass}
|
||||
|
@@ -21,9 +21,15 @@ declare global {
|
||||
};
|
||||
"drag-start": undefined;
|
||||
"drag-end": undefined;
|
||||
"item-cloned": HaSortableClonedEventData;
|
||||
}
|
||||
}
|
||||
|
||||
export interface HaSortableClonedEventData {
|
||||
item: any;
|
||||
clone: any;
|
||||
}
|
||||
|
||||
export type HaSortableOptions = Omit<
|
||||
SortableInstance.SortableOptions,
|
||||
"onStart" | "onChoose" | "onEnd" | "onUpdate" | "onAdd" | "onRemove"
|
||||
@@ -148,6 +154,7 @@ export class HaSortable extends LitElement {
|
||||
onUpdate: this._handleUpdate,
|
||||
onAdd: this._handleAdd,
|
||||
onRemove: this._handleRemove,
|
||||
onClone: this._handleClone,
|
||||
};
|
||||
|
||||
if (this.draggableSelector) {
|
||||
@@ -187,6 +194,10 @@ export class HaSortable extends LitElement {
|
||||
fireEvent(this, "item-removed", { index: evt.oldIndex });
|
||||
};
|
||||
|
||||
private _handleClone = (evt) => {
|
||||
fireEvent(this, "item-cloned", evt);
|
||||
};
|
||||
|
||||
private _handleEnd = async (evt) => {
|
||||
fireEvent(this, "drag-end");
|
||||
// put back in original location
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import Spinner from "@shoelace-style/shoelace/dist/components/spinner/spinner.component";
|
||||
import spinnerStyles from "@shoelace-style/shoelace/dist/components/spinner/spinner.styles";
|
||||
import type { PropertyValues } from "lit";
|
||||
import Spinner from "@awesome.me/webawesome/dist/components/spinner/spinner";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
import { StateSet } from "../resources/polyfills/stateset";
|
||||
|
||||
@customElement("ha-spinner")
|
||||
export class HaSpinner extends Spinner {
|
||||
@property() public size?: "tiny" | "small" | "medium" | "large";
|
||||
@@ -32,21 +33,31 @@ export class HaSpinner extends Spinner {
|
||||
}
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
spinnerStyles,
|
||||
css`
|
||||
:host {
|
||||
--indicator-color: var(
|
||||
--ha-spinner-indicator-color,
|
||||
var(--primary-color)
|
||||
);
|
||||
--track-color: var(--ha-spinner-divider-color, var(--divider-color));
|
||||
--track-width: 4px;
|
||||
--speed: 3.5s;
|
||||
font-size: var(--ha-spinner-size, 48px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
attachInternals() {
|
||||
const internals = super.attachInternals();
|
||||
Object.defineProperty(internals, "states", {
|
||||
value: new StateSet(this, internals.states),
|
||||
});
|
||||
return internals;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
Spinner.styles,
|
||||
css`
|
||||
:host {
|
||||
--indicator-color: var(
|
||||
--ha-spinner-indicator-color,
|
||||
var(--primary-color)
|
||||
);
|
||||
--track-color: var(--ha-spinner-divider-color, var(--divider-color));
|
||||
--track-width: 4px;
|
||||
--speed: 3.5s;
|
||||
font-size: var(--ha-spinner-size, 48px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -20,7 +20,7 @@ export class HaTextField extends TextFieldBase {
|
||||
|
||||
@property() public autocomplete?: string;
|
||||
|
||||
@property() public autocorrect?: string;
|
||||
@property({ type: Boolean }) public autocorrect = true;
|
||||
|
||||
@property({ attribute: "input-spellcheck" })
|
||||
public inputSpellcheck?: string;
|
||||
@@ -57,8 +57,8 @@ export class HaTextField extends TextFieldBase {
|
||||
}
|
||||
}
|
||||
if (changedProperties.has("autocorrect")) {
|
||||
if (this.autocorrect) {
|
||||
this.formElement.setAttribute("autocorrect", this.autocorrect);
|
||||
if (this.autocorrect === false) {
|
||||
this.formElement.setAttribute("autocorrect", "off");
|
||||
} else {
|
||||
this.formElement.removeAttribute("autocorrect");
|
||||
}
|
||||
|
@@ -211,6 +211,7 @@ export class HaYamlEditor extends LitElement {
|
||||
}
|
||||
ha-code-editor {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -116,7 +116,7 @@ class DialogMediaManage extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<ha-button
|
||||
class="danger"
|
||||
variant="danger"
|
||||
slot="navigationIcon"
|
||||
.disabled=${this._deleting}
|
||||
@click=${this._handleDelete}
|
||||
@@ -327,10 +327,6 @@ class DialogMediaManage extends LitElement {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.danger {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
|
||||
ha-tip {
|
||||
margin: 16px;
|
||||
}
|
||||
|
@@ -38,7 +38,7 @@ class MediaManageButton extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-button appearance="plain" size="small" @click=${this._manage}>
|
||||
<ha-button appearance="filled" size="small" @click=${this._manage}>
|
||||
<ha-svg-icon .path=${mdiFolderEdit} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.manage"
|
||||
|
@@ -18,9 +18,9 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { isUnavailableState } from "../../data/entity";
|
||||
import type {
|
||||
MediaPlayerItem,
|
||||
MediaPickedEvent,
|
||||
MediaPlayerBrowseAction,
|
||||
MediaPlayerItem,
|
||||
MediaPlayerLayoutType,
|
||||
} from "../../data/media-player";
|
||||
import {
|
||||
@@ -32,6 +32,7 @@ import { browseLocalMediaPlayer } from "../../data/media_source";
|
||||
import { isTTSMediaSource } from "../../data/tts";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
brandsUrl,
|
||||
@@ -44,16 +45,15 @@ import "../ha-alert";
|
||||
import "../ha-button";
|
||||
import "../ha-button-menu";
|
||||
import "../ha-card";
|
||||
import "../ha-spinner";
|
||||
import "../ha-fab";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
import "../ha-list";
|
||||
import "../ha-list-item";
|
||||
import "../ha-spinner";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
import "./ha-browse-media-tts";
|
||||
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
|
@@ -30,6 +30,10 @@ export const ACTION_ICONS = {
|
||||
wait_template: mdiCodeBraces,
|
||||
wait_for_trigger: mdiTrafficLight,
|
||||
repeat: mdiRefresh,
|
||||
repeat_count: mdiRefresh,
|
||||
repeat_while: mdiRefresh,
|
||||
repeat_until: mdiRefresh,
|
||||
repeat_for_each: mdiRefresh,
|
||||
choose: mdiArrowDecision,
|
||||
if: mdiCallSplit,
|
||||
device_id: mdiDevices,
|
||||
@@ -57,7 +61,10 @@ export const ACTION_GROUPS: AutomationElementGroup = {
|
||||
delay: {},
|
||||
wait_template: {},
|
||||
wait_for_trigger: {},
|
||||
repeat: {},
|
||||
repeat_count: {},
|
||||
repeat_while: {},
|
||||
repeat_until: {},
|
||||
repeat_for_each: {},
|
||||
choose: {},
|
||||
if: {},
|
||||
stop: {},
|
||||
@@ -83,3 +90,28 @@ export const isService = (key: string | undefined): boolean | undefined =>
|
||||
|
||||
export const getService = (key: string): string =>
|
||||
key.substring(SERVICE_PREFIX.length);
|
||||
|
||||
export const COLLAPSIBLE_ACTION_ELEMENTS = [
|
||||
"ha-automation-action-choose",
|
||||
"ha-automation-action-condition",
|
||||
"ha-automation-action-if",
|
||||
"ha-automation-action-parallel",
|
||||
"ha-automation-action-repeat",
|
||||
"ha-automation-action-sequence",
|
||||
];
|
||||
|
||||
export const ACTION_BUILDING_BLOCKS = [
|
||||
"choose",
|
||||
"if",
|
||||
"parallel",
|
||||
"sequence",
|
||||
"repeat_while",
|
||||
"repeat_until",
|
||||
];
|
||||
|
||||
// Building blocks that have options in the sidebar
|
||||
export const ACTION_COMBINED_BLOCKS = [
|
||||
"repeat_count", // virtual repeat variant
|
||||
"repeat_for_each", // virtual repeat variant
|
||||
"wait_for_trigger",
|
||||
];
|
||||
|
@@ -2,14 +2,15 @@ import type {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
import type { Context, HomeAssistant } from "../types";
|
||||
import type { BlueprintInput } from "./blueprint";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "./condition";
|
||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import type { Action, MODES } from "./script";
|
||||
import type { Action, Field, MODES } from "./script";
|
||||
import { migrateAutomationAction } from "./script";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
|
||||
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
|
||||
export const AUTOMATION_DEFAULT_MAX = 10;
|
||||
@@ -325,7 +326,7 @@ export const expandConditionWithShorthand = (
|
||||
};
|
||||
}
|
||||
|
||||
for (const condition of ["and", "or", "not"]) {
|
||||
for (const condition of CONDITION_BUILDING_BLOCKS) {
|
||||
if (condition in cond) {
|
||||
return {
|
||||
condition,
|
||||
@@ -512,6 +513,14 @@ export const isCondition = (config: unknown): boolean => {
|
||||
return "condition" in condition && typeof condition.condition === "string";
|
||||
};
|
||||
|
||||
export const isScriptField = (config: unknown): boolean => {
|
||||
if (!config || typeof config !== "object") {
|
||||
return false;
|
||||
}
|
||||
const field = config as Record<string, unknown>;
|
||||
return "field" in field && typeof field.field === "object";
|
||||
};
|
||||
|
||||
export const subscribeTrigger = (
|
||||
hass: HomeAssistant,
|
||||
onChange: (result: {
|
||||
@@ -545,3 +554,78 @@ export interface AutomationClipboard {
|
||||
condition?: Condition;
|
||||
action?: Action;
|
||||
}
|
||||
|
||||
export interface BaseSidebarConfig {
|
||||
toggleYamlMode: () => boolean;
|
||||
delete: () => void;
|
||||
close: (focus?: boolean) => void;
|
||||
}
|
||||
|
||||
export interface TriggerSidebarConfig extends BaseSidebarConfig {
|
||||
save: (value: Trigger) => void;
|
||||
rename: () => void;
|
||||
disable: () => void;
|
||||
duplicate: () => void;
|
||||
cut: () => void;
|
||||
copy: () => void;
|
||||
config: Trigger;
|
||||
yamlMode: boolean;
|
||||
uiSupported: boolean;
|
||||
}
|
||||
|
||||
export interface ConditionSidebarConfig extends BaseSidebarConfig {
|
||||
save: (value: Condition) => void;
|
||||
rename: () => void;
|
||||
disable: () => void;
|
||||
test: () => void;
|
||||
duplicate: () => void;
|
||||
cut: () => void;
|
||||
copy: () => void;
|
||||
config: Condition;
|
||||
yamlMode: boolean;
|
||||
uiSupported: boolean;
|
||||
}
|
||||
|
||||
export interface ActionSidebarConfig extends BaseSidebarConfig {
|
||||
save: (value: Action) => void;
|
||||
rename: () => void;
|
||||
disable: () => void;
|
||||
duplicate: () => void;
|
||||
cut: () => void;
|
||||
copy: () => void;
|
||||
run: () => void;
|
||||
config: {
|
||||
action: Action;
|
||||
};
|
||||
yamlMode: boolean;
|
||||
uiSupported: boolean;
|
||||
}
|
||||
|
||||
export interface OptionSidebarConfig extends BaseSidebarConfig {
|
||||
rename: () => void;
|
||||
duplicate: () => void;
|
||||
defaultOption?: boolean;
|
||||
}
|
||||
|
||||
export interface ScriptFieldSidebarConfig extends BaseSidebarConfig {
|
||||
save: (value: Field) => void;
|
||||
config: {
|
||||
field: Field;
|
||||
selector: boolean;
|
||||
key: string;
|
||||
excludeKeys: string[];
|
||||
};
|
||||
yamlMode: boolean;
|
||||
}
|
||||
|
||||
export type SidebarConfig =
|
||||
| TriggerSidebarConfig
|
||||
| ConditionSidebarConfig
|
||||
| ActionSidebarConfig
|
||||
| OptionSidebarConfig
|
||||
| ScriptFieldSidebarConfig;
|
||||
|
||||
export interface ShowAutomationEditorParams {
|
||||
data?: Partial<AutomationConfig>;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
@@ -378,7 +378,17 @@ const tryDescribeTrigger = (
|
||||
|
||||
// Tag Trigger
|
||||
if (trigger.trigger === "tag") {
|
||||
return hass.localize(`${triggerTranslationBaseKey}.tag.description.full`);
|
||||
const entity = Object.values(hass.states).find(
|
||||
(state) =>
|
||||
state.entity_id.startsWith("tag.") &&
|
||||
state.attributes.tag_id === trigger.tag_id
|
||||
);
|
||||
return entity
|
||||
? hass.localize(
|
||||
`${triggerTranslationBaseKey}.tag.description.known_tag`,
|
||||
{ tag_name: computeStateName(entity) }
|
||||
)
|
||||
: hass.localize(`${triggerTranslationBaseKey}.tag.description.full`);
|
||||
}
|
||||
|
||||
// Time Trigger
|
||||
|
@@ -50,3 +50,11 @@ export const CONDITION_GROUPS: AutomationElementGroup = {
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const CONDITION_BUILDING_BLOCKS = ["and", "or", "not"];
|
||||
|
||||
export const COLLAPSIBLE_CONDITION_ELEMENTS = [
|
||||
"ha-automation-condition-and",
|
||||
"ha-automation-condition-not",
|
||||
"ha-automation-condition-or",
|
||||
];
|
||||
|
@@ -42,6 +42,19 @@ export const getSubEntries = (hass: HomeAssistant, entry_id: string) =>
|
||||
entry_id,
|
||||
});
|
||||
|
||||
export const updateSubEntry = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
subentry_id: string,
|
||||
updatedValues: SubEntryMutableParams
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "config_entries/subentries/update",
|
||||
entry_id,
|
||||
subentry_id,
|
||||
...updatedValues,
|
||||
});
|
||||
|
||||
export const deleteSubEntry = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
@@ -60,6 +73,8 @@ export type ConfigEntryMutableParams = Partial<
|
||||
>
|
||||
>;
|
||||
|
||||
export type SubEntryMutableParams = Partial<Pick<SubEntry, "title">>;
|
||||
|
||||
// https://github.com/home-assistant/core/blob/2286dea636fda001f03433ba14d7adbda43979e5/homeassistant/config_entries.py#L81
|
||||
export const ERROR_STATES: ConfigEntry["state"][] = [
|
||||
"migration_error",
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { formatDurationDigital } from "../common/datetime/format_duration";
|
||||
import type { FrontendLocaleData } from "./translation";
|
||||
|
||||
// These attributes are hidden from the more-info window for all entities.
|
||||
export const STATE_ATTRIBUTES = [
|
||||
"entity_id",
|
||||
"assumed_state",
|
||||
@@ -26,6 +27,14 @@ export const STATE_ATTRIBUTES = [
|
||||
"available_tones",
|
||||
];
|
||||
|
||||
// These attributes are hidden from the more-info window for entities of the
|
||||
// matching domain and device_class.
|
||||
export const STATE_ATTRIBUTES_DOMAIN_CLASS = {
|
||||
sensor: {
|
||||
enum: ["options"],
|
||||
},
|
||||
};
|
||||
|
||||
export const TEMPERATURE_ATTRIBUTES = new Set([
|
||||
"temperature",
|
||||
"current_temperature",
|
||||
|
@@ -34,6 +34,8 @@ export interface FanEntity extends HassEntityBase {
|
||||
attributes: FanEntityAttributes;
|
||||
}
|
||||
|
||||
export type FanDirection = "forward" | "reverse";
|
||||
|
||||
export type FanSpeed = "off" | "low" | "medium" | "high" | "on";
|
||||
|
||||
export const FAN_SPEEDS: Partial<Record<number, FanSpeed[]>> = {
|
||||
|
@@ -44,6 +44,14 @@ export interface DatadiskList {
|
||||
disks: Datadisk[];
|
||||
}
|
||||
|
||||
export interface HostDisksUsage {
|
||||
total_bytes?: number;
|
||||
used_bytes: number;
|
||||
id: string;
|
||||
label: string;
|
||||
children?: HostDisksUsage[];
|
||||
}
|
||||
|
||||
export const fetchHassioHostInfo = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<HassioHostInfo> => {
|
||||
@@ -180,3 +188,21 @@ export const listDatadisks = async (
|
||||
await hass.callApi<HassioResponse<DatadiskList>>("GET", "/os/datadisk/list")
|
||||
);
|
||||
};
|
||||
|
||||
export const fetchHostDisksUsage = async (hass: HomeAssistant) => {
|
||||
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
|
||||
return hass.callWS<HostDisksUsage>({
|
||||
type: "supervisor/api",
|
||||
endpoint: "/host/disks/default/usage",
|
||||
method: "get",
|
||||
timeout: 3600, // seconds. This can take a while
|
||||
});
|
||||
}
|
||||
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<HostDisksUsage>>(
|
||||
"GET",
|
||||
"hassio/host/disks/default/usage"
|
||||
)
|
||||
);
|
||||
};
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export interface ActionHandlerOptions {
|
||||
hasTap?: boolean;
|
||||
hasHold?: boolean;
|
||||
hasDoubleClick?: boolean;
|
||||
disabled?: boolean;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
function urlBase64ToUint8Array(base64String) {
|
||||
function urlBase64ToUint8Array(base64String: string) {
|
||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
||||
|
||||
|
@@ -1,7 +1,10 @@
|
||||
import type {
|
||||
HassEntity,
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface BasePerson {
|
||||
@@ -67,3 +70,28 @@ export const deletePerson = (hass: HomeAssistant, personId: string) =>
|
||||
type: "person/delete",
|
||||
person_id: personId,
|
||||
});
|
||||
|
||||
const cachedUserPerson: Record<string, string> = {};
|
||||
|
||||
export const getUserPerson = (hass: HomeAssistant): undefined | HassEntity => {
|
||||
if (!hass.user?.id) {
|
||||
return undefined;
|
||||
}
|
||||
const cachedPersonEntityId = cachedUserPerson[hass.user.id];
|
||||
if (cachedPersonEntityId) {
|
||||
const stateObj = hass.states[cachedPersonEntityId];
|
||||
if (stateObj && stateObj.attributes.user_id === hass.user.id) {
|
||||
return stateObj;
|
||||
}
|
||||
}
|
||||
|
||||
const result = Object.values(hass.states).find(
|
||||
(state) =>
|
||||
state.attributes.user_id === hass.user!.id &&
|
||||
computeStateDomain(state) === "person"
|
||||
);
|
||||
if (result) {
|
||||
cachedUserPerson[hass.user.id] = result.entity_id;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
@@ -11,8 +11,6 @@ import {
|
||||
union,
|
||||
array,
|
||||
assign,
|
||||
literal,
|
||||
is,
|
||||
boolean,
|
||||
refine,
|
||||
} from "superstruct";
|
||||
@@ -68,17 +66,6 @@ export const serviceActionStruct: Describe<ServiceActionWithTemplate> = assign(
|
||||
})
|
||||
);
|
||||
|
||||
const playMediaActionStruct: Describe<PlayMediaAction> = assign(
|
||||
baseActionStruct,
|
||||
object({
|
||||
action: literal("media_player.play_media"),
|
||||
target: optional(object({ entity_id: optional(string()) })),
|
||||
entity_id: optional(string()),
|
||||
data: object({ media_content_id: string(), media_content_type: string() }),
|
||||
metadata: object(),
|
||||
})
|
||||
);
|
||||
|
||||
export interface ScriptEntity extends HassEntityBase {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
last_triggered: string;
|
||||
@@ -182,14 +169,6 @@ export interface WaitForTriggerAction extends BaseAction {
|
||||
continue_on_timeout?: boolean;
|
||||
}
|
||||
|
||||
export interface PlayMediaAction extends BaseAction {
|
||||
action: "media_player.play_media";
|
||||
target?: { entity_id?: string };
|
||||
entity_id?: string;
|
||||
data: { media_content_id: string; media_content_type: string };
|
||||
metadata: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface RepeatAction extends BaseAction {
|
||||
repeat: CountRepeat | WhileRepeat | UntilRepeat | ForEachRepeat;
|
||||
}
|
||||
@@ -266,7 +245,6 @@ export type NonConditionAction =
|
||||
| ChooseAction
|
||||
| IfAction
|
||||
| VariablesAction
|
||||
| PlayMediaAction
|
||||
| StopAction
|
||||
| SequenceAction
|
||||
| ParallelAction
|
||||
@@ -291,7 +269,6 @@ export interface ActionTypes {
|
||||
wait_for_trigger: WaitForTriggerAction;
|
||||
variables: VariablesAction;
|
||||
service: ServiceAction;
|
||||
play_media: PlayMediaAction;
|
||||
stop: StopAction;
|
||||
sequence: SequenceAction;
|
||||
parallel: ParallelAction;
|
||||
@@ -398,11 +375,6 @@ export const getActionType = (action: Action): ActionType => {
|
||||
return "set_conversation_response";
|
||||
}
|
||||
if ("action" in action || "service" in action) {
|
||||
if ("metadata" in action) {
|
||||
if (is(action, playMediaActionStruct)) {
|
||||
return "play_media";
|
||||
}
|
||||
}
|
||||
return "service";
|
||||
}
|
||||
return "unknown";
|
||||
@@ -443,6 +415,31 @@ export const migrateAutomationAction = (
|
||||
delete action.scene;
|
||||
}
|
||||
|
||||
// legacy play media
|
||||
if (
|
||||
typeof action === "object" &&
|
||||
action !== null &&
|
||||
"action" in action &&
|
||||
action.action === "media_player.play_media" &&
|
||||
"data" in action &&
|
||||
((action.data as any)?.media_content_id ||
|
||||
(action.data as any)?.media_content_type)
|
||||
) {
|
||||
const oldData = { ...(action.data as any) };
|
||||
const media = {
|
||||
media_content_id: oldData.media_content_id,
|
||||
media_content_type: oldData.media_content_type,
|
||||
metadata: { ...(action.metadata || {}) },
|
||||
};
|
||||
delete action.metadata;
|
||||
delete oldData.media_content_id;
|
||||
delete oldData.media_content_type;
|
||||
action.data = {
|
||||
...oldData,
|
||||
media,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof action === "object" && action !== null && "sequence" in action) {
|
||||
for (const sequenceAction of (action as SequenceAction).sequence) {
|
||||
migrateAutomationAction(sequenceAction);
|
||||
|
@@ -26,7 +26,6 @@ import type {
|
||||
EventAction,
|
||||
IfAction,
|
||||
ParallelAction,
|
||||
PlayMediaAction,
|
||||
RepeatAction,
|
||||
SequenceAction,
|
||||
SetConversationResponseAction,
|
||||
@@ -303,27 +302,6 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
});
|
||||
}
|
||||
|
||||
if (actionType === "play_media") {
|
||||
const config = action as PlayMediaAction;
|
||||
const entityId = config.target?.entity_id || config.entity_id;
|
||||
const mediaStateObj = entityId ? hass.states[entityId] : undefined;
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.play_media.description.full`,
|
||||
{
|
||||
hasMedia:
|
||||
config.metadata.title || config.data.media_content_id
|
||||
? "true"
|
||||
: "false",
|
||||
media:
|
||||
(config.metadata.title as string | undefined) ||
|
||||
config.data.media_content_id,
|
||||
hasMediaPlayer:
|
||||
mediaStateObj || entityId !== undefined ? "true" : "false",
|
||||
mediaPlayer: mediaStateObj ? computeStateName(mediaStateObj) : entityId,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (actionType === "wait_for_trigger") {
|
||||
const config = action as WaitForTriggerAction;
|
||||
const triggers = ensureArray(config.wait_for_trigger);
|
||||
|
@@ -74,7 +74,9 @@ export type Selector =
|
||||
| BackupLocationSelector;
|
||||
|
||||
export interface ActionSelector {
|
||||
action: {} | null;
|
||||
action: {
|
||||
optionsInSidebar?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface AddonSelector {
|
||||
@@ -130,7 +132,9 @@ export interface ColorTempSelector {
|
||||
}
|
||||
|
||||
export interface ConditionSelector {
|
||||
condition: {} | null;
|
||||
condition: {
|
||||
optionsInSidebar?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface ConversationAgentSelector {
|
||||
@@ -319,6 +323,7 @@ export interface MediaSelectorValue {
|
||||
media_class?: string;
|
||||
children_media_class?: string | null;
|
||||
navigateIds?: { media_content_type: string; media_content_id: string }[];
|
||||
browse_entity_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -397,6 +402,7 @@ export interface StateSelector {
|
||||
entity_id?: string | string[];
|
||||
attribute?: string;
|
||||
hide_states?: string[];
|
||||
multiple?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
1
src/data/selector/selector_selector.ts
Normal file
1
src/data/selector/selector_selector.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const SELECTOR_SELECTOR_BUILDING_BLOCKS = ["condition", "action"];
|
@@ -4,7 +4,7 @@ import type {
|
||||
HassEntityBase,
|
||||
HassEvent,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { BINARY_STATE_ON } from "../common/const";
|
||||
import { BINARY_STATE_ON, BINARY_STATE_OFF } from "../common/const";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
@@ -52,6 +52,15 @@ export const updateCanInstall = (
|
||||
(showSkipped && Boolean(entity.attributes.skipped_version))) &&
|
||||
supportsFeature(entity, UpdateEntityFeature.INSTALL);
|
||||
|
||||
export const latestVersionIsSkipped = (entity: UpdateEntity): boolean =>
|
||||
!!(
|
||||
entity.attributes.latest_version &&
|
||||
entity.attributes.skipped_version === entity.attributes.latest_version
|
||||
);
|
||||
|
||||
export const updateButtonIsDisabled = (entity: UpdateEntity): boolean =>
|
||||
entity.state === BINARY_STATE_OFF && !latestVersionIsSkipped(entity);
|
||||
|
||||
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
|
||||
!!entity.attributes.in_progress;
|
||||
|
||||
|
@@ -12,6 +12,7 @@ export interface Zone {
|
||||
}
|
||||
|
||||
export interface HomeZoneMutableParams {
|
||||
name?: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
radius: number;
|
||||
|
106
src/dialogs/dialog-list-items/dialog-list-items.ts
Normal file
106
src/dialogs/dialog-list-items/dialog-list-items.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-icon";
|
||||
import "../../components/ha-md-list";
|
||||
import "../../components/ha-md-list-item";
|
||||
import "../../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HassDialog } from "../make-dialog-manager";
|
||||
import type { ListItemsDialogParams } from "./show-list-items-dialog";
|
||||
|
||||
@customElement("dialog-list-items")
|
||||
export class ListItemsDialog
|
||||
extends LitElement
|
||||
implements HassDialog<ListItemsDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _params?: ListItemsDialogParams;
|
||||
|
||||
public async showDialog(params: ListItemsDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _itemClicked(ev: CustomEvent): void {
|
||||
const item = (ev.currentTarget as any).item;
|
||||
if (!item) return;
|
||||
item.action();
|
||||
this._dialogClosed();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params || !this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
.heading=${createCloseHeading(this.hass, this._params.title ?? " ")}
|
||||
@closed=${this._dialogClosed}
|
||||
hideActions
|
||||
>
|
||||
<div class="container">
|
||||
<ha-md-list>
|
||||
${this._params.items.map(
|
||||
(item) => html`
|
||||
<ha-md-list-item
|
||||
type="button"
|
||||
@click=${this._itemClicked}
|
||||
.item=${item}
|
||||
>
|
||||
${item.iconPath
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${item.iconPath}
|
||||
slot="start"
|
||||
class="item-icon"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: item.icon
|
||||
? html`
|
||||
<ha-icon
|
||||
icon=${item.icon}
|
||||
slot="start"
|
||||
class="item-icon"
|
||||
></ha-icon>
|
||||
`
|
||||
: nothing}
|
||||
<span class="headline">${item.label}</span>
|
||||
${item.description
|
||||
? html`
|
||||
<span class="supporting-text">${item.description}</span>
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-md-list>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-dialog {
|
||||
/* Place above other dialogs */
|
||||
--dialog-z-index: 104;
|
||||
--dialog-content-padding: 0;
|
||||
--md-list-item-leading-space: 24px;
|
||||
--md-list-item-trailing-space: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-list-items": ListItemsDialog;
|
||||
}
|
||||
}
|
24
src/dialogs/dialog-list-items/show-list-items-dialog.ts
Normal file
24
src/dialogs/dialog-list-items/show-list-items-dialog.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
interface ListItem {
|
||||
icon?: string;
|
||||
iconPath?: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
action: () => any;
|
||||
}
|
||||
|
||||
export interface ListItemsDialogParams {
|
||||
title?: string;
|
||||
items: ListItem[];
|
||||
}
|
||||
|
||||
export const showListItemsDialog = (
|
||||
element: HTMLElement,
|
||||
params: ListItemsDialogParams
|
||||
) =>
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-list-items",
|
||||
dialogImport: () => import("./dialog-list-items"),
|
||||
dialogParams: params,
|
||||
});
|
@@ -3,18 +3,19 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog-header";
|
||||
import "../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../components/ha-md-dialog";
|
||||
import "../../components/ha-dialog-header";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../components/ha-textfield";
|
||||
import { KeyboardShortcutMixin } from "../../mixins/keyboard-shortcut-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { DialogBoxParams } from "./show-dialog-box";
|
||||
|
||||
@customElement("dialog-box")
|
||||
class DialogBox extends LitElement {
|
||||
class DialogBox extends KeyboardShortcutMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: DialogBoxParams;
|
||||
@@ -52,7 +53,7 @@ class DialogBox extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const confirmPrompt = this._params.confirmation || this._params.prompt;
|
||||
const confirmPrompt = this._params.confirmation || !!this._params.prompt;
|
||||
|
||||
const dialogTitle =
|
||||
this._params.title ||
|
||||
@@ -62,7 +63,8 @@ class DialogBox extends LitElement {
|
||||
return html`
|
||||
<ha-md-dialog
|
||||
open
|
||||
.disableCancelAction=${confirmPrompt || false}
|
||||
.disableCancelAction=${!this._params.enableCancelAction &&
|
||||
confirmPrompt}
|
||||
@closed=${this._dialogClosed}
|
||||
type="alert"
|
||||
aria-labelledby="dialog-box-title"
|
||||
@@ -100,19 +102,20 @@ class DialogBox extends LitElement {
|
||||
: ""}
|
||||
</div>
|
||||
<div slot="actions">
|
||||
${confirmPrompt &&
|
||||
html`
|
||||
<ha-button
|
||||
@click=${this._dismiss}
|
||||
?dialogInitialFocus=${!this._params.prompt &&
|
||||
this._params.destructive}
|
||||
appearance="plain"
|
||||
>
|
||||
${this._params.dismissText
|
||||
? this._params.dismissText
|
||||
: this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
`}
|
||||
${confirmPrompt
|
||||
? html`
|
||||
<ha-button
|
||||
@click=${this._dismiss}
|
||||
?dialogInitialFocus=${!this._params.prompt &&
|
||||
this._params.destructive}
|
||||
appearance="plain"
|
||||
>
|
||||
${this._params.dismissText
|
||||
? this._params.dismissText
|
||||
: this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
<ha-button
|
||||
@click=${this._confirm}
|
||||
?dialogInitialFocus=${!this._params.prompt &&
|
||||
@@ -167,6 +170,12 @@ class DialogBox extends LitElement {
|
||||
this._closeResolve = undefined;
|
||||
}
|
||||
|
||||
protected supportedSingleKeyShortcuts(): SupportedShortcuts {
|
||||
return {
|
||||
Enter: () => this._confirm(),
|
||||
};
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host([inert]) {
|
||||
pointer-events: initial !important;
|
||||
|
@@ -17,6 +17,7 @@ export interface ConfirmationDialogParams extends BaseDialogBoxParams {
|
||||
confirm?: () => void;
|
||||
cancel?: () => void;
|
||||
destructive?: boolean;
|
||||
enableCancelAction?: boolean;
|
||||
}
|
||||
|
||||
export interface PromptDialogParams extends BaseDialogBoxParams {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user